STM32F103的开发板到手那天,我拆快递的手都有点抖。不是矫情,是这块蓝色小板子背后代表的东西太多了——从51单片机那种“寄存器直接怼”的玩法,切换到32位ARM Cortex-M3内核,外设多到记不住,时钟树复杂得像迷宫,光是一个GPIO的配置就有七八个寄存器要填。很多人就是在这个阶段被劝退的:板子买回来了,Keil装好了,例程也下载了,但就是点不亮第一颗LED,然后就开始怀疑自己是不是不适合搞嵌入式。
这篇内容就是写给刚拿到STM32F103开发板、准备从零开始啃的朋友。我会把从开箱到跑通第一个工程这条链路上,所有容易卡住的地方拆开讲清楚——不是那种“第一步安装Keil,第二步新建工程”的流水账,而是告诉你为什么这么做、不这么做会出什么问题、以及我踩过的那些坑。不管你是电子专业的学生、转行做嵌入式的工程师,还是纯粹想玩硬件的爱好者,只要手里有一块F103的板子,这篇内容都能帮你少走至少两周的弯路。
1. 拿到板子先别急着上电,这几件事决定了你后面顺不顺
1.1 确认芯片型号和封装,别被“STM32F103”这个笼统名字骗了
“STM32F103”只是一个系列名,下面细分了几十种型号。你板子上焊的到底是C8T6、RCT6还是ZET6,直接决定了你能用多少Flash、多少RAM、有哪些外设。C8T6是64KB Flash、20KB RAM、48个引脚;RCT6是256KB Flash、48KB RAM、64个引脚;ZET6是512KB Flash、64KB RAM、144个引脚。如果你拿C8T6的例程往RCT6上烧,可能能跑,但一旦用到额外引脚就会出问题。
怎么确认?看芯片表面的丝印。STM32F103后面跟的那串字符,比如“C8T6”,C代表48脚封装,8代表64KB Flash,T代表LQFP封装,6代表工业级温度范围。把这个搞清楚了,后面选例程、配工程、查数据手册才不会张冠李戴。
提示:如果你买的是那种“最小系统板”,板子上通常只引出了排针,没有板载外设。这种板子适合已经有一定基础的人做项目验证,但纯新手建议买带LED、按键、串口芯片的“开发板”,不然你连点灯都要自己飞线。
1.2 检查供电跳线帽和BOOT引脚,这两个地方最容易让板子“装死”
很多开发板上有跳线帽用来选择供电来源——是USB供电还是外部DC供电。如果跳线帽没插对,板子上电后电源指示灯亮但芯片不工作,你会以为是芯片坏了,其实只是供电没通到MCU。
BOOT引脚更关键。STM32F103有两个BOOT引脚:BOOT0和BOOT1。它们的组合决定了芯片从哪启动:
| BOOT0 | BOOT1 | 启动模式 |
|---|---|---|
| 0 | X | 从主Flash启动(正常运行) |
| 1 | 0 | 从系统存储器启动(串口下载) |
| 1 | 1 | 从SRAM启动(调试用) |
正常跑程序时BOOT0必须接地。如果你用串口下载程序,需要把BOOT0拉到高电平,下载完再拨回去。我见过太多人下载完程序忘了把BOOT0拨回低电平,然后纳闷为什么程序不跑——因为芯片一直在等串口下载指令。
1.3 安装STM32芯片包之前,先搞清楚Keil的版本问题
Keil MDK从5.0版本开始,芯片支持包(Device Family Pack)和编译器是分开安装的。你装完Keil MDK本体之后,里面是空的,新建工程时找不到STM32F103的型号。需要额外下载安装Keil.STM32F1xx_DFP这个包。
这里有个坑:Keil MDK的编译器版本(ARM Compiler)和芯片包之间有兼容性要求。如果你用的是Keil MDK 5.38以上版本,默认编译器是AC6,而很多老例程是基于AC5写的,编译时会报一堆错误。解决办法是在工程设置里把编译器切回AC5,或者手动安装AC5编译器。我个人的建议是:新手直接用Keil MDK 5.36版本,这个版本对AC5和AC6的支持都比较均衡,网上大部分F103的例程都能直接编译。
2. 从零建一个能跑的工程,比你想的要多做几步
2.1 标准库还是HAL库,这个选择会影响你后面所有的学习路径
STM32F103的代码开发主要有三种方式:标准外设库(Standard Peripheral Library)、HAL库、以及直接操作寄存器。标准库是ST早期推出的,代码直观、结构清晰,适合理解底层原理;HAL库是ST现在主推的,配合STM32CubeMX可以图形化配置,跨系列移植方便,但封装层次深,出了问题不好排查。
对于F103这块芯片,我的建议是:先学标准库,再了解HAL库。原因很简单——F103的标准库资料最丰富,网上能找到的例程最多,而且标准库的代码逻辑更接近硬件本身,学完之后你对STM32的理解会更扎实。如果你一上来就用CubeMX生成HAL库代码,可能项目跑起来了,但问你“串口波特率是怎么算出来的”你答不上来。
2.2 新建工程的目录结构,决定了你后面找文件的速度
很多人新建工程就是把所有文件往一个文件夹里一扔,刚开始文件少还能忍,等工程大了之后找个头文件要翻半天。我习惯的目录结构是这样的:
Project/ ├── CMSIS/ # 内核相关文件 ├── Library/ # 标准库外设驱动 ├── User/ # 用户代码(main.c等) ├── Start/ # 启动文件 ├── HardWare/ # 硬件驱动(LED、按键、串口等) └── Output/ # 编译输出文件这个结构不是必须的,但好处是清晰。CMSIS和Library直接从ST的库文件里拷贝,User放你自己的代码,HardWare放你写的外设驱动。编译输出单独放一个文件夹,方便清理。
2.3 添加文件到工程时的分组逻辑,别让Keil的工程树变成一团乱麻
Keil的工程窗口里可以建“Group”,相当于文件夹。我一般会建这几个组:Start、CMSIS、Library、User、HardWare。然后把对应的.c文件加进去。注意:头文件的路径要在“Options for Target → C/C++ → Include Paths”里添加,否则编译时会报“cannot open source input file”的错误。
这里有个细节:标准库的“stm32f10x_conf.h”文件用来配置哪些外设驱动参与编译。如果你只用GPIO和串口,就把其他外设的include注释掉,这样可以加快编译速度。但新手经常犯的错误是把所有include都注释了,然后调用RCC_APB2PeriphClockCmd时找不到声明——因为这个函数在stm32f10x_rcc.h里,你得确保这个头文件被include了。
2.4 时钟配置不对,后面所有外设的时间都是错的
STM32F103的外部晶振通常是8MHz,经过PLL倍频到72MHz作为系统时钟。但如果你用的是内部RC振荡器(HSI),默认是8MHz,精度差很多,串口通信容易出误码。所以第一步就是确认你的板子上有没有焊8MHz晶振,然后在SystemInit函数里确认时钟配置。
标准库的system_stm32f10x.c文件里有一个宏定义叫“SYSCLK_FREQ_72MHz”,确保它没有被注释掉。然后在main函数开头调用SystemInit(),这个函数会把系统时钟配到72MHz。如果你不调用它,芯片跑在默认的8MHz HSI上,延时函数的时间会差9倍,串口波特率也会错。
注意:有些开发板的例程里把SystemInit()放在启动文件里自动调用了,你不需要在main里再调一次。但如果你是自己新建的工程,一定要确认这个函数被调用了。验证方法很简单:写一个LED闪烁程序,延时用500ms,如果实际闪得比预期快很多,大概率就是时钟没配对。
3. 烧录程序这件事,卡住的人比想象中多
3.1 ST-Link、J-Link、串口下载,选哪个取决于你手头有什么
STM32F103支持三种下载方式:SWD/JTAG调试器下载(ST-Link或J-Link)、串口ISP下载、以及USB DFU下载(需要芯片支持USB外设)。新手最常用的是ST-Link和串口下载。
ST-Link的优势是可以在线调试,设断点、看变量、单步执行,排查问题时非常有用。串口下载的优势是便宜,一个USB转TTL模块只要十几块钱,但缺点是不能调试,只能下载。
如果你买的是那种几十块钱的“STM32最小系统板”,通常板载了USB转串口芯片(CH340或CP2102),可以直接用串口下载。但要注意:串口下载需要把BOOT0拉高、BOOT1拉低,下载完再拨回去。而且串口下载的波特率不能太高,一般用115200比较稳。
3.2 Keil里配置ST-Link下载时,这几个选项必须检查
在Keil的“Options for Target → Debug”里选择“ST-Link Debugger”,然后点“Settings”。在弹出的窗口里,确认“Port”选的是SWD而不是JTAG。SWD只需要两根线(SWDIO和SWCLK),比JTAG少占用引脚。
然后在“Flash Download”标签页里,确认“Programming Algorithm”里有STM32F10x High-density Flash(如果是C8T6这种中容量芯片,选Medium-density)。如果这个算法没加载,下载时会报“Flash Download failed”的错误。
还有一个容易忽略的地方:“Reset and Run”选项。勾上它之后,下载完程序会自动复位运行,不用手动按复位键。不勾的话,下载完芯片停在复位状态,你得手动按一下板子上的复位按钮。
3.3 编译成功但烧录失败,排查顺序应该是这样的
“编译成功但烧录不进去”是新手遇到最多的问题之一。排查顺序我建议从硬件到软件:
第一步,检查ST-Link和板子的连线。SWDIO、SWCLK、GND、3.3V这四根线必须接对。有些板子的SWD接口顺序不是标准的,要看板子上的丝印。
第二步,检查ST-Link驱动有没有装好。在设备管理器里看有没有“STMicroelectronics STLink dongle”这个设备,如果有黄色感叹号就是驱动问题。
第三步,检查Keil里的调试器配置。确认选的是ST-Link,Port选SWD,Flash算法选对了。
第四步,检查芯片是不是被读保护了。如果之前烧过程序设置了读保护,ST-Link会连不上。解决办法是用ST-Link Utility工具连接芯片,在“Target → Option Bytes”里把读保护关掉。
第五步,检查供电。有些板子用ST-Link供电时电流不够,芯片工作不稳定。试试用外部电源供电。
3.4 串口下载失败,八成是BOOT引脚或波特率的问题
串口下载需要用STM32的System Memory启动模式,也就是BOOT0=1、BOOT1=0。然后打开STM32CubeProgrammer或者FlyMcu这类工具,选择串口,配置波特率(一般115200),然后点下载。
如果一直提示“同步失败”,先检查BOOT0是不是真的拉高了。有些板子的BOOT0跳线帽设计得很隐蔽,你可能以为拨过去了其实没有。然后检查串口线是不是TX接RX、RX接TX——串口通信是交叉连接的,TX对RX,RX对TX,GND对GND。
还有一个坑:有些USB转TTL模块的3.3V输出电流不够,导致STM32在下载过程中复位。试试用外部电源给板子供电,USB转TTL只接TX、RX、GND三根线。
4. 第一个程序跑通之后,下一步该往哪走
4.1 点灯只是开始,GPIO的八种模式你得挨个试一遍
点亮LED只是验证了GPIO的输出功能,但STM32的GPIO有八种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。每种模式的应用场景不同。
推挽输出用来驱动LED、继电器这类需要强驱动能力的负载;开漏输出用来做I2C通信或者电平转换;输入上拉用来接按键(按键另一端接地);模拟输入用来接ADC采集电压。我建议你写一个测试程序,把这八种模式都配置一遍,用万用表量一下引脚电压,这样理解最深刻。
4.2 串口是调试之母,先把printf重定向搞定
嵌入式开发没有屏幕,串口就是你跟芯片对话的窗口。把printf函数重定向到串口,你就可以在电脑上用串口助手看到程序内部的变量值、状态信息,排查问题时效率翻倍。
标准库下重定向printf的方法是在main.c里加这段代码:
int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }然后在Keil的“Options for Target → Target”里勾上“Use MicroLIB”。不勾的话,printf会调用标准C库,代码体积大很多,而且可能跑不起来。
4.3 定时器和中断,是STM32真正好玩的地方
GPIO和串口只是开胃菜,定时器和中断才是STM32的精髓。F103有多个通用定时器(TIM2到TIM5)和一个高级定时器(TIM1)。通用定时器可以做定时中断、PWM输出、输入捕获;高级定时器还能做互补输出,用来驱动电机。
我建议的学习顺序是:先用TIM做1秒定时中断,在中断里翻转LED;然后用TIM输出PWM,调节LED亮度;最后用TIM的输入捕获功能测量超声波模块的回波时间。这三步走完,你对定时器的理解就到位了。
4.4 别急着上RTOS,裸机跑明白了再说
很多教程一上来就教你移植FreeRTOS或者RT-Thread,但对于刚接触STM32的人来说,裸机编程才是打基础的关键。裸机状态下你需要自己管理中断优先级、自己写状态机、自己处理时序,这些经验在上了RTOS之后依然有用。
等你裸机能写出一个完整的多任务程序(比如同时处理按键、串口、定时任务),并且对中断嵌套、临界区保护这些概念有实际体会了,再上RTOS会顺畅很多。否则你只是学会了调用RTOS的API,但底层发生了什么完全不清楚。
5. 那些教程里不会写但实际会遇到的坑
5.1 延时函数delay卡死,多半是中断优先级的问题
很多人用SysTick做延时函数,在main里调用没问题,但一放到中断服务函数里就卡死。原因是SysTick的中断优先级默认比较低,如果在高优先级中断里调用delay,SysTick中断进不来,计数器不增加,delay就一直等。
解决办法有两个:一是把SysTick的中断优先级设到最高;二是delay函数里不用中断,直接读SysTick的当前值做轮询。我一般用第二种,更可靠。
5.2 串口发送数据丢失,检查TC和TXE标志位的区别
USART_SendData之后,很多人用while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)来等发送完成。TC是“传输完成”标志,TXE是“发送数据寄存器空”标志。如果你用TXE来判断,连续发送时可能上一个字节还没发完就往数据寄存器里写下一个,导致数据被覆盖。
正确的做法是:发送第一个字节前等TXE,发送后续字节时等TC。或者简单点,每次都等TC,虽然效率低一点但不会丢数据。
5.3 按键抖动导致误触发,硬件和软件总得解决一个
机械按键按下和松开时会有5到10毫秒的抖动,如果不处理,按一次键程序可能检测到好几次。硬件上可以加RC滤波电路,但开发板上通常没有。软件上可以在检测到按键按下后延时20毫秒再读一次,确认状态一致才认为是有效按键。
更好的做法是用定时器中断每隔10毫秒扫描一次按键,连续三次读到相同状态才确认。这样不阻塞主循环,响应也快。
5.4 芯片发热严重,先查有没有引脚被配置成了推挽输出但接了地
STM32的推挽输出引脚如果被配置成输出高电平,但外部被短接到地,会有几十毫安的电流灌入引脚,芯片会发烫。检查方法很简单:写一个程序把所有GPIO都配置成输入浮空模式,如果芯片不烫了,说明是某个输出引脚的问题。然后逐个配置成输出,看哪个引脚一配置就发烫,就能定位到问题。
5.5 程序下载后不运行,先看BOOT引脚再看复位电路
除了前面说的BOOT0必须接地之外,还要检查复位引脚(NRST)的电压。正常工作时NRST应该是高电平(3.3V),如果一直是低电平,芯片就一直在复位状态。有些板子的复位电路设计有问题,电容漏电导致NRST电压被拉低。用万用表量一下NRST对地的电压,如果是0V或者很低,就是复位电路的问题。
6. 给不同基础的朋友几条实在的建议
如果你是完全零基础,连C语言指针都不太熟,我建议你先花一周时间把C语言的基础补一下,重点是指针、结构体、位操作。STM32的库函数里大量使用了结构体指针和位运算,这些不熟的话看代码会很吃力。
如果你有51单片机的基础,那上手会快很多。GPIO、定时器、中断这些概念是相通的,主要区别在于STM32的时钟配置更复杂、外设寄存器更多。建议你从标准库的例程开始,先跑通再改,改着改着就理解了。
如果你是为了做毕业设计或者项目,时间比较紧,那我建议你直接上STM32CubeMX加HAL库。图形化配置引脚和时钟,自动生成初始化代码,你只需要在生成的框架里填业务逻辑就行。虽然底层细节了解得少一些,但出活快。
不管你属于哪种情况,有一件事是共通的:动手写代码,别光看教程。看十遍视频不如自己写一遍代码、调一次bug。STM32的学习曲线确实比51陡,但一旦跨过那个坎,后面就是一马平川。
我在实际带新人的过程中发现,大部分人卡住的地方其实就那么几个:时钟没配对、BOOT引脚没拨对、下载器配置错了、中断优先级搞混了。这些问题在教程里往往一笔带过,但实际遇到的时候能卡你好几天。希望这篇内容能帮你把这些坑提前填上,让你把精力花在真正有意思的地方——写出能跑的程序,做出能动的项目。