经纬恒润 自动驾驶嵌入式软件-秋招面经
1. 实习以及在校项目中使用过哪些开发、调试、测试工具与环境
开发环境
- 操作系统:Ubuntu(20.04/22.04)、Windows10/11、WSL2;Linux 下 SSH 远程开发
- IDE / 编辑器
- VS Code(搭配插件:C/C++、Python、CMake、Remote-SSH)
- STM32CubeIDE、Keil MDK(ARM 嵌入式)
- PyCharm、IDEA(Java 后端)
- 构建工具:CMake、Make、colcon(ROS2)、Maven、pip
✅ 调试工具
- 嵌入式:J-Link、ST-Link,串口工具
minicom、SecureCRT、TeraTerm;DWT 高精度延时调试 - Linux:gdb、gdbserver,
printk日志,top/htop/dmesg/strace - 远程调试:VSCode + Remote-SSH,远程 gdb 调试
- 网络调试:Wireshark,tcpdump,curl,PowerShell,
ipconfig/ifconfig
✅ 测试工具
- 单元测试:pytest(Python)、GoogleTest (GTest)(C/C++)
- 接口测试:Postman,curl
- 日志:log4cpp、logging、ros log
- 性能:time、perf;YOLO 推理性能评估工具
- 版本 & 项目管理:Git + GitLab/Gitee,Git 分支管理
✅ 框架 / 库(配套环境)
- 计算机视觉:OpenCV、PyTorch、YOLOv8,ROS2 Humble,RViz、Gazebo 仿真环境
- 嵌入式:STM32 HAL 库,FreeRTOS
- 后端:SpringBoot、MySQL8,Django
2. 详细介绍项目整体功能、软件模块、初始化配置内容;FreeRTOS任务数量、每个任务工作内容、中断处理逻辑、休眠与唤醒流程
本项目基于 STM32,搭载 FreeRTOS 实时操作系统,实现设备的数据采集、传感器读取、通信收发、状态控制、故障检测、上报上位机功能。设备上电后自动初始化硬件与 RTOS,周期性采集传感器数据;检测到按键或者外部事件时响应中断;数据经过校验处理后,通过串口上传;支持低功耗休眠,空闲时进入停机模式,外部中断触发唤醒;同时监测系统异常,发生故障时记录日志并执行保护动作。
软件模块划分
- 硬件驱动模块:GPIO、UART、ADC、TIM、外部中断、RCC 时钟、低功耗配置。负责底层寄存器 / HAL 库硬件初始化,向上提供统一 API,不包含业务逻辑。
- FreeRTOS 内核与系统模块:内核初始化、任务创建、队列、信号量、软件定时器、堆内存配置;任务栈大小、优先级配置。
- 传感器采集模块:读取 ADC / 外部传感器,做滤波、量程转换、数据有效性校验。
- 通信协议模块:串口帧打包、解析、CRC 校验,和上位机交互,接收指令、上报数据。
- 按键 & 事件检测模块:外部中断捕获按键,用信号量通知任务,不在中断服务函数里做业务处理。
- 故障与日志模块:故障判断、状态标记,保存错误信息,通过串口输出日志。
- 低功耗管理模块:系统空闲时进入休眠,配置唤醒源,唤醒后恢复外设和 RTOS 运行。
初始化配置内容
上电从main开始:
- HAL 库初始化:
HAL_Init(),配置系统时钟,开启外设时钟。 - GPIO 初始化:输入输出引脚、上拉下拉、外部中断引脚配置。
- 外设初始化:ADC、UART、定时器等;配置串口中断、ADC 转换中断。
- FreeRTOS 基础配置:在
FreeRTOSConfig.h里配置,开启抢占式调度,配置系统节拍configTICK_RATE_HZ,设置总堆大小,开启任务统计、软件定时器,关闭不需要的功能节省 RAM。 - 创建内核对象:队列、二值信号量(用于中断通知任务)。
- 创建所有 FreeRTOS 任务,设置每个任务的入口函数、栈深度、优先级。
- 最后调用
vTaskStartScheduler(),启动 RTOS 调度器。
⚠️ 调度器启动之后,不再执行 main 里调度器后面的代码。
FreeRTOS 任务数量 & 每个任务工作内容(示例:4 个任务,经典配置,面试高频)
优先级数字越大优先级越高
- Task_Sensor(优先级 3,栈大小 256)传感器采集任务 周期性阻塞运行。用
vTaskDelay延时。定时读取 ADC 和传感器原始值;做滑动平均滤波;校验数据是否越界;把打包好的数据放入消息队列,交给通信任务。如果检测到超限,置故障标志位。无数据时阻塞,不轮询占用 CPU。 - Task_Comm(优先级 2,栈大小 256)通信处理任务 阻塞等待消息队列。收到传感器数据后,组帧、添加 CRC;调用串口发送接口向上位机上报。同时解析上位机下发指令,解析完成后设置对应的事件标志,控制设备动作。串口接收中断只把收到字节存入环形缓冲区,不解析协议。
- Task_KeyEvent(优先级 1,栈大小 128)按键事件任务 阻塞等待二值信号量。外部按键中断 ISR 里只释放信号量
xSemaphoreGiveFromISR(),不做长耗时操作。任务拿到信号量之后,做按键消抖,判断短按 / 长按,执行对应的业务动作。 - Task_Idle(RTOS 自带空闲任务,优先级 0) 系统没有更高优先级就绪任务时运行。本项目在空闲任务钩子函数
vApplicationIdleHook()中判断:设备状态正常、无未发送数据、无未处理事件时,触发进入低功耗休眠。
任务设计原则:中断快进快出,业务全部放到任务里执行;用队列 / 信号量做中断到任务的通信。
中断处理逻辑
- 串口接收中断(USART ISR): 中断触发 → 在 ISR 中读取 DR 寄存器,把字节写入环形接收 buffer;判断接收帧尾,如果收到完整帧,用
xSemaphoreGiveFromISR释放信号量;调用portYIELD_FROM_ISR()判断是否需要任务切换。不做协议解析、CRC 计算,解析交给通信任务。 - 按键外部中断(EXTI ISR): 中断触发 → 清除中断挂起位;调用
xSemaphoreGiveFromISR释放按键信号量,通知按键任务;portYIELD_FROM_ISR()。禁止在中断里面加延时、打印日志、调用非 ISR 安全 API。
核心原则:ISR 尽量简短。中断只做:保存接收数据、释放信号量 / 消息、清标志。复杂业务交给任务。FreeRTOS 提供
xxxFromISR系列 API,普通xSemaphoreGive不能直接在中断里调用。
休眠与唤醒流程(FreeRTOS + STM32 停机模式示例)
休眠进入流程(空闲钩子)
- 空闲任务钩子函数
vApplicationIdleHook()运行。 - 判断条件:所有任务都阻塞、队列空、没有待处理中断事件。
- 关闭不需要的外设时钟(ADC、TIM 等,保留唤醒源外设)。
- 调用
__WFI(),进入 CPU 停机休眠,内核停止,系统节拍定时器停止。
⚠️ 注意:当进入停机模式,SysTick 会停掉。唤醒之后,FreeRTOS 需要补偿系统节拍,配置
configUSE_TICKLESS_IDLE = 1开启 Tickless 低功耗。
唤醒流程(外部中断唤醒)
- 外部中断(按键 / 外部信号)触发,CPU 退出休眠,继续执行
__WFI()之后的代码。 - 恢复被关闭的外设时钟。
- Tickless 自动补偿系统节拍,修正 RTOS 内部的系统时间,保证
vTaskDelay时间准确。 - 中断服务函数继续执行:清中断标志,释放信号量。
- 退出中断,RTOS 调度器切换到就绪任务,继续正常运行。
- 设备恢复采集、通信,继续业务流程。
✨面试精简口述版本(2 分钟)
项目基于 STM32 和 FreeRTOS,实现传感器采集、串口通信、按键事件处理、故障检测和低功耗休眠。软件拆成硬件驱动、RTOS 系统、传感器采集、通信协议、按键事件、故障日志、低功耗管理共 7 个模块。 初始化流程:先 HAL 库和外设初始化,配置 FreeRTOSConfig.h,创建队列和信号量,创建 4 个任务,最后启动调度器。一共 4 个应用任务:传感器采集任务定时读 ADC、滤波,把数据放进队列;通信任务阻塞等待队列消息,组帧加 CRC,和上位机收发指令;按键任务等待信号量,做消抖和按键处理;RTOS 自带空闲任务,用来判断进入低功耗。 中断遵循快进快出原则:串口中断接收字节存入环形缓冲区,按键外部中断释放信号量,所有耗时业务都放到任务中,中断里面只调用 FromISR 版本 API。低功耗使用 Tickless 模式,空闲钩子满足条件后进入停机模式,调用 WFI;外部中断触发唤醒,唤醒后补偿系统节拍,恢复外设,RTOS 继续调度任务。
3. DMA、UART、Flash、中断分别需要初始化哪些参数,初始化函数主要做什么
1. UART 串口(HAL 库:UART_HandleTypeDef)
初始化参数(huart 结构体)
- BaudRate:波特率,如 115200、9600
- WordLength:数据位,8bit/9bit
- StopBits:停止位,1 位 / 2 位
- Parity:校验位,无校验 / 奇校验 / 偶校验
- Mode:模式,TX、RX 或者 TX+RX 收发
- HwFlowCtl:硬件流控(CTS/RTS,一般关闭)
- OverSampling:过采样,16 倍 / 8 倍,默认 16 倍
2. DMA(Direct Memory Access,直接内存访问)
DMA 可以不用 CPU 搬运数据,内存 ↔ 外设。
初始化参数(DMA_HandleTypeDef)
- Request:DMA 请求映射,比如映射到 USART1_TX / USART1_RX
- Direction:传输方向:内存到外设 / 外设到内存 / 内存到内存
- PeriphInc:外设地址是否自增(串口 DR 寄存器固定地址 → 关闭)
- MemInc:存储器地址自增(缓冲区,一般开启)
- PeriphDataAlignment:外设数据宽度:字节 (8bit)/ 半字 (16bit)/ 字 (32bit)
- MemDataAlignment:内存数据宽度,和外设匹配
- Mode:Normal 正常模式(传完一次停止) / Circular 循环模式(循环不停传)
- Priority:DMA 通道优先级:低 / 中 / 高 / 极高
3. Flash(STM32 片内 Flash,用于参数掉电保存)
Flash读不需要初始化;擦写操作才需要配置。Flash 读就像读内存,直接地址访问。
相关参数
- Page:Flash 页号(擦除最小单位是页,不能只擦单个字节)
- PageAddress:页起始地址
- EraseInitStruct:擦除结构体:要擦除的 bank、起始页、页数
- 写入数据:目标 Flash 地址、源数据缓冲区、写入字节数
4. 中断(NVIC,嵌套向量中断控制器,属于 Cortex-M 内核)
NVIC 不是外设,是内核组件;每个外设中断,都要配置 NVIC
初始化参数(HAL_NVIC_InitTypeDef)
- NVIC_IRQChannel:中断通道号(例如 USART1_IRQn、EXTI0_IRQn、DMA1_Stream5_IRQn)
- NVIC_IRQChannelPreemptionPriority:抢占优先级(可打断更低抢占优先级中断)
- NVIC_IRQChannelSubPriority:响应优先级(同抢占优先级,排队顺序,不能互相抢占)
- NVIC_IRQChannelCmd:ENABLE / DISABLE,使能或者关闭这个中断通道
4. 4个不同优先级周期任务如何被调度,优先级如何发挥作用,任务如何跑起来
FreeRTOS 是抢占式调度。4 个周期任务,优先级越高,抢占能力越强。只要高优先级任务就绪,会立刻抢占正在运行的低优先级任务。每个周期任务做完业务,调用延时函数进入阻塞,释放 CPU;延时时间到,在系统节拍中断里面把任务置为就绪态。只有所有更高优先级任务全部阻塞,低优先级任务才可以执行。空闲任务优先级最低,没有应用任务就绪时运行。周期任务如果使用 vTaskDelay,周期会漂移;要严格固定周期,就用 vTaskDelayUntil。
5. 项目中优先级反转的具体场景、问题现象以及实现的优化方案
优先级反转:高优先级任务等待低优先级任务持有的互斥锁;低优先级任务在释放锁之前,被中优先级任务抢占,导致高优先级任务被中优先级任务阻塞,实时性破坏。
场景:TaskH 高、TaskM 中、TaskL 低。低优先级拿到锁,高优先级等待锁,中优先级抢占低优先级,低优先级无法释放锁。 现象:高优先级任务响应时间不可预测,延迟变大;不是死锁。
解决方案:
1)优先级继承:低优先级拿到锁,高优先级阻塞等待锁时,临时把持有锁的低优先级提升到高优先级;锁释放,优先级恢复。FreeRTOS 互斥锁支持,二值信号量没有。它限制最大等待时间为临界区执行时间。
2)优先级天花板:预先给资源设置天花板优先级,任务拿到锁直接抬到天花板优先级。AUTOSAR/OSEK 常用。 工程最佳实践:临界区代码尽可能短,锁内禁止阻塞,减少共享资源。 区分:优先级反转≠死锁。死锁是多任务互相持有对方锁,无法自动恢复。
6. PID控制器的输入输出量、量纲
PID 的输入是偏差 e,e = 设定值 r − 反馈测量值 y。r,y,e 三者量纲相同。 控制器输出 u 是控制量,发给执行器;u 的量纲由执行机构决定,不一定和偏差 e 相同。
比例系数 Kp 承担量纲转换。积分时间 Ti、微分时间 Td 量纲都是秒。 积分项是偏差对时间积分;微分项是偏差对时间的变化率。
7. 上位机转速指令如何通过UART接收
上位机 → UART 硬件(TX) → 板卡 UART 硬件(RX) → 接收寄存器 → 中断 / DMA 读取字节 → 帧解析 → 提取转速值 → 送入 PID。
8. 增量正交编码器和STM32的信号交互、转速采集实现方式
增量正交编码器输出 A、B 两相相位差 90° 的方波,正反转时相位超前关系不同。将 A、B 接到 STM32 定时器 CH1、CH2,开启定时器编码器模式,硬件自动检测边沿,自动增减 CNT 计数值,支持 4 倍频提升分辨率。Z 相是索引信号,可接外部中断做原点校准。
转速采集常用 M 法:固定采样周期读取 CNT 增量,计算转速,适合高速;T 法测量脉冲间隔,适合低速;M/T 法融合两者,兼顾高低速。软件上一般用定时中断读取计数值,要处理计数器溢出,计算得到转速后送入 PID。编码器和 MCU 必须共地,否则容易引入噪声导致跳数。
9. CPU利用率的计算方法,不是指公式,而是落实到具体项目
项目中 RTOS 常用空闲钩子法统计 CPU 利用率。利用 RTOS 特性,没有就绪任务时运行空闲任务。在空闲钩子中读取 DWT 高精度定时器,累加空闲任务运行时间,设置固定统计窗口。窗口到期,用窗口总时间减去空闲时间,除以窗口总时间,得到 CPU 利用率。 还可以使用任务切换钩子,每次任务切换记录时间,统计每个任务单独占用 CPU 的时间,方便定位高负载任务。 注意:这两种方式默认不统计中断耗时,所以结果会略偏小;统计窗口一般取 100ms,窗口太小抖动大;空闲钩子里不能调用阻塞或者触发调度的 API。CPU 满载时空闲任务得不到执行,利用率显示 100%,容易出现任务执行不及时、数据丢包等问题。
10. 任务栈状态怎么监控?任务切换钩子什么时候进入?
FreeRTOS 常用uxTaskGetStackHighWaterMark栈水印法。任务创建时栈预先填充魔数,运行后扫描未被覆盖区域,得到栈高水位,代表历史最小剩余栈空间,用来评估栈余量。 还有 RTOS 自带栈溢出检查,在任务切换时校验栈尾魔数,一旦被改写进入栈溢出钩子,属于报警。更高阶是 MPU 内存保护,硬件捕获栈越界。 栈水印只是软件扫描,有滞后,不能捕获外部野指针踩栈,而且频繁调用会占用 CPU。
任务切换钩子 vApplicationTaskSwitchedHook(FreeRTOS)什么时候进入?
重点:每次任务切换完成之后,进入新任务的时候调用。 触发时机:调度器完成上下文切换,已经把新任务的寄存器从栈恢复好了,即将跑新任务代码的那一刻。
触发场景:
- 同优先级任务,时间片轮转,发生任务切换 → 触发钩子
- 高优先级任务就绪,抢占低优先级任务(抢占式调度),发生切换 → 触发钩子
- 当前任务阻塞(vTaskDelay、队列等待,信号量等待),调度器切到别的就绪任务 → 触发钩子
11. 低功耗Standby模式进入判断条件
硬件上,要清除唤醒标志 WUF,配置 PWR 的 PDDS 位、NVIC 的 SLEEPDEEP,执行 WFI/WFE,硬件才允许进入 Standby。 在项目 RTOS 工程里,软件要做多重判断:所有业务任务阻塞、没有正在进行的硬件传输,电源引用计数为 0;提前配置好有效的唤醒源;IO 做漏电防护;需要持久保存的数据提前存入 Flash 或备份寄存器;没有待到期的软件定时器。全部条件满足,才允许进入 Standby。 Standby 会关闭内核电源,SRAM 全部丢失,唤醒相当于系统复位,程序从头执行,不能像 Stop 模式那样唤醒后继续执行 WFI 后面的代码。
12. 进入Standby休眠前要做哪些处理?
- 等待所有正在执行的业务结束:等待 DMA、UART、SPI 等传输完成;PID、电机控制停止,关闭 PWM 输出,防止休眠期间误动作。
- 确认没有就绪任务、没有即将到期的软件定时器。如果还有定时事件,不能进 Standby,一旦进去定时器状态全部丢失。
- 释放电源锁(引用计数减到 0)。项目常用电源锁机制,只要某个模块持有电源锁,禁止进入 Standby。
- 停止 RTOS 调度器(可选,工程稳妥做法:暂停调度,防止在准备过程中被其他任务打断)。
13. 进入休眠后怎么唤醒?什么是唤醒源?
一、Stop 模式(Stop1/Stop2)唤醒
Stop:1.8V 内核电源域保持供电,SRAM、寄存器内容不会丢。唤醒之后,从 WFI/WFE后面那一行代码继续执行,不是复位。 ✅支持的唤醒源:
- 外部中断 EXTI(任意 GPIO 外部中断)
- UART、SPI、I2C 的接收唤醒(外设自带唤醒功能)
- RTC 闹钟 / RTC 时间戳
- LPTIM 低功耗定时器
- 比较器 COMP
二、Standby 模式唤醒
Standby:主 1.8V 域断电,SRAM、寄存器全部丢失;唤醒等价于硬件复位,程序从头启动。 ✅Standby 允许的唤醒源(就这 4 类,记住)
- WKUP 引脚(上升沿唤醒,最常用)
- RTC 闹钟、RTC 时间戳
- IWDG 独立看门狗溢出
- NRST 外部复位(这也算唤醒,但属于硬复位)
表格
| 项目 | Stop 模式(Stop1/Stop2) | Standby 待机模式 |
|---|---|---|
| 电源域 | VDD 主电源保持,1.8V 内核域不掉电;只有内核时钟、AHB/APB 时钟停止 | 1.8V 内核电源域断电;SRAM、外设寄存器全部掉电丢失,仅备份域(RTC、备份寄存器、WKUP 检测电路)由 VDD/VBAT 供电 |
| 内存 & 寄存器 | SRAM、寄存器内容保留 | SRAM 全部清空,普通寄存器全部丢失 |
| 唤醒行为 | 唤醒后,从 WFI/WFE 的下一行继续执行,不是重启 | 唤醒等价硬件复位,程序从头执行(复位入口) |
| 可用唤醒源 | 丰富:EXTI、LPTIM、RTC 闹钟、UART 接收唤醒、I2C、比较器 COMP 等 | 很少:WKUP 引脚、RTC 闹钟 / 时间戳、IWDG 溢出、NRST 复位。UART、普通 EXTI、TIM 都不行,外设已经断电 |
| 功耗 | 中等,uA 级,几十 uA 级别(视芯片) | 功耗最低,可到亚 uA 级(远低于 Stop) |
| 适用场景 | 短时间休眠,Tickless Idle(RTOS 空闲),间歇性休眠;休眠几百 ms~ 几秒,频繁休眠唤醒 | 长时间休眠,几分钟、几小时才唤醒一次;唤醒不能太频繁,频繁唤醒会反复复位,不可用 |
| 唤醒后动作 | 硬件自动退出低功耗;软件需要重新开启 HSE/PLL,恢复系统时钟,不用重新跑全部初始化 | 程序从头跑,所有外设、全局变量都要重新初始化;只能读备份寄存器区分是待机唤醒还是上电 |
| RTOS 适用性 | 适合 Tickless idle。空闲时进 Stop;唤醒后继续跑原来任务,任务状态、栈、队列全部保留,调度器不用重启 | 不能用于 Tickless。一旦进入 Standby,所有任务、栈、软件定时器全部销毁,唤醒直接重启,没法继续原来调度 |
14. 编码器是怎么采集的?SPI有没有用过?SPI的工作模式?
一、增量正交编码器采集
增量编码器输出 A、B 两相方波,相位相差 90°,部分带 Z 索引零相。 STM32 定时器配置成编码器模式,A、B 接到 TIM 的 CH1、CH2。硬件自动检测边沿,根据 A、B 的相位先后,硬件自动给 CNT 计数、自动判向,不需要软件轮询 IO。 一般配置模式 3,4 倍频:A 上升沿、A 下降沿、B 上升沿、B 下降沿都触发计数。1000 线编码器,4 倍频一圈就是 4000 个脉冲。 Z 相每转输出 1 个脉冲,接到外部中断,用来做单圈位置校准,消除累计误差;Z 相不能用来实时计算转速。 转速算法三种:M 法、T 法、M/T(M/T 法伺服最常用)。
注意:很多编码器是开集输出,外部需要加上拉电阻;主机和编码器必须共地,否则容易出现跳数。
口述短句:正交编码器 A、B 接入定时器通道,开启定时器编码器模式,硬件自动计数判向,常用 4 倍频。使用 M/T 法计算转速;Z 相外部中断做零位校准。
## 二、SPI 有没有用过? 用过。SPI 是高速同步串行总线,项目里用 SPI 读取磁编码器 AS5048、FLASH、ADC、陀螺仪。 主从架构,主机提供 SCLK 时钟。四线:SCLK 时钟、MOSI 主机发、MISO 从机发、NSS 片选。 特点:同步、全双工;只要时钟就可以收发。SPI 没有内置应答 ACK,没有硬件校验,校验需要软件自己加 CRC。 STM32 SPI 通信支持查询、中断、DMA 三种方式,高速传输优先 DMA,降低 CPU 占用。 坑点:SPI 读从机的时候,主机必须发送哑字节 dummy(一般 0xFF),产生 SCLK 时钟,从机才会把数据放到 MISO 线上。
## 三、SPI 四种工作模式(CPOL、CPHA) CPOL:时钟极性,空闲时 SCLK 电平
- CPOL=0:空闲 SCLK 为低电平
- CPOL=1:空闲 SCLK 为高电平
CPHA:时钟相位,在哪一个边沿采样数据
- CPHA=0:第一个时钟边沿采样
- CPHA=1:第二个时钟边沿采样
组合成 4 种模式:
- Mode0:CPOL=0,CPHA=0(工程最常用,AS5048、W25Q Flash 都是 mode0)
- Mode1:CPOL=0,CPHA=1
- Mode2:CPOL=1,CPHA=0
- Mode3:CPOL=1,CPHA=1
⚠️主从两端必须保持模式一致,否则读到乱码。
✅精简口述背诵 SPI 是同步串行总线,主从结构,四根线 SCLK、MOSI、MISO、NSS,主机输出时钟,支持全双工。四种模式由 CPOL 时钟极性和 CPHA 时钟相位组合,Mode0 使用最多。读取从机时主机需要发送哑字节来产生时钟。SPI 没有硬件应答,可靠性靠软件校验。工程上用 SPI 读取磁编码器 AS5048,属于绝对编码器,上电直接读角度,不用回零;和正交增量编码器不一样。
15. ADC转换原理?2V模拟量转换成数字量怎么算?换算公式?
ADC 原理
ADC:模数转换器,把连续变化的模拟电压 → 离散数字量。 核心:比较模拟输入电压 和 参考电压 Vref。 ADC 是近似量化,存在量化误差。
分辨率 n 位:输出数字范围
0 ~ 2ⁿ -1例如 12 位 ADC:n=12,范围0~4095
换算公式
数字量
D,输入模拟电压Vin,参考电压Vref\(D = \frac{Vin}{V_{ref}} \times (2^n -1)\) 反过来,已知读到数字量求电压: \(V_{in}= D \times \frac{V_{ref}}{2^n-1}\)
题目例子:2V 模拟量。假设12 位 ADC,Vref=3.3V 代入: \(D=\frac{2}{3.3}\times 4095 \approx 2482\)
16. 对中断向量表怎么理解?
中断向量表是一张存储中断服务函数入口地址的表。Cortex-M 向量表第一个元素是 MSP 栈初值,第二个是复位入口,之后是内核异常和外设中断函数地址。 中断触发后,NVIC 判定响应中断,使用中断号作为索引,在向量表取出中断服务函数地址,CPU 跳转过去执行。默认存 Flash,可通过 VTOR 寄存器重定向到 RAM。NVIC 负责管理中断使能和优先级,向量表保存函数入口。
17. PC指针和SP指针分别是什么?栈的作用和区别?
PC(程序计数器)
- PC 里面存放下一条要取指执行指令的地址。CPU 根据 PC 的值,去 Flash 取指令。
- 正常顺序执行:CPU 读完指令后,PC 自动递增,指向下一条指令。
- 跳转、函数调用、中断:修改 PC 的值,CPU 就跳到新地址执行。
⚠️Cortex-M 里,指令是 Thumb 指令,PC 最低位固定为 1,这是标记,不是地址的一部分。
一句话口述:PC 保存下一条指令地址,用来取指令,决定 CPU 跑哪段代码。
SP(栈指针 Stack Pointer)
SP 指向栈当前栈顶位置。栈是一块 RAM 内存。 Cortex-M 是满递减栈(Full Descending): 栈从高地址往低地址增长;SP 永远指向最后压入栈的数据。
- 压栈 PUSH:SP 先减 4,再把数据存入 SP 指向地址
- 出栈 POP:先读取 SP 指向的数据,然后 SP 加 4
两个 SP:MSP 主栈指针(复位默认用,内核、中断用);PSP 进程栈指针,给任务用(RTOS 里切换任务时切换 PSP)。
栈的作用
- 函数调用:保存返回地址。调用函数时,把返回地址压栈;函数执行完 POP 返回地址给 PC,回到调用处继续跑。
- 保存局部变量(auto 局部变量,不是 static)
- 中断响应,现场保护:进入中断,硬件自动把 R0~R3、R12、LR、PC、xPSR 压入栈;中断退出时自动弹出,恢复寄存器。
- 保存寄存器临时值,汇编里手动 PUSH/POP。
18. Flash和RAM的区别?Flash擦除除了速度慢还有哪些特点?
表格
| 项目 | RAM(SRAM,片内内存) | Flash(片上闪存) |
|---|---|---|
| 断电 | 掉电丢失数据 | 掉电数据保留,非易失 |
| 读写单位 | 按字节 / 半字 / 字,随机读写,读、写都很快 | 读可以随机按字节读;写不能直接改写,必须先擦除 |
| 操作 | 读、写自由,无需特殊命令 | 擦除是按块 / 扇区操作;编程(写)按页 |
| 用途 | 运行程序、全局变量、栈、堆,运行时存放变量 | 存放程序代码 (.text)、常量 (.rodata),保存参数、日志 |
| 寿命 | 无擦写次数限制 | 有擦写寿命(一般 10k~100k 次),反复擦写会磨损 |
擦除:bit 置 1(全部变成 0xFF) 编程(写 / 烧写):只能把 bit 从 1 → 0,不能直接 0→1
19. 已初始化和未初始化全局变量,初始化时有什么差异?
非 0 初始化全局变量:Flash 保存初值,启动时拷贝到 RAM (.data);
未初始化 / 初始为 0 全局变量:不占 Flash,启动阶段直接把 RAM (.bss) 区域清零。
20. volatile、static、const、extern四个关键字的作用?
1. volatile
作用:告诉编译器,这个变量的值可能被硬件 / 中断修改,不要做优化,每次都要从内存重新读取,不要缓存到 CPU 寄存器。
编译器优化:正常变量,会把值读到 CPU 寄存器里,反复读的时候直接读寄存器,不访问内存。volatile 禁止这个优化。
✅使用场景:
- 外设寄存器(例如 STM32 的寄存器)
- 被中断修改的全局变量,主循环读取这个变量
- 多任务共享变量(RTOS 任务间共享)
⚠️重点坑:volatile不能保证原子性,不能代替互斥锁。只是阻止编译器优化,解决编译优化带来的读取错误,不能防止多核 / 多任务的竞态冲突。
易错:很多人以为 volatile 可以保证线程安全,不行!
口述短句:volatile 防止编译器优化,每次都从内存读取变量,用于寄存器、中断共享变量。不保证原子操作,不能解决多任务竞争。
2. static
static 分两种场景: ① 函数内部局部 static 变量 局部变量本来在栈;加 static →放到全局静态存储区。只在第一次进入函数初始化一次;函数退出,变量不销毁,值保留;作用域仍然仅限本函数内部,外面访问不到。
② 全局 static(static 全局变量 /static 函数) 不加 static 的全局变量,作用域是整个工程,可以被其他.c 文件 extern 引用; 加 static:作用域限制在当前.c 文件(文件作用域,内部链接),其他源文件无法访问。 static 函数同理,别的.c 不能调用这个 static 函数。
一句话总结 static:局部 static:改变存储位置,只初始化一次,值持久保留;全局 / 函数 static:限制链接范围,仅本文件可见。
##3. const const:修饰变量只读,代码里不能写赋值修改(编译阶段检查)
注意:只是 C 语言层面的只读,不是硬件上不可修改。指针强转之后依然可以写,编译不报错,运行未定义行为。
存储位置:
- 全局 const 常量:一般放在 Flash (.rodata 段),不占 RAM(嵌入式非常重要!节省 RAM)
- 函数内 const 局部变量:有的编译器放栈上(占用 RAM)
常见写法:const uint8_t buf[] = {1,2,3}; 存 flash。 const uint8_t *p:指针指向 const 数据;uint8_t * const p:指针本身不可变(指针固定)。
口述短句:const 代表只读,编译期禁止赋值。全局 const 一般放在 Flash,节省 RAM。只是语言层面保护,强制指针转换仍然可以改写,不是硬件只读。区分常量指针和指针常量。
##4. extern extern:声明,不是定义! 告诉编译器:这个变量 / 函数,已经在别的.c 文件里定义好了,编译当前文件的时候不用开辟内存;等到链接阶段去别的文件找实体。
声明不分配内存;定义才分配内存。
一、static 修饰变量分 3 种情况
- static 全局变量(函数外面)
static int g_static_data = 100; // 初始化非0
static int g_static_bss; // 未初始化,默认0
- 有初始值(非 0):放在 .data 段(RAM)
- 未初始化或者初始化为 0:放在 .bss 段(RAM),程序启动时,启动文件把.bss 全部清零。
注意:static只改变链接属性(本文件可见),不决定放 Flash 还是 RAM! 很多人踩坑:以为 static 就放 Flash,错!只有加
const才会到 rodata。
- 函数内 static 局部变量
void test(void)
{
static int s_a = 10; //非0初值
static int s_b; //初始0
}
它不在栈里! 栈是普通局部 auto 变量。static 局部变量属于静态存储区。
- 非 0 初始化 →
.data(RAM) - 初始化为 0 / 不写初始化 →
.bss(RAM) 初始化只执行一次,在 main 之前,不是每次调用函数执行。
static const
static const int val = 123;
const:放到 .rodata(Flash);static 限制链接作用域(仅当前.c 文件可见)。 👉 static 控制链接可见性;const 控制存放段,两个互不干扰。
21. 结构体和联合体的区别?
1. struct 结构体
结构体把多个不同类型的变量打包成一个整体。 每个成员都有独立地址,内存空间叠加,总大小 ≥ 所有成员大小之和(存在内存对齐填充)。
struct Test{
uint8_t a;
uint16_t b;
};
假设 4 字节对齐:a 占 1 字节,填充 1 字节,b 占 2 字节。总大小 4 字节。 成员可以同时有效,可以同时读写 a、b,互不覆盖。
##2. union 联合体(共用体) 所有成员起始地址完全相同,共享同一块内存。联合体的总大小 = 它最大成员的大小。 同一时刻,只有一个成员有效,写一个成员会覆盖其他成员的值。
union Test{
uint8_t buf[2];
uint16_t val;
};
大小是 2 字节。给 val 赋值,buf 数组对应的字节就跟着变;修改 buf,val 的值也变。 👉工程最常用场景:大小端转换、把 16bit/32bit 数据拆成字节。
⚠️重点坑:C 标准里,不支持直接写一个成员,读另一个(类型双关),属于未定义行为;但是绝大多数编译器(GCC/Keil)支持,嵌入式工程大量这么写,面试可以提这个点。
## 对比表格
表格
| 项目 | struct 结构体 | union 联合体 |
|---|---|---|
| 内存分配 | 各成员独立内存,地址不同,内存累加 | 所有成员共用首地址,共享同一块内存 |
| 总大小 | ≥所有成员之和,受内存对齐影响 | 等于最大成员的大小 |
| 成员有效性 | 所有成员可以同时有效 | 同一时间,只有一个成员有效,互相覆盖 |
| 用途 | 描述一组相关属性,如设备参数、帧协议、电机参数 | 数据类型转换,高低字节拆分 |
22. 结构体内存对齐规则?
第一,每个成员的对齐单位等于自身类型大小,成员起始地址必须是自身大小的整数倍,地址不满足就插入填充字节 padding。
第二,结构体整体大小,必须等于结构体里面最大基本成员大小的整数倍;放完所有成员,不足的在末尾补填充。
第三,结构体首地址本身也要对齐,对齐模数等于结构体最大成员。
23. 指针加一是什么含义?
指针加 1,不是地址值 + 1 字节;是地址 += 指针指向类型的 sizeof 大小
24. 学业排名、四六级分数
25. 期望工作地点
26. 反问环节
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)