最近在帮学弟学妹们看嵌入式毕设,发现一个挺普遍的现象:很多基于STM32F103的项目,功能实现得比较“碎”。比如,单独驱动个DHT11温湿度传感器能行,串口打印数据也没问题,但一旦要把传感器数据通过无线模块发出去,还得兼顾低功耗,代码就变得一团乱麻,各种全局变量满天飞,while(1)里塞满了延时和标志位检查。

这其实反映了从“单片机实验”到“嵌入式系统设计”的思维跨越。今天,我就以“温湿度监测+LoRa无线传输”这个非常典型的毕设场景为例,分享一套在STM32F103C8T6这类资源受限的Cortex-M3平台上,构建一个完整、可维护、可演示的毕设项目的实战思路。

温湿度监测场景示意图

1. 痛点分析:为什么你的毕设代码看起来“不专业”?

在动手之前,我们先梳理一下常见问题,这能帮助我们明确设计目标:

  1. 功能碎片化,耦合度高:传感器读取、数据处理、通信发送的代码全部堆在main.c里,修改任何一部分都可能引发连锁错误。
  2. 没有功耗管理:设备需要电池供电时,代码依然在while(1)里空转,电量消耗极快,不符合物联网设备的基本要求。
  3. 代码不可维护:大量使用魔法数字(如0x48),硬件相关的引脚定义散落在各个.c文件,任务调度全靠delay_ms和标志位,可读性差。
  4. 缺乏健壮性考量:通信失败怎么办?传感器偶尔读取出错怎么处理?程序跑飞了如何自救?这些在实际产品中必须考虑的问题,在毕设中常被忽略。
  5. 可演示性差:上电后只有一个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(实时时钟)定时唤醒。

  1. 配置RTC:使用外部低速晶振LSE(32.768kHz)为RTC提供时钟,精度高且功耗低。
  2. 配置唤醒中断:设置RTC的唤醒周期(例如每10秒一次)。
  3. 进入STOP模式:在采集并发送完数据后,关闭外设时钟,调用PWR_EnterSTOPMode()进入停止模式。
  4. 唤醒处理: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(“AT+CFG=433500000,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. 性能与安全性:让项目从“能用”到“可靠”

  1. ADC采样抗干扰:如果使用STM32内部的ADC测量电池电压,需要在采样通道并联一个0.1uF的滤波电容,软件上可以采用多次采样取平均值的算法。
  2. 通信重传机制:上述LoRa_Send_AT_Cmd函数已经有了超时判断。我们可以在此基础上封装一个带重试的发送函数,失败3次后再上报错误。
  3. Flash写保护:如果需要记录历史数据到片内Flash,在写操作前一定要先解锁Flash,写完立即上锁,防止程序跑飞误擦写。同时,注意Flash的寿命(约1万次擦写),需要做均衡磨损处理。
  4. 看门狗独立看门狗IWDG窗口看门狗WWDG都配上。IWDG用于防止程序死锁,喂狗任务放在低优先级。WWDG用于防止程序跑飞,喂狗放在关键循环中。切记:在进入STOP模式前,看门狗必须被暂停或妥善处理,否则会复位。

5. 生产环境避坑指南(硬件篇)

这些是实验室开发容易忽略,但实际做产品时必须考虑的问题:

  1. 晶振负载电容匹配:STM32的8MHz外部晶振(HSE)两脚对地需要接两个20pF左右的负载电容。电容值不匹配会导致晶振不起振或频率不准。最好参考芯片数据手册和晶振厂家推荐值。
  2. 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
    }
    
  3. 电源去耦:每个芯片的VCC和GND之间,尽可能靠近引脚的地方,都要并联一个0.1uF的陶瓷电容和一个10uF的钽电容,用于滤除高频和低频噪声。
  4. 信号完整性:LoRa模块的射频天线接口要严格按照手册设计匹配电路,天线周围要净空,不要走其他信号线。

嵌入式开发硬件调试

总结与展望

通过以上步骤,我们搭建了一个结构清晰、低功耗、健壮的STM32F103毕设项目框架。它不仅仅实现了功能,更展示了你对嵌入式系统设计的多方面思考。

这个项目还有很大的扩展空间,非常适合作为你深入学习的起点:

  • 扩展为多节点网络:可以再做一个相同的节点,让它们将数据发送到同一个LoRa网关,网关再通过4G或以太网上传到云服务器。你可以研究一下简单的LoRaWAN协议或自定一个轻量级的TDMA(时分多址)协议来避免无线冲突。
  • 加入OTA升级功能:将Flash划分为Bootloader区和Application区。Bootloader通过串口或LoRa接收新的固件包,校验后写入Application区,实现远程无线升级。这是物联网设备的必备技能。
  • 接入云平台:在网关端,可以将数据转发到阿里云IoT、腾讯云IoT或ThingsBoard等开源平台,实现数据可视化。

嵌入式开发最有魅力的地方在于,你能亲眼看到自己写的代码如何与物理世界互动。建议你对照这个思路,亲手复现一遍,过程中遇到的每一个错误和解决过程,都是你宝贵的经验。祝你毕设顺利,收获满满!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐