☰
STM32F103开发板入门指南:从点灯到串口、中断与PWM实战
2026/10/5 7:36:31 网站建设 项目流程

板子到手那天,我拆开静电袋,把STM32F103开发板翻来覆去看了一遍,然后陷入了十分钟的沉默——不知道第一步该干嘛。网上教程一大堆,但真正贴着自己手上这块板子走的路线,好像没有人给我画清楚。这篇文章就是把我从“插上USB线不知道看哪里”到“能自己接传感器、调串口、写中断”这个过程完整复盘一遍,给同样刚拿到开发板的你一个能直接抄的路径参考。

1. 为什么是STM32F103这块“老古董”依然是新手的合理选择

1.1 F103在入门生态里的位置:不是最炫,但最稳

我观察到很有意思的现象:现在身边玩STM32的新人,第一块板子十有八九是F103,或者号称“兼容F103”的国产同型号芯片。有人嫌它老,觉得Cortex-M3核心已经是上个时代的东西,为什么不去买带M4、M7甚至RISC-V的新板子?我有过同样的怀疑,但真踏实用下来,才理解F103在这个圈子里的生态地位有多特殊。

先说性能。STM32F103的最高主频72MHz,放在今天确实不算炸裂,但它的定位从来不是跑浓重计算,而是“控制”。你要做的按键检测、LED控制、电机调速、传感器读取、串口通信,72MHz绰绰有余。如果你连外设都还没玩明白,直接上一块几百兆赫兹的新芯片,大概率只是把时间花在配置时钟树和调试IDE上面,而不是真正学习嵌入式逻辑。

再说资料密度。F103的教程之多,已经到了一个词条能被反复咀嚼的程度。出问题的时候,搜“stm32 usart 乱码”能搜出几个论坛上百条帖子的完整排查思路,而冷门芯片搜到的结果可能只有官方的英语勘误表。这个信息密度对新手来说是巨大的隐性收益。不是每块板子都能让你在凌晨两点debug失败时找到一条救了命的老帖子。

最后是成本。F103系列的核心板和最小系统板价格非常低廉,几十块钱能买到带一堆外设的豪华版本。即使不小心把BOOT引脚接错或者烧错程序导致芯片锁死,再换一片也就是一顿午饭钱。这种“试错成本低”的优势,在学习阶段比任何纸面性能都重要。

1.2 买板子时真正要看的硬件选项

我的建议是分两种买法。一种是走捷径,直接买带板载外设的整合板,比如常见的正点原子、野火等开发板。好处是板上直接做好了LED、按键、USB转串口、屏幕接口、传感器排针,不用自己飞线,拿到手就能跑demo。坏处是外设被板子“代管”了,你对时序、上拉下拉电阻这些物理细节感知会弱一点。

另一种是买一张纯粹的“最小系统板”或者“核心板”,只有芯片、晶振、供电、调试口和排针。这种板子更像裸芯片,能让你从电路层面理解每一个外设为什么需要对应的引脚和配置。我实际用下来,两者交替是最好的:先拿整合板跑通软件层面的逻辑,再拿核心板自己去搭LED电路和传感器接线,知识才算真正闭环。

具体挑选时重点看四个点:电源接口是否带防反接、BOOT0/BOOT1跳线是否方便切换、调试接口是ST-Link还是J-Link的哪种排针定义、板载LED是否引到了容易操作的GPIO上。很多新手买回板子先摔一跤的地方就是烧录口定义搞混,把SWD的四根线接错,芯片没反应,吓得以为板子坏了。其实只要对照板子丝印和芯片数据手册里的引脚图重新核对一遍就好了。

2. 板子到货第一天:从环境搭建到点亮第一颗LED

2.1 开发环境选型:Keil、标准库与HAL库的分歧

这是新手上路遇到的第一道选择题,而且网上吵得不可开交。有人说必须用HAL库,有人说标准库才是王道,还有一群人推荐VS Code加插件走开源工具链。我不替你做决定,但我的建议是基于你的最终目标来选。

如果你打算以后进公司做常规嵌入式开发,用HAL库是更顺应主流的路径,因为ST官方已经停止标准库的持续更新,新产品工具链都往HAL和CubeMX上靠。如果你是为了理解芯片底层的寄存器操作,标准库的代码更直白,一个寄存器一个寄存器地赋值,你能更清楚地看到“写这个位到底控制了什么”。

我个人的路线是:第一周用HAL库配合STM32CubeMX生成工程,把注意力放在外设配置逻辑上而不是写代码的体力活上。等跑通几个demo之后,再回头用标准库或者直接操作寄存器的方式,把一个GPIO的初始化代码自己手写一遍。这个过程会逼迫你去看芯片参考手册里的GPIO寄存器描述,等于把外部逻辑和内芯逻辑串起来了。

环境本身其实非常简单。Keil MDK安装之后,需要装对应的芯片支持包,很多新手在这里卡住是因为安装Keil时没有勾选对应的器件库,导致新建工程时找不到STM32F103C8T6这种型号。另外要注意的是,如果你之前装过Keil for C51,和MDK共存在一个电脑里是常见操作,但安装顺序有讲究,先装C51再装MDK通常不会互相干扰,反过来偶尔会出注册许可冲突。如果实在搞不定,直接用STM32CubeMX生成Makefile工程,然后配一套VS Code加ARM GCC也是完全可以,我有段时间就是这么干的,还顺手解决了代码跳转和格式化的问题。

2.2 烧录前的三个检查点

不管你用什么工具链,烧录第一个程序之前都建议按顺序做三个检查。

第一是给板子供电,用USB线或者外部电源把板子点亮,观察电源指示灯。如果灯不亮,先量一下电压,MP1584这类板载稳压模块输入输出是不是正常,而不是怀疑芯片烧了。第二是确认调试器,不管是板载ST-Link还是外接J-Link,在设备管理器里看到对应的COM口和调试器设备才算就绪,没有出现就检查驱动和线材。第三是检查BOOT0跳线,如果想通过调试器直接烧录并运行Flash里的程序,BOOT0必须保持在低电平状态,如果被跳到了高电平,芯片会从系统存储器启动,调试器连上了也控制不了你的程序。

这三个检查我每次都做,因为它们能帮你把问题域迅速切割开:是供电问题还是连接问题还是启动模式问题。只要一路查到某一个环节发现异常,基本不用瞎猜。

2.3 点亮LED背后的GPIO代码与寄存器直觉

点亮LED是嵌入式界的“Hello World”,但它比打印一行文字有意义得多,因为它强迫你去理解一个GPIO引脚的完整状态。HAL库的代码大概是这样:

GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); while(1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); HAL_Delay(500); }

这段代码里值得琢磨的不只是“拉高拉低”。首先你要理解GPIOA的时钟为什么需要单独使能,因为STM32的外设时钟默认是关闭的,你不打开它,寄存器写进去根本没反应。很多人第一次做这个工程,发现代码明明编译烧录成功了,LED纹丝不动,排查半天结果是漏了__HAL_RCC_GPIOA_CLK_ENABLE()。其次要注意GPIO_MODE_OUTPUT_PP是推挽输出,这个模式可以让引脚主动输出高和低两种电平,而如果是开漏输出,就必须靠外部上拉电阻才能输出高电平,LED接法完全不一样。

判断LED应该接在哪一引脚、是高电平点亮还是低电平点亮,取决于板子原理图。大多数入门板会把LED正极接在GPIO上、负极经限流电阻到地,这样引脚输出高电平点亮;也有少数板子是反的,输出低电平点亮。不看原理图,凭感觉写代码,你就等着第五次烧录之后怀疑人生吧。

3. 让它开口说话:串口调试和中断的基础功

3.1 串口为什么是学嵌入式第一课

LED能闪了之后,下一个立刻值得学的就是串口。因为它能让你看到芯片内部的状态:变量值多少、程序跑到哪个分支、传感器数据是多少。调程序最痛苦的就是“感觉没跑对但不知道里面发生了什么”,串口输出一行debug: value=123就能把迷雾切开。

使用串口之前需要理解几个关键参数:波特率、数据位、停止位、校验位。我和绝大多数人一样,直接使用默认配置,8个数据位、1个停止位、无校验,波特率设成115200。这些都是发送端和接收端之间的事先约定,就像两个人打电话前先约好用哪种语言,两边对不上就是满屏乱码。

3.2 printf重定向与串口调试的实际用法

Student在调试阶段频繁做的事情是通过串口把信息打印出来。在HAL库里,标准库的printf并不会自动从串口输出,需要自己实现一个fputc函数把字符丢进串口发送寄存器,这个过程叫做“重定向”。写法也不复杂:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }

注意在Keil里你还得勾选“Use MicroLIB”,否则标准库的底层实现会占用大量资源,重定向可能不生效。这里有一个我踩过的坑:MicroLIB版本的printf默认不支持浮点输出,你在串口里打印出来的温度值直接变成个空串或者奇怪字符。解决办法要么用整数运算自己把小数部分拆出来拼两个整数打印,要么换用vsnprintf手动格式化。新手碰到这个问题时往往以为是接线问题,其实完全是编译选项的锅。

串口调试还有一个非常实用的操作:把调试开关做成宏。在代码顶部定义#define DEBUG_ENABLE 1,调试阶段把所有关键节点打印出来,正式运行的时候改成0关掉打印,避免串口输出拖慢实时控制逻辑。这个习惯越早养成越好,后续做电机控制、飞控调参时能省一半时间。

3.3 中断:按键消抖、超声波测距的第一步

串口学会了之后,自然要面对的一个概念就是中断。我用一个生活化的类比:你在厨房煮汤,定时去看一眼是轮询;锅里发出噗噗声你立刻关火,这是中断。CPU不能一直在某个引脚上倒计时等待,那样干不了别的事,只有引脚电平变化时让CPU暂停当前任务去处理一下,这才是中断的意义。

STM32的外部中断(EXTI)配置在HAL库里通常分三步:引脚配置为中断输入模式、设置中断触发条件(上升沿、下降沿,或者双边沿)、在中断回调函数里写处理代码。比如接一个按键,我把按键一端接地,另一端接PA0,内部上拉使能,平时读到高电平,按下时被拉低,于是配置下降沿触发。在实际测试中,我遇到过按键按一次程序触发了两三次的情况,这就是“抖动”问题,单纯靠中断的沿触发是无法解决的。我要么在中断回调里加一个简单的延时或者状态机消抖,要么改用定时器来实现扫描去抖,按键应用才真正稳定。

超声波测距模块的HC-SR04原理也和中断扯得上关系:给它一个10us的触发脉冲,它会返回一个高电平脉冲,这个高电平持续时间就是超声波往返时长。想在STM32上测这个时长,正确做法是用定时器的输入捕获功能,而不是用HAL_Delay去卡。这里引入的中断思路是定时器捕获中断:当引脚电平跳变时,定时器把当前计数值记录下来,并产生中断让你读取。下次我再细讲这个计算过程,但你现在可以先建立概念。

4. 定时器与PWM:从“会亮”到“会动”的关键一跃

4.1 理解定时器的本质

如果说GPIO是STM32的手脚,定时器就是它的节拍器。我一开始对定时器最大的误解,是把它当成了一个只能做延时的工具——HAL_Delay不也是延时吗?实际使用后才发现定时器是独立的硬件计数器,它不占用CPU,自己在那里数时钟脉冲,数到设定值就可以产生中断、翻转引脚、触发其他外设,这一切都在后台并行发生。

初学者最容易翻车的地方就是“延时函数delay卡死”。我遇到过好几次:HAL_Delay(500)下去程序直接卡住不动了。原因排查了半天,最典型的是在中断回调函数里调用了HAL_Delay。HAL库的延时是基于SysTick中断实现的,如果你正在处理优先级更高的中断,SysTick中断无法抢占,HAL_Delay就永远等不到那个tick,死循环了。解决办法是在中断处理里避免延时,改用标志位,或者在主循环里再处理实际动作。

另一类卡死在定时器相关代码的情况,是定时器没有启动。你说我明明调用了HAL_TIM_Base_Start_IT,怎么中断就是不进?检查一下定时器的时钟是否在CubeMX里被正确勾选,以及预分频PSC和自动重载ARR的值是否合理。比如我给你一个计算方法:假如APB1定时器时钟是72MHz,预分频设为72减1,那么计数频率就是1MHz,再设置ARR为1000减1,就得到1ms中断一次。这个“减1”是无数新手在这里看半天手册才能反应过来的地方,因为硬件从0开始数,数值本身就是周期数。

4.2 用PWM驱动LED亮度与电机控制

PWM(脉冲宽度调制)简单说就是快速切换高低电平,通过改变高电平占空比来改变“平均电压”。用在LED上就是亮度变化,用在电机上就是转速变化。关键在于理解“频率”和“占空比”是两回事,实际项目里,一个是维持稳定感,一个是决定输出大小。

在STM32上做PWM,基本思路是配置一个定时器的某个通道工作在PWM模式。还是以F103为例,用定时器2的通道1输出到PA0,配置好预分频、周期和比较值,输出引脚上就会产生波形。比较值CCR和周期ARR的比值就是占空比,比如ARR设成999,CCR设成500,占空比就是50%。我用PWM控制过一个舵机,舵机的角度就是靠周期20ms、占空比0.5ms到2.5ms的方波来映射的,当时拿着逻辑分析仪调了好一会儿才明白:不是占空比越大角度越大那么简单,而是要严格按照舵机数据手册的脉宽范围来。

如果要控制电机,还需要在PWM输出后面加一个驱动电路,最常见的就是L298N或者DRV8323这类驱动板,再配合几个GPIO控制电机正反转。很多入门者在这里搞混:单片机给到驱动板的PWM信号和电机线之间的电压不是一回事,驱动板本身要有独立电源,PWM只是控制信号。你如果拿单片机引脚直接去驱动电机,轻则带不动,重则电流倒灌,一片芯片就这么烧了。

4.3 定时器输入捕获:超声波测距与测频率的实战

我因为想做超声波测距,才真正深入学了定时器的输入捕获模式。HC-SR04模块返回一个宽度和距离成正比的高电平脉冲,这个宽度往往是几十微秒到二十几毫秒不等,如果用HAL_Delay去读GPIO电平,误差大得离谱,主循环一忙就丢脉冲。输入捕获的做法是:把超声波的Echo引脚接到定时器通道上,配置上升沿和下降沿都触发捕获中断,在中断回调里分别记录两个计数值,差值就是脉宽,再乘以时间分辨率算出时间,最后用声速340米每秒除以2得到距离。

同样思路也能用来测频率:固定一个测频时间窗口,统计上升沿或下降沿的捕获次数,就能换算出信号频率。实际操作中要注意定时器的位宽,F103的通用定时器大多是16位,最大计数值65535。如果预分频设得不好,计数溢出这件事你必须处理,否则测时间会差一个周期。我当时做超声波项目时就把ARR说得很大还是不够用,最后实在不行就在中断里加了溢出次数计数,把高位和低位拼起来。这也是嵌入式开发的乐趣之一:每个看似简单的问题,深入下去都是寄存器位宽、溢出处理、中断优先级这些基础功的比拼。

5. 进阶路上的翻车现场:JTAG引脚、CAN通信和USB设备的坑

5.1 我把调试口禁用之后……

事情是这样的:我想把PB3、PB4这两个引脚当成普通IO用,于是翻手册查到了“禁用JTAG、保留SWD”这个配置。代码写下去,重新烧录时发现调试器连不上了。我当时还没有想到,如果程序里已经把JTAG引脚复用成GPIO,调试器的连接就会被你的程序在启动瞬间切断。

这个坑的解决办法分两种。如果你用的是SWD接口(两根线加复位、地),那么要保证程序里不要禁用SWD,只禁用JTAG。很多STM32型号的SWD引脚是PA13和PA14,而JTAG占用的PB3、PB4等引脚完全释放之后不影响SWD调试,问题不大。还有一种通用解法,改接BOOT0跳线到高电平,让芯片从系统存储器启动,内核不会执行Flash里的程序,调试器就可以重新连上,再把跳线拨回去重新烧录正常代码。这套操作本质上是“用硬件启动模式绕过坏程序”,我强烈建议每个新手学会,因为你在折腾引脚复用的时候一定会遇到。

这个坑教会我一个非常重要的原则:改复用功能之前先确认调试接口用哪组引脚,尽量不要去动PA13/PA14这种调试必需的引脚,否则轻则连不上,重则芯片锁死。

5.2 CAN通信突然连不上

热搜里有个词条叫“stm32 can通信突然连不上”,我一看就笑了,因为这太常见了。CAN总线用的是两根差分信号线CANH和CANL,终端电阻必须接在两个端点,通常每个节点120欧,整条总线两端一共60欧,这是基本要求。有一次我把板子从工作台挪到另一个地方,换了一段更长的杜邦线,发现怎么都进不了中断,查到最后是终端电阻被我不小心拆掉了,导致信号反射、电平差不够。

另一个新手常犯的错误是把CAN收发器的地线和信号地忽略了。USB转CAN模块和开发板之间如果地电位不一致,差分信号再强也可能出现大量错误帧,通信表现为“时好时坏”。我的经验是先用一个简单的回环测试把CAN控制器内部回路接通,排除硬件问题,再接入外部收发器测试。当“突然连不上”发生的时候,先别急着改代码,检查物理层:地线、终端电阻、CANH和CANL有没有接反。CANH和CANL接反是最隐蔽的,因为不用的收发器不会立刻冒烟,只是通信完全不通。

5.3 USB设备和外设兼容性问题的思路

想做STM32的USB设备,是很多人的进阶目标。STM32F103带USB外设,可以虚拟串口、模拟键盘鼠标、做成HID设备。但这里面有个坑:USB需要外部晶振提供精确时钟,很多最便宜的开发板用的是内部RC振荡器或者不合规格的晶振并联电容,导致USB枚举时主机端报“无法识别的USB设备”。我当时买过一片板子,USB枚举时灵时不灵,后来换了一块带8MHz晶振且负载电容匹配的板子就彻底解决了。

另外一个高频问题是和ILI9341屏幕相关的:用某款屏幕模块读ID得到A1A1而不是期望的9341。这个多半是SPI时序不匹配或者复位引脚时序不对,串行接口识别出错。解决方式是先重点检查复位引脚,确保在初始化时拉低一段时间再拉高,然后逐步降低SPI时钟频率试试。很多时候不是代码逻辑错,而是时序裕量不足,新手会在这里陷入“以为自己没配对”的死循环。

6. 从板子到项目:学习路线的落地建议

6.1 三个适合新手的STM32小项目

当你把GPIO、串口、定时器、中断、PWM这几块都跑过一遍之后,继续做串讲型小项目是快速拉升能力的方式。我推荐三条路线,难度正好成阶梯。

一是超声波测距仪,把HC-SR04的触发接一个GPIO,回波接定时器输入捕获,测得距离后在OLED屏上显示,再通过串口打印曲线。这个项目覆盖了GPIO输出、定时器捕获、I2C/SPI驱动显示、串口调试,知识面很综合。做完之后你对“传感器数据是怎么一步步从引脚电平变成人可读数字”的链路会有完整的体感。

二是环境监测加联网上报,比如DHT11或者DS18B20测温度,通过ESP8266模块或者直接接一个便宜的上位机,把数据传到云平台。热搜里“巴法云”“stm32鱼缸”都是这种玩法。这个项目会逼你去解决通信协议、数据解析、错误重传的问题,比单纯点灯有意思得多。

三是做一个带减速电机的小车,用PWM控制速度、用定时器编码器接口读转速,再用按键切换模式。这是从“控制”角度切入的经典项目。如果还能加入五线四相步进电机这种需要严格时序的对象,你还会更加深入地理解GPIO翻转速度和中断间隔的关系。

6.2 项目式学习的节奏

做项目最忌讳的是想着“把全部外设学完再动手”。我用实际经历证明,一开始就动手做超声波测距的人,比把所有库函数手册翻了一遍再动手的人,进步速度快一倍。遇到不懂的功能,直接查芯片参考手册对应章节,把用到的寄存器理解透,剩下没用的暂时放着。这种“即用即学”的方式可能知识结构不如看手册那样体系化,但知识的记忆牢度和应用能力是最扎实的。

同时建议养成写学习笔记的习惯。不需要写得多么工整,哪怕就是记录“今天遇到了XX问题,尝试了A和B,C解决了”也好。这些记录会在一个月后变成你最宝贵的debug资料库。我在调试CAN通信时就是翻出了自己一个月前记录的一条“上次没接地线导致不稳定的经验”,才快速定位问题。

6.3 一些体会

说回那块让我沉默十分钟的STM32F103开发板。如果现在有人问我拿到它之后最重要的第一步是什么,我不会说是装软件,也不会说是看原理图,而是:先别怕把它弄坏。开发板这种东西,烧错一两个引脚、搞坏一两个模块太正常了,它的价值本来就是在试错中被吸收掉的。你把它放在桌上当宝贝供着,反倒学不到东西。

每一次“点个关注”式的分享冲动背后,其实都藏着一次有价值的学习记录。我越往后越发现,嵌入式这个圈子进步最快的方式,不是收藏无数教程,而是公开复述自己踩过的坑。等到你能把一个翻车经历讲得有条有理,让人隔着屏幕都能感受到当时排查时的挫败和豁然开朗,你就真的入门了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询