一个STM32环境监测系统的课程设计实战(附:完整工程与源码解析)
1. 项目背景与硬件选型第一次接触STM32环境监测系统时我对着开发板和一堆传感器零件完全无从下手。直到把DHT11温湿度传感器成功读出数据LCD屏幕上跳出25.3℃ 62%那刻才真正理解嵌入式开发的魅力。这个课程设计以STM32F103C8T6为核心俗称蓝 pill的开发板性价比极高某宝30元就能入手特别适合学生党。硬件选型直接影响项目成败我的踩坑经验是传感器DHT11虽然精度一般±2℃但胜在价格便宜5元/个且数字信号输出比模拟传感器省去ADC配置显示模块选用0.96寸OLED还是LCD1602实测发现OLED的I2C接口更省IO口但LCD在强光下可视性更好报警模块有源蜂鸣器比无源型号驱动简单但要注意GPIO驱动能力不足时会声音微弱扩展性预留的USART接口可连接ESP8266实现物联网功能课程设计进阶玩法电路连接有个易错点光敏电阻需要配合10KΩ上拉电阻直接接3.3V会导致ADC值始终为4095。我在面包板上调试时因此浪费了两小时后来用万用表量电压才发现问题。2. 开发环境搭建新手常卡在第一步环境配置我推荐用这套懒人方案软件组合Keil MDK STM32CubeMX避免手动写初始化代码驱动安装CH340串口驱动和ST-Link驱动必须装对版本工程模板直接使用正点原子提供的标准库模板CSDN搜STM32F103标准库遇到最典型的报错是NO ST-LINK detected解决方法分三步检查开发板跳线帽是否接在SWD模式更新ST-Link固件用ST官方工具换条质量好的MicroUSB线劣质线会导致供电不稳// 用CubeMX生成时钟配置代码示例 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // 配置HSE晶振为8MHz RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; // 72MHz主频 HAL_RCC_OscConfig(RCC_OscInitStruct); // 时钟树配置 RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; // 36MHz RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; // 72MHz HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2); }3. 传感器数据采集实战DHT11的时序要求极其严格我最初用HAL_Delay()实现时序结果成功率不到50%。后来改用寄存器级操作后稳定性大幅提升// 改进后的DHT11读取函数 uint8_t DHT11_Read_Data(u8 *temp, u8 *hum) { u8 buf[5] {0}; u8 i,j; // 主机拉低18ms DHT11_OUT(0); DHT11_DQ_OUT(); HAL_Delay(18); // 主机拉高20-40us DHT11_OUT(1); delay_us(30); // 切换输入模式检测响应 DHT11_DQ_IN(); if(DHT11_IN()1) return 1; // 检测从机响应 while(DHT11_IN()0); // 等待80us低电平结束 while(DHT11_IN()1); // 等待80us高电平结束 // 开始接收40bit数据 for(i0;i5;i) { for(j0;j8;j) { while(DHT11_IN()0); // 等待50us低电平 delay_us(40); // 判断高电平持续时间 buf[i] 1; if(DHT11_IN()1) buf[i] | 1; while(DHT11_IN()1); // 等待高电平结束 } } // 校验和数据 if(buf[0]buf[1]buf[2]buf[3]buf[4]) { *hum buf[0]; *temp buf[2]; return 0; } return 2; // 校验失败 }光敏电阻的处理更简单直接用STM32的ADC采集// 获取光照强度值0-100% uint8_t Get_Light_Level(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t adc_val HAL_ADC_GetValue(hadc1); return (uint8_t)(adc_val * 100 / 4095); // 12位ADC最大值4095 }4. 人机交互设计菜单系统是项目难点我采用状态机模式实现三级菜单顶层显示温湿度/光照实时数据二级菜单设备控制LED/风扇/蜂鸣器三级菜单阈值设置温度报警值按键扫描使用定时器中断实现消抖// 按键状态机处理 void KEY_Scan(void) { static u8 key_state 0; switch(key_state) { case 0: // 检测按下 if(KEY_UP0 || KEY_DOWN0 || KEY_OK0) { HAL_Delay(10); // 消抖 key_state 1; } break; case 1: // 确认按下 if(KEY_UP0) key_val KEY_UP_PRESS; else if(KEY_DOWN0) key_val KEY_DOWN_PRESS; else if(KEY_OK0) key_val KEY_OK_PRESS; else key_state 0; if(key_val ! KEY_NONE) key_state 2; break; case 2: // 等待释放 if(KEY_UP1 KEY_DOWN1 KEY_OK1) { key_state 0; } break; } }LCD显示优化技巧使用sprintf生成格式化字符串局部刷新代替全屏刷新减少闪烁重要参数反色显示// LCD显示示例 void Show_Main_Page(void) { char buf[20]; sprintf(buf, Temp:%2dC Hum:%2d%%, temp, hum); LCD_ShowString(0, 0, buf); sprintf(buf, Light:%3d%%, light); LCD_ShowString(0, 1, buf); // 报警状态特殊显示 if(alarm_flag) { LCD_ShowString(0, 3, !ALARM!); } }5. 报警逻辑与外围控制报警策略采用软硬结合方式软件判断主循环中持续监测阈值硬件响应触发蜂鸣器风扇LED闪烁// 报警控制函数 void Alarm_Control(void) { static u8 blink_cnt 0; if(temp temp_threshold || hum hum_threshold) { // 启动报警 BUZZER_ON(); FAN_ON(); if(blink_cnt 10) { // 0.5秒间隔闪烁 blink_cnt 0; LED1_TOGGLE(); } alarm_flag 1; } else { // 关闭报警 BUZZER_OFF(); FAN_OFF(); LED1_OFF(); alarm_flag 0; } }风扇控制有个坑普通IO口驱动电流不够需要加三极管放大。我用S8050搭建的驱动电路如下VCC(5V) --- [风扇] --- [风扇-] --- [三极管C极] | E --- GND B --- 1K电阻 --- PA66. 工程架构优化建议初期我的代码全写在main.c里后来发现几个严重问题修改传感器要重新编译整个工程功能添加导致代码越来越乱多人协作无法并行开发改进后的模块化架构├── Drivers │ ├── bsp_dht11.c // 传感器驱动 │ ├── bsp_lcd.c // 显示驱动 │ └── bsp_key.c // 按键驱动 ├── Middlewares │ ├── menu.c // 菜单系统 │ └── alarm.c // 报警逻辑 └── Application ├── app_main.c // 主业务流程 └── app_iot.c // 物联网扩展关键技巧使用头文件声明外部函数全局变量用extern声明模块间通过消息队列通信// 典型头文件示例bsp_dht11.h #ifndef __DHT11_H #define __DHT11_H #include stm32f1xx_hal.h #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_PIN_12 void DHT11_Init(void); uint8_t DHT11_Read_Data(u8 *temp, u8 *hum); #endif7. 常见问题解决方案调试过程中遇到的典型问题及解决方法DHT11读取失败检查接线是否接触不良时序严格控制在微秒级增加失败重试机制LCD显示乱码确认初始化序列正确检查对比度电压可调电阻确保发送数据/命令标志位正确按键响应迟钝消抖时间调整到10-20ms改用中断方式检测按键检查上拉电阻是否接好ADC值跳变严重增加软件滤波算法中值均值在ADC引脚加0.1uF滤波电容避免与大功率设备共用电源// 改进的ADC滤波算法 #define FILTER_LEN 5 uint16_t ADC_Filter(uint16_t raw_val) { static uint16_t buf[FILTER_LEN] {0}; static uint8_t index 0; uint32_t sum 0; buf[index] raw_val; if(index FILTER_LEN) index 0; // 冒泡排序找中值 for(uint8_t i0; iFILTER_LEN-1; i) { for(uint8_t ji1; jFILTER_LEN; j) { if(buf[i] buf[j]) { uint16_t temp buf[i]; buf[i] buf[j]; buf[j] temp; } } } // 取中间3个值的平均 for(uint8_t i1; iFILTER_LEN-1; i) { sum buf[i]; } return sum / (FILTER_LEN-2); }8. 项目扩展方向完成基础功能后可以尝试这些进阶玩法物联网接入通过ESP8266连接云平台使用MQTT协议上传数据手机APP远程监控数据存储添加SPI Flash存储历史数据实现SD卡数据记录导出CSV格式报表低功耗优化使用STM32的STOP模式定时唤醒采样关闭不必要的外设时钟GUI升级移植LittlevGL图形库实现触摸屏控制添加曲线显示功能// 低功耗模式示例 void Enter_Stop_Mode(void) { // 配置唤醒源如RTC或外部中断 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // 关闭外设时钟 __HAL_RCC_GPIOA_CLK_DISABLE(); __HAL_RCC_GPIOB_CLK_DISABLE(); // 进入STOP模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新初始化系统时钟 SystemClock_Config(); }完整工程代码已打包上传包含Keil完整工程文件STM32CubeMX配置文件原理图PDF版本器件清单BOM表演示视频