1. 从“点灯”到“控世界”:STM32理论到底在讲什么
很多人第一次接触STM32,都是从一块F103C8T6最小系统板开始的。板子到手,装好Keil,打开例程,编译下载,LED亮了,然后呢?然后就没有然后了。我见过太多人卡在这个“然后”上——会点灯,但不知道为什么灯会亮;会抄代码,但换个引脚就报错;会用库函数,但说不清HAL库和标准库到底差在哪。STM32理论这个词听起来很虚,但它恰恰是把你从“抄代码的人”变成“写代码的人”的那道分水岭。
这篇文章想聊的,就是围绕STM32F103C8T6这颗经典芯片,把GPIO的八种工作模式、中断函数的执行逻辑、PWM从占空比计算到实际输出的完整链路、定时器的各种模式选择,以及串口中断回调、编码器接口、时钟树配置这些核心理论点,用从业者的视角掰开揉碎讲清楚。不管你是刚入门的电子专业学生,还是从51单片机转过来的工程师,或者是正在做毕业设计需要快速上手STM32的开发者,这些内容都能帮你建立起一套完整的认知框架。我不会只告诉你“这样配置就行”,而是会解释“为什么必须这样配置”,以及“不这样配置会出什么问题”。
STM32的理论体系其实可以拆成三条主线:第一条是时钟与总线,它决定了芯片内部各个外设的运行节奏;第二条是GPIO与复用功能,它是芯片与外部世界交互的物理通道;第三条是定时器与中断,它是实现精确时序控制和实时响应的核心机制。这三条线交织在一起,构成了STM32绝大部分应用的基础。理解了这三条线,再看PWM调速、串口通信、编码器测速、超声波测距这些具体应用,就会发现它们不过是这三条线的不同组合方式而已。
2. GPIO的八种工作模式:选错了,电路就废了
2.1 八种模式到底在解决什么问题
GPIO的八种工作模式经常被当成八选一的填空题来记,但如果你不理解每种模式背后的电路结构,选型时就只能靠猜。这八种模式本质上是四个维度的组合:输入还是输出、推挽还是开漏、上拉还是下拉、复用还是普通。把这四个维度理清了,八种模式自然就记住了。
先看输入方向。浮空输入模式下,引脚内部既不上拉也不下拉,电平完全由外部电路决定。这种模式适合外部已经有明确驱动源的场景,比如外部接了上拉电阻的按键,或者由另一个芯片驱动的信号线。但如果你把一个浮空输入的引脚悬空,读到的值就是随机的,因为引脚处于高阻态,任何微弱的干扰都能改变它的电平。上拉输入和下拉输入则是在内部接入了一个几十千欧的电阻,分别把引脚默认拉到高电平或低电平。按键检测是最典型的应用:按键一端接引脚,另一端接地,引脚配置为上拉输入,按下时读到低电平,松开时读到高电平。这样就不需要外部再加电阻了。
模拟输入模式比较特殊,它直接关闭了引脚的数字输入通道,让信号原封不动地送到ADC模块。如果你用ADC采集电压但引脚配置成了浮空输入,读出来的值会跳得厉害,因为施密特触发器还在工作,会把模拟信号整形成数字信号再送给ADC,那采集出来的就不是真实电压了。
再看输出方向。推挽输出是最常用的模式,它内部有两个MOS管,一个负责拉高,一个负责拉低,输出能力强,高低电平都有驱动能力。开漏输出则只有下拉的MOS管,拉高需要外部上拉电阻配合。开漏输出的优势在于可以实现“线与”逻辑——多个开漏引脚接在同一条线上,任何一个拉低,整条线就是低电平。I2C总线的SDA和SCL就是典型的开漏输出应用,因为总线上有多个设备,谁都可以拉低,但谁都不能强行拉高。
复用推挽和复用开漏则是把引脚的控制权交给片上外设,比如定时器的PWM输出、串口的TX引脚。这时候你不能再通过写GPIO的输出数据寄存器来控制引脚了,引脚电平完全由外设决定。很多人配置PWM输出时忘了把引脚设为复用模式,结果PWM死活出不来,就是因为引脚还处于普通输出模式,定时器的信号根本传不到引脚上。
2.2 GPIO地址与寄存器映射:为什么库函数能操作硬件
STM32的GPIO之所以能被程序控制,是因为它的寄存器被映射到了内存地址空间。以F103C8T6为例,GPIOA的基地址是0x40010800,GPIOB是0x40010C00,GPIOC是0x40011000,每个GPIO端口占用0x400的地址空间。在这个空间里,偏移0x00是配置低寄存器,偏移0x04是配置高寄存器,偏移0x08是输入数据寄存器,偏移0x0C是输出数据寄存器,偏移0x10是位设置寄存器,偏移0x14是位复位寄存器。
标准库里的GPIO_SetBits(GPIOA, GPIO_Pin_5)本质上就是往GPIOA的位设置寄存器写入0x0020。HAL库里的HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET)最终也是操作同一个寄存器。理解这一层,你就能明白为什么库函数能操作硬件——它不过是对内存地址的读写封装而已。更重要的是,当你在调试时发现某个引脚行为异常,你可以直接在调试器的内存窗口里查看这些寄存器的值,看看配置位到底写对没有,这比反复检查代码要高效得多。
2.3 GPIO模式选择的实战判断流程
面对一个具体的应用场景,怎么快速确定该用哪种GPIO模式?我总结了一个判断流程:先问这个引脚是输入还是输出,再问信号是数字还是模拟,然后问是否需要外部多个设备共享,最后问控制权在GPIO还是外设。
按键检测用上拉或下拉输入,I2C用复用开漏,SPI用复用推挽,PWM输出用复用推挽,ADC采集用模拟输入,普通LED驱动用推挽输出,多个LED共阳极驱动用开漏输出。这个对应关系不是死记硬背的,而是由电路原理决定的。比如为什么I2C必须用开漏?因为I2C总线上可以挂多个主从设备,如果都用推挽输出,一个设备输出高电平另一个输出低电平,两个MOS管直接对电源和地短路,芯片就烧了。开漏输出则不会出现这个问题,因为没有人能强行拉高,大家只能拉低或者释放总线。
注意:配置GPIO之前必须先使能对应端口的时钟。F103的GPIOA挂在APB2总线上,忘记调用
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)或者HAL库的__HAL_RCC_GPIOA_CLK_ENABLE(),后面所有配置都不会生效,而且不会报错,引脚就是没反应。这个坑我踩过不止一次。
3. 中断函数:从触发到返回的完整链路
3.1 中断优先级与嵌套的底层逻辑
STM32的中断系统比51单片机复杂得多,核心在于NVIC(嵌套向量中断控制器)。每个中断通道都有两个优先级:抢占优先级和响应优先级。抢占优先级决定了一个中断能否打断另一个正在执行的中断,响应优先级则决定了同一抢占优先级下多个中断同时到来时的处理顺序。
举个例子,假设中断A的抢占优先级是1,中断B的抢占优先级是2。当B正在执行时,A触发了,A可以打断B,因为A的抢占优先级更高。等A执行完,B继续执行。但如果A和B的抢占优先级相同,只是响应优先级不同,那么A不能打断B,必须等B执行完才能处理A。这个机制在实际项目中非常关键,比如你在做电机控制,PWM更新中断需要极高的实时性,而串口接收中断可以稍微慢一点,那就应该把PWM中断的抢占优先级设得比串口高。
优先级分组决定了抢占优先级和响应优先级各占几位。F103支持5种分组方式,从0到4。分组0表示4位全用于响应优先级,没有抢占优先级,所有中断都不能互相打断。分组4表示4位全用于抢占优先级,没有响应优先级。最常用的是分组2,2位抢占优先级,2位响应优先级,这样既有嵌套能力,又有足够的优先级区分度。
3.2 中断服务函数的编写规范
中断服务函数的函数名不是随便起的,它必须和启动文件里的中断向量表里的名字完全一致。比如串口1的中断服务函数在标准库中叫USART1_IRQHandler,在HAL库中也是这个名字,你写成USART1_IRQHandler少一个字母都不行,编译能过,但中断永远不会触发。
中断服务函数里要做的事情应该尽可能少。我见过有人在串口中断里做协议解析、做数据存储、甚至做浮点运算,结果就是中断执行时间过长,导致其他中断被延迟甚至丢失。正确的做法是在中断里只做最紧急的事情——比如把接收到的数据存到一个缓冲区,然后设置一个标志位,主循环里检测到标志位再做后续处理。这就是所谓的“快进快出”原则。
还有一个容易忽略的点是中断标志位的清除。很多中断在进入服务函数后需要手动清除标志位,否则中断会反复触发。比如串口接收中断,你读取了数据寄存器DR,硬件会自动清除RXNE标志。但如果你用的是HAL库的HAL_UART_IRQHandler,它内部会帮你处理这些标志位。如果你自己写中断服务函数,就必须在合适的位置调用__HAL_UART_CLEAR_FLAG或者直接读寄存器来清除标志。
3.3 串口中断回调函数的执行流程
HAL库把中断处理分成了两层:底层的中断服务函数和上层的回调函数。以串口接收为例,USART1_IRQHandler被触发后,会调用HAL_UART_IRQHandler,这个函数会根据中断类型做相应处理,然后调用弱定义的HAL_UART_RxCpltCallback。你只需要在代码里重写这个回调函数,就能在收到数据时执行自己的逻辑。
这个机制的好处是解耦——你不需要关心底层寄存器怎么操作,只需要关注“收到数据后要做什么”。但坏处是执行路径变长了,从触发中断到你的回调函数被执行,中间经过了多层函数调用。对于波特率很高的场景,比如115200甚至更高,如果回调函数里做的事情太多,就可能来不及处理下一个字节。这时候可以考虑用DMA加空闲中断的方式,让DMA自动搬运数据,空闲中断只负责通知主循环“一帧数据收完了”。
提示:使用HAL库的串口接收中断时,
HAL_UART_Receive_IT函数每次只能接收指定数量的字节,收完后需要重新调用才能继续接收。如果你忘记在回调函数里再次调用它,串口就只能收到一次数据。这个坑在新手中极其常见。
4. PWM从理论到输出:占空比、频率与故障保护
4.1 PWM的基本原理与占空比计算公式
PWM的全称是脉冲宽度调制,说白了就是通过调节方波的高电平时间占总周期的比例,来等效地控制输出功率。比如一个5V的电源,用50%占空比的PWM驱动一个LED,LED两端的平均电压就是2.5V,亮度大约是一半。如果用80%占空比,平均电压就是4V,亮度明显更亮。这就是PWM调光的原理。
占空比的计算公式很简单:占空比 = 高电平时间 / 周期时间 × 100%。但在STM32里,占空比是通过比较寄存器CCR和自动重装载寄存器ARR来设置的。假设ARR的值是999,CCR的值是300,那么占空比就是300/1000=30%。这里为什么是除以1000而不是999?因为计数器从0数到999,一共是1000个计数周期。这个细节很多人会搞错,导致占空比总是差一点点。
PWM的频率则由ARR和预分频器PSC共同决定。公式是:PWM频率 = 定时器时钟频率 / ((PSC+1) × (ARR+1))。假设定时器时钟是72MHz,PSC设为71,ARR设为999,那么PWM频率就是72MHz / (72 × 1000) = 1kHz。这个频率下,LED调光不会看到闪烁,电机调速也能平稳运行。
4.2 不同应用场景下的PWM频率选择
PWM频率选多少,取决于你驱动的是什么负载。LED调光一般用1kHz到5kHz,太低会看到闪烁,太高则可能因为LED的响应速度跟不上而导致亮度非线性。舵机控制必须用50Hz,也就是周期20ms,因为舵机的内部电路就是按照这个标准设计的,高电平宽度在0.5ms到2.5ms之间对应0度到180度。如果频率不对,舵机会抖动甚至不转。
直流电机调速的频率范围比较宽,从1kHz到20kHz都可以。频率太低电机会有啸叫声,频率太高则MOS管的开关损耗会增加。我一般用10kHz到15kHz,这个范围内电机运行平稳,MOS管也不会太热。步进电机的PWM频率则取决于细分设置和转速要求,需要根据具体驱动器的手册来定。
有一个热词提到了“mg310p20_v7.4v用多少pwm频率”,这应该是一个具体的电机型号。对于这类直流减速电机,我建议从10kHz开始试,如果电机发热严重就降低频率,如果有啸叫就提高频率。没有绝对的标准值,要根据实际负载和驱动电路来调整。
4.3 PWM故障保护与刹车功能
在一些安全要求高的应用中,比如电机驱动,PWM输出需要具备故障保护功能。STM32的高级定时器(如TIM1和TIM8)支持刹车输入,当刹车引脚检测到有效电平时,硬件会自动把PWM输出强制到预设的安全状态,比如全部拉低或者全部拉高,而不需要软件干预。这个响应速度是纳秒级的,比软件中断快得多。
配置刹车功能需要设置刹车极性、刹车后的输出状态、以及是否自动恢复。刹车极性决定了刹车引脚是高电平有效还是低电平有效。刹车后的输出状态可以设为高阻、拉高或拉低,具体取决于你的驱动电路。自动恢复功能则决定了刹车信号消失后,PWM是自动恢复输出还是需要软件手动清除标志后才能恢复。在电机驱动中,我通常会把刹车后的输出设为拉低,这样电机就会自由滑行而不是被短路制动。
4.4 PWM呼吸灯与RGB调光的实现细节
呼吸灯的本质是让PWM占空比随时间做周期性变化。最简单的做法是在主循环里用一个变量记录当前占空比,每次循环增加或减少一个步进值,到达边界后改变方向。但这种方式占空比的变化是线性的,人眼对亮度的感知却是指数型的,所以线性变化的呼吸灯看起来会“中间亮得久,两头暗得快”。
更好的做法是用查表法,预先计算好一条指数曲线或者正弦曲线的占空比序列,存在数组里,主循环里依次读取。这样呼吸效果会非常自然。RGB调光也是类似的道理,三个通道分别用PWM控制,通过调节三个通道的占空比来混合出不同的颜色。需要注意的是,RGB LED的三个通道亮度特性可能不一致,需要分别校准,否则混出来的白色会偏色。
5. 定时器:STM32最复杂的片上外设
5.1 定时器的分类与选型
STM32F103C8T6内部有多个定时器,分为高级定时器、通用定时器和基本定时器。高级定时器(TIM1)功能最全,支持PWM互补输出、刹车输入、死区插入,适合电机控制。通用定时器(TIM2到TIM5)支持PWM输出、输入捕获、编码器接口,适合大多数常规应用。基本定时器(TIM6和TIM7)只能做定时中断,没有外部引脚,适合做系统时基。
选型的原则很简单:需要互补PWM和刹车就用高级定时器,需要编码器接口就用通用定时器,只需要定时中断就用基本定时器。但要注意引脚冲突,因为定时器的通道和GPIO引脚是绑定的,比如TIM2的通道1只能在PA0上输出,如果你PA0已经被占用了,就只能换其他定时器或者重映射引脚。
5.2 定时器模式详解:从单脉冲到编码器
定时器的模式非常多,每一种模式解决一类特定问题。单脉冲模式适合需要精确控制脉冲宽度的场景,比如超声波测距的触发信号,你只需要输出一个特定宽度的高电平脉冲,然后定时器自动停止。输入捕获模式用来测量外部信号的频率和占空比,比如读取编码器的脉冲或者测量超声波回波的高电平时间。编码器接口模式则是专门为增量式编码器设计的,它可以自动根据A相和B相的相位关系进行加减计数,不需要软件干预。
PWM输入模式是输入捕获的一个特例,它可以同时测量一个PWM信号的频率和占空比,只需要一个定时器通道。原理是同一个信号同时接到两个捕获通道,一个捕获上升沿,一个捕获下降沿,通过两个捕获值的时间差计算占空比。这个模式在解码遥控接收机的PWM信号时非常有用。
5.3 定时器时钟树与频率计算
定时器的时钟来源不是直接来自系统时钟,而是经过APB总线分频后再倍频。F103的APB1总线最高36MHz,APB2最高72MHz。但定时器的时钟有一个特殊规则:如果APB分频系数是1,定时器时钟等于APB时钟;如果APB分频系数大于1,定时器时钟等于APB时钟的2倍。所以当APB1分频系数为2时,APB1时钟是36MHz,但挂载在APB1上的定时器时钟是72MHz。
这个规则很容易让人算错定时时间。比如你要用TIM2做一个1ms的定时中断,TIM2挂在APB1上,时钟是72MHz。你设置PSC=71,ARR=999,那么定时周期就是(71+1)×(999+1)/72MHz = 1ms。如果你误以为TIM2的时钟是36MHz,算出来的定时时间就会差一倍。
6. 常见问题与排查技巧实录
6.1 程序下载后没反应怎么办
这是新手遇到最多的问题。程序编译通过了,下载也提示成功了,但板子就是没反应。排查顺序应该是这样的:先确认时钟配置对不对,用调试器看看SystemInit函数有没有被正确执行,时钟频率是不是你期望的值。再确认GPIO时钟使能了没有,这是最常见的遗漏。然后确认引脚配置对不对,用万用表量一下引脚电压,如果是推挽输出高电平应该是3.3V,低电平应该是0V。最后确认程序有没有卡在某个死循环里,比如HAL库的HAL_Delay在中断里调用会导致死锁,因为HAL_Delay依赖SysTick中断,而中断里不能再被中断。
6.2 串口通信乱码或收不到数据
串口问题一般出在三个地方:波特率、时钟配置、引脚连接。波特率不匹配是最常见的,发送方和接收方必须用同一个波特率。但即使波特率设成一样的,如果系统时钟配置错了,实际波特率也会偏。比如你外部晶振是8MHz,但代码里按12MHz来配置PLL,系统时钟就不是72MHz,串口波特率自然也不对。用示波器量一下TX引脚,测量一个位的宽度,就能反推出实际波特率。
引脚连接方面,TX要接对方的RX,RX要接对方的TX,这个不用多说。但有时候开发板上的USB转串口芯片和MCU之间的连接是交叉的,你再用杜邦线接出去的时候又交叉了一次,结果就变成了TX接TX,自然收不到数据。
6.3 PWM输出没有波形
PWM没输出的原因通常有这几个:定时器时钟没使能、GPIO没配置为复用模式、定时器没启动、比较值设置不对。我遇到过一次,定时器配置都正确,但PWM就是没输出,最后发现是GPIO的复用功能重映射没开启。F103的某些定时器通道默认不在常用的引脚上,需要调用GPIO_PinRemapConfig开启重映射才能输出到指定引脚。
还有一个隐蔽的问题是高级定时器的PWM输出需要额外使能主输出。TIM1和TIM8的PWM输出默认是关闭的,必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)才能让PWM到达引脚。这个在标准库里是一个单独的步骤,很多人会漏掉。
6.4 中断不触发或反复触发
中断不触发,先检查中断使能了没有,包括外设层面的中断使能和NVIC层面的中断使能,两个都要开。再检查中断标志位清了没有,如果标志位没清,中断会反复触发,看起来像是程序卡死在中断里。还有一种情况是中断优先级配置冲突,两个中断的抢占优先级和响应优先级完全一样,同时触发时行为不确定。
反复触发最常见的原因是忘记清除中断标志。比如定时器更新中断,你需要在中断服务函数里调用TIM_ClearITPendingBit或者HAL库的__HAL_TIM_CLEAR_IT。如果用的是HAL库的HAL_TIM_IRQHandler,它内部会帮你清除标志,但如果你自己写了中断服务函数而没有调用HAL的处理函数,就必须手动清除。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 程序下载后无反应 | GPIO时钟未使能 | 检查RCC配置代码 |
| 串口乱码 | 系统时钟配置错误 | 用示波器测位宽反推波特率 |
| PWM无输出 | GPIO未设为复用模式 | 检查GPIO初始化结构体 |
| 中断不触发 | NVIC未使能 | 检查NVIC_EnableIRQ调用 |
| 中断反复触发 | 标志位未清除 | 在中断服务函数中清除标志 |
| 定时时间不对 | 定时器时钟计算错误 | 确认APB分频与倍频关系 |
| ADC读数跳动 | 引脚未设为模拟输入 | 检查GPIO_Mode配置 |
| 编码器计数方向反 | A/B相接线颠倒 | 交换A/B相引脚或取反计数 |
提示:调试STM32时,善用调试器的外设寄存器查看功能。在Keil的Debug模式下,可以通过System Viewer直接查看GPIO、TIM、USART等外设的寄存器值,比在代码里加打印要直观得多。特别是配置了但没生效的时候,看一眼寄存器就知道问题出在哪。
7. 工具链与开发环境:从Keil到VSCode
7.1 Keil5兼容C51和STM32的安装要点
很多人电脑上同时装了Keil C51和Keil MDK,两个版本装在一起容易冲突。正确的做法是先装C51,再装MDK,并且装在不同的目录下。装完之后需要把C51的安装目录下的UV4文件夹里的文件复制到MDK的对应目录,或者用注册表工具合并。但更省事的做法是用Keil5,它本身就支持多内核,只需要安装对应的芯片包就能同时开发C51和STM32。
芯片包的安装也很关键。F103C8T6需要安装Keil.STM32F1xx_DFP包,安装完成后在新建工程时才能找到对应的器件。如果找不到器件,多半是芯片包没装或者装错了版本。STM32G070CBT6则需要STM32G0系列的芯片包,不同系列的包不能混用。
7.2 VSCode配置STM32开发环境
用VSCode开发STM32需要几个组件:编译器用arm-none-eabi-gcc,调试器用OpenOCD或者ST-Link Utility,构建系统用Make或者CMake。VSCode本身只负责编辑和调试界面,实际的编译和下载还是靠命令行工具。
配置步骤大致是:先安装arm-none-eabi-gcc并添加到PATH,再安装OpenOCD,然后在VSCode里安装Cortex-Debug插件。工程目录下需要有一个Makefile或者CMakeLists.txt来定义编译规则,还需要一个OpenOCD的配置文件来指定调试器类型和目标芯片。这套环境搭起来比Keil麻烦,但好处是跨平台、免费、而且可以用VSCode强大的代码补全和Git集成功能。
7.3 ST-Link Utility与程序下载
ST-Link Utility是ST官方提供的下载工具,支持读取、写入、擦除芯片Flash,还可以查看芯片的UID和选项字节。当你用IDE下载程序失败时,可以用ST-Link Utility先连接一下,看看能不能识别到芯片。如果能识别但下载失败,可能是Flash被读保护了,需要在选项字节里解除保护。如果连芯片都识别不到,那就是硬件连接问题,检查SWDIO和SWCLK两根线有没有接反,复位引脚有没有被拉低。
8. 从理论到项目:几个典型应用的理论映射
8.1 超声波测距中的定时器输入捕获
超声波测距模块HC-SR04的工作流程是:给Trig引脚一个10微秒的高电平脉冲,模块发射超声波,然后Echo引脚会输出一个高电平,高电平的持续时间就是超声波往返的时间。用STM32测量这个时间,最合适的方法就是定时器输入捕获。
配置思路是:用一个定时器通道捕获Echo引脚的上升沿和下降沿,上升沿时记录计数器的值,下降沿时再记录一次,两次之差就是高电平的持续时间。假设定时器时钟是1MHz,计数一次是1微秒,那么时间差乘以声速340米/秒再除以2,就是距离。这里除以2是因为超声波从发射到接收走了一个来回。
实际项目中要注意的是,超声波模块的Echo引脚输出是5V电平,而STM32的GPIO是3.3V耐压,直接接上去可能会损坏引脚。需要在Echo和MCU之间加一个电平转换电路,最简单的做法是用两个电阻分压,把5V降到3.3V左右。
8.2 编码器测速中的定时器编码器接口
增量式编码器输出A相和B相两路方波,相位差90度。正转时A相超前B相,反转时B相超前A相。STM32的定时器编码器接口模式可以自动识别相位关系并加减计数,不需要软件干预。配置时把A相接定时器的通道1,B相接通道2,然后调用TIM_EncoderInterfaceConfig选择编码器模式。
编码器模式有三种:只在A相计数、只在B相计数、在A相和B相都计数。第三种模式是四倍频,分辨率最高。假设编码器线数是500线,四倍频后转一圈就是2000个计数。如果定时器在1秒内读到的计数值是2000,那么转速就是1圈/秒,也就是60RPM。
8.3 串口中断回调在协议解析中的应用
很多传感器模块通过串口输出数据,比如GPS模块、蓝牙模块、WiFi模块。这些模块的数据通常是按帧发送的,每帧有固定的帧头和帧尾。用串口中断接收时,可以在回调函数里逐字节判断帧头,收到帧头后开始把数据存入缓冲区,收到帧尾后置位一个标志,主循环里检测到标志就解析这一帧数据。
这种方式的难点在于处理帧头误判和缓冲区溢出。如果数据内容里恰好出现了和帧头一样的字节,就会导致解析错误。解决办法是用状态机来解析,而不是简单的字节匹配。状态机根据当前状态决定下一个字节是帧头、数据还是帧尾,这样即使数据里出现了帧头字节,只要状态不对就不会误判。
9. 时钟树:STM32的心跳与节奏
9.1 时钟源的选择与PLL配置
STM32F103有三个时钟源:HSI内部高速时钟(8MHz)、HSE外部高速时钟(通常8MHz晶振)、LSI内部低速时钟(40kHz)。系统时钟一般选择HSE经过PLL倍频到72MHz。PLL的配置是:HSE先除以一个预分频系数得到PLL输入时钟,然后乘以倍频系数得到PLL输出时钟。F103的PLL输入时钟必须在4MHz到16MHz之间,所以8MHz的HSE可以直接输入,不需要预分频。
配置PLL的代码在标准库里是RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9),意思是HSE不分频,倍频9倍,8MHz×9=72MHz。在HAL库里则是通过RCC_OscInitStruct.PLL.PLLM和PLLN来设置,F1系列的HAL库PLL配置略有不同,需要参考具体的HAL版本。
9.2 总线分频与外设时钟
系统时钟72MHz出来之后,要经过AHB分频器、APB1分频器、APB2分频器,分别给不同的外设提供时钟。AHB最高72MHz,APB2最高72MHz,APB1最高36MHz。所以APB1的分频系数至少是2,APB2的分频系数可以是1。
外设挂载的总线决定了它的最高时钟频率。GPIO、USART1、SPI1、ADC1挂在APB2上,可以跑到72MHz。USART2、USART3、I2C1、I2C2、TIM2到TIM7挂在APB1上,最高36MHz。但前面说过,定时器有时钟倍频机制,所以TIM2到TIM7的实际时钟还是72MHz。这个细节在计算定时器参数时一定要搞清楚,否则定时时间会差一倍。
9.3 时钟安全系统与备份域
STM32还有一个时钟安全系统(CSS),当HSE失效时,CSS会自动切换到HSI,并产生一个中断通知软件。这个功能在工业控制中很重要,因为外部晶振可能因为振动、温度等原因停振,如果没有CSS,系统就会死机。配置CSS只需要调用RCC_ClockSecuritySystemCmd(ENABLE),然后在中断服务函数里处理切换后的善后工作。
备份域则涉及到RTC和备份寄存器,它们由VBAT供电,即使主电源断开,只要纽扣电池还有电,RTC就能继续走时。备份域的时钟源可以是LSI、LSE或者HSE的128分频。LSE是外部32.768kHz晶振,精度最高,适合做RTC时钟源。
10. 我个人的一些实操体会
STM32的理论知识看再多,不动手都是空的。我的建议是,每学一个外设,就找一个具体的应用场景去实现它。学GPIO就做按键控制LED,学定时器就做PWM呼吸灯,学中断就做旋转编码器计数,学串口就做上位机通信。做的过程中遇到问题,再去查手册、查资料,这样学到的知识才是活的。
还有一点,不要怕看英文参考手册。STM32的参考手册有上千页,但真正用到的可能就那几十页。学会用PDF的搜索功能,根据关键词定位到相关章节,比从头到尾看要高效得多。寄存器描述部分看起来枯燥,但当你遇到库函数解决不了的问题时,直接操作寄存器往往是最快的解决途径。
最后,代码要养成规范注释的习惯。STM32的配置代码往往很长,过两个月再看,如果没有注释,你自己都不记得当时为什么这么配。特别是时钟配置和GPIO配置,注释清楚每个参数的含义,以后移植到其他项目时能省很多时间。