是什么:STM32 的所有外设都由一棵"时钟树"供电,树的根是时钟源,枝干是分频器。G431 的典型路径:HSE 外部晶振(常见 8MHz)或 HSI 内部 16MHz RC → 进入 PLL 锁相环(先 /M 得到 VCO 输入,再 ×N 倍频、/R 分频输出)→ 得到 SYSCLK 系统时钟(G431 上限 170MHz,需要电压调节器 Range 1 Boost)→ 分给 AHB 总线(HCLK,CPU/DMA/SRAM)→ 再分给 APB1(低速外设:TIM2~7、USART2/3、I2C、SPI2/3)与 APB2(高速外设:TIM1/8/15/16/17、USART1、ADC、SPI1)。为什么这么绕:内核要高频,晶振做不了高频(高了难起振、EMC 差),所以用"低频晶振 + PLL 倍频"的分工;各总线外设速度要求不同,所以 AHB/APB 分级供血。
| 层级 | 时钟 | G431 上限 | 挂在上面有什么 | 分频注意事项 |
|---|---|---|---|---|
| 时钟源 | HSE / HSI16 | HSE ≤ 48MHz | HSI 快速可用但精度 ±1%,HSE 精度高 | PLL 只能选 HSE 或 HSI 之一 |
| 锁相环 | PLL(×N /M /R) | VCO 输入 2.66~8MHz(保证 N 合理) | 产生 SYSCLK;另有 PLLSAI1 等 | M/N/R 有合法范围,组合不对直接起不来 |
| 系统级 | SYSCLK | 170MHz(需 Range 1 Boost) | 内核、SYSTICK 源 | Flash 等待周期随频率升到 4WS |
| 总线 | AHB(HCLK) | 170MHz | Cortex-M4、DMA1/2、SRAM、GPIO | 常配 /1 直接满速 |
| 总线 | APB1 / APB2 | 170MHz | APB1: TIM2~7、USART2/3;APB2: TIM1/8、ADC1/2、USART1、SPI1 | 分频 ≠ 1 时定时器时钟 ×2 规则(第 7 节误区) |
/* ============ G431 170MHz 典型 RCC 配置(HAL 风格, CubeMX 生成等价) ============ */
RCC_OscInitTypeDef osc = {0};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* 8MHz 外部晶振 */
osc.PLL.PLLM = 1; /* 8MHz / 1 = 8MHz */
osc.PLL.PLLN = 85; /* 8 × 85 = 680MHz (VCO) */
osc.PLL.PLLP = RCC_PLLP_DIV2; /* 340MHz → 备用输出 */
osc.PLL.PLLR = RCC_PLLR_DIV4; /* 680 / 4 = 170MHz !!! */
HAL_RCC_OscConfig(&osc);
RCC_ClkInitTypeDef clk = {0};
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1; /* HCLK = 170MHz */
clk.APB1CLKDivider = RCC_HCLK_DIV1; /* APB1 = 170MHz */
clk.APB2CLKDivider = RCC_HCLK_DIV1; /* APB2 = 170MHz */
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4); /* 170MHz 需 4 个 Flash 等待周期 */
面试怎么问:"把 G431 从 8MHz 晶振配到 170MHz,M、N、R 怎么取?有什么约束?"——答:先定 VCO 输入(÷M 后落在 2.66~8MHz),再定 VCO 输出(×N,合法范围内),最后 ÷R 得 SYSCLK;同时检查 Flash 等待周期与电压 Range 1 Boost。能补充"HSI 精度不够做高精度通信/ADC 基准时钟,产线产品用 HSE"是加分。
是什么:Cortex-M4 的每个中断有 8 位优先级寄存器,STM32 只实现高 4 位(16 级)。这 4 位可切分:抢占优先级(preemption priority)决定"能否打断别人" —— 抢占优先级更高的中断可以打断正在执行的低抢占优先级中断(中断嵌套);子优先级(sub priority)只在"同时挂起"时决定谁先执行,不能打断任何正在运行的中断。分组(grouping)规定 4 位怎么分:NVIC_PRIORITYGROUP_4 = 4 位全给抢占、0 位子优先级(16 级抢占、无子优先级);GROUP_2 = 2+2。同抢占同级的中断之间永远不会互相打断,排队由子优先级与向量号决定。
怎么做(关节控制器典型分配,GROUP_4):数值越小优先级越高 —— 0~4:不调用 RTOS API 的最紧急中断(ADC 电流环、比较器过流保护);5~15:可调用 RTOS API 的普通中断(CAN、USART、编码器 Z 脉冲);SysTick 与 PendSV:15(最低,FreeRTOS 移植要求,机理见 04-21 FreeRTOS 页第 5 节)。
①分组必须配 GROUP_4(或等效全抢占):FreeRTOS 优先级管理只看抢占优先级,子优先级会让"数值比较"语义失真,CubeMX 集成 FreeRTOS 时会强制此项。②调用 FreeRTOS API(含 FromISR 版)的中断,抢占优先级数值必须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY(典型 5):内核临界区通过 BASEPRI 只屏蔽这条线以下的中断,更紧急的中断若碰 API 会损坏内核数据结构。反之,放在 0~4 的中断(如 20kHz 电流环)享受"任何临界区都挡不住我"的最高确定性,代价是与 RTOS 完全隔离 —— 数据交互走"写内存标志 + 其他中断读取"或无锁单写者模式。
/* ============ 中断优先级配置示例(GROUP_4 + FreeRTOS) ============ */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* FreeRTOS 必须 */
/* ADC1+2 联合中断: 电流环, 不碰 FreeRTOS API → 放屏蔽线之上 */
HAL_NVIC_SetPriority(ADC1_2_IRQn, 1, 0); /* 抢占级 1 */
HAL_NVIC_EnableIRQ(ADC1_2_IRQn);
/* CAN 收包: 要 xQueueSendFromISR → 数值必须 ≥ 5 */
HAL_NVIC_SetPriority(CEC_CAN_IRQn, 6, 0); /* 抢占级 6 */
/* EXTI 编码器 Z 脉冲: 同样需要 RTOS API → ≥ 5 */
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 7, 0);
/* SysTick / PendSV: FreeRTOS 移植层自动设为最低(15) */
面试怎么问:"抢占优先级和子优先级有什么区别?FreeRTOS 环境下分组怎么设?"——答:抢占级可嵌套打断、子级只能排队;FreeRTOS 只认抢占级,必须配 GROUP_4 并遵守"数值 ≥ 5 才可调 API"的屏蔽线约定。能画出本节那张 0~15 分层表即满分。
是什么:DMA(Direct Memory Access)是独立于 CPU 的搬运硬件:外设发出"请求(Request)"→ DMA 控制器响应 → 按 配置 在外设寄存器与内存之间搬数据 → 搬完发中断。CPU 全程不参与,只在"一批搬完"时被通知。G4 有 DMA1/DMA2 各 8 个通道,且通过 DMAMUX(_DMA 请求复用器)_把通道与外设请求灵活配对 —— 不同于 F1 系列的"通道-外设死绑定",G4 上 ADC1 可以映射到任意空闲通道,配错"哪个外设请求接到哪个通道"这类 F1 时代事故在 G4 上基本消失。每个通道一份配置:源地址/目标地址、传输数量 NDTR、数据宽度 PSIZE/MSIZE(字节/半字/字)、地址增量 PINC/MINC、模式(正常/循环)、优先级。
| 对比项 | 正常模式(NORMAL) | 循环模式(CIRCULAR) |
|---|---|---|
| 计数到零后 | DMA 停止,需软件重新启动 | 自动回卷到起始地址,永不停止 |
| 典型用途 | 一次性发送串口帧、批量搬运采集数据 | ADC 连续采样缓冲区(电流环标准姿势)、串口不定长接收环形缓冲 |
| CPU 介入 | 每批数据一次重启 | 零介入;用半满/全满中断做"乒乓"处理 |
| 风险点 | 忘记重启 → 数据断流 | CPU 读缓冲与 DMA 写缓冲并发,需半满/全满同步或双缓冲 |
怎么做(ADC + DMA 标准管线):ADC 配连续/定时器触发转换,DMA 配循环模式把每次转换结果自动写入 16 位缓冲数组;开 DMA 半满(HT)与全满(TC)中断 —— 半满时处理前半段、全满时处理后半段(乒乓),CPU 与 DMA 永不碰同一半缓冲。多通道顺序采样(如三相电流 + 母线电压)时,ADC 扫描组的每个通道转换结果按序列顺序落进数组,数组下标与通道的对应关系要对着 ADC_SQR 序列寄存器配置核对,这是例程移植最容易张冠李戴的地方。
PSIZE/MSIZE 与 MINC:PSIZE 定义"从外设读多宽"(ADC 数据寄存器是 16 位 → 半字),MSIZE 定义"写进内存多宽",二者不等时 DMA 自动截断/扩展;PINC/MINC 决定搬完一次后源/目标地址是否递增。经典翻车:ADC(半字)搬到 32 位 int 数组且 MINC 配错,结果数组里全是"半字拼接"的错位数据。缓存一致性:Cortex-M4(G431)没有数据 Cache,DMA 与 CPU 看同一块 SRAM,天然一致 —— 但换到 Cortex-M7 平台(H7 系列)有 D-Cache,DMA 写入的缓冲区若还在 Cache 里是"脏"的,CPU 读到旧值;处理方式是缓冲区放不缓存的 SRAM 区、或搬前 SCB_InvalidateDCache_by_Addr() / 搬后 SCB_CleanDCache_by_Addr(),并把缓冲区按 32 字节对齐(__attribute__((aligned(32))))。写可移植代码时把这条差异写进注释,是工程师成熟度的体现。
面试怎么问:"ADC 连续采 8 通道,DMA 循环模式,软件怎么安全消费数据?"——答:循环缓冲 + HT/TC 双中断乒乓处理,处理段数据期间 DMA 只写另一段;再补"G4 无 Cache 无需维护,H7 需 invalidate/align 32 字节"即超出预期。
是什么:G431 的 ADC1/ADC2 是 12 位逐次逼近型(硬件可过采样到 16 位),最高 4 Msps 级。转换组织成两个"组":规则组(regular group) —— 最多 16 个通道的扫描序列,结果存入一个共用数据寄存器(DR),多通道必须配 DMA 否则前一个结果被覆盖;注入组(injected group) —— 最多 4 个通道,可打断规则组优先转换("注入"之名由此而来),每个通道有独立数据寄存器(JDR1~4)。
| 对比项 | 规则组 | 注入组 |
|---|---|---|
| 通道数 | 最多 16 | 最多 4 |
| 数据寄存器 | 共用 1 个 DR(多通道必须 DMA) | 独立 JDR1~4,无需 DMA 也能取 |
| 可否被打断 | 可被注入组打断(autoInject/autoDelay 等模式控制细节) | 打断规则组,转换完规则组自动恢复 |
| 典型用途 | 慢速后台巡检:温度、电位器、母线电压 | 电流环同步采样:ia/ib 相电流,定时器精准触发、中断读取 |
| 触发源 | 软件/定时器 TRGO/外部引脚 | 软件/定时器 TRGO2 等(与规则组可独立触发) |
是什么:逐次逼近 ADC 每次转换 = 采样保持时间(SMP 可选档) + 12.5 个 ADC 时钟的逐次比较时间(12 位分辨率)。G4 的 SMP 档位:2.5 / 6.25 / 12.5 / 24.5 / 47.5 / 92.5 / 247.5 / 640.5 个 ADC 时钟周期。
怎么选:采样时间决定内部采样电容给信号源"充电"多久 —— 信号源阻抗高(经大电阻分压的母线电压)就要更长 SMP,否则保持电容没充到位,读数系统性偏低且串扰到上一通道("通道间串扰"是 SMP 不足的标志性症状);而电流环要快,又要留够精度。实用流程:先按 datasheet 的采样时间 vs 源阻抗表选下限,再在真实板上做"已知电压注入"验证精度;总采样率别顶满上限,留转换排序余量。
为什么必须定时器触发:软件随时启动转换 = 采样点随机散布在 PWM 周期里,采到的相电流一半是开关噪声。正确做法:高级定时器产生中心对齐 PWM,把 TRGO(触发输出)指向计数器计到上溢/下溢的点 —— 这恰是 PWM 中心、下管导通半桥中点,电流纹波最平滑;ADC 收到触发自动转换,转换完触发中断,ISR 里做 FOC(完整链路见 04-19 FOC 图解页)。双重/联合模式:ADC1 与 ADC2 可组成双 ADC 模式(规则同步、注入同步、交替采样等)—— 双电阻采样两相电流时,两个 ADC 同时触发、各采一相,时间上严格对齐;交替模式还能把等效采样率翻倍。
/* ============ TIM1 触发 ADC 注入组: 电流环同步采样(G4 HAL 要点) ============ */
/* 1) TIM1: 中心对齐 PWM, TRGO 在 Update(计到上溢)事件发出 */
htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1;
HAL_TIM_PWM_Init(&htim1);
TIM_MasterConfigTypeDef mst = {0};
mst.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 上溢点 = PWM 中心 */
HAL_TIMEx_MasterConfigSynchronization(&htim1, &mst);
/* 2) ADC1: 注入组采 ia, 外部触发 = TIM1_TRGO, 上升沿启动 */
ADC_InjectionConfTypeDef inj = {0};
inj.InjectedChannel = ADC_CHANNEL_1; /* PA0 → ia */
inj.InjectedNbrOfConversion = 1;
inj.InjectedSamplingTime = ADC_SAMPLETIME_6CYCLES_5; /* 6.5 ADC clk */
inj.ExternalTrigInjecConvEdge= ADC_EXTERNALTRIGINJEC_CONV_EDGE_RISING;
inj.ExternalTrigInjecConv = ADC_EXTERNALTRIGINJEC_T1_TRGO;
inj.InjectedRank = 1;
HAL_ADCEx_InjectedConfigChannel(&hadc1, &inj);
HAL_ADCEx_InjectedStart(&hadc1);
/* 3) 注入转换完成中断 → ISR 里读 JDR1 做 Clarke/Park(见 04-19 页) */
面试怎么问:"电流环为什么用注入组 + 定时器触发,而不是规则组 + 软件轮询?"——答:注入组独立数据寄存器免 DMA、可打断后台规则组保证电流采样最高优先级;定时器 TRGO 把采样点锁定在 PWM 中心,采到的是无开关纹波的"真值";软件轮询的采样时刻随机,采到开关噪声,电流环输出随之抖动。
| 家族 | 代表(G431) | 关键特征 | 典型用途 |
|---|---|---|---|
| 基本定时器 | TIM6 / TIM7 | 只有计数与更新中断,无输出通道,可触发 DAC | 纯时基:HAL 时基(CubeMX 把 SysTick 让给 FreeRTOS 后的替代)、周期性中断 |
| 通用定时器 | TIM2(32 位)/ TIM3 / TIM4 / TIM15~17 | 计数 + 输入捕获 + 输出比较 + 编码器接口(TIM2~5) | PWM(无互补)、测频/测脉宽、编码器测速、超时检测 |
| 高级定时器 | TIM1 / TIM8(G4 部分型号还有 TIM20) | 通用全部功能 + 互补输出 CHxN + 死区插入 + 刹车输入 BKIN + 重复计数器 | 三相逆变 PWM(电机驱动必用)、电子变压器、数字电源 |
为什么电机驱动必须互补输出 + 死区:半桥的上下管绝不能同时导通(母线直通短路)。高级定时器的 CHx 与 CHxN 由硬件产生逻辑互补的死区波形 —— 上升沿延迟 t_dead 才拉高、下降沿也延迟,任何软件错误都无法造出直通(刹车输入 BKIN 还能在外部故障时硬件级关断全部输出,不经过软件)。死区取值:大于功率管关断时间 + 栅驱传播延迟,典型 0.2~1μs;过大则占空比损失、低速电流过零畸变(死区效应,驱动器层可做死区补偿,见 02-06/02-08 页)。
是什么:通用定时器 TIM3/TIM4 的编码器接口模式把 TI1/TI2 两个输入(A/B 相)交给硬件解码:计数器随正交脉冲自动加减 —— 顺时针 A 超前 B 计数器增,反转则减,4 倍频(A、B 每个边沿都计数)由硬件免费完成。配置要点:CR1 的 SMS 位选编码器模式 3(双边沿 4 倍频)、CCMR1 输入滤波(防毛刺,ICFilter)、ARR 按编码器线数 ×4 − 1 设置。软件只做一件事:周期性读 CNT 换算角度(并处理跨零溢出),配合 Z 脉冲做绝对零位校准(流程见 02-12 标定页)。相比 EXTI 中断数脉冲,硬件解码零 CPU 负载、高转速不丢步 —— 高转速下 EXTI 每个边沿一次中断的方案会直接把 CPU 打满。
面试怎么问:"1000 线编码器,4 倍频后定时器 ARR 设多少?一圈计多少数?"——答:ARR = 4000 − 1,一圈 4000 计数;若说明"计数器模式中心对齐时倍数另有定义、以手册为准"更严谨。再追问"为什么用硬件编码器模式而不是 EXTI",答 CPU 负载与丢步即可。
是什么:调试打印最常用的路径是 USART1(常接 USB-TTL)。重定向 printf:实现 int fputc(int ch, FILE *f) 内部调 HAL_UART_Transmit(或直接写 TDR 等待 TXE 标志)。三条性能军规:①阻塞发送每字节 ≈ 波特率倒数(115200bps 下 ~87μs/字节),高频控制环里严禁 printf —— 一行日志能把 50μs 的电流环预算吃光十遍;②要吐波形数据用 DMA 发送或二进制协议(浮点 4 字节直传,别用 ASCII);③printf 默认占用几十 KB 栈与 Flash(浮点格式化),任务栈按 2KB 起配(对应 04-21 页栈估算)。
是什么:SWD(Serial Wire Debug)用 SWDIO/SWCLK 两根线提供下载、断点、单步、寄存器/内存观察 —— 关节驱动板 Layout 必留 SWD 排针(见 02-13 Layout 清单)。实用技巧:①被优化代码调试 —— -O2 下断点会"跳行",变量显示 optimized out,调试时降级 -O0 或对函数加 volatile 临时观察;②ITM/SWO —— 加一根 SWO 线可用 printf 重定向到调试器通道,不占 USART 与 CPU 串行等待;③硬故障定位 —— HardFault 后从 LR/PC 值回溯调用栈,配合 map 文件定位崩到哪个函数。
/* ============ DWT CYCCNT: 测任意代码块的 CPU 周期数 ============ */
#define DWT_ENABLE() do { \
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* 打开调试跟踪 */ \
DWT->CYCCNT = 0; /* 清零计数器 */ \
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; /* 启动计数 */ \
} while (0)
static inline uint32_t dwt_cycles(void) { return DWT->CYCCNT; }
/* 用法: 170MHz 下 1 cycle ≈ 5.88ns, 精度远超示波器 GPIO 法 */
void measure_control_loop(void)
{
uint32_t t0 = dwt_cycles();
foc_current_loop(); /* 被测代码 */
uint32_t dt = dwt_cycles() - t0; /* 纯周期差, 零额外开销 */
g_worst_cycles = MAX(g_worst_cycles, dt); /* 长跑取最大 = WCET */
/* 例: dt = 3400 → 3400/170MHz ≈ 20μs, 占 50μs 周期预算 40% → 偏紧 */
}
为什么它是首选:GPIO 翻转 + 示波器需要硬件接线且只能测宏观间隔;DWT 直接读内核周期计数器,一条指令读数、分辨率 1 cycle,长跑统计 WCET 完美贴合 04-21 页的实时性指标方法。注意:计数器 32 位、170MHz 下约 25 秒回卷一次,短时测量不受影响;HAL 时基与 SysTick 操作不影响 DWT(独立硬件)。
面试怎么问:"不接示波器,怎么测一个函数的执行时间?"——答 DWT CYCCNT:使能 TRCENA + CYCCNTENA,前后读数差即周期数,除以主频得时间;长跑取 max 得 WCET。能补"32 位回卷约 25s、测量窗口要避开回卷或做差前判断"是细节加分。
规则:当 APB 预分频系数不是 1 时,挂在该 APB 上的定时器时钟自动翻倍(= PCLK × 2);分频为 1 时定时器时钟 = PCLK。这是 RCC 设计者为"低速总线 + 高速定时器"留的补偿机制(RCC_CFGR 的 TIMPRE 位族控制细节,以 RM0440 为准)。事故:例程在 HCLK=170/APB1=85MHz 下算出 PSC/ARR,你移植到 APB1=170MHz 的配置里忘改 —— PWM 频率直接偏一倍,开关损耗、电流纹波全变。正解:每次动时钟树,把"APB 分频 → PCLK → 定时器时钟(应用 ×2 规则)"三步写进注释重算一遍。
症状:ADC 数据数组出现 0x0FFF0FFF 这种"同一个值重复两半"或错位拼接的诡异值。机理:ADC DR 是 16 位(半字),PSIZE 必须配 HALF_WORD;目标数组若开成 32 位 int 数组且 MSIZE 仍是半字,两个结果会拼进同一个 32 位单元(取决于 MINC 与地址推进方式),解析全乱。正解:"外设宽度 → PSIZE、变量宽度 → MSIZE、两者一致 + 数组元素类型匹配",拷例程时这三处必须重核而不是照抄。
症状:开了浮点库的高频中断偶尔超时,或低优先级中断集体迟到。机理:Cortex-M4F 硬件 FPU 单次乘加 1 周期本身很快,但中断入口/出口要压弹 FPU 上下文(S0~S15 + FPSCR,额外约 17 个寄存器) —— 中断里用一次浮点,进出场成本从 ~12 周期涨到 ~30+ 周期;更糟的是无 FPU 库函数(sinf/cosf 查表 + 多项式)动辄数百周期。电流环里 Clarke/Park 的 sin/cos 必须用查表法或 CORDIC(见 04-19 页)。正解:①高频中断浮点不可免时确认编译器开启了硬 FPU(-mfpu=fpv4-sp-d16 -mfloat-abi=hard),别掉进软件浮点;②非必要的浮点处理挪出中断、放在任务里。
症状:GPIO_Init 配置后引脚毫无反应,读回配置寄存器永远是 0。机理:STM32 所有外设时钟默认关闭以省电,时钟没开的外设寄存器写入无效。手写寄存器级代码必写 RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;(G4 的 GPIO 挂 AHB2);HAL 侧由 __HAL_RCC_GPIOx_CLK_ENABLE() 承担 —— 例程迁移时最容易漏的就是这一行(或 MSP 初始化文件没拷)。正解:"外设不响应,先查时钟使能"应成为条件反射;另外注意时钟使能后要短暂的几个周期等待(写后立即读可能未生效,HAL 已处理)。
| 误区 | 典型症状 | 一句话正解 |
|---|---|---|
| APB 定时器时钟忘 ×2 | PWM 频率偏差一倍 | APB 分频 ≠1 时定时器时钟 = PCLK×2,配置后重算 PSC/ARR |
| DMA PSIZE/MSIZE 错配 | 数据错位拼接、数组值诡异 | 宽度三处对齐:外设寄存器 / DMA 配置 / 数组类型 |
| 中断里大量浮点 | 高频中断超时、其他中断迟到 | 硬 FPU 编译选项 + 查表法 sin/cos + 浮点尽量出中断 |
| 忘使能 GPIO 时钟 | 引脚无反应、寄存器读回 0 | 外设不响应先查 RCC 时钟使能位 |
面试怎么问:"例程在别人板上 PWM 是 20kHz,到你板上变成 10kHz,可能的原因?"——答主方向:两板时钟树不同(HSE 频率、APB 分频),叠加定时器时钟 ×2 规则差异,PSC/ARR 没跟着重算;再排查 SYSCLK 实际值(读 RCC 寄存器或 MCO 引脚输出验证)。