☰
STM32F103开发板入门到进阶:环境搭建、点灯串口与常见问题排查
2026/11/6 1:03:36 网站建设 项目流程

1. 拿到板子先别急着点灯,搞懂你手里这块东西到底是什么

STM32F103开发板到手的那一刻,大部分人的第一反应是插上USB线看灯亮不亮。这个动作没错,但如果你打算长期跟STM32打交道,我建议你先花二十分钟把板子上的东西认全,后面能省下大量抓头发的时间。

STM32F103这块芯片属于ARM Cortex-M3内核,主频72MHz,常见封装有LQFP48、LQFP64、LQFP100、LQFP144几种。市面上卖得最多的“最小系统板”一般是LQFP48或者LQFP64封装的C8T6、RCT6、ZET6这几个型号。C8T6是48脚,64KB Flash、20KB RAM;RCT6是64脚,256KB Flash、48KB RAM;ZET6是144脚,512KB Flash、64KB RAM。你买板子的时候如果没注意型号,先翻过来看芯片表面的丝印,那串字母数字就是它的身份证。

开发板上通常会集成这些东西:一颗STM32F103主控、一个8MHz晶振(给芯片提供主时钟源)、一个32.768kHz晶振(给RTC用)、一个复位按键、一个或多个用户按键、若干LED、一个USB接口(有些是USB转串口芯片,有些是直接接STM32的USB口)、一个SWD调试接口(通常是四针或五针的排针)、一个3.3V稳压芯片(比如AMS1117-3.3)、以及引出所有GPIO的排针。

这里有个新手特别容易踩的坑:USB接口的类型决定了你烧录程序的方式。如果你的板子上USB口旁边有一颗CH340或者CP2102之类的芯片,那这个USB口是“USB转串口”,你只能用串口方式(通过Boot0/Boot1引脚配置)来烧录,或者用它来看串口打印信息。如果板子上有一个标注了“USB”且直接连到STM32的PA11/PA12引脚的口,那才是STM32自带的USB Device接口,可以用来做USB设备开发。很多人拿到板子插上USB发现电脑没反应,就是因为没装CH340驱动,或者USB线是纯充电线没有数据线芯。

注意:拿到板子第一件事,用万用表蜂鸣档测一下3.3V和GND之间有没有短路。我见过不止一次因为板子上的稳压芯片焊接不良或者电容击穿导致上电就发烫的情况。确认没有短路再插USB。

另外,STM32芯片的第一脚怎么确认?芯片表面通常有一个小圆点或者一个斜切角,那个位置对应的就是第1脚。LQFP封装的芯片,逆时针方向数,第1脚在左上角。如果你把芯片方向搞反了焊上去,通电基本就是冒烟的下场。买现成的开发板一般不会犯这个错,但如果你后面要自己画板子或者换芯片,这一点必须刻在脑子里。

2. 开发环境搭建:Keil、CubeMX、VS Code到底怎么选

2.1 Keil MDK还是STM32CubeIDE,新手别纠结

网上关于STM32开发环境的争论从来没停过。有人说Keil好用,有人说CubeIDE免费真香,还有人用VS Code加插件搞出一套花里胡哨的配置。我的建议很直接:新手先用Keil MDK,把第一个工程跑起来再说。

Keil MDK的优势在于资料多、教程多、遇到问题搜一下基本都能找到答案。缺点是要收费,但如果你只是学习用途,用评估版也完全够用,32KB的代码限制对于F103来说能写不少东西了。安装Keil5的时候有一个经典问题:Keil5兼容C51和STM32的安装。如果你之前装过Keil C51,再装MDK的时候要选不同的安装路径,否则会覆盖掉之前的文件。装完之后需要单独安装STM32F1系列的Device Family Pack,也就是常说的“芯片包”。这个包不装,新建工程的时候找不到STM32F103的型号。

STM32CubeIDE是ST官方推出的免费IDE,基于Eclipse,集成了CubeMX配置工具。它的好处是免费、跨平台、官方支持,缺点是界面响应速度确实不如Keil流畅,而且国内有些网络环境下下载芯片包会比较慢。如果你打算长期做STM32项目,我建议两个都装,Keil用来快速验证代码,CubeIDE用来做正式项目。

2.2 VS Code里编译成功却烧录不进去,问题出在哪

这个问题在热词里出现了,说明踩坑的人不少。VS Code本身只是一个编辑器,它通过插件调用外部的编译工具链(比如arm-none-eabi-gcc)和烧录工具(比如OpenOCD、ST-Link Utility、pyocd)。编译成功说明你的工具链配置没问题,但烧录不进去通常是以下几个原因:

第一,烧录器驱动没装好。ST-Link需要装驱动,J-Link也需要装驱动。设备管理器里如果看到黄色感叹号,先去装驱动。第二,烧录器类型选错了。VS Code的配置文件里要指定你用的是什么烧录器,ST-Link和J-Link的配置参数不一样。第三,芯片被读保护了。如果你之前烧录过程序并且启用了读保护,再次烧录的时候需要先解除保护。用ST-Link Utility连接芯片,在Target菜单里选择Option Bytes,把Read Out Protection改成Disabled,然后应用。

第四,复位电路或者BOOT引脚状态不对。STM32F103的BOOT0引脚决定了芯片从哪启动。BOOT0接GND是从Flash启动(正常运行模式),BOOT0接VCC是从系统存储器启动(串口下载模式)。如果你用SWD烧录,BOOT0必须接GND。有些开发板上BOOT0是通过跳线帽选择的,检查一下跳线帽是不是插在正确的位置。

实操心得:我习惯在烧录之前先用ST-Link Utility连接一下芯片,能读到芯片ID说明硬件连接没问题,读不到就是硬件或者驱动的问题。这一步能帮你快速定位问题出在软件还是硬件上。

2.3 标准库还是HAL库,这是个问题

STM32F103的标准库最新版本是V3.5.0,ST已经停止更新了,但网上有大量的工程模板可以下载。HAL库是ST现在主推的库,配合CubeMX可以快速生成初始化代码。我的建议是:先学标准库,再过渡到HAL库。

标准库的代码结构清晰,寄存器操作封装得比较薄,你能看到很多底层的东西。比如配置一个GPIO,标准库的代码是这样的:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

而HAL库的代码是这样的:

GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

看起来差不多,但HAL库在底层做了更多的封装和状态管理。对于F103这种资源有限的芯片,标准库编译出来的代码通常更小、运行效率更高。但HAL库的跨系列兼容性更好,你从F103换到F407或者G0系列,HAL库的代码改动很小。

3. 从点灯到串口,第一个工程应该这么搭

3.1 新建工程的正确姿势

不管你用Keil还是CubeIDE,新建工程的流程都差不多。以Keil为例,步骤如下:

  1. 打开Keil,Project -> New uVision Project,选一个空文件夹作为工程目录。
  2. 在弹出的器件选择窗口里找到STM32F103对应的型号。如果你用的是C8T6就选STM32F103C8,RCT6就选STM32F103RC,ZET6就选STM32F103ZE。
  3. 弹出的Manage Run-Time Environment窗口里,勾选CMSIS下的CORE和Device下的Startup。这两个是必须的,其他的外设库可以后面再加。
  4. 工程建好之后,在工程目录下新建几个文件夹:User(放main.c)、Library(放标准库的.c文件)、Startup(放启动文件)、CMSIS(放内核相关的文件)。
  5. 把标准库V3.5.0里的文件复制到对应的文件夹里。具体来说,stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_usart.c这些放到Library里,stm32f10x.h、system_stm32f10x.h这些放到CMSIS里。
  6. 在Keil的工程窗口里把这些文件添加到对应的Group里。
  7. 在Options for Target里,C/C++选项卡的Define里加上USE_STDPERIPH_DRIVER, STM32F10X_MD。注意MD代表Medium Density,对应的是C8T6和RCT6;如果是ZET6要改成STM32F10X_HD。
  8. 在Include Paths里把所有的头文件目录都加进去。

这一套流程走下来,你的工程模板就搭好了。以后新建工程直接复制这个模板改名字就行。网上有很多现成的标准库V3.5.0工程模板可以下载,但建议你自己手动搭一遍,搭的过程中你会搞清楚每个文件的作用。

3.2 点灯代码背后的时钟配置

点灯是每个STM32新手的第一个程序,但很多人只是把代码复制进去编译烧录,灯亮了就完事了。如果你只满足于灯亮,那后面的路会越走越窄。我建议你在点灯之前,先搞清楚系统时钟是怎么配置的。

STM32F103默认使用内部的8MHz RC振荡器作为时钟源,但大多数开发板外部都有一个8MHz的晶振。通过配置PLL(锁相环),可以把8MHz倍频到72MHz作为系统时钟。标准库里的SystemInit函数会帮你做这件事,但你需要知道它做了什么。

系统时钟的配置流程大致是这样的:使能外部高速晶振HSE,等待HSE稳定,配置PLL的倍频系数为9倍(8MHz * 9 = 72MHz),选择PLL作为系统时钟源,配置AHB、APB1、APB2的分频系数。APB1的最大频率是36MHz,APB2的最大频率是72MHz。如果你在APB1上挂的外设(比如USART2、USART3、TIM2~TIM7)需要更高的时钟,就要注意分频系数不能设错。

为什么要点灯之前先搞时钟?因为后面的串口、定时器、SPI、I2C全都依赖时钟配置。时钟配错了,串口波特率就是错的,定时器定时就不准。我见过有人调串口调了一整天,最后发现是系统时钟没配成72MHz,导致波特率计算全错。

3.3 串口打印:你的第一双眼睛

灯亮了之后,下一步就是调通串口。串口的重要性怎么强调都不为过,它是你调试程序的主要手段。没有串口,你只能靠灯闪来猜程序跑到哪了;有了串口,你可以打印变量值、打印状态信息、打印错误码。

STM32F103通常有3个USART(USART1、USART2、USART3)和2个UART(UART4、UART5,只有大容量型号才有)。USART1挂在APB2上,最高时钟72MHz;USART2和USART3挂在APB1上,最高时钟36MHz。配置串口的时候,波特率的计算公式是:

波特率 = fCK / (16 * USARTDIV)

其中fCK是串口挂载的总线时钟,USARTDIV是一个浮点数,整数部分写在BRR寄存器的[15:4]位,小数部分写在[3:0]位。比如你要配115200的波特率,USART1的时钟是72MHz,那么USARTDIV = 72000000 / (16 * 115200) = 39.0625。整数部分是39,小数部分是0.0625 * 16 = 1。所以BRR寄存器的值应该是0x271。

标准库的USART_Init函数会自动帮你算这个值,你只需要传入波特率就行。但如果你哪天发现串口打印出来是乱码,第一个要检查的就是系统时钟和波特率是否匹配。

串口配置好之后,重定向printf函数是个很实用的技巧。在标准库工程里,你只需要在代码里加上:

int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }

然后在Keil的Options for Target里勾选Use MicroLIB,就可以直接用printf打印了。这个技巧能让你在调试的时候方便很多。

4. 那些年我们一起踩过的坑:常见问题与排查实录

4.1 程序烧录不进去的几种典型情况

烧录失败是新手遇到最多的问题,没有之一。我把常见的几种情况整理成了一张表,方便你对照排查:

现象可能原因排查方法
ST-Link连接不上芯片驱动未安装或版本不对设备管理器查看是否有ST-Link设备,重装驱动
能连接但烧录报错芯片读保护未解除用ST-Link Utility解除读保护
烧录成功但程序不运行BOOT引脚状态错误检查BOOT0是否接GND
烧录成功但运行异常晶振未起振用示波器测晶振引脚是否有波形
烧录过程中断电源供电不足换一个USB口或外接电源

还有一种情况是STM32禁用JTAG之后导致SWD也连不上。STM32F103的PA13、PA14、PA15、PB3、PB4这几个引脚默认是JTAG功能。如果你在代码里把JTAG禁用了但没保留SWD,或者把SWD引脚配置成了普通GPIO,那下次烧录就连不上了。解决办法是按住复位键,点击烧录的同时松开复位键,让芯片在复位后的瞬间被烧录器抓住。或者把BOOT0接VCC,从系统存储器启动,这样芯片不会运行你之前烧录的程序,烧录器就能正常连接。

实操心得:我在代码里习惯在禁用JTAG的时候保留SWD,具体操作是调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE),这样只禁用JTAG但保留SWD。另外,调试用的SWD引脚(PA13和PA14)永远不要配置成普通GPIO,除非你确定以后不需要再烧录了。

4.2 延时函数delay卡死是怎么回事

热词里出现了“stm32延时函数delay卡死”,这个问题通常有两种原因。第一种是中断优先级配置不当。如果你在中断服务函数里调用了delay函数,而delay函数依赖SysTick中断来计数,但SysTick中断的优先级低于当前中断,那SysTick中断就进不来,delay就会一直卡在while循环里。解决办法是提高SysTick中断的优先级,或者不要在中断里用delay。

第二种是delay函数的实现有问题。很多教程里的delay函数是用循环计数实现的,这种延时不准而且容易受编译器优化影响。我建议用SysTick定时器来实现延时,配置好SysTick的 reload 值,每次延时的时候读取当前计数值来判断时间是否到了。标准库里的delay函数通常是这么写的:

void delay_ms(u16 nms) { u32 temp; SysTick->LOAD = 9000 * nms; SysTick->VAL = 0; SysTick->CTRL |= 1; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL &= ~1; SysTick->VAL = 0; }

这里的9000是72MHz除以8得到的,因为SysTick的时钟源默认是HCLK的8分频。如果你改了系统时钟,这个值也要跟着改。

4.3 HardFault监控:程序跑飞了怎么办

HardFault是Cortex-M内核里最严重的一种异常,通常是因为访问了非法内存地址、除零、未对齐访问等原因触发的。程序一旦进入HardFault,默认的处理函数是一个死循环,你只能看到程序不跑了,但不知道跑飞到哪里去了。

热词里提到了“stm32f103单片机hardfault监控程序”,说明有人在做这方面的排查。我的做法是在HardFault_Handler里加入打印功能,把出错时的寄存器状态通过串口打印出来。具体来说,在进入HardFault的时候,堆栈里保存了出错时的PC指针、LR寄存器、R0~R3等值。你可以写一个HardFault_Handler,从堆栈里把这些值取出来打印,然后根据PC指针的值去反汇编文件里查找对应的代码位置。

void HardFault_Handler(void) { __asm volatile ( "tst lr, #4\n" "ite eq\n" "mrseq r0, msp\n" "mrsne r0, psp\n" "ldr r1, =hard_fault_handler_c\n" "bx r1\n" ); } void hard_fault_handler_c(unsigned int *hardfault_args) { printf("HardFault! PC = 0x%08X\r\n", hardfault_args[6]); printf("LR = 0x%08X\r\n", hardfault_args[5]); while(1); }

这段代码的核心思路是判断出错时使用的是MSP还是PSP堆栈,然后把堆栈指针传给C函数,从堆栈里取出PC值。有了PC值,你就能定位到出错的代码行了。

5. 进阶方向:从点灯到做项目,中间还差什么

5.1 定时器:STM32最强大的外设之一

STM32F103的定时器资源非常丰富,有高级定时器TIM1和TIM8、通用定时器TIM2~TIM5、基本定时器TIM6和TIM7。每个定时器都有多个通道,可以实现PWM输出、输入捕获、输出比较、编码器接口等功能。

热词里提到了“stm32f103定时器实现软件串口”和“stm32定时器捕获测频率”,这两个都是定时器的典型应用。软件串口就是用定时器的输入捕获来测量串口数据的位宽,或者用定时器的输出比较来模拟串口发送。捕获测频率则是用输入捕获功能测量外部信号的周期,然后算出频率。

定时器的配置步骤大致是:使能定时器时钟、配置时基单元(预分频器、自动重装载值、计数模式)、配置通道(输入捕获或输出比较)、配置中断(如果需要)、使能定时器。预分频器和自动重装载值的计算公式是:

定时器溢出频率 = 定时器时钟 / ((预分频器 + 1) * (自动重装载值 + 1))

比如你要产生一个1kHz的定时中断,定时器时钟是72MHz,预分频器设为71,自动重装载值设为999,那么溢出频率 = 72000000 / (72 * 1000) = 1000Hz。

5.2 USB设备开发:STM32F103的USB能做什么

STM32F103自带一个USB 2.0全速设备接口,支持12Mbps的传输速率。热词里提到了“stm32 如何做usb设备”和“stm32 usb虚拟串口发送数据”,说明很多人对USB开发感兴趣。

STM32F103的USB外设可以配置成多种设备类型:虚拟串口(CDC)、键盘(HID)、鼠标(HID)、大容量存储设备(MSC)、自定义设备等。做USB虚拟串口是最常见的入门项目,因为它的用途很直接——给电脑增加一个串口,但不需要额外的USB转串口芯片。

USB虚拟串口的实现需要用到ST提供的USB库,配置过程比较复杂,涉及到端点配置、描述符定义、中断处理等。我的建议是先用CubeMX生成一个USB CDC的工程,把基本的收发跑通,然后再去研究描述符和端点的细节。USB协议本身就很复杂,一上来就啃协议栈容易劝退。

5.3 基于STM32的毕业设计怎么做才不烂大街

热词里出现了“基于stm32的毕业设计”,这是一个很现实的需求。每年毕业季都有大量的学生做STM32相关的毕设,但大部分都是“温湿度监测+LCD显示+报警”这种套路。如果你想做出点不一样的东西,我有几个方向可以参考。

第一个方向是多传感器融合。不要只用一个DHT11测温度,可以加上气压传感器、光照传感器、空气质量传感器,做一个环境监测站。关键是要把数据融合起来做趋势分析,而不是简单地显示数值。

第二个方向是无线通信+云端。用ESP8266或者ESP32做WiFi通信,把STM32采集的数据上传到云平台,做一个远程监控系统。这个方向的技术栈比较完整,从传感器到单片机到无线模块到云端,能体现你的综合能力。

第三个方向是实时控制。用STM32的定时器和PWM做电机控制,比如做一个小型的平衡车或者机械臂。这个方向对实时性要求高,能体现你对中断、定时器、PID算法的理解。

不管选哪个方向,关键是要有“自己的东西”。哪怕你只是把别人的方案改进了10%,只要你能说清楚为什么改、改了什么、效果如何,那就是一个合格的毕设。

5.4 仿真工具:没有硬件也能学STM32

热词里提到了“renode如何模拟stm32f103”,这是一个很实用的工具。Renode是一个开源的仿真框架,可以模拟多种嵌入式平台,包括STM32F103。如果你手头没有开发板,或者想在不烧录的情况下验证代码逻辑,Renode是一个不错的选择。

Renode的安装和使用不算复杂,下载安装包之后,写一个简单的脚本描述你的硬件平台(比如STM32F103的存储器映射、外设地址等),然后加载你的elf文件就可以运行了。它支持UART输出,你可以看到串口打印的信息。不过Renode的仿真精度有限,定时器和中断的时序可能和真实硬件有差异,所以它更适合验证逻辑,不适合验证时序敏感的功能。

6. 工具链和调试技巧:让开发效率翻倍

6.1 ST-Link Utility和STM32CubeProgrammer怎么选

ST-Link Utility是ST早期的烧录工具,界面简单,功能单一,但胜在稳定。STM32CubeProgrammer是ST现在主推的烧录工具,支持更多的芯片系列和更多的功能(比如选项字节配置、OTP读写、外部存储器烧录等)。如果你只是烧录F103,两个都够用。但如果你后面要接触G0、H7、MP1等系列,建议直接上手STM32CubeProgrammer。

这两个工具都有一个很实用的功能:读取芯片内的Flash内容。如果你有一个已经烧录好的板子,想读出里面的程序,可以用这个功能把Flash内容读出来保存成bin或者hex文件。当然,如果芯片启用了读保护,这个操作是会被拒绝的。

6.2 用VS Code搭建STM32开发环境的正确姿势

虽然我建议新手先用Keil,但如果你已经有一定基础,想在VS Code里开发STM32,也是完全可以的。核心需要装这几个东西:Cortex-Debug插件(用于调试)、STM32 VS Code Extension(ST官方插件)、arm-none-eabi-gcc工具链、OpenOCD或者ST-Link GDB Server。

配置的关键在launch.json和tasks.json这两个文件。launch.json告诉VS Code用什么调试器、连接什么目标、加载什么elf文件。tasks.json定义编译任务,调用make或者cmake来编译工程。如果你用CubeMX生成Makefile工程,VS Code可以直接调用make来编译。

编译成功但烧录不进去的问题,前面已经分析过了。这里补充一点:VS Code的调试配置里,svdFile这个选项可以加载STM32F103的SVD文件,加载之后你在调试的时候就能看到所有外设寄存器的值,非常方便。

6.3 常见外设的调试要点速查

外设常见问题排查要点
GPIO输出电平不对检查时钟使能、模式配置、引脚复用
USART乱码或收不到数据检查波特率、时钟配置、TX/RX是否交叉
TIM定时不准检查预分频和重装载值、时钟源
SPI通信失败检查CPOL/CPHA、片选信号、时钟极性
I2C总线锁死检查上拉电阻、时钟频率、从机地址
ADC采样值跳动检查参考电压、采样时间、滤波电路
DMA传输不完成检查通道配置、传输方向、中断标志

这张表里的每一个问题我都实际遇到过,尤其是I2C总线锁死,几乎是每个用I2C的人都会踩的坑。I2C锁死的典型表现是SCL被从机拉低,主机无法产生时钟脉冲。解决办法是在初始化的时候手动发送几个时钟脉冲,让从机释放总线。

7. 学习路径建议:从F103出发能走多远

STM32F103是一块很好的入门芯片,但它的性能有限,72MHz的主频、最大512KB的Flash、64KB的RAM,对于复杂的应用来说确实不够用。但如果你能把F103玩透,后面换到F4、F7、H7甚至G0、L4系列,上手会非常快。

我的建议是:在F103上至少做三个完整的项目。第一个项目是基础外设的综合应用,比如用GPIO、定时器、串口、ADC做一个简单的数据采集器。第二个项目是通信协议相关的,比如用SPI驱动一个显示屏,或者用I2C驱动一个传感器。第三个项目是带实时操作系统的,比如移植FreeRTOS,做多任务调度。

做完这三个项目,你对STM32的理解就不再是“点灯”的水平了。你会知道怎么阅读参考手册、怎么查数据手册、怎么用示波器抓波形、怎么用调试器看寄存器。这些能力比你会用多少个外设更重要。

热词里还有“3588开发板”“t113开发板”“zynq开发板”这些更高端的平台,但那是另一个阶段的事情了。先把F103吃透,后面的事情水到渠成。我见过太多人一上来就想搞Linux、搞FPGA,结果连GPIO都没搞明白,最后两头都没学好。

最后分享一个我自己的习惯:每次调通一个新外设,我都会把配置代码整理成一个独立的.c和.h文件,放在一个公共的文件夹里。下次做新项目的时候直接复制过来改改就能用。几年下来,我攒了一套自己的外设驱动库,虽然不如HAL库那么完善,但胜在轻量、可控、每一行代码我都知道它在干什么。这个习惯让我在换芯片平台的时候,移植代码的速度比其他人快很多。

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

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

立即咨询