多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32F103C8T6开源三件套:代码、原理图与仿真完整项目

STM32F103C8T6开源三件套:代码、原理图与仿真完整项目 1. 项目缘起与整体设计思路1.1 为什么选择做一套“三件套”开源STM32 的入门资料网上铺天盖地但真正能拿来就用的完整项目包其实不多。大部分情况是代码给了一半原理图是截图仿真文件压根没有。初学者拿到这种“残缺包”往往卡在硬件连线对不上、编译报错找不到原因、仿真跑不起来这几个环节最后不了了之。我这次开源的这套东西核心出发点就一个让一个零基础的人从拿到文件到跑通第一个功能不超过半小时。所以我把项目拆成了三个互相咬合的模块——代码、原理图、仿真。代码负责逻辑实现原理图负责硬件定义仿真负责在没实物的情况下验证逻辑。三者共用同一套引脚定义和外设配置改一处三处同步不会出现“代码里是 PA9原理图上画的是 PB6”这种经典翻车现场。这套组合适合几类人正在做课程设计或毕业设计的学生、想从 51 单片机过渡到 STM32 的电子爱好者、需要快速验证某个外设驱动方案的嵌入式工程师。哪怕你之前没碰过 STM32只要会基本的 C 语言和电路常识跟着走一遍就能把整个链路打通。1.2 技术选型背后的取舍逻辑主控选的是STM32F103C8T6也就是大家常说的“蓝板”或“最小系统板”。这颗芯片在开源社区的生命力极强原因很实在价格低、资料多、外设够用、LQFP48 封装手工焊接难度可接受。比起 F4 系列F103 的主频 72MHz 对于跑传感器驱动、串口通信、PWM 输出这些常规任务绰绰有余而且标准外设库和 HAL 库的兼容性经过这么多年打磨已经非常稳定。开发环境用的是Keil MDK 5配合 STM32F1 的器件支持包。有人会问为什么不选 STM32CubeIDE 或者 PlatformIO我的考虑是Keil 在国内嵌入式教学和中小企业的存量最大你拿这套代码去问人、去面试、去交作业兼容性最好。而且 Keil 的软件仿真功能虽然不算强但配合外部的逻辑仿真工具做联合验证已经够用。仿真部分我用了两层一层是Proteus做电路级仿真验证原理图连线是否正确、外设时序是否匹配另一层是Keil 自带的 Simulator做指令级调试用来单步跟踪寄存器状态。这两层配合起来基本能在不焊板子的情况下把 80% 的逻辑问题暴露出来。注意Proteus 的 STM32 模型库对 F103 的支持比较完整但如果你用的是 F4 或 G0 系列仿真模型可能缺失需要提前确认。1.3 项目文件结构一览整个开源包解压后是这样一个目录树我刻意保持了扁平化避免嵌套太深导致找不到文件STM32_OpenSource_Project/ ├── Code/ │ ├── Core/ │ │ ├── main.c │ │ ├── stm32f1xx_it.c │ │ └── system_stm32f1xx.c │ ├── Drivers/ │ │ ├── CMSIS/ │ │ └── STM32F1xx_HAL_Driver/ │ ├── App/ │ │ ├── sensor_dht11.c │ │ ├── oled_ssd1306.c │ │ └── uart_protocol.c │ └── MDK-ARM/ │ └── Project.uvprojx ├── Schematic/ │ ├── STM32_Main.SchDoc │ ├── Power.SchDoc │ └── BOM.xlsx ├── Simulation/ │ ├── Proteus/ │ │ └── STM32_Sim.pdsprj │ └── Keil_Sim/ │ └── SimConfig.ini └── Docs/ ├── PinMap.md └── QuickStart.md这个结构的好处是代码、硬件、仿真三条线各自独立又互相引用。比如PinMap.md里定义的引脚表在代码的main.h、原理图的网络标签、Proteus 的连线里都是同一套命名改的时候三处一起改不会漏。2. 核心细节解析与实操要点2.1 代码层的模块化拆分策略很多开源 STM32 项目的代码是一坨main.c从头写到尾看着就头大。我这套代码按功能切成了三层硬件抽象层、驱动层、应用层。硬件抽象层就是 HAL 库那套东西负责寄存器操作和中断向量。驱动层是我自己写的sensor_dht11.c、oled_ssd1306.c这些文件每个文件只干一件事——把某个外设的时序翻译成函数调用。应用层在main.c里做业务编排比如“读温湿度→显示到 OLED→通过串口上报”。这样拆的好处是你想换一个传感器只需要改驱动层的一个文件应用层几乎不用动。比如把 DHT11 换成 SHT30只要新写一个sensor_sht30.c保持函数接口一致main.c里把 include 换一下就行。关键代码片段长这样/* sensor_dht11.c 中的读取函数 */ uint8_t DHT11_Read_Temp_Humi(float *temp, float *humi) { uint8_t buf[5] {0}; uint8_t i, j; /* 主机发送起始信号 */ DHT11_Start(); if (DHT11_Wait_Response() ! 0) { return 1; /* 传感器无响应 */ } /* 读取40位数据 */ for (i 0; i 5; i) { for (j 0; j 8; j) { buf[i] 1; buf[i] | DHT11_Read_Bit(); } } /* 校验 */ if (buf[4] ! (buf[0] buf[1] buf[2] buf[3])) { return 2; /* 校验失败 */ } *humi buf[0] buf[1] * 0.1f; *temp buf[2] buf[3] * 0.1f; return 0; }这段代码里有个细节DHT11 的时序对延时非常敏感DHT11_Read_Bit()里用的是delay_us()而不是 HAL 的HAL_Delay()因为后者最小分辨率是 1ms根本抓不住 DHT11 的 26-28us 高电平窗口。这是很多新手第一次调 DHT11 必踩的坑。2.2 原理图设计中的引脚分配原则原理图这块我用的是Altium Designer画的但导出了 PDF 和嘉立创可用的网表文件方便不同工具链的人查看。引脚分配遵循三个原则第一功能优先。USART1 的 TX/RX 固定在 PA9/PA10I2C1 的 SCL/SDA 固定在 PB6/PB7这些都是 STM32F103 的默认复用功能用默认引脚可以少配寄存器降低出错概率。第二物理就近。OLED 的电源引脚和 DHT11 的电源引脚都安排在板子同一侧走线短减少干扰。DHT11 的数据线单独走一根旁边铺地避免和 PWM 输出线平行走太长。第三预留调试口。SWD 的 SWCLK/SWDIO 固定在 PA14/PA13旁边留出 4 针排针VCC、GND、SWCLK、SWDIO方便接 ST-Link 下载器。这个口子千万别省不然后面调试只能靠串口打印效率低一半。具体的引脚分配表如下功能模块引脚外设资源备注DHT11 数据PA0GPIO 输入需 4.7K 上拉OLED SCLPB6I2C1_SCL默认复用OLED SDAPB7I2C1_SDA默认复用串口 TXPA9USART1_TX接 CH340 RX串口 RXPA10USART1_RX接 CH340 TXLED 指示PC13GPIO 输出低电平点亮SWD 调试PA13/PA14SWDIO/SWCLK保留排针提示PC13 作为输出时驱动能力较弱只能点 LED不要用来驱动继电器或蜂鸣器否则可能烧口。2.3 仿真环境的搭建与配置要点Proteus 仿真这块很多人卡在“找不到 STM32 模型”或者“加载 hex 文件后没反应”。我总结下来主要是三个配置点第一器件库要选对。在 Proteus 里搜STM32F103C8选STM32F103C8T6那个模型不要选STM32F103C8不带后缀的后者引脚映射可能不对。第二晶振频率要匹配。Proteus 里 STM32 模型的属性中Crystal Frequency要设成 8MHz外部晶振或 72MHz内部 PLL 后这个值必须和代码里SystemInit()配置的一致否则串口波特率会偏得离谱。第三hex 文件路径不能有中文。这是 Proteus 的老毛病路径里有中文或空格加载 hex 时会静默失败仿真跑起来但芯片不执行。建议把整个工程放在纯英文路径下比如D:\STM32_Sim\。Keil 这边的仿真配置相对简单在Options for Target→Debug里选Use Simulator然后在Initialization File里指定SimConfig.ini里面写好// SimConfig.ini // 配置仿真时的外设状态 PORT0 0x00 PORT1 0x00 XTAL 8000000这个文件的作用是告诉 Keil 仿真器上电时各端口初始状态和晶振频率。不配的话仿真时读到的寄存器值可能是随机的调试起来会怀疑人生。3. 实操过程与核心环节实现3.1 从零搭建 Keil 工程的完整步骤虽然我提供了现成的.uvprojx文件但你还是应该知道怎么从零建一个万一哪天要换芯片或者加文件不至于抓瞎。第一步打开 Keil MDK 5Project→New uVision Project选一个纯英文路径工程名比如STM32_Demo。弹出器件选择框展开STMicroelectronics→STM32F1 Series→STM32F103→STM32F103C8点 OK。第二步弹出的Manage Run-Time Environment窗口里勾选CMSIS→CORE以及Device→Startup。这两个是必须的前者提供内核定义后者提供启动文件。HAL 库的话如果你用标准库就跳过用 HAL 就勾Device→STM32Cube HAL→GPIO、RCC、USART等你需要的外设。第三步新建main.c写一个最简框架#include stm32f1xx_hal.h void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(500); } }第四步配置时钟。在SystemClock_Config()里把 HSE 设为 8MHz 晶振PLL 倍频到 72MHzAHB 不分频APB1 分频系数 236MHzAPB2 不分频72MHz。这个配置是 F103 的经典跑法稳定且性能拉满。第五步在Options for Target→C/C→Define里加上USE_HAL_DRIVER, STM32F103xB这两个宏不定义HAL 库会报一堆未定义错误。第六步Output选项卡里勾上Create HEX File这样编译后自动生成 hex方便喂给 Proteus。3.2 DHT11 驱动调试的实战记录DHT11 这个传感器便宜、简单但时序是真的娇气。我第一次调的时候读出来的数据全是 0用逻辑分析仪抓波形才发现起始信号拉低 18ms 之后我释放总线只等了 20us 就开始读响应而 DHT11 的响应时间典型值是 80us 低电平 80us 高电平。等太短芯片还没反应过来。修正后的起始时序是这样的void DHT11_Start(void) { /* 配置为推挽输出 */ GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(20); /* 拉低至少18ms */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); delay_us(30); /* 释放总线等20-40us */ /* 切换为输入模式准备读响应 */ gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, gpio); }这里有个关键点拉低时间宁长勿短。DHT11 手册写的是至少 18ms我实际用 20ms留点余量。释放后的等待时间用 30us卡在 20-40us 的窗口中间。这两个参数调好响应读取的成功率从 30% 直接拉到 99%。还有一个坑是电源。DHT11 工作电流虽然不大但瞬间拉低总线时会有电流尖峰如果和 OLED 共用一条 3.3V 走线且线太细会导致电压跌落读出来的湿度值乱跳。我的做法是在 DHT11 的 VCC 和 GND 之间并一个 100nF 陶瓷电容紧贴传感器引脚放置问题就解决了。3.3 Proteus 联合仿真的操作流程把 Keil 编译出的 hex 喂给 Proteus整个流程分四步第一步在 Proteus 里画好原理图。STM32 芯片放上去DHT11 用DHT11模型OLED 用OLED12864I2C模型串口用COMPIM虚拟串口。连线按照前面 PinMap 表来一根都不能错。第二步双击 STM32 芯片在属性里找到Program File浏览到 Keil 生成的 hex 文件。Crystal Frequency填8M。这两个填完点 OK。第三步双击 DHT11 模型设置温湿度初值比如温度 25.0湿度 60.0。这样仿真启动后代码读到的就是这两个值方便验证显示和上报逻辑。第四步点左下角运行按钮。如果 OLED 上显示出T:25.0C H:60.0%串口助手收到同样的数据说明整条链路通了。注意Proteus 仿真时DHT11 的响应速度比实物快代码里的delay_us如果基于 SysTick 实现在仿真环境下可能不准。建议在仿真时把延时参数适当放大或者直接用HAL_Delay替代微秒级延时做功能验证等上实物再换回精确延时。3.4 串口通信协议的实现细节串口这块我定义了一个简单的帧格式方便上位机解析帧头(0xAA) | 长度(1字节) | 命令(1字节) | 数据(N字节) | 校验和(1字节) | 帧尾(0x55)校验和的计算方式是从长度字节开始到数据最后一个字节逐字节累加取低 8 位。这个算法简单在单片机上跑不费劲上位机用 Python 或 C# 解析也容易。发送温湿度的代码void UART_Send_Temp_Humi(float temp, float humi) { uint8_t frame[10]; uint8_t i, sum 0; frame[0] 0xAA; frame[1] 0x05; /* 长度命令1 数据4 */ frame[2] 0x01; /* 命令上报温湿度 */ frame[3] (uint8_t)temp; frame[4] (uint8_t)(temp * 10) % 10; frame[5] (uint8_t)humi; frame[6] (uint8_t)(humi * 10) % 10; for (i 1; i 6; i) { sum frame[i]; } frame[7] sum; frame[8] 0x55; HAL_UART_Transmit(huart1, frame, 9, 100); }这里有个细节浮点数转字节时我先取整数部分再取小数部分避免直接做指针转换导致字节序问题。虽然代码看起来笨一点但跨平台移植时不会出幺蛾子。4. 常见问题与排查技巧实录4.1 编译报错速查表报错信息大概率原因解决方法cannot open source input file stm32f1xx.h头文件路径没加Options → C/C → Include Paths 里加上 CMSIS 和 HAL 的 inc 目录undefined symbol HAL_Init没定义USE_HAL_DRIVER在 Define 里加上USE_HAL_DRIVER, STM32F103xBL6218E: Undefined symbol SystemInit启动文件没加把startup_stm32f103xb.s添加到工程Flash Download failed下载器配置错Debug → Settings → Flash Download 里选对算法Error: L6406E: No space in execution regions芯片型号选错确认 Device 选的是 STM32F103C8 而不是 C6这张表里的问题我几乎每次带新人都会遇到。尤其是第一个和第二个十个人里有八个会踩。记住一个原则HAL 库的工程Define 里必须有USE_HAL_DRIVER和对应的芯片宏缺一个都编不过。4.2 仿真跑不通的排查思路Proteus 仿真跑不起来按这个顺序查先看 hex 有没有加载成功。双击 STM32 芯片确认Program File路径正确且文件存在。如果路径里有中文换成纯英文再试。再看晶振频率。属性里的Crystal Frequency和代码里SystemClock_Config的 HSE 值必须一致。代码用 8M 外部晶振仿真里就填 8M代码用内部 8M RC仿真里也填 8M。然后看电源和地。Proteus 里 STM32 模型的 VDD 和 VSS 引脚必须接上哪怕仿真不检查不接的话芯片不工作。我见过有人只接了 VDD 忘了 VSS查了一下午。最后看复位引脚。NRST 如果悬空仿真启动时可能处于复位状态。接一个 10K 上拉到 VDD再并一个 100nF 到地保证上电复位正常。4.3 实物调试中的独家避坑经验坑一ST-Link 识别不到芯片。先检查 SWD 四根线有没有接反尤其是 SWCLK 和 SWDIO接反了识别不到但不会烧芯片。然后看芯片是不是被读保护了用 ST-Link Utility 连一下如果提示Read Protection全片擦除一次再试。坑二串口乱码。九成是波特率不对。检查三处代码里huart1.Init.BaudRate、串口助手里的波特率、系统时钟配置。如果系统时钟配错了比如 HSE 没起振导致跑在内部 8M RC 上那 72M 下算出来的波特率寄存器值就会偏实际波特率变成 9600 的 1/9自然乱码。坑三OLED 不亮。先量 VCC 和 GND 之间是不是 3.3V再量 SCL 和 SDA 有没有上拉电阻。I2C 总线必须上拉STM32 内部上拉太弱驱动不了长走线。加两个 4.7K 上拉到 3.3V基本就能亮。坑四DHT11 读一次就死。这是总线没释放干净。读完之后把数据引脚重新配成推挽输出并拉高保持 1 秒以上让传感器内部状态机复位。下次读之前再走一遍起始时序。4.4 项目扩展与二次开发建议这套东西跑通之后你可以往几个方向扩加一个ESP8266 模块把温湿度数据传到物联网平台实现远程监控。串口协议不用改ESP8266 那边用 AT 指令透传就行。换一个STM32F401或者G030练手不同系列的外设配置差异。F4 的时钟树更复杂G0 的 HAL 库 API 有变化都是很好的进阶练习。把FreeRTOS移植进来把 DHT11 读取、OLED 刷新、串口上报拆成三个任务用信号量和队列做同步。这是从裸机到 RTOS 的经典过渡项目。最后再分享一个小技巧这套代码里的delay_us我是用 SysTick 的计数寄存器实现的不占用定时器资源。具体做法是关掉 SysTick 中断读SysTick-VAL的差值来算微秒。这个方法在 F1 和 F4 上都验证过精度能到 ±2us跑 DHT11 和超声波测距都够用。代码在App/delay.c里需要的直接拿去。这个项目后续还可以这样扩展把原理图里的 CH340 换成 CP2102做一版 USB 直连的或者把 OLED 换成 SPI 接口的 ST7789 彩屏练手 SPI 驱动。硬件这东西改着改着就熟了。
返回列表