云计算百科
云计算领域专业知识百科平台

【STM32】 阶段性面试题(百题复习)


  STM32 全套外设面试题库  

模块一 · GPIO(LED 输出、按键输入)G1–G10

G1. STM32 的 8 种 GPIO 模式分别是什么?各用于什么场景?(必考)

模式特点典型用途
输入浮空 高阻,电平完全由外部决定 无上下拉的信号监测
输入上拉 内部上拉,默认读高 按键按下接地检测
输入下拉 内部下拉,默认读低 按键按下接 VCC 检测
模拟输入 关闭数字缓冲 ADC 采样
输出推挽 强驱动高/低电平 LED、普通数字输出
输出开漏 只能拉低,高电平靠外部上拉 I2C、电平转换、线与
复用推挽 外设驱动引脚 USART TX、SPI MOSI/SCK
复用开漏 外设开漏输出 I2C SCL/SDA
  • 追问:开漏输出为什么不能直接输出高电平?→ 内部无上拉 MOS,高电平必须外部上拉电阻,否则悬空。

G2. 推挽输出和开漏输出的本质区别?

  • 推挽:P-MOS + N-MOS 互补,能主动输出 0 和 1,驱动能力强,但两个 IO 不能线或。
  • 开漏:只有 N-MOS,输出 0 拉低、输出 1 释放(高阻),靠上拉电阻拉高;多设备可以线与(I2C 仲裁原理),还能做 3.3V↔5V 电平转换(上拉到 5V)。
  • 追问:线与是怎么实现的?→ 任一设备拉低 SDA,总线就是低;都不拉就是高(上拉),天然"与"逻辑。

G3. 按键检测为什么常用上拉输入?软件消抖怎么做?

  • 按键一端接 GND,另一端接 IO + 内部上拉:松开读 1、按下读 0,省外部电阻。
  • 机械抖动 510ms,消抖:读到变化后  延时 1020ms 再确认(软件);或 RC 滤波/施密特触发器(硬件)。
  • 追问:延时消抖有什么代价?→ 阻塞延时期间 CPU 干等,实时系统用"计时器+状态机"非阻塞消抖。

G4. LED 点亮有两种接法,GPIO 配置有什么不同?

  • 低电平点亮:LED 阳极接 VCC,阴极接 IO(串限流电阻)→ IO 输出 0 亮;推挽输出,初始化先输出 1 避免上电闪亮。
  • 高电平点亮:LED 阳极接 IO → 输出 1 亮,但驱动电流要查手册(GPIO 灌/拉电流约 ±25mA,长期建议 820mA,串 330Ω1kΩ 限流)。
  • 追问:为什么必须串限流电阻?→ 不串则 LED 电流只受内阻限制,可能烧毁 LED/引脚。

G5. GPIO 输出速度(2/10/50MHz)影响什么?

  • 影响 IO 翻转边沿的陡峭度:速度越高边沿越陡、EMI 越强、功耗越高。
  • 选型:普通按键/电平选低速档;SPI 时钟、高速 PWM 选高速档;不是越快越好。
  • 追问:速度档会影响实际输出频率上限吗?→ 会,档位决定输出级驱动能力,配错高速信号会失真。

G6. GPIO 常用寄存器有哪些?BSRR 为什么特殊?

  • MODER(模式)、OTYPER(输出类型)、OSPEEDR(速度)、PUPDR(上下拉)、IDR(读输入)、ODR(写输出)、BSRR(置位/复位)、LCKR(锁定)。
  • BSRR 高 16 位复位、低 16 位置位,一次写操作完成,原子性,不会被中断打断的"读-改-写"竞争破坏——这就是位带之外快速改 IO 的标准手段。
  • 追问:为什么不用 ODR 直接改?→ ODR |= (1<<5) 是"读-改-写",中断里也在改就可能互相覆盖;BSRR 单次写无竞争。

G7. 位带操作对 GPIO 有什么用?

  • 通过位带别名地址对 ODR/IDR 的单个 bit 原子置位/复位,一条总线操作完成,不用关中断、不用 BSRR。
  • 例:把 PA5 置位 → 写别名地址(0x4200 0000 起的映射区)1 字节 = 1。
  • 追问:F4 支持吗?→ 支持(Cortex-M3/M4 都带位带区)。

G8. GPIO 初始化前为什么必须先开时钟?(结合 RCC)

  • GPIO 挂在 APB2 总线上,时钟门控默认关闭;不开时钟则寄存器写不进、引脚不工作,严重时访问未使能外设触发 HardFault。
  • 顺序:开 GPIO 时钟 → 配模式/速度/上下拉 → (输出)写初值。
  • 追问:忘了开时钟的表现?→ 电平无变化、读值恒 0/恒定,排查第一步就是查 RCC。

G9. 按键长按 / 短按 / 双击怎么识别?(状态机思路)

  • 用状态机 + 定时器计数:空闲 → 按下消抖 → 按住计时 → 释放判定。
  • 长按阈值(如 1s)、双击窗口(如 300ms 内第二次按下)。
  • 追问:为什么用状态机不用多个 if?→ 状态机把"抖动/按下/按住/释放"离散化,抗抖、可扩展、可复用,是嵌入式面试加分点。

G10. 按键用轮询还是中断?

  • 轮询:主循环周期扫描 + 消抖,简单,但实时性差、可能漏按。
  • 中断(EXTI):按键按下立即响应,但抖动可能触发多次中断,仍需软件消抖;低功耗场景用中断唤醒更合适。
  • 追问:中断里能直接处理按键业务吗?→ 不行,中断里只置标志/计数,消抖和业务放主循环。

模块二 · RCC 与时钟树(6/6-1)R1–R12

R1. 为什么 STM32 要有时钟树?(目的题)

  • 三个目的:① 分频适配——内核/总线/外设各自有不同的最高频率,统一时钟必须分频;② 门控省电——不用的外设关时钟,动态功耗大降;③ 灵活选择时钟源(HSI/HSE/PLL)。
  • 追问:每个外设都开着时钟会怎样?→ 功耗上升、EMI 增加,与外设功能无关但影响系统稳定。

R2. 四个时钟源各自用途?(背表)

时钟源频率用途
HSI 8MHz RC 上电默认、省晶振低成本方案
HSE 4~16MHz 晶振(常用 8M) 高精度主时钟源
LSI 32kHz RC 独立看门狗 IWDG
LSE 32.768kHz 晶振 RTC 实时时钟

R3. 系统时钟怎么从 8MHz 变到 72MHz?(PLL 流程)

  • HSE 8MHz → PLL 输入(可选 /2)→ PLL 倍频 ×9 → SYSCLK = 72MHz → AHB /1 → APB2 /1、APB1 /2。
  • 追问:倍频系数怎么定?→ 目标 SYSCLK / PLL 输入频率;F1 上限 72MHz,超频要小心外设上限和 Flash 等待周期。

R4. APB1 和 APB2 有什么区别?为什么 APB1 要除 2?

  • APB2 最高 72MHz,挂高速外设(USART1、SPI1、GPIO、ADC、TIM1);
  • APB1 最高 36MHz,挂低速外设(USART2/3、I2C、SPI2、TIM2~7、DAC)。
  • 除 2 是为了总线功耗/带宽平衡——低速外设不需要 72MHz,还能省电。
  • 追问:外设挂在哪条总线是固定的吗?→ 是硬件固定的,查手册外设章节的"bus matrix/APB"归属;选型时影响你能跑多快(如 APB1 的串口波特率上限更低)。

R5. 定时器时钟为什么"APB1 分频 ≠1 时 ×2"?(高频陷阱)

  • 定时器需要更高时钟驱动计数,硬件设计:APB1 预分频 =1 时定时器时钟 = APB1;预分频 >1 时自动 ×2。
  • 例:APB1=36MHz 时,TIM2~7 的时钟实际是 72MHz——算定时器溢出/分频必须按 72MHz,否则定时时间差一倍。
  • 追问:这也是面试常出的计算题:APB1=36MHz、TIM3 分频 71、重载 999,多久中断一次?→ 72MHz/(71+1)=1MHz,1MHz 计数到 1000 → 1ms。

R6. 系统时钟配置流程(HAL 的 SystemClock_Config 做了什么)?

  • ① 开启 HSE → ② 配置 Flash 等待周期(72MHz 需 2WS)→ ③ 配置 PLL(HSE/1 ×9)→ ④ 使能 PLL 并等就绪 → ⑤ 选择 PLL 作 SYSCLK → ⑥ 配置 AHB/APB1/APB2 预分频。
  • 追问:顺序能不能乱?→ 不能:先有稳定时钟源和 Flash 等待,再切高速 PLL,否则跑飞/HardFault。

R7. 为什么串口/USB 必须用 HSE 而不是 HSI?

  • HSI 精度约 ±1%(温度漂移更大),9600 波特率误差要求 <2% 勉强可用,但 USB 要求 0.25% 以内——HSI 必然超差 → 必须 HSE 晶振。
  • 追问:HSI 能校准吗?→ 可以软件微调(HSITRIM),但温度漂移不可控,量产还是 HSE。

R8. 外设时钟使能(RCC->APB1ENR/APB2ENR/AHBENR)怎么记?

  • 一条总线一个使能寄存器,每个外设一个位:APB2ENR 管 USART1/SPI1/GPIO/TIM1/ADC,APB1ENR 管 USART2/3/I2C/SPI2/TIM2~7/DAC,AHBENR 管 DMA/Flash/SRAM。
  • 追问:时钟使能的意义?→ 门控电路,不开时钟外设寄存器"看不见",写无效。

R9. 什么是时钟安全系统 CSS?

  • 使能后:若 HSE 失效(晶振停振/引脚短路),自动切换回 HSI,并产生 NMI 中断通知软件。
  • 追问:CSS 触发后程序会怎样?→ 系统时钟降为 HSI 8MHz,外设频率全变(波特率乱),软件应在 NMI 里做安全处理(报警/降级)。

R10. 低功耗模式下时钟行为?

  • 睡眠 Sleep:内核时钟停,外设时钟照跑。
  • 停机 Stop:所有时钟停,靠 EXTI 等外部事件唤醒,唤醒后要重配时钟(常需重新初始化 PLL)。
  • 待机 Standby:全部断电,只能复位/唤醒唤醒脚。
  • 追问:停机模式唤醒后为什么波特率会乱?→ 唤醒后时钟源可能回到默认配置,串口按旧波特率配置对不上,需重新初始化时钟和外设。

R11. MCO 引脚是干什么的?

  • MCO 可以把内部时钟输出到引脚,供示波器测量/给外部器件供时钟;可输出 HSI/HSE/PLL/系统时钟分频。
  • 追问:调试时钟问题第一步?→ 量 MCO 输出,验证 PLL 是否真的锁到目标频率。

R12. 时钟相关面试计算题汇总

  • 算波特率:USART1 在 APB2=72MHz、USART2/3 在 APB1=36MHz(见串口篇 S2)。
  • 算定时器中断周期:注意 ×2 规则。
  • 算 SPI 波特率:SPI1 用 APB2=72MHz,SPI2 用 APB1=36MHz,再除预分频。
  • 算 ADC 采样率:ADC 时钟 ≤14MHz(APB2 再分频),采样周期 + 转换周期固定。

模块三 · USART 串口(课程角度补充)U1–U5

U1. 轮询模式收发数据(7-1)内部机制?

  • HAL_UART_Transmit:写 DR → 忙等 TXE → 写完等 TC;
  • HAL_UART_Receive:忙等 RXNE → 读 DR。全程 CPU 阻塞,靠超时(HAL_GetTick)防卡死。
  • 追问:轮询接收有什么致命缺点?→ 主循环被卡住,其他任务全部停摆;高波特率下极易 ORE 丢字节。

U2. printf 重定向 + 多文件工程(7-2)怎么组织代码?

  • uart.c/uart.h 封装:void UART1_Init(void)、int fputc(int ch, FILE *f);头文件放函数声明和 extern USART_HandleTypeDef huart1;主函数 include 头文件即可。
  • extern 的作用:跨文件访问全局变量声明;
  • static:文件内私有函数/变量。
  • 追问:为什么要多文件?→ 模块化、可复用、编译增量快、多人协作;面试常问 include 顺序和头文件守卫(#pragma once / #ifndef)。

U3. 重定向后 printf 底层怎么到串口的?

  • printf → vsnprintf 格式化 → 逐字符调 fputc → fputc 里等 TXE 写 DR(见 S10)。勾选 MicroLIB 是为了禁用半主机(否则 fputc 里会调 ARM 半主机机制卡死)。
  • 追问:重定向后还能用串口收吗?→ 能,重定向只改输出方向,接收不受影响。

U4. 轮询/中断/DMA 三模式怎么在工程里选?(课程 7-1 vs 10.1 对比)

  • 轮询:简单调试、单次收发;
  • 中断:命令交互、不阻塞主循环;
  • DMA:大数据块、高吞吐。
  • 追问:工程里通常怎么搭?→ 主循环轮询发送控制指令,中断/DMA 接收数据,前后台架构。

U5. 串口调试常遇 3 个问题速查(7-1 实战)

  • 收不到:时钟/引脚 AF 没配(S11);
  • 乱码:波特率或 APB 时钟算错(S2/S9);
  • 卡死:printf 半主机没禁(S10)。

模块四 · I2C(8/8-1/10.2)I1–I12

I1. I2C 是什么?物理层特点?(必考)

  • 两线:SCL(时钟)+ SDA(数据),开漏 + 外部上拉电阻,半双工,支持多主机。
  • 特点:地址寻址(7 位/10 位)、线与仲裁、速度分标准 100k / 快速 400k / 快速+ 1M。
  • 追问:为什么开漏?→ 才能线与,多设备共享总线不会短路;上拉电阻负责拉高。

I2. I2C 的时序:起始/停止/数据有效性/ACK 怎么说?(手撕题)

  • 起始:SCL 高时 SDA 拉低;
  • 停止:SCL 高时 SDA 拉高。
  • 数据有效:SCL 高电平期间 SDA 必须稳定;SCL 低时 SDA 才能变化。
  • ACK:第 9 个时钟,接收方拉低 SDA;主机收完最后字节发 NACK + 停止。
  • 追问:写全这四步是模拟 I2C 的手撕题,面试常让当场写起始/停止/发字节/收字节。

I3. I2C 地址机制?

  • 第一字节 = 7 位从机地址 + 1 位 R/W;
  • 器件地址 = 芯片固定地址 + 引脚配置位(如 A0/A1/A2)。
  • 追问:地址冲突怎么办?→ 器件 A0~A2 引脚改接,换地址;同地址只能错开使用。

I4. 一次完整读/写流程(以 EEPROM 为例)?

  • 写:起始 → 从机地址+W → ACK → 寄存器地址 → ACK → 数据 → ACK → 停止。
  • 读:起始 → 地址+W → ACK → 寄存器地址 → ACK → 重复起始 → 地址+R → ACK → 读数据 → NACK → 停止。
  • 追问:为什么读要"重发起始"而不是停止再开始?→ 防止总线被释放后其他主机插入,保证本次操作原子性。

I5. 阻塞模式 HAL 源码(8-1)内部是怎么实现的?

  • HAL_I2C_Master_Transmit:生成起始 → 发地址 → 轮询/中断等待各状态标志(TXIS/BTF/ADDR)→ 发数据 → 停止;每步带 HAL_GetTick 超时,超时返回 HAL_TIMEOUT。
  • 追问:HAL 阻塞模式真的是纯轮询吗?→ 内部是"中断状态机 + 超时轮询",工程上叫阻塞式调用,ISR 里还在推进状态机。

I6. I2C 总线卡死(SDA 一直被拉低)怎么恢复?

  • 原因:从机异常、主机时序错、上电时序、引脚被外部拉死。
  • 恢复手段:主机发 9~16 个 SCL 时钟脉冲,让卡住的从机释放 SDA;再发停止;严重时断电重启。
  • 追问:软件上怎么预防?→ 上电等从机就绪、软件重试、错误后重新初始化外设。

I7. 收到 NACK(无应答)怎么排查?

  • 顺序:地址对不对(器件手册)→ 器件上电没(量 VCC)→ 上拉电阻有没有(SDA 空闲是否高)→ SCL/SDA 有没有接反 → 从机是否被占用/卡死。
  • 追问:主机读最后一个字节为什么要发 NACK?→ 告诉从机"不再要了",从机释放总线,接着发停止。

I8. 上拉电阻怎么取值?

  • 折中速度与功耗:电阻小 → 上升沿快(可跑高速)但功耗大;电阻大 → 省电但边沿慢。
  • 常用:100kHz 用 4.7k10k,400kHz 用 2.2k4.7k(3.3V 下);器件多/容性大要更小。
  • 追问:漏选上拉会怎样?→ SDA 空闲电平不确定,通信完全失败或时好时坏。

I9. 硬件 I2C vs 模拟 I2C(IO 口模拟)怎么选?

  • 模拟:任意引脚、时序自己控、不受硬件 BUG 影响;代价是占 CPU、代码多。
  • 硬件:外设自动产生时序、可配 DMA/中断、省 CPU;但 F1 老型号硬件 I2C 有已知 errata(总线错误、卡死),很多教程宁可用模拟——面试可以提这点,展示看过勘误手册。
  • 追问:F4 之后硬件 I2C 稳定吗?→ 基本稳定,工程上硬件 I2C + 中断/DMA 是主流。

I10. I2C 中断模式收发(10.2)的事件中断有哪些?

  • 事件中断 EV(TXE/RXNE/ADDR/STOPF/BTF)+ 错误中断 ERR(BERR/ARLO/AF/OVR)。
  • HAL 回调:HAL_I2C_MasterRxCpltCallback / MasterTxCpltCallback / ErrorCallback;中断模式不阻塞主循环,配合状态机推进。
  • 追问:中断模式比阻塞好在哪?→ 主循环不被 I2C 等待卡住,可同时处理串口/按键/定时器;代价是状态机复杂度。

I11. 多个 I2C 从机挂一条总线,怎么互不干扰?

  • 靠地址区分:每次通信先发地址,只有地址匹配的从机应答;从机不匹配时保持高阻。
  • 追问:如果两个从机地址相同?→ 冲突,只能换地址引脚或分总线。

I12. I2C 面试手撕题:写模拟 I2C 起始条件(必背)

void I2C_Start(void) {
SDA_H(); SCL_H(); // 空闲:都高
delay(); SDA_L(); // SCL 高时 SDA 拉低 = 起始
delay(); SCL_L(); // 进入传输
}

  • 追问:停止条件?→ SCL 低时 SDA 拉低 → SCL 拉高 → SDA 拉高(SCL 高时 SDA 上升沿)。

模块五 · SPI(9/9-1/9-2/10.3)P1–P12

P1. SPI 四根线分别干什么?(必考)

  • SCK 时钟(主机产生)、MOSI 主机出从机入、
  • MISO 从机出主机入、CS/NSS 片选(低有效)。
  • 全双工、同步(时钟由主机给)、一主多从靠 CS 区分。
  • 追问:SPI 需要上拉电阻吗?→ 不需要(推挽驱动),这是与 I2C 最大的物理层区别。

P2. SPI 四种模式(CPOL/CPHA)怎么理解?Flash 常用哪个?

  • CPOL:空闲时时钟电平(0=低,1=高);
  • CPHA:采样沿(0=第一个沿,1=第二个沿)。
  • 模式 0:CPOL=0,CPHA=0(空闲低、上升沿采样);
  • 模式 3:CPOL=1,CPHA=1(空闲高、上升沿采样)。
  • Flash(W25Q 等)常用 MODE 0 / MODE 3,两者空闲电平相反但相位相同,都兼容。
  • 追问:模式不匹配会怎样?→ 主机采样点和从机数据输出时刻对不上,收到乱码/0xFF。

P3. 全双工意味着什么?为什么发送数据时同时会收到数据?

  • 移位寄存器一个时钟移出 1 bit、移入 1 bit → 发一字节的同时收一字节。
  • 经典技巧:想读从机数据,主机发 0xFF(哑字节) 推时钟。
  • 追问:这个特性带来什么坑?→ 只发不收也要处理 RXNE/读 DR,否则 FIFO 溢出丢数据。

P4. CS 片选的作用?多从机怎么挂?

  • CS 拉低选中从机;多从机各接一根 CS(主机多引脚),SCK/MOSI/MISO 共享。
  • 追问:CS 时序注意什么?→ 先拉低 CS 再发数据,发完等 BSY/TXE 清再拉高 CS,否则数据残缺;Flash 操作期间 CS 不能乱动。

P5. 阻塞式通信 HAL 源码(9-1)内部流程?

  • HAL_SPI_TransmitReceive:开 SPI → 循环:写 DR(发)→ 等 RXNE(收)→ 读完所有字节 → 等 BSY 清(移位寄存器发完)→ 返回。
  • 追问:为什么最后要等 BSY?→ BSY=1 表示还在移出最后一位,不等就关 SPI/拉高 CS 会丢尾字节(同串口 TC 的道理)。

P6. SPI Flash 驱动开发(9-2)必背命令与流程

操作命令码说明
读 ID 0x9F 返回 3~4 字节 JEDEC ID(识别芯片)
写使能 0x06 置 WEL 位,写/擦除前必须发
读状态寄存器 0x05 查 WIP 忙标志,忙时轮询等
读数据 0x03 任意地址连续读
页编程 0x02 一页最多 256 字节,跨页要拆
扇区擦除 0x20 4KB 粒度;写前必须先擦除
  • 追问 1:为什么写之前必须先擦除?→ Flash 编程只能把位从 1→0,擦除把所有位恢复为 1,才能再编程。
  • 追问 2:跨页写数据会怎样?→ 地址回绕到页首覆盖旧数据,必须按页切分。

P7. Flash 驱动开发的状态机/流程(面试怎么讲)?

  • 完整链路:上电读 ID 校验 → 写使能 → 擦除(轮询 WIP)→ 写使能 → 页编程(轮询 WIP)→ 读回校验。
  • 追问:擦除/编程失败怎么处理?→ 重试有限次、报错、掉电保护(先擦后写的掉电窗口是经典可靠性问题)。

P8. SPI vs I2C 对比(必背表)

维度SPII2C
线数 4(含 CS) 2
双工 全双工 半双工
速率 可达数十 MHz 100k~1M
寻址 片选 CS 地址帧
多主机 不支持 支持(仲裁)
驱动 推挽,无上拉 开漏 + 上拉
典型器件 Flash、SD、屏、ADC EEPROM、传感器、RTC
  • 什么时候选 SPI?→ 高速、大数据(Flash、屏);
  • 什么时候 I2C?→ 引脚少、低速、多设备总线(传感器集群)。

P9. SPI 中断模式收发(10.3)和 DMA 怎么结合?

  • 中断:TXE 空进中断发下一个字节、RXNE 满进中断收字节——每字节一次中断,适合中量数据。
  • 大块数据用 DMA:内存↔SPI_DR 硬件搬移,传输完成中断收尾(参考串口 DMA 思路,S13)。
  • 追问:SPI 收 DMA 和发 DMA 可以同时开吗?→ 可以,SPI 全双工天然支持双通道 DMA。

P10. SPI 收乱码常见原因排查?

  • 顺序:CPOL/CPHA 模式匹配 → MISO/MOSI 是否接反 → CS 时序(过早/过晚拉高)→ 时钟频率太高(从机上限)→ 上拉/走线干扰。
  • 追问:收 0xFF 特指什么?→ 从机没被正确选中(CS 问题)或 MISO 没接/悬空。

P11. SPI 时钟频率能随便设吗?

  • 上限受从机数据手册约束(W25Q 一般支持 100MHz+,但板级走线/主机外设上限先到);F1 SPI1 由 APB2=72MHz 分频。
  • 追问:高速 SPI 要注意什么?→ 走线短、阻抗一致、必要时降低速率换稳定性。

P12. SPI 手撕题:写 SPI 主机收发一字节(寄存器风格)

uint8_t SPI_SendByte(uint8_t byte) {
SPI1->DR = byte; // 发一个字节
while (!(SPI1->SR & SPI_SR_RXNE)); // 等收到(全双工必有回应)
return SPI1->DR; // 读回从机数据
}

  • 追问:为什么发一字节后一定能等到 RXNE?→ 全双工:发 8 bit 的同时收到 8 bit,DR 必填。

模块六 · 中断与异常(10/10.1~10.4)E1–E10

E1. 弱引用 __weak / WEAK 是什么?有什么作用?(10.4 必考)

  • 链接器符号弱定义:用户定义同名强符号时,覆盖弱符号;没有强符号则用弱实现兜底。
  • 典型:HAL 库所有回调都是 __weak 默认空实现(HAL_UART_RxCpltCallback 等),用户在任意源文件写同名函数即可生效,无需改库源码。
  • 追问 1:两个文件都定义了同名强符号会怎样?→ 链接错误(重复定义);weak 就不会。
  • 追问 2:为什么不直接给回调写死实现?→ 库与用户代码解耦,用户想用就重写、不想用就留空,HAL 可移植性来源之一。
  • 追问 3:自己项目里能用吗?→ 能,写驱动默认处理 + 让应用层可覆盖(回调模式)。

E2. 中断模式收发 vs 阻塞模式收发,本质区别?(10.1 vs 7-1)

  • 阻塞:调用线程/主循环一直等到收发完才返回,期间啥也干不了。
  • 中断:发起后立即返回,字节到了 ISR 处理/置标志,主循环继续跑——前后台架构的核心。
  • 追问:中断模式会不会漏数据?→ 会,ISR 必须及时,且接收缓冲/处理要跟上;高负载时改用 DMA。

E3. 为什么"中断 + 主循环"叫前后台架构?

  • 前台 = 中断(突发事件立即响应);
  • 后台 = 主循环(轮询处理累积任务)。
  • 两者通过标志位/队列通信。
  • 追问:这套架构的瓶颈?→ 后台任务多时轮询周期变长,实时性差 → 才需要 RTOS。

E4. USART 中断模式(10.1)中断里该做什么?

  • 收:读 SR 判断 RXNE → 读 DR 存缓冲/环形队列 → 清标志;
  • 发:TXE 空 → 写 DR 发下一字节,发完等 TC。
  • 追问:中断和主循环之间用什么传数据?→ 环形缓冲(S16),中断只写索引、主循环只读索引。

E5. I2C 中断模式(10.2)和 SPI 中断模式(10.3)的状态机为什么复杂?

  • 一次 I2C 读可能要经历:起始 → 地址+W → ACK → 寄存器地址 →ACK → 重复起始 → 地址+R → ACK → 收数据 → NACK → 停止,每一步都是独立中断事件,ISR 里要用状态机推进。
  • SPI 相对简单:TXE/RXNE 两个事件,逐字节推进。
  • 追问:怎么避免状态机写乱?→ 用枚举状态 + 每个事件一个 case,回调里只改状态和搬数据。

E6. 中断优先级总原则:串口/I2C/SPI/定时器/按键怎么排?(综合题)

  • 原则:数据易丢 > 控制实时性 > 非实时任务;同抢占级用子优先级排序。
  • 推荐示例:串口接收/定时器控制(高抢占)→ I2C/SPI 通信(中)→ 按键/低优先级事件(低);PendSV 最低。

E7. 中断与 DMA 协作的总框架?

  • 规则:逐字节/小数据用中断,大块数据用 DMA + 完成中断;DMA 完成中断里置"数据就绪"标志,主循环消费。
  • 追问:DMA 和中断能同时用于同一外设吗?→ 能,分工不同(DMA 搬数据、中断处理错误/IDLE/完成)。

E8. 中断里常见的 3 个"死锁"反例?

  • ① ISR 里调 HAL_Delay(等 SysTick,但 SysTick 优先级低进不来)→ 卡死;
  • ② ISR 里 printf(串口中断自己等自己)→ 卡死;
  • ③ ISR 里关中断忘了开/临界区过长 → 系统失聪。
  • 追问:怎么预防?→ ISR 只置标志 + 搬数据;调用的任何函数必须可重入。

E9. 弱引用回调的实战:怎么写一个"用户可覆盖"的驱动?

// 驱动库(弱实现)
__weak void OnDataReady(uint8_t *buf, uint16_t len) { }
// 用户工程(强符号,覆盖库默认)
void OnDataReady(uint8_t *buf, uint16_t len) {
/* 用户自己的处理 */
}

  • 追问:工程里回调名冲突怎么办?→ 加前缀/命名空间风格(如 APP_OnDataReady),或用函数指针注册。

E10. 中断相关面试计算:一个 921600 波特率的串口,中断模式 CPU 占用多大?

  • 921600 bps ≈ 92,160 字节/秒(10bit/帧)→ 约 10.7µs 一个字节;若 ISR 处理+压栈出栈花 23µs,CPU 占用约 **2030%**,还有定时器/其他中断 → 明显不够用 → 这就是必须上 DMA 的量化理由。

综合实战题(面试压轴)X1–X3

X1. 设计题:用 STM32 做一个温湿度采集上报系统,怎么分配中断与轮询?

  • 方案:I2C 读传感器(中断/DMA,不阻塞)→ 数据在回调里入环形队列 → 主循环处理 → 串口定时上报(轮询发送简单场景/DMA 大场景)→ 按键改配置(EXTI 中断 + 消抖)→ LED 状态指示(GPIO 推挽)。
  • 考点:模块化、中断职责划分、缓冲设计、时钟/波特率正确性。

X2. 现场手撕:模拟 I2C 完整收发一字节

  • 写:Start → SendByte(addr|W) → 等 ACK → SendByte(reg) → 等 ACK → 数据 → 等 ACK → Stop;读再加 RepeatStart 换 R。
  • 考点:时序四步(起始/停止/数据有效/ACK)+ 模拟位操作(位带或直接 IO)。

X3. 故障题:上电后串口打印乱码、LED 不亮、I2C 读传感器全是 0xFF,怎么排查?

  • 先查时钟:HSE 有没有起振、PLL 有没有锁、APB 分频对没对(一票否决,全挂)。
  • 再查 GPIO:时钟开了没、模式/复用配了没。
  • 再逐个外设:波特率算对没、地址/上拉/时序对没。
  • 考点:先系统后外设的排查顺序,嵌入式工程师的基本素养。


三大串行通信协议面试题库(USART / I2C / SPI · 51 题)

一、USART 串口篇(U1–U17)

U1. UART 和 USART 有什么区别?

  • USART 支持同步模式(带 CK 时钟引脚,可当 SPI 主机)、异步、单线半双工、LIN、智能卡、IrDA、Modbus;UART 只有异步。
  • STM32F1:USART1/2/3 是完整 USART,UART4/5 只是 UART(无 CK 引脚)。

U2. 波特率怎么算?误差要求?

  • 公式:USARTDIV = fck / (16 × Baud),结果存 BRR(高 12 位整数 + 低 4 位小数)。
  • 例:72MHz、9600 → USARTDIV=468.75 → BRR=0x1D4C,正好 9600。
  • 误差 < 2%(硬件 3/16 位采样容差);HSI 精度 ±1% 可能叠加到危险区,高精度必须 HSE。
  • 8N1 一帧 10 bit → 9600bps ≈ 960 字节/秒。
  • 常见坑:USART2/3 在 APB1=36MHz,拿 72MHz 算必乱码。

U3. 帧格式有哪些参数?

  • 起始位(低 1bit) → 数据位(8/9, LSB 先发) → 校验(无/奇/偶) → 停止位(0.5/1/1.5/2)。经典 8N1。
  • 校验只能检单 bit 错,不能纠错;用校验时数据位设 9。

U4. 轮询 / 中断 / DMA 三种收发方式对比?

方式CPU 占用实时性适用
轮询 忙等 100% 阻塞 调试、低速少量
中断 每字节进出 ISR 及时 命令交互、不定长小包
DMA 搬运不占 CPU 整块收完才知 高速大块、不定长+IDLE

U5. IDLE 空闲中断 + DMA 不定长接收流程?(必考)

  • ① DMA 循环模式收进缓冲 → ② 开 IDLE 中断 → ③ 一帧收完总线空闲触发 IDLE → ④ 先读 SR 再读 DR 清标志 → ⑤ len = BUFSZ − DMA_GetCounter() → ⑥ 置帧完成标志 → ⑦ 关 DMA 重置 CNDTR 继续收。

void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_IDLE) {
USART1->SR; USART1->DR; // 清 IDLE
uint16_t len = RX_BUF_SIZE – DMA1_Channel5->CNDTR;
frame_ready = 1;
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA1_Channel5->CNDTR = RX_BUF_SIZE; // 关闭状态才能写
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}


U6. TXE 和 TC 有什么区别?

  • TXE = DR 空,可写下一字节;
  • TC = 移位寄存器也发完了,字节真正上引脚。
  • 只等 TXE 就关串口/切 485 方向 → 最后一字节丢失;收尾必须等 TC。

U7. ORE 溢出错误怎么处理?

  • 原因:RXNE=1 未读走时新字节到达 → 丢数据。
  • 处理:读 SR 再读 DR 清标志;中断处理要快或改 DMA。
  • 同类标志:FE(帧错)、NE(噪声)、PE(校验错),清除方式相同(读 SR+DR)。

U8. 为什么串口中断里"先读 SR 再读 DR"?

  • RXNE/IDLE/ORE 的清除条件是"读 SR + 读 DR"组合动作;
  • 顺序反了会误清/清不掉 → 反复进中断。

U9. printf 重定向怎么做?

  • 重写 fputc(等 TXE → 写 DR);Keil 勾 Use MicroLIB 或 GCC 加 -specs=nosys.specs 禁半主机,否则卡死。
  • ISR 里禁止 printf(可能和串口中断互相等待死锁);RTOS 下需加锁。

U10. 乱码 / 收 0xFF 怎么排查?

  • 波特率一致?时钟源/APB 分频算对?2. 帧格式一致?3. TX↔RX 交叉、共地?4. 电平标准匹配(TTL/RS232/RS485)?5. 引脚 AF 配好、串口时钟开了?6. 示波器量空闲电平(应为高)+ 帧宽。
    • 收 0xFF 常见:RX 悬空、接线没接、电平不匹配。

    U11. 引脚配置:F1 重映射 vs F4 复用?

    • F1:复用推挽 + AFIO 重映射(USART1 可 PA9/10 或 PB6/7);
    • F4:GPIO_AF7_USART1 选复用功能,无 AFIO。
    • 时钟:USART1 在 APB2,USART2/3 在 APB1;初始化顺序:开时钟 → 配引脚 → 配串口。

    U12. 多字节协议怎么解析?(粘包/拆包)

    • 状态机:帧头(AA 55) → 长度 → 数据 → 校验(CRC16/和校验),状态错乱回帧头。
    • 无长度字段时:定长帧 / 帧尾 / 空闲超时判定。

    U13. 环形缓冲怎么实现?(手撕)

    #define RB_SIZE 256 // 2 的幂才能用 &
    typedef struct { uint8_t buf[RB_SIZE]; volatile uint16_t head, tail; } ring_t;
    // 空:head==tail;满:(head+1)&mask == tail(浪费一格法)
    void rb_put(ring_t *r, uint8_t d) {
    uint16_t n = (r->head + 1) & (RB_SIZE – 1);
    if (n != r->tail) { r->buf[r->head] = d; r->head = n; }
    }
    int rb_get(ring_t *r, uint8_t *d) {
    if (r->head == r->tail) return 0;
    *d = r->buf[r->tail]; r->tail = (r->tail + 1) & (RB_SIZE – 1); return 1;
    }


    U14. RS485 方向控制注意什么?

    • DE/RE 引脚:发 DE=1,收 DE=0;发送完必须等 TC 再拉低 DE,否则尾字节丢。
    • 485 半双工,自己发时会收到自己(回环),不能边发边收。

    U15. 全双工/半双工/单线模式?

    • 全双工:TX/RX 独立;
    • 单线半双工:复用一个引脚(1.5 停止位);USART 同步模式可输出 CK 当 SPI 用。

    U16. 串口 DMA 配置要点?

    • 方向、外设地址 &USARTx->DR、数据宽度 8 位、发送 normal / 接收 circular、发送完成用 TC 收尾。
    • 大缓冲建议 4 字节对齐(32 位宽度时必需)。

    U17. 串口高频追问速答

    • 中断会丢数据吗?→ 会,ISR 不及时 → ORE。
    • 一帧最多传多少?→ 协议无上限,受缓冲/超时约束。
    • 怎么知道一帧收完了?→ IDLE / 长度字段 / 帧尾 / 超时。

    二、I2C 篇(I1–I17)

    I1. I2C 物理层特点?

    • 两线:SCL + SDA;开漏 + 外部上拉;半双工;支持多主机。
    • 速度:标准 100k / 快速 400k / 快速+ 1M。
    • 追问:为什么开漏?→ 才能线与,多设备共享总线不短路;上拉负责拉高。

    I2. 时序四件事:起始/停止/数据有效/ACK?(手撕基础)

    • 起始:SCL 高时 SDA 拉低;
    • 停止:SCL 高时 SDA 拉高。
    • 数据有效:SCL 高期间 SDA 必须稳定,SCL 低时才允许变化。
    • ACK:第 9 个时钟,接收方拉低 SDA;主机收完最后字节回 NACK 再停止。

    I3. 地址机制?

    • 第一字节 = 7 位从机地址 + 1 位 R/W;
    • 器件地址 = 芯片固定位 + 引脚配置位(A0~A2)。
    • 同地址冲突:改引脚或分总线。

    I4. 完整读/写流程(EEPROM 例)?

    • 写:起始 → 地址+W → ACK → 寄存器地址(2B) → ACK → 数据 → ACK → 停止。
    • 读:起始 → 地址+W → 寄存器地址 → ACK → 重复起始 → 地址+R → ACK → 收数据(最后 NACK) → 停止。

    I5. 为什么读要"重复起始"而不是停止再开始?

    • 保持本次操作原子性,防止停止后总线被其他主机插队,寄存器指针状态被打断。

    I6. 阻塞模式 HAL 源码内部怎么走?

    • HAL_I2C_Mem_Read:发起始+地址(等 TXIS)→ 发寄存器地址(等 ACK)→ 重复起始 → 地址+R → 逐字节收(前 N−1 回 ACK,最后 NACK)→ 停止;全程 HAL_GetTick 超时。
    • 本质是中断状态机 + 外层忙等的阻塞调用。

    I7. 总线卡死(SDA 被拉低)怎么恢复?

    for (int i = 0; i < 9; i++)
    { SCL_H(); delay(); SCL_L(); delay(); } // 9 个脉冲
    I2C_Stop(); // 补停止

    • 原理:9 个时钟强制推进从机内部状态,让它释放 SDA。

    I8. 收不到 ACK(NACK)怎么排查?

    • 地址对不对(手册)→ 器件上电没 → 上拉电阻有没有(SDA 空闲是否高)→ SCL/SDA 接反 → 从机卡死。

    I9. 上拉电阻怎么取值?

    • 电阻小 → 边沿快可跑高速,但功耗大;大 → 省电但边沿慢。
    • 3.3V:100k 用 4.7k10k,400k 用 2.2k4.7k;器件多/容性大要更小。

    I10. 硬件 I2C vs 模拟 I2C?

    • 模拟:任意引脚、时序可控、避开 F1 硬件 I2C 的已知 errata,占 CPU。
    • 硬件:自动时序、可 DMA/中断、省 CPU;F4 后基本稳定。
    • 面试点:能说出 F1 硬件 I2C 有勘误(卡死/总线错误)是加分。

    I11. I2C 中断模式为什么要有状态机?(10.2 深度)

    • 一次读有 6+ 个事件(起始/地址 ACK/寄存器/重复起始/地址 R/数据/NACK/停止),每个事件一次 ISR,调用早返回 → 必须用枚举状态记录"走到哪一步"。

    typedef enum { ST_START, ST_ADDR_W, ST_WAIT_ACK1, ST_SEND_REG,
    ST_REPEAT_START, ST_ADDR_R, ST_READ, ST_SEND_NACK, ST_STOP } i2c_state_t;


    I12. 多主机仲裁谁赢?

    • 同发 1 没事;发 1 的主机遇到对方发 0 → 丢仲裁退出(开漏线与),总线归发 0 者,数据不损坏。

    I13. 模拟 I2C 手撕:Start/SendByte/ReadByte/ACK

    void I2C_Start(void) { SDA_H(); SCL_H(); delay(); SDA_L(); delay(); SCL_L(); }
    void I2C_Stop(void) { SDA_L(); SCL_H(); delay(); SDA_H(); delay(); }
    void I2C_SendByte(uint8_t b) {
    for (int i = 7; i >= 0; i–) {
    if (b & (1 << i)) SDA_H(); else SDA_L();
    SCL_H(); delay(); SCL_L(); delay(); // SCL 高时 SDA 稳定
    }
    }
    uint8_t I2C_ReadByte(void) {
    uint8_t d = 0; SDA_H(); // 释放 SDA 让从机驱动
    for (int i = 7; i >= 0; i–) {
    SCL_H(); delay();
    if (SDA_READ()) d |= (1 << i);
    SCL_L(); delay();
    }
    return d;
    }
    uint8_t I2C_CheckACK(void) { SDA_H(); SCL_H(); delay(); uint8_t a = !SDA_READ(); SCL_L(); return a; }


    I14. EEPROM 读一字节手撕(重复起始 + NACK 全在)

    uint8_t EEPROM_ReadByte(uint16_t addr) {
    uint8_t d;
    I2C_Start();
    I2C_SendByte(0xA0); I2C_CheckACK(); // 地址+W
    I2C_SendByte(addr >> 8); I2C_CheckACK();
    I2C_SendByte(addr & 0xFF); I2C_CheckACK();
    I2C_Start(); // 重复起始
    I2C_SendByte(0xA1); I2C_CheckACK(); // 地址+R
    d = I2C_ReadByte();
    I2C_SendACK(0); // 最后一字节 NACK
    I2C_Stop();
    return d;
    }


    I15. I2C 常见坑速查

    • 漏上拉电阻 → 通信全失败;
    • SDA 在 SCL 高时变了 → 从机误判成起始/停止;
    • 多字节读忘 NACK → 从机继续发,总线乱;
    • ISR 里做数据处理 → 拖死其他中断。

    I16. 为什么 ISR 里不处理 I2C 数据?

    • I2C 中断密集(每字节至少一次),业务逻辑放 ISR 会阻塞其他中断;
    • 回调只搬缓冲 + 置标志。

    I17. I2C 高频追问速答

    • I2C 能像 SPI 一样全双工吗?→ 不能,半双工。
    • 没有上拉会怎样?→ 电平不确定,通信失败。
    • 从机怎么知道自己被选中?→ 地址匹配后应答 ACK。

    三、SPI 篇(P1–P17)

    P1. SPI 四根线各干什么?

    • SCK(主机给时钟)、MOSI(主机出从机入)、
    • MISO(从机出主机入)、CS(片选,低有效)。
    • 全双工、同步;多从机靠 CS 区分,无需上拉电阻(推挽驱动)。

    P2. 四种模式(CPOL/CPHA)?Flash 常用哪个?

    • CPOL:空闲时钟电平;
    • CPHA:采样沿(1=第一个沿,0=第二个沿)。
    • MODE0(CPOL=0,CPHA=0)和 MODE3(CPOL=1,CPHA=1):都是第一个沿采样,只差空闲电平,Flash 两者都兼容。
    • 采样点错了(MODE1/2)→ 一定乱码。

    P3. 全双工意味着什么?哑字节技巧?

    • 发 1 字节的同时收 1 字节(移位寄存器进一出)。
    • 想读从机 → 主机发 0xFF 哑字节推时钟;只发不收也要处理 RXNE,否则溢出。

    P4. CS 片选时序注意?

    • 先拉低 CS 再发数据,发完等 BSY 清再拉高,否则尾字节残缺。

    P5. 阻塞式 HAL 流程 + 为什么等 BSY?

    • HAL_SPI_TransmitReceive:写 DR → 等 RXNE 收 → 全部发完 → 等 BSY 清(移位寄存器移完)→ 返回。
    • 同串口 TC 的道理:DR 空 ≠ 发完。

    P6. SPI Flash 命令表(必背)

    命令码说明
    读 ID 0x9F 返回 JEDEC ID
    写使能 0x06 写/擦除前必须发(WEL 自动清)
    读状态 0x05 轮询 WIP 忙标志
    读数据 0x03 任意地址连续读
    页编程 0x02 一页 256 字节,跨页会回绕
    扇区擦除 0x20 4KB 粒度,写前必须擦

    P7. 为什么写之前必须先擦除?

    • Flash 编程只能把位 1→0;擦除把所有位恢复为 1 后才能再编程。

    P8. 跨页写怎么处理?(手撕)

    while (len) {
    Flash_WriteEnable();
    uint32_t page_left = PAGE_SIZE – (addr % PAGE_SIZE); // 本页剩余
    uint32_t chunk = (len < page_left) ? len : page_left; // 最多写到页尾
    // 发 0x02 + 24 位地址 + chunk 字节
    Flash_WaitBusy(); // 轮询 WIP
    addr += chunk; data += chunk; len -= chunk;
    }


    P9. 读 ID 时序手撕

    SPI_CS_LOW();
    SPI_SendByte(0x9F);
    id[0] = SPI_SendByte(0xFF); // 哑字节推时钟,读回
    id[1] = SPI_SendByte(0xFF);
    id[2] = SPI_SendByte(0xFF);
    SPI_CS_HIGH();

    • 读到全 0xFF → CS 没选中 / MISO 悬空 / 命令不支持。

    P10. 硬件 NSS vs 软件 CS?

    • 硬件 NSS 时序受外设状态机约束、多从机切换麻烦;
    • 软件 CS 任意 GPIO 手动控制,工程上 90% 用它。

    P11. SPI DMA 怎么配?

    • 大块数据(Flash 页编程、刷屏)必须 DMA;
    • 全双工同传用 HAL_SPI_TransmitReceive_DMA。
    • 小数据轮询、中量中断、大块 DMA(和串口同一套选型逻辑)。

    P12. SPI 中断模式(10.3)怎么工作?

    • TXE 空中断发下一字节、RXNE 满中断收字节,逐字节推进;比阻塞省 CPU,比 DMA 代码简单。

    P13. SPI 收乱码排查顺序?

    • 模式(CPOL/CPHA)匹配 → MISO/MOSI 接反 → CS 时序 → 时钟频率超从机上限 → 走线干扰。

    P14. SPI 时钟频率能随便设?

    • 受从机上限(W25Q 支持很高)和主机外设限制(F1 SPI1 由 APB2=72MHz 分频);高速注意走线。

    P15. 手撕:SPI 主机收发一字节

    uint8_t SPI_SendByte(uint8_t byte) {
    SPI1->DR = byte;
    while (!(SPI1->SR & SPI_SR_RXNE)); // 全双工必有回应
    return SPI1->DR;
    }

    P16. SPI vs I2C 对比表

    维度SPII2C
    线数 4(含 CS) 2
    双工 全双工 半双工
    速率 数十 MHz 100k~1M
    寻址 CS 片选 7/10 位地址
    多主机 不支持 支持仲裁
    驱动 推挽 开漏+上拉
    典型 Flash/SD/屏/ADC EEPROM/传感器/RTC

    P17. SPI 高频追问速答

    • SPI 有波特率误差问题吗?→ 没有,同步时钟由主机给。
    • 从机多怎么接?→ 共享 SCK/MOSI/MISO,各接一根 CS。
    • 发 0xFF 是干嘛?→ 提供时钟推数据(读操作)。

    四、三协议综合对比题(C1–C6)

    C1. 完整对比表(终极版)

    维度USARTI2CSPI
    线数 2~3 2 4
    双工 全 半 全
    同步性 异步(各自时钟) 同步 同步
    速率 常见 ≤115200,上限 fck/16 100k~1M 数十 MHz
    寻址 无 地址帧 CS 片选
    驱动 TTL 推挽/RS232/485 开漏+上拉 推挽
    多主机 无 有(仲裁) 无
    典型器件 GPS/蓝牙/上位机 EEPROM/传感器/RTC Flash/SD/屏/ADC

    C2. 选型题:温湿度传感器、GPS、SPI Flash、RTC、屏幕分别用什么?

    • 温湿度(SHT30 等 I2C)、GPS(USART 输出 NMEA)、Flash(SPI 高速大块)、RTC(I2C,如 DS3231)、屏幕(SPI 刷屏快)。

    C3. 同步 vs 异步的核心区别(为什么只有 USART 有"波特率对齐"问题)?

    • 异步没有时钟线,双方各自计时 → 必须约定并校准波特率,误差 <2% 才稳;
    • 同步由主机提供时钟,从机跟着沿走 → 不存在波特率误差,但主机时钟上限决定速率。

    C4. 综合设计题:一个采集系统(SPI Flash 存数据 + I2C 读传感器 + USART 上报),中断优先级怎么排?

    • 数据易丢优先:USART 接收/DMA 完成(高)→ I2C 事件(中)→ SPI 传输完成(中)→ 业务处理(低);共用缓冲必须加环形队列 + volatile。

    C5. 一句话说出每个协议的"致命弱点"?

    • USART:异步速率受波特率误差/APB 时钟限制,点对点不能挂多设备;
    • I2C:半双工 + 开漏速率上不去,总线卡死要恢复;
    • SPI:线多、无地址(从机多要 CS 引脚多)、不能多主机。

    C6. 手撕综合:一个系统里三协议各自初始化顺序?

    • 先时钟(RCC)→ GPIO 复用/开漏配置 → 各自外设初始化(USART 波特率、I2C 时序、SPI 模式/分频)→ 中断优先级 → 使能;
    • 顺序错了:没开时钟先配外设 = 寄存器写入无效。

    三大协议速记卡

    考点USARTI2CSPI
    一句话本质 异步点对点 开漏总线 + 地址 同步全双工 + 片选
    最常考计算 波特率公式/误差 上拉取值 CPOL/CPHA 模式
    最常考代码 IDLE+DMA 收 模拟 I2C 收发 Flash 跨页写
    最常考陷阱 TXE≠TC 读要重复起始+最后 NACK 发完等 BSY、先擦后写
    最高频追问 轮询/中断/DMA 总线卡死恢复 SPI vs I2C 选型

      STM32 面试深水区题库(代码级 · 原理级 · 计算级 · 31 题)

    深挖一 · 中断/异常底层(D1–D8)

    D1. 硬件压栈到底压了哪些寄存器?顺序如何?(现场还原题)

    • 进入异常时硬件自动压入 8 个 32 位寄存器(SP 减 32 字节):xPSR、PC、LR、R12、R3、R2、R1、R0。
    • 压栈顺序是"先入高地址再入低地址",PC 里保存的是被中断指令的下一条(返回地址)。
    • M4F(带 FPU):默认惰性压栈(Lazy Stacking)——只有 ISR 真用了浮点寄存器,才压 FPSCR + S0~S15,省掉大部分中断的时间开销。
    • 追问:为什么 R4~R11 不压?→ 由编译器在 ISR 开头压(软压栈),且只有用到的才压,节省时间。

    D2. EXC_RETURN 三个值的完整含义?调试时怎么用?

    值含义
    0xFFFFFFF1 返回 Handler 模式(嵌套中断返回)
    0xFFFFFFF9 返回线程模式 + 主栈 MSP(裸机主程序)
    0xFFFFFFFD 返回线程模式 + 进程栈 PSP(RTOS 任务)
    • 用法:断点进 HardFault 后看 LR,0xFFFFFFFD → 故障发生在任务栈,多半是 RTOS 任务栈溢出/越界;0xFFFFFFF9 → 主栈(裸机栈溢出)。
    • 追问:为什么这些值以 0xFFFFFFF 开头?→ 高 28 位全 1 是 Cortex-M 保留的特殊地址段,CPU 识别为"异常返回指令"而非普通跳转地址。

    D3. FreeRTOS 上下文切换的汇编级思路?(必考加分题)

    • SysTick ISR:判定需要切换 → 触发 PendSV(挂起,不立即执行)。
    • PendSV ISR(vPortSVCHandler/xPortPendSVHandler):
    • 关中断(BASEPRI/PRIMASK,防切换过程被打断);
    • 软件保存 R4~R11 到当前任务栈(R0~R3/PC/LR/xPSR 已在进异常时被硬件压栈);
    • 更新当前任务控制块的 SP 指针;
    • 取新任务 SP,PSP = 新SP;
    • 从新任务栈恢复 R4~R11;
    • 开中断,BX LR(0xFFFFFFFD → 以 PSP 出栈恢复新任务现场)。
    • 追问:为什么 R4R11 由软件保存而 R0R3 不用?→ R0R3/PC/LR/xPSR 硬件已经压栈了,只差 R4R11 这 8 个"callee-saved",软件补压即可——这就是硬件压栈设计省软件代码的关键。

    D4. HardFault 实战:数组越界写坏了相邻变量,怎么一步步定位?(实操题)

    • 步骤:
    • 断点停进 HardFault_Handler;
    • 看 LR 的 EXC_RETURN 定 MSP/PSP(裸机还是任务);
    • 找到对应栈指针,读出压栈区的 PC(出问题指令的下一条)和 R0~R3(当时参数);
    • 在反汇编窗口定位 PC 指令,看它写/读了哪个地址;
    • 对照故障状态寄存器:CFSR 的 MMFSR/BFSR/UFSR 分别指向内存管理/总线/用法错误,BFAR/MMFAR 给出故障地址——地址明显异常(如 0xDEADBEEF、越界数组尾)就锁定根因;
    • 回代码查对应数组下标/指针运算。
    • 追问:栈溢出怎么主动检测?→ 启动时把栈区填满 0xCC,周期性检查栈底标记是否被冲掉;或 RTOS 任务栈同样手法;FreeRTOS 有 configCHECK_FOR_STACK_OVERFLOW 钩子。

    D5. 临界区题:主循环 counter++、中断里读 counter,不加保护会怎样?怎么修?

    • 会怎样:counter++ 是"读-加-写"三条指令,中断正好打在中间 → 读到错位值(老值/中间值)。
    • 修:volatile(保证不缓存)+ 读时关中断(保证读-改写原子):

    volatile uint32_t counter;
    uint32_t safe_read(void) {
    __disable_irq();
    uint32_t v = counter;
    __enable_irq();
    return v;
    }

    • 追问:为什么位带救不了计数器?→ 位带只对单个 bit 原子,32 位计数器是多 bit 读改写,关中断才是正确解。

    D6. 位带计算题:SRAM 变量 0x20000000 的 bit2,别名地址是多少?

    • 公式:别名地址 = 别名基址 + (目标地址−位带基址)×32 + bit×4
    • SRAM:0x2000 0000 → 0x2200 0000;
    • 代入:0x22000000 + 0×32 + 2×4 = 0x22000008。
    • 追问:对 0x22000008 写 1 等价于什么?→ 原子地把 0x20000000 的 bit2 置 1,单总线操作,不关中断。

    D7. SysTick 计算题:72MHz 下 1ms 节拍,LOAD 填多少?

    • LOAD = 时钟/节拍频率 − 1 = 72000000/1000 − 1 = 71999 = 0x1193F。
    • 追问:如果系统切到 HSI 8MHz(不分频),重算?→ 8000−1 = 7999;忘了改会导致节拍变快/变慢(时间全错),这正是 R9 里"HSE 失效切 HSI 后定时全乱"的量化来源。

    D8. 场景题:A 中断(抢占1) 和 B 中断(抢占1) 同时来,谁先?如果 A 先执行完之前 B 又来?

    • 抢占相同 → 比子优先级 → 再相同 → 比中断号(编号小先)。
    • 抢占相同不能嵌套:B 只能等 A 执行完;但若 B 抢占更高(如 0),会立即打断 A(嵌套)。
    • 追问:这就是为什么"抢占优先级决定实时上限,子优先级只决定排队顺序"。

    深挖二 · USART 代码级(U6–U11)

    U6. IDLE + DMA 不定长接收:完整初始化 + 中断代码(手撕题)

    // 初始化:DMA1_Channel5 循环模式收 USART1_RX
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buf;
    DMA_InitStructure.DMA_BufferSize = RX_BUF_SIZE;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 关键:循环
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel5, ENABLE);
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 空闲中断

    void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
    USART1->SR; USART1->DR; // 清 IDLE:读 SR+DR
    uint16_t len = RX_BUF_SIZE – DMA_GetCurrDataCounter(DMA1_Channel5);
    frame_ready = 1; // 主循环按 len 解析 rx_buf
    DMA_Cmd(DMA1_Channel5, DISABLE); // 重置前必须关 DMA
    DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE);
    DMA_Cmd(DMA1_Channel5, ENABLE);
    }
    }

    • 追问 1:为什么重置计数前要 DISABLE?→ CNDTR 只能在 DMA 关闭时写入,否则写入无效。
    • 追问 2:两帧连发会怎样?→ 第一帧 IDLE 中断后缓冲区还没被消费,第二帧 DMA 又开始覆盖写 → 必须保证主循环消费速度 > 接收速度,或换双缓冲。

    U7. 大数据连续接收怎么不丢?(双缓冲 + 半满中断方案)

    • 两个 BUFSZ 缓冲区轮换:DMA 收满 A 时(传输完成中断)→ 立刻切到 B 收 → 主循环慢慢消费 A;B 满再切回。
    • 或单缓冲 + 半满中断:缓冲前半满时中断通知搬走前半,DMA 继续收后半,搬完再等。
    • 追问:这本质是什么?→ 生产者(DMA)与消费者(主循环)解耦,双缓冲保证"边收边处理",是大流量串口/SPI 收发的标准架构。

    U8. 波特率误差计算题:APB1=36MHz 跑 115200,误差多大?

    • USARTDIV = 36e6/(16×115200) = 19.53125 → 取整数 19 + 小数 0.53125×16 ≈ 8/16 → 实际 = 36e6/(16×19.5) = 115384.6。
    • 误差 = |115200 − 115384.6|/115200 ≈ 0.16% < 2%,可用。
    • 追问:为什么 36MHz 的 APB1 跑 115200 也能过?→ 采样容差 3/16 位 ≈ 1.875%,0.16% 远低于阈值;但如果用 HSI 8MHz 且精度 ±1%,累积误差就危险了。

    U9. 发送中断完整流程(TXE 中断 + TC 收尾)

    void UART_Send_Start(uint8_t *data, uint16_t len) { // 主循环调用
    tx_idx = 0; tx_len = len; tx_buf = data;
    USART_ITConfig(USART1, USART_IT_TXE, ENABLE); // 开 TXE 中断
    }
    void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_TXE)) {
    if (tx_idx < tx_len) { USART1->DR = tx_buf[tx_idx++]; }
    else { USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
    USART_ITConfig(USART1, USART_IT_TC, ENABLE); } // 转等 TC
    }
    if (USART_GetITStatus(USART1, USART_IT_TC)) { // 全部发完
    USART_ITConfig(USART1, USART_IT_TC, DISABLE);
    tx_done = 1; // 通知主循环
    }
    }

    • 追问:为什么发完要"TXE 转 TC"两步?→ TXE 只保证 DR 空,TC 才保证移位寄存器发完;两阶段是串口发送的标准收尾(对应 S7)。

    U10. 环形缓冲完整实现(手撕题)

    #define RB_SIZE 256 // 必须是 2 的幂,才能用 & 取模
    typedef struct {
    uint8_t buf[RB_SIZE];
    volatile uint16_t head; // 写索引:中断里只改它
    volatile uint16_t tail; // 读索引:主循环只改它
    } ring_t;

    // 中断写
    void rb_put(ring_t *r, uint8_t d) {
    uint16_t next = (r->head + 1) & (RB_SIZE – 1);
    if (next != r->tail) { r->buf[r->head] = d; r->head = next; } // 满则丢
    }
    // 主循环读
    int rb_get(ring_t *r, uint8_t *d) {
    if (r->head == r->tail) return 0; // 空
    *d = r->buf[r->tail];
    r->tail = (r->tail + 1) & (RB_SIZE – 1);
    return 1;
    }

    • 追问:为什么"满"用 next == tail 判断?→ 浪费一格法,避免 head==tail 同时表示"空"和"满"的歧义;代价是最多存 255 字节。

    U11. 多文件工程:uart.c/h 里 extern / static / 头文件守卫怎么用?

    /* uart.h */
    #ifndef __UART_H // 头文件守卫:防止重复包含
    #define __UART_H
    void UART1_Init(void);
    int fputc(int ch, FILE *f);
    extern UART_HandleTypeDef huart1; // 跨文件共享句柄(定义在 uart.c)
    #endif

    /* uart.c */
    #include "uart.h"
    static uint8_t tx_buf[64]; // static:仅本文件可见
    UART_HandleTypeDef huart1; // 全局定义

    • 追问:头文件守卫为什么必须?→ 两个 .c 都 include 同一个头时,没有守卫会重复定义报错;守卫(或 #pragma once)让第二次包含变空。
    • 追问:extern 和 static 方向相反?→ extern 声明"它定义在别处,这里可用";static 限制"只在当前文件可见",正好一对。

    深挖三 · I2C 代码级(I13–I19)

    I13. 模拟 I2C 完整手撕代码(Start/Stop/收发/ACK + EEPROM 读写)

    #define SCL_H() GPIOB->BSRR = GPIO_Pin_6
    #define SCL_L() GPIOB->BRR = GPIO_Pin_6
    #define SDA_H() GPIOB->BSRR = GPIO_Pin_7
    #define SDA_L() GPIOB->BRR = GPIO_Pin_7
    #define SDA_READ() ((GPIOB->IDR & GPIO_Pin_7) ? 1 : 0)

    void I2C_Start(void) { SDA_H(); SCL_H(); delay(); SDA_L(); delay(); SCL_L(); delay(); }
    void I2C_Stop(void) { SDA_L(); SCL_H(); delay(); SDA_H(); delay(); }

    void I2C_SendByte(uint8_t b) {
    for (int i = 7; i >= 0; i–) {
    if (b & (1 << i)) SDA_H(); else SDA_L();
    SCL_H(); delay(); SCL_L(); delay(); // SCL 高时 SDA 必须稳定
    }
    }
    uint8_t I2C_ReadByte(void) {
    uint8_t d = 0; SDA_H(); // 释放 SDA,让从机驱动
    for (int i = 7; i >= 0; i–) {
    SCL_H(); delay();
    if (SDA_READ()) d |= (1 << i);
    SCL_L(); delay();
    }
    return d;
    }
    uint8_t I2C_CheckACK(void) { SDA_H(); SCL_H(); delay(); uint8_t a = !SDA_READ(); SCL_L(); return a; }
    void I2C_SendACK(uint8_t ack) { if (ack) SDA_L(); else SDA_H(); SCL_H(); delay(); SCL_L(); SDA_H(); }

    uint8_t EEPROM_WriteByte(uint16_t addr, uint8_t data) {
    I2C_Start();
    I2C_SendByte(0xA0); if (!I2C_CheckACK()) return 0; // 地址+W
    I2C_SendByte(addr >> 8); if (!I2C_CheckACK()) return 0;
    I2C_SendByte(addr & 0xFF); if (!I2C_CheckACK()) return 0;
    I2C_SendByte(data); if (!I2C_CheckACK()) return 0;
    I2C_Stop();
    return 1;
    }
    uint8_t EEPROM_ReadByte(uint16_t addr) {
    uint8_t d;
    I2C_Start();
    I2C_SendByte(0xA0); I2C_CheckACK();
    I2C_SendByte(addr >> 8); I2C_CheckACK();
    I2C_SendByte(addr & 0xFF); I2C_CheckACK();
    I2C_Start(); // 重复起始
    I2C_SendByte(0xA1); I2C_CheckACK(); // 地址+R
    d = I2C_ReadByte();
    I2C_SendACK(0); // 最后一字节 NACK
    I2C_Stop();
    return d;
    }

    • 追问:每个 delay 为什么必须?→ 模拟 I2C 靠软件延时满足时序参数(上升/下降/保持时间),高速场景需按 tSU;STA/tHD;DAT 查手册调延时。
    • 追问:这套代码怎么挂多个器件?→ 换从机地址即可(如 0xA0/0xA2/0xA4…由 A0~A2 引脚决定)。

    I14. HAL_I2C_Mem_Read 内部大致怎么走?(源码级)

    • HAL_I2C_Mem_Read → I2C_RequestMemoryRead:发起始+地址+W(等 TXIS)→ 发寄存器地址高/低字节(等 TXIS,每字节等 ACK)→ 重复起始 → 发地址+R → 收数据:前 N−1 字节等 RXNE 读 + 回 ACK,最后字节回 NACK → 停止 → 置完成标志。
    • 全程带 HAL_GetTick 超时,超时返回 HAL_TIMEOUT 并进 ErrorCallback。
    • 追问:HAL 阻塞模式为什么还要中断?→ HAL 内部用"中断服务 + 状态机推进 + 外层忙等",本质是中断驱动的阻塞调用——这解释了为什么阻塞模式里 ISR 不能被打断太久。

    I15. 总线卡死恢复:9 个时钟脉冲怎么写?

    void I2C_Bus_Recover(void) {
    for (int i = 0; i < 9; i++) { // 最多 9 个时钟
    SCL_H(); delay(); SCL_L(); delay();
    }
    I2C_Stop(); // 补一个停止条件
    }

    • 原理:卡死通常是从机把 SDA 拉死不释放,主机用时钟脉冲强制推进从机内部状态,直到从机在第 9 位释放 SDA。
    • 追问:9 为什么够?→ 从机一个字节/状态最多 9 个时钟(8 数据+1 ACK),脉冲一轮后必然到停止条件附近。

    I16. 多主机同时发数据,谁赢?(仲裁题)

    • 双方都发 1 时总线 1 没问题;某个主机发 1、另一个发 0 → 发 0 的赢(开漏线与,发 1 的主机检测到总线被拉低 → 丢仲裁,退出)。
    • 追问:仲裁发生在哪个阶段?→ 地址位和数据位都可能仲裁;丢仲裁的主机停止发送,总线被胜者接管,不会损坏数据。

    I17. I2C 中断模式读 EEPROM 状态机怎么设计?(10.2 深度)

    typedef enum { ST_START, ST_SEND_ADDR_W, ST_WAIT_ACK1, ST_SEND_REG,
    ST_REPEAT_START, ST_SEND_ADDR_R, ST_WAIT_ACK2,
    ST_READ, ST_SEND_NACK, ST_STOP } i2c_state_t;

    • 每收到一个事件中断就推进一格:发地址完成 → 等 ACK → 发寄存器 → 重复起始 → 切 R → 收字节(计数)→ NACK → 停止。
    • 追问:为什么中断模式要状态机而阻塞不用?→ 阻塞是"一次调用里按顺序等完所有事件";中断模式一个事件一次 ISR,调用早返回了,必须用状态记录"走到哪了",否则不知道下一步发什么。

    I18. 上拉电阻 + 总线电容怎么估算?(进阶硬件题)

    • 上升时间 ≈ 0.8473 × R × C;快速模式要求上升 <300ns(400kHz),3.3V 常用 2.2k~4.7k。
    • 追问:器件多了电阻要换小?→ 总线电容变大,为保持上升沿够快要减小 R,但功耗上升——这就是"挂 8 个从机容易不稳定"的原因。

    I19. 为什么 ISR 里不做 I2C 数据处理?

    • I2C 中断(尤其读多字节)会连续密集触发,在 ISR 里解析业务逻辑会拖住其他中断;正确做法:ISR/回调只搬进缓冲 + 置标志,解析和业务放主循环/任务。

    深挖四 · SPI 代码级(P13–P18)

    P13. Flash 跨页写完整实现(手撕题)

    #define PAGE_SIZE 256
    void Flash_WriteBytes(uint32_t addr, uint8_t *data, uint32_t len) {
    while (len) {
    Flash_WriteEnable(); // 0x06 置 WEL
    uint32_t page_left = PAGE_SIZE – (addr % PAGE_SIZE); // 本页剩余
    uint32_t chunk = (len < page_left) ? len : page_left; // 最多写到页尾
    // 发 0x02 + 24 位地址 + chunk 字节
    SPI_CS_LOW();
    SPI_SendByte(0x02);
    SPI_SendByte((addr >> 16) & 0xFF);
    SPI_SendByte((addr >> 8) & 0xFF);
    SPI_SendByte(addr & 0xFF);
    for (uint32_t i = 0; i < chunk; i++) SPI_SendByte(data[i]);
    SPI_CS_HIGH();
    Flash_WaitBusy(); // 轮询 WIP(0x05)
    addr += chunk; data += chunk; len -= chunk;
    }
    }

    • 追问 1:为什么必须算 page_left?→ 页编程跨页会地址回绕到页首,覆盖旧数据——这是 Flash 面试第一坑。
    • 追问 2:每次写前为什么都要 WriteEnable?→ WEL 位在一次编程/擦除完成后自动清 0,下次必须先置位。

    P14. Flash 读 ID 的完整时序(手撕)

    SPI_CS_LOW();
    SPI_SendByte(0x9F); // JEDEC ID 命令
    id[0] = SPI_SendByte(0xFF); // 3 个哑字节推时钟,读回 3 字节
    id[1] = SPI_SendByte(0xFF);
    id[2] = SPI_SendByte(0xFF);
    SPI_CS_HIGH(); // 读 ID 时 CS 全程低

    • 追问:为什么读要发 0xFF?→ SPI 全双工:读数据必须靠主机发字节提供时钟,0xFF 是通用哑字节(对应 P3)。
    • 追问:读到 ID 全 0xFF 说明什么?→ CS 没拉低/从机没选中、MISO 悬空、或型号不支持该命令。

    P15. SPI DMA 收发配置(HAL 代码)+ 选择依据

    HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len); // 发:内存→SPI
    HAL_SPI_Receive_DMA (&hspi1, rx_buf, len); // 收:SPI→内存
    void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { tx_done = 1; }

    • 追问:SPI 全双工 + DMA 应该用哪个函数?→ 收发同时开时用 HAL_SPI_TransmitReceive_DMA,一次驱动两条 DMA 通道。
    • 追问:什么时候用中断/DMA,什么时候轮询?→ 几字节调试轮询;几十字节中断;大块数据(Flash 页编程、刷屏)必须 DMA,量化理由同 U6。

    P16. 硬件 NSS vs 软件 CS,为什么推荐软件 CS?

    • 硬件 NSS:外设自动控制片选,但时序受外设状态机约束(如起始/结束对齐),多从机切换麻烦,易踩坑。
    • 软件 CS:任意 GPIO 手动拉低/拉高,时序完全自己控制,多从机随便扩展——工程上 90% 用软件 CS。
    • 追问:软件 CS 要注意什么?→ 先拉低再发首字节、发完等 BSY 清再拉高(见 P4),这两个时机错一个数据就丢。

    P17. 为什么发完要等 BSY?(逐字节时序)

    • TXE 清 = DR 空,但移位寄存器可能还在移出最后一位;BSY=1 表示仍在移位,不等就拉高 CS/关 SPI → 尾字节残缺。
    • 追问:和串口 TC 是一个道理?→ 完全同构:DR 空 ≠ 发送完成,寄存器级"最后一字节"必须等硬件完成信号。

    P18. SPI 模式计算题:MODE 0 和 MODE 3 的波形区别?

    • 都是第一个时钟沿采样(CPHA=0/1 相位一致),区别只在空闲电平:MODE0 空闲低、MODE3 空闲高。
    • 追问:为什么 Flash 两个都兼容?→ 大多数 Flash 对空闲电平不敏感(只在采样点读数据),主机按手册配 MODE0 或 MODE3 都能通信——但采样点错了(MODE1/2)一定乱码。

    深挖五 · 时钟树计算题(C1–C4)

    C1. 综合计算:72MHz 系统下,配 115200 串口(USART2)、1ms 定时器(TIM3)、10MHz SPI(SPI1),各自分频填多少?

    • USART2 在 APB1=36MHz:USARTDIV = 36e6/(16×115200) ≈ 19.53 → BRR≈0x13B(误差 0.16%,见 U8)。
    • TIM3 在 APB1,预分频≠1 → 定时器时钟 72MHz:(PSC+1)×(ARR+1) = 72e6/1000 = 72000 → PSC=71, ARR=999(见 R5)。
    • SPI1 在 APB2=72MHz:要 10MHz → 预分频 8(72/8=9MHz,最接近且不超)。
    • 考点:三条线分别用对 APB1/APB2/定时器×2 规则——错一条答案就错。

    C2. ADC 采样率上限怎么算?

    • F1 ADC 时钟上限 14MHz(APB2 再分频 2/4/6/8);最短转换 = 1.5(最短采样)+ 12.5(固定转换)= 14 周期。
    • 上限 ≈ 14MHz/14 = 1MSPS;实际还要留裕量,常用 500k~900k。
    • 追问:提高采样率的两个杠杆?→ 提高 ADC 时钟(≤14M)、缩短采样周期(≤1.5),但采样周期太短精度下降——高速高精度不可兼得。

    C3. Flash 等待周期(WS)和主频什么关系?为什么超频要改它?

    • F1 规则:主频 ≤24MHz → 0WS;≤48MHz → 1WS;≤72MHz → 2WS。
    • 配错:主频高 + WS 小 → CPU 从 Flash 取指读到旧/错数据 → 随机 HardFault。
    • 追问:为什么 72MHz 必须 2WS?→ Flash 访问速度有物理上限(约 24MHz/WS),CPU 更快就必须插等待周期,否则取指错乱。

    C4. 场景题:换了个 12MHz 晶振,程序还按 8MHz ×9 配,会怎样?

    • 实际 SYSCLK = 12×9 = 108MHz → 超出 72MHz 上限:Flash WS 不够、外设超频(APB1=54MHz>36MHz)→ 乱码/随机 HardFault/外设异常。
    • 正确做法:PLL 倍频改成 6(12×6=72)。
    • 追问:这也是产品化常考:晶振频率变了,时钟配置必须跟着变——所以量产用 Bootloader 自适应或写死前先验证。

    深水区高频速查

    题一句话答案
    压栈几个寄存器 8 个:xPSR/PC/LR/R12/R3~R0
    EXC_RETURN 0xFFFFFFFD 线程模式+PSP = RTOS 任务栈
    R4~R11 谁压 软件(PendSV/编译器)
    IDLE 长度怎么算 BUFSZ − CNDTR
    环形缓冲"满"判断 (head+1)%size == tail(浪费一格)
    读 EEPROM 为什么重复起始 防其他主机插队,保持原子
    最后一字节为什么 NACK 告知从机停止,才能正常发停止位
    Flash 为什么先擦后写 编程只能 1→0,擦除恢复全 1
    跨页写为什么拆 页地址回绕会覆盖旧数据
    发完为什么等 BSY/TC DR 空≠发完,尾字节还在移位
    72MHz 下 SysTick 1ms LOAD = 71999
    F1 ADC 采样率上限 ≈1MSPS(14MHz/14 周期)

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【STM32】 阶段性面试题(百题复习)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!