1. 实习以及在校项目中使用过哪些开发、调试、测试工具与环境开发环境操作系统Ubuntu20.04/22.04、Windows10/11、WSL2Linux 下 SSH 远程开发IDE / 编辑器VS Code搭配插件C/C、Python、CMake、Remote-SSHSTM32CubeIDE、Keil MDKARM 嵌入式PyCharm、IDEAJava 后端构建工具CMake、Make、colconROS2、Maven、pip✅ 调试工具嵌入式J-Link、ST-Link串口工具minicom、SecureCRT、TeraTermDWT 高精度延时调试Linuxgdb、gdbserverprintk日志top/htop/dmesg/strace远程调试VSCode Remote-SSH远程 gdb 调试网络调试WiresharktcpdumpcurlPowerShellipconfig/ifconfig✅ 测试工具单元测试pytestPython、GoogleTest (GTest)C/C接口测试Postmancurl日志log4cpp、logging、ros log性能time、perfYOLO 推理性能评估工具版本 项目管理Git GitLab/GiteeGit 分支管理✅ 框架 / 库配套环境计算机视觉OpenCV、PyTorch、YOLOv8ROS2 HumbleRViz、Gazebo 仿真环境嵌入式STM32 HAL 库FreeRTOS后端SpringBoot、MySQL8Django2. 详细介绍项目整体功能、软件模块、初始化配置内容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_IdleRTOS 自带空闲任务优先级 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、9600WordLength数据位8bit/9bitStopBits停止位1 位 / 2 位Parity校验位无校验 / 奇校验 / 偶校验Mode模式TX、RX 或者 TXRX 收发HwFlowCtl硬件流控CTS/RTS一般关闭OverSampling过采样16 倍 / 8 倍默认 16 倍2. DMADirect Memory Access直接内存访问DMA 可以不用 CPU 搬运数据内存 ↔ 外设。初始化参数DMA_HandleTypeDefRequestDMA 请求映射比如映射到 USART1_TX / USART1_RXDirection传输方向内存到外设 / 外设到内存 / 内存到内存PeriphInc外设地址是否自增串口 DR 寄存器固定地址 → 关闭MemInc存储器地址自增缓冲区一般开启PeriphDataAlignment外设数据宽度字节 (8bit)/ 半字 (16bit)/ 字 (32bit)MemDataAlignment内存数据宽度和外设匹配ModeNormal 正常模式传完一次停止 / Circular 循环模式循环不停传PriorityDMA 通道优先级低 / 中 / 高 / 极高3. FlashSTM32 片内 Flash用于参数掉电保存Flash读不需要初始化擦写操作才需要配置。Flash 读就像读内存直接地址访问。相关参数PageFlash 页号擦除最小单位是页不能只擦单个字节PageAddress页起始地址EraseInitStruct擦除结构体要擦除的 bank、起始页、页数写入数据目标 Flash 地址、源数据缓冲区、写入字节数4. 中断NVIC嵌套向量中断控制器属于 Cortex-M 内核NVIC 不是外设是内核组件每个外设中断都要配置 NVIC初始化参数HAL_NVIC_InitTypeDefNVIC_IRQChannel中断通道号例如 USART1_IRQn、EXTI0_IRQn、DMA1_Stream5_IRQnNVIC_IRQChannelPreemptionPriority抢占优先级可打断更低抢占优先级中断NVIC_IRQChannelSubPriority响应优先级同抢占优先级排队顺序不能互相抢占NVIC_IRQChannelCmdENABLE / DISABLE使能或者关闭这个中断通道4. 4个不同优先级周期任务如何被调度优先级如何发挥作用任务如何跑起来FreeRTOS 是抢占式调度。4 个周期任务优先级越高抢占能力越强。只要高优先级任务就绪会立刻抢占正在运行的低优先级任务。每个周期任务做完业务调用延时函数进入阻塞释放 CPU延时时间到在系统节拍中断里面把任务置为就绪态。只有所有更高优先级任务全部阻塞低优先级任务才可以执行。空闲任务优先级最低没有应用任务就绪时运行。周期任务如果使用 vTaskDelay周期会漂移要严格固定周期就用 vTaskDelayUntil。5. 项目中优先级反转的具体场景、问题现象以及实现的优化方案优先级反转高优先级任务等待低优先级任务持有的互斥锁低优先级任务在释放锁之前被中优先级任务抢占导致高优先级任务被中优先级任务阻塞实时性破坏。场景TaskH 高、TaskM 中、TaskL 低。低优先级拿到锁高优先级等待锁中优先级抢占低优先级低优先级无法释放锁。 现象高优先级任务响应时间不可预测延迟变大不是死锁。解决方案1优先级继承低优先级拿到锁高优先级阻塞等待锁时临时把持有锁的低优先级提升到高优先级锁释放优先级恢复。FreeRTOS 互斥锁支持二值信号量没有。它限制最大等待时间为临界区执行时间。2优先级天花板预先给资源设置天花板优先级任务拿到锁直接抬到天花板优先级。AUTOSAR/OSEK 常用。 工程最佳实践临界区代码尽可能短锁内禁止阻塞减少共享资源。 区分优先级反转≠死锁。死锁是多任务互相持有对方锁无法自动恢复。6. PID控制器的输入输出量、量纲PID 的输入是偏差 ee 设定值 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。任务切换钩子vApplicationTaskSwitchedHookFreeRTOS什么时候进入重点每次任务切换完成之后进入新任务的时候调用。 触发时机调度器完成上下文切换已经把新任务的寄存器从栈恢复好了即将跑新任务代码的那一刻。触发场景同优先级任务时间片轮转发生任务切换 → 触发钩子高优先级任务就绪抢占低优先级任务抢占式调度发生切换 → 触发钩子当前任务阻塞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唤醒Stop1.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/Stop2Standby 待机模式电源域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 IdleRTOS 空闲间歇性休眠休眠几百 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。 一般配置模式 34 倍频A 上升沿、A 下降沿、B 上升沿、B 下降沿都触发计数。1000 线编码器4 倍频一圈就是 4000 个脉冲。 Z 相每转输出 1 个脉冲接到外部中断用来做单圈位置校准消除累计误差Z 相不能用来实时计算转速。 转速算法三种M 法、T 法、M/TM/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 电平CPOL0空闲 SCLK 为低电平CPOL1空闲 SCLK 为高电平CPHA时钟相位在哪一个边沿采样数据CPHA0第一个时钟边沿采样CPHA1第二个时钟边沿采样组合成 4 种模式Mode0CPOL0CPHA0工程最常用AS5048、W25Q Flash 都是 mode0Mode1CPOL0CPHA1Mode2CPOL1CPHA0Mode3CPOL1CPHA1⚠️主从两端必须保持模式一致否则读到乱码。✅精简口述背诵 SPI 是同步串行总线主从结构四根线 SCLK、MOSI、MISO、NSS主机输出时钟支持全双工。四种模式由 CPOL 时钟极性和 CPHA 时钟相位组合Mode0 使用最多。读取从机时主机需要发送哑字节来产生时钟。SPI 没有硬件应答可靠性靠软件校验。工程上用 SPI 读取磁编码器 AS5048属于绝对编码器上电直接读角度不用回零和正交增量编码器不一样。15. ADC转换原理2V模拟量转换成数字量怎么算换算公式ADC 原理ADC模数转换器把连续变化的模拟电压 → 离散数字量。 核心比较模拟输入电压 和 参考电压 Vref。 ADC 是近似量化存在量化误差。分辨率 n 位输出数字范围0 ~ 2ⁿ -1例如 12 位 ADCn12范围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 位 ADCVref3.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 PointerSP 指向栈当前栈顶位置。栈是一块 RAM 内存。 Cortex-M 是满递减栈Full Descending 栈从高地址往低地址增长SP 永远指向最后压入栈的数据。压栈 PUSHSP 先减 4再把数据存入 SP 指向地址出栈 POP先读取 SP 指向的数据然后 SP 加 4两个 SPMSP 主栈指针复位默认用内核、中断用PSP 进程栈指针给任务用RTOS 里切换任务时切换 PSP。栈的作用函数调用保存返回地址。调用函数时把返回地址压栈函数执行完 POP 返回地址给 PC回到调用处继续跑。保存局部变量auto 局部变量不是 static中断响应现场保护进入中断硬件自动把 R0~R3、R12、LR、PC、xPSR 压入栈中断退出时自动弹出恢复寄存器。保存寄存器临时值汇编里手动 PUSH/POP。18. Flash和RAM的区别Flash擦除除了速度慢还有哪些特点表格项目RAMSRAM片内内存Flash片上闪存断电掉电丢失数据掉电数据保留非易失读写单位按字节 / 半字 / 字随机读写读、写都很快读可以随机按字节读写不能直接改写必须先擦除操作读、写自由无需特殊命令擦除是按块 / 扇区操作编程写按页用途运行程序、全局变量、栈、堆运行时存放变量存放程序代码 (.text)、常量 (.rodata)保存参数、日志寿命无擦写次数限制有擦写寿命一般 10k~100k 次反复擦写会磨损擦除bit 置 1全部变成 0xFF编程写 / 烧写只能把 bit 从 1 → 0不能直接 0→119. 已初始化和未初始化全局变量初始化时有什么差异非 0 初始化全局变量Flash 保存初值启动时拷贝到 RAM (.data)未初始化 / 初始为 0 全局变量不占 Flash启动阶段直接把 RAM (.bss) 区域清零。20. volatile、static、const、extern四个关键字的作用1. volatile作用告诉编译器这个变量的值可能被硬件 / 中断修改不要做优化每次都要从内存重新读取不要缓存到 CPU 寄存器。编译器优化正常变量会把值读到 CPU 寄存器里反复读的时候直接读寄存器不访问内存。volatile 禁止这个优化。✅使用场景外设寄存器例如 STM32 的寄存器被中断修改的全局变量主循环读取这个变量多任务共享变量RTOS 任务间共享⚠️重点坑volatile不能保证原子性不能代替互斥锁。只是阻止编译器优化解决编译优化带来的读取错误不能防止多核 / 多任务的竞态冲突。易错很多人以为 volatile 可以保证线程安全不行口述短句volatile 防止编译器优化每次都从内存读取变量用于寄存器、中断共享变量。不保证原子操作不能解决多任务竞争。2. staticstatic 分两种场景 ①函数内部局部 static 变量局部变量本来在栈加 static →放到全局静态存储区。只在第一次进入函数初始化一次函数退出变量不销毁值保留作用域仍然仅限本函数内部外面访问不到。②全局 staticstatic 全局变量 /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 初始化 →.dataRAM初始化为 0 / 不写初始化 →.bssRAM 初始化只执行一次在 main 之前不是每次调用函数执行。static conststatic const int val 123;const放到.rodataFlashstatic限制链接作用域仅当前.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 数组对应的字节就跟着变修改 bufval 的值也变。 工程最常用场景大小端转换、把 16bit/32bit 数据拆成字节。⚠️重点坑C 标准里不支持直接写一个成员读另一个类型双关属于未定义行为但是绝大多数编译器GCC/Keil支持嵌入式工程大量这么写面试可以提这个点。## 对比表格表格项目struct 结构体union 联合体内存分配各成员独立内存地址不同内存累加所有成员共用首地址共享同一块内存总大小≥所有成员之和受内存对齐影响等于最大成员的大小成员有效性所有成员可以同时有效同一时间只有一个成员有效互相覆盖用途描述一组相关属性如设备参数、帧协议、电机参数数据类型转换高低字节拆分22. 结构体内存对齐规则第一每个成员的对齐单位等于自身类型大小成员起始地址必须是自身大小的整数倍地址不满足就插入填充字节 padding。第二结构体整体大小必须等于结构体里面最大基本成员大小的整数倍放完所有成员不足的在末尾补填充。第三结构体首地址本身也要对齐对齐模数等于结构体最大成员。23. 指针加一是什么含义指针加 1不是地址值 1 字节是地址 指针指向类型的 sizeof 大小24. 学业排名、四六级分数25. 期望工作地点26. 反问环节