开箱先别急着通电。上周我的STM32F103开发板终于到货了,拆开包装第一件事不是插USB线,而是把板子翻过来看丝印。很多新手拿到开发板恨不得马上点灯,但埋的坑往往就在这一步——板载芯片型号、晶振频率、LED引脚丝印,这些决定了你后面所有代码能不能跑起来。这篇就把我从零开始玩F103的过程、踩过的坑、以及一路摸索出来的学习路线完整写下来,给刚入手开发板的你一个参考。
1. 开发板到手第一步:认清你的板子和芯片
1.1 先分清F103家族的具体型号
STM32F103是意法半导体STM32家族里最经典的型号,也是国内教学和DIY用得最多的。它的细分型号非常多,但核心差别就几个:Flash容量、SRAM容量、引脚数量、外设数量。常见的F103型号有这么几类:
| 型号 | Flash | SRAM | 引脚数 | 封装 | 常见场景 |
|---|---|---|---|---|---|
| STM32F103C8T6 | 64KB | 20KB | 48 | LQFP48 | 最小系统板、小项目 |
| STM32F103CBT6 | 128KB | 20KB | 48 | LQFP48 | 功能稍多的小项目 |
| STM32F103RCT6 | 256KB | 48KB | 64 | LQFP64 | 中大型项目、学习板常见 |
| STM32F103ZET6 | 512KB | 64KB | 144 | LQFP144 | 大容量、全功能,很多开发板标配 |
我手上这块板子焊的是STM32F103C8T6,"C"代表48引脚,"8"代表64KB Flash,"T"代表LQFP封装,"6"代表工业级温度范围。别小看这个丝印,很多同学下载程序时选错芯片型号,导致烧录失败或者程序跑飞,根源就是没先确认芯片具体型号。如果你是买的正点原子、野火、普中这些厂家的开发板,包装盒或者商家页面都会写清楚型号,实在不确定就拆开看芯片表面,字很小但用手机微距拍一下都能看清。
1.2 检查板载资源有哪些
开发板不是只有芯片,还有一堆外围电路和器件。新手最容易犯的错是拿到板子就开始写代码,结果代码里用的引脚和板子上实际连接的器件对不上。我拿到板子以后专门花了一个小时做了个"资源盘点":
- 板载LED:常见是一个电源指示灯(PWR)+ 一个用户LED(通常接PB1或PC13),有的板子还有RGB灯。确认LED接在哪个引脚,这决定了你的点灯程序怎么写。
- 板载按键:一般有复位键(RESET)和一个用户按键(KEY)。按键接的引脚也要确认,通常是PA0或PC13。
- USB转串口芯片:CH340或者CP2102居多,这个芯片的作用是让电脑通过USB口和开发板的串口通信。对应驱动要装好,不然串口打不开。
- 晶振:F103主板通常配8MHz晶振,内部还有16MHz的HSI(内部高速RC振荡器)可以使用。注意,C8T6最小系统板有些设计是板载晶振,有些是直接用内部HSI跑,这会影响你后面配置时钟系统。
- 排针引出的引脚:看原理图或者板子丝印,确认哪些引脚被引出,哪些被板载外设占用。
- ST-LINK接口:现在很多开发板板载了ST-LINK/V2调试器,直接用一根USB线就能下载程序和调试。我的板子板载了ST-LINK,省了买独立调试器的钱。
自己动手对着原理图把上述资源标一遍,效果远大于看十篇教程。找不到原理图就先看商家的资料包,几乎每个开发板卖家都会提供完整的原理图PDF,这是你后面排查硬件问题最重要的参考。
1.3 确认供电方式和调试器
F103开发板的供电方式主要有两种:USB供电(5V经过板载AMS1117稳压到3.3V)和外部电源供电(DC座或排针接5V/3.3V)。新手入门阶段用USB线供电就完全够用,但要注意电流问题——如果你后面外接了舵机、电机、OLED屏这类耗电较大的模块,不能直接从开发板的3.3V引脚大功率取电,会烧稳压芯片或导致电压跌落。这种情况要给模块单独供电,并且共地(GND相连)。
调试器这块是非常容易让人卡住的地方。F103支持JTAG和SWD两种调试接口,目前主流用的是SWD,只需要两根线(SWDIO、SWCLK)加上GND和3.3V,就能完成下载程序和在线调试。板载ST-LINK的板子,电脑端需要安装ST-LINK驱动,装好之后在设备管理器里能看到一个"ST-Link Debug"设备。如果你用的是独立的ST-LINK、J-LINK或者DAP-Link,接线方式各有区别,但SWD的接法基本都是:SWDIO接SWDIO,SWCLK接SWCLK,GND接GND,VCC接3.3V(部分调试器可不接VCC,只做电平参考)。
2. 开发环境搭建:Keil5、芯片包和第一个工程模板
2.1 为什么新手首选Keil MDK
STM32的开发环境选项其实很多,有人用IAR、有人用VS Code + GCC工具链、有人用STM32CubeIDE。我个人的建议是,新手入门阶段首选Keil MDK(也就是Keil uVision5),原因有三:
- 教程资料最多。国内市面上的STM32教学视频、书籍、博客,百分之八十用的是Keil,你遇到任何报错都能搜到现成的解决方案。这一条对新手来说几乎是决定性的。
- 调试功能直观。Keil的Debug模式下可以直接看寄存器值、变量值、外设状态,还能打断点单步执行。对理解芯片内部工作原理帮助非常大。
- 安装和配置相对简单。相比之下VS Code那套从零配置编译工具链的流程,对新手来说光是编译原理就够喝一壶的,而Keil基本是装好就能用。
这里顺便回应一下很多同学问的"VS Code里编译成功,却怎么也烧录不进开发板"的问题。实际上VS Code本身是个编辑器,编译工作是由arm-none-eabi-gcc等工具链完成的,烧录则要额外配置pyOCD、OpenOCD或ST-Link工具。三步分开管,任何一步配置不对都会导致"编译成功但下载失败"。Keil则把编译和下载集成在一起,选好调试器、配好Flash算法,一键下载就行,对新人友好得多。VS Code那套可以等你把F103玩熟了再折腾,不迟。
2.2 安装Keil5、芯片包和ST-LINK驱动的完整流程
Keil5的安装分成三个部分,缺一不可,而且每部分都有坑。
第一部分是Keil MDK主程序的安装。去Keil官网下载MDK-ARM,安装路径建议保持默认(C盘),虽然占用几个G空间,但后面很多教程和第三方工具默认查找这个路径,改到别的盘容易出现路径不匹配的问题。安装时会有一步让你选择"Pack installer"的代理设置,直接跳过即可,国内访问Keil的包服务器很慢,我们后面用离线包安装。
第二部分是STM32F1系列的芯片支持包(Device Family Pack)。打开Keil后会自动弹出版本管理器,在里面找到STM32F1系列下载安装,但国内网络经常下载失败或者卡在某个进度。我建议直接去Keil官网下载离线pack文件(Keil.STM32F1xx_DFP.xxx.pack),下载好之后双击或者用Pack Installer的"Import"功能导入。这个pack文件一定要对应你实际用的芯片型号,如果你用的是F103C8T6,需要的就是STM32F1系列的支持包,如果你的板子是GD32或者极海APM32这类国产替代芯片,还需要额外安装对应的pack,不能通用。
第三部分是调试器驱动的安装。如果你的板子板载ST-LINK,去ST官网下载ST-LINK驱动和ST-LINK Utility工具,驱动装好后设备管理器里应该能看到"ST-Link Debug"设备。如果你用的是J-LINK,安装J-LINK驱动软件到SEGGER官网下载。这里要特别提醒:Win10/Win11系统有时候会在插入USB时自动装上一个不完整的驱动,导致Keil识别不到调试器,遇到这种情况就去设备管理器里把设备卸载,勾选"删除驱动程序软件",然后重新插拔USB,手动安装官网驱动。
2.3 标准库和HAL库怎么选
F103的开发方式有两大流派:标准外设库(Standard Peripheral Library)和HAL库(Hardware Abstraction Layer)。另外还有直接操作寄存器的方式,但那种通常不作为正常项目的起点。
标准库是ST官方早期推出的,把寄存器操作封装成一个个函数,比如GPIO_Init()、USART_SendData(),理念接近"面向过程",代码简洁直白,函数名一眼能看出是做什么的。国内大部分经典教程(尤其是讲F103的)都是基于标准库,所以学标准库最大的优势是资料丰厚、案例多。HAL库是ST后来的主打,用了类似面向对象的分层思想,配合CubeMX图形化配置工具能快速生成初始化代码。HAL库代码冗余多一些,运行效率略低,但胜在移植性好、配置直观。
我的建议很明确:新手学F103优先选标准库。原因很朴素——你把GPIO、定时器、串口的寄存器配置逻辑搞明白了,再去看HAL库的底层实现,一目了然;反过来从HAL库学起,往往会在封装层里绕晕,出了问题都不知道底层发生什么。标准库的经典工程模板到处都能找到,正点原子和野火都有整理好的库函数版本工程。
2.4 从模板到你的第一个工程
这里我列一下用户最常遇到的问题——新建工程时选芯片型号、配置Flash算法、添加启动文件和标准库文件。一个最简化可用的标准库工程至少包含:
- 芯片型号选择:在Keil的Device栏里选你具体的STM32F103型号,以C8T6为例就是STM32F103C8。
- 启动文件:startup_stm32f10x_md.s(中等容量)或者_hd.s(高容量)。这个汇编文件负责初始化堆栈、设置中断向量表、调用SystemInit和main。选错启动文件,程序可能无法运行或者编译报错。
- 标准库的核心文件:stm32f10x.h(寄存器定义)、system_stm32f10x.c(系统时钟配置)、stm32f10x_rcc.c和stm32f10x_gpio.c等外设驱动文件。
- 一个stm32f10x_conf.h配置文件,里面用#include把要用的外设头文件包含进来。
- 魔法棒(Options for Target)里的设置:Output页勾选Create HEX File、Debug页选择ST-Link调试器、Utilities页勾选Flash Download并正确配置Programming Algorithm算法为STM32F10x Med-density Flash等。
模板这个东西,新手建议直接从已有模板修改,而不是从零手搓。手搓你是能学到启动流程,但代价是容易被各种链接错误、编译告警劝退。等你能顺畅地改别人的模板了,再回头研究启动文件和分散加载文件(sct文件)里的细节也不迟。
3. 第一个程序:点灯背后的GPIO世界
3.1 GPIO的八种工作模式
STM32的每个引脚都可以配置成多种模式,很多新手点灯直接照抄代码,把GPIO_Mode_Out_PP当成固定写法,完全不知道是什么意思。了解GPIO模式非常关键,因为后面你接按键、接传感器、驱动屏幕都要跟它打交道。
F103的GPIO有8种模式,官方手册有详细表格,我按实际使用频率重新梳理一下:
- 推挽输出(GPIO_Mode_Out_PP):最常用的输出模式。引脚可以直接输出高电平(近似3.3V)或低电平(近似0V),驱动能力较强。点LED、驱动数字信号都用它。
- 开漏输出(GPIO_Mode_Out_OD):输出低电平时引脚是低电平,输出高电平时引脚变成高阻态,必须外接上拉电阻才能拉到高电平。这个模式常用在需要"线与"逻辑的场合,或者驱动电平与芯片电压不一致的器件。
- 复用推挽/复用开漏(AF_PP/AF_OD):当引脚由片上外设(比如USART、SPI、定时器的PWM输出)控制时使用。典型例子是USART发送引脚要用复用推挽,I2C的SCL/SDA要用复用开漏。
- 浮空输入(GPIO_Mode_IN_FLOATING):引脚不拉高也不拉低,外部输入是啥电平就是啥电平。注意,悬空状态下引脚电平不定,容易受干扰,实际很少用。
- 上拉输入(GPIO_Mode_IPU):引脚内部接一个上拉电阻,外部不接时默认高电平,按下按键接地则读到低电平。按键检测、读取开关状态最常用。
- 下拉输入(GPIO_Mode_IPD):引脚内部接下拉电阻,默认低电平,外部接高则读到高电平。
- 模拟输入(GPIO_Mode_AIN):引脚信号直接进ADC或DAC模块,不用数字逻辑,采集传感器模拟电压时用。
点灯用的就是推挽输出。LED灯接在引脚和GND之间,引脚输出高电平时灯亮。也有些开发板LED接在3.3V和引脚之间,引脚输出低电平时灯亮,所谓"低电平点亮",看板子的原理图就能确认。
3.2 用寄存器写一个最朴素的点灯程序
很多人学STM32是被HAL库和标准库的层层封装搞晕了,所以我先给你看一段最底层的寄存器写法。假设板载LED接在PC13(这块板子PC13接了一个LED,低电平点亮),点亮它的代码最少只需要三行:
RCC->APB2ENR |= (1 << 4); // 使能GPIOC的时钟 GPIOC->CRH &= ~(0xF << 20); // 清掉PC13的配置位 GPIOC->CRH |= (0x2 << 20); // 配置PC13为推挽输出,50MHz GPIOC->ODR &= ~(1 << 13); // PC13输出低电平,点亮LED拆开来讲:
- RCC是你开启外设时钟的总闸,F103的所有外设默认都不供时钟,必须先开时钟才能用。APB2ENR寄存器第4位控制GPIOC的时钟。为什么PC13是配置CRH而不是CRL?因为CRL管脚配置的是GPIO0~7,CRH配置的是GPIO8~15。PC13是13号引脚,所以用CRH。
- 每4位配置一个引脚模式。0x2表示"输出模式,50MHz"。
- ODR寄存器直接控制引脚电平,第13位写0就是输出低电平。
这段代码即便你不理解每一步,也能从中建立"寄存器操作的本质就是改内存地址上某些位"这个认知。有了这个认知打底,后面的库函数基本就是套壳,你看一眼就能猜到里面做了什么。
3.3 工程化的点灯:标准库的GPIO配置
实际项目中不会真用裸寄存器写,标准库封装得更清晰。同样点灯,标准库写法是这样的:
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 输出低电平,对应2.2寄存器示例注意这个顺序:先开时钟,再配置引脚,最后操作输出。新手抄代码最容易漏的是开头那句时钟使能,漏了的话GPIO配置无效,灯怎么都不亮——因为寄存器的读写根本没生效。
主函数里再用一个延时函数控制闪烁。延时这块是新手第一个"玄学问题"来源,我展开说一下。你搜到的例程里大概有三种延时:
- 循环延时:自己写一个for循环空转几千次。简单但精度极差,而且编译器开优化后可能直接被优化掉,导致延时失效。
- 用SysTick定时器延时:Cortex-M3内核自带的24位递减计数器,计数到0可以产生中断。标准库例程里常见的Delay函数就基于SysTick,精度到微秒级没问题。
- 用定时器延时:用一个TIM产生定时中断,精度更高还能做PWM之类的复杂功能。
很多同学遇到"delay卡死"的问题,十有八九是SysTick配置错误或者中断优先级配置冲突。网上很多例程的延时函数写法比较粗糙,直接关中断开中断,一旦你在中断服务程序里调用延时函数,整个系统就卡死了。关于这点,第5章我会专门展开讲。
4. 烧录失败排查:编译成功不等于能跑
4.1 同样是烧录失败,原因天差地别
最近逛论坛看到有人发帖说"VS Code里编译成功,却怎么也烧录不进开发板",评论区一堆人跟着说同病相怜。这种问题我当年也遇到过,当时折腾了整整一个晚上,最后发现是ST-LINK的SWD引脚被禁用了。所以这里我直接给你整理一张烧录失败排查表,按顺序逐条排查:
| 序号 | 排查项 | 具体方法 | 解决方案 |
|---|---|---|---|
| 1 | USB识别问题 | 设备管理器查看是否出现ST-Link或COM口 | 重装驱动、换USB线、换USB口、禁用再启用手动安装驱动 |
| 2 | 调试器选择错误 | Keil的Debug页是否选的ST-Link,Settings里能否识别设备 | 在Debug页选择正确的调试器,点Settings确认IDCODE |
| 3 | 芯片型号选错 | Device栏是否和你板子实物一致 | C8T6就选STM32F103C8,别选成CBT6或ZET6 |
| 4 | Flash算法缺失 | Utilities页的Flash Download里有没有配置 | 添加对应容量的Flash算法,常见是STM32F10x Med-density Flash |
| 5 | BOOT0引脚状态错误 | 芯片无法进入下载模式 | BOOT0接0(从Flash启动),多数开发板在正常模式即可下载 |
| 6 | SWD引脚被禁用/复用 | 之前的程序把PA13/PA14配置成了普通GPIO | 按住复位键的同时点击下载,或者在程序里避免配置SWD引脚 |
| 7 | 供电问题 | 开发板电源指示灯是否亮、是否稳定 | 换USB线、拔掉大功率外设、必要时外部供5V |
| 8 | 芯片锁死 | 下载时报RDDI-DAP Error | ST-LINK Utility里执行整片擦除(Full Chip Erase),再重新下载 |
这里重点说几个容易被忽略的坑。第一,编译成功真的不代表什么——Keil里编译出目标文件只是第一步,烧录是另一套流程,调试器、Flash算法、芯片状态任何一个环节出问题都会卡在下载那一步。第二,BOOT0引脚状态很多人会忽略,F103一共有三种启动方式,BOOT0接1、BOOT1接0时是从系统存储器启动,此时哪怕你点了下载,芯片执行的也不是Flash里的程序。第三,最折磨人的是SWD引脚禁用的坑,有人图省事把PA13/PA14用来驱动LED,结果程序一下载进去,调试器就再也连不上了。解决办法是按住复位键,在Keil里点下载按钮的一瞬间松开复位键,趁程序还没运行把Flash擦掉。
4.2 ST-LINK Utility的用途
ST官方提供了ST-LINK Utility工具,这个工具强大但容易被新手忽略。它的核心功能是直接对芯片的Flash进行擦除、写入、校验,还能读取芯片信息。当你的Keil都连不上芯片、报错一片红的时候,ST-LINK Utility往往能杀出一条血路。
具体用法:打开工具连接芯片,如果能识别到芯片信息(比如Device ID 0x410),说明硬件通路OK。这时候先用Full Chip Erase把整个Flash擦空,再回到Keil里配置好调试器,重新下载程序。很多"板子变砖"的错觉,其实就是因为Flash里留了个占用SWD引脚的程序,擦掉就复活了。
这个工具还支持把HEX文件直接烧进Flash,有时候Keil抽风了,你也可以把编译好的HEX文件拖进ST-LINK Utility写入芯片,完全绕开编译器和调试器的耦合问题。
4.3 VSCode烧录失败的几个原因
既然热搜里都提到了"VS Code里编译成功,却怎么也烧录不进开发板",我顺便把VS Code烧录的坑也整理了。VS Code下烧录F103通常通过OpenOCD或者pyOCD配合ST-LINK进行,失败原因主要集中在:
- 配置文件路径错误:openocd需要指定interface和target两个配置文件,路径配错就直接启动失败。
- ST-LINK固件版本过旧:新版的OpenOCD对ST-LINK固件有要求,旧固件会报STLINK_V3错误,去ST官网升级ST-LINK固件即可。
- 权限问题:某些工具需要管理员权限才能访问USB驱动层。
- OpenOCD的target配置文件选错:F103系列不同容量对应的target文件可能不同,选错会报target not found。
相比之下Keil把配置都封装了,所以你如果是用VS Code入门的新手,遇到烧录问题别急着怀疑硬件,先回头检查OpenOCD的配置文件和ST-LINK固件版本。如果你还没有搭VS Code环境,就先老老实实用Keil,等熟悉了下载流程再考虑迁移。
5. 继续进阶:定时器、串口和中断是必须啃下的硬骨头
5.1 定时器:从延时到PWM和输入捕获
点灯玩熟以后,下一个绕不开的就是定时器。F103的定时器分三层:高级定时器TIM1/TIM8、通用定时器TIM2/3/4/5、基本定时器TIM6/TIM7。新手最常用的是通用定时器。
定时器最核心的原理就是一个计数器不断递增(或递减),到达设定值就触发中断或者改变输出电平。它的工作频率 = 内部时钟频率 / (预分频系数+1) / (自动重装值+1)。举个例子,F103内部时钟是72MHz,如果你想产生1ms的定时中断,可以这样配:预分频寄存器PSC=71,自动重装寄存器ARR=999。这样定时器频率 = 72MHz / (71+1) = 1MHz,也就是1us计数一次,再数1000次正好1ms。
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / (71+1) = 1MHz TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 999; // 计数1000次,1ms中断 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE);注意TIM2是挂在APB1总线上的,所以开时钟用的是RCC_APB1PeriphClockCmd而不是APB2。这个细节很多人踩坑,开错总线时钟会导致定时器配置无效。
定时器还有两种高频应用。一种是PWM输出,通过改变占空比控制LED亮度或者舵机角度。另一种是输入捕获,可以测量外部信号的周期和频率。网上搜"stm32超声波测距"的例程,本质就是定时器输入捕获测Echo引脚的高电平脉冲宽度,再乘以声速除2得到距离。这些应用都建立在你理解了定时器计数原理的基础上,所以第一步永远是搞明白PSC和ARR。
5.2 串口:它与USART虚拟串口和USB的关系
串口是嵌入式开发里最常用的调试交互手段。F103的多个USART/UART支持同步和异步通信,默认引脚在PA9/PA10(USART1)。配置串口的要点:
- 波特率设置:常见的9600、115200要匹配上位机。
- 数据位、停止位、校验位:默认8-N-1即可。
- 发送引脚配置为复用推挽AF_PP,接收引脚配置为浮空输入或上拉输入。
- 使能串口时钟和GPIO时钟,注意USART1挂在APB2总线,USART2/3挂在APB1总线。
USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE);串口这里有个概念容易混淆:USART和USB的区别。F103的USART是普通的TTL电平串口,要和电脑USB通信必须经过一块USB转TTL芯片(开发板上的CH340就是干这个的)。而F103本身也有一个USB外设,比如C8T6自带USB全速设备功能。很多同学搜"stm32如何做usb设备"或者"stm32 usb虚拟串口发送数据",其实是想让STM32直接通过USB接口和电脑通信,不需要CH340。这个方向可以玩,但USB协议栈比串口复杂不少,虚拟串口(CDC类)需要处理枚举、描述符、端点传输等概念,建议先把USART玩明白,理解"串口收发数据"的基本逻辑,再上USB设备。
5.3 中断系统:为什么delay会在中断里卡死
F103使用的Cortex-M3内核支持嵌套向量中断控制器NVIC,每个外设都能产生中断。中断优先级分抢占优先级和子优先级,由NVIC配置。新手经常问"为什么我的延时函数在中断服务函数里卡死",本质上是对中断优先级和SysTick的理解不到位。
先说SysTick延时卡死的原因。网上常见的SysTick延时实现是:
static u32 fac_us; void SysTick_Init(void) { SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); fac_us = SystemCoreClock / 8000000; } void delay_us(u32 nus) { u32 temp; SysTick->LOAD = nus * fac_us - 1; SysTick->VAL = 0; SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0); SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; }这段代码在main主循环里用没问题,但如果在中断服务函数里调用,就会出大问题:SysTick的优先级被你配置得很高,它能抢占当前正在执行的中断服务函数,而delay_us里的while循环又依赖SysTick的COUNTFLAG置位,如果SysTick中断本身被更高级的中断打断,或者SysTick中断和当前中断互相抢占,就可能死锁。
更稳妥的做法是:在延时函数里只做循环等待,不依赖SysTick中断发生;或者把SysTick优先级设置为最低,确保其他中断能抢到执行。如果要在中断里使用延时而又不想被各种抢断搞得焦头烂额,建议换成"延时不做任何跟中断相关的事"的方式——比如上文提到的纯CPU空转循环,虽然精度差点,但绝不卡死。
另外再提一个操作细节:注意中断服务函数的名字不能随便起。F103的启动文件里已经定义好了中断向量表,中断服务函数名如EXTI0_IRQHandler、TIM2_IRQHandler、USART1_IRQHandler,你在源文件里写同名函数,链接时就会覆盖启动文件里的弱定义。函数名写错一个字母,中断触发后就会跑进默认的无限循环或者直接死机,你查半天也查不出原因。用标准库开中断前,先打开startup_stm32f10x_md.s文件,看看里面中断向量表的名字。
5.4 按键输入与外部中断
按键检测是GPIO输入的最典型应用。F103的按键检测有两种方式:轮询和外部中断。轮询就是主循环里不断读GPIO输入电平,发现有变化就处理。外部中断则是按键按下触发EXTI中断,CPU立刻响应,适合需要及时响应的场景。
按键接法一般是按键一端接GND,一端接GPIO,GPIO配置为上拉输入。按键没按下时引脚读高电平,按下时被拉低,所以检测到下降沿就是按键按下。这里有个新手必踩的坑:按键抖动。机械按键按下瞬间会有几毫秒的抖动,如果不做消抖处理,一次按下可能触发多次逻辑。最简单的消抖就是加10~20ms延时再确认一次电平状态,或者用定时器做扫描消抖。
外部中断EXTI的配置流程:GPIO配置为上拉输入,然后绑定到EXTI线上(PA0对应EXTI0线),配置EXTI触发方式为下降沿触发,最后在NVIC里使能对应的外部中断通道。注意EXTI0_IRQHandler中断服务函数里,处理完之后必须调用EXTI_ClearITPendingBit清除中断挂起位,否则中断会反复触发。
很多"stm32按键模块电路设计"的搜索结果里还提到了矩阵键盘,那本质就是把多个按键排列成行列结构,通过行列扫描减少IO占用。原理不复杂,但会用到更多代码逻辑,可以等把单按键彻底吃透以后再玩。
6. 学习路线规划:点灯之后怎么走下去
6.1 一个适合自学的顺序
很多新手学STM32最大的问题是贪多嚼不烂,今天点灯、明天I2C、后天又去问USB怎么做设备。实际上F103的学习路径是有明确先后关系的,我根据自己的经验整理了一个参考顺序:
- GPIO输出:点灯、蜂鸣器。
- GPIO输入:按键检测、外部中断。
- 定时器:定时中断、PWM、输入捕获。
- 串口:发送调试信息、接收上位机数据。
- I2C:读取传感器(如DS3231时钟模块)或驱动OLED屏。
- SPI:驱动Flash芯片或SD卡。
- ADC:采集电压值。
- 看门狗、DMA、RTC等进阶外设。
- 综合项目:超声波测距、智能小车、环境监测站等。
每学一个新外设,就尝试把它跟前一个外设结合起来做一个完整的小项目。比如点灯学会之后,用按键控制LED;学习定时器之后,用PWM做成呼吸灯;学习串口之后,把温湿度数据通过串口发到电脑。知识只有在组合使用中才会形成真正的理解。
6.2 找对资料和避开弯路
资料这块我给一个不踩坑的建议:认准一到两个来源反复跟,不要今天看正点原子、明天看野火、后天又去B站搜零散视频。不同的教程在工程组织、库函数版本、引脚定义上都有差异,来回切换会让你非常混乱。选定一个教程系列以后,把它的例程一个个自己敲一遍,配合开发板验证,遇到不懂的再回到视频或者手册里查。
手册阅读能力是必须提前培养的。STM32参考手册的英文版一千多页,不用全看,但你至少要会查GPIO章节的寄存器描述、定时器章节的模式配置、DMA请求映射表。国内的教程能帮你快速跑起来,但底层细节、时序参数、寄存器位定义都以官方手册为准。遇事查手册,这个习惯越早建立越好。很多高手能从寄存器位看出别人代码里的错误,就是手册翻多了。
6.3 从开发板到实战项目:毕业设计和DIY方向
玩到一定程度,手里的板子不能满足需求了,自然会想做一个完整的项目。常见的实战方向有:
- 环境监测:DHT11温湿度传感器 + OLED显示 + 串口上报。
- 智能家居:继电器控制灯光,按键/红外遥控切换场景。
- 超声波测距:HC-SR04模块测距,数码管或者OLED显示距离,可扩展报警提示。
- 平衡小车:MPU6050姿态传感器 + 电机驱动 + PID控制,综合程度高,适合进阶。
- 鱼缸控制系统:热电偶或DS18B20测水温,控制加热棒和灯光,这也是搜"stm32鱼缸"看到的热门方向。
这些项目用到的外设其实都跳不出第六章列出的那些基础模块,区别在于要把多个外设协同起来,同时要考虑电源管理、代码架构、可靠性这些工程问题。做项目的时候强烈建议养成模块化编程的习惯,每个外设独立写成一个.c/.h源文件,对外暴露接口,不要在main函数里堆两三百行代码。
另外一个容易被忽略但很重要的点是原理图能力。做项目遇到硬件问题,如果你能看懂原理图,排查效率会翻倍。开发板的资料包里有完整原理图PDF,先学着找到LED、按键、串口、ST-LINK这几个模块的电路连接,再学会看数据手册里的引脚复用表(AFIO),你会发现很多"奇怪现象"都有了解释。
回头看整个入门过程,动手拆板、看原理图、写寄存器、配环境、烧录、调试、串口通信、定时器中断、做小项目,每一个环节都踩过坑,但也都是一点点爬出来的。学STM32最难的地方不是某个具体的知识点,而是"不知道接下来该学什么"的迷茫期。只要抓住一条主线——从GPIO起步、往定时器和串口走、用中断串起来、最后落到一个综合项目——每天推进一点点,用不了两个月你会发现这块巴掌大的板子已经能按照你的想法做不少事了。