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(i=0;i<5;i++) { for(j=0;j<8;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_UP==0 || KEY_DOWN==0 || KEY_OK==0) { HAL_Delay(10); // 消抖 key_state = 1; } break; case 1: // 确认按下 if(KEY_UP==0) key_val = KEY_UP_PRESS; else if(KEY_DOWN==0) key_val = KEY_DOWN_PRESS; else if(KEY_OK==0) key_val = KEY_OK_PRESS; else key_state = 0; if(key_val != KEY_NONE) key_state = 2; break; case 2: // 等待释放 if(KEY_UP==1 && KEY_DOWN==1 && KEY_OK==1) { 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 i=0; i<FILTER_LEN-1; i++) { for(uint8_t j=i+1; j<FILTER_LEN; j++) { if(buf[i] > buf[j]) { uint16_t temp = buf[i]; buf[i] = buf[j]; buf[j] = temp; } } } // 取中间3个值的平均 for(uint8_t i=1; i<FILTER_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表
- 演示视频