简介这份资源是面向STM32初学者的开发参考例程代码集基于ARM Cortex-M3内核的STM32F103C8T6微控制器帮助开发者从零理解并上手该芯片的硬件资源与编程方法。内容覆盖ADC采样配置、GPIO输入输出与中断、UART串口收发、按键检测与消抖、SPI主从通信、SysTick及通用定时器应用并包含UCOS实时操作系统的任务调度、信号量与互斥锁等进阶示例由浅入深串联各功能模块。压缩包共1616个文件以c与h源码、工程配置及编译中间文件为主另有hex烧录文件、uvproj工程文件与说明文档整体约16.54MB目录按模块划分便于检索。已有9168人学习下载适合边调试边对照例程理解外设初始化流程与中断处理思路为实际项目开发积累可复用的代码基础。1. 拿到一份 STM32F103C8T6 例程包先别急着烧录某次帮同事排查一块“点灯不亮”的板子代码是从一份 STM32F103C8T6 开发参考例程里直接拷的编译零警告烧录也成功可 PA5 上的 LED 就是不亮。最后发现是例程默认按 8MHz 外部晶振配置时钟树而那块板子焊的是 12MHz 晶振系统时钟跑飞延时函数全乱。这件事说明一个道理STM32F103C8T6 的参考例程代码价值不在于“能编译”而在于它把时钟、外设初始化、中断向量这些容易翻车的环节摊开给你看。这份资源就是围绕这颗 48 脚、64KB Flash、20KB RAM 的 Cortex-M3 芯片按外设分模块组织的 C 语言例程集合覆盖 GPIO、串口、定时器、ADC、I2C、SPI 等常见场景。适合刚上手 STM32 的嵌入式开发者照着改也适合熟手拿来当寄存器配置的对照表。下面按“先看懂结构、再跑通最小系统、最后避开坑”的顺序拆。2. 例程包的目录结构与工程骨架先认清哪块能动拿到压缩包解压后第一件事不是打开 Keil 点编译而是把目录树看一遍。这类参考例程通常按“外设名 功能”分文件夹每个文件夹是一个独立工程而不是一个大工程里塞几十个 main。认清结构能省掉后面大量“改了没反应”的时间。2.1 典型目录分层与各层职责一份组织得比较规范的 STM32F103C8T6 例程包常见分层是这样的层级典型目录/文件职责改动频率启动层startup_stm32f10x_md.s复位向量、堆栈初始化、跳转到 main几乎不动内核层core_cm3.c/hCortex-M3 内核寄存器访问不动器件层stm32f10x.h、system_stm32f10x.c寄存器定义、SystemInit 时钟初始化谨慎改标准外设库stm32f10x_gpio.c、stm32f10x_rcc.c 等外设驱动函数不动板级配置board.c/h 或 bsp_xxx.c引脚定义、外设参数宏常改应用层main.c、app_xxx.c业务逻辑主要改这里这里要特别注意startup_stm32f10x_md.s里的md后缀。STM32F103C8T6 属于中容量Medium Density产品Flash 64KB必须用 md 版本的启动文件。如果误用了hd大容量或ld小容量的启动文件链接阶段可能不报错但中断向量表偏移和堆栈大小对不上跑起来就是随机死机。这是新手最容易忽略的一处血泪经验先确认启动文件后缀再谈其他。system_stm32f10x.c里的SystemInit()会在进入 main 之前被调用负责把系统时钟从内部 8MHz HSI 切到外部晶振并倍频。很多例程在这里写死了 8MHz 晶振、9 倍频到 72MHz。如果你的板子晶振不是 8MHz要么改这里的宏要么在 main 里重新配置 RCC。不改的后果就是串口波特率全错、延时不准也就是开头那个 LED 不亮的翻车现场。2.2 用 Keil 打开工程前要核对的三个参数工程文件.uvprojx里藏着几个决定成败的配置打开后先逐项核对别直接按 F7。第一步看 Target 选项卡里的晶振频率Xtal。这个值只影响软件仿真和部分延时计算但填错了仿真波形全错容易误导调试方向。填你板子上实际焊的晶振值。第二步看 C/C 选项卡里的宏定义。STM32F10X_MD 和 USE_STDPERIPH_DRIVER 这两个宏必须存在。前者告诉标准外设库当前是中容量器件后者启用库函数。缺了 STM32F10X_MDstm32f10x.h里的条件编译会走到错误分支寄存器地址可能对不上。/* 在 Keil 的 C/C - Define 里应看到类似内容 */ USE_STDPERIPH_DRIVER, STM32F10X_MD第三步看 Debug 选项卡选的下载器。例程包一般默认 ST-Link 或 J-Link如果你手上是 DAP-Link要在这里切换否则点下载会提示找不到目标。切换后还要在 Settings 里确认 SWD 接口和芯片识别正常能读到 Device ID 再往下走。提示核对完这三项先编译一次看有没有 error 和 warning。有 warning 不一定是坏事但涉及“implicit declaration”的必须处理那意味着函数没声明返回值类型默认 int在指针场景下会截断地址。2.3 最小可跑工程只留时钟和 GPIO例程包里的工程往往带了一堆用不上的外设初始化第一次跑建议自己裁一个最小工程把变量降到最少。以点亮 PA5 为例完整流程如下。#include stm32f10x.h static void Delay(__IO uint32_t n) { while (n--) { __NOP(); } /* 粗略延时仅用于点灯验证 */ } int main(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 开 GPIOA 时钟 */ gpio.GPIO_Pin GPIO_Pin_5; gpio.GPIO_Mode GPIO_Mode_Out_PP; /* 推挽输出 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); while (1) { GPIO_SetBits(GPIOA, GPIO_Pin_5); Delay(2000000); GPIO_ResetBits(GPIOA, GPIO_Pin_5); Delay(2000000); } }这段代码的逻辑很直白先使能 GPIOA 的 APB2 时钟不使能时钟就配置寄存器是无效操作这是 STM32 和外设打交道的第一条铁律然后填初始化结构体推挽输出适合直接驱动 LED开漏输出则要外接上拉最后在主循环里翻转电平。Delay用空循环实现只用于验证正式项目里应该用 SysTick 或定时器因为空循环的时长随编译优化等级变化-O2下可能被优化掉。参数上GPIO_Speed设 50MHz 对点灯没影响但在做 SPI 或高速并口时速度档位选低了会导致波形上升沿变缓、通信失败。GPIO_Mode的选择要看负载驱动 LED 用推挽做 I2C 的 SDA 线用开漏读按键用上拉或下拉输入。这几个模式在例程里都有对应文件改的时候对照着看比查手册快。3. 外设例程逐个跑串口、定时器、ADC 的配置套路最小工程跑通后就可以按外设逐个验证例程了。这一章挑三个最常用也最容易出问题的外设——串口、定时器、ADC把配置步骤和参数含义讲透。每个外设都遵循“开时钟 → 配引脚 → 配外设 → 配中断可选→ 使能”的固定套路掌握这个套路例程包里其他外设文件基本能自己读。3.1 串口 USART1波特率、引脚复用与中断接收串口是调试的第一双眼睛配不通串口后面全是盲调。STM32F103C8T6 的 USART1 挂在 APB2 上TX 是 PA9RX 是 PA10。配置分两步GPIO 复用推挽输出 浮空或上拉输入然后配 USART 参数。void USART1_Init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; /* TX */ gpio.GPIO_Mode GPIO_Mode_AF_PP; /* 复用推挽 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_10; /* RX */ gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; /* 浮空输入 */ GPIO_Init(GPIOA, gpio); usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); /* 接收非空中断 */ USART_Cmd(USART1, ENABLE); }关键点在GPIO_Mode_AF_PP。TX 引脚必须配成复用推挽配成普通推挽输出的话USART 外设的信号出不来现象是能发但对方收不到。RX 配浮空输入即可如果线上有干扰可以改上拉。波特率参数直接填数值库函数会根据当前 APB2 时钟自动算分频但前提是SystemInit里的时钟配置正确——这就是第 2 章强调时钟的原因时钟错了波特率寄存器算出来的值就是错的串口助手收到的全是乱码。中断接收要在stm32f10x_it.c里实现USART1_IRQHandler读USART_ReceiveData清标志。常见翻车是忘了在 NVIC 里使能中断通道或者优先级分组没配现象是中断进不去。例程包里一般有NVIC_Configuration函数照着改通道号即可。3.2 定时器 TIM3预分频与自动重装的计算定时器例程的核心是两个寄存器PSC预分频和 ARR自动重装。定时周期公式是T (PSC1)*(ARR1)/TIMx_CLK。以 TIM3 挂在 APB1、时钟 72MHz 为例要产生 1ms 中断void TIM3_Init_1ms(void) { TIM_TimeBaseInitTypeDef tb; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); tb.TIM_Prescaler 71; /* 72MHz / (711) 1MHz计数周期 1us */ tb.TIM_Period 999; /* 1000 个 1us 1ms */ tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tb); TIM_ClearFlag(TIM3, TIM_FLAG_Update); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); nvic.NVIC_IRQChannel TIM3_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 1; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_Cmd(TIM3, ENABLE); }参数含义PSC 设 71 是把 72MHz 分频到 1MHzARR 设 999 是数 1000 个数溢出一次合起来 1ms。这里有个容易踩的坑——APB1 的定时器时钟有倍频机制。当 APB1 分频系数不为 1 时定时器时钟是 APB1 时钟的 2 倍。系统时钟 72MHz 时 APB1 通常是 36MHz但 TIM3 实际拿到的是 72MHz。如果按 36MHz 去算 PSC定时周期会差一倍。判断方法看SystemCoreClock和 RCC 配置或者直接用示波器量中断里翻转的引脚实测最稳。3.3 ADC1 单通道采样校准、采样时间与 DMAADC 例程的坑集中在“采出来的值飘”和“第一次转换不准”。STM32F103 的 ADC 上电后必须先校准否则零点偏移明显。void ADC1_Init_PA0(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 72MHz/6 12MHzADC 上限 14MHz */ gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; /* 模拟输入 */ GPIO_Init(GPIOA, gpio); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode DISABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }RCC_PCLK2_Div6这行不能省。ADC 时钟来自 PCLK2APB272MHz 直接喂给 ADC 会超规格转换结果不可信。分频到 12MHz 是常见做法。采样时间ADC_SampleTime_55Cycles5适合中高阻抗信号源如果传感器输出阻抗高采样时间要加长否则采样电容充不满读数偏低。校准顺序也不能颠倒先复位校准寄存器等标志再启动校准等标志之后才能开始转换。跳过校准的典型现象是输入接地时读数不是 0 而是几十。4. 避坑与排查例程跑不通时先看这几处例程包再规范换一块板子、换一个编译环境就可能跑不通。这一章按“现象 → 原因 → 解决”整理几条高频翻车记录都是实际调试中反复遇到的。4.1 现象编译通过下载成功程序不运行原因通常是启动文件与器件容量不匹配或者链接脚本里的 Flash/RAM 大小写错。STM32F103C8T6 是 64KB Flash、20KB RAM如果工程沿用了大容量模板的 512KB 配置链接器会把代码放到超出实际容量的地址下载器写入失败但不一定报错。解决检查 .sct 分散加载文件或 Keil 的 IROM1/IRAM1 设置IROM 起始 0x08000000 大小 0x10000IRAM 起始 0x20000000 大小 0x5000。4.2 现象串口能发不能收或收到乱码乱码先查时钟和波特率八成是外部晶振频率与SystemInit里的宏不一致。能发不能收则查 RX 引脚模式和中断配置RX 配成复用推挽是错的应为浮空或上拉输入中断没使能 NVIC 通道也收不到。还有一种隐蔽情况——串口助手开了硬件流控而代码里USART_HardwareFlowControl_None两边对不上数据被吞。解决逐项核对引脚模式、NVIC 通道、流控设置。4.3 现象定时器中断进不去或周期不对中断进不去先看TIM_Cmd有没有调用再看 NVIC 优先级分组是否在别处被改过。周期不对则回到 APB1 倍频问题按 72MHz 而非 36MHz 重算 PSC。还有一种情况是中断服务函数名拼错比如写成TIM3_IRQHandlerr编译器不报错因为它是普通函数但向量表里指向的还是默认的死循环。解决对照启动文件里的向量表名字逐个核对。4.4 现象ADC 读数跳动大或始终为满量程跳动大先加长采样时间再检查参考电压是否稳定VDDA 和 VSSA 之间的去耦电容不能省。始终满量程通常是输入引脚配错模式配成了普通输入而非模拟输入或者通道号与引脚对应关系搞错——ADC_Channel_0 对应 PA0Channel_1 对应 PA1以此类推别按物理引脚序号想当然。解决用万用表量引脚电压对照转换值反推能快速定位是硬件还是配置问题。注意排查时优先用“最小改动法”一次只改一个配置项改完立即验证。同时改三处再烧录出问题就不知道是哪处引起的反而更慢。5. 把例程改成自己的工程参数化与验证习惯例程跑通只是起点真正要用起来得把它改造成能复用的工程骨架。我的习惯是把板级相关的引脚和参数全部抽到一个board_config.h里用宏集中管理换板子只改这一个文件。/* board_config.h —— 板级参数集中管理 */ #ifndef BOARD_CONFIG_H #define BOARD_CONFIG_H #define BOARD_HSE_VALUE 8000000U /* 外部晶振频率按实际焊接值改 */ #define BOARD_LED_PORT GPIOA #define BOARD_LED_PIN GPIO_Pin_5 #define BOARD_UART_BAUD 115200U #define BOARD_ADC_SAMPLE_T ADC_SampleTime_55Cycles5 #endif这样做的价值在于时钟频率、引脚、波特率这些最容易随硬件变化的量只有一个出处改板子时不用在十几个文件里搜索替换。配合一个board_init()统一调用各外设初始化main 函数就只剩业务逻辑可读性大幅提升。验证方法上我一般分三层走。第一层是编译验证开-Wall看警告重点盯隐式声明和类型截断。第二层是静态验证用调试器单步走到每个外设初始化之后读寄存器确认时钟使能位、模式位真的写进去了比盲信代码可靠。第三层是动态验证串口打印关键变量、定时器中断里翻转引脚用示波器看周期、ADC 输入已知电压反算精度。三层都过才算这个外设真的通了。还有一个具体技巧把例程包里的每个外设工程单独编译一次记录各自的 Flash 和 RAM 占用。STM32F103C8T6 的 20KB RAM 在做串口缓冲加 ADC 多通道 DMA 时并不宽裕提前知道每个模块的占用组合功能时才不会链接溢出。我吃过一次亏两个单独跑都正常的模块合到一起栈溢出导致随机 HardFault查了大半天才发现是缓冲区开太大。从那以后我每次合并模块前都强制看一眼 map 文件的 RAM 分布希望这个习惯也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?