
板子到手那天我拆开静电袋把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. 板子到货第一天从环境搭建到点亮第一颗LED2.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: value123就能把迷雾切开。使用串口之前需要理解几个关键参数波特率、数据位、停止位、校验位。我和绝大多数人一样直接使用默认配置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设成999CCR设成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和BC解决了”也好。这些记录会在一个月后变成你最宝贵的debug资料库。我在调试CAN通信时就是翻出了自己一个月前记录的一条“上次没接地线导致不稳定的经验”才快速定位问题。6.3 一些体会说回那块让我沉默十分钟的STM32F103开发板。如果现在有人问我拿到它之后最重要的第一步是什么我不会说是装软件也不会说是看原理图而是先别怕把它弄坏。开发板这种东西烧错一两个引脚、搞坏一两个模块太正常了它的价值本来就是在试错中被吸收掉的。你把它放在桌上当宝贝供着反倒学不到东西。每一次“点个关注”式的分享冲动背后其实都藏着一次有价值的学习记录。我越往后越发现嵌入式这个圈子进步最快的方式不是收藏无数教程而是公开复述自己踩过的坑。等到你能把一个翻车经历讲得有条有理让人隔着屏幕都能感受到当时排查时的挫败和豁然开朗你就真的入门了。