简介:STM32F10x官方例程是一套由意法半导体提供的嵌入式开发示例集合,面向使用ARM Cortex-M3内核的STM32F10x系列微控制器的开发者与学习者。资源系统覆盖GPIO、定时器、ADC/DAC、USART/SPI、I2C、USB、CAN、DMA、RTC、EXTI及FFT等常见外设模块,每个示例均含可编译工程与配置流程,便于对照寄存器库理解芯片工作机制。压缩包共947个文件,以C源文件(348个)、头文件(275个)、汇编启动文件(44个)及txt说明文档(114个)为主,同时包含链接脚本、工程配置文件与CHM帮助文档,整体约22.41MB。目前已有1496人学习下载。这套基于标准外设库V3.5.0的官方例程,既适合初级嵌入式开发者系统入门,也可作为实际项目中外设驱动移植和调试的重要参考,能帮助读者快速上手STM32F10x开发。
1. 为什么我建议你别急着扔了官方例程
前阵子帮一个刚转嵌入式的小兄弟调板子,他拿着某宝买的开发板,第一件事就是把官方例程全部删掉,理由是“代码太乱看不懂”。结果折腾了三天,GPIO 翻转都点不亮 LED。后来我让他把官方例程重新拉回来,对照着改,十分钟就通了。
这种事儿我见过太多次了。
STM32F10x 官方例程,也就是 ST 官方提供的 Standard Peripheral Library(标准外设库)配套示例代码,是学习这颗芯片最好的起点,没有之一。它解决的问题非常明确:告诉你每个外设到底怎么配置、怎么用,从最简单的 GPIO 点灯,到 USART 通信、DMA 搬运、ADC 采集、定时器中断,全都有现成可跑的工程。
这套例程适合谁?刚入坑 STM32 的新手,靠它建立“寄存器配置看不懂但 API 调用能跑通”的信心;做产品的工程师,靠它快速验证某个外设在这个型号上有没有坑;甚至资深开发者,偶尔也要翻翻官方代码,确认某个时序参数到底该怎么配。这篇文章我就把这套例程的目录结构、核心设计思路、典型外设的实现方式以及我踩过的那些坑,一次性讲清楚。
2. 官方例程的整体设计和目录结构
2.1 为什么 ST 要把例程分类成三大块
如果你下载过 STM32F10x 标准外设库(现在 ST 主推 HAL 库,但这套老库仍大量存在于存量项目和工业设备里),解压后你会看到Project、Libraries、Utilities三个核心文件夹,外加一堆说明文档。
先说Libraries,这里面装的是 CMSIS 内核头文件和标准外设驱动源码。CMSIS 是 ARM 制定的 Cortex-M 内核软件接口标准,说人话就是:不管你是 STM32 还是别的 Cortex-M 芯片,内核寄存器访问方式都统一了,这样你换芯片时底层代码不用从零写。外设驱动源码则是stm32f10x_gpio.c、stm32f10x_usart.c这类文件,每个文件对应一个外设模块,对外暴露的是一组以GPIO_Init、USART_SendData这种命名规则为主的 API 函数。
再说Project,这里放的是各个编译工具链的工程模板和官方例程。ARMCC、GCC、IAR 都有对应目录,你用什么 IDE 就打开哪个。STM32F10x 官方例程的标准命名是STM32F10x_StdPeriph_Examples,底下按外设又分了一堆子文件夹,什么ADC、DMA、GPIO、TIM、USART,每个文件夹里是一个个独立的小工程。
最后是Utilities,这块放的是 ST 官方评估板(比如 STM32F10x-EVAL)的板级驱动代码,用来操作评估板上集成的 LCD、按键、LED、音频芯片等外设。如果你用的是自己画的板子,这部分通常直接忽略,但如果你手上正好有官方评估板,这些代码能让你快速跑起一个带界面的演示程序。
2.2 例程工程里那几个关键文件的真实作用
随便打开一个官方例程的工程,比如GPIO_IOToggle,你会发现里面有一堆文件。很多新手看到头就大,其实核心就三类:
第一类是启动文件,名字一般是startup_stm32f10x_hd.s。不同的后缀代表不同容量的芯片,hd是 high-density,对应大容量型号;md是 medium-density,对应中等容量,比如最常见的 STM32F103C8T6;ld是 low-density,对应小容量。选错启动文件,程序很可能跑飞或者直接进 HardFault,这是新手最容易忽略的问题。
第二类是系统初始化文件,包括system_stm32f10x.c和stm32f10x_it.c。前者负责配置系统时钟,把外部晶振频率或者内部 HSI 时钟倍频到 72MHz;后者是所有中断服务函数的默认入口,你写中断处理逻辑就在这里加。
第三类就是你自己要改的main.c和stm32f10x_conf.h。前者是用户代码,后者是一个外设头文件的汇总配置文件,你想用哪个外设,就把对应头文件的#include注释打开,同时把源文件加入工程编译。
这里我想特别说一句:官方例程的stm32f10x_conf.h默认会把所有外设的头文件都列出来,你如果不裁剪,编译时会把整个外设库全部编译一遍,费时间不说,Flash 占用也会大不少。实际项目里建议只保留用到的那几个外设,比如只用 GPIO 和 USART,就把其他的全注释掉。
3. 核心外设例程的细节解析
3.1 GPIO 例程:一切外设操作的基本功
GPIO 例程一般长这样:初始化时钟、配置 Pin 的模式和速度、然后在主循环里翻转电平。官方例程里用的是GPIO_Init+GPIO_WriteBit这种标准库 API,代码如下:
/* 使能 GPIOC 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; 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_12); Delay(0xFFFF); GPIO_ResetBits(GPIOC, GPIO_Pin_12); Delay(0xFFFF); }这段代码看起来简单,但里面有几个值得琢磨的点。
RCC_APB2PeriphClockCmd这行是很多新手最容易漏掉的。STM32 的外设时钟默认是关闭的,你不用 RCC 先打开,直接配寄存器是无效的。这一点和 51 单片机完全不同,51 的 IO 口上电就能用,STM32 必须先开时钟。
GPIO_Mode有输入、输出、复用、模拟四种大类,输出又分为推挽和开漏。官方例程里默认用的是推挽输出,这也是绝大多数数字信号输出的选择,因为它能主动输出高电平和低电平,驱动能力也强。开漏输出则常用于 I2C 总线或者需要外部上拉的场景。
GPIO_Speed则决定了 IO 口的翻转速率,官方例程常用 50MHz。有同学问过:是不是速度配得越高越好?不是。高速模式会带来更大的 EMI 噪声和功耗,实际项目里建议按需配置,点个 LED 用 2MHz 都绰绰有余。
3.2 USART 例程:串口通信的标准姿势
USART 例程是官方例程里另外一组含金量很高的代码,因为串口是调试阶段和外界打交道的主动脉。官方例程一般分两部分:轮询方式和中断方式。
轮询方式的核心逻辑是:发送时用USART_SendData往数据寄存器丢数据,然后等USART_GetFlagStatus(USARTx, USART_FLAG_TXE)变成 SET 再发下一个;接收时则等在USART_FLAG_RXNE,一旦置位就表示收到数据了,调用USART_ReceiveData取出来。
中断方式的代码则多了一个USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)和对应的中断服务函数USART1_IRQHandler。在中断里判断中断标志位,然后接收数据。
很多人在串口例程上遇到的问题就一个:乱码。
乱码的根源一般不是代码逻辑问题,而是波特率不匹配或者时钟配置不对。官方例程默认用的外部晶振是 8MHz,倍频到 72MHz 系统时钟,然后 USART 分频器按给定波特率反算分频系数。如果你的板子上晶振不是 8MHz,比如是 12MHz 或者 25MHz,那你必须改system_stm32f10x.c里的 PLL 配置参数,否则串口波特率算出来是错的,收发必然是乱码。
我踩过最隐蔽的一个坑是:有的板子用了 8MHz 晶振,但内部 HSE 启动失败后自动切换到了 HSI(内部 8MHz RC 振荡器),而 HSI 的精度远不如晶振,串口跑到 115200 时偶发乱码。这种问题很难查,因为程序能跑、功能基本正常,就是偶尔丢字节。最后是在system_stm32f10x.c里加了 HSE 启动检测加循环等待,确保晶振起振后才继续执行。
3.3 DMA 例程:释放 CPU 的关键手段
DMA(直接存储器访问)是 STM32F10x 里非常重要的一个外设,它的核心价值就是“搬运数据不占用 CPU”。官方 DMA 例程通常会演示两件事:内存到内存的数据拷贝,以及外设到内存的数据搬运。
内存到内存的例程比较直观:定义一个源数组和一个目标数组,配置好 DMA 通道、方向、数据宽度,启动一次传输,然后等传输完成标志,最后比较两个数组内容。这类例程的价值在于让你建立对 DMA 工作流程的体感:源地址、目标地址、传输长度、传输方向、优先级,这五个核心要素是 DMA 配置的全部精髓。
外设到内存的例程则更贴近实际应用,比如 ADC 连续采样,通过 DMA 把采样结果自动搬到内存缓冲区,这样 CPU 完全不用管 ADC 数据的读取,直接处理数组里的数据就行。官方例程里这个例程叫做ADC_DMA,我建议每个学 STM32 的人都亲手跑一遍,它打通了“模拟信号 -> 数字量 -> 内存数据”这条完整链路,对理解整个系统的数据流非常有帮助。
不过 DMA 例程有个注意点:STM32F10x 的 DMA 有 DMA1 和 DMA2 两个控制器,每个控制器下有多个通道,每个通道能服务的外设是固定的。比如 USART1 的接收只对应 DMA1_Channel5,你配到其他通道是触发不了的。官方例程里虽然把映射关系写进去了,但很多新手不看注释直接照抄,换了个外设就发现 DMA 不工作,其实是通道映射错了。
4. 实操过程:从零跑通一个官方例程工程
4.1 从下载到编译烧录的完整链路
我以最常见的 MDK-ARM(Keil)环境为例,讲一下怎么把一个官方例程工程跑起来。
第一步,下载标准外设库。去 ST 官网搜STM32F10x standard peripheral library,下载 zip 包解压。国内网络环境不好的话,可以找一些镜像站或者第三方托管,不过建议还是尽量以官网版本为准,避免被二次修改过的库坑到。
第二步,找到例程工程。以最常用的GPIO_IOToggle为例,路径是STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Examples\GPIO\GPIO_IOToggle\MDK-ARM,里面的GPIO_IOToggle.uvproj就是 Keil 工程文件,双击打开。
第三步,选择芯片型号。打开工程后,在 Options for Target 里把 Device 选成你实际使用的型号。很多同学用的最小系统板是 STM32F103C8T6,但官方工程默认可能是 STM32F103ZE,这里要改掉。不改的话启动文件、Flash 大小、RAM 大小都对不上,编译能过,下载后大概率跑不起来。
第四步,确认调试器配置。在 Debug 选项卡里选择你手上实际的调试器,比如 ST-Link 或者 J-Link,同时在 Utilities 选项卡里勾选 Reset and Run,这样烧录后程序会自动复位运行,省去手动复位的麻烦。
第五步,编译下载。点击 Build,确认没有错误,然后 Load,程序就进芯片了。
到这里你就能看到 LED 在闪烁了。别小看这个流程,我见过不少同学卡在“编译报错找不#包含文件”这一步。官方例程在工程里用的是相对路径,如果你把整个工程移动位置或者改了目录结构,头文件路径就失效了。解决办法是在 C/C++ 选项卡里的 Include Paths 里重新添加Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x等几个关键目录。
4.2 把官方例程改造成自己的工程模板
跑通一个例程只是第一步,更有价值的操作是把官方例程改造成自己项目的基础模板。我自己的做法是这样的:
第一步,复制整个示例工程目录,重命名成项目名。
第二步,删掉不需要的示例代码,保留Libraries和Project两个根目录。
第三步,在Project中新建User、Hardware、App三个文件夹,分别放主函数、板级驱动、业务逻辑。
第四步,在Hardware里写自己的外设初始化文件,比如led.c、uart.c、adc.c,每个文件配套一个同名头文件。这里的写法完全参考官方例程的风格:一个初始化函数 + 若干操作函数。
第五步,把自己写的驱动源文件加入工程编译,同时在stm32f10x_conf.h里只保留需要的外设头文件。
这个模板一旦建立起来,后面写任何功能就都是在往上添砖加瓦,而不是每次从零敲寄存器。我自己的经验是:一套精心维护的模板,能让新项目的启动时间从两天压缩到两个小时。
4.3 官方例程在不同 Compiler 下的迁移要点
除了 Keil,还有不少开发者用 IAR 或者 GCC 工具链。官方例程虽然三种工具链的工程都给了,但实际用起来有个明显的差异点需要留意,就是启动文件里的堆栈配置和编译器指令。
比如 Keil 工程里的启动文件由 ARMCC 的汇编器编译,IAR 工程里的启动文件是 IAR 格式,两者不能通用。如果你在 Keil 里写好了代码,换到 IAR 编译,必须切换成 IAR 的启动文件,否则第一行汇编就报错。
GCC 工具链用的人相对少一些,一般配合 Makefile 或者 CMake 使用,官方例程里没有为 GCC 提供现成的链接脚本,需要你自己写.ld文件。不过现在很多人直接用 STM32CubeMX 生成 GCC 工程,配合 HAL 库或者 LL 库,已经不需要手写链接脚本了。
我的建议是:学习阶段专心用一套工具链,吃透官方例程的代码逻辑;生产阶段再根据自己的团队技术栈选择迁移方案。频繁切换工具链会浪费大量时间在环境配置上,对学习本身没有太大帮助。
5. 官方例程的常见问题与排查技巧
5.1 例程编译报错的头文件路径问题
官方例程最经典的问题就是编译报fatal error: stm32f10x.h: No such file or directory。
它的根源在于 Keil 的头文件搜索路径配置里,官方用的是相对路径,一旦工程被移动或者从压缩包直接解压到中文路径下,相对路径失效,头文件就找不到了。
排查思路很简单:先在工程目录下确认stm32f10x.h实际位于哪个文件夹,然后在 Options for Target -> C/C++ -> Include Paths 里添加一个绝对路径,指向该目录。同时也要把Libraries\CMSIS\CM3\CoreSupport和DeviceSupport下的路径加进去,因为内核头文件和设备头文件不在同一个目录。
另外提醒一点:工程路径不要有中文,也不要带空格。如果你把工程放在D:\我的项目\STM32 测试\这种目录下,编译器对中文和空格的支持在某些版本下是有问题的,会报一些莫名其妙找不到文件的错误。
5.2 例程烧录后无法运行的可能原因
如果你把官方例程烧录进芯片后,发现 LED 不闪、程序像死了一样,可以从这几个方向排查,按优先级从高到低:
第一,启动文件选错。这是最常见的。STM32F103C8T6 是 64KB Flash,对应中容量,用startup_stm32f10x_md.s;如果你的芯片是 STM32F103ZET6,是 512KB Flash,对应大容量,用startup_stm32f10x_hd.s。启动文件里的向量表偏移量是和 Flash 容量挂钩的,选错了直接跑飞。
第二,时钟启动失败。如果你的板子上晶振坏了或者没焊,HSE 起振失败后,如果代码里没有做超时检测,程序会卡在while循环里。这个时候用调试器暂停程序,看 PC 指针停在哪里,如果是停在system_stm32f10x.c里的某个while,那就是时钟问题。
第三,调试器连接不上。这个倒好判断,把 BOOT0 拉高后再连接,将芯片强制进入系统存储器模式,就能绕过用户程序连上调试器,然后擦除 Flash 再查问题。BOOT0 拉高这个操作是嵌入式调试里的万能恢复手段,一定要记牢。
5.3 官方例程和实际项目的差距到底在哪
明确了官方例程的价值和使用方法以后,我还想泼一盆冷水。官方例程的代码风格是“教学式”的,它要保证任何时候读代码的人都能看懂,因此牺牲了不少工程化特性。
最典型的一点:官方例程里大量使用轮询等待。比如我上面贴的 GPIO 翻转代码,主循环里用了软件延时,循环期间 CPU 什么都干不了。这在教学里没问题,但在真正的产品里,主循环往往要处理多个任务,任何一个阻塞延时都是灾难。
再比如,官方例程的错误处理普遍很薄弱。很多外设例程里,数据发送失败、接收超时、DMA 传输错误,处理方式要么是死等,要么是直接忽略。实际项目里这些异常都必须有合理的返回值和错误处理机制,否则系统一运行就暴露各种隐性 bug。
所以我的建议是:官方例程是你学习外设工作原理的教科书,但它不是你的产品代码。正确的方法是吃透官方例程的代码逻辑,理解每个配置项的作用,然后根据自己项目的需求写一套精简、有错误处理、有合理时间片分配的外设驱动代码。我在实际项目中一直用的就是这套策略——先跑官方例程验证外设没有硬件问题,再写进自己的驱动框架。这个流程走下来,开发稳定高效,排查问题的时候也容易定位。
最后再分享一个小技巧。官方例程里每个外设文件夹下都有一个readme.txt,里面有这个例程的详细说明:演示了什么功能、依赖哪些引脚、预期效果是什么。很多人从下载到放弃都没点开过这个文件。其实它比网上 90% 的教程写得都清楚,尤其是引脚连接关系和硬件配置要求,照着接线基本不会出错。
本文还有配套的精品资源,点击获取