嵌入式必背八股文,用心整理总结(四万字)
这是我个人的学习中,整理与积累的一些知识体系。分了具体的类别,其中有一些错误,大家请见谅,也欢迎给我留言,一起学习交流
目录
3.RTOS多线程,线程数多,如果内存不够,应该怎么办?如果是在设计时就减少线程,那么减少的任务应该怎么处理?
3.嵌入式程序中代码,变量,常量是如何在RAM和FLASH中进行分配的
·11.STM32F103C8T6有几个外设总线?如APB,AHB1,AHB
17.头文件中 ifndef/define/endif 的作用
18.C语言标准的库函数,比如memory set和memory copy,会用到吗?(★★★)
1.两个设备通信交互,一个是速率通信很高,另有一个很低,如何实现
3.需要您用RT-Thread设计一个在STM32上跑的应用。要求把网络通信和业务处理分开,分别由不同的Thread来完成,那么,这个设计的核心关键思路是什么?
5.如果现在有个单片机连接了一个传感器,这个传感器不定时的会给单片机传数据,你怎么判断数据的一帧是否结束,这个帧没有具体的格式?2
3.软件 I2C 相比硬件 I2C 有哪些限制或需要额外考虑的因素?
1.硬件设计过程中经常遇见IO引脚接上拉电阻或下拉电阻的情况,请给出自已的理解,并说明在嵌入式开发过程中的注意事项。
操作系统
1.进程与线程
- 什么是进程,什么是线程
进程(Process)是操作系统分配资源和调度的基本单位,它代表着计算机中已经启动并正在运行的程序。每个进程都有自己独立的地址空间,拥有独立的内存和系统资源。当一个程序启动时,它就变成了一个进程。进程是动态的,有生命周期,它会被创建、执行并最终终止。在多任务操作系统中,如Windows或Linux,进程允许多个任务同时运行,每个任务就是一个进程1。
线程(Thread)是进程中的一个独立的控制流程,是CPU调度和执行的最小单位。线程被包含在进程之中,可以与同一个进程中的其他线程共享进程资源。线程有自己的堆栈和局部变量,但没有独立的地址空间。一个线程的失败通常会导致整个进程的失败,因此多线程程序比多进程程序更容易出错,但线程之间的切换效率比进程间高1。
进程与线程的主要区别包括:
地址空间和资源:进程有独立的地址空间,而线程共享其所属进程的地址空间和资源。
通信方式:进程间通信(IPC)需要特定的机制,如管道、信号量或共享内存,因为不同进程有不同的内存空间。线程间通信更简单,因为它们自然共享进程资源,如全局变量等。
系统开销:由于进程有独立的地址空间,创建和管理进程的开销比线程大。线程的创建、结束和切换的开销相对较小,因为它们共享进程的资源2。
为什么使用多线程?多线程可以提高应用程序的响应性,尤其是在执行多个任务或需要并发操作时。例如,一个文本编辑器可以在一个线程中处理用户输入,而在另一个线程中进行拼写检查。多线程也可以更有效地利用多核处理器的能力,因为线程可以在不同的处理器核心上并行运行。
总的来说,进程是资源分配的最小单位,而线程是程序执行的最小单位。在设计程序时,选择使用多进程还是多线程取决于应用程序的需求、资源管理和预期的性能。
- 进程与线程的通信方式分别是什么
- 进程与线程的定义;进程---操作系统中每一个应用程序的运行都会申请一片独立的内存空间,一个进程可以有多个线程;线程--进程中一个执行任务(基本控制单元),负责当前进程中程序的执行,一个进程至少有一个线程;根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位(共享一个进程的地址空间)
具体通信方式(线程)
{
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
2.信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
3.信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。}
2.描述一下中断处理过程
中断分为两种,内部中断与外部中断
内部中断直接响应,外部中断则是中断源发出中断请求,cpu接受到中断请求,保存当前任务信息到栈中,然后cpu会跳转到中断服务程序入口,根据中断服务程序的中断号来确认到底需要处理哪一个中断服务函数,处理完之后恢复之前保存的cpu状态
3.RTOS多线程,线程数多,如果内存不够,应该怎么办?如果是在设计时就减少线程,那么减少的任务应该怎么处理?
答:
如果是因为线程数多导致内存不够用,可以看看是不是线程的栈内存设置太大导致浪费了太多内存,可以适当减小栈的大小。如果不需要那么多线程,就合并线程,在设计的时候就减少线程,减少的任务可以选择放到一个线程里面去,主要保证功能的实现。如果线程设计合理就是要这么多,栈大小也合理,那就是资源不够用,考虑换内存更大的MCU型号。
如何考虑一个线程需用多大的任务栈
- 从哪几个方面考虑
--局部变量,函数调用,上下文切换
函数调用方面,一个任务函数要将返回地址,参数,寄存器的值压入栈,并且还要考虑函数调用深度(4-32字节,要考虑函数调用层数)-(可见开销)
--上细下文切换和中断方面,,每次切换任务保存的寄存器组按照128字节预算,还有中断或者中断嵌套的开销(隐藏开销)
初始栈大小= 可见消耗加冗余消耗加隐藏消耗
控制方法,用rtt的工具-其智慧栈使用检测,观察最大栈消耗。Rtt还有栈溢出检测的机制,也可以做实时的检验
4.介绍互斥锁 信号量的作用
互斥锁与信号量的主要区别:
作用范围:
互斥锁:用于保证在同一时刻,只有一个线程可以访问共享资源,通常用于保护临界区。
信号量:用于控制多个线程对有限数量资源的访问。可以有多个资源的访问权限,而不仅仅是一个资源。
使用方式:
互斥锁:只能用于保护一个临界区,通常是一个共享资源的访问,且锁必须由同一线程解锁。
信号量:通常具有一个计数器,可以允许多个线程同时访问资源。例如,控制最多 3 个线程访问共享资源。可以由其他线程解锁
问题00如何锁起来共享资源区域
信号量:可以由任何线程释放信号量(即任何线程都可以进行 signal 操作)
5.Freertos中调度器的具体执行措施
基于时间片轮转和抢占式调度算法去执行调度,本质就是选取就绪列表中任务优先级最高的任务去执行
高优先级的任务会先执行
同优先的任务基于时间片轮转算法,分别执行一定的时钟节拍。
调度器在任务切换中决定何时切换:也就是关注调度器的触发时机,
两种触发类型:
1.主动触发(调用阻塞api,或者任务主动让出cpu(rt_thread_yield)等操作)-包含操作触发调度器的函数
2.被动触发(时间片耗尽,高优先级的任务就绪--中断(中断上下文中,不会直接调用 rt_schedule()(避免中断嵌套导致的问题),而是通过设置调度请求标志,在中断退出时(返回线程上下文前)判断是否需要调度)或者函数(信号量释放-事件触发))
时间片耗尽执行:嘀嗒时钟中断函数中调用实现节拍计时,同时在该函数中会检测当前任务的时钟片是否用完,且如果就绪链表不只有一个任务,触发调度器介入。
发生阻塞:比如等待信号量函数执行,检测其标志位,如果没有等待到该事件就触发调度器实现任务切换(线程被挂起,但不影响只是调度器不会将该任务不插入就绪队列)
更高优先级任务就绪:比如信号量的释放,导致阻塞链表的任务被释放,然后触发调度器。
调度器执行总结:
- 关中断
- 首先会检查调度器是否被锁定
- 检查位图是否有就绪任务,并获取最高优先级的任务(通过.next访问实现)
- 检查当前任务状态
--如果处于运行态(先验条件的检查)
情况1.如果当前任务优先级高于最高就绪任务优先级--》不切换
情况2.优先级相同当前任务未主动让出cpu--》不切换
情况3.当前任务优先级低或者主动让出cpu--》切换(通过状态判断如上所示),boll变量标记to thread
- 然后判断当前任务与目标任务是否是同一个,如果不同更新系统当前任务最高优先级,记录当前任务-旧任务,更新当前任务为新任务。然后判断标志need_insert_from_thread,代表是否要将老任务插入就绪链表,如果要的话就调用api实现任务插入(在内会插入头还是尾取决于是否是调用api任务主动让出的cpu)。然后将新任务从就绪链表中移除,并设置其状态为运行态
6.进行一些栈检查和调试的代码,后续则是传入新老任务的栈指针和结构体的地址实现上下文的切换
不同点:触发机制与调度时机
三种场景的核心差异在于 “为什么切换” 和 “调度器如何被触发介入”:
|
场景 |
触发原因 |
调度器介入时机 |
任务状态转换逻辑 |
是否依赖中断 |
|
时间片耗尽 |
同优先级任务的时间片用完 |
时钟中断(定期触发,如 10ms 一次) |
当前任务从 “运行”→“就绪”(仍可运行) |
是(时钟中断) |
|
更高优先级任务就绪 |
高优先级任务从阻塞 / 挂起→就绪 |
事件触发(如 I/O 完成、信号量释放)后 |
当前任务从 “运行”→“就绪”(被抢占) |
通常是(事件中断) |
|
任务主动阻塞 |
任务主动调用阻塞接口(如 sleep、wait) |
任务主动调用调度相关函数 |
当前任务从 “运行”→“阻塞”(等待资源) |
否(主动触发) |
当任务的到达运行时限或者进入阻塞,到达时限的话就把这个任务转移到就绪链表的尾部,如果进入阻塞的话就挂起或者将就执行上下文切换的操作,当前任务的cpu的执行信息会被保存到栈上,然后将在一个任务的信息上传到cpu上执行,直到咋次轮到该任务执行,再把其信息从栈上再恢复到cpu的寄存器中,修改pc指针,跳转到该任务执行
- R free和rtt的比较
1.理念差别
Rtt以实时为核心,他提供了轻量级的线程模型和可裁剪的内核
Freertos设计理念是简单性和可移植性,提供了轻量级的内核,只有几个核心的调度器和同步机制,适应多种不要同平台
2.特点比较
内核结构
Rtt的内核相对复杂,具有较多的特性和功能,例如msh命令行,env裁剪工具整个内核体积较大,具备更强大实时性和灵活性
Free的内核相对轻量级,仅包含基本的调度器和同步机制,减少了内存消耗。但复杂场景下可能要使用外部库来实现更多功能
复杂的具体体现
RT-Thread 的 “复杂” 本质是 **“从‘轻量内核’到‘完整实时操作系统’的定位差异”**——FreeRTOS 追求 “最小化实时核心”,而 RT-Thread 追求 “嵌入式场景的一站式操作系统”,这种定位差异导致其在架构、功能、交互上必然更复杂,具体体现在以下 5 个维度:
RT-Thread 的复杂主要源于定位差异:
- 架构上,它是分层操作系统(内核 + 组件 + 应用层),而非 FreeRTOS 的扁平内核;
- 功能上,内置设备驱动、FinSH、网络栈等深度集成组件,FreeRTOS 仅核心功能 + 外接库;
- 调度与内存管理更灵活(多策略适配),FreeRTOS 逻辑极简;
- 配置需处理组件依赖,FreeRTOS 单文件宏定义即可。
本质是:RT-Thread 是 “嵌入式操作系统”,FreeRTOS 是 “实时内核工具包”,前者为全场景覆盖牺牲了简洁性。
内存管理
Rtt提供了较为完善的内存管理机制,包括了动态内存分配和垃圾回收等功能,他提供了内存池,内存算法,控制块等组件能够灵活的分配和管理内存
Free内存管理比较简单主要使用五种分配方案,照显示情况选择使用
设备驱动
rtt的设备驱动框架比较完整,对外提供了一套统一的设备操作接口也提供了一些常见的设备驱动例如i2c,spi
Free对设备驱动的支持较弱,用户需要自行开发或者使用第三方库来实现设备驱动
3.性能比较
实时性能:rtt在实时性能较高,调度时间精确可控,系统响应速度比较快,适合处理实时任务,free则稍逊一筹
实时性更高因为--调度器更精细(支持更多优先级级别),任务切换效率高,中断响应机制更高效
系统资源占用
rtt虽然内核占
用较大,但是可裁剪性好,根据系统需求选择只加载所需的模块。Free占用小,功能少
- 动态创建线程和静态创建线程的区别0
|
维度 |
动态创建线程 |
静态创建线程 |
|
内存分配 |
从堆(heap)中动态分配内存 |
使用预先定义的静态内存(全局 / 静态变量) |
|
函数接口 |
rt_thread_create() |
rt_thread_init() |
|
销毁方式 |
rt_thread_delete()(释放内存) |
rt_thread_detach()(不释放内存) |
|
适用场景 |
内存充足、线程需要动态创建 / 销毁 |
内存受限、对确定性要求高的场景 |
|
确定性 |
较低(动态内存分配可能有延迟) |
较高(内存预先分配,无动态分配开销) |
动态创建线程
// 动态创建线程(系统动态分配资源)rt_thread_t tid = rt_thread_create("dynamic", thread_entry, NULL, 1024, 25, 10);rt_thread_startup(tid); // 启动线程
静态创建线程
// 静态创建线程(用户预先分配资源)
struct rt_thread static_thread;
static rt_uint8_t stack[1024];
void thread_entry(void *arg) { /* 线程逻辑 */ }
// 初始化静态线程rt_thread_init(&static_thread, "static", thread_entry, NULL, stack, sizeof(stack), 25, 10);rt_thread_startup(&static_thread); // 启动线程
Tcb控制块是什么
TCB 如同线程的 “身份证”,主要承担以下功能:
线程状态管理:记录线程当前状态(如就绪、运行、阻塞),供调度器决定是否切换线程。
上下文保存:存储线程的 CPU 寄存器值(如程序计数器 PC、通用寄存器),确保线程被中断后能恢复执行。
资源分配依据:包含线程的栈指针、优先级、任务入口地址等,是系统分配 CPU 时间和内存资源的关键依据。
调度决策支持:通过优先级、等待事件等信息,帮助调度器确定线程执行顺序。
8.FreeRTOS中的任务同步方式
队列:还可进行数据的传递。
信号量:在FreeRTOS中会分成两种:二进制信号量(实现共享资源的访问),计数型信号量(实现生产者和消费者模型)
互斥量:实现共享资源的访问。
事件组:可以等待多个任务的通知。
任务通知:一种轻量级的任务同步方式,TCB。任务通知不需要创建就可以使用。(用于一对一通信)
9.介绍下信号量和互斥量,说一些其区别和使用场景
信号量和互斥量都是用于解决线程间的同步和互斥。信号量有计数型信号量或者二值信号量,信号量值可以大于1。而互斥量的计数值只有0和1,此外互斥量因为有优先级继承机制,因此可以解决优先级反转的问题。
信号量主要用于多线程间的同步,互斥量主要用于对互斥资源的保护,以及解决优先级反转的问题。
互斥量使用场景:共享数据修改,硬件设备的访问,短临界区保护
信号量:读写完成标志,控制并发数量,小车到达后另一个事情方可启动
10.什么是死锁?产生的条件有哪些?
死锁是指多个进程/线程因争夺资源而相互等待,导致程序无法继续执行。
四个必要条件:
互斥请求不可等待
互斥
请求与保持
不可抢占
循环等待
- 实时操作系统和中断的区别(实时响应方面)
中断是硬件级的事件响应机制,解决 “如何快速处理紧急事件” 的问题;RTOS 是软件级的系统管理框架,解决 “如何协调多任务在时间约束下高效运行” 的问题。两者的核心区别可归纳为:执行模式,资源管理。中断适合处理高频、短时的事件触发,而 RTOS 擅长管理多任务的协作、资源分配和时间约束。
- rtt操作系统标准版和nano的区别
|
Nano 版本 |
标准版 |
|
|
代码体积 |
3KB~4KB(ROM) + 1.2KB~1.5KB(RAM) |
50KB~500KB(ROM) + 10KB~100KB(RAM) |
|
功能完整性 |
仅核心调度与基础通信 |
含设备框架、文件系统、网络协议等 |
|
扩展性 |
无动态组件加载,需手动裁剪 |
支持软件包动态添加,灵活配置 |
|
适用场景 |
资源受限的嵌入式控制 |
复杂物联网设备、工业自动化、智能终端 |
|
开发工具链 |
无需 Env 工具,直接编译 |
依赖 Env 进行组件配置与包管理 |
可以用scons --size 在env显示组件分类的大小
标准版
ROM(程序存储,即 Flash 占用)
最小内核配置(仅内核核心功能,不含任何组件):约 30KB ~ 60KB。
带基础组件(如设备驱动框架、内存管理、线程调度):约 60KB ~ 150KB。
带丰富组件(如文件系统、网络协议栈、GUI 等):可能达到 200KB ~ 500KB+,具体取决于启用的功能模块。
RAM(运行时内存,即 SRAM 占用)
内核核心运行需求(含 idle 线程栈、内核数据结构等):最小约 5KB ~ 10KB。
实际应用场景:需加上用户任务栈(每个任务栈默认通常为 512B ~ 4KB,可配置)、堆内存(默认一般 4KB ~ 32KB,可通过 RT_HEAP_SIZE 调整)、以及组件运行所需内存(如网络缓存、文件系统缓存等)。
因此,简单应用通常需要 10KB ~ 32KB RAM,复杂应用(多任务、带网络等)可能需要 32KB ~ 128KB+。
Nano
ROM(程序存储,Flash 占用)
最小内核配置(仅包含核心调度功能:线程管理、调度器、时钟节拍等,无任何扩展组件):仅需 2KB ~ 10KB。
(例如:在 STM32F103C8T6 上,仅编译内核核心代码,Flash 占用可低至 5KB 左右)。
带基础功能(如软件定时器、信号量、互斥锁等基础 IPC 机制):约 5KB ~ 20KB。
2. RAM(运行时内存,SRAM 占用)
内核核心运行需求(含 idle 线程栈、内核数据结构、调度器上下文等):最小仅需 1KB ~ 4KB。
idle 线程栈默认可配置为 256B ~ 1KB(可通过宏 RT_THREAD_IDLE_STACK_SIZE 调整);
内核数据结构(如线程控制块 TCB、就绪列表等)占用几百字节。
实际应用场景:加上用户任务栈(每个任务栈可小至 128B ~ 512B,根据任务复杂度配置),简单应用的总 RAM 占用通常可控制在 2KB ~ 8KB。
- 为什么rtos可实现微秒级的响应
- 抢占式的内核,保证了高优先的任务就绪及时响应
- 高效的上下文切换,上下文切换时间得到充分的优化
- 基于动态的高精度的时间片分配策略
二.单片机方面
- 单片机上电启动过程
1.单片机从main函数开始进行编程,但上电后却不是从main函数开始执行
2.Stm32有三种启动方式
从flash启动(非易失性),把flash地址映射到0x00000000,那么启动就相当于从flash地址启动(什么是映射--cpu的逻辑地址和硬件的物理地址的转换规则,当cpu发出一个逻辑地址时,系统会根据这一套规则。自动的将其对应转华为对应的物理地址,然后就去访问了实际的硬件,就是cpu不认识家门,他会从逻辑的000000000执行,但是系统把他领到了flash前面告诉他这就是000000,这就是映射的过程)
从sram(掉电丢失)启动,同样映射原理(一般调试使用)
从系统存储器启动,同样的映射原理,系统存储器里面存储的是stm32自带的bootloader代码,这是一个官方的iap,他提供了可以通过串口1将用户的代码下载到flash中的功能,下载完后再切换回从flash启动就可以了
3如果是从flash启动所呈现的流程
上电后硬件设置sp,pc
启动时钟
软件设置sp(栈指针)
加载.data,.bss,并初始化栈区
1.电源上电--电源模块开始工作,核心电压从0逐渐上升到稳定值
2.复位触发,电压稳定后。硬件电路触发复位信号,强制单片机所有寄存器回复原始状态
3.初始化sp,pc指向复位向量,复位信号撤销后单片机硬件逻辑自动的将pc指针初始化为固定的复位向量地址,
4.地址映射生效,将flash的地址映射到复位向量地址
执行复位向量指令,也就是调到了flash中的启动代码入口
- 然后启动代码执行(预设在flash中的内容--启动文件),,初始化数据段和.bss段还有栈区,软件设置sp(初始化sp后栈空间才真正可用)--初始化化静态区之后才初始化栈指针,完成时钟初始化。
- 然后就跳转到main函数的入口,按照用户编写的代码顺序执行指令
问题----映射过程是操控pc的过程吗
“Flash 地址映射到复位向量地址” 的本质
这个过程的核心是程序烧录(下载)到 Flash 时的初始化操作:
当程序被下载到单片机的 Flash 中时,编译器和烧录工具会确保一件事 —— 在硬件规定的 “复位向量地址” 处,写入程序实际起始地址(例如 main 函数的入口地址,或启动文件中初始化代码的地址)。
简单说,就是把 “程序真正要开始跑的 Flash 地址”,“登记” 到复位向量这个 “固定的硬件锚点” 上。这个过程是对 Flash 存储空间的写入操作,目的是为后续 PC 指针的工作提供 “目标地址”。
3. 这一过程与 “操控 PC 指针” 的关系
不是直接操控 PC 指针:
“映射” 过程(即烧录时向复位向量地址写入程序起始地址)是准备工作,此时单片机可能处于断电或未复位状态,PC 指针尚未开始工作(甚至可能处于未初始化的随机状态)。这个过程不涉及对 PC 寄存器的直接修改,只是在 Flash 中 “埋下” PC 后续要指向的目标地址。
是 PC 指针初始化的前提:
当单片机上电复位时,硬件会强制触发一个核心动作 ——将 PC 指针的值自动设置为复位向量地址(这是硬件固化的逻辑,无需软件干预)。随后,CPU 会从 PC 指向的复位向量地址读取数据(即之前 “映射” 好的程序起始地址),并将 PC 更新为这个起始地址,从此开始执行用户程序。
也就是说,“映射” 过程为 PC 指针的第一次跳转提供了 “目的地”,而 PC 指针的实际 “指向” 是在复位瞬间由硬件自动完成的。
- sp与pc指针的学习
- pc(程序计数器),pc始终保存下一条要执行的指令地址,cpu完成当前指令后,自动从pc获得下一条指令的地址,然后更新pc指针,程序的执行本质都是通过修改pc指针实现的
- 栈指针sp指向当前栈顶元素的地址,控制栈的生长方向,通常压栈sp减小,出栈sp增大,启动代码如果没有正确设置sp,函数调用时压栈操作(返回地址会先压栈)会写入未知地址,导致崩溃
注意事项:野指针可能修改pc的值,导致跳转到随机地址执行
断点观察sp值的变化确认栈操作是否正常
2.请就RAM、FLASH的异同来表述下自已观点。
RAM的作用:1.RAM主要用于运行时的临时存储。2.RAM的高速度对于实时性要求高的嵌入式系统至关重要。3.RAM是易失性存储器。
flash的作用:1.Flash用于长期存储系统固件、配置文件以及应用程序。2.Flash存储器的非易失性特性。3.Flash存储的速度比RAM慢,特别是写入速度,但其大容量和非易失性弥补了这一点。
RAM和Flash通常是协同工作的:1.系统启动时,设备从Flash中读取固件或操作系统,并将其加载到RAM中运行。这种机制利用了Flash的非易失性特点来存储数据,确保设备每次启动都能从永久存储中获取必要的系统文件。2.在系统运行时,所有的临时数据处理和计算任务都发生在RAM中,以保证系统的响应速度和数据处理效率。这包括实时操作系统中的任务调度、输入输出操作、传感器数据处理等。3.对于一些嵌入式系统,还可以通过RAM缓存来提高Flash的访问速度。例如,系统将经常使用的数据从Flash读取并存储在RAM中,减少直接从Flash读取的频率,提高系统的性能。
3.嵌入式程序中代码,变量,常量是如何在RAM和FLASH中进行分配的
在嵌入式系统中,代码、变量、常量的分配遵循以下基本原则:
代码和常量:存储在Flash中,因为它们在运行过程中不需要修改,且Flash是非易失性的,适合保存程序和不变的数据。
初始化的全局变量和静态变量会保存在flash中(因为要保证初值不变。而局部变量随函数运行后续就销毁了),未初始化的和初始化为零的bss段在ram
局部变量和栈、堆:完全存储在RAM中,因为它们与程序运行的动态行为相关,必须支持快速读写。
4.32和51单片机以及国民技术的芯片在开发中有什么区别?
1.性能:
32单片机:基于ARM Cortex-M系列,通常具有较高的处理速度、更大的内存和更丰富的外设接口。
51单片机:8051架构的单片机,处理速度和内存相对较低,外设接口也较少。
国民技术芯片:性能取决于具体型号,一些型号基于ARM Cortex-M系列,具有较高的性能;其他型号可能采用不同的架构。
2.生态系统和支持:
32单片机:拥有广泛的生态系统,包括大量的第三方库、开发板、教程和社区支持。
51单片机:虽然生态系统较老,但仍然有一些社区支持,包括一些第三方库和开发板。
国民技术芯片:根据具体型号,生态系统和支持可能有所不同。一些基于ARM Cortex-M的型号可能拥有较好的支持。、
- 应用场景:
32单片机:适用于复杂的嵌入式系统,如智能家居、工业自动化、物联网设备等。
51单片机:适用于简单的嵌入式系统,如小型控制单元、传感器节点等。
国民技术芯片:根据具体型号,适用于从简单的到复杂的各种嵌入式应用。
5.给我一个完全陌生的单片机如何去开发?
1.了解单片机特性:
获取单片机的数据手册,了解其基本特性,如CPU类型、工作电压、工作温度、内存大小、外设接口、功耗等。
检查数据手册中的引脚定义、封装类型和封装尺寸。
了解单片机的时钟系统,包括时钟源、时钟频率和时钟管理。
2.选择开发工具和环境:
根据单片机的特性选择合适的开发板。
选择合适的编程器和调试器,如ST-Link、J-Link等。
选择合适的集成开发环境(IDE),如Keil、IAR Embedded Workbench、MPLAB X IDE等。
3硬件软件测试。
- 获取核心的开发资料--数据手册,参考手册,开发文档,官方例程和用户手册,了解单片机的一些的基本特性,比如外设接口,内存大小,工作电压等
- 搭建硬件环境--如果没有开发板,我会搭建设计一个最小系统确保单片机可以正常的工作(电源,复位电路,时钟电路,下载接口)
- 搭建软件的开发环境--调试接口,选择开发工具,选择芯片型号
- 从最小功能验证开始,逐步熟悉外设功能
- 开发中常见问题--烧录失败,检查接线和芯片匹配
- 功能异常--示波器抓波形,单步调试排查逻辑错误
全局变量的 “静态” 与 “非静态” 本质差异是链接属性:非静态全局变量是 “外部链接”(全项目可见),静态全局变量是 “内部链接”(仅当前文件可见)。二者生命周期、存储位置完全相同(都是程序全程存在于静态存储区),但作用域和跨文件访问能力不同。
核心区别总结:
局部变量的 “静态” 与 “非静态” 核心差异在生命周期和存储位置:非静态局部变量是 “栈存储 + 短期生命周期”,静态局部变量是 “静态存储 + 全程生命周期”。二者作用域相同(仅函数内可见),但值的保留性和初始化次数完全不同。
|
变量类型 |
作用域范围 |
存储位置 |
生命周期 |
初始化次数 |
跨文件访问能力 |
典型用途 |
|
非静态全局变量 |
全项目(所有源文件) |
静态存储区 |
程序全程 |
1 次(程序启动时) |
可(需 extern) |
跨模块共享数据 |
|
静态全局变量 |
仅当前文件 |
静态存储区 |
程序全程 |
1 次(程序启动时) |
不可 |
文件内隐藏数据(避免冲突) |
|
非静态局部变量 |
仅函数 / 代码块内 |
栈 |
函数调用期间 |
每次进入作用域 |
无(局部作用域) |
临时数据存储 |
|
静态局部变量 |
仅函数 / 代码块内 |
静态存储区 |
程序全程 |
仅第一次进入时 |
无(局部作用域) |
保存函数调用状态(如计数器) |
- 静态变量的 “静态” 仅影响存储和生命周期,不影响可修改性:静态变量的值可以被修改(如static int a=0; a=5;是合法的),与常量(const)无关。
- 全局变量的命名冲突风险:非静态全局变量在多文件项目中若重名,会导致链接错误;静态全局变量仅在当前文件有效,可重名(不同文件互不影响)。
- 局部静态变量的线程安全问题:在多线程环境中,多个线程调用同一个含静态局部变量的函数时,可能因共享变量导致数据竞争(需加锁保护)。
- 内存效率:静态变量(无论全局还是局部)会长期占用内存,频繁使用可能导致 RAM 紧张(尤其嵌入式系统);非静态局部变量仅临时占用栈内存,更高效。
通过以上对比,可明确不同变量的特性,避免在编程中因混淆作用域或生命周期导致的 bug(如误将局部静态变量当普通局部局部静态变量当普通局部变量使用,或试图跨文件访问静态全局变量)。
7.ADC的转换过程、ADC转化的重要指标
ADC实际上就是将连续变量的模拟信号转换为离散变量的数字信号。
ADC的基本转换原理可以分为四个过程:
①抗混叠滤波,可以理解为一个低通滤波器
②采样保持电路
③量化
④编码
ADC转化的重要指标:
分辨率、采样率、噪声、功耗、精度等.
ADC的内部实现原理
ADC:模数转换器。它是一种电路模块,用于将模拟信号转换为数字信号。其内部实现原理主要分为采样和量化两个步骤。
在采样过程中,模拟信号会被周期性地取样,将其离散化为一系列采样值。采样的频率越高,所得到的采样值越接近原始模拟信号。
在量化过程中,采样值会被转换为数字信号。量化器会将每个采样值与一个离散的量化电平比较,并将其舍入到最接近的电平。量化电平的数量越多,所得到的数字信号越接近原始模拟信号。
8.简述dma的功能
DMA控制器可以独立地控制内存和外设之间的数据传输,不需要CPU的干预。
DMA控制器可以按照预设的传输模式,自动控制数据的传输。
DMA控制器可以实现多个外设之间的数据传输,并且可以优先处理多个外设的数据传输请求。
DMA控制器可以实现数据的直接存储和直接读取,从而节约了CPU的时间和计算资源。
DMA控制器可以有效地提高数据传输的效率和速度,使计算机系统的整体性能得到提升。
9.国产与进口芯片平台的优劣


10.单片机复位启动的流程

·11.STM32F103C8T6有几个外设总线?如APB,AHB1,AHB
1 个高性能总线(AHB - Advanced High-performance Bus)
2 个外设总线(APB - Advanced Peripheral Bus)
备注:数据手册的重要性!大家不要忽视,这才是我们学习的重点
12.什么是看门狗定时器
概念的简单理解:看门狗定时器是一个硬件计计时器,要求系统在规定时间内定期对其进行复位(喂狗),如果没有喂狗,定时器溢出后会触发预定的响应,例如系统复位或重启。
作用:防止系统长时间卡死。
提高系统可靠性。
系统卡死时,恢复程序运行。
12.GPIO工作模式
共有八种模式:
输出分为推挽输出,开漏输出,复用推挽输出,复用开漏输出。复用功能:GPIO引脚用于外设功能,如UART,SPI,PWM等。
输入分为上拉输入,下拉输入,浮空输入,模拟输入。
在gpio复用为外设功能的时候,必须使用复用推挽,而非普通的推挽
三大区别:
1.信号控制权从goio控制器到uart控制器
2.协议强制要求:外设功能的依赖性,比如说spi需要精准控制时序,所以必须交给spi控制器来执行
3.复用功能的硬件绑定
13.GPIO 的工作原理
是通过寄存器配置决定引脚功能(输入 / 输出),再通过数据寄存器实现与外部电路的电平信号交互
方向寄存器(Direction Register):决定引脚是输入还是输出模式(通过软件写入 0/1 配置)。
数据寄存器(Data Register):
输出模式时:写入 0/1 控制引脚输出低电平(0V)或高电平(如 3.3V/5V)。
输入模式时:读取 0/1 获取外部电路施加到引脚上的电平状态。
上拉 / 下拉电阻(Pull-up/Pull-down Resistor):输入模式下可选,防止引脚 “悬空”(无外部信号时电平不确定)。
上拉:默认将引脚拉到高电平(外部无信号时读为 1)。
下拉:默认将引脚拉到低电平(外部无信号时读为 0)。
14.stm32的特性参数
1.STM32F103C8T6
核心与性能
- 内核:ARM Cortex-M3
- 主频:最高 72MHz
存储资源
- Flash 容量:64KB(程序存储)
- RAM 容量:20KB(数据存储)
. 外设资源
- 定时器:4 个通用定时器(TIM2~TIM5)、2 个高级定时器(TIM1、TIM8),支持 PWM、输入捕获等
- 通信接口:
- 3 个 UART(支持 ISO 7816、LIN、IrDA)异步最大可达4.5mhz
- 2 个 SPI(支持 I2S 接口)最高为36mhz
- 2 个 I2C(支持 SMBus/PMBus)(基于模式分析100kb,400kb,高速可达3.4mhz)
- 1 个 USB 2.0 全速接口(支持 Device 模式)
- ADC:1 个 12 位 ADC,最多 16 个通道(支持单次 / 连续转换)
- DAC:无
- GPIO:37 个通用 I/O 口(可容忍 5V 电平)
2.STM32F412RX(STM32F4 系列)
核心与性能
- 内核:ARM Cortex-M4(带单精度 FPU,支持 DSP 指令)
- 主频:最高 100MHz
- Flash 容量:512KB(程序存储,支持 ECC 纠错)
- RAM 容量:128KB(数据存储,含 64KB DTCM 用于高速数据访问)
外设资源
- 定时器:2 个高级定时器(TIM1、TIM8)、10 个通用定时器(TIM2~TIM5、TIM9~TIM14),支持 PWM、死区控制等
- 通信接口:
- 4 个 UART(支持 LIN、IrDA、调制解调器模式),最大可达4.5mhz
- 3 个 SPI(支持 I2S,最高 45Mbps)最高50mhz
- 3 个 I2C(支持 SMBus/PMBus,高速模式 3.4Mbps),100,400,3.4mb
- 1 个 USB 2.0 全速接口(支持 OTG 功能)
- 1 个 SDIO 接口(支持 SD 卡、MMC)
- ADC:2 个 12 位 ADC(支持 16 位超采样),最多 16 个通道,采样率最高 2.4MSPS
- DAC:2 个 12 位 DAC(支持噪声波形生成)
- GPIO:50 个通用 I/O 口(部分支持 5V 容忍)
- 其他:硬件加密(AES、DES、SHA)、CRC 计算单元、2 个看门狗定时器
三.c语言知识
1.c语言程序运行时的内存分配
程序在内存中的分配情况有,栈(局部变量),堆,数据段(静态的局部变量和全局变量),代码段,bss段(初始化为零的数据段)
2.const关键字的作用
限定一个变量不可以被修改,同时要注意const int *p是指的是指针所指向的地址内的值不可变;int* const p代表指针所指向地址不可变。
3.static关键字的作用
修饰局部变量
static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)
修饰全局变量
全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问(即改变了作用域)。
修饰函数
static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。
Satic的作用是什么
4.野指针
面试题:什么是野指针?如何避免野指针?
答案解析:
野指针是指向无效内存地址的指针
产生原因:指针未初始化指针所指向的内存被释放指针超出变量作用域
避免方法:
// 1. 初始化时置NULL
int* p = NULL;
// 2. 释放后置NULL
free(p);
p = NULL;
// 3. 使用前判断
if (p != NULL) {
*p = 100;
}
5.内存泄露是什么
使用完后的内存没有释放就指向了别的地方
6.栈与堆的使用和区别、
管理方式
空间大小
内容填充
分配效率
生命周期
管理空间内容分配
生命
堆与栈实际上是两种常见的内存分配方式。主要有如下几种区别:
(1)管理方式不同
栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
(2)空间大小不同
每个进程拥有的栈大小要远远小于堆大小,理论上,进程可申请的堆大小为虚拟内存大小,进程栈的大小 64bits 的 Windows 默认1MB,64bits 的 Linux 默认 10MB
(3)生长方向不同
堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
(4)分配方式不同
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配出 alloca()函数分配,但是栈的动态分配和堆是不同的只,它的动态分配是由操作系统进行释放,无需我们手工实现,。
(5)分配效率不同
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机,制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
(6)存放内容不同
栈存放的内容:函数返回地址、相关参数、局部变量和寄存器内容等,当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者 BSS 段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
7.volatile关键字的作用
防止编译器优化
适用场景:硬件寄存器多线程共享变量中断服务程序
// 示例:硬件寄存器
volatile uint32_t* const PORTB = (uint32_t*)0x40020400;
// 中断标志
volatile bool flag = false;
每一次访问这个变量都是去访问寄存器里的数值
比如说a=1,b=a,c=a,这个过程c的值如果被送入寄存器了,那么c读取的时候就不会从内存中再次读取a的值,但是有些情况会发生问题,例如中断发生导致a的值再次被改变,此时如果读取寄存器中的值就不对了,应该重新从内存中读取a的值
哪些情况下使用volatile
(1)并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
(2) 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
(3)多线程应用中被几个任务共享的变量。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_43706223/article/details/133301198
8.大小端
面试题:如何判断系统是大端还是小端?
答案解析:
// 方法一:联合体
union {
short value; // 0x1234
char bytes[2]; // bytes[0]是低地址
} test;
test.value = 0x1234;
if (test.bytes[0] == 0x34) {
printf("小端系统\n");
} else {
printf("大端系统\n");
}
// 方法二:指针转换
int checkEndian() {
int value = 1;
char *p = (char*)&value;
return *p; // 1为小端,0为大端
9.函数指针
面试题:函数指针的定义和使用方法是什么?
答案解析:
// 定义函数指针类型
typedef int (*Operation)(int, int);
// 函数定义
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// 使用函数指针
Operation op = add;
int result = op(10, 5); // 结果为15
op = sub;
result = op(10, 5); // 结果为5
其和直接调用函数进行传参的区别是什么
----------
首先,两者最核心的区别在于函数调用目标的确定时机:
直接调用是 “静态绑定”,编译阶段就明确要执行哪个函数(通过函数名直接定位地址);而函数指针调用是 “动态绑定”,运行阶段才通过指针变量确定目标函数的地址,目标可以在运行时动态切换。
其次,从实际表现看有三个明显差异:
- 灵活性:直接调用只能固定调用某个函数,无法在运行中切换;函数指针则支持动态选择 —— 比如根据用户输入或状态变化,让同一段代码执行不同函数(比如回调场景中,传入不同处理函数实现不同逻辑)。
- 性能:直接调用时编译器可能做内联优化(消除函数调用开销),效率更高;函数指针因为需要间接寻址(先读指针再跳转),通常有轻微性能损耗,且编译器难优化。
- 代码可读性:直接调用直观,一看就知道调用的是哪个函数;函数指针需要结合上下文理解指向的目标,可读性稍弱。
最后,应用场景的区别很关键:
直接调用是常规操作,适合函数目标明确、无需动态变化的场景(比如普通工具函数调用);
函数指针则是 “动态行为” 的核心工具,典型场景包括回调函数(如qsort的比较函数)、实现简单多态(根据运行时条件选逻辑)、状态机或插件系统(通过函数表动态加载功能)等。
总结来说:直接调用胜在简单高效,适合固定逻辑;函数指针胜在灵活动态,适合需要运行时决策的场景,本质是 “编译期确定” 和 “运行期确定” 的区别。
10.常见的位操作技巧
1.对于某一位想复位或者置位--a&(~1<x)---a|(1<x)
11.strlen 和 sizeof 的区别
主要区别
类型strlen:函数,计算字符串长度sizeof:运算符,计算数据类型或变量的字节数
使用场景strlen:只用于字符串sizeof:可用于任何数据类型
计算方式strlen:遇到'\0'结束sizeof:编译时确定
示例代码:
void compareStrlenSizeof(void) {
char str[] = "Hello";
char *ptr = "World";
printf("strlen(str) = %d\n", strlen(str)); // 输出5
printf("sizeof(str) = %d\n", sizeof(str)); // 输出6(包含'\0')
printf("strlen(ptr) = %d\n", strlen(ptr)); // 输出5
printf("sizeof(ptr) = %d\n", sizeof(ptr)); // 32位系统输出4,64位输出8
// 数组示例
int arr[10];
printf("sizeof(arr) = %d\n", sizeof(arr)); // 输出40(10*4字节)
}
注意事项
strlen 不计算'\0',sizeof 计算所有字节
sizeof 是编译时运算符,strlen 是运行时函数
sizeof 可以用于未初始化的数组,strlen 必须用于以'\0'结尾的字符串
这些概念在C语言编程中非常重要,尤其是在内存管理和字符串处理方面。理解它们的区别和使用场景对于写出高质量的代码至关重要。
12.全局变量和局部变量的区别
主要区别
作用域全局变量:整个程序局部变量:声明它的函数或代码块内
生命周期全局变量:程序运行期间一直存在局部变量:函数或代码块执行期间
存储位置全局变量:数据段;局部变量:栈区
13. typedef和 define有什么区别?
typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下4个方面的不同。
(a)原理不同
#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检査,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并报错。 例如, # define Pl3.1415926 ,当程序执行 area=Pr * r 语句时,PI会被替换为3.1415926。于是该 语句被替换为 area=3.1415926*r*r 。如果把# define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。 typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如,typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了,例如:INTEGER a,b; 用 typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如: typedef int a[10]; 表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)表示p是一种指向void型的指针类型。
(b)功能不同
typedef用来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。
例如: typedef int (*PF)(const char *, const char*) 定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为 const char*。typedef还有另外一个重要的用途,那就是定义机器无关的类型。例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度: typedef long double REAL ,在不支持 long double的机器上,该 typedef 看起来会是下面这样: typedef double real ,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL 。 #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
(c)作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。
(d)对指针的操作不同
INTPTR1 pl, p2和INTPTR2 p3,p4的效果截然不同。 INTPTR1 pl, p2进行字符串替换后变成 int*p1,p2 ,要表达的意义是声明一个指针变量p1和一个整型变量p2。
而INTPTR2 p3,p4,由于 INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于 int*pl,*p2 .从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用 typedef 为一种数据类型起的别名是带有一定含义的。
上述代码中, const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是 p1可以指向其他内容。而对于 const INTPTR2 p2,由于 INTPTR2表示的是个指针类型,因此用 const去 限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可以通过p2修 改其当前指向的内容。 INTPTR2 const p3同样声明的是一个指针常量。
14. 头文件的作用有哪些?
头文件的作用主要表现为以下两个方面:
1. 通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。
2. 头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。
15.在头文件中定义静态变量是否可行,为什么?
不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果 在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定义任何变量,当然也包括静态变量。
每个.c文件的编译是独立的,当链接器将所有的目标文件合并的时候。发现多个同名变量的定义,从而报错,正确做法是用extern声明
如果使用静态变量,虽然不会导致连接错误,但是会爆汁每个包含该头文件的的源文件都拥有了独立的副本,这违背了全局变量的设计初衷,可能引发逻辑错误。
比如说a文件操作了了这个静态变量,但是main.c文件中的该变量没有改变
因为静态变量会将连接属性限制在该。c文件内,这显然不符合
17.头文件中 ifndef/define/endif 的作用
答案与解析:
防止重复定义:在大型项目中,同一个头文件可能会被多个源文件间接或直接包含。如果没有头文件保护,编译器会多次处理同一个头文件,导致类、函数或变量的重复定义错误。(主要作用)
提高编译速度:减少编译器处理同一个头文件的次数,从而提高编译效率。
简化依赖管理:确保每个头文件只被处理一次,简化了头文件的包含关系和依赖管理。
3.代码下载到单片机中过程
答:首先代码编写完成,要经过编译,链接,最后生成hex文件或者bin文件,通过下载器识别到芯片的内存地址,读取相应的地址信息和数据内容,写入到目标内存的相应位置,一般会存放在芯片的程序存储区和数据存储区。
- 代码编译流程

预处理,编译,汇编,链接
- 预处理就是处理代码中的辅助指令出了类如define和include,条件编译#ifdef
- 编译将源代码翻译为对应硬件架构的汇编指令,对代码进行语法分析,语义分析,同时进行基本优化(删除冗余代码,简化运算表达式)
- 汇编就是解析汇编指令,转换为二进制的机器码
- 链接就是将多个目标缝合为可执行文件,汇编会生成多个。o文件,以及程序依赖的库文件,分配最终的内存地址,合并段表(代码段数据段)等
16.一些标准库的使用
strcpy 是专门用来处理字符串的复制,自动复制字符串的终止符 \0。
sprintf 是用来将格式化的文本输出到字符串中,支持多种数据类型的格式化输出。
memcpy 用于按字节复制内存块,适用于任何类型的数据,但它不处理字符串终止符,也不进行格式化。
18.C语言标准的库函数,比如memory set和memory copy,会用到吗?(★★★)
C语言的标准库函数,它们已经被广泛使用并且经过充分测试和验证,可以提高程序的可靠性和效率。
其中,memory set和memory copy是常用的库函数。memory set函数可以将一段内存区域中的所有字节设置为指定的值,memory copy函数可以将一段内存区域中的内容复制到另一个内存区域中。这两个函数通常用于处理内存相关的操作。
t
13.结构体如何存储数据才能占内存最小?
使用数据类型的最小字节长度来定义结构体成员变量,例如:使用int8_t代替int,使用float代替double等。或者使用占用内存较小的基本类型(如 int, char, short)而不是大类型(如 long, double)。
合理地排列结构体成员变量的顺序,将长度较短的数据类型放在一起节省内存。
适当取消结构体成员变量的字节对齐规则。
16.局部变量是否可以和全局变量重名
答案与解析:
可以重名,但是两者的作用域不同。
局部变量:它只在定义它的函数或代码块中有效。也就是说,局部变量在该函数或代码块外是不可见的,作用范围仅限于其所在的函数或代码块。
全局变量:它在整个程序中都可见,所有函数和代码块都可以访问它。
当局部变量和全局变量重名时,局部变量优先。在局部范围内,访问的是局部变量,而不是全局变量。这是因为在同一个作用域内,局部变量的优先级比全局变量高。
避免使用大量的指针成员变量,因为指针本身会占用额外的内存空间。
使用共用体,共用体允许不同类型的变量共享同一块内存,这样如果只存储其中一种类型,其他类型的数据将不会占用额外的空间。
内联成员:对于小型成员,可以考虑将它们内联到包含它们的结构体定义中,以减少实际的结构体大小。
静态分配:如果可能,使用静态分配代替动态分配,静态数据成员会被放置在更大的内存区域,而不是每次创建结构体实例时都分配新的内存。
避免不必要的成员初始化:如果数据成员默认值即为空或0,不需要显式初始化。
17.如何避免“野指针”?
初始化指针: 在声明指针变量时,最好将指针初始化为 NULL,这样可以确保指针不指向一个不确定的内存位置。
释放内存后将指针置为 NULL: 在使用 free() 释放动态内存后,将指针设置为 NULL,避免它继续指向已释放的内存区域。
18.简述队列和栈的异同
队列(Queue)与栈(Stack) 是常见的数据结构,它们在使用和操作上有所不同。以下是它们的异同:
相同点:
线性结构:队列和栈都是线性数据结构,即数据的存储方式是顺序的。
储方式:它们都使用连续的存储空间(如数组)或链式存储(如链表)来保存数据。
基本操作:两者都支持插入(入队/入栈)和删除(出队/出栈)操作。
不同点:
数据存取方式:
队列(Queue)遵循“先进先出”(FIFO, First In, First Out)原则,意味着先进入队列的数据会先被取出。
栈(Stack)遵循“后进先出”(LIFO, Last In, First Out)原则,意味着最后进入栈的数据会先被取出。
插入与删除位置:
在 队列 中,元素的插入(入队)发生在队列的尾部,删除(出队)发生在队列的头部。
在 栈 中,元素的插入和删除都发生在栈顶,栈顶元素首先被删除。
应用场景:
队列 常用于需要保持数据处理顺序的场景,如任务调度、消息传递等。
栈 常用于需要回溯操作的场景,如函数调用栈、括号匹配等
。
19.变量的声明和定义有什么区别?
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
一个变量可以在多个地方声明,但是只在一个地方定义。加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
- 简述strcpy、sprintf与memcpy的区别
- (返回目标地址)Strcpy(目标字符地址,源字符地址)
作用(将源字符串复制到目标字符串包括终止符\0)
实现:遍历字符串,循环实现覆盖
- Sprintf
作用:将格式化的数据写入到指定的字符串
实现:正常字符正常打印,非正常的字符就按照其对应方式打印出去
- memcpy(目标地址,原地址,字节数)
实现原理:本质上同strcpy一样但是memecopy不认识终止符,只会按照字节复制,定义一个指针呢指向我的源地址和目标地址,强制类型转换器单字节,然后按照给的个数实现依次的赋值
操作对象不同:strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
执行效率不同:memcpy 最高,strcpy 次之,sprintf 的效率最低。
实现功能不同:strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。
21.链表和数组有什么区别?
存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
22.结构传递和返回是如何实现的 ?
当结构作为函数参数传递的时候 ,通常会把整个结构都推进栈,需要多少空间就使用多少空间。(正是为了避免这个代价,程序员经常使用指针而不是结构。)
某些编译器仅仅传递一个结构的指针 ,但是为了保证按值传递的语义 ,它们可能不得不保留一份局部副本 。
编译器通常会提供一个额外的 “隐藏” 参数,用于指向函数返回的结构。有些老式的编译器使用一个特殊的静态位置来返回结构。这会导致返回结构的函数不可再入 ,这是ANSIC 所不允许的 。
23.内联函数
1.内联函数是一种特殊的函数声明方式,通过在函数前面加上inline关键字,来指示编译器在调用这个函数时将他展开,而不是直接进行调用。
2.减小函数调用的开销。
3.可以提高执行效率。
4.允许编译器进行优化来提示性能。
四.情景分析
1.两个设备通信交互,一个是速率通信很高,另有一个很低,如何实现
缓冲区(Buffer)
高速设备可以将数据写入缓冲区,然后低速设备从缓冲区读取数据。缓冲区充当了高低速设备之间的"中介",确保高速度的数据生产不会压倒低速设备的处理能力。
缓冲区的管理:高频设备生成的数据会被存入缓冲区,低频设备根据自己的处理能力慢慢读取。为了避免缓冲区溢出(即高速设备写入数据时低速设备还未能读取完),通常需要设计合理的缓冲区大小,并结合流控制机制进行调节。
两个线程如何完成任务
生产者线程(高速设备):负责将数据放入共享空间。由于数据生产的速度较快,它可能会在缓冲区中快速填充数据。
消费者线程(低速设备):负责从共享空间中取出数据并进行处理。由于处理速度较慢,消费者线程可能会在某些时候等待数据的生成。
线程同步与协调
同步机制:由于生产者和消费者访问同一个共享空间,需要使用同步机制来防止竞争条件和数据冲突。常见的同步方式有:
互斥锁(Mutex):确保在任意时刻只有一个线程能访问共享空间。
条件变量(Condition Variable):生产者线程写入数据时,如果缓冲区已满,消费者线程会等待直到生产者线程写入数据;消费者线程读取数据时,如果缓冲区为空,生产者线程会等待。
信号量(Semaphore):可以控制两个线程之间的并发执行,例如,生产者线程可以使用信号量通知消费者数据已准备好
解读线程同步与协调的三种方式
- 互斥锁:保证共享资源在同一时间只能被一个线程访问,解决同时读写导致的数据错误,例如缓冲区指针被两个线程同时修改
- 条件变量:一般和互斥锁搭配使用,线程间等待通知的工具
- 信号量:资源计数与并发控制
- 设计缓冲区时如何避免缓冲区溢出
--合理设置缓冲区大小,用高速设备的峰值数据量和低速设备的最大延迟处理计算,如果高速设备一秒产生1000,低速一秒读500,那缓冲区至少要1000,应对短期峰值
--流控制机制,当缓冲区达到阈值,高速设备暂停并告诉低速设备加速读取
--动态扩容、环形缓冲区(数据读写循环覆盖)
- 如果缓冲区是空的,消费者尝试读取,或者满了生产者尝试写入怎么把
需要通过线程同步的机制让线程进入等待-唤醒的状态,避免无效循环,如果缓冲区是空的,消费者线程会进入阻塞,直到生产生通过同步信号(条件变量,信号量唤醒他),同理满了,生产者会阻塞直到被唤醒
- 条渐变量和互斥锁如何配合
线程访问共享资源前,先加互斥锁,确保独占访问,如果条件不满足,就释放互斥锁,当条件满足,另一个线程通过通知唤醒等待的线程,重新获得互斥锁
- 假如缓冲区大小固定,当高速设备突发大量视觉导致缓冲区溢出,除了丢弃数据,还有别的办法吗
--覆盖最旧的数据(例如环形缓冲区,写指针超过读指针直接覆盖,
--阻塞高速设备
--动态分流
2.为什么要分为用户空间和内存空间
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。
当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态
五.
3.需要您用RT-Thread设计一个在STM32上跑的应用。要求把网络通信和业务处理分开,分别由不同的Thread来完成,那么,这个设计的核心关键思路是什么?
- 重点在于,任务分离,消息传递,资源同步
任务分离就是网络通信负责收发消息,业务处理负责应用逻辑和数据处理
消息传递--读取,发送失败,发生中断
资源同步:互斥量,信号量的占有
4.如何确定任务的栈大小,应该怎么给定才合适?(★★)
确定任务的栈大小一般需要考虑到任务所需的内存空间大小、任务中使用的局部变量等等。
一般可以用下面这几个方法:
(1)通过代码分析计算任务所需的最大栈大小
(2)可以在运行时动态分配栈空间
(3)再就是靠经验了,常规的任务栈大小一般都是4KB或者8KB
- 从哪几个方面考虑
--局部变量,函数调用,上下文切换
函数调用方面,一个任务函数要将返回地址,参数,寄存器的值压入栈,并且还要考虑函数调用深度(4-32字节,要考虑函数调用层数)-(可见开销)
--上细下文切换和中断方面,,每次切换任务保存的寄存器组按照128字节预算,还有中断或者中断嵌套的开销(隐藏开销)
初始栈大小= 可见消耗加冗余消耗加隐藏消耗
控制方法,用rtt的工具-其智慧栈使用检测,观察最大栈消耗。Rtt还有栈溢出检测的机制,也可以做实时的检验
5.如果现在有个单片机连接了一个传感器,这个传感器不定时的会给单片机传数据,你怎么判断数据的一帧是否结束,这个帧没有具体的格式?2
采用不定长数据定义帧格式为:起始帧头0X55 0XAA,第三个字节为CRC,第四个字节为有效数据长度,最后全部为有效数据
6.解释一下什么是状态机?(★★)
答案与解析:
状态机(State Machine)是一种用于设计和实现系统行为的模型,它通过一系列的状态、事件和在这些状态间转换的规则和响应事件来描述系统。状态机能够清晰地定义在不同输入或条件下系统应如何响应。
7.hal库和标准库开发的区别
- 开发效率
开发效率hal库更高,其不仅可以和cubemx配合使用,方便的配置io口和时钟树等,并且其支持多种类型的芯片,方便在不同类型的芯片之间移植代码,而标准库像是gpio和时钟都需要自己去手动配置,芯片适配性不高
但是hal配合cubemx的使用只支持st公司的芯片也是具有一定的局限性
- 开发性质,无论是标准库还是hal库,其本质都是对于寄存器的操作,但是hal的封装性更高,只需要会调用其库函数即可不关心底层的实现细节。这也牺牲了一些效率,而标准库的开发提供了更多的控制权和精度,可以充分释放芯片的潜力
- 开发性能
标准库更偏向于寄存器控制,而展现了更高的性能上限。
可以从
项目需求--需要多高的精度,是否需要移植
开发环境--芯片种类,开发周期考虑
8.对开发流程的认知
我们要知道开发一款芯片有几种方式
寄存器操作
标准库
Hal库
我们想操作一款芯片执行其相应功能,分为两个大块调通外设,实现产品功能,最本质的开发方式是研读芯片手册基于其寄存器的地址,对于其寄存器进行读写操作是为功能开发,但是很多芯片外设的使用,他们寄存器的封装的逻辑是一样的,为了提高开发效率我们把这些常用的外设比如gpio,比如定时器,将如果想使用他们的功能对于寄存器的操作封装为了功能函数,我们不必直接操控底层的寄存器,而是通过库函数的api去使用我们所想使用的外设,极大的简化了两大步骤中的第一步,其实调用这些库函数的本质就是对于寄存器的操作,涉及到库开发我们常用的就有hal和标准库,他们的底层逻辑是一样的但是hal库却封装了更多的功能但只适配于st的芯片,标准库的开发更偏向于底层因此适配性更广,但是移植性由于hal的功能集成度高,所以更方便移植。但具体功能的差异就要对这两款库函数的逻辑架构有着更深入的了解。
如何自己开发实现一个库
- 我可以定义一个机构体类型的指针,按照地址排布封装该外设的寄存器信息,设置初始化函数,传入一个该类型的指针(本质就是找到该外设寄存器的首地址),然后函数里面就会通道指针对于这一片寄存器实现访问修改。注意我也要在头文件中通过宏定义定义每一个外设模块的地址,我需要找到地址,强制类型转化为这个结构体指针的类型
第二。我还可以直接实现库操作,比如定义一个函数实现寄存器配置,我首先要宏定义寄存器的基地址,然后定义不同寄存器基于基地址的偏移量实现加和得到各个寄存器的地址,然后再通过一层宏定义实现对寄存器直接操作的实现,把各个寄存器地址,强制转化为int*型然后再解引用,这样就可以在main文件中,直接访问该寄存器进行赋值
9.程序什么时候应该使用线程,什么时候单线程效率高?
耗时的操作使用线程,提高应用程序响应 。
并行操作时使用线程,如 C/S 架构的服务器端并发线程响应用户的请求。
多 CPU 系统中,使用线程提高 CPU 利用率
改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独 立的运行部分,这样的程序会利于理解和修改。
其他情况都使用单线程。
本质上多线程的架构是为了追求实时性,提高程序的响应,系统功能的实现是否有高实时性的要求
我想一边读数据,一边检测停止条件。
10.环形缓冲区
环形缓冲区经常被用于串口通信,socket,使用环形缓冲区可以将数据保存下来,使用R,W指针来进行数据的操作。
运行
什么是FIFO(环形缓冲区)
可以使用软件实现,一个环形缓冲区。
使用场景:串口通信,数据采集,DMA,音频处理。
先进先出,使用FIFO可以减轻CPU的负担。
11. 如果串口通信收不到数据,你会怎么排查?
检查波特率、数据位、停止位是否一致
是否开启接收中断或DMA
检查引脚连接是否正确(TX-RX交叉)
使用示波器/逻辑分析仪抓信号
添加打印语句或LED指示灯辅助调试
12.你常用的调试工具有哪些?
JTAG/SWD调试器(如ST-Link、J-Link)
示波器、逻辑分析仪(如Saleae)
printf调试、日志系统
GDB调试工具
单步调试
- hal库的高可移植性体现在什么地方
HAL 库的高可移植性并非指 “代码完全不用修改就能直接复制到不同芯片上运行”,而是通过统一的抽象层设计,大幅降低了不同芯片(尤其是同厂商不同系列芯片)之间的移植成本。其核心优势体现在以下几个方面:最重要的就是统一接口
1.统一的 API 接口,屏蔽底层差异
HAL 库为同类外设(如 GPIO、UART、SPI 等)定义了标准化的函数接口和数据结构,无论芯片型号如何变化,操作逻辑和函数名保持一致
移植到 STM32F4 系列时,函数名、结构体成员完全不变,只需根据新芯片的引脚资源(如是否有 GPIOA、PIN5 是否可用)调整Pin等参数,核心初始化逻辑无需修改。
2.硬件细节的抽象与封装
HAL 库将芯片底层的寄存器操作、硬件特性(如时钟树、引脚复用规则)封装在内部,开发者无需关注具体寄存器地址和时序差异。
例如:
不同芯片的 GPIO 寄存器布局可能不同(如端口配置寄存器的位定义、复位值),但 HAL 库通过HAL_GPIO_WritePin()等函数统一了引脚操作,无论底层寄存器如何变化,上层调用方式不变。
时钟配置等复杂逻辑(不同芯片的时钟树差异极大)被封装在HAL_RCC_Init()等函数中,移植时只需修改时钟配置结构体的参数(如主频、分频系数),无需重新理解新芯片的时钟树设计。
----如何实现硬件无关层
有可能寄存器映射的差异被封装在头文件中(寄存器的地址不同),而非函数内部
不同芯片的 GPIO 寄存器布局(如寄存器数量、位定义)可能不同,但这些差异被隐藏在芯片专属的头文件(如stm32f1xx.h、stm32l0xx.h)中,而非HAL_GPIO_WritePin函数内部。用函数对结构体的访问和修改,基于不同的头文件对应了不同的修改,但其逻辑的操作本质不变
对于差异较大的芯片,其适配逻辑也可能通过条件编译,来实现不同资源的适配,比如说名称一样但是调用不同的gpio库, 库为每个芯片系列(如 F1、L0、H7)提供独立的驱动文件夹,内部包含专属的外设实现代码,比如说如果定义了f1,就包含f1系列的gpio.h
- 芯片选型思路
- 首先要明确好自己的开发目标,需要用到的外设,程序的大小,对于时钟频率的要求大概有一个框架。
- 明确必选和可选功能的差异,我必须要做的功能是什么,什么是可选扩展的
- 用关键性能指标卡阈值
--算力:有关于cortex-m系列,系列越高一般来说算力越强
--存储:flash和ram的大小考虑
--功耗
--接口兼容性
- 评估开发与维护成本,优先选择生态成熟的方案
--工具链,sdk与开发文档,外在技术支持
5.可靠性和环境适应性的考虑还有成本考虑
五.通信协议分析
- i2c问题汇总
- 为什么要接上拉电阻
实现线与的逻辑,任何设备都可以拉低总线
配合开漏输出,实现把拉回高电平的能力
定义了总线的空闲时的状态
- 为什么开漏输出
实现线与的逻辑,允许多个设备共享一个总线而不会出现冲突,避免一个输出高一个输出低,形成短路烧毁器件
仲裁机制,在多主机的环境中,开漏输出遵循低电平优先的准则
- i2c的地址位数
七位地址格式最常用,7+1(读写位)
十位地址格式,本质还是字节通信,但是优先发送十位通信的命令,会把高两位优先发送
6.IIC如何实现多主机通信?
--总线仲裁。采用低电平优先原则。总线采用线与逻辑,谁先发送低电平,谁将先获得总线控制权。
--时钟同步。因为是线与逻辑,只有当所有设备同时释放SCL时,总线才会被拉高。因此所有设备会以最慢的设备的时钟速率运行,以保证时钟同步。
-- 冲突检测。主机发送每一位数据时都会检查SDA线实际电平,如果检测到的电平与期望发送的不同,则失去仲裁权。
8.IIC的时钟同步和时钟拉伸是什么?
时钟同步:I2C总线上的SCL线是所有设备逻辑与的结果。当任一设备将SCL拉低,总线SCL就为低电平。只有当所有设备都释放SCL(高阻态),SCL才会变为高电平,这确保了最慢的设备也能跟上通信节奏。
时钟拉伸:从设备可以通过持续拉低SCL来延长时钟周期。这样从设备可以有更多时间处理数据。主设备必须等待SCL实际变为高电平后才能继续。这是I2C协议中从设备控制通信速度的机制。
9.软件 I2C 多线程安全的核心方案
基础:互斥锁(Mutex)实现总线独占
给 I2C 发送 / 接收接口加全局互斥锁,确保同一时间只有一个线程能执行总线操作。线程需先获取锁才能访问 GPIO 引脚,未获取到的线程阻塞等待,从根本上避免引脚冲突。这是最简洁有效的底层保障。
优化:结合超时与优先级机制
给锁设置超时时间(如 100ms),防止线程因锁未释放而永久阻塞;
若在实时系统中,使用支持优先级继承的互斥锁,避免低优先级线程持有锁时,高优先级线程被长期阻塞(优先级反转问题)。
进阶:软件模拟仲裁逻辑(可选)
对需要多主机公平竞争的场景,在锁保护基础上,每发送一位数据时检测总线实际电平:若与自身发送值不一致,立即释放总线退让,模拟硬件 I2C 的仲裁特性,提升总线利用率。
10.2C通信中常见的问题及解决方案?
分析现象,思考原因,思路排查
--硬件连接类问题
- 上拉电阻的缺失或者阻值不匹配,可以用万用表测总线空闲时的电压
- 连接通路的接线问题
- 共地不良或者电源噪声(偶尔通信成功,数据错乱)
--地址与应答类的问题
- 从机地址发送错误或者冲突(主设发送后,从机无ack或者错误从机ack),可以查阅从机的datasheet确认地址,并保证无同样的设备地址的设备
- 应答信号丢失(单字节传输正常多个就不行)。可能原因--从机忙,主设备未正常释放sda。解决策略,降低通信速率,检查从机是否有缓冲区满标志
--时序与速率类的问题
- 时钟频率过高(低速正常,高速通信不正常)
- 时钟拉伸处理不当(如果从设备支持时钟拉伸而主动拉低时钟,但是主设备未进行相应的配置),这会导致通信突然卡住,主设备超时报错;解决思路:排查时钟是否要进行时钟拉伸。
--软件逻辑诶类
- 起始停止信号配置的正确性一定符合时序
- 数据帧发送错误,主设备没有按照从机要求的数据帧格式传输。可能导致接收数据和预期不符;严格排查从机通信协议中的要求,记得发送应答信号
--总线故障类
- 总线死锁,症状:SDA或SCL被某个设备一直拉低。可能是因为芯片损坏或者通信的突然中断,解决方案:可以逐个断开从机,检验总线是否恢复,还可以主机发送控制命令,强制从机释放
2.多主设备仲裁失败,多主设备的仲裁是要进行使能;添加相应的总线仲裁机制。
- 什么是i2c通信(简单介绍一下)
1.协议定位和核心特性
串行同步通信协议,专为短距离设备间通信设计。其核心特点是:
仅需两根线:串行数据线(SDA)和串行时钟线(SCL)1,通过 “线与” 逻辑实现多设备共线通信;
主从架构:主设备(如 MCU)发起通信,从设备(如传感器)被动响应,每个从设备有唯一的 7 位或 10 位地址2;
开漏输出与上拉电阻:SDA/SCL 需外接上拉电阻(典型值 4.7kΩ),空闲时总线保持高电平,设备通过拉低总线表示逻辑 03。
- 通信流程和关键信号
起始,结束,应答,数据传输,地址与方向传输
- 典型应用
传感器,屏幕通信
优势:节省引脚、支持多主多从、可靠性高(应答机制)。
局限性:速率较低(标准模式 100kbps,快速模式 400kbps),总线长度受电容限制(通常≤0.3 米)
-
- spi问题汇总
- 什么是spi通信协议
同步串行协议,全双工结构,可以同时收发数据,主从架构。一个主设备可以控制多台从设备,没有复杂的寻址机制,使用专门的片选线选择从设备,没有应答机制。
- spi对比uarr和i2c优缺点
速度快,全双工,无需寻址,传输效率高
需要更多的信号线,无应答机制,通信距离有限,且多从设备需要多跟片选线,需要占用更多引脚
- spi的工作模式与区别
四种工作模式由cpol和cpha来决定(时钟极性与时钟相位)
极性决定空闲时的电平状态,相位决定在第一个还是第二个时钟边沿进行采样
- 描述一下spi的基本通信时序
主设备发起通信,拉低对应从设备片选线,主设备开始产生时钟信号,mosi发数据,miso收数据,结束通信则停止时钟,拉高cs线
- 如何提高spi通信效率以及影响因素
硬件优化,更高的时钟频率,软件优化使用dma进行数据传输
主设备时钟频率,pcb布线长短,软件实现效率
- 实际项目如何选择spi时序
根据从设备的支持频率,根据实际需求在可靠性和速度之间权衡,可以为不同设备设置不同频率
- 如何编写可靠的spi驱动
实现分封装的思想,基于hal的硬件抽象层下封装硬件细节
使用状态机管理通信过程
实现超时机制
添加错误检测和恢复机制
- 如何调试spi的问题
逻辑分析仪观察波形
检查时钟频率,相位和极性
回环测试验证
分步调试,先实现通信在完善其他功能
- 在rtos种如何实现spi的资源管理
使用互斥量或者信号量保护spi总线访问
优化任务优先级,确保关键任务能及时通信
超时机制
使用消息队列缓冲spi请求,减少任务调度的开销
10.SPI的传输流程
首先把对应从设备的片选线拉低,主机发出时钟信号,然后根据设置的工作模式,在特定的时钟边沿进行采样。主机先发起通讯,写入一字节数据。这个字节数据的意义主要是取决于目标设备,常见的7位地址+1位读写位。然后从机就会在特定的时钟边沿通过MISO发出数据给主机。
传输数据过程,是MSB 高位优先,发送到移位寄存器。
传输完成后,主设备拉高片选先,结束通信。
11.spi传输中容易出现的问题
--硬件问题排查
- 接线接反,虚接
- 片线线没有正常的控制和连接
- 没有共地(导致参考电平不一致
--时序配置
- 主从机的时钟相位和时钟极性不匹配,导致从机采样时间错误
- 通信速率过高,从机无法及时响应
--软件逻辑类
- 片选信号的释放过早(没通信完)或者过晚(从机错的将多帧数据视为一帧)
2.没有及时读取返回的数据,导致数据堆积或者覆盖
- 串口问题汇总
- 串口通信怎么约定好通信速率
2.UART的帧格式是什么?
起始位:1位。
数据位:5-9位。通常是8位,LSB(先发送低位)
校验位:0-1位,可选。
停止位:1-2位,固定为高电平。
- can通信
1.CAN总线的数据帧类型有哪些?各自特点是什么?
数据帧,遥控帧,错误帧,过载帧,间隔帧。
2.标准格式和拓展格式的区别
标准格式ID为11位,范围0x000-0x7FF。数据段长度最大八字节。
拓展格式ID为29位,范围0x00000000-0x1FFFFFFF。数据段长度最大64字节。
3.CAN通信的数据段包含多少位()?标准帧多少位
标准帧数据段最多包含8字节,拓展帧数据段最大可包含64字节。
4.CAN通信协议和UART,I2C、SPI通信协议的比较
UART:全双工,异步,点对点通信。适用于两个设备互相通信。
I2C:半双工,同步,一主多从
SPI:全双工,同步,一主多从(高速)
CAN:半双工,异步,多个主控互相通信。
5.CAN通信如何配置?
首先是先配置好波特率,
根据需求进行模式的选择,一般,测试用回环模式,多设备通信用正常模式。
然后是根据需要接收的报文进行过滤器的配置。根据想要的报文数量确定是列表模式还是屏蔽模式。
6.CAN总线的仲裁机制是如何工作的?
CAN总线采用非破坏性总线仲裁机制。
先判断优先级,ID越小,优先级越高。发送节点在发送数据时,通过线与机制,逐位比较ID。
当节点发送隐性位1,但是总线为显性0时,则自动退出仲裁并转为接收状态,不会破坏高优先级的消息传输。
7.CAN总线的仲裁机制(非破坏性仲裁)
多个节点同时发送数据时,通过标识符优先级竞争总线。
标识符ID越小。优先级越高。
节点在发送时同时监听总线,若发现更高优先级信号,则自动退出发送,等待总线空闲后重试。
8.CAN报文怎么发送的
9. CAN有用中断吗
对can的理解
多主通信:任何节点可主动发送数据。
非破坏性仲裁:ID 越小优先级越高,仲裁时不破坏报文。
差分信号:抗干扰能力强,适合工业环境。
错误检测与恢复:支持 CRC 校验、自动重传。
应用:汽车电子(如 ECU 通信)、工业自动化。
CAN通信的数据段包含多少位?标准帧多少位
can通信的话怎么用的,是调库吗
1.SPI与I²C的优缺点对比
通信接口结构,选择速率格式传输
差异性:
通信方式:SPI是全双工通信,数据可以在两个方向上同时传输。
总线结构:SPI支持多个从设备,但每个从设备需要独立的片选(CS)线,故连线数量随从设备增多而增加。
时钟控制:SPI由主设备提供时钟信号,所有的从设备共享同一个时钟信号,且速率可调节。
通信速率:SPI的速度通常比I²C更快,可达数Mbps到数十Mbps。
优点:
高速:传输速度快,支持高达几百MHz的数据传输速率,适合对传输速度要求高的应用场景。
灵活:主设备可以单独控制每个从设备的数据传输,适合需要独立控制多个设备的场合。
缺点:
硬件接线复杂:每个从设备都需要额外的片选(CS)信号线,当从设备数量增加时,连线成本和布线难度增大。
不支持多主机通信,只能单主机与多从机通信;传输距离受限制,通常适合短距离通信。
差异性:
通信方式:I²C是半双工通信,同一时刻只能在一个方向上传输数据,但可以在数据传输间隙进行方向切换。
总线结构:I²C仅需两条线(SDA和SCL),允许多个设备共享总线,采用广播式的地址寻址方式。
时钟控制:I²C的时钟由主设备提供,所有设备共享同一时钟,且速率相对较慢。
通信速率:I²C速率较低,典型速率从几kbps到几百kbps,现代高速I²C(Fast-mode Plus)可达到几Mbps。
优点:
硬件接线简单,只需要两根信号线(SDA和SCL)。
支持多主机通信,多个主机可以竞争总线控制权、可以连接多个从机设备,支持点对点和多对多通信结构。
缺点:
传输速度相对较慢,最大速率为400kbps。
总线负载能力有限:过多的设备挂在总线上会影响通信质量和可靠性。
不支持全双工通信,只能进行半双工通信。
总结:
总结:
SPI更适合高速、实时性强、需要与多个独立从设备进行高速交互的场合,而I²C则适合低成本、低速、轻型设备互联和系统内部通信。两者在实际应用中可根据具体需求选择合适的接口标准。
2.硬件i2c和软件i2c的区别

3.软件 I2C 相比硬件 I2C 有哪些限制或需要额外考虑的因素?
答:使用软件 I2C 主要适用于资源有限或不支持硬件 I2C 的系统,且通常用于较低速的应用场合。虽然它提供了灵活性和较低的硬件要求,但在性能、实时性、精度、CPU 占用率等方面相比硬件 I2C 存在显著限制。如果应用要求较高的通信速率和低延迟,或需要低 CPU 占用率,则应该优先考虑使用硬件 I2C。
。
4.解释一下你对CAN总线的理解?(★★)
答案与解析:
多主通信:CAN总线允许多个主设备在同一总线上通信,无需复杂的主机介入。每个设备可以独立发送和接收信息。
消息优先级:CAN总线使用标识符来标识每个消息的优先级。标识符数值较低的消息具有较高的优先级,这使得重要的通信可以更快地传输。
错误检测和容错:CAN协议内置了复杂的错误检测机制,包括校验和、帧检查、位填充等。此外,它还具备自动重传损坏的消息帧的能力,增强了通信的可靠性。
非破坏性仲裁:在CAN总线上,当两个或多个控制器同时尝试发送消息时,使用非破坏性的仲裁方法,确保优先级高的消息可以被发送,而不会损坏其他消息。
有限的数据长度:CAN协议规定,每个消息帧的数据长度最多为8字节,这使得消息传输简洁高效。
5简述i2c,串口和spi特点和应用场景
1.串口
适合中长距离、低速、点对点通信
核心优势:异步通信对布线要求低,可在中等距离下稳定传输,且协议简单(无需时钟同步逻辑)。
典型场景:
设备调试(如开发板与 PC 的调试信息传输,波特率 115200bps);
远距离数据交互(如 GPS 模块、蓝牙模块与主机的通信,距离几米);
工业控制(通过 RS485 扩展为多节点通信,如传感器网络、电机控制,距离可达百米)。
2. I2C:适合短距离、多设备、中低速通信
核心优势:仅需 2 根线即可连接多个设备(通过地址区分),布线成本极低,适合空间受限场景。
典型场景:
传感器网络(如温湿度传感器 SHT3x、加速度计 MPU6050,多个设备共享总线);
小型外设(如 EEPROM 存储、实时时钟 RTC、小尺寸 OLED 屏 SSD1306);
主板内部低速设备互联(如 BIOS 与外设的通信)。
3. SPI:适合短距离、高速率、单主多从通信
核心优势:速率远高于 I2C 和 UART,支持全双工,协议简单(无复杂仲裁),适合高速数据传输。
典型场景:
高速存储(如 SPI Flash、SD 卡,需快速读写数据);
高清显示(如 LCD/OLED 控制器,需高速刷新画面);
高速转换(如 ADC/DAC 模块,需实时传输高精度数据);
摄像头模块(如 OV7670,需高速输出图像数据)
- 几种常见的通信方式的速率差异
|
通信方式 |
STM32外设名称 |
速率范围 |
同步方式 |
通信方向 |
总线拓扑 |
DMA支持 |
中断类型 |
典型配置参数 |
典型应用场景 |
核心差异与优缺点 |
|
UART |
USART/UART |
低速(9600bps~4.5Mbps) |
异步 |
全双工 |
点对点(TX/RX) |
支持(DMA1/2 通道) |
接收完成、发送完成、空闲中断、错误中断 |
波特率寄存器 BRR(如 USART_BRR)、数据位(8/9 位)、停止位(1/2 位)、校验位(无 / 奇偶) |
传感器数据采集(如 NMEA-0183 格式 GPS 模块)、调试输出(USART1 连接 PC 串口助手)、蓝牙透传模块通信 |
无需时钟线,实现简单(仅 TX/RX),但无硬件流控,需注意波特率匹配和时序误差。实际常用 115200bps,STM32F103 USART1 最高 4.5Mbps1415。 |
|
SPI |
SPI1/2/3 |
高速(STM32F4 系列可达 45MHz) |
同步 |
全双工 |
主从(SCK/MOSI/MISO/CS) |
支持(DMA1/2 通道) |
发送完成、接收完成、溢出中断 |
时钟极性 CPOL(0/1)、相位 CPHA(0/1)、预分频器(如 SPI_CR1 的 BR [2:0]) |
OLED 屏幕驱动(如 SSD1306)、高速 ADC/DAC 通信(如 ADS1256)、外部 Flash 读写(如 W25Q128) |
速率快(F4 系列 SPI1 可达 45MHz17),但需 4 根线,多从机需独立 CS 引脚。支持双线双工模式(STM32F407)16。 |
|
I2C |
I2C1/2/3 |
标准模式 100kbps,快速模式 400kbps,高速模式 3.4Mbps |
同步 |
半双工 |
多主从(SCL/SDA) |
支持(DMA1/2 通道) |
仲裁丢失中断、数据发送 / 接收中断、错误中断 |
地址模式(7/10 位)、速率控制寄存器(如 I2C_CR2 的 FREQ)、上拉电阻(4.7kΩ) |
多传感器组网(如 BMP280 温湿度 + MPU6050 加速度计)、EEPROM 存储(如 AT24C02)、实时时钟(如 DS1307) |
仅需 2 根线,支持多主仲裁,但总线电容≤400pF(含 PCB 寄生电容)523。STM32F407 支持 DMA 突发传输18。 |
|
CAN |
CAN1/2 |
最高 1Mbps(距离 < 40 米) |
异步 |
半双工 |
多主(差分总线) |
支持(DMA1 通道) |
接收邮箱满、发送完成、错误警告、总线关闭中断 |
波特率参数 BRP(预分频器)、TS1(时间段 1)、TS2(时间段 2)、SJW(同步跳转宽度) |
汽车电子(ECU 控制)、工业机器人(多轴同步)、复杂设备组网(如电梯控制系统) |
差分信号抗干扰,支持优先级仲裁和错误重传,但需配置滤波器(如 CAN_FMR)和波特率参数67。 |
六.硬件设计
1.硬件设计过程中经常遇见IO引脚接上拉电阻或下拉电阻的情况,请给出自已的理解,并说明在嵌入式开发过程中的注意事项。
上拉电阻和下拉电阻的核心原理是通过连接高电平或低电平,为引脚提供默认的状态,从而避免引脚悬空时的电平不确定性。
上拉电阻(Pull-up Resistor)
将引脚的电平“拉”到高电平(通常是电源电压Vcc)。当引脚处于浮空状态(没有主动驱动时),上拉电阻通过连接到Vcc,将引脚的电平固定为高电平。上拉电阻常用于输入引脚,尤其是在使用开关或按钮的情况下。如果开关或按钮未按下时,引脚悬空,容易受到干扰信号的影响。通过上拉电阻将其默认状态设置为高电平,当开关按下时,电平可以被拉低至低电平,形成明确的逻辑状态。
下拉电阻(Pull-down Resistor)
将引脚的电平“拉”到低电平(通常是GND)。当引脚处于浮空状态时,下拉电阻通过连接到地,将引脚的电平固定为低电平。下拉电阻也常用于输入引脚。它使引脚在没有被驱动时保持低电平状态,当某个信号或者按钮驱动引脚时,可以将其电平拉高,从而产生明确的逻辑变化。
在嵌入式系统开发过程中,选择并使用上拉电阻或下拉电阻时,需要注意以下几个关键问题:
避免浮空引脚
如果IO引脚在没有信号输入时是悬空状态,它可能会随机浮动,导致不确定的电平状态。浮空引脚会容易受外部电磁干扰(EMI)影响,导致系统行为异常。因此,上拉电阻或下拉电阻的存在可以确保引脚的电平明确、稳定。
选择合适的电阻值
电阻值选择过小:如果上拉/下拉电阻阻值过小(如几欧姆到几十欧姆),那么引脚在被外部驱动时,可能会导致电流过大,增加功耗并可能损坏器件。电阻值选择过大:如果电阻阻值过大(如几兆欧姆),则引脚在切换电平时响应较慢,电路的抗干扰能力也会变弱。一般来说,常见的上拉/下拉电阻值在4.7kΩ到10kΩ之间,具体值根据实际情况和电路要求来选择。
避免短路风险
如果在外部驱动信号为强驱动时,上拉/下拉电阻选择不当,可能会造成电源与地之间的短路。例如,如果上拉电阻将引脚拉至高电平,但外部信号却强制拉低,且电阻值过小,可能导致电流过大,造成电路损坏或烧毁芯片。
开漏(Open-Drain)电路的使用
上拉电阻常用于开漏输出或**集电极开路(Open-Collector)**电路中。在这种电路中,输出只有低电平或悬空状态,通过上拉电阻提供默认的高电平,从而形成完整的逻辑信号。
嵌入式开发中的内部上拉/下拉电阻
很多微控制器(如STM32、AVR、ESP32等)内部集成了上拉/下拉电阻,可以通过软件配置来启用。这通常是通过配置寄存器来完成的,比如设置引脚为输入模式时可以选择是否启用内部上拉或下拉电阻。使用内部电阻可以节省电路设计和硬件成本,但有时内置电阻值较高(通常为几十kΩ),可能不适用于对抗干扰要求较高的场合。
功耗问题
在低功耗设计中,上拉/下拉电阻的功耗是需要考虑的因素。即使电阻值比较大,也会存在微小的漏电流。在电池供电的系统中,如果对功耗有严格要求,应该合理选择电阻值或通过禁用不必要的引脚来减少功耗。
- 重复记忆
- define和typedef的区别
- Rtt和free的区别
- 链表和数组的区别
- 头文件的作用
- 文件编译过程
- 简述dma控制器的功能
更多推荐



所有评论(0)