最近在帮学弟学妹们看毕业设计,发现一个挺普遍的现象:很多偏硬件的项目,想法不错,但最后要么是代码跑不通,要么是电路板焊出来一堆问题,或者干脆就是一堆模块堆在一起,离一个“可用的系统”差很远。这让我想起了自己当年做毕业设计时踩过的坑,所以决定结合一个具体的例子——温控物联网终端,把从需求到可部署源码的全过程梳理一遍,希望能给正在为毕设发愁的你一些实实在在的帮助。

温控物联网终端示意图

1. 毕业设计中的那些“坑”:我们到底在解决什么问题?

在做偏硬件的毕设时,大家遇到的困难其实很相似。我总结了一下,主要有这么几个痛点:

  • 资源受限:不像实验室有示波器、逻辑分析仪,个人手头可能只有一块万用表和一台电脑。这意味着调试手段有限,很多问题需要靠“推理”和“试错”。
  • 软硬协同困难:软件同学不懂硬件,硬件同学写代码吃力。一个简单的传感器读数,可能因为I2C上拉电阻没加、时序不对、电源不稳,导致几天都调不通。
  • 文档与代码缺失:网上很多教程只讲单个模块(比如“STM32驱动DHT11”),但如何把这些模块组合成一个稳定运行的系统,中间的逻辑、状态机、错误处理,往往没有现成的、注释清晰的代码可以参考。
  • “一次性”工程:代码写得很随意,没有模块化,电路图也画得乱。等答辩老师问起某个细节,或者需要修改功能时,自己都看不懂当初是怎么做的了。

我们这个“温控物联网终端”项目,就是为了正面解决这些问题。它麻雀虽小,五脏俱全:需要采集温度(模拟信号处理),控制加热/制冷(功率驱动),通过无线方式上报数据(通信协议),还要考虑省电(低功耗设计)。搞定它,你对一个完整嵌入式系统的认知会上一个大台阶。

2. 核心器件选型:为什么是STM32F103,而不是ESP32?

选型是第一步,也是最容易让人纠结的。这里以最常见的STM32F103C8T6(蓝色药丸核心板)和ESP32-C3为例做个对比。

  • STM32F103C8T6(Cortex-M3内核)

    • 优势:经典,资料巨多,社区资源丰富。外设齐全,特别是ADC、定时器、PWM等模拟和电机控制相关外设性能稳定且易于配置。Keil/IAR开发环境成熟,仿真调试方便。价格便宜,核心板20元左右。
    • 劣势:本身不带无线功能,需要外接Wi-Fi(如ESP-01S)或NB-IoT模块。主频72MHz,性能对于复杂物联网协议栈(如直接跑MQTT)有些吃力。
    • 适用场景:对实时性、模拟信号采集精度、多路PWM控制有要求的场景,比如我们的温控系统(需要高精度ADC测温,PWM控制半导体制冷片)。
  • ESP32-C3(RISC-V内核)

    • 优势:内置Wi-Fi和蓝牙,单芯片解决通信问题。开发环境(ESP-IDF、Arduino)对网络支持友好,上手快。功耗管理优秀。
    • 劣势:ADC精度和稳定性通常不如专门的MCU。外设数量和灵活性可能稍逊。在强电磁干扰的电机控制场景下,需要更仔细的PCB设计。
    • 适用场景:以网络连接为核心、对模拟精度要求不极端、希望快速联网的应用。

我们的选择:对于“温控物联网终端”,温度采集的稳定性和精度是核心,同时PWM控制制冷片也需要稳定的定时器。因此,我们选择STM32F103C8T6作为主控,搭配一个ESP-01S Wi-Fi模块负责通信。这样既能发挥STM32在模拟控制和实时性方面的长处,又能通过串口AT指令让ESP-01S处理复杂的网络连接,各司其职,系统更稳定。

3. 软硬件协同设计核心逻辑

这是项目的灵魂。硬件设计和软件架构必须一起考虑。

硬件框架

  1. 传感器端:采用PT100铂电阻配合运算放大器电路,将温度变化转换为0-3.3V电压信号,送入STM32的ADC引脚。为什么不用DHT11?因为工业级温控需要更高精度和更宽的量程。
  2. 执行端:使用MOS管驱动电路,由STM32的PWM输出控制,来调节半导体制冷片(TEC)的功率,实现加热或制冷。
  3. 通信端:STM32的USART2通过TTL电平与ESP-01S模块连接,发送AT指令进行数据上报。
  4. 电源:特别重要!采用两级稳压:外部12V输入,先通过DC-DC降压到5V给TEC和ESP-01S供电,再通过LDO降压到3.3V给STM32和传感器供电,以隔离数字噪声对模拟前端的影响。

软件核心逻辑(以伪代码逻辑说明): 整个程序围绕一个主循环和几个中断展开,遵循“前后台”系统模型。

// 全局变量与状态定义
typedef enum { STATE_IDLE, STATE_SAMPLING, STATE_CONTROLLING, STATE_REPORTING } SysState_t;
volatile SysState_t gSystemState = STATE_IDLE;
float gCurrentTemperature = 0.0f;
uint32_t gLastReportTick = 0;

int main(void) {
    // 1. 硬件初始化
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init(); // ADC初始化,配置为规则通道,使能连续转换
    MX_TIM3_Init(); // PWM定时器初始化
    MX_USART2_UART_Init(); // 串口初始化,用于连接ESP-01S
    // 初始化ESP-01S,连接Wi-Fi和服务器(AT指令流程)

    // 2. 启动定时器中断(用于系统节拍和周期任务)
    HAL_TIM_Base_Start_IT(&htim2); // TIM2 1ms中断

    // 3. 主循环
    while (1) {
        switch (gSystemState) {
            case STATE_IDLE:
                // 等待定时中断触发采样
                __WFI(); // 进入低功耗等待模式,由中断唤醒
                break;
            case STATE_SAMPLING:
                // 状态已在ADC转换完成中断中自动切换和完成
                // 此处可添加采样完成后的标志位检查
                break;
            case STATE_CONTROLLING:
                PID_Calculate(); // 根据目标温度和当前温度计算PWM占空比
                PWM_SetDuty();   // 更新PWM输出
                gSystemState = STATE_IDLE;
                break;
            case STATE_REPORTING:
                if (IsTimeToReport()) { // 例如每5秒上报一次
                    SendDataViaUART(); // 封装JSON数据,通过串口发送AT指令给ESP-01S
                    gLastReportTick = HAL_GetTick();
                }
                gSystemState = STATE_IDLE;
                break;
        }
    }
}

// 关键中断服务例程
// TIM2 1ms中断(系统心跳)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        static uint8_t adc_sample_counter = 0;
        adc_sample_counter++;
        if (adc_sample_counter >= 100) { // 每100ms触发一次采样
            adc_sample_counter = 0;
            HAL_ADC_Start_IT(&hadc1); // 启动ADC转换(单次或连续)
        }
        // 其他定时任务,如检查通信超时
    }
}

// ADC转换完成中断
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    uint32_t adc_value = HAL_ADC_GetValue(hadc);
    gCurrentTemperature = ConvertADCToTemperature(adc_value); // 查表或公式计算
    gSystemState = STATE_CONTROLLING; // 触发控制计算
}

低功耗策略

  • 动态频率调整:在STATE_IDLE时,调用__WFI()指令,CPU进入睡眠模式,由定时器中断唤醒。
  • 外设分时供电:对于ESP-01S这类耗电大户,可以通过一个GPIO控制MOS管来开关其电源,仅在需要上报数据前才上电。
  • ADC间歇采样:如上代码所示,不是一直采样,而是定时(如100ms)采样一次,满足温控响应速度即可。

4. 核心源码与接线图详解

完整的工程代码已经开源(文末会提供仓库链接),这里挑几个关键函数和接线点说一下。

ADC采样与滤波(adc.c)

#define SAMPLE_NUM 10
float Get_Average_Temperature(void) {
    uint32_t temp_sum = 0;
    for(uint8_t i=0; i<SAMPLE_NUM; i++) {
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 10);
        temp_sum += HAL_ADC_GetValue(&hadc1);
        HAL_Delay(1);
    }
    float adc_avg = (float)temp_sum / SAMPLE_NUM;
    // 一阶滞后滤波(可选)
    static float filtered_temp = 0.0f;
    filtered_temp = 0.8 * filtered_temp + 0.2 * adc_avg;
    return ConvertToCelsius(filtered_temp); // 调用校准后的转换函数
}

说明:采用软件平均滤波和一阶滞后滤波结合,能有效抑制偶然跳动,让温度值更平滑。

PWM控制(pwm.c)

void Set_TEC_Power(float duty_cycle) {
    // duty_cycle范围:-100.0f ~ 100.0f,负值为加热,正值为制冷
    if(duty_cycle > 100.0f) duty_cycle = 100.0f;
    if(duty_cycle < -100.0f) duty_cycle = -100.0f;

    uint16_t pulse;
    if(duty_cycle >= 0) {
        // 制冷通道输出PWM,加热通道关闭
        pulse = (uint16_t)((duty_cycle / 100.0f) * __HAL_TIM_GET_AUTORELOAD(&htim3));
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse); // 制冷PWM
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);     // 加热PWM关闭
    } else {
        // 加热通道输出PWM,制冷通道关闭
        pulse = (uint16_t)((-duty_cycle / 100.0f) * __HAL_TIM_GET_AUTORELOAD(&htim3));
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);     // 制冷PWM关闭
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, pulse); // 加热PWM
    }
}

说明:使用一个定时器的两个通道,分别控制加热和制冷MOS管,实现无级调节。注意要加入死区时间防止上下桥臂直通(本项目为分时控制,未涉及H桥,故无需死区)。

接线图关键点

  • STM32与ESP-01SPA2(TX) -> ESP-01S RX; PA3(RX) -> ESP-01S TX; 共地。ESP-01S的CH_PDVCC接3.3V(需确保电源能提供足够电流,建议单独一路500mA以上的LDO)。
  • PT100放大电路输出:接PA0 (ADC1_IN0)。注意在PCB布局时,该走线要短,并用地线包围,远离数字信号线。
  • PWM输出PA6 (TIM3_CH1) 接制冷MOS管栅极;PA7 (TIM3_CH2) 接加热MOS管栅极。栅极务必串联10-100Ω电阻,防止振荡。

PCB布局建议图

5. 实测性能指标:数据说话

在室温25℃下,对系统进行了测试:

  • 静态功耗(仅STM32运行,无线模块断电):2.1 mA @ 3.3V
  • 主动控制功耗(PWM输出50%,无线模块断电):85 mA @ 12V(主要功耗在TEC)。
  • 温度采样分辨率:12位ADC,经过硬件和软件滤波,实际有效分辨率约0.1℃
  • 控制响应延迟:从温度变化到PWM占空比调整完成,小于150ms
  • Wi-Fi数据上报成功率(局域网服务器):>99.5%(加入了简单的重发机制)。

这些指标对于毕业设计来说已经足够亮眼,能清晰体现你对系统整体性能的把握和优化能力。

6. 生产环境避坑指南(血泪经验总结)

这部分是书本上很少讲,但实际做项目一定会遇到的“坑”。

  1. 电源噪声抑制

    • 现象:ADC采样值跳动大,尤其是当PWM动作或Wi-Fi模块发射时。
    • 解决
      • 模拟部分(运放、ADC基准源)使用独立的LDO供电(如AMS1117-3.3)。
      • 在MCU的每个电源引脚附近放置0.1uF10uF的退耦电容,且尽量靠近引脚。
      • 电源走线要粗,形成“星型”接地或单点接地,避免数字地电流流过模拟地线。
  2. Flash写保护与意外擦除

    • 现象:调试时程序偶尔“跑飞”,重新上电后无法启动,像是程序丢了。
    • 解决
      • 在程序开头对关键数据区(如保存校准参数的Flash扇区)进行写保护检查。
      • 避免在中断服务程序中进行Flash擦写操作。
      • 使用__disable_irq()__enable_irq()在擦写Flash前后关开总中断。
      • 给Boot0引脚加一个下拉电阻,防止静电干扰导致进入ISP模式。
  3. JTAG/SWD接口失效

    • 现象:下载一次程序后,再也连不上调试器了。
    • 解决
      • 检查是否将JTAG/SWD复用功能的引脚(如PA13, PA14, PA15, PB3, PB4)当作普通GPIO使用并初始化了。如果必须使用,要在程序初始化时重映射调试功能。
      • 最可靠的补救办法:通过串口ISP方式擦除整个芯片,然后重新下载程序。
  4. 无线模块通信不稳定

    • 现象:ESP-01S有时能连上服务器,有时连不上,AT指令无响应。
    • 解决
      • 电源是关键:确保其VCC电压在3.3V且稳定,最好单独供电。上电瞬间电流冲击较大,电容要足。
      • 发送AT指令后等待足够时间:例如发送AT+CIPSEND后,要等待模块返回>提示符,再发送数据。每个指令后都解析明确的回复。
      • 加入硬件复位电路:用一个GPIO控制MOS管来对ESP-01S进行彻底断电重启,这是解决“死机”的终极手段。

7. 总结与扩展

通过这个项目,我们完整走了一遍硬件选型、原理图设计、PCB布局、固件开发、调试优化的流程。它不仅仅是一个温控系统,更是一个嵌入式物联网终端的最小可行原型(MVP)

你可以基于这个稳定的框架,轻松地扩展功能:

  • 接入MQTT:在ESP-01S的固件中,将简单的TCP连接改为MQTT协议,即可接入阿里云、腾讯云等物联网平台。
  • 实现OTA升级:利用STM32的IAP功能,或者使用ESP-01S下载固件并通过串口转发给STM32升级,实现远程无线更新。
  • 增加更多传感器:如湿度、光照传感器,只需在ADC或I2C/SPI总线上添加设备,并在软件中增加相应的驱动和数据处理逻辑。
  • 优化PID算法:尝试更先进的控制算法,如模糊PID,提升控温精度和速度。

希望这篇长文能为你点亮一盏灯。毕设不是堆模块,而是构建一个可靠、可维护、可解释的系统。完整的项目源码、原理图、PCB文件、以及更详细的开发文档,我已经整理并开源在Gitee上。如果你在实现过程中有更好的想法,或者发现了代码中的不足,非常欢迎提交Issue或Pull Request,让我们一起完善这个项目。

最后的话:动手去做,从点亮一个LED开始,到让整个系统稳定运行。过程中遇到的所有问题,都是你简历上最宝贵的经验。祝你毕业设计顺利!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐