1. 拿到板子先别急着点灯,搞懂F103的脾气再说
STM32F103这块板子,在嵌入式圈子里基本就是“新手村第一只怪”。便宜、资料多、坑也经典,几乎每个搞单片机的人都从它手里过过一遍。你手里这块开发板,大概率是F103C8T6最小系统板或者ZET6那种大板子,不管哪种,核心都是ARM Cortex-M3内核,72MHz主频,64KB到512KB的Flash,20KB的SRAM。这个配置放在今天看确实寒酸,但用来学外设、跑裸机、理解中断和时钟树,绰绰有余。
我见过太多人板子买回来,Keil装完,新建工程,编译通过,然后卡在“烧录不进去”这一步。VS Code里编译成功却烧录失败,这个热搜词不是白来的。问题往往不在代码,而在调试器配置、启动模式跳线、或者芯片被读保护了。所以这篇东西不跟你扯虚的,从开箱到跑通第一个工程,再到几个典型外设的实操,把F103开发板上手会遇到的坑一个个填平。
适合谁看?刚买板子的大学生、转行嵌入式的软件工程师、带毕设的倒霉蛋,以及想从51过渡到32位机的老玩家。你不需要有STM32基础,但至少得知道C语言指针和结构体是啥,不然看寄存器操作会有点懵。
2. 开发板到手后的第一轮硬件检查与工具链搭建
2.1 板子上的跳线帽和供电,先确认这三处
拿到板子先别插USB。翻到背面或者侧面,找三个东西:BOOT0和BOOT1跳线、电源指示灯、调试器接口。BOOT0决定芯片从哪启动,接GND是主Flash启动,接VCC是系统存储器启动(用于串口下载)。正常跑程序,BOOT0必须接地,BOOT1随便。很多板子出厂跳线帽是悬空的,你直接上电,芯片找不到启动地址,表现就是“程序烧进去了但不跑”。
供电方面,F103开发板一般有两种取电方式:USB口直接供5V,板载LDO转3.3V;或者外部DC座供7-12V。如果你用ST-Link调试器供电,注意调试器的3.3V输出电流有限,带不动板载外设多的情况。我实测过某款C8T6最小板,插上OLED和超声波模块后,ST-Link供电直接掉到2.9V,芯片反复复位。所以调试器只接SWDIO、SWCLK、GND三根线,供电单独走USB,这是最稳的接法。
调试接口认准SWD模式,四根线:3.3V、GND、SWDIO、SWCLK。JTAG虽然也能用,但占用引脚多,F103上SWD和JTAG复用,你如果开了JTAG,PB3、PB4、PA15就不能当普通IO用。热搜里“stm32禁用jtag”就是这个原因。在代码里加这两句:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这样JTAG关了,SWD还能用,PB3、PB4、PA15释放出来做普通GPIO。
2.2 Keil、CubeMX、VS Code怎么选,别纠结
工具链这块,新手最容易陷入“哪个IDE最好”的争论。我的建议很直接:前期用Keil MDK + 标准库或HAL库,中期转VS Code + PlatformIO或CubeCLT,后期看项目需求。
Keil的好处是资料多,随便搜个“STM32F103 LED闪烁”,出来的代码99%是Keil工程。安装Keil5之后,必须装STM32F1xx_DFP芯片包,不然新建工程找不到器件。热搜里“stm32芯片包安装”就是这一步。芯片包去Keil官网下,或者用Pack Installer在线装,注意版本别太新,F1系列用2.3.0左右的包最稳。
CubeMX用来生成初始化代码,省得你手算时钟树。F103的时钟树不算复杂,但新手容易把APB1和APB2的分频搞混。APB2最高72MHz,APB1最高36MHz,定时器时钟要看APB预分频系数:如果APB预分频是1,定时器时钟等于APB时钟;否则定时器时钟是APB时钟的2倍。这个规则在CubeMX里会自动算好,但你得知道它为什么这么算。
VS Code方案适合已经熟悉命令行的老手。装Cortex-Debug插件,配OpenOCD或ST-Link GDB Server,编译用Makefile或CMake。热搜里“stm32 vscode配置”和“vs code里编译成功却怎么也烧录不进开发板”往往是OpenOCD的配置文件选错了。F103用interface/stlink-v2.cfg和target/stm32f1x.cfg,如果你用的是ST-Link V3,接口文件要换成stlink-dap.cfg。烧录失败先看OpenOCD的输出,它会告诉你到底是连不上芯片还是Flash锁了。
2.3 新建工程的两种路子:标准库和HAL库
标准库新建工程的步骤:建文件夹、拷库文件、加头文件路径、写main.c、配时钟、配GPIO。这套流程在CSDN上能搜到无数遍,但坑在于库文件的版本。F103的标准库最后版本是V3.5.0,网上有些工程用的是V3.0.0,两者在stm32f10x.h里的宏定义不一样。你如果混用,编译报错能报一屏。
HAL库用CubeMX生成,好处是跨系列移植方便,坏处是代码臃肿,一个GPIO初始化能给你生成几十行。但新手用HAL库不容易卡在时钟配置上,因为CubeMX的图形界面把时钟树画出来了,你点几下鼠标就能看到每个总线的频率。
我个人的做法:学习阶段用标准库,因为寄存器操作更直观,你能看到GPIO_SetBits(GPIOA, GPIO_Pin_5)背后到底写了哪些寄存器。做项目赶时间用HAL库,CubeMX生成完直接写业务逻辑。
3. 第一个工程:从点亮LED到串口打印的完整实操
3.1 LED闪烁背后的时钟和GPIO配置
点亮LED是嵌入式界的“Hello World”,但这里面藏着F103的时钟使能逻辑。F103的GPIO挂在APB2总线上,用之前必须先开时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);然后配置GPIO模式。LED接在PA5上,通常是推挽输出、50MHz速度:
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);推挽输出和开漏输出的区别:推挽能输出高低电平,驱动能力强;开漏只能输出低电平,高电平靠外部上拉。LED一般用推挽,I2C总线用开漏。
延时函数别用delay_ms那种循环空跑,F103的SysTick定时器可以配成1ms中断,在中断里递减计数。热搜里“stm32延时函数delay卡死”通常是因为中断优先级配置不对,SysTick中断被其他高优先级中断阻塞了。检查NVIC_PriorityGroupConfig的设置,确保SysTick的抢占优先级足够高。
3.2 串口配置与printf重定向
串口是调试利器。F103的USART1挂在APB2,USART2/3挂在APB1。配置步骤:开时钟、配GPIO(TX推挽复用、RX浮空输入)、配USART参数(波特率、数据位、停止位、校验位)、使能USART。
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_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_InitTypeDef USART_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_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE);printf重定向要重写fputc函数:
int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }注意Keil里要勾选“Use MicroLIB”,否则printf会卡在半主机模式。VS Code里用newlib的话,重定向函数名是_write。
3.3 烧录失败的五个常见原因和排查顺序
热搜里“vs code里编译成功却怎么也烧录不进开发板”和“load error: flash”这两个问题,我按排查顺序列一下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 连不上芯片 | SWD线接反或接触不良 | 万用表测SWDIO对地电阻,正常几百欧 |
| 能连上但烧录报错 | Flash被读保护 | ST-Link Utility里解除读保护 |
| 烧录成功但不运行 | BOOT0跳线不对 | 确认BOOT0接地 |
| 烧录到一半失败 | 调试器供电不足 | 板子单独供电 |
| 提示Flash算法错误 | Keil工程器件选错 | 确认选的是STM32F103C8/ZET6 |
ST-Link Utility是个好工具,能直接看芯片的Flash内容、选项字节、读保护状态。如果芯片被锁了,用ST-Link Utility的“Target -> Option Bytes”把Read Out Protection改成Disabled,然后全片擦除。
4. 进阶外设实操:定时器、超声波和USB虚拟串口
4.1 定时器模式:PWM输出和输入捕获测频率
F103有4个通用定时器(TIM2-TIM5)和2个高级定时器(TIM1、TIM8)。通用定时器做PWM输出,配置步骤:开时钟、配GPIO为复用推挽、配时基结构体(预分频、自动重装载值、计数模式)、配输出比较结构体(PWM模式、占空比、极性)、使能。
预分频和重装载值的计算:假设系统时钟72MHz,你要1kHz的PWM,频率公式是72M / ((prescaler+1) * (period+1)) = 1000。取prescaler=71,则period=999。占空比用TIM_SetCompare1(TIM3, value)设置,value从0到999对应0%到100%。
输入捕获测频率:超声波模块的Echo引脚接定时器通道,配置为输入捕获模式,上升沿触发。捕获到上升沿时记录CCR1值,下降沿时再记录一次,两次差值乘以计数周期就是高电平时间。热搜里“stm32定时器捕获测频率”和“stm32超声波测距”就是这个套路。注意输入捕获的中断里要清除标志位,否则会反复进中断。
4.2 USB虚拟串口:F103的USB设备开发
F103C8T6有USB外设,但只能做全速设备(12Mbps)。做虚拟串口(CDC)需要ST的USB库,或者用CubeMX生成。热搜里“stm32 如何做usb设备”和“stm32 usb虚拟串口发送数据”是很多人的痛点。
关键点:USB时钟必须是48MHz。F103的USB时钟来源是PLL,PLL输出72MHz,USB预分频1.5分频得到48MHz。CubeMX里把USB时钟源选PLL,然后确认时钟树里USB那栏显示48MHz。
CDC虚拟串口的代码结构:USB中断处理、CDC类请求处理、数据收发端点。ST的USB库有点老,但能用。如果你用HAL库,CubeMX勾选USB Device和CDC,生成代码后直接调用CDC_Transmit_FS发送数据。注意发送前要判断上一次发送是否完成,否则数据会丢。
4.3 超声波测距的完整实现
HC-SR04超声波模块,Trig接GPIO输出,Echo接GPIO输入或定时器输入捕获。用GPIO轮询的方式简单但精度低:
GPIO_SetBits(GPIOA, GPIO_Pin_1); delay_us(10); GPIO_ResetBits(GPIOA, GPIO_Pin_1); while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0); TIM_SetCounter(TIM2, 0); while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 1); uint16_t time = TIM_GetCounter(TIM2); float distance = time * 0.017;0.017是声速340m/s除以2再换算成cm/us的结果。实际用的时候要加温度补偿,声速随温度变化,v = 331.4 + 0.6 * T。
用输入捕获的方式精度更高,但配置复杂。Echo接TIM的通道,上升沿捕获时清零计数器,下降沿捕获时读CCR值。注意捕获中断里要切换触发极性。
5. 常见问题速查与避坑经验
5.1 编译报错和烧录问题的快速定位
| 报错信息 | 原因 | 解决 |
|---|---|---|
| undefined symbol | 库文件没加或路径不对 | 检查C/C++选项卡的Include Paths |
| Flash Download failed | 调试器配置错 | Options -> Debug -> Settings里选对Flash算法 |
| No target connected | SWD线序错 | 确认SWDIO、SWCLK、GND、3.3V对应 |
| HardFault_Handler | 空指针或数组越界 | 用调试器看LR和PC寄存器 |
| 程序跑飞 | 中断优先级冲突 | 检查NVIC分组和抢占优先级 |
HardFault是新手最怕的,其实定位方法很简单:在HardFault_Handler里加死循环,调试时看调用栈。或者用__asm("bx lr")把LR的值打出来,查LR指向的地址。
5.2 标准库和HAL库混用的坑
标准库和HAL库不能混在一个工程里,因为它们的寄存器定义和中断向量表结构不一样。你如果从网上拷了一个标准库的LED代码,又用CubeMX生成了HAL库的初始化,编译能过但运行必挂。判断方法:看stm32f10x.h和stm32f1xx_hal.h是否同时存在。
5.3 开发板挂载Ubuntu和网络更新程序
热搜里“开发板挂载ubuntu”和“zu开发板怎么通过网络更新程序”跟F103关系不大,那是Zynq和ZU系列的事。F103没有MMU,跑不了Linux。如果你想学Linux开发板,F103只能当个协处理器,通过串口或SPI跟主控通信。热搜里“k210与stm32通讯”就是这个场景,K210跑视觉,F103跑控制,两者串口对接。
网络更新程序对F103来说,要么用IAP(在应用编程),通过串口或CAN接收固件包,写到Flash的APP区;要么加个ESP8266模块,通过WiFi下载固件。IAP的关键是中断向量表偏移,APP区的代码要重映射中断向量:
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000);0x4000是APP区的起始地址偏移,具体看你的Flash分区。
5.4 基于STM32的毕业设计选题建议
热搜里“基于stm32的毕业设计”和“stm32项目”是学生党刚需。F103能做的毕设方向:智能家居(温湿度+继电器+WiFi)、平衡车(MPU6050+PID+电机驱动)、鱼缸控制器(热搜里“stm32鱼缸”)、报站系统(热搜里“stm32报站程序完整代码”)。选题目注意三点:工作量够、有实物演示、代码自己能讲清楚。别选太偏的,答辩老师问“你这个创新点在哪”,你说“用了STM32”,那基本就凉了。
6. 从F103到更复杂的开发板,学习路径怎么走
F103玩熟了之后,下一步通常是F407(Cortex-M4,带FPU和DSP指令)或者H750(M7,480MHz)。再往上就是Linux开发板,比如imx6ull、T113、RK3588。热搜里“3588开发板”和“t113开发板”就是这类。从单片机到Linux的跨度在于:操作系统、设备树、驱动模型、多进程。你在F103上写的裸机代码,在Linux里就是一个字符设备驱动。
如果对无线感兴趣,ESP32-S3是个好跳板。热搜里“esp32-s3开发板硬件解析与lorawan实战指南”就是LoRaWAN方向。ESP32-S3自带WiFi和蓝牙,双核240MHz,用ESP-IDF开发,跟STM32的HAL库思路类似但API更现代。
RoboMaster开发板C型(热搜里“robomaster开发板c型数据手册”)是F407为主控,跑RT-Thread或FreeRTOS,适合学实时操作系统。从裸机到RTOS的关键是理解任务调度、信号量、消息队列。你在F103上可以用FreeRTOS练手,C8T6的20KB RAM跑FreeRTOS够用,但任务别开太多,3个以内。
我个人在实际操作中的体会是,F103最大的价值不是性能,而是让你把嵌入式的基本概念都踩一遍:时钟树、中断、DMA、ADC、通信协议。这些概念在更高级的芯片上是一样的,只是寄存器名字变了。所以别急着换板子,把F103的每个外设都手写一遍寄存器操作,比用CubeMX生成十遍代码都有用。最后再分享一个小技巧:买板子的时候顺手买一个ST-Link V2调试器和一个USB转TTL模块,这两个东西能帮你省下大量排查硬件问题的时间。