STM32标准库例程包:从GPIO到CAN的寄存器级驱动详解
2026/9/16 13:40:00 网站建设 项目流程

简介:这是一份面向STM32单片机开发者的常用例程源码合集,覆盖外部中断与串口、TIM定时器、RS485/CAN通信、GPIO控制、PWR电源管理、系统时钟SysTick、Flash存储(NAND/NOR)、I2C、RTC实时时钟、AD采集与串口输出等典型外设场景,并附带VS1003与MP3 Player语音模块驱动、USB Bootloader等进阶工程,适合嵌入式入门学习、日常调试及项目原型验证。压缩包共3921个文件,以1690个C源码、1095个头文件、912个汇编文件为主,配合Keil工程文件、Hex烧录文件、PDF文档与脚本工具,整体28.41MB,目录按功能模块组织,便于按需检索。目前已有445人学习下载,这套工程级例程可直接编译参考,能帮助开发者快速掌握外设初始化、通信协议实现与存储读写流程,显著缩短开发排错时间。

1. 这套 STM32 例程包,能帮你把外设驱动一次认全

接触 STM32 时,与其在 HAL 库的抽象层里绕圈子,不如先翻一遍基于寄存器或标准外设库的例程源码。这个包里不仅有 GPIO 点灯、TIM 计数、外部中断这类入门驱动,还覆盖了 RS485、CAN、I2C、实时时钟 RTC、AD 采集以及 Flash 存储与串口通信的联动场景;工程目录里的 os_cpu_a.asm、lib_mem_a.asm 更是带着 uC/OS-II 移植文件的 RTOS 骨架。对刚转嵌入式的新手,能快速建立“外设时钟—引脚复用—数据流”的完整心智模型;对 5 年以上开发者,也可以拿它当现成的波特率、分频系数和 Flash 操作时序参考。

2. GPIO 八种工作模式与中断、定时器例程的实测边界

2.1 GPIO 的八种模式,先对照模式表选型

GPIO 的所有行为都建立在输入/输出方向之上,STM32 把它细分成了八种工作模式。例程包里 GPIO 实例对每个模式都留了入口:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、复用推挽、复用开漏。选错模式的典型症状是电平不对、漏电偏大或者串口乱码,所以下载下来先不要急着改代码,对照硬件图纸确定每个引脚该用什么模式。

GPIO 模式典型用途使用注意
GPIO_Mode_AINADC 采样引脚关闭数字输入缓存,避免灌电流
GPIO_Mode_IN_FLOATING外部按键、红外接收悬空时电平不定,长线易受干扰
GPIO_Mode_IPU机械按键、开漏总线内部上拉约 30~50kΩ
GPIO_Mode_IPD电平检测外部短路到地时功耗增加
GPIO_Mode_Out_PPLED、蜂鸣器可输出高电平,拉电流能力较弱
GPIO_Mode_Out_ODI2C、电平转换必须外接上拉电阻
GPIO_Mode_AF_PPUSART、SPI、CAN 等输出输出方向由外设控制
GPIO_Mode_AF_ODI2C 复用、MCO仍需外部上拉

标准外设库的初始化代码结构是固定的,关键在于 RCC 时钟和结构体成员。下面这段配置把 PB0 作为输入上拉:

#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void BSP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // GPIOB 和 AFIO 时钟必须同时打开,外部中断和重映射依赖 AFIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 按键,输入上拉 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输入模式其实不关心速度 GPIO_Init(GPIOB, &GPIO_InitStructure); }

这里GPIO_Speed只在输出模式下有实际意义,输入模式不关心翻转速度,但标准库要求结构体先完整初始化,否则GPIO_Init会读取到未赋值的成员。开启 AFIO 时钟是因为后面要用 EXTI,如果不做重映射,很多人会漏掉这一句。读取按键电平只需要GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0),配合按键弹起检测,就能做成单击、长按。

2.2 外部中断 EXTI 与去抖的关键判断

外部中断线的编号和引脚编号是对应关系,比如 PB0 只能映射到 EXTI0。例程包里一般先把 GPIO 设为输入,再通过EXTI_InitTypeDef设置中断线、触发模式和中断使能。一个高频失败点是只开了 GPIO 时钟,没开 AFIO 时钟,导致中断服务函数永远不会执行。另一个失败点是中断标志没清除,程序一直驻留在中断里,看起来像死机。

void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); g_ext_flag = 1; // 不在这里做业务,只置位标志 } }

服务程序里不要调用HAL_Delay或长循环,正确做法是记录g_ext_flag,在主循环里消费。机械按键按下时会有约 5~20ms 的抖动,如果直接读引脚,会看到高低电平反复跳变。务实方案是在中断里记录当前 tick,主循环比较相邻两次触发间隔,小于 20ms 的连续触发只算一次。外部中断的边沿选择也要看外围电路:按键接上拉到电源,按下接地,用下降沿触发;如果外部设备输出的是低有效的下拉信号,则用上升沿。

2.3 TIM 定时器例程和 PWR 低功耗的配合

TIM 例程通常用来做毫秒级时基或 PWM 输出。STM32 的 APB1 定时器时钟有个特殊规律:只要 APB1 预分频不为 1,定时器时钟就是 APB1 的 2 倍。比如 72MHz 主频、APB1 预分频为 2 时,TIM2 到 TIM7 的时钟是 72MHz,不是 36MHz。要出 1ms 中断,可以设预分频PSC=71,自动重载值ARR=999,这样定时器计数频率为 1MHz,每隔 1000 次计数产生更新事件。

PWR 例程通过进入 STOP 模式降低功耗,唤醒时通常配合外部中断或 RTC 闹钟。需要注意 STOP 模式下引脚输入保持,但输出引脚的电平会被锁存,唤醒后要重新初始化需要变化的引脚。WFIWFE的区别也常在这类例程里出现:WFI必须等一次中断唤醒,WFE遇到事件线会立即返回。如果同时使用串口和低功耗,建议进入 STOP 前先把 USART 时钟关掉并失能接收中断,否则任何输入线上的噪声都可能把芯片唤醒。

晶振匹配电容对定时器精度影响很大。外部主晶振标注负载电容 CL 时,匹配电容约为2*CL减去引脚寄生电容(约 5pF),18pF 负载电容常用 30~33pF 的贴片电容。如果晶振实际起振频率偏差大,TIM 的 1ms 时基会随温度漂移,最终导致串口波特率误差超过 2%,接收端出现偶发乱码。

3. Flash 存储例程:内部 Flash、NOR 与 NAND 的差异和下载失败排错

3.1 内部 Flash 的按页擦写流程

STM32 内部 Flash 掉电不丢数据,最小擦除单位是页而不是字节。这决定了即使只想改 2 字节,也必须先把整个页读出来,在缓冲区里修改,再擦除整页后重新写回。例程里的 Flash 存储代码一般按“解锁—清标志—整页擦除—逐字写入—加锁”执行。

#define FLASH_TEST_ADDR 0x0803F800 // 最后一页,防止覆盖代码区 uint8_t Flash_WritePage(uint16_t page_offset, uint32_t *buffer, uint16_t count) { uint32_t addr = FLASH_TEST_ADDR + page_offset * FLASH_PAGE_SIZE; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); if (FLASH_ErasePage(addr) != FLASH_COMPLETE) { FLASH_Lock(); return 1; } for (uint16_t i = 0; i < count; i++) { if (FLASH_ProgramWord(addr + 4 * i, buffer[i]) != FLASH_COMPLETE) { FLASH_Lock(); return 2; } } FLASH_Lock(); return 0; }

代码说明:FLASH_Unlock()必须写入 KEY1 和 KEY2,否则后续操作会返回写保护错误;FLASH_ErasePage()执行后整页数据变为 0xFF;FLASH_ProgramWord按 32 位写入,如果传入地址不对齐会失败。工程里经常把存储区放到 0x0803F800,这个地址位于 F103 高密度型号的最后一页。自己改地址时,确认链接脚本没有把只读数组分配到同一区域,否则调试时会遇到“烧写一次代码、运行一次后被覆盖”的诡异现象。

3.2 NOR 和 NAND 的工作原理差异

外部扩展存储常见的是 SPI NOR Flash,例如 W25Q64、W25Q128,通过 SPI 接口挂在 SPI1 或 SPI2。NOR 与 NAND 的读写模式差异很大:NOR 支持按字节/半字访问,可以直接映射到地址空间执行代码;NAND 按页读写、按块擦除,容量大,但必须处理坏块和 ECC。

指标内部 FlashNOR FlashNAND Flash
最小可写单元字节(按字编程)字节/半字页(常见 2048B+64B)
最小擦除单元页(1KB~2KB)扇区(4KB 起)块(128KB 左右)
是否可直接执行
坏块处理一般无需要出厂坏块表
写寿命约 1 万次约 10 万次约 10 万次

NAND Flash 工作原理的核心在地址序列:命令、列地址、行地址依次发送到 I/O 总线,随后 MCU 轮询状态寄存器里的RDY/Busy位。例程包里的 NAND 驱动如果没有 ECC,建议只把数据区当掉电保存日志用,不要承载需要高可靠的文件系统。我一般会在写入每个页后回读,并把坏块标记记录在该块第一个备用区为 0xFF 的位置。外部 SPI NOR 的烧录,MCU 通过 SPI 命令先写使能,再传输页缓冲数据,最后等待内部写周期结束,这和使用 SP Flash Tool 这类通用工具给 NOR 芯片离线下载的流程是同一套思想。

3.3 Flash download failed 的排查步骤

Keil 下载程序时常见的报错是Error: Flash Download failed - Target DLL has been cancelled。这条消息的直接原因是调试器给下载算法发送指令后,算法没有在预期时间内完成握手。先不要怀疑电源,绝大多数情况是芯片型号或下载算法配置不匹配。

排查顺序:先用 J-Link 或 ST-Link 的连接测试功能读 IDCODE,读不到说明 SWDIO/SWCLK 接反、目标板没有独立供电,或者 BOOT0 被拉高导致芯片停在系统存储器。能连上但一写就报Target DLL has been cancelled,打开 Keil 的 Debug 设置,在 Flash Download 页检查算法列表里有没有当前芯片对应的 FLM 文件,起始地址和容量要和所选型号一致。比如从 STM32F103C8T6 换成 STM32F103RCT6,Flash 起始地址都是 0x08000000,但容量从 64KB 变成 256KB,算法文件也要从低密度换成高密度包。在 STM32CubeMX 里更改单片机型号后,必须重新生成工程并更新 Keil 的 Device,否则编译能过,下载仍然调用旧型号算法。有些电脑装过 Keil5 的 C51 和 STM32 两个支持包,算法列表混入旧的 8051 器件,直接手动选择STM32F10x Med-density FlashSTM32F10x High-density Flash就能解决。

4. USART、RS485 与 CAN、I2C、RTC 通信例程的参数配置

4.1 一张表理清 UART/SPI/I2C/CAN 的选型

例程包把常用接口放在一起,最容易出错的是选型理解。USART 适合点对点全双工调试;RS485 是半双工差分信号,适合多节点工业总线;I2C 适合机内短距离挂传感器;SPI 适合大数据量读写 Flash 或音频模块;CAN 则用报文 ID 管理多设备优先级,适合实时控制。

总线信号线半双工/全双工常见速率典型场景
USARTTX/RX 2 根全双工9600~921600 bps调试、GPS、蓝牙
RS485A/B 2 根半双工9600~10Mbps变频器、仪表
SPI4 根 +CS全双工最高几十 MbpsFlash、SD 卡、VS1003
I2CSCL/SDA 2 根半双工100k/400k/1MEEPROM、温湿度
CANCANH/CANL 2 根半双工5k~1Mbps汽车、电机控制

4.2 RS485 半双工 Modbus 帧接收状态机

RS485 例程和 USART 例程最大的不同是方向控制。发送前必须把 DE 引脚拉高,最后一个字节移位结束后延时一位再拉低,否则对端会收到回波或者错误帧。如果直接套用 USART 中断处理,通常会漏字节,问题不在波特率,而在方向切换太快。

Modbus RTU 用 3.5 个字符间隔区分帧,9600 波特率下约 4ms。下面是适合移植到串口接收中断里的状态机,覆盖了热词“modbus 单片机帧接收数据程序”的实际场景:

#define MODBUS_ADDR 0x01 #define MODBUS_FUNC 0x03 #define FRAME_MAX 64 uint8_t frame_buf[FRAME_MAX]; uint8_t frame_len = 0; uint8_t frame_ready = 0; uint32_t last_rx_tick = 0; void Modbus_UART_ByteHandler(uint8_t byte) { uint32_t now = SysTick_GetTick(); if (now - last_rx_tick > 4) // 超时 4ms,认为上一帧结束 frame_len = 0; last_rx_tick = now; if (frame_len < FRAME_MAX) frame_buf[frame_len++] = byte; if (frame_len == 8) { if (frame_buf[0] == MODBUS_ADDR && (frame_buf[1] == MODBUS_FUNC || frame_buf[1] == 0x06)) frame_ready = 1; // 主循环处理完再清零 else frame_len = 0; // 地址不匹配,直接丢弃 } }

逻辑说明:SysTick_GetTick()返回毫秒计数,now - last_rx_tick使用无符号减法,即使 tick 回绕也能正确计算差值。判断frame_len == 8是因为 Modbus RTU 最短有效帧是 8 字节:地址、功能码、数据加上两个 CRC 字节。地址不匹配时直接清空,避免脏数据占据缓冲区。接到变频器场景时,通常用 03H 读频率、06H 写启停,帧尾必须追加 CRC16-MODBUS 校验。这个状态机去掉地址匹配限制后,也能用在 K210 与 STM32 的串口透传改造中。

4.3 CAN 初始化参数:波特率要靠位时间拆解

CAN 例程里,波特率不是直接给数值,而是由预分频器和位时间组成。假设 APB1 时钟 36MHz,目标波特率 500kbps,那么总时间量子必须是36M / 500k = 72个 tq。把 72 分成同步段、BS1、BS2,常见取法是1 + 8 + 3,也就是同步段 1tq、采样点前 8tq、采样点后 3tq,采样点在(1+8)/12=75%

标准外设库对应写为CAN_BS1_8tqCAN_BS2_3tqCAN_Prescaler=6。两个节点要通信,除了波特率一致,还必须保证过滤器掩码匹配。CAN_H 和 CAN_L 接反时,示波器能看到差分波形,但接收端一直不 ACK;网络两端各加一个 120Ω 终端电阻,否则高速率下波形反射明显。例程包里的 CAN 收发实例一般会回显报文 ID 和数据,验证时先用回环模式检查 MCU 内部链路,再切换到正常模式接外部设备。

4.4 I2C、RTC 和外部音频模块的边界

I2C 例程和 RTC 实时时钟经常一起出现。I2C 总线的两根线必须有上拉电阻,如果测到 SCL/SDA 为低电平,多半上拉没接。内部上拉只能用于低速场景,400kHz 通信建议外接 4.7kΩ,长线时换 2.2kΩ,并在主机端各串联 33Ω 电阻抑制振铃。

RTC 例程使用外部 32.768kHz 晶振时,匹配电容一般在 6pF~12.5pF 之间,计算方式和主晶振一样。如果 RTC 一天漂移 5 秒以上,先排除负载电容和温度影响,再考虑软件校准。例程打印的日历时间来自RTC_GetCounter(),它只是秒计数器,年份月份需要自己换算,不要直接当 UNIX 时间戳用。

VS1003 语音模块和 MP3 Player 模块本质上是 SPI 控制的音频芯片。VS1003 的 SPI 时钟不能超过模块额定值,发送音频数据时最好每个 512 字节分组之间插入短暂延迟,避免 FIFO 溢出。同时跑音频和 CAN/RS485 时,把 SPI 相关 DMA 优先级调低,否则 CAN 报文会错过发送窗口。包里的 STM32 USB Bootloader 例程值得单独研究:跳转到用户程序前,要先检查栈顶地址是否落在 RAM 范围内,再检查复位向量是否为浮动向量的有效范围,否则固件烧坏了会直接进 HardFault。

5. SysTick 延时、AD 采集与串口上报:一个可复现的最小链路

5.1 SysTick 延时函数,把节拍变量做成全局

系统时钟节拍是整个例程包最容易被忽略的前提。使用标准外设库时,SysTick_Config(SystemCoreClock / 1000)会开启 1ms 中断。在中断里只累加一个全局变量,不要在中断里调用用户业务代码。

volatile uint32_t g_tick_ms = 0; void SysTick_Handler(void) { g_tick_ms++; } void DelayMs(uint32_t ms) { uint32_t start = g_tick_ms; while (g_tick_ms - start < ms); // 无符号减法,溢出不乱跳 }

这里用差值比较而不是绝对值,即使g_tick_ms从 0xFFFFFF00 回绕到 0,只要ms小于 2^32,延时依然准确。这个节拍变量在 RS485 和 Modbus 例程里就是判断 3.5 字符超时的基准,在串口调试中也可以打印。

5.2 ADC 采集到串口上报,重点在采样时间

AD 采集例程把 ADC 通道配置成模拟输入后,最关键的是ADC_SampleTime。信号源阻抗越高,采样时间越要拉长。比如 NTC 经过 10kΩ 分压进入 ADC,采样时间太短会让采样电容充不满,读数偏低。直接设置为ADC_SampleTime_239Cycles5会更稳。

ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); uint16_t value = ADC_GetConversionValue(ADC1); printf("ch1=%d\r\n", value);

printf如果走重定向,确认fputc实现用的是目标串口。串口助手停止显示时,先检查 RS485 半双工模式下 DE 引脚是不是停留在发送状态。软件滤波建议在主循环里做 10 次均值,不要在中断里循环累加,否则会拖慢中断响应。提高吞吐量时改用 DMA 循环模式,主程序只读缓冲区尾部。

验证这条链路最直接的方法是把电位器接到 ADC 引脚,转动时串口读数应从 0 平滑变化到 4095。由于 VREF+ 直接来自 MCU 电源,末位抖动 1~2 个 LSB 是正常的,超过 20 个 LSB 就要查电源纹波。用 USB 转串口调试时,波特率升到 460800 以上容易丢包,建议留在 115200,线材长度控制在 30cm 以内,并关掉串口助手的十六进制自动格式化显示。

本文还有配套的精品资源,点击获取

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

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

立即咨询