
1. 项目缘起与整体设计思路1.1 为什么选择“代码原理图仿真”三位一体的开源方式做STM32项目这些年我越来越觉得单纯丢一个工程压缩包到网上对学习者的帮助其实很有限。你拿到一个.uvprojx文件打开一看里面几十个.c和.h外设初始化一大堆但硬件怎么连的引脚为什么这么分配某个电阻电容为什么取这个值这些信息如果缺失代码就变成了“能跑但看不懂”的黑盒。所以这次我把自己做的一个STM32项目完整开源出来核心思路就是三件套一起给代码、原理图、仿真。代码解决“逻辑怎么实现”的问题原理图解决“硬件怎么连接”的问题仿真解决“没有实物板子怎么验证”的问题。这三者缺一不可。很多初学者卡在“我照着代码写了但下载进去没反应”十有八九是硬件连接和代码里的引脚定义对不上。如果一开始就有一份对应的原理图对着检查一遍问题往往迎刃而解。这个项目本身不算复杂主控用的是STM32F103C8T6也就是大家常说的“蓝桥杯同款”或者“江科大同款”。这颗芯片在初学者和中小型项目里出场率极高资源够用、价格便宜、资料铺天盖地。选它作为开源载体就是想让尽可能多的人能低成本复现。1.2 项目功能定位与适用人群这个项目实现的功能可以概括为基于STM32的多路数据采集与显示控制。具体来说包括ADC采集模拟量、DHT11读取温湿度、OLED显示实时数据、串口输出调试信息以及通过按键进行模式切换。功能听起来不花哨但每一个模块都是嵌入式开发里的基本功。把这些模块串起来就构成了一个完整的小系统。适合谁来参考我分三类说。第一类是正在学STM32的在校学生尤其是要做课程设计或毕业设计的这个项目可以作为模板把里面的模块替换成自己需要的功能。第二类是从51单片机转过来的工程师想看看STM32的工程结构、外设配置和中断管理跟8位机有什么不同。第三类是需要快速验证某个传感器或电路的开发者可以直接拿原理图里的对应部分去用省去重新画板的时间。提示这个项目的外设驱动都做了分层封装如果你想换芯片型号比如从F103C8T6换到F103ZET6只需要改引脚定义和时钟配置上层逻辑基本不用动。1.3 开源仓库的结构规划既然要开源目录结构就不能乱。我见过不少开源项目所有文件堆在一个文件夹里找起来非常痛苦。这个项目的仓库结构是这样的Hardware/目录放原理图源文件和PDF导出文件源文件用立创EDA格式方便大家直接修改。Firmware/目录放完整的Keil工程里面按Core、Drivers、User三层组织。Simulation/目录放Proteus仿真工程包含.pdsprj文件和必要的元件库。Docs/目录放引脚分配表、模块说明和常见问题记录。Tools/目录放一些辅助脚本比如串口数据解析的Python小工具。这样分的好处是你拿到仓库后想看图就看图想跑代码就跑代码想仿真就开仿真各取所需互不干扰。而且后续如果我要更新某个部分也不会影响其他目录。2. 硬件设计细节与原理图拆解2.1 主控最小系统电路的关键要点STM32F103C8T6的最小系统说起来简单但有几个地方特别容易出错。首先是供电F103的工作电压是2.0V到3.6V典型值3.3V。我见过有人直接接5V芯片当场发烫。所以原理图里我用了AMS1117-3.3这颗LDO输入5V输出3.3V。输入输出端各放一个10uF的钽电容和一个0.1uF的陶瓷电容大电容储能小电容滤高频这是标准做法。其次是复位电路我用了10K电阻上拉到3.3V配合0.1uF电容到地复位按键并联在电容两端。这里有个细节复位引脚NRST内部有上拉但外部还是建议加上因为内部上拉电阻比较大抗干扰能力弱。电容取0.1uF而不是常见的10uF是因为0.1uF的复位时间常数更短大概1ms左右既能保证可靠复位又不会因为电容太大导致复位信号上升太慢。第三是晶振电路外部高速晶振用8MHz配合两个20pF的负载电容。这里要注意负载电容的值不是随便取的要根据晶振规格书里的负载电容参数来算。公式是CL (C1 * C2) / (C1 C2) Cstray其中Cstray是PCB走线的寄生电容一般取3到5pF。如果晶振规格书要求负载电容是20pF那么C1和C2大概取36pF左右。我实际用的是22pF因为手头只有这个值实测起振没问题但严格来说应该按公式算。第四是启动模式BOOT0和BOOT1各接一个10K电阻到地默认从主闪存启动。如果你要串口下载就把BOOT0跳到3.3V下载完再跳回来。这个跳线帽在原理图上标得很清楚照着接就行。2.2 外设模块的接口分配与电气连接外设接口分配是原理图设计的重头戏。我的原则是功能相关的引脚尽量放在同一组GPIO上方便用寄存器一次性配置。比如OLED用的是SPI接口我就把SCK、MOSI、CS、DC、RES都放在GPIOB上这样初始化的时候只需要使能GPIOB的时钟配置一次寄存器就行。具体分配如下表外设模块引脚GPIO端口功能说明OLEDPA5GPIOASPI1_SCKOLEDPA7GPIOASPI1_MOSIOLEDPB0GPIOB片选CSOLEDPB1GPIOB数据/命令DCOLEDPB10GPIOB复位RESDHT11PA0GPIOA单总线数据ADC采集PA1GPIOAADC1_IN1按键1PB12GPIOB模式切换按键2PB13GPIOB确认串口TXPA9GPIOAUSART1_TX串口RXPA10GPIOAUSART1_RX这里有几个坑要提醒。第一PA13和PA14是SWD调试口默认功能是下载和调试不要拿来当普通GPIO用否则下次下载程序可能就连不上了。第二PB3和PB4是JTAG引脚如果要用普通GPIO功能需要先关闭JTAG复用这个在代码里用GPIO_PinRemapConfig处理。第三DHT11是单总线器件数据线需要接一个4.7K到10K的上拉电阻我用的4.7K实测通信稳定。2.3 电源管理与滤波设计电源部分我单独拿出来说因为这是很多初学者最容易忽略的地方。整个系统的电源路径是USB 5V输入经过一个自恢复保险丝然后分两路。一路直接给需要5V的模块比如某些传感器另一路经过AMS1117-3.3降到3.3V给STM32和OLED供电。滤波方面我在每个电源引脚旁边都放了0.1uF的退耦电容这是基本操作。但有个细节退耦电容要尽量靠近芯片的电源引脚走线越短越好否则电容就白放了。我在PCB布局的时候把0.1uF电容放在芯片背面正对着电源引脚过孔直接连过去效果很好。另外ADC采集的参考电压我用的是VDDA也就是3.3V。为了减少噪声我在VDDA和VSSA之间加了一个1uF的钽电容和一个10nF的陶瓷电容。实测下来ADC的读数波动从原来的正负5个LSB降到了正负2个LSB效果很明显。注意如果你要用ADC采集高精度信号建议单独给VDDA供电或者加一个LC滤波电路把数字电源的噪声隔离开。3. 代码架构与核心模块实现3.1 工程目录组织与分层思想拿到一个STM32工程我第一眼看的就是目录结构。如果所有.c文件都堆在User文件夹里那这个工程基本没法维护。这个项目的代码分了三层Core层放main.c、stm32f1xx_it.c、stm32f1xx_hal_conf.h这些跟芯片启动和中断相关的文件。这一层尽量不动保持CubeMX生成的原样。Drivers层放HAL库文件和芯片外设驱动比如stm32f1xx_hal_gpio.c、stm32f1xx_hal_adc.c。这一层是ST官方提供的除非有bug要修否则不改。User层放我自己写的模块驱动和应用逻辑比如oled.c、dht11.c、adc_app.c、key.c。这一层是项目的核心也是大家最需要看的部分。这样分层的好处是当你换芯片或者换HAL库版本的时候只需要替换Core和Drivers层User层的代码基本不用动。而且调试的时候如果怀疑是硬件问题可以先把User层的逻辑简化只留一个LED闪烁确认底层没问题再往上加。3.2 关键外设驱动代码解析先看OLED驱动。我用的是SPI接口的0.96寸OLED驱动芯片是SSD1306。初始化流程分几步先拉低RES复位延时100ms然后发一系列命令配置显示模式、对比度、扫描方向等。这里有个容易出错的地方SPI的时钟极性CPOL和相位CPHA。SSD1306要求CPOL1CPHA1也就是空闲时SCK为高第二个边沿采样。如果配错了屏幕要么不亮要么显示乱码。// SPI配置关键参数 hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8;再看DHT11驱动。DHT11是单总线协议时序要求比较严格。主机先拉低数据线至少18ms然后拉高20到40us接着释放总线等待DHT11响应。DHT11会拉低80us再拉高80us然后开始传输40位数据。每一位数据以50us的低电平开始高电平的持续时间决定是0还是126到28us是070us是1。// DHT11读一位数据 uint8_t DHT11_ReadBit(void) { uint8_t retry 0; while (DHT11_DATA_READ() retry 100) { retry; delay_us(1); } retry 0; while (!DHT11_DATA_READ() retry 100) { retry; delay_us(1); } delay_us(40); if (DHT11_DATA_READ()) return 1; else return 0; }这里的关键是延时精度。我用的是SysTick定时器做微秒延时精度大概在1us左右。如果你用普通的for循环延时在不同优化等级下延时时间会变导致通信失败。所以强烈建议用定时器做延时基准。3.3 主循环逻辑与任务调度主循环我没有用RTOS因为功能不复杂用时间片轮询就够了。具体做法是SysTick每1ms产生一次中断在中断里给各个任务的计数器加1。主循环里判断计数器是否达到设定值到了就执行对应任务然后清零计数器。// 任务调度结构 typedef struct { void (*task)(void); uint16_t period; uint16_t counter; } Task_t; Task_t tasks[] { {OLED_Refresh, 100, 0}, {DHT11_Read, 2000, 0}, {ADC_Process, 500, 0}, {Key_Scan, 20, 0}, };这样做的优点是结构清晰每个任务的执行周期一目了然。缺点是如果某个任务执行时间过长会影响其他任务的实时性。所以我在每个任务里都尽量避免长时间阻塞比如DHT11读取虽然需要几毫秒但它是每2秒才执行一次影响可以接受。实操心得时间片轮询的周期不要设得太短否则CPU大部分时间都在切换任务实际计算时间反而少了。一般来说按键扫描20ms一次显示刷新100ms一次传感器读取1到2秒一次这个节奏比较合理。4. 仿真验证与调试过程实录4.1 Proteus仿真工程的搭建步骤没有实物板子的时候Proteus仿真就是救命稻草。搭建步骤我一步步说。第一步新建工程在Proteus里选“New Project”然后选一个合适的模板我一般选“Landscape A4”。第二步添加元件在元件库搜索“STM32F103C8”找到后放到原理图里。然后搜索“OLED 128x64 I2C”或者“OLED 128x64 SPI”注意要选对接口类型。DHT11在Proteus里叫“DHT11”ADC输入可以用“POT-HG”电位器模拟。第三步连线。按照前面原理图的引脚分配把元件连起来。这里有个技巧Proteus里的STM32模型引脚名称和实际芯片可能不完全一样比如PA0在模型里可能标的是“PA0/ADC0”你要看清楚。第四步加载程序。双击STM32元件在“Program File”里选编译好的.hex文件时钟频率填8MHz。第五步运行仿真点左下角的播放按钮。仿真能跑起来说明代码逻辑和引脚配置基本没问题。但要注意Proteus仿真和实物有差异。比如DHT11的时序Proteus模型可能比实物宽松实物上能跑的代码在仿真里不一定能跑反过来也一样。所以仿真通过只是第一步最终还是要上实物验证。4.2 常见仿真报错与解决方法仿真过程中我遇到过几个典型问题列出来供大家参考。报错现象可能原因解决方法仿真启动后立即停止时钟配置错误检查SystemInit里的时钟设置仿真时可以用内部HSIOLED不显示SPI引脚接错核对Proteus模型引脚编号和代码定义DHT11读数始终为0上拉电阻缺失在数据线上加一个4.7K上拉电阻ADC读数不变化电位器没接对检查电位器中间抽头是否接到ADC输入引脚串口无输出波特率不匹配确认Proteus虚拟终端波特率和代码一致还有一个坑Proteus的STM32模型不支持某些HAL库功能比如DMA传输、某些中断优先级配置。如果仿真里某个功能死活不工作但在实物上正常那大概率是模型的问题不用死磕。4.3 实物调试与仿真结果的对比分析仿真通过后我打了五块板子做实物验证。对比下来有几个地方仿真和实物表现不一样。第一ADC噪声。仿真里ADC读数非常干净电位器转到哪里读数就稳定在哪里。但实物上读数会有正负几个LSB的波动。这是正常的因为实物有电源噪声、地弹、走线寄生参数。解决办法是加滤波电容或者在软件里做滑动平均滤波。第二DHT11响应速度。仿真里DHT11几乎瞬间响应实物上从上电到第一次成功读取大概需要1到2秒的稳定时间。所以代码里我加了一个上电延时等DHT11内部稳定后再开始读取。第三OLED初始化时间。仿真里OLED上电就亮实物上需要大概100ms的初始化时间。如果初始化太快屏幕可能不亮或者显示异常。我在OLED_Init函数开头加了200ms延时确保SSD1306内部复位完成。提示实物调试时建议先用串口打印关键日志比如“DHT11 init OK”、“ADC value: 1234”这样能快速定位问题出在哪个环节。5. 开源项目维护与复现指南5.1 如何快速复现这个项目如果你拿到这个开源项目想最快跑起来按这个顺序来。第一步看文档。先打开Docs/目录下的README.md里面写了硬件版本、软件版本、依赖库版本。第二步装环境。需要Keil MDK5版本建议5.30以上然后安装STM32F1的芯片包。如果你用CubeMX版本建议6.0以上。第三步编译代码。打开Firmware/目录下的工程文件直接编译应该零错误零警告。第四步下载程序。用ST-Link或者串口下载都行ST-Link更稳定。第五步上电测试。先看OLED有没有显示再看串口有没有输出最后测DHT11和ADC。如果你没有实物板子就走仿真路线。打开Simulation/目录下的Proteus工程加载Firmware/里编译出来的.hex文件直接运行。仿真里能看到OLED显示和串口输出但DHT11和ADC需要手动模拟输入。5.2 代码修改与功能扩展建议这个项目的代码结构留了很多扩展口。比如你想加一个SD卡数据记录功能可以在User/目录下新建sd_card.c和sd_card.h然后在主循环的任务列表里加一个任务每1秒把ADC和DHT11的数据写到SD卡里。SPI接口的SD卡模块引脚可以复用OLED的SPI只需要再加一个片选引脚就行。如果你想改成无线传输比如用ESP8266或者NRF24L01也是类似的做法。在User/目录下加对应的驱动文件然后在任务列表里加一个发送任务。注意无线模块的供电要单独处理因为发射瞬间电流比较大直接从STM32的3.3V取电可能会导致电压跌落最好单独加一个LDO。实操心得扩展功能的时候尽量保持“一个模块一个文件”的原则。不要把新功能的代码塞到main.c里否则主循环会越来越臃肿后期维护很痛苦。5.3 版本管理与协作规范既然是开源项目版本管理就要规范。我用的是Git分支策略是main分支放稳定版本dev分支放开发中的代码每个功能开一个feature/xxx分支开发完合并到dev测试通过后再合并到main。提交信息我要求写清楚三件事改了什么、为什么改、怎么测试的。比如“修复DHT11在低温下读数错误的问题原因是延时函数在低温下偏差变大改用定时器延时实测-10度到50度范围内读数正常”。如果你要提交代码建议先开一个Issue说明你要改什么然后提Pull Request。我会 review 代码风格和功能逻辑没问题就合并。代码风格方面我要求用4空格缩进变量名用小写加下划线函数名用大驼峰宏定义全大写。这些规范写在Docs/CONTRIBUTING.md里提交前先看一遍。6. 踩坑记录与经验总结6.1 硬件设计中的五个典型错误第一个错误晶振不起振。我第一版板子用的是12pF负载电容结果晶振死活不起振。后来查晶振规格书发现要求负载电容是20pF换成22pF后正常。所以画板前一定要看规格书不要凭经验拍脑袋。第二个错误复位电路电容太大。我一开始用了10uF结果复位信号上升太慢芯片有时候启动不了。换成0.1uF后问题解决。复位电容不是越大越好够用就行。第三个错误ADC参考电压不稳。我把VDDA直接接到3.3V没有加滤波电容结果ADC读数波动很大。后来在VDDA和VSSA之间加了1uF加10nF的电容波动明显减小。第四个错误串口TX和RX接反。这个低级错误我犯过不止一次。画板的时候一定要对着芯片手册确认引脚定义TX接RXRX接TX不要想当然。第五个错误LED限流电阻太小。我用了一个100欧的电阻结果LED亮度太高电流超过20mA长期运行可能烧掉。后来换成1K亮度合适电流也在安全范围内。6.2 软件开发中的调试技巧调试STM32我最常用的工具是串口打印和逻辑分析仪。串口打印不用多说在关键位置加printf通过串口助手看输出。逻辑分析仪用来抓时序比如DHT11的单总线波形、SPI的时钟和数据一看就清楚。还有一个技巧是用GPIO翻转做时间测量。在函数入口拉高一个GPIO出口拉低然后用示波器或者逻辑分析仪测高电平持续时间就知道这个函数执行了多久。这个方法比用定时器计数更直观而且不影响函数本身的逻辑。注意用GPIO翻转测时间的时候要选一个空闲的GPIO不要跟其他功能冲突。而且翻转本身也有开销大概几个时钟周期对于微秒级的测量要扣除这个开销。6.3 开源项目维护的个人体会做开源项目这些年我最大的体会是文档比代码重要。代码写得再好如果没有文档别人看不懂项目就传播不开。所以我在Docs/目录里放了引脚分配表、模块说明、常见问题、版本变更记录尽量让新人能自己解决问题。第二个体会是及时回复Issue。有人提Issue说明他对你的项目感兴趣愿意花时间试用。哪怕问题很简单也要认真回复。我一般24小时内回复如果暂时解决不了也会说明原因和预计解决时间。第三个体会是不要追求完美。开源项目不可能一开始就完美先跑起来再迭代。我第一版代码也有很多问题但发出来之后有人用、有人提意见慢慢就完善了。如果等什么都准备好了再发可能永远发不出来。最后分享一个小技巧如果你想让更多人看到你的开源项目可以在README里放一张实物运行的照片或者仿真截图比纯文字描述直观得多。另外把引脚分配表和常见问题放在README最前面能减少很多重复提问。