
简介这是一份面向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 中断可以设预分频PSC71自动重载值ARR999这样定时器计数频率为 1MHz每隔 1000 次计数产生更新事件。PWR 例程通过进入 STOP 模式降低功耗唤醒时通常配合外部中断或 RTC 闹钟。需要注意 STOP 模式下引脚输入保持但输出引脚的电平会被锁存唤醒后要重新初始化需要变化的引脚。WFI和WFE的区别也常在这类例程里出现WFI必须等一次中断唤醒WFE遇到事件线会立即返回。如果同时使用串口和低功耗建议进入 STOP 前先把 USART 时钟关掉并失能接收中断否则任何输入线上的噪声都可能把芯片唤醒。晶振匹配电容对定时器精度影响很大。外部主晶振标注负载电容 CL 时匹配电容约为2*CL减去引脚寄生电容约 5pF18pF 负载电容常用 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()执行后整页数据变为 0xFFFLASH_ProgramWord按 32 位写入如果传入地址不对齐会失败。工程里经常把存储区放到 0x0803F800这个地址位于 F103 高密度型号的最后一页。自己改地址时确认链接脚本没有把只读数组分配到同一区域否则调试时会遇到“烧写一次代码、运行一次后被覆盖”的诡异现象。3.2 NOR 和 NAND 的工作原理差异外部扩展存储常见的是 SPI NOR Flash例如 W25Q64、W25Q128通过 SPI 接口挂在 SPI1 或 SPI2。NOR 与 NAND 的读写模式差异很大NOR 支持按字节/半字访问可以直接映射到地址空间执行代码NAND 按页读写、按块擦除容量大但必须处理坏块和 ECC。指标内部 FlashNOR FlashNAND Flash最小可写单元字节按字编程字节/半字页常见 2048B64B最小擦除单元页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 换成 STM32F103RCT6Flash 起始地址都是 0x08000000但容量从 64KB 变成 256KB算法文件也要从低密度换成高密度包。在 STM32CubeMX 里更改单片机型号后必须重新生成工程并更新 Keil 的 Device否则编译能过下载仍然调用旧型号算法。有些电脑装过 Keil5 的 C51 和 STM32 两个支持包算法列表混入旧的 8051 器件直接手动选择STM32F10x Med-density Flash或STM32F10x 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 卡、VS1003I2CSCL/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采样点在(18)/1275%。标准外设库对应写为CAN_BS1_8tq、CAN_BS2_3tq、CAN_Prescaler6。两个节点要通信除了波特率一致还必须保证过滤器掩码匹配。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 以内并关掉串口助手的十六进制自动格式化显示。本文还有配套的精品资源点击获取