ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32开源项目实战:代码、原理图与Proteus仿真全打通

STM32开源项目实战:代码、原理图与Proteus仿真全打通 1. 项目缘起与整体设计思路STM32 项目开源这件事我前前后后做过好几轮从最早只丢一个 Keil 工程压缩包到后来把代码、原理图、仿真工程打包成一套完整资料中间踩过的坑基本能写一本小册子。这次想聊的这套开源项目核心思路就一句话让拿到资料的人不用问任何人自己就能把项目跑起来、看懂、改掉。听起来简单但真正做过开源的人都知道能做到这一点的项目其实不多。我见过太多所谓的“开源 STM32 项目”点进去一看只有一个 main.c外设初始化全靠 CubeMX 生成原理图是截图仿真压根没有。这种项目对作者来说是记录对别人来说是谜题。所以这次我在整理这套资料的时候给自己定了三条硬标准第一代码必须能直接编译下载不能有缺失的库文件或者路径依赖第二原理图必须是可编辑的源文件不是一张模糊的图片第三仿真工程必须能独立运行不依赖实物硬件就能看到关键波形。这三条标准看起来是基本要求但真正落地的时候每一个都会牵扯出一堆细节问题。为什么要把代码、原理图、仿真这三样东西放在一起因为这三者对应的是嵌入式开发中三个不同的认知层次。代码解决的是“逻辑怎么跑”的问题原理图解决的是“电流怎么流”的问题仿真解决的是“时序对不对”的问题。很多初学者学 STM32 的时候只盯着代码看结果遇到硬件不工作就完全懵了因为他不知道代码背后的电平到底有没有真正翻转。反过来只懂硬件不懂代码的人调试的时候连在哪里打断点都不知道。把这三样东西打通才算是真正理解了一个嵌入式项目。从技术选型上来说这套项目用的是 STM32F103C8T6 作为主控也就是大家常说的“蓝板”或者“最小系统板”。选这个芯片的理由很实在价格便宜资料多社区活跃几乎每个做嵌入式的人都手上有几块。外设方面涵盖了 GPIO、USART、TIM、ADC、I2C 这几个最常用的模块基本上把入门到进阶的关键知识点都覆盖了。仿真部分用的是 Proteus虽然 Proteus 对 STM32 的支持不如对 51 单片机那么完美但对于验证 GPIO 时序、串口收发、定时器中断这些基础功能来说已经足够用了。提示如果你打算参考这套项目的结构来整理自己的开源资料建议先把“别人能不能跑起来”作为第一优先级而不是“我的代码写得多优雅”。开源项目的第一价值是可复现第二价值才是可学习。2. 代码部分的核心细节与实操要点2.1 工程目录结构的设计逻辑先看代码部分的目录结构这个是我改过好几版之后才定下来的。最早的版本是把所有 .c 文件都堆在 User 文件夹里结果文件一多就乱得没法看。现在的结构是这样的Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32f1xx_hal_conf.h │ │ └── stm32f1xx_it.h │ └── Src/ │ ├── main.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── BSP/ │ ├── Inc/ │ │ ├── bsp_led.h │ │ ├── bsp_uart.h │ │ ├── bsp_timer.h │ │ └── bsp_adc.h │ └── Src/ │ ├── bsp_led.c │ ├── bsp_uart.c │ ├── bsp_timer.c │ └── bsp_adc.c ├── App/ │ ├── Inc/ │ │ └── app_main.h │ └── Src/ │ └── app_main.c └── MDK-ARM/ └── Project.uvprojx这个结构的关键在于把BSP板级支持包和App应用逻辑分开了。BSP 层负责直接操作寄存器或者调用 HAL 库App 层只调用 BSP 层提供的接口。这样做的好处是如果以后要换芯片或者换开发板只需要改 BSP 层App 层的逻辑基本不用动。我试过把一个基于 F103 的项目移植到 F407 上因为 BSP 层封装得好App 层只改了两个宏定义就搞定了。另一个细节是stm32f1xx_it.c这个文件很多人用 CubeMX 生成之后就不管了但实际上中断服务函数是最容易出问题的地方。我在这个文件里给每个中断都加了注释说明这个中断是谁触发的、触发频率大概是多少、里面做了什么事情。比如定时器中断那里我会写清楚“TIM2 中断配置为 1kHz每次进中断翻转 LED 并更新一个全局 tick 计数”。这样别人看代码的时候不用去翻初始化代码就能知道这个中断在干什么。2.2 关键外设驱动的实现与避坑先看 LED 驱动这个最简单但也最能体现封装思路。bsp_led.c里面提供了三个函数LED_Init()、LED_On()、LED_Off()、LED_Toggle()。看起来很简单对吧但这里有个坑GPIO 的初始状态。如果你在初始化的时候不先把引脚电平设成灭灯状态上电瞬间 LED 可能会闪一下。我的做法是在LED_Init()里面先把 ODR 寄存器对应的位清零然后再配置 GPIO 模式。这个顺序不能反反了就会闪。void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* 先设置默认电平为灭灯状态 */ HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_SET); GPIO_InitStruct.Pin LED_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(LED_GPIO_PORT, GPIO_InitStruct); }串口驱动这块我踩过一个很典型的坑中断接收和轮询发送混用导致的丢数据。最早的时候我用HAL_UART_Receive_IT()接收然后在主循环里用HAL_UART_Transmit()发送结果发现发送的时候如果正好来了接收中断数据就会丢。后来改成发送也用中断并且加了一个环形缓冲区问题才解决。具体做法是定义一个UART_RxBuffer数组和两个指针rx_head、rx_tail中断里只负责把数据塞进缓冲区并移动rx_head主循环里从rx_tail取数据。这样收发完全解耦实测在 115200 波特率下连续收发几万字节都不会丢。定时器部分我用的是 TIM2配置成 1ms 中断一次主要用来做系统 tick 和任务调度。这里有个细节中断优先级。如果你用了 HAL 库的HAL_Delay()它依赖 SysTick 中断而 SysTick 的优先级默认是最低的。如果你把 TIM2 的优先级设得比 SysTick 还高并且在 TIM2 中断里调用了HAL_Delay()那就会死锁。我的做法是 TIM2 中断里只做计数和标志位设置绝对不调用任何阻塞函数。ADC 部分用的是规则通道单次转换模式采样电位器的电压。这里要注意的是采样时间的设置。STM32F103 的 ADC 时钟最高 14MHz采样时间太短的话高阻抗信号源会采不准。我一般设成 55.5 个周期对应 12 位精度下大概 4us 左右的采样时间对于电位器这种信号源来说绰绰有余。如果你要采更快的信号可以适当降低采样时间但要确保信号源的输出阻抗足够低。2.3 代码开源的注意事项代码开源不是把文件丢上去就完事了有几个细节直接决定了别人能不能顺利跑起来。第一库文件的版本要写清楚。HAL 库的版本不同有些函数的参数和返回值会变。我一般会在 README 里写明“本工程基于 STM32CubeF1 V1.8.0 版本HAL 库版本为 V1.1.8”。第二编译器的版本和设置要说明。Keil MDK 的 AC5 和 AC6 编译器对代码的语法要求不一样AC6 对类型转换更严格。我遇到过别人用 AC6 编译我的 AC5 工程报了一堆 warning 甚至 error。第三下载方式要写清楚。是用 ST-Link 还是串口 ISPBOOT0 和 BOOT1 怎么跳线这些看起来是小事但对新手来说就是拦路虎。注意如果你在代码里用了任何第三方库或者别人的代码片段一定要在文件头注明来源和许可协议。这不仅是尊重别人的劳动也是保护你自己。3. 原理图设计与仿真验证的完整流程3.1 原理图绘制的关键决策原理图这部分我用的是立创 EDA 专业版来画。选它的原因很简单免费、云端存储、元件库全、可以直接导出 PDF 和 Gerber。对于开源项目来说还有一个很重要的点别人可以一键打开并编辑。如果你用 AD 或者 Cadence 画别人还得装软件、找库文件门槛一下子就上去了。整个原理图分为几个模块电源部分、MCU 最小系统、LED 指示、串口通信、ADC 输入、按键输入。电源部分用的是 AMS1117-3.3输入 5V 输出 3.3V。这里有个细节输入输出电容的选型。AMS1117 的数据手册要求输入输出各接一个 10uF 的钽电容但实际用的时候如果只接 0.1uF 的陶瓷电容输出会有比较大的纹波。我的做法是输入接 10uF 钽电容加 0.1uF 陶瓷电容输出同样配置实测纹波在 20mV 以内。MCU 最小系统部分除了常规的晶振电路和复位电路我特别加了BOOT0 和 BOOT1 的跳线帽。很多新手不知道 STM32 的启动模式怎么设置导致程序下载不进去。跳线帽的好处是直观拔插一下就能切换启动模式。另外SWD 调试接口一定要留出来而且最好用标准的 4 针接口VCC、GND、SWDIO、SWCLK这样别人用 ST-Link 的时候直接插上就行不用去查引脚定义。串口部分我用的是 CH340C 芯片比 CH340G 少一个晶振电路更简洁。这里有个坑CH340C 的 V3 引脚。如果你用的是 3.3V 供电V3 引脚要接 3.3V如果用 5V 供电V3 引脚要接一个 0.1uF 电容到地。我见过有人接错了导致串口完全没反应查了半天以为是代码问题。ADC 输入部分我加了一个电压跟随器用的是 LM358。为什么要加这个因为电位器的输出阻抗比较高直接接到 ADC 引脚上采样的时候会拉低电压导致采样值不准。加一个电压跟随器做阻抗变换采样就稳了。这个细节在很多开源项目里都被忽略了但实际用的时候差别很明显。3.2 仿真工程的搭建与调试仿真部分我用的是 Proteus 8.13虽然 Proteus 对 STM32 的支持有一些限制比如不能仿真 USB 和某些高级外设但对于 GPIO、串口、定时器、ADC 这些基础功能来说完全够用。搭建仿真工程的步骤大概是这样的第一步在 Proteus 里放置 STM32F103C8 元件然后双击配置晶振频率为 8MHz。这里要注意Proteus 里的 STM32 模型默认是从内部 8MHz RC 振荡器启动的如果你在代码里配置了外部晶振需要在仿真属性里把时钟源改成外部。第二步添加必要的外围元件LED、电阻、虚拟串口终端、电位器、示波器。虚拟串口终端用来观察串口输出的数据示波器用来观察 PWM 波形或者定时器翻转引脚的电平变化。第三步加载编译好的 hex 文件。这里有个关键点hex 文件的路径不能有中文。我遇到过好几次因为路径里有中文导致 Proteus 加载失败的情况后来养成了把所有工程都放在纯英文路径下的习惯。第四步运行仿真观察现象。如果 LED 不亮先检查引脚配置对不对如果串口没输出先检查波特率设置和虚拟终端的波特率是否一致如果 ADC 采样值不对先检查电位器的分压电路。仿真最大的价值在于验证时序。比如我用定时器做 PWM 输出的时候在仿真里用示波器看波形可以很直观地看到占空比和频率对不对。如果不对改代码重新编译加载几分钟就能验证一轮比在实物上反复烧录快得多。但仿真也有局限性比如中断响应时间在仿真里和实物上差别很大仿真里可能几微秒就响应了实物上可能要十几微秒。所以仿真验证的是逻辑正确性不是实时性。3.3 代码、原理图、仿真三者的对应关系这三者之间必须能对得上否则别人拿到资料就会懵。我的做法是做一个引脚分配表把每个外设用的引脚、在代码里的宏定义、在原理图里的网络标号、在仿真里的连接点全部列出来。比如外设引脚代码宏定义原理图网络标号仿真连接LEDPA5LED_PINLED1LED 阳极UART TXPA9UART_TX_PINUART1_TX虚拟终端 RXDUART RXPA10UART_RX_PINUART1_RX虚拟终端 TXDADCPA0ADC_CHANNELADC_IN0电位器中间引脚TIM PWMPA1PWM_PINPWM_OUT示波器 A 通道有了这张表别人拿到资料之后不管是从代码入手还是从原理图入手都能快速找到对应的部分。这个表我一般放在 README 的最前面比任何文字说明都管用。提示引脚分配表最好用 Markdown 表格的形式写在 README 里这样在 GitHub 或者 Gitee 上可以直接渲染出来阅读体验比图片好得多。4. 常见问题排查与实操经验实录4.1 代码编译与下载阶段的典型问题问题一编译报错 “cannot open source input file stm32f1xx_hal.h”。这个基本就是头文件路径没加对。Keil 里需要在 Options for Target - C/C - Include Paths 里把 HAL 库的 Inc 目录加进去。我一般会把所有需要包含的路径都列出来Core/Inc、Drivers/STM32F1xx_HAL_Driver/Inc、Drivers/CMSIS/Device/ST/STM32F1xx/Include、Drivers/CMSIS/Include、BSP/Inc、App/Inc。少一个都会报错。问题二下载程序后没反应LED 不亮。先别急着怀疑代码用万用表量一下 MCU 的 VDD 引脚有没有 3.3V。如果没有检查电源电路如果有检查 BOOT0 和 BOOT1 的跳线是不是在正确的位置。BOOT0 接 GND 是从 Flash 启动接 VCC 是从系统存储器启动。很多人下载完程序忘了把 BOOT0 跳回 GND结果程序根本不运行。问题三串口能发不能收。这个我遇到过好几次最后发现是TX 和 RX 接反了。STM32 的 TX 要接 CH340 的 RXRX 要接 CH340 的 TX。画原理图的时候如果没注意PCB 打样回来就得飞线。所以在画原理图的时候我一般会在网络标号上直接标注 “MCU_TX - CH340_RX”避免搞混。问题四ADC 采样值跳动很大。除了前面说的加电压跟随器还有一个原因是参考电压不稳。STM32F103 的 VREF 默认接 VDDA如果 VDDA 的滤波没做好采样值就会跳。我的做法是在 VDDA 和 VSSA 之间并一个 1uF 加一个 0.1uF 的电容尽量靠近芯片引脚。4.2 仿真阶段的常见故障故障一Proteus 加载 hex 文件后提示 “No program loaded”。这个通常是 hex 文件路径的问题或者 hex 文件本身没有生成。检查 Keil 的 Output 选项卡里 “Create HEX File” 有没有勾选。另外Proteus 对 hex 文件的格式有要求有些编译器生成的 hex 文件 Proteus 不认这时候可以试试用fromelf工具转换一下格式。故障二仿真运行后串口终端没有输出。先检查虚拟终端的波特率设置和代码里的是否一致。然后检查 STM32 模型的时钟频率设置如果代码里用的是 72MHz而 Proteus 里设的是 8MHz那串口波特率就会差很多终端收到的就是乱码或者什么都收不到。故障三定时器中断在仿真里不触发。Proteus 对 STM32 中断的仿真支持有限有些中断在仿真里确实不会触发。这时候可以改用轮询方式验证逻辑或者直接在实物上测试。仿真不是万能的有些东西必须上实物。4.3 开源项目维护的经验之谈开源项目发出去之后一定会有人提 issue 或者发邮件问问题。我的经验是把常见问题整理成 FAQ 放在 README 里比一个个回复效率高得多。FAQ 的内容包括编译环境要求、库版本要求、下载方式说明、常见报错及解决方法。我一般会按照 “问题描述 - 可能原因 - 解决方法” 的格式来写简洁明了。另外版本管理很重要。每次更新代码或者原理图都要在 README 里写清楚改了什么。我一般用这样的格式## 更新记录 ### V1.1 (2024-01-15) - 修复串口接收丢数据的问题增加环形缓冲区 - 原理图增加电压跟随器提高 ADC 采样精度 - 仿真工程更新增加 PWM 波形观察 ### V1.0 (2024-01-01) - 初始版本发布 - 包含 LED、串口、定时器、ADC 驱动 - 提供原理图源文件和 Proteus 仿真工程这样别人一看就知道每个版本有什么区别要不要更新。注意开源项目最怕的就是 “一次性发布”。发完之后不管了别人提的问题不回复issue 不处理慢慢地就没人关注了。我的做法是每周固定花半个小时看一下 issue 和邮件能回复的尽量回复不能回复的至少标记一下 “已收到后续处理”。5. 从这套项目延伸出去的学习路径这套项目虽然不大但涵盖的知识点其实很全面。如果你能把这套项目完全吃透接下来可以往几个方向延伸。第一个方向是RTOS把裸机的任务调度换成 FreeRTOS学习任务创建、信号量、消息队列这些概念。第二个方向是通信协议在现有串口的基础上加 Modbus 或者自定义协议学习帧格式设计和校验算法。第三个方向是上位机开发用 Python 或者 C# 写一个简单的串口调试助手配合下位机做数据可视化。我自己是从这套项目开始慢慢做到了一个完整的工业数据采集终端。回头来看最关键的不是学会了多少外设而是养成了一个习惯每做一个功能都要在代码、原理图、仿真三个层面同时验证。代码层面看逻辑对不对原理图层面看电路通不通仿真层面看时序准不准。三个层面都过了这个功能才算真正做完。最后分享一个小技巧如果你在整理开源项目的时候觉得无从下手可以先从写 README开始。把你想让别人知道的东西先写下来然后反过来检查你的代码和原理图有没有对应的内容。写 README 的过程其实就是梳理项目结构的过程很多时候写着写着就发现哪里缺了东西。这个方法我用了很多次每次都能帮我发现一些自己没注意到的问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进