最近在帮学弟学妹们看嵌入式毕设发现一个挺普遍的现象很多基于STM32F103的项目功能实现得比较“碎”。比如单独驱动个DHT11温湿度传感器能行串口打印数据也没问题但一旦要把传感器数据通过无线模块发出去还得兼顾低功耗代码就变得一团乱麻各种全局变量满天飞while(1)里塞满了延时和标志位检查。这其实反映了从“单片机实验”到“嵌入式系统设计”的思维跨越。今天我就以“温湿度监测LoRa无线传输”这个非常典型的毕设场景为例分享一套在STM32F103C8T6这类资源受限的Cortex-M3平台上构建一个完整、可维护、可演示的毕设项目的实战思路。1. 痛点分析为什么你的毕设代码看起来“不专业”在动手之前我们先梳理一下常见问题这能帮助我们明确设计目标功能碎片化耦合度高传感器读取、数据处理、通信发送的代码全部堆在main.c里修改任何一部分都可能引发连锁错误。没有功耗管理设备需要电池供电时代码依然在while(1)里空转电量消耗极快不符合物联网设备的基本要求。代码不可维护大量使用魔法数字如0x48硬件相关的引脚定义散落在各个.c文件任务调度全靠delay_ms和标志位可读性差。缺乏健壮性考量通信失败怎么办传感器偶尔读取出错怎么处理程序跑飞了如何自救这些在实际产品中必须考虑的问题在毕设中常被忽略。可演示性差上电后只有一个LED在闪无法直观展示数据流和系统状态不利于答辩展示。2. 技术选型为你的项目选择合适的技术栈针对“温湿度LoRa上传”的需求我们来做个简单的选型对比操作系统 vs 裸机裸机开发前后台系统适合逻辑简单、对实时性要求不苛刻的小项目。优点是资源占用极小启动快。难点在于需要自己规划好状态机管理好各任务的时间片复杂度稍高就容易变成“面条代码”。FreeRTOS即使是在只有64KB Flash的STM32F103C8T6上裁剪后的FreeRTOS内核也完全能跑起来。它提供了任务调度、队列、信号量等机制能让你以更清晰的方式组织代码。例如可以创建“Sensor_Task”负责采集“Comm_Task”负责发送。对于毕设而言我强烈建议尝试使用FreeRTOS这不仅能大幅提升代码结构更是你简历上的一个亮点。通信模块选型ESP8266/ESP32 WiFi优势是直接接入互联网方便资料多。劣势是功耗较高即使深度睡眠对电池供电不友好且在某些无WiFi覆盖的场合如农田、仓库不适用。LoRa模块如SX1278优势是传输距离远可达数公里功耗极低非常适合野外、楼宇等远距离低速率数据传输场景。劣势是需要额外的网关来接入网络速率慢。对于需要体现“低功耗”和“远距离”特性的毕设LoRa是更出彩的选择。本例我们确定的技术栈为STM32F103C8T6 FreeRTOS DHT11 LoRa模块AT指令版。使用AT指令版的LoRa模块可以简化射频部分的开发让我们更专注于系统集成。3. 核心实现细节构建完整的数据链路一个健壮的系统需要分层设计。我们可以规划为硬件驱动层、数据处理层、任务应用层。3.1 DHT11单总线驱动编写DHT11的时序要求严格必须用微秒级延时。在FreeRTOS中直接使用vTaskDelay()不行因为它最小单位是Tick通常1ms。我们需要一个精准的微秒延时函数通常用SysTick或定时器实现。// dht11.h - 集中管理硬件定义和接口 #ifndef __DHT11_H #define __DHT11_H #include “stm32f10x.h” // 引脚定义修改这里即可适配不同硬件连接 #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_12 #define DHT11_RCC_APB2Periph RCC_APB2Periph_GPIOB // 函数声明 void DHT11_Init(void); uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity); #endif// dht11.c - 关键时序实现 #include “dht11.h” #include “delay.h” // 需要实现一个us级延时库 static void DHT11_IO_Out(void) { /* 配置为推挽输出 */ } static void DHT11_IO_In(void) { /* 配置为浮空输入 */ } static uint8_t DHT11_Read_Bit(void) { // 等待低电平结束 while(DHT11_DATA_IN() 0); // 延时40us后检测引脚电平高则为‘1’低则为‘0’ delay_us(40); if(DHT11_DATA_IN() 1) return 1; else return 0; } uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] {0}; uint8_t i, j; // 主机发起开始信号 DHT11_IO_Out(); DHT11_DATA_OUT(0); delay_ms(18); // 至少18ms低电平 DHT11_DATA_OUT(1); delay_us(30); // ... 省略等待从机响应和40位数据读取的代码 ... // 校验和数据 if(buf[4] (buf[0]buf[1]buf[2]buf[3])) { *humi buf[0]; *temp buf[2]; return 0; // 成功 } return 1; // 校验失败 }3.2 低功耗与RTC唤醒要实现低功耗就不能让MCU一直全速运行。我们可以让系统大部分时间处于STOP模式功耗极低然后通过RTC实时时钟定时唤醒。配置RTC使用外部低速晶振LSE32.768kHz为RTC提供时钟精度高且功耗低。配置唤醒中断设置RTC的唤醒周期例如每10秒一次。进入STOP模式在采集并发送完数据后关闭外设时钟调用PWR_EnterSTOPMode()进入停止模式。唤醒处理RTC唤醒中断发生后系统会继续执行中断服务程序然后回到主循环。注意从STOP模式唤醒后系统时钟会重置为HSI需要重新配置系统时钟。// power_mgr.c 节选 void Enter_LowPower_Mode(void) { // 1. 挂起FreeRTOS调度器防止任务切换 vTaskSuspendAll(); // 2. 关闭不需要的外设时钟 (如GPIO, USART等) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, DISABLE); // 3. 配置所有未使用的IO为模拟输入以省电 GPIO_Analog_Config(); // 4. 进入STOP模式可通过RTC或外部中断唤醒 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 5. 唤醒后恢复系统时钟HSE/PLL SystemClock_Config(); // 6. 重新初始化必要的外设 USART1_Init(); // 7. 恢复FreeRTOS调度器 xTaskResumeAll(); }3.3 USART与LoRa模块的AT指令通信AT指令通信的关键在于稳定性和超时处理。绝不能因为一次没收到回复就死等。// lora_driver.c 节选 #define LORA_UART USART1 #define LORA_RECV_TIMEOUT_MS 2000 // 接收超时2秒 // 发送AT指令并等待预期响应 uint8_t LoRa_Send_AT_Cmd(const char *cmd, const char *expect_resp, uint16_t timeout_ms) { uint8_t recv_buf[256] {0}; uint16_t recv_len 0; uint32_t start_tick xTaskGetTickCount(); // 清空接收缓冲区 UART_ClearRxBuffer(LORA_UART); // 发送指令 printf(“%s\r\n”, cmd); // 假设printf重定向到LORA_UART // 循环读取直到收到预期响应或超时 while((xTaskGetTickCount() - start_tick) pdMS_TO_TICKS(timeout_ms)) { if(UART_ReceiveString(LORA_UART, recv_buf, recv_len)) { if(strstr((char*)recv_buf, expect_resp) ! NULL) { return 0; // 成功 } } vTaskDelay(pdMS_TO_TICKS(10)); // 让出CPU避免忙等 } return 1; // 超时失败 } // 在任务中调用 void LoRa_Send_Data_Task(void *pvParameters) { uint8_t temp, humi; char send_buf[64] {0}; // 初始化LoRa模块 if(LoRa_Send_AT_Cmd(“ATCFG433500000,5,0,0,7,0,0,0,0,3000,8,8”, “OK”, 3000) ! 0) { // 初始化失败可以重试几次或记录错误 LOG_ERROR(“LoRa Init Failed!”); } while(1) { // 等待传感器数据就绪信号量 if(xSemaphoreTake(xSemaphore_SensorReady, portMAX_DELAY) pdTRUE) { // 读取全局变量中的传感器数据需注意互斥访问 temp g_current_temp; humi g_current_humi; // 组装JSON格式数据 snprintf(send_buf, sizeof(send_buf), “{\“temp\”: %d, \“humi\”: %d}”, temp, humi); // 发送数据 (假设模块支持直接发送) LoRa_Send_AT_Cmd(send_buf, “OK”, 2000); } } }4. 性能与安全性让项目从“能用”到“可靠”ADC采样抗干扰如果使用STM32内部的ADC测量电池电压需要在采样通道并联一个0.1uF的滤波电容软件上可以采用多次采样取平均值的算法。通信重传机制上述LoRa_Send_AT_Cmd函数已经有了超时判断。我们可以在此基础上封装一个带重试的发送函数失败3次后再上报错误。Flash写保护如果需要记录历史数据到片内Flash在写操作前一定要先解锁Flash写完立即上锁防止程序跑飞误擦写。同时注意Flash的寿命约1万次擦写需要做均衡磨损处理。看门狗独立看门狗IWDG和窗口看门狗WWDG都配上。IWDG用于防止程序死锁喂狗任务放在低优先级。WWDG用于防止程序跑飞喂狗放在关键循环中。切记在进入STOP模式前看门狗必须被暂停或妥善处理否则会复位。5. 生产环境避坑指南硬件篇这些是实验室开发容易忽略但实际做产品时必须考虑的问题晶振负载电容匹配STM32的8MHz外部晶振HSE两脚对地需要接两个20pF左右的负载电容。电容值不匹配会导致晶振不起振或频率不准。最好参考芯片数据手册和晶振厂家推荐值。JTAG/SWD引脚复用冲突PA13, PA14, PA15, PB3, PB4这些调试引脚在上电后默认是JTAG功能。如果你要用它们作为普通GPIO比如控制LED必须在程序一开始就禁用JTAG仅使能SWD占用引脚少。// 在SystemInit()之后main()最开始调用 void JTAG_Disable(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 禁用JTAG保留SWD }电源去耦每个芯片的VCC和GND之间尽可能靠近引脚的地方都要并联一个0.1uF的陶瓷电容和一个10uF的钽电容用于滤除高频和低频噪声。信号完整性LoRa模块的射频天线接口要严格按照手册设计匹配电路天线周围要净空不要走其他信号线。总结与展望通过以上步骤我们搭建了一个结构清晰、低功耗、健壮的STM32F103毕设项目框架。它不仅仅实现了功能更展示了你对嵌入式系统设计的多方面思考。这个项目还有很大的扩展空间非常适合作为你深入学习的起点扩展为多节点网络可以再做一个相同的节点让它们将数据发送到同一个LoRa网关网关再通过4G或以太网上传到云服务器。你可以研究一下简单的LoRaWAN协议或自定一个轻量级的TDMA时分多址协议来避免无线冲突。加入OTA升级功能将Flash划分为Bootloader区和Application区。Bootloader通过串口或LoRa接收新的固件包校验后写入Application区实现远程无线升级。这是物联网设备的必备技能。接入云平台在网关端可以将数据转发到阿里云IoT、腾讯云IoT或ThingsBoard等开源平台实现数据可视化。嵌入式开发最有魅力的地方在于你能亲眼看到自己写的代码如何与物理世界互动。建议你对照这个思路亲手复现一遍过程中遇到的每一个错误和解决过程都是你宝贵的经验。祝你毕设顺利收获满满