1. 为什么第一课就要死磕开发环境
很多人学STM32,第一步就卡住了。不是卡在代码逻辑,不是卡在寄存器配置,而是卡在“环境搭不起来”。我见过太多人,板子买回来插上电脑,设备管理器里一个黄色感叹号,然后就开始怀疑人生。更常见的是,Keil装好了,工程编译通过了,一点下载按钮,弹出来一句“No ST-Link detected”,然后整个人就僵在那里。
这一课的核心目标很明确:把STM32F103的开发环境从零搭起来,让电脑认识你的开发板,让Keil能编译能下载,让你能亲眼看到第一个程序跑起来。听起来简单,但这里面涉及的环节其实不少——Keil MDK的安装与激活、器件支持包的安装、ST-Link驱动的部署、工程模板的建立、下载配置的调试。每一个环节都有坑,而且这些坑在官方文档里往往一笔带过,只有真正踩过的人才知道疼。
这篇文章面向的是刚拿到STM32F103开发板、准备入门嵌入式的朋友。不管你手里是某宝几十块的最小系统板,还是正点原子、野火那种配套齐全的开发板,环境搭建的逻辑是一样的。我会把每个步骤背后的“为什么”讲清楚,把常见的报错和排查方法整理出来,让你少走弯路。如果你已经搭好了环境,也可以对照着检查一下自己的配置有没有隐患。
2. 开发板与核心器件认识
2.1 STM32F103到底是什么
STM32F103是意法半导体(ST)推出的基于ARM Cortex-M3内核的32位微控制器。它不是一个芯片型号,而是一个系列,内部又细分成很多具体型号,比如STM32F103C8T6、STM32F103ZET6、STM32F103RCT6等等。这些型号的区别主要在于引脚数量、Flash容量、RAM大小、外设资源。C8T6是48脚、64KB Flash、20KB RAM,ZET6是144脚、512KB Flash、64KB RAM。
为什么STM32F103能成为国内嵌入式入门的“事实标准”?原因有几个。第一,资料极其丰富,中文社区里关于F103的教程、例程、问答铺天盖地,遇到问题基本都能搜到答案。第二,价格便宜,C8T6核心板十几块钱就能买到,学习成本极低。第三,外设齐全,GPIO、串口、SPI、I2C、ADC、定时器、CAN、USB该有的都有,足够覆盖大部分入门和中级项目。第四,生态成熟,Keil、IAR、GCC都能开发,ST官方还提供了标准外设库和HAL库两套代码框架。
你拿到手的那块开发板,不管长什么样,核心就是这颗F103芯片。板子上其他的东西——晶振、稳压芯片、LED、按键、USB接口、排针——都是为这颗芯片服务的。
2.2 最小系统的关键电路
“最小系统”这个词你肯定听过。所谓最小系统,就是让单片机能够正常工作的最少外围电路。对于STM32F103来说,最小系统包含以下几个部分:
电源电路。F103的工作电压是2.0V到3.6V,典型值3.3V。但你的USB口输出的是5V,所以板子上必须有一个5V转3.3V的电路。常见方案是用AMS1117-3.3这颗LDO稳压芯片,或者用RT9193这类低压差稳压器。你可以在板子上找找,通常是一个SOT-223封装的小芯片,旁边配两个滤波电容。这里有个细节:AMS1117的压差大约是1.1V,也就是说输入5V输出3.3V完全没问题,但如果输入降到4V以下,输出就不稳了。所以有些板子会用低压差更好的芯片。
晶振电路。F103通常有两个晶振:一个8MHz的高速晶振(HSE),接在OSC_IN和OSC_OUT引脚上,经过内部PLL倍频后给系统提供72MHz的主频;一个32.768kHz的低速晶振(LSE),主要给RTC实时时钟用。有些最小系统板为了省成本,只焊了8MHz晶振,甚至两个都不焊,靠内部RC振荡器工作。内部RC的精度和稳定性都不如外部晶振,做串口通信时容易出问题,所以建议优先用带外部晶振的板子。
复位电路。F103的NRST引脚是低电平复位。典型电路是一个10kΩ上拉电阻加一个100nF电容,再并联一个复位按键。按下按键时NRST被拉到地,松开后电容充电,NRST缓慢恢复到高电平,完成复位。这个电容的值不能太大,否则复位时间过长;也不能太小,否则按键抖动会导致多次复位。
启动模式配置。F103有两个启动配置引脚:BOOT0和BOOT1。通过这两个引脚的电平组合,决定芯片从哪块存储器启动。BOOT0=0时从主Flash启动,这是正常运行的模式;BOOT0=1、BOOT1=0时从系统存储器启动,也就是进入ISP串口下载模式;BOOT0=1、BOOT1=1时从SRAM启动,一般用于调试。大部分开发板会用跳线帽或者按键来控制BOOT0的电平。
下载调试接口。F103支持JTAG和SWD两种调试接口。JTAG需要5根线(TCK、TMS、TDI、TDO、nTRST),SWD只需要2根线(SWCLK、SWDIO)。现在主流都用SWD,因为引脚少、接线简单。你的开发板上应该有一个4针或5针的排针,标着SWDIO、SWCLK、GND、3.3V,这就是接ST-Link的地方。
2.3 开发板上那些外设
除了最小系统,开发板通常还会板载一些外设方便学习。最常见的是LED灯,一般接在PC13或者PA5这类引脚上,用来做点灯实验。按键通常接在PA0或者PB0,配上外部上拉或下拉电阻。串口一般用USART1,通过CH340或CP2102这类USB转串口芯片连接到电脑,用来打印调试信息。有些板子还会带OLED屏幕、温湿度传感器、蜂鸣器、红外接收头等等。
你不需要一上来就把所有外设都搞明白。第一课的目标就是让板子能跑起来,所以先关注电源、晶振、复位、SWD接口这四个部分就够了。
3. 软件环境搭建全流程
3.1 Keil MDK的安装与激活
Keil MDK(Microcontroller Development Kit)是国内STM32开发最主流的IDE。它集成了编辑器、编译器、调试器,配合ST-Link可以实现一键下载和在线调试。虽然现在VS Code + PlatformIO、STM32CubeIDE等方案也很流行,但Keil的生态和资料量仍然是最大的,入门阶段用Keil能少踩很多坑。
安装Keil MDK的步骤如下。首先去Keil官网下载MDK-ARM的安装包,目前最新版本是5.38a左右。下载完成后双击安装,一路Next,安装路径建议用默认的C:\Keil_v5,不要放在中文路径下。安装过程中会弹出Pack Installer的安装选项,可以先跳过,后面单独装。
安装完成后需要激活。Keil MDK是商业软件,但提供了32KB代码限制的免费版本。对于F103的学习来说,32KB基本够用,因为F103C8T6的Flash总共才64KB。如果你不想折腾激活,直接用免费版也行。如果要激活完整版,需要获取License,这里不展开讨论具体方法,你懂的。
激活之后,还需要安装器件支持包(Device Family Pack)。Keil MDK本身不包含任何芯片的器件信息,需要单独下载Pack。打开Keil,点击Pack Installer图标,在左侧找到STMicroelectronics,展开后找到STM32F1系列,点击Install。如果网络不好,也可以去Keil官网手动下载STM32F1xx_DFP的pack文件,双击安装。
注意:Pack的版本不要盲目追新。有些新版本的Pack对老工程兼容性不好,建议用2.3.0或2.4.0这类经过大量项目验证的版本。如果你用的是Keil 5.36以上版本,Pack Installer的界面有变化,但逻辑是一样的。
3.2 ST-Link驱动的部署
ST-Link是ST官方推出的调试下载器,价格便宜(山寨版十几块钱),支持SWD和JTAG,配合Keil可以实现下载、单步调试、断点、变量查看等功能。你买开发板时通常会附带一个ST-Link V2,长得像U盘一样,一头是USB公头,另一头是10针的排线。
ST-Link插上电脑后,需要安装驱动。Windows 10和Windows 11通常会自动识别并安装驱动,但有时候会装错版本。你可以在设备管理器里查看,如果看到“STMicroelectronics STLink dongle”或者“STLink Virtual COM Port”就说明驱动装好了。如果看到黄色感叹号,就需要手动安装。
手动安装的方法是:去ST官网下载ST-Link驱动包,安装后重新插拔ST-Link。或者用ST-Link Utility这个工具,它自带驱动安装功能。ST-Link Utility是ST官方出的独立下载工具,可以单独用来烧录hex文件,也可以用来升级ST-Link固件。有时候Keil识别不到ST-Link,但ST-Link Utility能识别,那就说明是Keil的配置问题,不是驱动问题。
实操心得:山寨ST-Link的固件版本参差不齐,有些老固件不支持新版Keil。如果遇到“No ST-Link detected”,先用ST-Link Utility连接一下,如果能连上,点击Firmware Update升级固件,很多时候就能解决。
3.3 工程模板的建立
环境装好了,接下来要建一个工程模板。为什么不直接从零新建工程?因为STM32的工程涉及启动文件、库文件、头文件路径、宏定义、链接脚本等一堆配置,每次新建都手动搞一遍太浪费时间。建一个干净的模板工程,以后每个新项目都从这个模板复制,效率高得多。
建模板工程的步骤如下。打开Keil,Project -> New uVision Project,选择一个空文件夹,工程名用英文,比如Template。弹出器件选择窗口,展开STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103C8,点击OK。然后会弹出Manage Run-Time Environment窗口,这里可以选择是否使用CMSIS和HAL库。对于入门学习,我建议先用标准外设库(Standard Peripheral Library),因为它的代码更直观,寄存器操作更透明。不过ST已经不再维护标准库了,新项目建议用HAL库。这里为了兼容大多数教程,先用标准库的方式手动添加文件。
工程建好后,需要手动添加文件。在工程目录下建几个文件夹:Startup放启动文件,Library放标准外设库的.c文件,User放main.c和中断处理文件。启动文件根据芯片型号选择,F103C8T6用startup_stm32f10x_md.s(md代表medium density)。库文件从ST官网下载的标准外设库包里复制,主要包括stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_usart.c等常用外设文件。
然后在Keil里右键Target,选择Manage Components,把刚才的文件分组添加进去。接着配置头文件路径:Options for Target -> C/C++ -> Include Paths,把Library和User的路径加进去。最后在Define里加上USE_STDPERIPH_DRIVER和STM32F10X_MD这两个宏定义。USE_STDPERIPH_DRIVER告诉编译器使用标准外设库,STM32F10X_MD告诉库文件当前芯片是中容量型号。
3.4 下载配置与调试设置
工程编译通过后,需要配置下载选项。点击Options for Target -> Debug,选择ST-Link Debugger。然后点击Settings,在Debug标签页里确认Port是SWD,能识别到芯片的IDCODE。如果识别不到,检查接线:SWDIO接PA13,SWCLK接PA14,GND接GND,3.3V接3.3V。注意有些板子的SWD接口顺序不一样,一定要看板子上的丝印。
在Flash Download标签页里,勾选Reset and Run,这样下载完程序后芯片会自动复位运行。Programming Algorithm里应该自动识别到STM32F10x Medium-density Flash,如果没有,点击Add手动添加。
常见坑:如果下载时提示“Flash Download failed - Target DLL has been cancelled”,通常是以下几个原因:ST-Link驱动没装好、SWD线接反了、芯片处于低功耗模式、BOOT0被拉高了。逐一排查即可。
4. 第一个程序:从点灯到串口打印
4.1 点灯程序的完整实现
点灯是嵌入式的Hello World。虽然简单,但它验证了整个工具链是否通畅:代码能编译、能下载、芯片能运行、外设能工作。
假设你的开发板上有一颗LED接在PC13上,低电平点亮。代码如下:
#include "stm32f10x.h" void Delay(__IO uint32_t nCount) { for(; nCount != 0; nCount--); } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); while(1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); Delay(500000); GPIO_ResetBits(GPIOC, GPIO_Pin_13); Delay(500000); } }这段代码的逻辑很清晰。第一步开时钟,STM32的外设默认时钟是关闭的,用之前必须使能对应总线的时钟。GPIOC挂在APB2总线上,所以调用RCC_APB2PeriphClockCmd。第二步配置GPIO模式,推挽输出、50MHz速度。第三步在死循环里翻转PC13的电平。
编译下载后,你应该能看到LED闪烁。如果没闪,先检查LED的引脚是不是PC13,有些板子用的是PA5或者PB0。再检查LED的极性,有些板子是高电平点亮。如果都不对,用万用表量一下引脚电压,看有没有变化。
4.2 串口打印的配置
点灯验证了下载和运行,串口打印则验证了通信和外设配置。串口是嵌入式调试最重要的工具,没有之一。通过串口,你可以打印变量值、输出调试信息、和上位机交互。
STM32F103通常用USART1做串口通信,TX是PA9,RX是PA10。配置步骤如下:
void USART1_Init(uint32_t bound) { GPIO_InitTypeDef GPIO_InitStructure; 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 = bound; 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); }TX引脚配置为复用推挽输出,RX引脚配置为浮空输入。波特率通常用115200,数据位8位,停止位1位,无校验。配置完成后调用USART_Cmd使能串口。
发送一个字节的函数:
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); USART_SendData(USARTx, data); }这里用TXE标志来判断发送数据寄存器是否为空。如果为空,就可以写入下一个字节。注意不要用TC标志,TC是发送完成标志,判断条件更严格,会导致发送效率降低。
4.3 用printf重定向到串口
直接调用USART_SendByte比较麻烦,尤其是要打印变量的时候。更好的方式是把C标准库的printf重定向到串口。方法是在代码里重写fputc函数:
int fputc(int ch, FILE *f) { USART_SendByte(USART1, (uint8_t)ch); return ch; }然后在Keil的Options for Target -> Target里勾选Use MicroLIB。MicroLIB是ARM提供的精简版C库,对printf的支持更轻量。如果不勾选MicroLIB,需要自己实现_sys_write等底层函数,比较麻烦。
重定向之后就可以直接用printf了:
printf("System Clock: %d Hz\r\n", SystemCoreClock); printf("Hello STM32F103!\r\n");注意:串口助手的波特率必须和代码里设置的一致,否则会打印乱码。另外,如果用的是USB转串口模块,TX接RX、RX接TX,别接反了。
5. 常见问题与排查技巧实录
5.1 编译与下载类问题
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 编译报错“cannot open source input file stm32f10x.h” | 头文件路径没加 | Options for Target -> C/C++ -> Include Paths添加库文件路径 |
| 编译报错“undefined symbol GPIO_Init” | 库文件没添加到工程 | 在Manage Components里添加stm32f10x_gpio.c |
| 下载提示“No ST-Link detected” | 驱动问题或接线问题 | 检查设备管理器、换USB口、检查SWD接线 |
| 下载提示“Flash Download failed” | Flash算法没选对 | 在Flash Download里添加STM32F10x Medium-density算法 |
| 下载成功但程序不运行 | BOOT0被拉高 | 把BOOT0跳线帽接回GND |
| 程序运行一次就停 | 看门狗没喂或HardFault | 检查是否开启了独立看门狗,检查数组越界 |
5.2 串口通信类问题
串口打印乱码是最常见的问题。原因通常有三个:波特率不匹配、晶振频率不对、串口助手配置错误。波特率不匹配最好排查,检查代码里的bound值和串口助手的设置是否一致。晶振频率不对比较隐蔽,比如你的板子焊的是8MHz晶振,但代码里HSE_VALUE宏定义的是12MHz,那系统时钟就会算错,波特率自然也不对。串口助手配置错误包括数据位、停止位、校验位,通常用8-N-1。
串口收不到数据,先检查TX和RX有没有接反。很多人习惯性地把TX接TX,这是不对的。正确的接法是开发板的TX接USB转串口模块的RX,开发板的RX接模块的TX。另外,如果开发板上的USB口本身就是USB转串口(比如CH340),那直接用USB线连电脑就行,不需要额外的模块。
5.3 那些教程不会告诉你的坑
坑一:Keil的Pack版本冲突。如果你电脑上装过多个版本的STM32F1 Pack,Keil可能会加载错误的版本,导致编译报一些莫名其妙的错误。解决方法是打开Pack Installer,把不需要的版本卸载,只保留一个稳定版本。
坑二:ST-Link固件升级失败。山寨ST-Link升级固件时如果中途断开,可能会变砖。变砖后可以用ST-Link Utility的固件恢复功能救回来,但需要拆开外壳短接某些引脚。所以升级固件时一定要保证USB连接稳定。
坑三:中文路径导致的问题。Keil对中文路径的支持不好,工程路径、安装路径、库文件路径都尽量用纯英文。我见过有人把工程放在“桌面\单片机学习\第一课”这样的路径下,编译时各种报错,改成英文路径就好了。
坑四:电源供电不足。有些开发板板载的外设比较多,USB口供电可能不够,导致芯片工作不稳定。表现是程序偶尔跑飞、串口数据丢包。解决方法是换一个输出电流更大的USB口,或者用外部电源供电。
坑五:忘记开时钟。STM32的外设时钟默认关闭,用之前必须使能。新手最常犯的错误就是配置了GPIO模式但忘了开GPIOC的时钟,然后纳闷为什么引脚没反应。记住一句话:用哪个外设,先开哪个时钟。
5.4 调试技巧:用ST-Link做在线调试
ST-Link不只是用来下载的,它还能做在线调试。在Keil里点击Debug按钮(或者按Ctrl+F5),程序会下载并进入调试模式。这时候你可以单步执行、设置断点、查看变量值、查看寄存器状态。
几个实用的调试技巧。第一,在关键代码处设置断点,程序运行到断点会暂停,你可以查看此时变量的值。第二,使用Watch窗口添加要监视的变量,实时观察变化。第三,如果程序跑飞了,点击Stop按钮,Keil会停在当前执行的指令处,通过Call Stack窗口可以看到函数调用关系,快速定位问题。第四,在Peripherals菜单里可以查看外设的寄存器状态,比如GPIO的ODR寄存器、USART的SR寄存器,非常直观。
实操心得:调试时如果发现程序总是进入HardFault_Handler,大概率是数组越界、空指针访问、堆栈溢出。可以在HardFault_Handler里加一句while(1),然后查看LR寄存器的值,判断是从哪里跳转过来的。
6. 环境搭建完成后的下一步
环境搭好、点灯成功、串口能打印,第一课的任务就算完成了。但这只是开始。接下来你需要熟悉GPIO的八种工作模式、中断系统、定时器、ADC、SPI、I2C等外设。每一个外设都有它的配置流程和注意事项,但底层的逻辑是一样的:开时钟、配引脚、配外设、使能外设、写业务代码。
我个人的建议是,不要急着上RTOS或者复杂的协议栈。先把裸机的外设玩熟,把寄存器手册翻几遍,把中断优先级搞明白。这些基础打牢了,后面学什么都快。另外,养成看参考手册的习惯。STM32F103的参考手册RM0008有一千多页,不需要从头看到尾,但用到哪个外设就翻到对应章节,把寄存器位定义和配置流程看清楚。中文社区的资料虽然多,但质量参差不齐,最终还是要以官方手册为准。
最后分享一个小技巧:建一个自己的代码仓库,把每次调通的工程按外设分类保存。比如GPIO_LED、USART_Print、TIMER_PWM、ADC_Sample。以后做新项目时,直接从仓库里复制对应的模块,改改引脚和参数就能用,效率会高很多。这个习惯我从入门坚持到现在,受益无穷。