1. 从“理论”到“能跑”STM32学习路径的底层逻辑很多人看到“STM32理论”这四个字第一反应就是去翻参考手册、看内核架构图、背时钟树。我刚开始接触STM32的时候也这样抱着F103的英文手册啃了三天结果连一个LED都点不亮。后来才想明白一件事STM32的理论不是用来“背”的而是用来“解释现象”的。你不需要在动手之前把所有的理论都搞懂但你必须知道每个操作背后大概发生了什么这样出了问题才有排查的方向。STM32是ST公司基于ARM Cortex-M内核的一系列32位微控制器。市面上最常见的F103系列用的是Cortex-M3内核F4系列是M4H7系列是M7。这些内核的区别主要在于运算能力、浮点单元、DSP指令集等但对于大多数入门和中级项目来说你真正需要关心的不是内核微架构而是外设怎么配、时钟怎么给、中断怎么响应。这三个问题贯穿了几乎所有STM32项目从最简单的点灯到复杂的以太网通信本质上都是在处理这三件事。这篇文章适合谁看如果你刚开始学STM32被各种库函数和寄存器搞得头晕或者你已经能跑例程但遇到问题不知道怎么排查又或者你正在做毕业设计、准备面试需要把零散的知识串成体系——那这篇内容就是写给你的。我会从实际项目的角度出发把STM32的核心理论拆开揉碎告诉你每个知识点在实际开发中到底怎么用、为什么这么用、用错了会怎样。2. 时钟树STM32的“心跳”到底怎么调2.1 为什么时钟配置是第一个要搞懂的东西STM32的时钟系统就像一栋大楼的供电网络。外部晶振是发电厂PLL是变压器AHB、APB1、APB2总线是不同楼层的配电箱每个外设是具体的用电设备。你给外设供电之前必须先确保配电箱有电而且电压要对。很多新手写代码的时候发现串口没输出、定时器不计数折腾半天代码逻辑最后发现是对应的总线时钟根本没使能。我见过太多人在RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)这行代码上栽跟头。GPIOA挂在APB2总线上你不开这个时钟后面怎么配寄存器都没用。这不是理论问题这是实操中第一个会遇到的坑。STM32F103的时钟源主要有四个HSI内部高速时钟8MHz、HSE外部高速时钟通常8MHz晶振、LSI内部低速40kHz给看门狗用、LSE外部低速32.768kHz给RTC用。系统时钟SYSCLK可以来自HSI、HSE或者PLL输出。PLL可以把HSE倍频到72MHzF103的最大系统时钟然后通过AHB预分频器分给HCLK再通过APB1和APB2预分频器分给各自的外设。这里有个关键限制APB1的最大时钟是36MHzAPB2的最大时钟是72MHz。如果你把APB1的分频系数设成1系统时钟72MHz直接灌进去APB1上的外设比如USART2、TIM2就会工作不正常。标准库的SystemInit函数默认帮你配好了72MHz的系统时钟但如果你自己写时钟初始化这个分频系数一定要算清楚。2.2 时钟配置的实操步骤与参数计算假设你用外部8MHz晶振目标系统时钟72MHz。计算过程是这样的HSE8MHzPLL倍频系数选9得到72MHz。然后AHB不分频HCLK72MHz。APB2不分频PCLK272MHz。APB1二分频PCLK136MHz。这套配置在标准库的system_stm32f10x.c里已经写好了你直接调用SystemInit()就行。但如果你用的是F4系列情况会复杂一些。F4的PLL配置涉及P、Q、R等多个分频系数还有主PLL和I2S PLL的区别。我一般建议新手先用CubeMX生成时钟配置代码看懂之后再自己手写。CubeMX的时钟树界面会实时显示每个节点的频率你改一个参数下面所有相关频率都会跟着变非常直观。注意外部晶振的负载电容要匹配。我遇到过一块板子晶振旁边焊了22pF的电容结果HSE起振不稳定系统时钟偶尔会切回HSI。后来换成12pF就正常了。如果你发现程序跑着跑着频率变了先检查晶振电路。2.3 时钟安全系统与备份域STM32有一个时钟安全系统CSS当HSE失效时硬件会自动切换到HSI并产生一个中断。这个功能在工业控制场景里很有用因为外部晶振可能会因为振动、温度等原因停振。你可以在CSS中断里做一些安全处理比如关闭电机、保存数据。备份域Backup Domain是另一个容易被忽略的部分。RTC和备份寄存器挂在备份域上由VBAT供电。如果你想让RTC在断电后继续走时就需要在VBAT引脚接一个纽扣电池。这里有个细节备份域的写保护。你要先调用PWR_BackupAccessCmd(ENABLE)才能写RTC寄存器否则写不进去。这个坑我在做数据记录仪的时候踩过调了一下午才发现是写保护没开。3. GPIO与最小系统从点灯到按键的完整设计3.1 GPIO的八种模式到底怎么选STM32的GPIO有八种模式输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。新手最常问的问题是我点个LED应该用哪种答案是推挽输出。推挽输出的驱动能力强能直接驱动LED记得串限流电阻。开漏输出一般用于I2C总线或者需要电平转换的场景因为它只能拉低拉高需要外部上拉电阻。按键输入用哪种模式如果按键一端接GPIO另一端接GND那GPIO应该配成输入上拉。这样按键没按下时内部上拉电阻把引脚拉到高电平按下时引脚被拉到GND读出来是低电平。如果你配成浮空输入引脚电平不确定读出来的值会乱跳。模拟输入用于ADC采样。这时候GPIO的施密特触发器会被关闭引脚直接连到ADC的模拟输入端。如果你用ADC采样但GPIO配成了其他模式采出来的值会不准或者根本不变。复用功能模式用于外设引脚比如USART的TX、RXSPI的SCK、MOSI等。配置复用功能的时候除了要设置GPIO模式还要使能对应的外设时钟并且调用GPIO_PinAFConfigF4系列或者直接配置寄存器F1系列来选择具体的复用功能编号。3.2 最小系统板的原理图要点一个能跑起来的STM32最小系统需要以下几部分电源电路3.3V稳压、晶振电路HSE和LSE、复位电路、启动模式选择电路、调试接口SWD或JTAG。电源部分STM32的VDD和VDDA都要接3.3VVSS和VSSA接地。VDDA是模拟部分的电源即使你不用ADC也建议接上并加滤波电容。每个VDD引脚旁边都要放一个100nF的去耦电容这个电容要尽量靠近引脚否则滤波效果大打折扣。复位电路就是一个10k电阻上拉到3.3V再加一个100nF电容到地。有些设计会加一个复位按键按下时把NRST拉到地。注意NRST引脚内部已经有上拉电阻外部这个10k是冗余设计但加上更稳妥。启动模式由BOOT0和BOOT1两个引脚决定。BOOT00时从主Flash启动这是正常运行的模式。BOOT01、BOOT10时从系统存储器启动用于串口下载程序。BOOT01、BOOT11时从SRAM启动一般用于调试。实际产品中BOOT0通常通过一个电阻下拉到地需要下载时再通过跳线帽拉到3.3V。调试接口我强烈推荐用SWD只需要SWDIO和SWCLK两根线加上VCC和GND一共四根。JTAG需要五根线而且占用更多引脚。STM32默认启用JTAG和SWD如果你想把JTAG占用的引脚PA15、PB3、PB4当普通GPIO用需要调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)来禁用JTAG保留SWD。这个操作在标准库里叫“禁用JTAG”在HAL库里叫“复用功能重映射”。3.3 按键模块的硬件设计与软件消抖按键电路看起来简单但要做好并不容易。最简单的方案是按键一端接GPIO另一端接GNDGPIO配成输入上拉。但实际使用中按键按下和松开时会有抖动一般持续5到20毫秒。如果你在中断里直接响应按键一次按下可能会触发多次中断。软件消抖的常规做法是检测到按键按下后延时20毫秒再检测一次如果还是按下状态才确认按键有效。但延时消抖会阻塞CPU在RTOS环境下不推荐。更好的做法是用定时器定期扫描按键状态或者用状态机来管理按键的按下、保持、释放三个状态。硬件消抖可以在按键两端并联一个100nF电容再串一个100欧姆电阻。电容吸收抖动电阻限制放电电流。但电容太大会导致按键响应变慢100nF是个比较平衡的值。实操心得如果你用按键触发外部中断记得把中断触发方式设成下降沿触发并且在中断服务函数里做消抖处理。我见过有人在中断里直接翻转LED结果按一次键LED闪了好几次就是因为没有消抖。4. 中断系统与定时器从理论到实战4.1 NVIC中断优先级的分组与抢占STM32的中断优先级由NVIC管理每个中断有一个8位优先级寄存器但实际使用的位数由优先级分组决定。F1系列支持5个分组从0到4。分组0表示4位都是抢占优先级没有子优先级分组4表示4位都是子优先级没有抢占优先级。抢占优先级决定中断能不能嵌套。高抢占优先级的中断可以打断低抢占优先级的中断。子优先级只在多个中断同时挂起时决定谁先执行不能嵌套。我一般用分组2即2位抢占优先级、2位子优先级。这样抢占优先级有4个级别子优先级也有4个级别足够大多数项目使用。这里有个容易混淆的地方优先级数值越小优先级越高。抢占优先级为0的中断可以打断抢占优先级为1的中断。很多人第一次配的时候会搞反以为数值越大越优先。4.2 定时器的三种核心模式STM32的定时器功能非常丰富但最常用的就三种模式基本定时、PWM输出、输入捕获。基本定时模式就是定时器从0数到自动重装载值ARR然后产生更新中断。定时时间 (ARR1) × (PSC1) / 定时器时钟频率。假设定时器时钟72MHzPSC7199ARR9999那么定时时间 10000 × 7200 / 72000000 1秒。这个公式要记牢配定时器的时候直接套。PWM输出模式用于控制电机速度、LED亮度等。PWM频率 定时器时钟 / ((ARR1) × (PSC1))占空比 CCR / (ARR1)。假设你要生成1kHz的PWM定时器时钟72MHzPSC71那么ARR 72000000 / (1000 × 72) - 1 999。占空比50%时CCR设为500。输入捕获模式用于测量脉冲宽度或频率。当引脚上出现指定边沿时定时器的当前计数值会被锁存到捕获寄存器里。通过两次捕获的差值可以算出脉冲宽度。测量频率时可以用两个定时器一个负责计数一个负责在固定时间内统计脉冲个数。4.3 定时器捕获测频率的实操细节用输入捕获测频率核心思路是配置定时器为输入捕获模式捕获边沿设为上升沿开启捕获中断。在中断里读取捕获值计算两次捕获的时间差频率 1 / 时间差。但这里有个问题如果信号频率很低定时器可能会溢出。比如72MHz的定时器ARR设为65535溢出周期大约是0.9毫秒。如果信号周期大于0.9毫秒两次捕获之间定时器会溢出多次。这时候需要在中断里记录溢出次数最终时间 溢出次数 × 溢出周期 捕获差值。如果信号频率很高比如几MHz捕获中断会非常频繁CPU可能来不及处理。这时候可以用定时器的从模式让外部信号触发定时器复位定时器计数值直接反映信号周期。这种方法不需要中断精度也更高。注意输入捕获的引脚要配置成复用功能并且要确保对应的定时器通道使能。我见过有人把信号接到TIM2_CH1PA0但代码里配置的是TIM3结果怎么都捕获不到。5. 通信接口串口、I2C、SPI的实战要点5.1 串口通信的完整配置流程串口是STM32最常用的通信接口也是调试利器。配置串口的步骤是使能GPIO时钟和USART时钟、配置GPIO为复用推挽输出TX和浮空输入RX、配置USART参数波特率、数据位、停止位、校验位、使能USART、如果需要中断则配置NVIC。波特率的计算USARTDIV 72MHz / (16 × 波特率)。假设波特率115200USARTDIV 72000000 / (16 × 115200) 39.0625。整数部分是39小数部分是0.0625 × 16 1。所以BRR寄存器的值 (39 4) | 1 0x271。标准库的USART_Init函数会自动帮你算但你要知道这个原理因为有时候波特率误差太大会导致通信失败。串口中断接收数据时要注意接收中断和空闲中断的配合。接收中断每收到一个字节触发一次空闲中断在总线空闲时触发。用空闲中断可以判断一帧数据是否接收完毕。这个技巧在Modbus通信、GPS数据解析等场景非常实用。5.2 I2C总线的时序与常见问题I2C是两根线的总线SCL和SDA。STM32的硬件I2C外设功能完整但早期型号比如F103的硬件I2C有一些已知问题比如在特定条件下会锁死。很多开发者宁愿用软件模拟I2C也不愿意用硬件I2C。软件模拟I2C的好处是引脚灵活、时序可控缺点是占用CPU时间。硬件I2C的好处是速度快、不占CPU缺点是调试麻烦。如果你用硬件I2C记得在初始化时把GPIO配成开漏复用模式并且外接上拉电阻通常4.7k。I2C通信失败最常见的原因是地址不对。很多传感器的7位地址在数据手册里写的是8位格式最低位是读写位。比如某传感器地址是0x68左移一位变成0xD0写和0xD1读。如果你直接把0x68当设备地址用肯定通信失败。5.3 SPI接口的四种模式SPI有四种模式由CPOL时钟极性和CPHA时钟相位组合决定。CPOL0表示时钟空闲时为低电平CPOL1表示空闲时为高电平。CPHA0表示在第一个时钟边沿采样CPHA1表示在第二个时钟边沿采样。大多数SPI Flash和传感器用模式0CPOL0CPHA0或模式3CPOL1CPHA1。具体用哪种模式要看器件手册。如果你发现SPI读出来的数据全是0xFF或者0x00先检查模式对不对。SPI的片选信号CS一般用普通GPIO控制拉低选中器件拉高取消选中。有些SPI器件要求CS在每字节之间拉高有些要求整帧数据期间一直拉低。这个细节要看手册搞错了通信会不稳定。6. 常见问题排查与避坑指南6.1 程序下载失败与调试器连接问题“No target connected”是新手最常遇到的错误。排查顺序是检查调试器驱动是否安装、检查SWD线序是否正确、检查目标板是否供电、检查复位引脚是否被拉低、检查芯片是否被读保护。ST-Link Utility是一个很实用的工具可以读取芯片信息、解除读保护、擦除Flash。如果你不小心把读保护打开了用ST-Link Utility连接时会提示“Cannot read memory”这时候在Target菜单里选择“Option Bytes”把读保护关掉就行。Keil5兼容C51和STM32的安装问题也经常被问到。Keil5默认只装了ARM编译器如果你要编译51单片机代码需要单独安装C51编译器。安装的时候注意路径不要有中文和空格否则可能会报错。6.2 延时函数卡死的几种原因delay_ms卡死通常有几个原因系统时钟没配好、SysTick中断优先级太低被其他中断阻塞、在中断里调用了延时函数。标准库的delay_ms是基于SysTick的。SysTick是一个24位的递减计数器配置成1毫秒中断一次。如果系统时钟不对比如实际是8MHz但你以为是72MHz延时时间会差9倍。如果SysTick中断被更高优先级的中断长时间阻塞延时函数也会卡住。在中断服务函数里调用延时函数是大忌。中断里应该尽量快进快出延时操作会阻塞其他中断。如果确实需要在中断里做延时用硬件定时器或者空指令循环不要用SysTick延时。6.3 串口乱码与数据丢失串口乱码第一反应是波特率不对。但除了波特率还有几个可能晶振频率和代码里写的不一致、串口助手的数据位/停止位设置不匹配、TX和RX接反了。数据丢失通常是因为接收缓冲区溢出。如果你用中断接收但处理速度跟不上接收速度数据就会丢。解决办法是加大缓冲区或者用DMA接收。DMA可以在不占用CPU的情况下把串口数据搬到内存里等一帧数据接收完毕再通知CPU处理。实操心得调试串口的时候先发一个固定的字符串比如“Hello”确认能正常收到再发其他数据。如果“Hello”都收不到那肯定是配置问题不用怀疑数据内容。6.4 常见问题速查表现象可能原因排查方法程序下载失败调试器连接不良、芯片读保护检查SWD线序、用ST-Link Utility解除保护LED不亮GPIO时钟未使能、模式配置错误检查RCC配置、确认推挽输出串口无输出波特率错误、TX/RX接反用示波器看TX引脚波形定时器不计数时钟未使能、ARR/PSC配置错误检查RCC配置、重新计算定时参数中断不触发NVIC未配置、优先级分组错误检查NVIC_Init、确认中断使能I2C通信失败地址错误、上拉电阻缺失用逻辑分析仪抓时序、检查地址格式程序跑飞堆栈溢出、数组越界增大堆栈、检查数组边界7. 进阶方向从能跑到跑得稳7.1 基于STM32的毕业设计选题思路毕业设计最怕的就是选题太大做不完或者太小没内容写。我建议选一个有明确输入输出、能演示、有数据可展示的题目。比如智能台灯输入是光敏电阻和按键输出是LED亮度和OLED显示再加上定时功能和串口调试。这个题目涵盖了ADC、GPIO、I2C、定时器、串口工作量适中演示效果也好。鱼缸控制器也是个不错的选题温度采集DS18B20或ADC、水位检测、加热棒控制继电器或PWM、喂食电机控制、OLED显示、WiFi模块上传数据。这个题目综合性强但每个模块都有成熟的方案不会卡在某个点上。两轮差速小车是经典题目电机驱动PWM、编码器测速定时器编码器模式、PID控制、蓝牙遥控、超声波避障。这个题目的难点在PID调参和电机控制需要一定的调试经验。7.2 编码器模式与电机控制STM32的定时器有编码器模式可以直接读取增量式编码器的脉冲数和方向。配置方法是把定时器设成编码器模式两个通道分别接编码器的A相和B相定时器会自动根据两相的相位关系增减计数值。编码器模式的关键参数是计数方向和计数范围。如果编码器正转时计数值减小把其中一个通道的极性反转一下就行。计数范围默认是0到ARR如果你需要负数可以在读取时减去ARR/2或者用32位定时器。电机控制通常用PWM加方向引脚。PWM频率一般选10kHz到20kHz太低会有啸叫声太高开关损耗大。占空比决定速度方向引脚决定转向。如果电机抖动或者不转先检查PWM有没有输出再检查电机驱动模块的使能引脚。7.3 从标准库到HAL库的迁移标准库和HAL库最大的区别是抽象层次。标准库直接操作寄存器代码效率高但可移植性差。HAL库封装了底层细节代码可移植性好但效率略低而且有些地方逻辑比较复杂。如果你从标准库转到HAL库需要适应几个变化GPIO初始化从GPIO_Init变成HAL_GPIO_Init串口发送从USART_SendData变成HAL_UART_Transmit中断处理从直接写中断服务函数变成用回调函数。HAL库的回调机制一开始可能不太习惯但用熟了会发现代码结构更清晰。CubeMX是HAL库的好搭档可以图形化配置引脚、时钟、外设自动生成初始化代码。我一般用CubeMX生成框架然后在自己写的业务代码里调用HAL库函数。这样既保证了初始化不出错又保留了代码的灵活性。7.4 USB虚拟串口与OTA升级STM32的USB外设可以做很多事虚拟串口是最常用的功能之一。配置USB虚拟串口需要用到USB库ST提供了完整的例程。你只需要修改描述符里的VID、PID和字符串就能让电脑识别出一个串口设备。USB虚拟串口的好处是速度快全速12Mbps、不需要额外的USB转串口芯片。缺点是配置复杂而且不同系列的USB库不通用。F103的USB库和F4的USB库差别很大移植的时候要注意。OTA升级是通过串口、USB或者无线模块更新固件。基本思路是把新固件写到Flash的备份区然后跳转到备份区执行或者用Bootloader把备份区的固件搬到运行区。STM32的Flash操作要注意擦除粒度F103的Flash擦除最小单位是1KB写入最小单位是2字节。如果你要写的数据不是2字节对齐的需要先读到缓冲区再一起写。8. 开发环境与工具链的选择8.1 Keil、IAR与VSCode的对比Keil MDK是STM32开发最常用的IDE优点是上手快、资料多、调试方便。缺点是编辑器功能弱、代码补全差、收费。IAR的编译效率比Keil高但界面更复杂收费也更贵。VSCode加插件是近年来越来越流行的方案。用VSCode写代码用Makefile或CMake管理工程用OpenOCD或ST-Link Utility下载调试。这套方案配置起来麻烦一些但写代码的体验非常好代码补全、跳转、Git集成都很完善。如果你刚开始学我建议先用Keil把主要精力放在STM32本身而不是开发环境上。等你对STM32比较熟悉了再折腾VSCode。我自己的配置是VSCode写代码Keil做编译和调试两者用同一个工程目录互不干扰。8.2 芯片包的安装与工程模板Keil5安装STM32芯片包有两种方式在线安装和离线安装。在线安装直接在Pack Installer里搜索STM32F1、STM32F4等系列点击Install就行。离线安装需要下载.pack文件双击安装。新建工程的时候我习惯先建一个标准工程模板包含启动文件、标准库文件、头文件路径、宏定义。以后新建项目直接复制这个模板改改名字就能用。模板里要包含stm32f10x.h、system_stm32f10x.c、startup_stm32f10x_hd.s这几个核心文件以及core_cm3.h等CMSIS文件。注意启动文件要根据芯片型号选择。F103C8T6是中等容量用startup_stm32f10x_md.sF103ZET6是大容量用startup_stm32f10x_hd.s。选错了启动文件程序可能跑不起来或者中断向量表错位。8.3 调试工具与技巧ST-Link是性价比最高的调试器支持SWD和JTAG价格便宜。J-Link功能更强支持更多芯片和更快的下载速度但价格贵。DAP-Link是开源的调试器功能介于两者之间。调试的时候除了打断点和单步执行还要善用变量监视和内存查看。Keil的Watch窗口可以实时查看变量值Memory窗口可以查看任意地址的内存数据。如果你怀疑某个数组越界了可以在Memory窗口里输入数组地址看看后面的数据有没有被意外修改。串口打印是最朴素的调试手段但也是最有效的。在关键代码位置插入打印语句输出变量值和执行状态可以快速定位问题。但要注意打印本身会消耗时间在时序敏感的代码里要慎用。9. 项目实战从需求到成品的完整流程9.1 需求分析与方案设计拿到一个项目需求先别急着写代码。花半个小时把需求拆解成功能模块每个模块列出输入、输出、依赖关系。比如做一个智能台灯功能模块有光敏采集ADC、按键输入GPIO、LED调光PWM、OLED显示I2C、定时功能RTC或定时器。然后评估每个模块的资源需求ADC用哪个通道、GPIO用哪些引脚、PWM用哪个定时器、I2C用硬件还是软件。把这些分配好画一个简单的框图再开始写代码。这一步花的时间会在调试阶段加倍省回来。9.2 分模块调试与集成不要把所有代码写完再一起调试那样出了问题很难定位。正确的做法是写一个模块调一个模块。先调通GPIO点灯再调通串口打印再调通ADC采集最后把所有模块集成起来。每个模块调通后写一个简单的测试函数记录下测试条件和结果。比如ADC模块输入已知电压看采集值是否准确。PWM模块用示波器看输出波形确认频率和占空比正确。这些测试记录在后期集成出问题时能帮你快速排除已经验证过的模块。9.3 代码规范与版本管理STM32项目代码量不大但也要注意规范。变量命名要有意义函数功能要单一注释要写清楚“为什么”而不是“是什么”。我见过太多项目半年后自己都看不懂当初写的代码。版本管理用Git哪怕只有一个人开发。每次完成一个功能就提交一次提交信息写清楚改了什么。如果改出问题了可以回退到上一个版本。Git仓库可以放在本地也可以放在代码托管平台。不要用“最终版”、“最终版2”、“真的最终版”这种命名方式那是给自己挖坑。10. 我踩过的那些坑与经验总结第一个坑是时钟配置。刚开始学的时候我直接复制了一个例程例程用的是8MHz晶振但我的板子是12MHz晶振。结果串口波特率差了1.5倍打印出来的全是乱码。后来学会了一件事拿到任何例程先看时钟配置确认和自己的硬件匹配。第二个坑是中断优先级。我在一个项目里同时用了串口中断和定时器中断串口中断优先级配得比定时器高。结果串口数据量大的时候定时器中断一直被阻塞PWM输出断断续续。后来把定时器中断优先级调高问题解决。中断优先级的分配要根据实时性要求来不是随便配的。第三个坑是Flash读写。F103的Flash写之前必须先擦除而且擦除是按页擦除的。我一开始不知道直接往Flash里写数据结果写进去的全是0xFF。后来看了手册才知道Flash只能把1写成0不能把0写成1要写新数据必须先擦除整页。第四个坑是堆栈大小。默认的堆栈大小是1KB我定义了一个大数组结果程序跑飞了。后来把堆栈调到4KB问题解决。如果你用了RTOS或者递归函数堆栈要留足余量。第五个坑是电源干扰。电机和继电器这类感性负载断电时会产生反向电动势干扰MCU电源。我在做小车的时候电机一启动MCU就复位。后来在电机两端加了续流二极管电源加了滤波电容问题才解决。数字电路和模拟电路要分开供电至少要用磁珠或电感隔离。这些经验在教科书上不会写但实际项目中一定会遇到。STM32的理论知识是基础但真正让你把项目做出来的是这些踩坑后积累的直觉。遇到问题不要慌先查手册再搜社区最后动手验证。每一次排查问题的过程都是对STM32理解加深的过程。