做嵌入式这几年最烦的一件事就是网上找了半天终于看到一个看着不错的STM32项目结果要么只有代码没有原理图要么原理图画得乱七八糟跟代码根本对不上。更别提仿真的东西了十个开源项目里能有一个带仿真就算运气好。所以这次我把一个完整可跑的环境监测项目全套开源出来代码、原理图、仿真三件套齐活。主控用的最常见的STM32F103C8T6搭配DHT11温湿度传感器和HC-SR04超声波测距模块数据实时显示在0.96寸OLED屏上两个按键切换页面一个蜂鸣器做距离报警串口同时把数据吐给电脑上位机。适合正在做课程设计、毕业设计以及刚入门嵌入式想找一份完整参考项目的老哥直接抄作业。先说一下这个项目开源的范围和价值。代码是基于标准外设库写的从GPIO配置到传感器驱动全部手撸没有用CubeMX一键生成所以每一行都能看懂。原理图用嘉立创EDA画的包含了最小系统、电源、传感器接口、蜂鸣器驱动这些完整模块不是那种网上流传的残缺图。仿真部分我做了两套一套在Wokwi在线平台跑逻辑验证一套在Proteus里跑电路仿真。你要是想复现手里只要有块十几块钱的STM32F103C8T6蓝板或者核心板就能上手成本撑死五十块。1. 项目整体设计与规划思路1.1 需求拆解与功能定位很多人拿到一个项目就开始写代码这是错误的。我习惯先把需求拆成功能点再对照着做技术选型。这个环境监测终端最核心的功能有四块采集温湿度、测量障碍物距离、本地显示、数据上报。围绕这四个功能我把系统分成传感器层、处理层、展示层和输出层层次之间通过明确的接口解耦。传感器层DHT11负责温湿度采集HC-SR04负责超声波测距两者都是嵌入式入门最常见的传感器资料多、便宜、驱动逻辑有代表性。处理层STM32F103C8T6做主控负责传感器时序驱动、数据计算、按键扫描、页面状态切换。展示层0.96寸OLED屏幕I2C接口SSD1306驱动芯片实时显示当前温湿度和距离。输出层串口1接到电脑用printf把数据格式化打印出来方便调试和二次开发。这样拆完每个模块的职责就很清晰了。你在写代码的时候不会出现这个变量到底是给谁用的这种混乱。后续不管是加蓝牙、加Wi-Fi模块还是在OLED上增加图标都只要在对应的层里改动就行。这个架构本身也是面试时能拿出来讲的亮点说明你有模块化设计的意识不是把代码堆一坨。1.2 芯片选型与外设资源规划选STM32F103C8T6不是因为它强而是因为它够用且皮实。这颗芯片有64KB Flash、20KB RAM主频72MHz价格在正规渠道不过十块钱上下开发板和学习资料多得离谱。对于这个项目20KB RAM跑几个传感器加OLED绰绰有余64KB Flash塞下整个标准库工程也毫无压力。选完芯片紧接着要做的一件事是引脚分配。GPIO资源是嵌入式项目的硬约束分配不好后期要么飞线要么改板子极其痛苦。这个项目的引脚分配如下我直接放表给大家抄外设引脚说明DHT11 数据线PB0单总线需要上拉电阻HC-SR04 TRIGPA0触发信号输出10us以上高电平HC-SR04 ECHOPA1回波信号输入捕获/电平检测OLED SCLPB10I2C时钟线OLED SDAPB11I2C数据线蜂鸣器PB12低电平驱动或加三极管按键KEY1PB13页面切换内部上拉按键KEY2PB14报警开关内部上拉串口1 TX/RXPA9/PA10调试与数据上报这里有个容易被新手忽略的细节分配引脚时要观察芯片手册里的复用功能冲突表。比如你既要使用串口又要使用USART1的引脚做PWM那就得提前避开。我的习惯是先把所有外设列出来标注它们最常用的默认引脚然后再检查有没有冲突。这个项目里PA9/PA10默认就是USART1PB10/PB11是I2C2所以直接复用默认功能省去了重映射的麻烦。2. 原理图设计核心细节2.1 最小系统与电源电路原理图是整个项目的硬件根基我先说最小系统。STM32F103C8T6的最小系统包含四块供电、时钟、复位、启动模式选择。很多人从网上随便抄一份最小系统图结果画完板子焊上芯片不跑十有八九是VDDA没滤波、VCAP电容没接或者BOOT引脚悬空。电源部分我用的是USB 5V输入经过AMS1117-3.3稳压到3.3V给MCU供电。AMS1117虽然老但稳定性足够、成本低、焊起来也容易。这里要注意输入和输出端都要加10uF和100nF的滤波电容缺一个高负载下就可能出现电压纹波导致单片机随机重启。VDDA引脚也要单独接一个1uF电容到地模拟数字转换器的参考电压才干净否则ADC采样数据会跳动。时钟部分外接8MHz晶振两个22pF负载电容这是F103的标准接法。晶振下面尽量铺地隔离不要在晶振下方走数字信号线。复位电路用10k电阻上拉复位脚到3.3V按键接地这是最常规的设计。启动模式上BOOT0串10k电阻下拉到地BOOT1不用管因为BOOT0为0时芯片从Flash启动也就是正常运行状态。我见过有人把BOOT0悬空结果芯片上电偶尔不跑程序这就是典型的下拉没做好的问题。2.2 传感器接口与电平匹配原理图里最容易出问题的不是MCU本身而是外设的电平匹配。DHT11数据线直接接PB0但要额外加一个4.7k到10k的上拉电阻到3.3V。DHT11是单总线协议数据线在空闲时必须保持高电平设备靠拉低总线来发起通信。如果没有上拉通信时序在边界处会不稳定表现出来就是读取的湿度值偶尔跳变、隔几分钟丢一次数据。HC-SR04超声波模块这里有个大坑我一定要重点说。这个模块官方建议VCC接5VECHO输出电平也是5V但F103的GPIO参考电压是3.3V直接把5V灌进PA1虽然F1系列引脚标称5V容忍但长期用不放心而且如果你换了一块没有5V容忍特性的型号比如某些F0、F4的引脚立马烧。正确的做法是在ECHO到PA1之间加一个电阻分压我用的是1k串330欧分压把5V降到3.3V以内。TRIG是输入引脚STM32输出3.3V的触发信号模块识别没问题所以不用管。OLED模块就比较省心了主流的0.96寸I2C OLED模组自带4.7k上拉电阻直接接PB10、PB11就行。但要注意模块供电接3.3VDHT11也是3.3V供电统一在一个电源域下最省事。USB口的串口转换芯片不一定每个板子都有如果你的开发板没有板载USB转串口原理图里可以预留一个4Pin排针——TX、RX、GND、5V方便外接CH340小板。2.3 原理图绘制工具与经验这个项目我用的是嘉立创EDA画原理图原因很实在免费、中文、元件库全、能直接打样对于学生和个人开发者来说是最省钱省力的方案。如果你是第一次画原理图有两点经验直接决定你后面会不会返工。第一网络标签要命名规范。电源用3V3、5V、GND并把相同网络名的引脚用标签连起来而不是跨越很长的线去连这样图面干净检查起来一目了然。千万不要画一根线从屏幕左边拉到右边那既难改图也容易连错。我画到传感器接口时直接用标号DHT11_DATA、TRIG、ECHO后面写代码时一一对应不用再去猜。第二画完一定要跑电气规则检查也就是ERC。嘉立创EDA里有这个功能会自动报出未连接的引脚、输出短路、电源接地异常等常见问题。另外出图前养成习惯检查一下每个电源引脚是否都接了去耦电容特别是MCU的VDD和VDDA。很多新手画的原理图看着功能都对但忘记了MCU每个VDD脚对应的滤波电容高速切换IO时电源电压会被拉出毛刺这就属于电气规则检查也发现不了的隐患。3. 代码架构与实现3.1 工程搭建与开发环境配置代码开发环境我选了Keil MDK5这基本是ST生态最广泛的IDE。但很多新手装完Keil会发现Device列表里根本没有ST的芯片这不是安装失败而是没装器件支持包。你需要在Pack Installer里搜索并安装STM32F1系列的DFP或者去Keil官网直接下载对应Pack包安装否则第一关就卡死。选完器件后工程配置里还要在C/C选项卡的Define中添加USE_STDPERIPH_DRIVER和STM32F10X_MD前者是标准外设库需要的宏后者告诉库文件当前编译的芯片是中等容量型号。编译选项里把MicroLIB勾上这样printf重定向的时候可以省下不少Flash空间。我在这份开源代码里刻意不用HAL库全程用标准外设库。原因有三第一这个项目的驱动逻辑并不复杂寄存器操作更直观对于学习GPIO、定时器、串口这些基础外设反而更有帮助第二网上DHT11、HC-SR04的成熟驱动例程绝大多数是标准库的你如果遇到问题一搜就能找到对照参考第三标准库编译出来的二进制比HAL小很多烧录和运行也更快。如果你是用CubeMX建工程也没关系把外设底层换成你的HAL初始化就行应用层代码可以直接搬。工程目录结构我也做了分层不搞乱七八糟的一堆裸文件project/ ├── App/ # main.c、主循环状态机、中断处理 ├── BSP/ # 板级驱动led、key、buzzer ├── Driver/ # 传感器驱动DHT11、HC-SR04、OLED ├── StdPeriph/ # ST标准外设库 └── Output/ # 编译生成的hex/bin驱动层的头文件里统一暴露初始化函数和读写函数应用层只调接口不看寄存器细节。这样的好处是后期换传感器型号时只需要改Driver层App层一行不动。3.2 驱动代码的关键时序DHT11看着简单其实时序非常讲究。通信的第一步是主机拉低总线至少18ms我写的20ms留余量然后释放总线并延时20到40us等待DHT11应答。DHT11会先把总线拉低80us再拉高80us表示准备好发数据了。接着输出40bit数据1字节湿度整数、1字节湿度小数、1字节温度整数、1字节温度小数、1字节校验和。每个bit的编码方式是高电平持续的时间长短26到28us代表070us左右代表1。读取代码里最怕的就是用纯粹的延时函数去卡时间比如delay_us(30)因为延时函数本身就有误差碰上中断还可能被截断。我采用的方案是用STM32内部的DWT计数器它是个32位的硬件计数器每经过一个CPU时钟周期就加一精确度非常高。读取的时候先等待总线变低表示bit开始然后延时30us做中间采样这时候如果是0总线已经拉低了如果是1总线还在高电平。采样完成后等待本次bit结束继续下一个bit。uint8_t DHT11_Read_Byte(void) { uint8_t val 0; for (uint8_t i 0; i 8; i) { while (DHT11_DQ_IN() 0); // 等待50us的低电平结束 delay_us(30); // 在bit中间点采样 if (DHT11_DQ_IN() 1) val | (0x80 i); // 高电平长则判定为1 while (DHT11_DQ_IN() 1); // 等待本次bit结束 } return val; }读完之后必须做校验和判断前四个字节求和后的低8位应该等于第五个字节。不相等就直接丢弃这次数据返回上一个有效的温湿度值而不是把脏数据传给应用层。这相当于给传感器数据加了一层保险能大幅减少偶发的跳变显示。HC-SR04的驱动核心是测高电平时间。TRIG引脚给一个大于10us的高电平脉冲模块会自动发出8个40kHz的超声波脉冲并等待回波然后把ECHO引脚拉高。距离越远高电平持续的时间就越长。距离和时间的换算公式是距离等于声速340米每秒乘以时间再除以2简化成厘米每微秒就是0.017厘米。我在项目里用的是DWT计数法先等ECHO变高记录当前计数值再等ECHO变低记录结束计数值相减后除以系统内核时钟频率换算成微秒。这块有一个非常关键的细节读出时间后必须加超时判断。如果传感器前方没有障碍物或者接线松动ECHO可能一直保持高电平不下降程序就会卡在等待低电平的while循环里出不来。我设置的超时上限是150ms超过这个时间强制将本次测量标记为无效距离显示为0或显示OUT。转账到实际应用中这个超时判断就是程序健壮性的分水岭很多人的超声波测距程序在自己板子上测得好好的装到小车上一跑就死机多半就是卡死在这个地方。3.3 应用层逻辑与状态机主循环的结构我用了一个简单的状态机来管理UI页面而不是直接线性执行。状态机的好处是扩展性好以后想加页面就多一个枚举和分支不会把main函数堆成面条。这个项目一共三个页面页面一温湿度大数字显示顶部是湿度中间是温度底部排显示测量的时间序列号。页面二距离显示大数字显示当前距离旁边用字符画模拟一个柱状图直观显示距离远近。页面三串口状态页显示串口上报的累计次数和当前蜂鸣器开关状态。按键扫描放在SysTick中断里每5毫秒触发一次检测到电平变化后延时10ms再次采样确认这是最基础的消抖。同时按键状态用边缘触发按下瞬间执行动作而不是高电平期间反复执行这样就不会按一次键却连续切换三个页面。蜂鸣器报警逻辑也放在主循环里当距离小于阈值比如30厘米并且报警开关打开时蜂鸣器以1Hz频率鸣叫距离大于阈值或者报警关闭时静音。这里用GPIO翻转加SysTick计时实现PWM效果没有用定时器的PWM输出因为对于单纯报警来说硬件PWM反而浪费一个定时器资源。当然这是取舍问题项目里我特意把PWM控制的注释留在代码里如果谁想让蜂鸣器音调渐变可以直接改成定时器PWM方案。串口部分用重定向fputc把printf定向到USART1波特率设置为115200。代价是勾选Keil的MicroLIB选项这样就不用额外移植printf。注意芯片的主频配置外部8MHz晶振经PLL倍频到72MHz如果这个配置不对串口的波特率会实际偏移表现出来就是字符乱码或者莫名丢字节。烧录后我一般先用逻辑分析仪抓TX引脚的电平宽度确认一帧起始位和停止位的时间是否符合115200的规定这一步排查效率比瞎猜高得多。4. 仿真环境的搭建与验证4.1 仿真平台怎么选做仿真的目的不是替代实物而是让你在画板子投钱之前先把代码逻辑和电路结构验证一遍。我对仿真平台做了一个对比直接放结论平台优势劣势适用场景Wokwi在线免安装、代码一键跑、支持DHT11/OLED等模型传感模型理想化、外设种类有限快速验证代码逻辑、学习阶段Proteus电路仿真完整、元件库大、可模拟单片机外围安装麻烦、仿真速度一般验证原理图、教学演示嘉立创EDA仿真与原理图无缝衔接元件库中STM32模型支持有限偏硬件的电路验证对于这个项目我在Wokwi里跑的是逻辑仿真目的是验证DHT11的读时序、OLED显示、按键和蜂鸣器这些纯代码逻辑。Wokwi里可以直接放置一个STM32F103C8T6芯片先初始化GPIO连上一颗DHT11模型和SSD1306 OLED模型代码写好直接编译运行。网页版的好处是省去搭建开发环境的功夫而且模型是可视化的温湿度变化在界面上能直接看到。Wokwi的DHT11组件有个很有意思的交互你可以直接拖动界面上温湿度的滑块改变传感器数值代码里读到的数据会随之变化这就把传感器仿真的体验做实了。HC-SR04超声波模块在Wokwi平台上没有现成的模型我采用的是自定义GPIO模拟方案用两个按键分别模拟回波信号的产生和结束。按下回波开始按键PA1引脚拉高代码里DWT计数器启动按下回波结束按键PA1引脚拉低计数器停止这样就能在仿真环境里模拟不同时长的ECHO信号进而验证距离计算公式和超时机制。虽然在按钮时序上要手动操作不如真实模块方便但关键代码路径是实打实跑过的。4.2 Proteus电路仿真实操记录如果想把原理图层面的电路也仿真验证一遍我推荐Proteus。Proteus里直接搭建一个STM32F103C8T6的原理图工程把晶振、复位、电源这些最小系统都放上去再把DHT11、OLED和按键连好。需要强调的一点是Proteus里的DHT11模型默认好像也只有固定数值但总比完全没有强至少能验证上拉电阻和引脚连接是否正确。Proteus的主流流程是把Keil编译出来的hex文件加载到单片机模型上然后点运行。很多人会卡在加载hex后单片机没反应这一步原因大多数不是因为电路错误而是Target芯片型号和Proteus里的器件配置不一致。Keil选的是STM32F103C8Proteus里放的也必须是同一型号晶振频率也要对应上。如果这些都对了OLED不会立刻显示出来因为它要等代码里的初始化完成代码里如果有个几百毫秒的延时或者启动按键检测界面上看起来就是等几秒才有反应这属于正常流程。我在Proteus里比较看重的是虚拟示波器和虚拟终端这两个工具。虚拟终端接在USART1的TX上能直接看到printf打出来的温湿度数据和测距结果检查数据是否和代码里预期一致。如果你在调试超声波的时间测量用虚拟示波器挂在ECHO引脚能观察到高电平持续的波形宽度配合计算出的距离值确认公式对不对。这个思路比在实物上用万用表或逻辑分析仪来得直接也适合教学演示场景。4.3 仿真与实物的差异在哪里仿真能做的终究有限这一点必须有清醒的认知。第一个差异是时序的精度。Wokwi和Proteus里传感器模型都是理想化的DHT11拉低总线的时间和真实传感器完全一致但你在仿真里读出的温湿度变化曲线是平滑的而真实DHT11在快速变化的空气环境里数据可能会有1到2度的波动。实物的抖动才是常态代码里必须做滤波和数据有效性判断。第二个差异是电平的电气特性。Proteus不会告诉你5V的ECHO直接接到3.3V引脚上会不会有隐患它默认你接得对就能跑。仿真跑通不等于实物安全所以原理图里我依然坚持用电阻分压。同理你在仿真里看到的OLED只是逻辑电平不会反映模块供电不足导致白屏这类实际问题。第三个差异是调试手段。仿真平台里断点随便下、变量随便看但在实物上要用ST-Link和调试器。我建议的流程是先用Wokwi把代码逻辑跑通再用Proteus验证电路连接最后才打样焊接实物。这个流程能帮你避开至少70%的调试时间浪费。说实话我自己也跳过Proteus直接画板结果板子回来后DHT11死活读不到数据查了两天才发现是某个去耦电容少画了一个教训相当深刻。5. 常见问题与排查技巧实录5.1 高频问题速查表把我在开发过程中踩过的坑以及群友高频问过的问题整理成一张表直接对照检查现象可能原因排查方向DHT11读不到数据上拉电阻缺失、引脚配置错、时序延时不准示波器抓DATA引脚确认起始信号和应答信号DHT11湿度偶尔跳变校验和没过、数据线过长、供电不稳代码里加强校验3.3V供电短线连接超声波总显示0或超时ECHO没接对、TRIG脉冲不够宽、前有障碍物多示波器/仿真截图看ECHO波形确认触发和回波OLED一直白屏I2C地址错误、SDA/SCL接反、模块供电不足扫描I2C地址确认0x3C还是0x3D串口乱码波特率不匹配、系统时钟配置错误确认PLL倍频正确换9600测一次Keil编译报大量错误芯片型号宏没加、库路径没配置好检查Defines里有没有STM32F10X_MDST-Link烧录连不上驱动缺失、接线错、BOOT0被拉高设备管理器看USB设备确认BOOT0接地烧录完程序不启动没勾选Reset and Run、复位电路有问题手动按一下复位键Flash Download里勾选第一行和第四行的坑最常见。DHT11我在实物上踩过最狠的一次是板子上电后第一次读取正常第二次开始读到0xFF最后排查发现是GPIO模式配置错了。单总线协议下数据线要是配置成推挽输出上拉电阻相当于没有读的时候引脚被强制拉高或者拉低自然读不到正确的bit序列。正确做法是主机输出阶段配置成推挽输出输入阶段要切换为浮空输入或开漏输出再读电平。5.2 排查工具与思路我的排查思路始终遵循一条主线信号在哪一步断了就抓哪一步的信号。硬件检查用万用表和逻辑分析仪软件检查用断点调试和串口打印仿真就靠Wokwi和Proteus的示波器。三个手段配合起来没有解决不了的嵌入式问题。给大家推荐一个实用组合工具ST-Link V2十几块钱那种就够用串口调试助手逻辑分析仪或者示波器。ST-Link既负责烧录也负责在线调试联合Keil可以设置断点、实时查看全局变量的值。串口调试助手用来验证数据上报。逻辑分析仪在排查I2C时序和DHT11单总线时序时价值极大几十块钱的采样率就够抓一个完整的bit流用插件解析电平宽度比盯着示波器数格子准确得多。我在调OLED I2C卡了一晚上的时候就是靠逻辑分析仪发现某一个字节的ACK位时序不对而这在单纯看代码时根本发现不了。另外调试阶段建议把串口打印做成一个统一的调试模块预留一个宏开关。项目跑稳定后你可以一键关掉所有printf输出释放串口资源。否则在实物上做压力测试时大量的串口打印会干扰时序尤其是DHT11这种微秒级的单总线协议打印一个字符就可能错过了电平跳变边缘辛辛苦苦调好的传感器数据又乱了。这个宏开关是很多成熟工程的标配但新手往往意识不到。还有一个很有效的排查技巧把代码里的关键状态变量暴露在OLED上。比如DHT11的原始数据、校验结果、超声波每次测量消耗的微秒数这些临时调试信息在页面三上滚动显示。这样不需要接串口线也能在实物上看到程序的运行状态尤其适合电池供电或板子已经固定在设备里的场景。调试完毕再把调试页面隐藏即可。最后的经验之谈这个项目从零开始到整理完开源前后花了三周。我最大的体会是做嵌入式项目一定不要想着一次到位先画出一个能跑的最小版本然后在这个基础上一点点加功能每次加一个功能都验证一次。我见过太多人一上来就想把温湿度、测距、蓝牙、Wi-Fi、云端全部集成结果代码写了一千行板子画了两版最后还是点亮一颗LED的水平。对于刚入门的朋友我给一条具体的行动路径先拿到开发板点亮LED再跑通串口printf然后是按键中断加消抖接着用一个传感器比如DHT11采集数据并串口打印最后把OLED转起来做显示。这五个步骤就是嵌入式入门的核心路径任何一个步骤卡住都在网上能搜到海量资料。这个开源项目的代码、原理图和仿真恰好覆盖了这五个步骤你可以把它当成一个带索引的脚手架在自己做课程设计或毕设时直接参考和改写。最后再分享一个小技巧如果你打算把这个项目扩展到实际应用比如做一个家庭的自动通风控制器只要在这个架构上加一个继电器控制风扇、加一个阈值判断逻辑就能实现。代码里每一个模块都是独立文件改起来非常顺手。项目的所有文件我都放在一个压缩包里原理图能直接打开代码编译一次通过有任何问题也欢迎在评论区留言交流。
