1. F407ZGT6 这块片子,凭什么被叫做"资源拉满"
第一次拿到 STM32F407ZGT6 的人,反应通常是两个极端:一种是"这不是我要的那块 F103 吗,引脚怎么这么多",另一种是"终于有足够的串口和 IO 了"。这块 LQFP144 封装的 Cortex-M4 单片机在圈子里口碑一直很稳,原因不复杂——它把 168MHz 主频、1MB Flash、192KB SRAM、以太网 MAC、摄像头接口、FSMC 外部总线、两路 USB OTG、六路串口、三路 ADC、两路 DAC、十七个定时器全部塞进了一颗芯片里,同时价格还停在大多数团队能接受的位置。对于要做图像采集加网络回传、多路电机协同加多串口通信、或者带 SDRAM 跑简单图形界面的项目,它基本是"一颗芯片解决问题"的那类选择。但 144 脚带来的不只是资源,还有一堆只在多外设同时上线时才会暴露的设计坑,电源、时钟、引脚复用、总线带宽,任何一环没想清楚,都能让你在调试台前坐一整天。
1.1 先把型号后缀读明白
STM32F407ZGT6这一串字符不是一个整体,拆开来看每一个字母都在告诉你这颗片子的能力边界。STM32是产品家族,F是通用型子系列定位,4指向 Cortex-M4 内核,07是这一代的外设与存储组合编号——实际上 F407 和 F417 的外设布局差别不大,主要区别在加密相关特性上。真正决定你画板子时能不能用的是后四位:Z表示 144 引脚,G表示 1MB Flash,T表示 LQFP 封装,6表示工业级温度范围,大致覆盖 -40℃ 到 85℃。
这里必须单独说一句Z。同一颗 F407 核心,封装不同,能同时开的外设天差地别。100 脚的 VGT6 只有 82 个 IO,你打开 FSMC 加上 SDRAM 就基本把引脚吃干净了,摄像头接口和以太网只能二选一;而 144 脚的 ZGT6 有 114 个 IO,分布在 PA 到 PH 共八组端口上,其中 PH0 和 PH1 是 144 脚封装独有的,专门留给外部晶振用。这就是为什么很多开源项目和竞赛板卡在方案阶段就锁定 ZGT6——不是因为算力不够,而是因为脚不够用。
再往细看,F407 的 192KB SRAM 不是一整块,而是拆成 112KB 的 SRAM1、16KB 的 SRAM2,再加 64KB 的 CCM 紧耦合内存。前 128KB 挂在系统总线上,DMA 能访问;CCM 只连到内核,DMA 完全够不着。这个细节我在早期项目里吃过亏:把 DMA 的缓冲区定义到 CCM 区,程序编译链接一切正常,跑起来 DMA 就是不动,查了两天才发现是缓冲区地址的问题。所以如果你打算用 CCM 那块"额外"的内存,先确认它只被 CPU 读写。
1.2 144 脚多出来的到底是什么
光说"资源多"太虚,我们把它摊开对比一下。同样是 F407 内核,100 脚和 144 脚在"能同时干什么"这件事上完全是两个量级。
| 项目 | LQFP100(VGT6) | LQFP144(ZGT6) | 说明 |
|---|---|---|---|
| GPIO 数量 | 82 | 114 | 多出 32 个可用 IO |
| 以太网 MAC | 可用(RMII 9 根脚) | 可用(RMII/MII 均可) | MII 需 16 根脚,100 脚很难挤出来 |
| FSMC 外部总线 | 可用但 IO 紧张 | 可挂 SRAM/NOR/NAND/SDRAM | SDRAM 要占约 40 根脚 |
| DCMI 摄像头 | 与以太网争脚 | 可与以太网、FSMC 同时开 | ZGT6 的典型玩法 |
| USB HS(ULPI) | 无 | 有完整 ULPI 引脚 | 需外接 ULPI PHY |
| 串口 | 6 路但需取舍 | 6 路可全部引出 | 多机通信场景差异明显 |
能看到关键点:ZGT6 的价值不在于"每一项都更强",而在于"多项可以同时开"。一个典型的高配用法是这样的——DCMI 接 OV2640 采图,FSMC 挂一颗 16 位 SDRAM 做帧缓冲,以太网 MAC 配 LAN8720A 做 RMII 传输,同时用四路串口接上位机、GPS、两路传感器,再留两路 CAN 给电机驱动。这套组合在 100 脚上根本排不下,在 144 脚上只是常规操作。
不过代价也真实存在。LQFP144 的引脚间距是 0.5mm,手工焊接对新手不友好,风枪加助焊剂加放大镜是标配;四层板几乎是硬性要求,因为 SDRAM 的数据线和地址线需要等长与参考平面;电源电流预算也要重新算,168MHz 全速加以太网加 SDRAM 同时工作,3.3V 侧预留 500mA 到 1A 的余量比较稳妥。用线性稳压器(比如常见的 AMS1117)在 5V 转 3.3V 时,压差乘电流就是纯发热,500mA 就是 0.85W,板子会很烫,这种情况我更推荐用 DC-DC 方案。
1.3 该选它和不该选它的分界线
选型这件事最忌讳"因为别人都在用"。F407ZGT6 适合什么,我按实际项目经验划几条线。
该用它的场景很明确:需要图像采集或视频流处理,摄像头接口加 SDRAM 加网络这套组合是它的主场;需要同时挂多路串口和 CAN 的工业控制板,六串口两 CAN 的配置省掉一堆外部扩展芯片;需要跑带界面的产品,FSMC 挂 SDRAM 做显存,配 emWin 或 LVGL 都能跑到可用的流畅度;需要做数据采集与分析,三路 12 位 ADC 配合 DMA 和定时器,能在不占 CPU 的情况下连续采样。还有一类是学习与竞赛用途,F407 的资料积累足够厚,遇到问题基本都能搜到答案。
不该用它的场景同样清楚。成本极度敏感的消费类产品,一颗 F103 或 G0 系列能省下不少物料钱,多出来的算力全在浪费;电池供电的便携设备,F407 的运行电流和待机功耗都不好看,L4 或 G0 的低功耗模式才是正解;需要 200MHz 以上连续算力做实时音频处理或复杂控制的,直接上 H7 系列会更省心,F407 超频到 200MHz 虽有人试过,但稳定性和温度表现都不适合量产。
还有一个很实在的判断标准:如果你的项目压根用不到 FSMC 和以太网这两个"吃脚大户",那 144 脚基本是浪费,100 脚甚至 64 脚就够了。选型的本质是"按最复杂的那一版需求买余量",而不是"按现在的需求买刚好"。
2. 硬件设计:钱和坑大多花在电源与时钟上
画 F407 的板子,画错了最贵的不是元件,是打样的时间和返工的周期。我见过太多人第一次做完板子,下载能成功但程序随机跑飞,或者串口能通但以太网就是收不到包,最后顺着排查发现根子都在电源和时钟这两个模块上。这两块占的原理图面积不大,但它们决定了芯片到底能不能按你设想的状态运行。
2.1 电源域、VCAP 和去耦网络
F407 的供电引脚分成好几类,混在一起处理的后果很严重。VDD是主供电,范围 1.8V 到 3.6V;VDDA是模拟供电,给 ADC、DAC、复位电路和内部 PLL 用;VREF+是 ADC 的参考电压;VBAT给后备域供电,接 RTC 和 4KB 后备 SRAM;还有两个容易被忽略的VCAP1和VCAP2。
VCAP 这两根脚是 F4 和 F1 最大的硬件差异。F407 内部有一个 1.2V 的内核稳压器,VCAP1 和 VCAP2 是它外接的滤波引脚,每个引脚必须接一颗 2.2µF 的低 ESR 陶瓷电容到地,X5R 或 X7R 材质,ESR 要低。你要是按 F103 的经验直接悬空,或者随手放一颗 100nF,芯片可能表现为"能下载、偶尔能跑、大部分时间随机死",这种问题极难定位,因为现象不像硬件故障。我第一次画 F4 板子就犯过这个错,查了两天晶振,最后是翻数据手册的引脚描述才发现的。
去耦网络的布置也有讲究。144 脚封装的 VDD 引脚数量不少,我的做法是每个 VDD 引脚配一颗 100nF,位置紧贴引脚,走线控制在 3mm 以内,然后在整块板子的电源入口处再并一颗 4.7µF 到 10µF 的电容做储能。VDDA 单独处理:用磁珠或者 0Ω 电阻从 3.3V 引过来,后面接 1µF 加 10nF 的滤波组合,VREF+ 也照这个思路单独滤波。模拟部分和数字部分的地在芯片下方单点汇合,不要大面积混铺。
提示:VCAP 电容的地和 VSS 走最短路径回到芯片下方的地平面,不要绕远,这段走线的寄生电感会直接影响内核稳压器的稳定性。
功耗预算建议按"最坏情况"算。168MHz 主频、Flash 全速、以太网和 SDRAM 同时工作的场景下,整板 3.3V 侧的电流需求准备好 500mA 以上的余量。如果你的板子还要驱动 LCD 背光或者多个传感器,1A 的电源能力更保险。
2.2 从 8MHz 晶振算到 168MHz 与 48MHz
时钟树是 F407 最容易出错又最容易被忽略的地方。很多人用 CubeMX 拖两下就生成了代码,跑起来能用,但一旦要自己改成别的频率或者排查时钟问题,就完全不知道从哪下手。我们把公式捋一遍。
F407 的 PLL 有三个关键参数:M是输入分频,N是倍频,P和Q是两个输出分频。计算顺序是这样的:
- VCO 输入频率 = HSE / M,这个值必须在1MHz 到 2MHz之间,推荐取 1MHz;
- VCO 输出频率 = VCO 输入 × N,范围必须在100MHz 到 432MHz之间;
- 系统时钟 SYSCLK = VCO 输出 / P,不能超过 168MHz,P 只能是 2、4、6、8;
- USB、SDIO、RNG 的时钟 = VCO 输出 / Q,必须精确等于 48MHz,否则 USB 无法枚举。
用最常见的 8MHz 晶振来算:M 取 8,得到 1MHz 的 VCO 输入;N 取 336,得到 336MHz 的 VCO 输出;P 取 2,得到 168MHz 的 SYSCLK;Q 取 7,得到 48MHz 的 USB 时钟。五个数字全部落在合法范围内,这就是那块经典配置的来历。你换晶振的时候要重算一遍,比如用 25MHz 晶振做以太网方案,M 取 25 得到 1MHz,N 取 336,P 取 2,Q 取 7,结果完全一样,因为 M 和 N 是成比例调整的。
再往下是总线分频。AHB 就是 168MHz,APB1 最大只能跑到 42MHz,所以要 4 分频;APB2 最大 84MHz,2 分频。这里有个反直觉的点:当 APB 预分频不是 1 的时候,挂在该总线上的定时器时钟会再乘 2。所以 TIM2 到 TIM7、TIM12 到 TIM14 的实际计数时钟是 42MHz × 2 = 84MHz,而挂在 APB2 上的 TIM1、TIM8、TIM9 到 TIM11 则是 168MHz。做定时器分频计算时搞错这个倍数,输出的 PWM 频率会整整差一倍。
Flash 等待周期也必须跟着频率调。F407 在 3.3V 供电下,168MHz 需要5 个等待周期,同时必须打开 ART 加速器的预取、指令缓存和数据缓存,否则性能会掉得很厉害。这个对应关系官方给得很明确:
| SYSCLK 范围 | 等待周期(3.3V) | 等待周期(2.7V~3.6V 通用) |
|---|---|---|
| ≤30MHz | 0 | 0 |
| 30~60MHz | 1 | 1 |
| 60~90MHz | 2 | 2 |
| 90~120MHz | 3 | 3 |
| 120~150MHz | 4 | 4 |
| 150~168MHz | 5 | 5 |
晶振本身的选型也有讲究。8MHz 无源晶振的负载电容CL通常标 8pF 或 10pF,外接的两颗起振电容要按CL ≈ (C1 × C2) / (C1 + C2) + Cstray来估,其中 Cstray 是 PCB 走线的寄生电容,一般按 3pF 到 5pF 算。负载 8pF 的晶振配 12pF 到 15pF 两颗比较合适,配大了起振慢、频率偏低,配小了可能不起振。如果你的板子对时钟精度要求高,用有源晶振直接喂进 OSC_IN 是更省事的做法,只是成本高一点。
提示:内部 HSI 是 16MHz 的 RC 振荡器,精度只有 ±1% 左右。它能让你在晶振坏掉的时候救急跑起来,但绝不能用来做 USB 或者以太网——48MHz 的 USB 时钟对精度的要求是 ±0.25%,HSI 差得太远。
2.3 引脚复用冲突与一份可用的分配方案
从 F103 转过来的朋友最容易在引脚复用上栽跟头。F1 用的是 AFIO 加"重映射"那套机制,F4 完全换了玩法:每个引脚在GPIOx_AFRL和GPIOx_AFRH两个寄存器里占 4 个 bit,写进去的数值就是复用功能编号(AF0 到 AF15)。同一个引脚可能支持十几种功能,选哪个全靠这个编号。好处是灵活,坏处是写错了不会报错,只会"这个引脚没反应"。
举几个实际会撞车的组合。PA4 和 PA5 既是 DAC 的输出通道,又是 SPI1 的 NSS 和 SCK,还兼 ADC 的输入通道,你要用 DAC 输出波形就不能同时用 SPI1。PB10 和 PB11 同时是 I2C2 和 USART3 的引脚,接两个 I2C 传感器加一路串口的板子要提前规划。PA11 和 PA12 是 USB OTG FS 的 D- 和 D+,但同时也是 CAN1 的 RX 和 TX,这两个外设没法同时用。还有 PA13 和 PA14 是 SWD 调试口,一旦被复用成普通 GPIO,你的调试器就再也连不上了,只能靠 BOOT0 进系统存储器救回来。
下面给一份我实际用过的分配思路,场景是"以太网 + 摄像头 + 四串口 + SDRAM",可以当作参考模板:
| 外设 | 占用引脚 | 备注 |
|---|---|---|
| 以太网 RMII | PA1、PA2、PA7、PC1、PC4、PC5、PB11、PB12、PB13 | 9 根脚,REF_CLK 由 PHY 提供 50MHz |
| DCMI 摄像头 | PC6~PC9、PE4、PB6、PE6、PB9、PA4、PA6、PB7、PA8 | D0~D7 加 HSYNC/VSYNC/PIXCLK/XCLK |
| 串口 | USART1(PA9/PA10)、USART2(PA2/PA3 冲突需换)、USART3(PB10/PB11)、UART4(PC10/PC11) | 注意与本表其他项避让 |
| FSMC SDRAM | PD0/PD1、PD4~PD6、PD8~PD10、PD14/PD15、PE0/PE1、PE7~PE15、PF0~PF5、PF11~PF15、PG0~PG5、PG8、PG15 | 约 40 根脚 |
| SWD 调试 | PA13、PA14、NRST | 全程保留,不做复用 |
这张表不是让你照抄,而是让你看到"为什么必须提前画表"。强烈建议在画原理图之前,先在 CubeMX 的引脚视图里把所有外设挂上去,它会自动标红冲突的引脚。等你布完线再发现冲突,改板的代价就是两周时间和一笔打样费。
2.4 下载调试与启动配置
调试接口这块,SWD 比 JTAG 省脚也够用。SWD 只需要 PA13 的 SWDIO、PA14 的 SWCLK 加 NRST 三根线,JTAG 要五根。除非你要做边界扫描测试,否则没有任何理由用 JTAG。NRST 上并一颗 100nF 到地可以有效抑制复位引脚上的毛刺,芯片内部本来就有上拉,不用额外加。
BOOT 配置决定了芯片上电后从哪里取代码。BOOT0接 10kΩ 下拉到地,正常从主 Flash 启动;需要进系统存储器用串口下载程序时,把 BOOT0 拉高再复位。F407 上BOOT1对应的是 PB2 引脚,它的作用是在 BOOT0 为高时决定从系统存储器还是从 SRAM 启动,常规用法是保持下拉。实际做板的时候,我一般把 BOOT0 做成跳线帽或者一个按键,方便量产时烧录固件。
还有一个实用技巧:在板子上留一组串口,专门接到上位机做日志输出,波特率固定 115200。哪怕你的产品最终不通过串口通信,调试阶段的 printf 重定向和日志打印能帮你省掉大量时间。Keil 环境下要注意半主机模式的问题,得在工程里加上禁用半主机的处理,否则程序会在_sys_exit里卡死,这个坑新手几乎必踩一次。
3. 软件环境搭建:工具链怎么选,时钟怎么配
硬件没问题之后,接下来是软件。这一步的选择会长期影响你的开发效率,所以值得花半小时想清楚,而不是随手打开一个教程就跟着装。
3.1 四套工具链的取舍
嵌入式开发的工具链这几年选择变多了,但主流还是那几套。我按实际使用感受列个对比。
| 方案 | 优势 | 劣势 | 适合谁 |
|---|---|---|---|
| Keil MDK5 + STM32F4xx_DFP | 上手快,调试器支持完善,教程最多 | 商业授权,免费版有 32KB 代码限制,编辑器体验一般 | 新手、教学、快速验证 |
| IAR EWARM | 编译优化强,代码体积小 | 授权贵,界面风格偏老 | 商用项目、对体积敏感的产品 |
| arm-none-eabi-gcc + CMake + VSCode | 完全免费,跨平台,可脚本化,配合 Cortex-Debug 调试体验好 | 环境搭建门槛高,链接脚本和启动文件要自己配 | 有 Linux 背景、追求工程化的开发者 |
| PlatformIO | 装好即用,依赖管理方便 | 灵活度不如手写 CMake,深层问题排查要绕一层 | 快速做多平台对比实验 |
对我个人来说,日常验证和教学用 Keil 最省事,正式项目我倾向 GCC 加 CMake 那一套,因为工程的编译配置可以用文本管理,能进 Git,团队协作时不会有"你机器上的工程配置和我这边不一样"的问题。VSCode 那边需要装的扩展主要是 C/C++、Cortex-Debug 和 STM32 VS Code Extension,再配上 OpenOCD 或者 ST-Link 的调试后端,配置一次之后体验很顺。
库的选择上,标准外设库(StdPeriph)早就停止维护了,但它的代码直白,寄存器操作一目了然,学原理的时候看它反而更清楚。HAL 库是现在的主流,配合 CubeMX 生成的初始化代码省掉大量重复劳动,代价是执行效率低一些、代码层数多。LL 库介于两者之间,接近寄存器操作又保留了一点封装,适合想把性能和开发效率兼顾的人。我的建议是:先用 HAL 把功能跑通,再挑关键路径用 LL 或者直接操作寄存器优化,不要一上来就纠结用哪套。
3.2 时钟树配置的实操与寄存器对照
CubeMX 里配时钟很直观:RCC 里把 HSE 选成 Crystal/Ceramic Resonator,然后在 Clock Configuration 页填 M、N、P、Q 四个值,它会实时显示各个总线的频率,超范围会标红。但我想把寄存器层面的东西也讲一遍,因为出了问题你还是得回来看这些位。
RCC->PLLCFGR这个 32 位寄存器里,M 占 bit0 到 bit5,N 占 bit6 到 bit14,P 占 bit16 到 bit17,Q 占 bit24 到 bit27。注意 P 的编码方式不是直接写数值:
| PLLP 位值 | 实际分频系数 |
|---|---|
| 00 | 2 |
| 01 | 4 |
| 10 | 6 |
| 11 | 8 |
所以要得到 168MHz,P 位要写00。另外 bit22 是 PLL 时钟源选择,0 表示 HSI,1 表示 HSE。把这些拼起来,8MHz 晶振配 168MHz 的配置值就是 M=8、N=336、P=0(对应除2)、Q=7、时钟源选 HSE。
启动顺序也不能乱,必须严格按这个流程走:
/* 1. 使能 HSE 并等待就绪 */ RCC->CR |= RCC_CR_HSEON; while ((RCC->CR & RCC_CR_HSERDY) == 0); /* 2. 配置 PLL 参数(必须在 PLL 关闭状态下写) */ RCC->PLLCFGR = (8 << 0) /* M = 8 */ | (336 << 6) /* N = 336 */ | (0 << 16) /* P = 2 */ | (RCC_PLLCFGR_PLLSRC_HSE) | (7 << 24); /* Q = 7 */ /* 3. 使能 PLL 并等待锁定 */ RCC->CR |= RCC_CR_PLLON; while ((RCC->CR & RCC_CR_PLLRDY) == 0); /* 4. 配置 Flash 等待周期与 ART,必须在切时钟之前完成 */ FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS; /* 5. 配置 AHB/APB 分频 */ RCC->CFGR |= RCC_CFGR_HPRE_DIV1 /* AHB = 168MHz */ | RCC_CFGR_PPRE1_DIV4 /* APB1 = 42MHz */ | RCC_CFGR_PPRE2_DIV2; /* APB2 = 84MHz */ /* 6. 切换系统时钟源到 PLL 并确认切换成功 */ RCC->CFGR |= RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);关键是第 4 步和第 6 步的顺序——Flash 等待周期一定要在切到高频之前设好,否则在切换的瞬间 CPU 会因为取指速度跟不上而跑飞,现象是"代码下载成功但一上电就死"。这个错误在动手写启动代码的人里出现频率很高。
有人会问能不能超频。F407 的内核确实有人拉到 200MHz 跑得起来,但这时候电压、温度、Flash 等待周期都要重新权衡,长时间运行出错概率明显上升。除非是做实验,我不建议在产品里超频,168MHz 对绝大多数应用已经足够。
3.3 中断优先级、SysTick 与向量表
Cortex-M4 的 NVIC 支持 4 位优先级,可以分成抢占优先级和子优先级两部分。F407 上 HAL 库默认用的是 Group 4,也就是 4 位全给抢占优先级,没有子优先级。这里最容易出的问题是数值越小优先级越高,跟很多人的直觉相反。如果你把串口中断设成 15,把定时器中断设成 0,那定时器会打断串口,反之则不会。
另一个高频坑是在中断服务函数里调用HAL_Delay()。这个函数依赖 SysTick 的中断来递减计数,如果当前中断的优先级高于或等于 SysTick 的优先级(默认是 15,最低),那么 SysTick 中断根本进不来,HAL_Delay就会永远卡在那里等待。表面现象是"串口接收中断一进就死机",其实原因是优先级配置。正确的做法是在中断里只置标志位,把耗时处理放到主循环,或者用一个不依赖中断的延时实现。
SysTick 的配置也值得留意。HAL 库默认把它配成 1ms 一次,用作系统时基。如果你的应用需要更细的时间粒度,可以改成 100µs,但要注意所有基于HAL_GetTick()的超时判断都会跟着变。向量表的位置一般在 Flash 起始处,如果你要做 IAP 升级(App 放在 0x08008000 之后),需要重设SCB->VTOR并调整链接脚本里的 Flash 起始地址和长度,这一步漏了会直接进 HardFault。
4. 三个能把资源用起来的实战
资源摆在那里不会自己变成生产力,得挑几个能真正压榨出价值的外设组合来练。下面这三个我都在实际项目里用过,从简单到复杂排一下,每个都给出可复现的关键代码和参数计算过程。
4.1 定时器触发 DAC + DMA 输出任意波形
用 F407 输出正弦波、三角波这类任意波形,最省 CPU 的做法是让定时器通过 TRGO 事件触发 DAC,DMA 负责把波形数据表循环搬到 DAC 的数据寄存器。整个过程不需要 CPU 参与,主循环可以去做别的事。
先算频率。假设我们要输出 1kHz 的正弦波,波形表用 100 个点。TIM6 挂在 APB1 上,前面算过 TIM6 的计数时钟是 84MHz。每个波形点之间的间隔时间是1 / (1000Hz × 100点) = 10µs,对应的定时器周期就是84MHz × 10µs = 840个计数。所以自动重装载值ARR = 840 - 1 = 839,预分频PSC = 0。
波形点之间如果间隔太小,DAC 的输出建立时间不够,波形顶部会被削平;如果间隔太大,高频分量会有明显的阶梯感。100 个点输出 1kHz 是个比较舒服的平衡,要输出更高频率就减少点数或者提高 DAC 更新率,但要接受的代价是波形分辨率下降。
生成 12 位数据表的 Python 脚本,比手算方便得多:
import math N = 100 for i in range(N): value = int(2047 * math.sin(2 * math.pi * i / N) + 2048) value &= 0x0FFF # 限制在 12 位范围 print(f"0x{value:03X},")得到的表放进代码里,初始化的核心部分是这样:
#include "stm32f4xx.h" #define WAVE_POINTS 100 extern const uint16_t sine_table[WAVE_POINTS]; void DAC_Wave_Init(void) { GPIO_InitTypeDef gpio; DAC_InitTypeDef dac; DMA_InitTypeDef dma; TIM_TimeBaseInitTypeDef tim; /* 时钟:DMA1、GPIOA 在 AHB1,DAC 和 TIM6 在 APB1 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1 | RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM6, ENABLE); /* PA4 配置为模拟输入模式,这是 DAC_OUT1 的引脚 */ gpio.GPIO_Pin = GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_AN; gpio.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &gpio); /* TIM6 作为触发源,TRGO 选更新事件 */ tim.TIM_Prescaler = 0; tim.TIM_Period = 839; /* 84MHz / 840 = 100kHz 触发 */ tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, &tim); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); /* DAC 通道 1:由 TIM6 的 TRGO 触发,开输出缓冲 */ DAC_StructInit(&dac); dac.DAC_Trigger = DAC_Trigger_T6_TRGO; dac.DAC_WaveGeneration = DAC_WaveGeneration_None; dac.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &dac); /* DMA1 Stream5 Channel7 对应 DAC1 */ DMA_StructInit(&dma); dma.DMA_Channel = DMA_Channel_7; dma.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1; dma.DMA_Memory0BaseAddr = (uint32_t)sine_table; dma.DMA_DIR = DMA_DIR_MemoryToPeripheral; dma.DMA_BufferSize = WAVE_POINTS; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; dma.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Stream5, &dma); DMA_Cmd(DMA1_Stream5, ENABLE); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); TIM_Cmd(TIM6, ENABLE); }这套配置里有三个容易踩的点。第一,DMA 的数据宽度必须和 DAC 寄存器匹配,DAC->DHR12R1是 32 位寄存器但只用低 12 位,配成半字(16 位)访问是对的,配成字节会乱套。第二,DAC 的输出缓冲建议打开,它能提高驱动能力,但代价是输出不能完全到 0 和满量程,做精密基准的时候要注意这个限制。第三,DAC 的数据表要放在 DMA 能访问的内存里,千万别放进 CCM 区,前面说过 CCM 是 DMA 够不着的。
实测下来这套方案的输出频率上限大概在几十 kHz 量级,再往上就受 DAC 建立时间和定时器更新率的限制了。如果要做音频播放,通常是直接播 8kHz 或 16kHz 采样的音频数据,通过 DMA 双缓冲送到 DAC,思路和上面完全一样,只是数据表变成了更长的音频采样序列。
4.2 FSMC 挂 SDRAM:给图像和 UI 腾地方
192KB 的内部 SRAM 听起来不少,但你一旦要开摄像头帧缓冲或者跑图形界面,很快就见底了。这时候 FSMC 外挂 SDRAM 就是标准解法。F407 的 FSMC 支持 SDRAM,映射到 0xC0000000 和 0xD0000000 两个 bank 地址,用起来像访问数组一样简单。
时钟是第一个要算清楚的。FSMC 的时钟源是 HCLK,也就是 168MHz。SDCLK 通过FSMC_SDClockPeriod配置成 HCLK 的 1/2 或 1/3,也就是 84MHz 或 56MHz,不能配成 1/1 因为 168MHz 对绝大多数 SDRAM 芯片来说太快了。常见的 16 位 SDRAM 芯片(比如 W9825G6KH-6)速度等级是 166MHz,84MHz 完全没问题;如果是 -7 等级的芯片,上限 143MHz,54MHz 更稳妥,那就用 56MHz 那一档。
时序参数需要根据 SDRAM 数据手册上的纳秒值换算成时钟周期数。84MHz 下一个周期是 11.9ns,我给出下面这组常用的配置值,它们在 84MHz 下对大多数 16 位 SDRAM 都能稳定工作:
| 参数 | 寄存器的值 | 含义 |
|---|---|---|
| LoadToActiveDelay | 2 | 上电到激活命令的延迟 |
| ExitSelfRefreshDelay | 7 | 退出自刷新后的等待周期 |
| SelfRefreshTime | 4 | 自刷新最短维持时间 |
| RowCycleDelay | 7 | 行周期时间 |
| WriteRecoveryTime | 2 | 写恢复时间 |
| RPDelay | 2 | 预充电到激活的延迟 |
| RCDDelay | 2 | 行到列的延迟 |
具体配置代码大致是这样:
void SDRAM_Init(void) { FMC_SDRAMInitTypeDef init; FMC_SDRAMTimingInitTypeDef timing; FMC_SDRAMCommandTypeDef cmd; /* 时钟与 GPIO 初始化省略,重点是下面这几段 */ timing.FMC_LoadToActiveDelay = 2; timing.FMC_ExitSelfRefreshDelay = 7; timing.FMC_SelfRefreshTime = 4; timing.FMC_RowCycleDelay = 7; timing.FMC_WriteRecoveryTime = 2; timing.FMC_RPDelay = 2; timing.FMC_RCDDelay = 2; init.FMC_Bank = FMC_SDRAM_Bank1; init.FMC_ColumnBitsNumber = FMC_ColumnBits_Number_9b; init.FMC_RowBitsNumber = FMC_RowBits_Number_13b; init.FMC_SDMemoryDataWidth = FMC_SDMemory_Width_16b; init.FMC_InternalBankNumber = FMC_InternalBank_Number_4; init.FMC_CASLatency = FMC_CAS_Latency_3; init.FMC_WriteProtection = FMC_Write_Protection_Disable; init.FMC_SDClockPeriod = FMC_SDClock_Period_2; /* HCLK/2 = 84MHz */ init.FMC_ReadBurst = FMC_Read_Burst_Disable; init.FMC_ReadPipeDelay = FMC_ReadPipe_Delay_1; init.FMC_SDRAMTimingStruct = timing; FMC_SDRAMInit(&init); /* 时钟配置寄存器:刷新计数根据 SDRAM 手册的 tREF 算 */ FMC_SDRAMProgramPMSD(FMC_SDRAM_ProgramRefreshRate, 1386); /* 发送 SDRAM 初始化命令序列 */ cmd.FMC_CommandMode = FMC_Command_Mode_CLK_Enabled; cmd.FMC_CommandTarget = FMC_Command_Target_bank1; cmd.FMC_AutoRefreshNumber = 1; cmd.FMC_ModeRegisterDefinition = 0; FMC_SDRAMCmdConfig(&cmd); /* 等待,然后发送预充电全部 bank 命令 */ /* ...后续按手册的初始化流程依次下发命令... */ }刷新计数1386这个数字来自count = (SDRAM 刷新周期 / 行数) × SDCLK频率的估算。以 64ms 刷新周期、8192 行、84MHz 时钟为例,理论值是 64ms / 8192 × 84MHz ≈ 656,但 HAL 里实际写入的寄存器值是按count - 20之类的偏移算的,具体要用 CubeMX 生成的值为准,我这里给的是经验值,不同 SDRAM 型号要对着手册核。
SDRAM 配好之后,最要紧的是验证它真的能稳定读写。光看初始化返回值正常不代表什么,我一般的做法是跑一段内存测试:先写全 0,读回校验;再写全 1,读回校验;然后写地址相关的花样数据(比如地址值本身取反),读回校验。这一套能查出大部分焊接和时序问题。如果测试偶尔失败,先降 SDCLK 到 HCLK/3 试试,如果降频就好了,说明是时序参数太紧或者 PCB 走线质量的问题。
4.3 DCMI + DMA 双缓冲采图
F407 的 DCMI 接口支持 8 位、10 位、12 位、14 位并行数据,配合 DMA 可以做"采集一帧、处理一帧"的流水线。这个接口是 144 脚封装的主要价值之一,因为光是把数据线、像素时钟、行同步、场同步这些引脚凑齐就要十几个 IO。
以常见的 OV2640 模块为例,典型的接法是 D0 到 D7 走 PC6 到 PC9 加 PE4、PB6、PE6、PB9,VSYNC 走 PB7,HSYNC 走 PA4,PIXCLK 走 PA6,XCLK 由 PA8 的 MCO1 输出 24MHz 给摄像头。但不同板子的接法会有差异,接线之前一定对着原理图和摄像头模块的说明核一遍,接错了现象是"一直采不到数据",而且不容易看出来。
配置的关键是 DMA 双缓冲和帧中断:
/* DMA2 Stream1 Channel1 是 DCMI 的专用通道 */ void DCMI_DMA_Init(uint32_t buf0, uint32_t buf1, uint32_t len) { DMA_InitTypeDef dma; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream1); dma.DMA_Channel = DMA_Channel_1; dma.DMA_PeripheralBaseAddr = (uint32_t)&DCMI->DR; dma.DMA_Memory0BaseAddr = buf0; dma.DMA_DIR = DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize = len; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; dma.DMA_Mode = DMA_Mode_Circular; /* 使用双缓冲需循环模式 */ dma.DMA_Priority = DMA_Priority_High; dma.DMA_FIFOMode = DMA_FIFOMode_Enable; dma.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; dma.DMA_MemoryBurst = DMA_MemoryBurst_INC4; dma.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA2_Stream1, &dma); /* 挂第二块缓冲区,开启双缓冲模式 */ DMA_DoubleBufferModeConfig(DMA2_Stream1, buf1, DMA_Memory_1); DMA_DoubleBufferModeCmd(DMA2_Stream1, ENABLE); /* 帧中断:每采完一帧进一次,用来切换处理缓冲区 */ DMA_ITConfig(DMA2_Stream1, DMA_IT_TC, ENABLE); DMA_Cmd(DMA2_Stream1, ENABLE); }DCMI 本身还要配同步方式(硬件同步用 VSYNC/HSYNC 还是嵌入码同步)、数据宽度、捕获模式(快照还是连续)、以及极性。极性配错是最常见的问题:HSYNC 和 VSYNC 的有效电平、PIXCLK 的采样边沿,这几项都得对着摄像头的时序图一项项核对。
两帧缓冲区最好放在外挂的 SDRAM 里,一块采集、一块处理。但这里有个容易被忽视的问题:DCMI 通过 DMA 写 SDRAM,同时 CPU 通过 FSMC 读 SDRAM 做处理,两路访问会争抢总线带宽。如果帧率不高、分辨率不大(比如 320×240 的 RGB565,一帧 150KB),问题不明显;一旦上到 640×480 甚至更高,就会出现丢帧或者撕裂。缓解的办法有几个:把 CPU 的处理放在帧间空闲期做、用 DMA 把数据搬到内部 SRAM 再处理、或者降低帧率让总线有喘息的机会。这在设计阶段就要算清楚,不要等发现丢帧才回头改架构。
5. 问题排查手册:从"不亮"到"跑飞"
调试这件事,有价值的部分不是"我知道怎么解决",而是"我知道先查哪里"。下面按问题类型整理一套排查路径,都是我在实际项目里反复用到的。
5.1 上电不运行类问题
这类问题的特点是"看起来很严重但原因往往很简单",按顺序过一遍效率最高。
| 现象 | 优先排查项 | 判断方法 |
|---|---|---|
| 下载器连不上芯片 | 供电是否正常、SWD 引脚是否被复用、BOOT0 电平 | 万用表量 VDD 和 VCAP 电压,量 BOOT0 |
| 下载成功但一上电就死 | Flash 等待周期、VCAP 电容、晶振起振 | 用示波器看晶振;查 VCAP 是否接了 2.2µF |
| 程序跑到某处卡住不动 | HSE 起振失败、时钟切换卡在等待循环 | 单步跟到while(RCC->CR & RCC_CR_HSERDY)看是否死循环 |
| 复位后随机跑飞 | 电源纹波、去耦不足、堆栈溢出 | 示波器看 3.3V 纹波;检查启动文件里的栈大小 |
| 偶尔能跑,大部分时间不行 | 晶振负载电容不匹配 | 换 12pF/15pF 电容试,或用示波器量起振时间 |
VCAP 忘了接电容这个坑我前面提过,但它值得再强调一次,因为现象太像"软件问题"了。如果你在 F1 上从来没遇到过,转到 F4 后第一次画板子就要把这两根脚写进 checklist。
电源纹波也是个隐形杀手。168MHz 主频下的电流变化很剧烈,如果 3.3V 的走线太细、去耦电容离芯片太远,芯片看到的实际电压会有明显的波动,表现为随机复位或外设异常。用示波器配合短地线的探头量芯片引脚处的电源,如果峰峰值超过 100mV 就需要注意了。
5.2 外设不工作类问题
外设问题的排查有一套通用逻辑:先确认时钟开了没,再确认引脚复用配对了没,最后确认外设本身的参数配置对不对。这个顺序能覆盖八成以上的问题。
串口不通或者乱码。乱码九成是波特率算错了。要注意串口挂在哪条总线上:USART1 和 USART6 在 APB2 上,时钟 84MHz;USART2、USART3、UART4、UART5 在 APB1 上,时钟 42MHz。同一套波特率寄存器值用在不同的总线上,出来的波特率差一倍。另外要注意过采样模式,F4 的串口支持 16 倍和 8 倍过采样,8 倍模式能提高高速率下的精度,但寄存器计算方式不同。
ADC 采样值不稳或者偏差大。先看采样时间够不够。F407 的 ADC 采样时间有 3、15、28、56、84、112、144、480 个周期可选,采样时间越短对信号源阻抗要求越高。大致对应关系是:3 周期适合 0.4kΩ 以下的源阻抗,15 周期约 5.9kΩ,56 周期约 25kΩ,144 周期约 65kΩ,480 周期约 220kΩ。如果你在采样一个通过分压电阻来的信号,源阻抗偏高但采样时间设成了 3 周期,读数必然偏低或者跳动。
还有个容易忽略的点是ADC 时钟不能超过 36MHz。PCLK2 是 84MHz,预分频选 2 得到 42MHz 就超标了,只能选 4 得到 21MHz。转换时间按(采样周期数 + 12) / ADCCLK算,21MHz 配 56 周期就是 3.24µs,对应约 300kSPS 的采样率。想要更高速度就得上多重 ADC 交替采样模式,理论上三路交错能到 7.2MSPS,但代价是引脚和配置复杂度都上去了。
DMA 不触发。这个问题的经典原因是中断标志没清。DMA 的传输完成标志在某些外设下需要手动清除,忘了清会导致第二次传输进不了中断。另一个原因是通道映射错:DMA1 和 DMA2 各有 8 个流,每个流有 8 个可选通道,具体哪个流对应哪个外设是硬件固定的,不能随便配。比如 DAC1 对应 DMA1 Stream5 Channel7,DCMI 对应 DMA2 Stream1 Channel1,查错的时候要翻参考手册的 DMA 请求映射表。
以太网收不到包。按这个顺序查:PHY 的复位引脚有没有被正确拉高、REF_CLK 有没有 50MHz 时钟进来、MDC/MDIO 能不能读到 PHY 的 ID 寄存器、PHY 地址配对了没。能读到 ID 说明管理接口通了,问题在数据和时钟线上;读不到 ID 说明 MDC/MDIO 或者 PHY 供电有问题。RMII 模式下那 9 根信号线一根都不能接错,接错了现象是"发送正常接收不正常"或者"完全没反应"。
5.3 HardFault 定位实操
HardFault 是没有经验的开发者最头疼的问题,因为现象就是"死机了,没有任何线索"。其实 Cortex-M4 提供了完整的故障诊断寄存器,用起来并不复杂。
进 HardFault 之后,第一件事是看SCB->CFSR这个寄存器,它把故障分成了几大类:
| 寄存器位段 | 名称 | 含义 |
|---|---|---|
| CFSR[7:0] | MMFSR | 存储器管理故障,访问了非法地址 |
| CFSR[15:8] | BFSR | 总线故障,访问了不存在的内存或总线错误 |
| CFSR[31:16] | UFSR | 用法故障,除零、非法指令、非对齐访问等 |
如果 BFSR 里的BFARVALID位是 1,那么SCB->BFAR里存的就是出错的地址,直接看这个地址就能判断问题在哪。常见的值有:访问了 0x00000000 附近说明空指针解引用;访问了 CCM 区的地址但由 DMA 发起,说明 DMA 缓冲区放错位置了;访问了0xE0000000以上的地址,可能是外设时钟没开就操作寄存器。
如果UFSR里的DIVBYZERO位是 1,那就是除零。整数除以零在 Cortex-M4 上不会像浮点那样默认返回 Inf,而是直接触发用法故障。这类问题通常出现在算法里没做零判断的保护。
栈溢出是另一个高频原因。启动文件里有个Stack_Size的宏,默认值往往是 0x400(1KB)左右,对于用了一些库函数的工程来说明显不够。判断方法很简单:把栈的初始内容填成固定值,运行一段时间后看有多少被覆盖了,如果接近栈底就说明快溢出了。我一般的做法是把栈调到 0x800 甚至 0x1000,如果用了图形库或者比较深的函数调用链,还要再往上加。
/* 在启动代码里把栈区域填充为固定模式,方便事后统计使用量 */ extern uint32_t _estack; void stack_check(void) { /* 具体实现依赖链接脚本里的段定义,思路是从栈顶向下扫描固定模式 */ /* 找到第一个被修改的位置,就能算出栈的峰值使用量 */ }最后分享一个定位技巧:在 HardFault 的处理函数里把出错时的寄存器现场保存下来,特别是 PC 和 LR 的值,通过 PC 去反查反汇编,能直接定位到出错的指令位置。Keil 和 GCC 都可以生成反汇编文件,用地址去搜索对应行,比盲猜快得多。
6. 学习路线与踩坑经验
前面讲了这么多技术细节,最后聊聊怎么把这块芯片真正学明白。F407 的资料量很大,但也正因如此容易迷失,东学一点西学一点,几个月下来还是只会点灯和串口。
6.1 从 51/STC 转到 F407 的过渡
很多人的嵌入式入门是从 51 或者 STC 开始的,这没什么不好,点灯、按键、数码管、串口这些基础概念在哪都有用。但从 51 转到 F407,有几个思维上的转变必须完成。
第一是从"直接操作寄存器"转到"理解时钟树和总线"。51 单片机的时钟基本是固定的,外设开关用几个寄存器位就搞定了。F407 上每个外设都挂在不同总线上,用之前要开对应的时钟门控,总线频率还决定了外设的工作频率和分频计算。不理解这一层,麻烦会一直跟着你。
第二是从"一个主循环打天下"转到"中断加缓冲区加状态机"。51 项目里常见的写法是主循环里轮询,但 F407 的外设速度快、数据量大,串口一秒钟能收上千个字节,轮询处理不过来。必须习惯用中断接收加环形缓冲区的方式,把数据接收和处理解耦。
第三是从"单文件搞定"转到"分层组织代码"。51 项目往往一个文件几百行就完了,F407 的外设多、初始化代码长,不分层的话维护起来很痛苦。我习惯按"硬件抽象层(外设初始化加读写接口)、业务逻辑层、主程序"三层来组织,外设驱动写好后可以跨项目复用。
转速这块不用急。先把串口、定时器、GPIO 这三个用熟,再往上加 ADC、DMA、SPI,最后碰 FSMC 和以太网。跳步的代价是遇到问题时不知道是哪一层出了问题。
6.2 项目练手梯度
学习路线我建议按这个梯度来,每一级都做到"能复现、能解释原理、能改参数看到预期变化"再进下一级。
第一阶段是基础外设。GPIO 点灯加按键消抖,串口收发加 printf 重定向,定时器做精确延时和 PWM 输出。这一阶段的目标是熟悉工程结构和调试流程,不要追求功能复杂。
第二阶段是数据通路。ADC 单通道采样、ADC 加 DMA 多通道扫描、DAC 输出波形、SPI 读写传感器、I2C 读温湿度。这一阶段的核心是理解 DMA 的工作方式,把"外设加 DMA 加中断"这个组合拳打熟。
第三阶段是存储与显示。FSMC 挂 SDRAM、SPI 或 FSMC 接 LCD 屏、跑一个简单的图形库。这一阶段你会第一次感受到"外部总线"的复杂性,时序参数、走线质量、带宽分配都会成为问题。
第四阶段是系统集成。DCMI 采图、以太网传输、SD 卡存储,把多个高速外设组合起来。这一阶段会遇到总线带宽竞争、中断优先级冲突、内存分配策略这些真实工程问题。
第五阶段是引入实时操作系统。FreeRTOS 或者 RT-Thread 都可以,重点是理解任务划分、优先级安排、任务间通信。有了前面四个阶段的基础,你会很清楚哪些操作该放在哪个任务里,而不是照抄示例。
如果是为了参加竞赛或者丰富简历,第三阶段和第四阶段的项目最有说服力。一个"基于 F407 的图像采集与网络传输系统",涉及的外设数量和调试难度都足够体现能力。
6.3 几个我踩过的坑
写到这里,分享几个我在 F407 项目里真实踩过的坑,都是那种"当时觉得很冤,事后觉得很值"的经验。
第一个是把 DMA 缓冲区放到了 CCM 区。当时为了让数据访问更快,特意把缓冲区定义到了 CCM 的地址段,结果 DMA 完全不动。查了整整两天,最后在参考手册的内存映射章节看到 CCM 只挂在内核总线上。现在我的习惯是:只要是给 DMA 用的缓冲区,一律不碰 CCM。
第二个是中断里用了带等待的逻辑。早期写串口接收处理,习惯在中断里直接处理完一整帧数据,中间还调了一个等待函数。结果是低波特率下没事,一旦波特率上到 921600 就开始丢数据。后来改成中断里只往环形缓冲区里塞字节,主循环里做协议解析,问题立刻消失。这个转变我用了挺久才想通,本质上是要把中断的"停留时间"压到最短。
第三个是忽略了代码体积限制。用 Keil 的免费版本开发时,工程一旦加了文件系统加图形库加协议栈,32KB 的编译限制直接被顶爆。当时的第一反应是"我代码没写多少啊",实际上是库文件的体积比想象中大得多。要么换成 GCC 工具链,要么买授权,绕不过去。
第四个是 APP 升级时忘了重设向量表偏移。做串口或者 SD 卡固件升级的时候,App 从 0x08008000 开始存放,链接脚本改了、跳转逻辑写了,但忘了在 App 开头设置SCB->VTOR = 0x08008000。现象是升级完之后中断全部失效,串口能发不能收。加一行代码就解决了,但找这个问题花了半天。
第五个是 PCB 上 SDRAM 的数据线没有做等长。理论上 84MHz 的频率对走线长度不敏感,但实际板子上如果数据线长度差得太多,加上没有完整的参考平面,就会出现"大部分时候正常、偶尔读出错误数据"的情况。这种偶发问题最难查,因为内存测试跑一万次才错一次。后来我养成的习惯是:只要上 SDRAM,就走四层板,数据线和地址线尽量等长,参考平面完整不断裂。
最后再分享一个小技巧。F407 有 96 位的唯一设备 ID 和硬件 CRC 单元,做产品的时候可以用来生成设备序列号或者做固件的完整性校验,不需要额外加芯片。唯一 ID 存在0x1FFF7A10开始的地址,用一个指针读出来就行,硬件 CRC 单元在RCC->AHB1ENR里开时钟之后,往CRC->DR写数据就能算。这两个外设平时没人注意,真到需要的时候挺好用。