ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32F103经典例程深度拆解:从最小系统到FreeRTOS与USB CDC

STM32F103经典例程深度拆解:从最小系统到FreeRTOS与USB CDC 简介STM32F103外设例程包面向嵌入式开发入门者与项目工程师围绕Cortex-M3内核芯片精选GPIO、定时器、ADC、DAC、UART、SPI、I2C、CAN、USB、PWM等模块的驱动示例解决外设配置繁琐、调试耗时的问题。压缩包共548个文件以c源码和h头文件为主附带txt说明与多种IDE工程文件整体约876KB目录结构清晰便于按功能检索。目前已有431人学习下载例程不仅演示基础外设操作还涉及中断处理、通信协议、数字模拟转换等应用场景配合官方手册阅读可帮助读者掌握CubeMX配置与HAL/LL库调用方法积累实际项目中的调试经验。 老嵌入式圈子有个共识STM32F103这颗芯片放到今天依旧是一块绕不开的“活化石”级经典。我这些年带过不少新人也收到过各种“stm32f103 例程”的求助说白了很多人的第一块板子就是它第一份代码也是从例程开始的。例程这东西看着是现成的代码可真正把它吃透、会改、能移植才算是把STM32F103这只“小麻雀”解剖明白了。今天我想换个角度聊不贴一份大而全的仓库式例程列表而是把高频关键词背后真正值得研究的例程场景串起来最小系统、PWMDMA、停机模式、FreeRTOS、USB CDC、掉电保存逐个拆解它们背后的核心思路和容易踩的坑。无论你是刚点亮第一颗LED还是已经准备做Bootloader项目这篇内容都更适合当成一份“例程阅读指南”来用。1. 例程怎么学才有效先搞懂它在教你什么1.1 例程背后不是代码是内核和外设的工作时序很多初学者拿到例程第一反应就是先编译下载看到灯亮了就觉得自己会了。实际上例程是ST或者同行工程师把芯片数据手册里几百页的寄存器描述浓缩成了一小段能跑的“标准答案”。STM32F103的例程最有价值的地方不是它怎么点亮一个LED而是它教你时钟树怎么配、GPIO模式怎么选、外设初始化顺序是什么。学习例程时我建议先看初始化函数再进主循环把每个库函数往参考手册里的寄存器位上一一对应这比抄代码有用得多。比如RCC_APB2PeriphClockCmd前面为什么先开GPIO时钟因为不先把外设时钟拉起来寄存器配置根本没写到总线上。这个细节看十遍教程都不如自己把时钟那行注释掉跑一次来得深刻。再比如串口例程配置完波特率寄存器之后为什么还要等USART_FLAG_TXE这背后是发送数据寄存器和移位寄存器之间的互锁关系理解了这些例程在你眼里就不再是一行行“魔法代码”。1.2 最小系统与GPIO耐压例程跑起来之前要搞定的硬件问题STM32F103最小系统看起来很简单电源、晶振、复位、Boot引脚再加SWD下载口就能跑。但我在例程测试时遇到过两个特别容易翻车的地方。第一个是8MHz晶振起振失败代码里配的是HSE外部高速时钟板子上却没焊晶振或者负载电容不对程序直接卡死在SystemInit的等待循环里这时候切到内部HSI时钟才能救回来。第二个是电源纹波稳压芯片出来如果没加足够容值的退耦电容ADC例程采集出来的数值会忽大忽小问题不在代码而在Layout。关于GPIO能不能承受5V这个经典问题要看引脚是否带FT标记。STM32F103数据手册里标注FT的引脚在开漏模式下可以接到5V外部器件普通引脚不行而且即使是FT引脚也不建议长时间直接灌入大电流串个限流电阻更稳。例程里如果要在5V逻辑环境接传感器尽量优先选带FT的引脚。另外最小系统板上如果BOOT0悬空或接错程序也可能运行异常跑谁都正常就你的板子不正常的时候先把BOOT引脚电平量一遍。2. 值得反复拆解的经典外设例程2.1 GPIO点灯与按键输入输出的模式选择才是真考点GPIO例程作为第一个例程核心不在GPIO_SetBits或者HAL_GPIO_WritePin而在于模式选择。输入要区分上拉、下拉和浮空输出要区分推挽、开漏和复用功能这个决定后面几乎每个外设例程能不能正常跑。比如按键检测例程如果外部已经接了下拉电阻内部就不要再配下拉否则读出来的电平变化会和预期相反。还有开漏输出很多人不理解它的用处实际在驱动WS2812B这类外部有上拉的信号时开漏加外部上拉可以匹配不同电平域这也是为什么I2C例程里SCL和SDA都要配成开漏。点灯看似简单但把推挽和开漏各试一次再用示波器量一下输出波形你对GPIO的理解会立刻上一个台阶。实际操作中我还发现GPIO翻转速度如果配得太高在长走线和杜邦线环境下会产生振铃所以例程里GPIO_Speed_50MHz不是越高越好低频信号用10MHz或2MHz模式更稳。2.2 PWMDMA驱动WS2812B高精度时序不是靠delayST官方例程里PWM部分通常就是呼吸灯一路定时器通道输出PWM占空比渐变。真正让很多人大呼“还能这样”的是PWM加DMA的组合尤其是用来驱动WS2812B这种对时序要求严格的全彩LED。WS2812B的0码和1码的高电平宽度差只有几百纳秒如果每颗灯都用延时模拟会占死CPU还容易受中断干扰。PWMDMA的思路是先把一整帧颜色数据按每个bit对应到PWM比较寄存器里再通过DMA自动把CCR数值搬运到定时器的比较通道整个过程不占CPU你只负责准备内存里的色码表。用F103的时候要注意DMA请求和定时器通道是绑定的比如TIM1的CH1对应DMA1的通道2具体映射关系要查参考手册的DMA请求表。对外的引脚选PA1、PA3这类取决于你想用哪一路定时器通道。注意很多人数据算对了却忘记开DMA的循环模式结果颜色只刷了一帧就停止输出了。WS2812B需要持续发送刷新信号DMA必须工作在循环模式这个配置细节经常是例程移植时最容易丢的一环。2.3 4路ADC采集多通道扫描要注意数据错位ADC例程同样经典从单通道采集到4路甚至16路规则组扫描是传感器类项目的标配。4路AD在例程里看起来简单配好通道序列和扫描顺序就能连续转换但很多人在Proteus仿真里看到数值对下载到实际板子上就乱了原因往往是规则组数据寄存器只有一个DMA搬运的顺序和ADC转换完成顺序没有同步好。配置ADC_RegularChannelConfig中的Rank顺序必须和DMA缓冲区里的通道顺序一一对应。采样时间也要给足尤其当信号源阻抗比较大时采样时间太短会导致采样电容充不满测出来的电压偏低。另外在低速测量场景下我习惯加上均值滤波连续采样16次求平均用整数累加再移位不要在中断里频繁做浮点运算。还有一个容易忽略的点F103的ADC最大工作频率是14MHz如果你把APB2时钟直接分频给ADC分频系数选小了转换结果会不稳定例程里默认的ADC_Prescaler配置最好别乱改。3. 低功耗、掉电保存和时钟这三块是进阶分水岭3.1 停机模式的例程省电不是一句WFI就完事关于STM32F103停机模式例程的框架基本是先关掉不需要的外设时钟把GPIO设为模拟输入或低功耗状态再调用PWR_EnterSTOPMode进入STOP模式最后由外部中断或RTC闹钟唤醒。很多刚接触低功耗的朋友会犯一个错误只调用停机函数就认为已经在省电了结果一量电流还是几毫安以上。电流高是因为没有把未使用的IO口和内部上拉全部处理干净浮空输入状态下引脚会反复翻转产生漏电流。我把这类例程调到几十微安的经验是进入停机之前把不用的引脚全部配成模拟输入模式这是数据手册都推荐的做法比设成低电平更稳然后再逐个测试唤醒中断源能不能把系统叫醒。PA0外部中断和RTC都可以做停机唤醒但SysTick在低功耗模式下是不工作的别指望靠它。另外停机模式不是掉电模式RAM内容还保留着唤醒后的执行流程基本是接着WFI之后往下走这也是例程里为什么习惯在唤醒后重新配置系统时钟的原因。3.2 掉电保存数据Flash、备份寄存器还是外挂EEPROM掉电保存数据这个例程F103方案有几种每种都有自己的适用条件。第一种是内部Flash写入把关键参数存到专门的页优点是成本低、不用外挂芯片但Flash擦写次数有限而且写之前要先擦除整个扇区如果数据量小却频繁更新反复擦写会加速器件损耗一般需要做磨损均衡。第二种是备份寄存器只要VBAT引脚有电池或者大电容掉电后数据不会丢而且写入速度很快适合存设备状态和校准值。第三种是外部EEPROM比如M24C32这类I2C EEPROM容量大、擦写寿命高例程核心是I2C读写时序。保存方式容量擦写寿命例程注意点适合场景内部Flash按页管理F103页大小1KB约1万次写前整页擦除建议做磨损均衡参数不频繁更新备份寄存器42个16位寄存器共84字节基本无限VBAT保持供电按普通寄存器读写状态量、校准值外挂EEPROM如M24C3232Kbit约4KB约100万次注意I2C时序和页写缓冲频繁更新、数据量中等做掉电保存项目我建议先想清楚三个问题数据更新的频率是多少数据丢失的代价有多大断电瞬间有没有时间把数据写进去三者权衡之后再选方案。很多例程在断电瞬间才触发保存往往来不及更可靠的做法是预先把数据更新到非易失区而不是等掉电中断触发后再写。3.3 内部晶振模式改时钟源不只是换两行宏让STM32F103工作在内部晶振模式用标准外设库的话核心是把SystemInit里的HSE相关代码段替换掉把系统时钟源切到HSI再配置PLL倍频。但有一个坑很容易忽略HSI内部RC振荡器的精度在室温下大约在1%到3%之间温度变化时漂移更明显。如果你后面的例程用到串口波特率或者需要精确延时的外设内部晶振会导致通信帧错误所以内部晶振模式一般只适合对时钟精度要求不高的场景。另外修改时钟时最好确认PLL的倍频系数和系统时钟频率设置是否一致。默认外部8MHz晶振配9倍频得到72MHz改用内部8MHz HSI后倍频系数理论上可以不变但要注意HSI实际频率不一定正好是8MHz。建议在系统初始化后加一个判断通过RCC_GetClocksFreq把SysClk读出来验证别在时钟不对的情况下调试了半天还找不到原因。还有个细节是当外部晶振失效时F103会自动切换时钟源但部分例程没有做时钟安全系统的中断处理这时候系统可能会用低速内部时钟跑起来程序整体变慢很多人甚至会误以为是板子坏了。4. Bootloader、FreeRTOS和USB CDC从裸机走向系统的分水岭4.1 C8T6的Bootloader例程Flash划分和中断向量重映射是核心STM32F103C8T6 Bootloader例程比普通外设例程高级因为它考验的是对整个存储结构的理解。C8T6有64KB Flash页大小1KBBootloader例程通常在Flash最前面放一段引导程序负责检查升级请求从串口接收固件写入应用区再跳转到APP。很多人移植时会踩一个大坑APP工程的Flash起始地址改了但中断向量表没有跟着重新映射导致APP里一进中断就死掉。用标准外设库时可以在SystemInit之后手动设置VTOR寄存器或者在链接脚本里指定地址后通过向量表偏移处理。同时要处理好两个程序之间的中断优先级分组一致性否则APP的调度可能会乱。Bootloader升级协议要尽量简单可靠校验通常用CRC32或者XOR累加每个数据包加上包序号和长度因为串口传输不可能百分百不出错。我做过一次C8T6的Bootloader后最大的体会是跳转前一定要把全局中断关掉还要把外设都反初始化否则APP初始化时会撞上残留的中断状态这种问题定位起来特别费时间。4.2 FreeRTOS例程任务划分比API调用更值得学FreeRTOS例程拿到手不要急着看xTaskCreate和vTaskDelay先看作者是怎么分配任务的。F103跑FreeRTOS完全够用但它的SRAM只有20KB左右稍不注意任务栈就爆了。例程里给你的每个任务栈大小、消息队列长度、定时器周期都值得推敲不要盲目加大栈空间否则内存不够用系统创建任务会直接失败。我调试FreeRTOS时常用的方法是在工程里打开栈高水位检查功能用uxTaskGetStackHighWaterMark观察每个任务还剩下多少栈空间而不是等HardFault了才去查。还要注意F103在FreeRTOS里默认使用SysTick作为系统节拍如果你的某个外设例程也依赖SysTick做延时两者会冲突一些例程会改用其他定时器作为HAL的时基这是移植FreeRTOS时经常要做的一步。另外临界区里不要调用会阻塞的API低优先级任务里也不要长时间占用CPU这些看似约定俗成的东西其实在例程的注释里都有提示只是大多数人都没当回事。4.3 USB CDC多串口枚举描述符和上位机联动是难点STM32F103的USB设备例程里CDC虚拟串口算是一个热点毕竟现在笔记本电脑上没有串口的人太多了。普通CDC例程实现一个虚拟串口不难难的是多串口复合设备比如做一个USB转双串口工具需要在设备描述符里配置多个CDC接口集合。很多例程只能看到枚举成功但打开两个COM口时只有一个能收发问题通常出在接口描述符数量和端点地址分配上双CDC占用的端点地址不能冲突而且上位机打开端口时要注意每个虚拟串口对应不同的接口索引。还有一个细节是USB上电时序。如果开发板由USB线供电有些例程在USB初始化前没有等待VBUS稳定设计不好可能会出现上电后没被识别的情况需要在代码里适当延时等待。USB协议栈在F103上跑起来对48MHz时钟精度很敏感所以一定要保证PLL时钟配置正确这也和前面时钟部分接上了。如果你发现自己做的USB设备插上去没有反应先看系统设备管理器里有没有出现未知设备出现说明枚举数据有问题完全没有反应则要先查USB D引脚的上拉电阻和时钟配置。5. 避坑实录红外接收、串口报错和PC端DLL加载失败5.1 HAL_UART_ERROR_FE不一定是串口线的问题HAL_UART_ERROR_FE这个错误码中文叫帧错误意思是收到的数据没有满足起始位、数据位和停止位的时序要求。很多例程在接红外接收头时特别容易出这个报错因为红外接收头输出的是38K载波解调后的脉冲不是标准UART电平直接把红外接收头输出接在串口RX上必然出现帧错误。红外解码的正确姿势是用输入捕获或者外部中断测量脉冲宽度根据高低电平持续时间来判断是引导码、逻辑0还是逻辑1。如果确实要接收外部模块通过串口发来的红外码检查波特率是否和发送端一致。两台设备时钟源差异大时比如一台用内部HSI、一台用外部晶振长时间传输也容易出现帧错误。代码层面遇到HAL_UART_ERROR_FE要先清除对应错误标志再重新开启接收否则串口会一直卡在错误状态里。很多人会在这里反复重启程序其实只需要在错误回调里加一句标志位清零和重新接收的代码就能解决。5.2 测量相位差例程找边沿算时间戳关于相位差测量F103例程一般用两个输入捕获通道分别抓取两路信号的上升沿然后通过捕获事件的时间戳差值和周期计算相位差。核心是让同一个定时器的两个通道一起工作并确定基准通道和测量通道的关系。比如用TIM2的CH1和CH2输入信号经过内部滤波后触发捕获中断里记录CCR1和CCR2的差值再除以一个完整周期的计数值换算成角度。要注意如果两路信号的频率在变化直接拿一次捕获结果算相位差误差会很大建议多测几个周期取平均。还有一点常被忽略F103输入捕获的时钟分辨率72MHz下捕获计数周期约13.8ns对低频信号够用。但如果测几百kHz以上的高频信号要合理选择预分频使计数值不溢出也不要打开过于激进的滤波不然边沿抖动会把相位差结果带偏。例程里涉及中断处理时尽量在中断里只做标记把相位差计算放到主循环里做可以避免中断函数里做除法带来的时间不确定性。5.3 PC端提示DLL初始化例程失败问题可能不在板子上最后聊一个很多人会来问的问题在电脑上跑STM32的上位机工具或者仿真软件时弹出OSError或者Windows错误1114提示动态链接库初始化例程失败加载某个DLL出错。这个真不是STM32F103芯片的问题而是PC端缺少VC运行库或者某个DLL依赖了系统里不存在的组件。遇到这个先安装Microsoft Visual C Redistributable合集再检查上位机软件路径是否有中文、权限够不够很多时候重新以管理员身份运行就解决了。注意我当初做USB CDC上位机时就碰到过一次板子枚举完全正常但Python调用DLL一直报1114最后发现是64位系统下装了32位的依赖库换对应位数的运行库后立刻恢复正常。这个经验提醒我们调试问题先定位清楚问题的层级不要在板子上浪费时间找不存在的寄存器问题。我最早学STM32F103的时候也是从例程开始跟大多数人一样下载、看灯、换下一份例程循环了两个月自认为会了结果自己动手写一个按键控制呼吸灯都卡了两天。后来把例程初始化代码一行行对着参考手册看才突然发现每个库函数背后都对应着寄存器位。从那以后再碰到新外设我都先找官方例程再找同芯片的工程最后改成自己的引脚和功能。老实说F103这颗芯片的例程到今天依然是很好的学习材料就算你已经上手了更高主频、更强外设的芯片回过头来看F103例程里的时钟配置和中断处理思路还是能收获不少沉淀下来的基本功。希望这篇例程拆解能帮到正在入门或者在调试中碰壁的朋友少走几个我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表