刚把手头这块STM32F103的开发板拆开,树脂味还没散,我就在琢磨怎么把这条学习路线写得清楚一点。对于刚入门的同学来说,STM32这几个字母可能已经听出茧子了,但真正拿到板子之后,很多人反而会愣住:芯片型号怎么读、用什么工具链、第一段代码怎么烧进去,每一步都有坑。这篇文章我不打算讲那种大而全的教程,而是以一个刚拿到STM32F103开发板的用户视角,从环境、点灯、定时器、串口、传感器到常见故障,把最关键的操作路径和一些容易踩的坑全部过一遍。不管你是学过51单片机想升级,还是直接零基础开整,只要手里有这块板子,这篇内容就能当一份开箱之后的实操手册。
我拿到的是市面很常见的STM32F103C8T6核心板,也就是所谓的蓝色药丸板。说实话这类板子现在已经很成熟了,几十块钱包邮还带USB转串口,非常适合第一个STM32项目。全文我会尽量说人话,中间穿插一些我实际调板子的经验和教训,希望能帮你少走弯路。
1. 先把手里的开发板认透
1.1 读懂芯片型号与板卡构成
很多初学者拿到板子第一反应就是找引脚图,其实更应该先搞懂芯片型号本身。就拿F103C8T6来说,这串字符信息量非常大:STM32表示是意法半导体的32位ARM Cortex-M3产品线,F代表通用型,103代表基础型系列,C代表引脚数量为48 pin,8代表Flash容量是64KB,T代表LQFP封装形式,6代表工作温度范围是-40℃到85℃。如果你是ZET6系列,Z代表144 pin,E代表512KB Flash,那个板子一般就是霸道战舰那种大板,资源更夸张,但学习方法完全一样。
开发板上除了芯片本身,通常还会有:一个USB转串口芯片(比如CH340或者CP2102),一两个用户按键,一颗电源指示灯,一两颗可编程LED灯,再加上晶振和复位电路。我建议拿到板子先别急着接电,把板子布局看明白,确认USB口是左上还是右下,下载器座子是2x3的SWD还是4针的JTAG,这样后面接线才不会乱。
供电方面要特别提醒:STM32F103的工作电压是2.0V到3.6V,大多数板子板载了3.3V LDO稳压器,所以通过USB供电没问题。但如果你想用外部电源同时驱动大电流外设,那就要注意共地,否则信号线参考电位不一致,通信就会变得非常诡异。判断开发板是否正常上电,最简单的办法就是看那颗VCC电源指示LED,一般接到电源轨上,亮起来代表板载稳压器在工作。
1.2 芯片第一脚怎么确认
这个知识点看着白痴,但实际操作中很多人翻过车。STM32芯片本体上会有一个圆点或者一个倒角切边,那个位置就是第一脚所在方向。以LQFP48封装为例,从圆点那一侧开始得按逆时针方向数引脚,第一脚在左上角,然后向下是2、3、4直到24脚,再从右下拐上来,这样一圈走完刚好48脚。市面上有些盗版或者打磨过的芯片丝印不太清楚,这时可以拿万用表量一下VDD和VSS的关系来辅助确认:通常VSS脚(GND)会和板子上的GND铜箔直接相连,把所有GND针脚的位置先找出来,再按对称性反推,也能锁定引脚位置。
另外,很多蓝色药丸板上的芯片丝印并不是正对着你,而是成品封装的时候转了90度,所以PCB上的丝印指示往往比芯片本身的印字更可靠。你画的第一个外设接线图,最好以板子引脚排针丝印为基准,不要想当然按芯片封装图去套。
引脚排布大概要记住哪几个关键位置:PA0PA15和PB0PB15是两组通用GPIO,其中大部分有复用功能;PB2是BOOT1引脚,上电期间尽量不要拿来当普通IO用;PA13和PA14默认是SWDIO和SWCLK,PA9和PA10默认是USART1_TX和USART1_RX。这些默认功能在数据手册里都能查到,但刚上手阶段只需要记常用复用,后面用得多自然就熟了。
2. 开发环境:三条路线怎么选
2.1 Keil MDK 入坑最稳
没有特殊原因,建议刚学还是用Keil MDK。原因不是它功能最强,而是网上教程最多,遇到问题搜索时命中率高,而且STM32的老用户、老代码、老工程基本都围绕Keil建立了一套完整生态。MDK-ARM和C51是分开的,如果你以前装过Keil C51,那么装MDK时要注意它们是可以共存的,但需要不同的许可证。装好MDK-ARM之后,还需要安装对应芯片的器件支持包(Device Family Pack),比如Keil_STM32F1xx_DFP,这个如果不装,新建工程时器件列表里根本没有STM32F103C8。
工程创建的时候有一个容易被新手忽略的启动文件选择问题。F103系列的启动文件分为hd、md、ld几种,分别对应大容量、中容量、小容量芯片。C8T6有64KB Flash,属于中容量medium density,用 startup_stm32f10x_md.s;ZET6属于高密度,用 hd。选错了启动文件,轻则中断向量错乱,重则程序直接跑飞。这也是为什么我建议你新建工程时用一套整理好的标准库工程模板,而不是每次从头手动添加文件,别把时间浪费在无意义的重复劳动上。
KL指纹说一下,桌面上的Keil图标可以识别,打开Pack Installer里有CM3和STM32F1的全套支持就算装对了。如果发现打开设备列表里连ST都找不到,说明MDK安装过程有问题,重新以管理员身份运行一次安装程序一般能解决。
2.2 CubeMX + HAL 提升效率
如果你以前用过51或者自己会些寄存器操作,那在标准库或者寄存器层面挣扎一段时间之后,可以过渡到STM32CubeMX+HAL库的组合。CubeMX是一个图形化配置工具,能直接生成初始化代码,时钟树、GPIO、串口、定时器都能鼠标点点就配好,甚至能自动帮你生成Keil工程。很多刚转STM32的人担心用了HAL库以后是不是不懂底层了,其实这是误解。在F103这个平台上,HAL库中间层的封装并没有复杂到看不懂,而且配置代码是全自动生成的,你更专注于业务逻辑。对于做毕业设计、比赛、工装这类场景,这个效率优势非常明显。
但我给你一个实际建议:刚开始学点灯和串口那几天,别急着用CubeMX生成完整项目,先手动建几个标准库工程,搞清楚启动文件、时钟树、GPIO初始化这几步操作到底在干什么。有了这个底子,再用CubeMX时你看着生成的代码不会两眼一黑。反过来,如果纯手动搞了一周还在点灯,那就赶紧切到CubeMX,别和自己过不去。
2.3 VSCode 与烧录差异
这两年用VSCode配ARM工具链做STM32开发的越来越多了,配合EIDE插件或者PlatformIO插件,编译体验其实比Keil更现代,代码补全和git集成也好很多。不过新手阶段用VSCode踩的坑主要集中在两个地方:一是arm-none-eabi-gcc工具链的下载和安装,二是debug配置里的launch.json。很多人Keil里编译成功很得意,换个VSCode就发现同样代码烧不进开发板,原因往往是Debugger类型没选对,或者烧录器型号配置错了。
我个人的态度是,VSCode可以作为备选,但入门阶段还是以Keil为主力,原因无他:当你在一个环境卡住了,换工具链的成本比解决问题本身还高。等问题积累多了,再往VSCode迁移,那时候你对编译参数、链接脚本、烧录器的理解都够了,迁移只是半天时间的事。
3. 第一个工程:从点亮LED到理解时钟
3.1 标准库工程骨架与点灯代码
这里我给出一套推荐的标准库工程目录结构:
Project/ ├── Core/ │ ├── inc/ // 头文件 │ └── src/ // 应用代码 ├── Libraries/ │ ├── CMSIS/ // 内核寄存器定义 │ └── StdPeriph/ // 标准外设库 ├── Startup/ // 启动文件 └── Objects/ // 编译输出,也就是.axf/hex放这里点灯的GPIO操作其实主要就是三步。第一步开启GPIO时钟,GPIO是挂载在APB2总线上的外设,C8T6里PA口对应RCC_APB2Periph_GPIOA,需要用RCC_APB2PeriphClockCmd(那个参数, ENABLE)打开。注意STM32里绝大部分外设在使用前都要先开时钟,忘了这一步是最常见的低级错误。第二步配置引脚模式和速率,用GPIO_InitTypeDef结构体指定Pin、Speed、Mode这三个字段,比如推挽输出模式GPIO_Mode_Out_PP,输出速度选GPIO_Speed_50MHz。第三步就是拉高拉低,往ODR寄存器写数据或者用GPIO_WriteBit。
下面是一个最传统的PB1引脚控制LED灯闪烁的示意代码:
#include "stm32f10x.h" void Delay_Init(void); void Delay_ms(uint32_t ms); int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); while (1) { GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); Delay_ms(500); GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); Delay_ms(500); } }注意上面这个Delay_ms我用的是自定义延时函数,没有用官方库的SysTick版本。初学为了简单可以直接用空循环计数,但后面正经工程建议用SysTick做时基,不然定时精度很差。每个开发板的外置晶振和芯片内部时钟配置会不一样,点灯这个阶段不需要折腾时钟树,默认用内部8MHz或者板载晶振都行。
3.2 点灯背后的时钟与引脚复用
我以前带人玩F103时发现一个现象:代码明明看起来没问题,但LED就是微亮或者干脆不亮。排查到最后,大多数是GPIO输出速度没配置,或者引脚复用没关导致其他功能抢占了控制权。F103的GPIO引脚引脚默认都是浮空输入状态,上电后几乎所有引脚都是高阻,所以如果你要用某个引脚做输出,GPIO_Init必须把方向模式改到位。还有一个坑是,PA13、PA14、PA15以及PB3、PB4这几个引脚默认是JTAG相关复用,标准库里如果不关闭JTAG功能,你把它们配置成普通IO输出时会出现怎么也驱动不起来的现象,实际是因为调试口占用了这些复用功能。要用这几根引脚做普通IO,需要在初始化时调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE),但这么做了以后JTAG下载功能就废了,只能靠SWD下载,所以很多人干脆把这两个下载方式都用起来,平时只用SWD。
关于时钟,F103有一个内部RC振荡器HSI和一个外部晶振HSE。绝大多数开发板外接了8MHz晶振,上电后默认是HSI作为系统时钟,你如果想让系统跑在72MHz,必须在SystemInit函数里切到HSE并且设置PLL倍频。STM32标准库自带的SystemInit已经默认把时钟配置到72MHz,前提是你系统初始化时能正常启动HSE,HSE起振失败的话代码会卡在等待超时的循环里。这也是为什么你发现程序在SystemInit里面死掉时,先量一下外部晶振两个引脚有没有波形,如果没有,检查晶振是否虚焊或者是劣质晶振。
4. 拿得出手的实战目标:定时器、串口与传感器
4.1 定时器PWM输出和频率捕获
点灯只是起步,真正让F103从玩具变工具的是它的定时器资源。F103里有TIM1和TIM8两个高级定时器,TIM2到TIM5四个通用定时器,TIM6和TIM7两个基本定时器,还有TIM10到TIM14一批加塞的定时器。每种定时器能做的事不一样:基本定时器只能做时基和触发DAC;通用定时器可以输出PWM、输入捕获、编码器接口;高级定时器还多了互补输出和刹车功能,适合做电机控制。
PWM输出本质是利用定时器的比较寄存器生成一个可调占空比的方波。以TIM2的通道1为例,你需要配置预分频器PSC和自动重装载值ARR,这两个数值共同决定PWM的周期,然后设置CCR1的值来控制占空比。假设系统时钟72MHz,PSC=719,ARR=999,那么计数器时钟是100kHz,一个PWM周期是0.01秒也就是100Hz,如果你想输出50%占空比,就设CCR1为500。PWM调光、呼吸灯、舵机角度控制都是这个套路,学通了之后非常万能。
定时器输入捕获测频率是另一个高频需求。它的原理是引脚上的边沿信号会触发捕获寄存器锁存当前计数器的值,通过两次相邻捕获值的差值再结合预分频系数就能算出信号频率。如果你要测一个低频方波,可以把预分频配大一点;如果要测高频,则要留意输入捕获的溢出处理,否则计数器回绕会造成测量值完全错误。实测下来用PA0复用映射到TIM2_CH1来测PWM信号非常顺手,但注意PA0上经常同时接了板上按键的输入,按键的上下拉会影响信号,测频率前最好把按键功能从板子上摘掉。
4.2 串口printf调试大法
串口是嵌入式调试很重要的手段,F103的USART1挂在APB2上,USART2和USART3挂在APB1上。最简单的串口输出流程是:配置GPIO复用推挽输出,配置USART参数,使能USART。这里重点说重定向printf的那段标准操作,在Keil里用microlib模式后,写一个fputc函数把单个字符送到串口发送寄存器即可,比如:
int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }这个函数的难点在于理解重定向原理。C标准库printf函数最终会调用底层字符输出函数,在ARM平台上这个底层默认是往调试器的ITM通道写,如果你没有配置调试端口,printf的输出就丢到黑洞去了。重定向成串口之后,printf就能把文本打到串口助手里,这对调试状态机、传感器数值、协议解析都特别好用。
接收数据是另一个话题。如果你要用串口收不定长的数据帧,建议直接开接收中断,在中断里把字节放进缓冲区,再配合定时器超时判断一帧数据是否完整。刚入门时很容易把主循环里的阻塞延时和串口中断混在一起,结果导致一个字节没读完整个系统就卡死,这类问题归根结底是对中断优先级和临界区理解不够。如果发现串口数据偶发乱码,先查波特率误差和外部晶振精度,很多串口通信问题最稳定的排查路径就是拿示波器量TX脚的波形周期。
4.3 传感器通信:I2C、单总线与超声波测距
STM32接传感器几乎是每个项目都躲不开的环节。拿超声波测距来说,最常见的HC-SR04模块逻辑很简单:给TRIG引脚一个至少10us的高电平,模块会自动发出8个40kHz脉冲,然后ECHO引脚会输出一个高电平,高电平的时间就是声音往返的时间,距离等于时间乘声速除以2。HC-SR04虽然不用串口也不需要I2C,但它的时序非常依赖微秒级延时,F103里用DWT内核定时器或者其他硬件定时器来做us延时最稳,用纯软件循环的话注意优化级别不同可能导致延时偏差很大。
BH1750这类数字光强度传感器走的就是标准I2C协议。F103硬件I2C外设在这颗芯片上口碑一般,很多人用着用着就会遇到总线卡死,根本原因是I2C的忙标志和超时处理不完善。我个人的经验是,对于F103平台,驱动I2C传感器优先考虑GPIO模拟I2C,虽然代码多几行,但是可控性和排查难度都明显改善。硬件I2C如果要稳定驱动,必须写好错误处理,尤其是总线忙时要能把SCL拉出发9个时钟脉冲来恢复总线,这个细节很多教程压根不提。
另外,BH1750这种传感器和OLED屏可以挂在同一条I2C总线上,只要地址不同就能共存,非常省引脚。如果遇到OLED显示字模乱码或者花屏,先排查通讯速率是不是降到100kHz以下,再检查电源纹波,最后再看初始化顺序。我见过不少OLED不亮的案例,最终原因是3.3V和5V供电混接导致逻辑电平不对,I2C上拉电阻接到5V上把GPIO引脚灌坏了一半。
5. 常见问题与排查技巧实录
5.1 烧录失败问题清单
Keil编译成功后烧录不进去,这个问题绝对能排进STM32新手问题前五。最常见的现象是点击下载后弹出Error: Flash Download failed - Cortex-M3,原因可能有很多,我列一个实际排查顺序供你参考:
| 排查点 | 操作方法 | 对应解决思路 |
|---|---|---|
| 烧录器连接 | 确认SWDIO/SWCLK/GND三条线都接了,且接触可靠 | 杜邦线松动是最大的隐性杀手 |
| 目标板供电 | 确认开发板有独立供电或者在调试器供电模式 | 目标板没上电时SWD无法初始化 |
| BOOT0引脚 | 把BOOT0接地,BOOT1任意 | BOOT0拉高会进入ISP模式,无法用调试器擦写 |
| 代码禁用调试口 | 检查代码里是否把PA13/PA14设为普通IO | 禁用SWD之后下载器找不到内核 |
| 烧录器型号 | 确认Keil里Debug选项选的是ST-Link还是JLink | ON-chip和外部仿真器选错会直接报错 |
| 芯片写保护 | 高级选项里关闭Flash的Readout Protection | 读保护开启后无法正常擦写 |
还有一个很隐蔽的坑:某些JLink或者ST-Link克隆版驱动不稳定,特别是USB线质量特别差的时候。你可以把USB线从机箱前置口换到主板原生口试试,淘汰掉劣质线和USB HUB之后,成功率能提升不少。
如果你的工程模板是自己建的,还有一个常见原因是Flash地址范围配置错了。F103C8T6是64KB Flash,起始地址0x08000000,大小0x00010000,如果你配置成128KB,下载算法还是会报错。这个地址配置在Keil的Options for Target -> Target -> IROM1里,修改后点下载前先看Memory显示的地址范围是否合理。
5.2 运行时“死刑”现象盘点
程序烧进去了却运行不对,这类问题更隐蔽。我遇到几个高频症状:
一个是Delay函数卡死。最常见的原因是中断环境里用了基于SysTick的延时,但SysTick中断优先级和别的中断冲突,导致延时函数一直等待计数标志位完成但永远等不到。另一个可能是你在初始化里把SysTick关了却没有重开。遇到卡死,第一件事就是暂停调试模式,看PC指针停在哪个函数,如果停在Delay的某个循环里,基本可以断定是中断向量或者SysTick配置问题。
另一个高频问题是CAN通信突然连不上。F103的CAN控制器挂在APB1上,硬件上还必须有外部CAN收发器(比如TJA1050),并且要在总线两端加120欧姆终端电阻。很多人板子单独测试收发器都没问题,一旦接到现场总线上就连不上,原因多半是没有共地、没有终端电阻或者波特率不匹配。CAN波特率不是随便配的,它由预分频和同步跳转宽度决定,不同厂家对位时间段的定义可能不同,一定要统一配置策略。还有,F103的CAN发送邮箱写满之后会有报错,但如果你在中断里处理接收又同时在主循环里发送,很容易出现发送超时,解决办法是每帧发送后设置一个超时看门狗,不要死等。
JTAG被禁用是第三个经典问题。很多人在调下载时发现板子突然不能烧了,排查后发现代码里执行了类似GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)的语句,它把整个SWJ调试端口关闭了,这样下载器自然失联。修复方法是进入ISP模式把Flash擦空,或者从BOOT0拉高进入系统存储器引导模式。所以我前面说,调试期间尽量只用SWD模式,不要全部禁用SWJ。实际上PA15/PB3/PB4如果非用不可,也只禁用JTAG而保留SWD,这样至少还能继续下载。
6. 接下来怎么学:一条可执行的路线建议
6.1 标准库到HAL库的过渡节奏
我经常被问到一个问题:现在学STM32到底学标准库还是HAL库?我的回答是,对于F103平台,两个都要接触但先后顺序要明确。前面学点灯、按键、串口、定时器,建议用标准库,因为标准库的代码更贴近寄存器,你能看到每个外设的配置过程,对底层结构印象深。等你把GPIO、时钟、中断、外设数据手册这四个维度理解得差不多了,再切到CubeMX+HAL库。HAL库的学习重点不是每个函数参数,而是学会读生成的初始化代码:PA9_USART1_TX是哪个宏决定的,TIM2的时钟为什么是72MHz分频下来的,这些理解了,HAL库在你手上就只是加速工具而不是黑盒。
有一些人一开始就从B站视频抄HAL配置,点灯倒是快,但遇到一个奇怪问题仍然无从下手,只能整个工程推翻重来。这就是缺少底层感觉的表现。好在F103的寄存器手册和库代码写得比较直白,花一个星期过渡完全来得及。
6.2 小项目驱动学习
嵌入式学习最大的敌人是“看起来都会了”。给你提一套我亲自试过比较有效的项目推进顺序:第一个项目是用GPIO做按键控制双色灯。这个项目会让你学会输入模式、上下拉、抖动消除。第二个项目是定时器PWM输出,用舵机或者LED亮度调节验证效果。第三个项目是串口通信,做一套PC端上位机交互,按键发指令控制LED状态。第四个项目是I2C读取温湿度传感器并在OLED上显示,把这个调通,你对通信协议的认知会直接上一个台阶。每一个项目都要从头到尾自己搭工程,中途可以查参考代码,但不要整篇复制。
到了这个阶段,你自然会遇到中断、DMA、环形缓冲区这些概念。这很正常,遇到一个学一个,不要一次性把所有外设细节背完。F103的资源在当今很多消费级芯片面前不算亮眼,但它胜在资料多、案例全、出问题好搜。凭借这块103开发板入门之后,再转STM32F4、H7甚至国产的CH32、GD32,迁移成本都不是问题。
在我实际接触的这些新手案例里,学得最快的往往不是基础最好的人,而是最敢动手试错的人。程序烧不进去就一遍遍查接线和配置,串口乱码就换波特率测波形,代码卡死就挂调试器看指针位置。这套调试思路一旦建立起来,比背下整个HAL库都有用。
最后再分享一个小技巧:这块F103开发板上如果留有LED和按键,尽量从一开始就把它们定义成固定引脚并且写进一个bsp_driver文件里。后面所有项目都复用这套驱动,时间长了你就拥有了一套自己的GPIOLED、按键、串口映射库。不同板子换着用的时候,只要改引脚宏和时钟使能宏,其他逻辑完全不用动,这体验比每次从零复制粘贴舒服太多了。