板子拆开快递那一刻,说实话有点懵。一块绿色PCB,满板子的排针,一个蓝色小芯片,一根USB线,连个说明书都没有。这块STM32F103开发板是我挑了三天才下的单,虽然也就几十块钱,但真正拿在手里,反而不知道从哪下手了。后来我给自己定了条最简单的路线:先点亮LED,再学会按键,然后啃定时器,最后把串口、I2C、CAN这些外设都过一遍。这条路线走下来,差不多就是STM32最常见的学习框架了。如果你也刚买了F103开发板,或者正在犹豫要不要入手,这篇就当作一个过来人的流水账,把我踩过的坑、觉得有用的步骤、以及那些文档里不会明说的经验都摊开来聊。
1. 为什么是STM32F103,这块板子到底能学什么
1.1 F103在STM32家族里的位置
ST的STM32系列型号多得吓人,从M0内核到M7内核,从几十引脚到两百多引脚,选型表能列好几页。但F103始终是入门首选,原因很简单:它用的是ARM Cortex-M3内核,主频72MHz,Flash从64KB到512KB可选,RAM最大64KB,外设齐全。这个配置放现在看不算高,但恰恰是这种“不高”,让新手能看清楚芯片内部是怎么工作的。
对比一下就能明白:ESP32是双核240MHz,还带WiFi蓝牙,但你点个灯得先搞明白IDF的构建系统;Arduino UNO的AVR单片机上手极快,可寄存器操作和中断逻辑跟Cortex-M完全不同。F103夹在中间,性能足够跑工业控制逻辑,资料又是全网最厚的,不管是寄存器版、标准库版还是HAL库版教程,随便一搜都是一大把。这就是生态红利。
1.2 开箱后先认清楚板上这些东西
拿到开发板,别急着插电,先把板子上的关键元器件认一遍。芯片正面第一行字是STM32F103C8T6或者RCT6,C8是64KB Flash、48引脚,RCT6是256KB Flash、64引脚,引脚越多可用的外设复用量越大。板上还一定有这几个东西:
- 电源部分:USB座或者DC座进来,经过一颗AMS1117之类的LDO变成3.3V,给芯片供电
- 晶振:8MHz主晶振,32.768kHz的RTC晶振不一定每块板都有
- 复位按键:拉低NRST引脚,让芯片重新跑程序
- 启动跳线:BOOT0和BOOT1,决定芯片从Flash还是从系统存储器启动
- 下载口:常见的是4针的SWD口,或者一个USB转串口芯片(比如CH340),用来串口下载和调试
新手最容易忽略的是BOOT0。默认情况下BOOT0接0,从Flash启动,正常跑你的程序。如果你哪天用串口下载却发现没用,先查一下BOOT0是不是被拉高了。
1.3 第一脚确认是所有人的第一个坑
热词里有一条“stm32芯片第一脚怎么确认”,这个问题几乎每个新人都问过。芯片上有个小圆点或者斜切角,那就是第一脚的标志。拿最常见的LQFP48封装来说,把芯片放正,圆点在左上角,那第一脚就在圆点左下角那个位置,然后逆时针数,第48脚正好在圆点右侧。我见过有人拿万用表蜂鸣档去测芯片引脚,结果表笔碰到相邻两个引脚还响,就以为短路了,其实那只是万用表精度问题。确认引脚编号最好用的方法是下载对应型号的Datasheet,翻到引脚分配图那一页,对着实物慢慢数,千万别靠记忆。
1.4 买板子时的几条实在建议
市面上的F103板子形态很多,有那种“最小系统板”的,芯片加晶振加几个电容就完事,也有“学习板”的,板载LED矩阵、数码管、按键、传感器接口全给你焊好了。我的建议是先买最小系统板,原因不是省钱,而是逼你自己去查原理图。接一个LED都要回到原理图上去找那个引脚是接到了PA1还是PB0,这个过程比任何教程都让人印象深刻。
选板时要注意的细节:看下载接口是不是标准SWD,有些精简板喜欢用魔改的排针定义,遇到调试器不兼容就麻烦;看有没有板载串口芯片,没有的话以后调串口还得自备USB转TTL;看3.3V电源的纹波,这倒不用测,主要看LDO旁边有没有足够大的滤波电容。
2. 环境搭建:Keil和VS Code到底选哪个
2.1 先说结论,两个都能用,但别贪多
我在写这篇的时候,网上争论Keil和VS Code哪个好的帖子已经吵了十年了。我的看法是:Keil MDK是STM32官方工具链里最“省心”的,芯片包、调试器、下载算法全给你集成好了,新手装上就能用;VS Code则胜在编辑体验,代码提示、Git集成、Markdown预览都舒服,但环境配置需要你自己折腾插件和工具链。
如果你只是想先把板子跑起来,那就直接Keil MDK,别折腾。如果你是从其他语言转过来的,习惯了现代编辑器,再考虑VS Code也不迟。两条路我都走通了,下面分别说。
2.2 Keil MDK的安装和C51共存的坑
Keil MDK装的时候,最容易被坑的是版本。网上很多人下载的是“Keil C51”,那是给8051用的,根本不能编译STM32。STM32用的是MDK-ARM,也就是现在改名后的Keil MDK。而且很多新手电脑上装了C51又想装MDK,两个版本是能共存的,装完会出现两个不同的图标,一个是C51的,一个是MDK的,千万别开错。
装完MDK之后,还要装芯片包。现在新版Keil用的是Pack Installer方式,打开MDK点那个绿色魔方图标,搜索STM32F1系列,安装对应的Device Family Pack。坑点在于国内下载这个包经常很慢,而且装到一半网络断了就会导致包损坏,打开工程时报错一堆,都是些找不到头文件的乱码。解决办法是去Keil官网手动下载pack文件,然后双击离线安装。
2.3 VS Code配STM32的完整思路
VS Code本身只是个编辑器,要让它编译STM32,需要三样东西:ARM编译器、工程管理插件、调试插件。
工具链方面,最直接的是用ST官方提供的STM32CubeCLI,里面包含arm-none-eabi-gcc。工程管理插件我推荐EIDE,它支持Keil工程和标准库工程,能自动识别芯片型号,还内置了烧录功能。调试插件则是Cortex-Debug,配合ST-Link使用,可以在VS Code里打断点、看变量。热词里有一条“vscode stm32调试powerlink如何设置launch.json”,这里面的PowerLink应该是打错了,实际是PowerShell或者某个调试扩展名,但不管怎样,Cortex-Debug的launch.json核心就是这么几项:
{ "version": "0.2.0", "configurations": [ { "name": "Cortex Debug", "cwd": "${workspaceFolder}", "executable": "${workspaceFolder}/build/project.elf", "request": "launch", "type": "cortex-debug", "servertype": "stlink", "device": "STM32F103C8", "interface": "swd", "runToEntryPoint": "Reset_Handler" } ] }其中最坑的两个字段是device和executable,device要和你芯片包里的型号完全一致,executable要指向实际生成的elf文件路径,路径错了就会报“找不到目标”。
2.4 标准库、HAL库、寄存器,三条学习路线的选择
这是STM32学习路线里争议最大的问题。我的观点是:如果想深入理解芯片工作机制,建议从寄存器或者标准库入手;如果目的是快速做项目,HAL库更高效。但这三种方式各有代价。
寄存器方式最直观,一个GPIOA->CRL = 0x44444444你就知道在配置引脚输入输出模式了,但写外设功能时代码量翻倍,还容易手滑写错位。标准库是ST早年官方出的,把寄存器操作封装成了GPIO_Init、USART_SendData这类函数,代码可读性高,F103资料里绝大部分都是基于标准库的,买开发板送的例程也多半是它。HAL库是ST力推的,最大的特点是移植性好,同样的代码换个芯片型号只要改一下底层配置就能跑,但层次封装多,出了问题不好追踪,尤其对新手来说,一次HAL_UART_Receive卡住半天都不知道在等什么。
我的建议是这样定:第一遍学习用标准库,把GPIO、串口、定时器、中断这些概念搞透彻;后面做项目时再切HAL库,享受跨芯片复用的便利。寄存器不必硬啃,但看懂寄存器操作能帮你排查95%的诡异问题。
3. 从点灯到按键,第一个值得反复折腾的工程
3.1 手把手搭一个标准库工程模板
千万别直接用开发板商家的例程一键点灯,那除了成就感什么都学不到。自己搭工程模板,才是真正理解Keil工程结构的方式。
模板最少需要这些东西:一个main.c,一个stm32f10x_it.c放中断处理,一个stm32f10x_conf.h做外设头文件汇总,再加上启动文件startup_stm32f10x_hd.s和相关标准库源文件。新建好文件后,三个地方一定要配置对,否则编译能过、下载就跑飞。
第一是宏定义,C++选项里要写STM32F10X_HD,这个宏决定启动文件里中断向量表的大小,写错了程序跑起来中断就是乱的。第二是Include路径,把所有头文件所在目录都加进去,少一个就有几百行报错。第三是ROM和RAM的起始地址和大小,F103C8的Flash是64KB,起始地址0x08000000,RAM是20KB,起始地址0x20000000,别照抄RCT6的配置。
3.2 GPIO输出点灯,背后发生了什么
点灯看似简单,但这一步能把你脑子里的“主循环”思维建立起来。看这段标准库的点灯代码:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0);第一句开时钟是最容易被忽略的。STM32为了降低功耗,所有外设的时钟默认都是关闭的,你要用GPIOA,必须先打开APB2总线上的对应时钟。这个设计对新手来说是多余的,但在低功耗产品上至关重要,记住:用任何外设之前,先问自己一句“时钟开了没有”。
GPIO_Mode_Out_PP是推挽输出,意思是芯片内部两个MOS管分别负责拉高和拉低,输出能力强,点LED正合适。如果用开漏输出模式,就不能主动输出高电平,必须靠外部上拉电阻。选错模式会导致LED亮度异常或者干脆不亮。
3.3 按键输入模块的电路设计
热词里有“stm32按键模块电路设计”,这个值得单独说。最简单的按键接法就是一个按键开关一端接GPIO,一端接地,GPIO内部上拉或外部上拉,平时读到高电平,按下读到低电平。但电路设计上有个关键点:按键没有RC滤波的话,机抖会产生几十毫秒的电平抖动,直接读GPIO会读到一串毛刺。
软件消抖的方案是检测到电平变化后延时20ms再确认。硬件消抖方案则是并一个0.1uF电容,让抖动在RC时间常数内被滤掉。我做按键模块时两种消抖都用过,经验是:自己手搓板子图省事就靠软件延时,但如果是做产品,面板按键必须加电容,因为软件消抖吃CPU时间,在主循环任务多的时候会拖累响应速度。
如果按键数量多,比如矩阵键盘,那就别一引脚一按键了,用行扫描加列扫描的方式,从4x4矩阵里读16个按键只需要8个引脚。这个思路会复用你最讨厌的GPIO配置,但真正理解了才发现它就是在不同时刻把引脚切成输入或输出。
3.4 delay卡死的问题和SysTick定时器
热词里有“stm32延时函数delay卡死”,这个问题我见到太多次了。最典型的场景是你在main里写了个简单的延时循环,比如for(i=0;i<1000000;i++);,结果调试时发现程序卡住不动了。大概率原因不是死循环,而是优化等级太高,编译器把这段没意义的循环直接优化掉了,程序就跑飞了。
正解是使用SysTick定时器,这是Cortex-M内核自带的24位倒计时定时器。用标准库做SystemInit之后,调用SysTick_Config(SystemCoreClock / 1000)就能配置成1ms中断一次,然后在中断里对毫秒计数变量自增,延时函数就等这个计数器变化。这样延时的精度和稳定性都比循环靠谱得多。
还有一类delay卡死的情况是中断优先级配置错误。SysTick中断默认优先级是15,也就是最低优先级。假如你在某个中断服务函数里关了总中断(__disable_irq()),然后又调用了依赖SysTick的延时函数,那延时永远不会结束,因为SysTick中断根本进不去。这个坑排查起来很隐蔽,我后来养成习惯:任何延时都不放在关中断的临界区里执行。
3.5 GPIO复用和禁用JTAG
有的人用着用着发现PA15、PB3、PB4这几个引脚程序控制不了,写高电平量出来还是低。这是因为这几个引脚默认被复用成了JTAG功能,PA15是JTDI,PB3是JTDO,PB4是NJTRST。想当普通GPIO用,必须先把JTAG关掉:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这条语句执行完,JTAG的四个脚就释放成普通GPIO了,但SWD还能用,因为SWD只需要PA13的SWDIO和PA14的SWCLK。做板子的时候如果引脚紧张,大可以把这几根JTAG线省掉,只留SWD口,下载调试照样顺滑。
4. 定时器是STM32的灵魂,中断和PWM的核心
4.1 定时器的基本构成和三种主要模式
F103有多个定时器,TIM1和TIM8是高级定时器,带互补PWM输出,能控制电机;TIM2到TIM5是通用定时器;TIM6和TIM7是基本定时器,只能做定时功能。学习顺序上,通用定时器就够了。
定时器构成上,外部是时基单元,内部是一堆寄存器:预分频器PSC、计数寄存器CNT、自动重装载寄存器ARR。工作模式最常见的是三种:向上计数模式(从0数到ARR,然后归零重新数)、向下计数模式(从ARR数到0)、中心对齐模式(先向上再向下)。
预分频器PSC的算法特别适合拿实际例子说。要让TIM2产生1kHz的频率,假设输入时钟是72MHz,那先设PSC=71,时钟变成1MHz,再设ARR=999,计数到1000次才溢出,最终就是1kHz。这个换算方式同样是PWM频率计算和输入捕获的标尺。
4.2 用定时器捕获模式测频率,电路都不用改
热词里的“stm32定时器捕获测频率”,核心原理是利用输入捕获通道检测输入信号边沿,记录相邻两次跳变之间CNT的差值,再换算成频率。
操作步骤分四步:第一步,把GPIO复用成TIM的输入通道;第二步,配置TIM输入捕获参数,选择上升沿触发;第三步,使能捕获中断,在中断里保存当前CNT值;第四步,算频率。频率公式很朴素:频率=定时器时钟/两次捕获CNT差值。
实测中最大的坑是定时器溢出。假设输入信号很慢,两次跳变之间CNT已经翻转了几圈,算出来的差值就会凭空少一大截。解决思路是在中断里同时记录是否发生了溢出,还有在捕获中断里同时开Update中断来计数“绕圈次数”,最后用总计数差值来算频率。这就是嵌入式领域常说的“分高低位”思想。
4.3 超声波测距的真实现场
热词里有“stm32超声波测距”,最常见的模块是HC-SR04。原理是:给它Trig引脚一个10us的高电平脉冲,模块内部会发出8个40kHz的超声波,然后Echo引脚会输出高电平,高电平持续时间就是声波往返的时间。距离=高电平时间×340m/s÷2。
但真正实现的时候,有两点特别容易翻车。第一点,HC-SR04的工作电压是5V,Echo引脚回过来的也是5V电平,直接接到STM32的PA口上,长期下来可能损伤引脚。稳妥的接法是加一个分压电阻网络,或者用两个电阻把5V分到3.3V。第二点,测距函数里Echo的脉冲等待绝不能用阻塞式延时,否则一块板子测距时别的任务全卡死了。我发现用外部中断加定时器捕获是最优雅的:Echo上升沿触发中断,打开定时器,下降沿再触发一次,读CNT值,两个沿之间就是时间。
4.4 PWM调速和舵机控制的参数设置
PWM就是把小周期方波的高占空比变化成平均电压的变化。控制LED亮度、电机转速、舵机角度,用的都是同一个机制。
舵机控制是PWM应用里最直观的。常见SG90舵机的控制信号是50Hz,也就是20ms周期,高电平在0.5ms到2.5ms之间对应0到180度。用定时器实现时,ARR设成1999,也就是20ms一个周期,CCR值从50到250,对应0.5ms到2.5ms。这组参数我当年背了很久,后来发现根本不用背,只要记住周期是20ms、脉宽是0.5到2.5ms,其他全是可以算出来的。
PWM相关的进阶问题,是高级定时器里刹车功能和互补输出的概念。如果你以后做电机驱动,TIM1和TIM8的PWM输出选项里那堆CH1N、CH2N才是主角,两路互补PWM加上死区插入,配合MOS全桥驱动,一个电机驱动板的核心闭环就出来了。
5. 通信外设,让芯片跟世界对话
5.1 串口USART,调试的第一根救命稻草
STM32的USART是最重要的调试手段,没有之一。你想知道程序跑到哪了、某个变量对不对、某个外设返回了什么数据,往串口上打印,全明白了。开发板上一般都接了USART1到板载串口芯片,PA9是TX,PA10是RX。
新手最容易搞混的是交叉接线。STM32的TX要接上位机的RX,STM32的RX要接上位机的TX,直接TXRX对接的话,数据会全丢。然而板载芯片通常已经帮你接好了,你只需要用USB线连电脑。
如果是自己画板子接USB转TTL模块,还涉及到共地问题。串口通信要求两边地电位一致,不共地方能通就很玄学了,可能偶尔能打印两行乱码然后就没了。我亲眼见过有人排查这个问题折腾了一下午,最后插了根地线马上就好。
还有个小经验:USART中断优先级一定要设好。中断里读数据时,处理流程尽量短,把数据先塞进环形缓冲区,等主循环空闲了再解析。如果直接在中断里做字符串解析,遇到高频数据时缓冲区会被淹没,又会出现“程序好像卡死”的假象。
5.2 I2C读写BH1750和OLED屏,动手做第一个完整传感器
I2C是个只有两根线的协议:SCL时钟线、SDA数据线,靠起始条件、停止条件、ACK应答来传输数据。F103的I2C外设香不香,论坛上吵了很多年。硬件I2C偶尔会出奇怪的时序问题,所以83.6%的手写例程都教你用软件模拟I2C。我的看法是,学习阶段可以用软件模拟,因为你更关注传感器怎么交互;但要深度体会I2C的时序,后期还是得碰硬件的。
BH1750是数字光照强度传感器,发出的是16位lux值。接法很简单:VCC接3.3V,GND接地,SCL和SDA分别上拉到3.3V,然后接到STM32的PB6和PB7,这是I2C1的硬件引脚。操作流程是:先向BH1750发一条上电命令,再发一条连续测量命令,然后读取两个字节的高位和低位,合成后的结果除以1.2才是最终光照值。
OLED屏那边就更有意思了。如果你用的是常见的SSD1306驱动的0.96寸屏,同样是I2C接口。画一个点看起来微不足道,但整屏操作时需要你理解显存的概念:一块128x32的屏幕,对应的显存是1024个字节,每个bit对应屏上一个像素。我想通这个点的时候,才明白为什么显示字符串要先做字模,为什么取模软件能直接把字符转成十六进制数组。
5.3 CAN通信突然连不上的排查实录
热词里有“stm32 can通信突然连不上”,这个问题很典型。CAN总线的物理层是两根差分线,CAN_H和CAN_L,总线两端各要有一个120欧终端电阻来匹配阻抗。如果你的板子没有集成终端电阻,又没有在网线两端接上,信号反射严重,就会出现“一开始通信好好的,过一会儿就一堆错误帧”的情况。
软件侧的第一个排查点是波特率。CAN的波特率由预分频和位时间配置决定,只要两端不一致,哪怕差一点点,同步段和采样点就不对,总线会一直在错误状态里出不来。第二个排查点是过滤器配置,F103的CAN控制器有28个过滤器组,如果过滤器掩码设得太严格,所有报文都会被丢弃,表现出来就是“我记得我发了帧,怎么对方收不到”。
最气人的实际问题往往是总线正负极接反。CAN_H被接成了CAN_L,数据灯看着在闪,但总线上全是错误帧。后来我做CAN调试都会先拿万用表量一下总线对地电压,CAN_H对GND大约是2.5V到3.0V,CAN_L对GND大约是1.5V到2.0V,两个电压都不对或者两个一样,多半就是接反了。
5.4 让F103变成USB设备
热词里有一条“stm32 如何做usb设备”,F103系列部分型号内置USB全速设备控制器,可以自己枚举成鼠标、键盘、串口设备。这里最容易踩的坑是:USB相关引脚PA11和PA12,你要么在代码里配置成USB功能,要么干脆别碰它,如果当成普通GPIO去点LED,USB就废了。
软件方面,如果用标准库,你可以直接搭一个USB鼠标的例程,关键就是配置三个端点:控制端点0用来响应标准请求,中断端点用来上传鼠标位置数据,再一个中断端点接收主机下发的数据。USB协议栈的逻辑对于初学者来说很绕,但最笨的办法是照着一份能跑的例程,把描述符里的VID和PID改成自己的,再把报告描述符改成你想要的数据长度。我第一次做USB鼠标的时候,就卡在Report Descriptor上报了几个字节的顺序,调整了三版才让Windows识别正确。
USB设备排除问题有个经典技巧:插上USB线后,Windows设备管理器里如果能看到“未知设备”,说明中断和描述符还是有问题的;如果完全无反应,先查PA11和PA12的焊点,再查晶振是不是起振。USB对时钟精度要求比串口高,F103用内部8MHz RC再怎么校准,插上电脑依然可能不稳定,最好还是用外部8MHz晶振。
5.5 从串口到网络:HTTP库和ESP32-C3的联动
热词里还混着“stm32 http库”“esp32-c3开发板包”。如果你想让STM32发HTTP请求,最直白的方案是接一个ESP8266或者ESP32模块,通过串口AT指令连WiFi,再用透明传输模式把HTTP报文发出去。普通F103自己是不能直接上网的,因为它没有以太网MAC或者WiFi协议栈,硬要它上网就得外挂模块。
ESP32-C3这种芯片是RISC-V内核,自带WiFi,跑起来是另一个世界了。我跟F103对比过,如果只是做传感器数据采集,F103性价比很高,但如果涉及到物联网平台对接、MQTT订阅、OTA升级,那直接上ESP32会省很多事。两个芯片之间的通信用串口(UART)最省心,很多项目都是F103负责实时控制,ESP32负责网络,通过串口交换JSON格式的数据,这思路做智能家居的原型很顺手。
6. 调试和烧录,编译成功只是万里长征第一步
6.1 下载器怎么选,ST-Link和J-Link的区别
热词里有一条“apt32101开发板怎么选jlink类型”,这里面的APT32101我没用过,但选择调试器的方法论是通用的。ST-Link是ST官方出的,最便宜的那种V2版本也就二三十块钱,对付F103完全够用,支持SWD和JTAG。J-Link是SEGGER的,价格贵一个数量级,好处是调试速度飞快,断点数量和Flash下载算法都很强悍,适合做大型复杂项目。
对新手来说,直接ST-Link V2,因为它和Keil的整合最成熟,插上USB,装好驱动,MDK里选择ST-Link Debugger,再把SWD的引脚选对,就能用了。J-Link的驱动有时会跟ST-Link驱动冲突,两个都装过的人会发现Keil里调试器下拉框多了好几个选项,选错了就会一直提示找不到目标。
6.2 VS Code编译成功却烧录不进,十大常见原因
热词里“vs code里编译成功,却怎么也烧录不进开发板”,这个我太有共鸣了。编译只是生成了elf和hex文件,烧录是另一条路。常见原因大致能排成一串:
- 调试器没接对。SWDIO、SWCLK、GND、3.3V四根线,接触不良是最隐蔽的,排针虚焊时有时无
- 芯片供电没独立。目标板如果由调试器供电,板子上又接了一个USB供电,两个电源打架,芯片反复复位
- 下载算法没选对。Keil里Utilities设置和STM32 ST-LINK Utility设置里,Flash Download的算法选成了别的型号,下载就报错
- BOOT0跳线接反。串口下载要拉高BOOT0,SWD下载要拉低,很多人从串口转SWD时忘了拨回去
- 引脚复用冲突。PA13和PA14虽然是SWD专用口,但如果你代码里把它们配置成了普通GPIO,芯片一运行就把调试接口禁用了,表现就是刚烧录第一次成功,第二次就再也连不上
- 芯片被读保护锁了。读保护启用后,调试器无法读取芯片内容,必须先全片擦除
热词里那条“load project.axf error: fla”,我在很多新手帖里见过完整版,通常是Flash下载算法不匹配或者Flash容量不足。F103C8的Flash是64KB,编译出来的axf超过64KB就放不下。解决办法是精简代码,或者换大容量型号。
6.3 调试手段从打印到断点,再到波形
串口打印是调试的地基,但只靠printf总有一天会遇到瓶颈。比如中断频率很高的场景,打印本身就会改变程序的时序,你看到的结果全是假象。这时候要用断点:在Keil里点一下行号前面的灰色区域,程序运行到那里就停下,然后看Watch窗口里的变量值,单步执行观察每一步的行为。
要看IO输出波形,最省钱的方式是用逻辑分析仪,8通道那种百来块钱的就能用。把探针夹在某个GPIO引脚上,观察是不是高电平、低电平、PWM脉宽对不对,比在示波器上摸索快十倍。热词里的“keilc stm32查看io输出波形”确实用逻辑分析仪更合适,因为示波器价格贵,而且两通道级别的示波器看数字信号不如逻辑分析仪一目了然。
调试高级技巧里还有RTT,用J-Link可以通过RTT在目标芯片和主机之间传数据,不占串口,时序影响极小。不过这对于F103新手来说属于锦上添花,先把断点和打印练熟再说。
6.4 延时不生效、程序跑飞、复位的排查逻辑
程序跑飞是嵌入式调试里最让人头大的问题,可它的常见原因就那么几种。第一种是数组越界或者野指针,把栈区破坏得一塌糊涂,中断返回后PC指针直接跳到奇怪地址。第二种是中断服务函数里没有清中断标志位,中断反复触发,程序看起来像死机。第三种是电源纹波太大,芯片在跑高频率外设时电压掉得厉害,直接复位。
排查时别上来就翻代码,先做排除法:换一个别人能跑的hex文件烧进去,如果还跑飞,问题在硬件;如果好了,问题在你的代码。然后缩小范围,把外设一个一个加上去,加一个测一次,很快就能定位到是哪个外设初始化或者中断处理的问题。
7. 学完F103之后,往哪个方向走
7.1 同门升级路线:F4、H7、G0,还是干脆换个赛道
F103学会之后,路线选择很宽。想继续深耕STM32的,可以上F407或者H743,一个多了DSP和FPU,一个性能翻了好几倍。想往Linux方向走的,可以接触全志T113、瑞芯微3588、Xilinx Zynq这种Linux开发板。热词里“arm开发板qt文泉字体”和“开发板挂载ubuntu”就是Linux板子会遇到的问题,这类板子和单片机的玩法完全不同,它们跑的是完整操作系统,你的代码跑在用户态,接触底层的方式变成了驱动开发和设备树,这是一个全新的学习曲线。
选型标准可以这么看:F103适合你对“位、寄存器、中断、时序”建立感觉;F407适合你要做音频、做电机控制这些需要算力的场景;3588这类高性能板子适合跑AI推理、人脸识别,或者同时带多个摄像头的场景。热词里的“星宸科技开发板”就是做视觉类方案的,这类板卡的资料是否齐全、SDK是否完善,比硬件参数更影响你的学习效率。
7.2 项目化学习比教程式学习更容易坚持下去
一个人盯着屏幕看教程是很容易放弃的。我的个人体会是,学的每一步都最好对应一个能看得见效果的小项目。学完GPIO,就把LED灯和呼吸灯做出来;学完定时器就做闹钟或者秒表;学完串口就做一个电脑上能显示温度的温湿度传感器;学完I2C就把OLED屏驱动起来,然后加一圈传感器做得像模像样。这些项目单个都不大,但组合起来,你对一块单片机从引脚到中断到总线的整个链路就有完整的体感。
进阶一点的玩法,是把F103和一些别的芯片联动起来,比如K210做视觉识别,STM32控制执行机构,两个芯片通过串口交换坐标数据,就能拼出一个跟随小车。或者把Agile Modbus移植到STM32上,自己实现Modbus从站,连PLC和触摸屏,这就直接往工控方向靠了。热词里“stm32+lin收发器”则是汽车电子方向的玩法,LIN总线在车身控制里到处都是,学起来也算是一门手艺。
7.3 踩过的坑汇总,给买板前后的人一份清单
最后我想把这一路踩过的坑按阶段整理一下。买板前要确认:板子接口是不是标准SWD、有没有板载串口芯片、原理图是否开源、芯片型号是不是F103C8/RCT6、供电方式是否是5V USB转3.3V稳压。装环境要确认:Keil选MDK而不是C51、芯片包装的是STM32F1系列、宏定义与芯片型号匹配、调试器驱动正常。写代码要确认:每次操作外设前先开时钟、延时用SysTick而不是for循环、开复用功能时把JTAG禁止、中断服务函数尽量短。调试要确认:串口RX接TX、USB转TTL共地、BOOT0状态、SWD四根线接触。这几个确认清单弄完,后面就顺了。
回想这段时间,F103这块几十块钱的板子对我的帮助,不在于它有多强的性能,而在于它以最原始的方式逼我搞清楚CPU、时钟、总线、中断和外设这些底层概念。之后的任何芯片,不管是M4内核的新款MCU,还是跑Linux的高性能平台,本质上都在复用这套思维框架。所以说,把这块小板子吃透,比急着换高价开发板更有价值,是不折不扣的长期主义。