ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于STM32的温室闭环控制系统:温湿度光照通风全集成

基于STM32的温室闭环控制系统:温湿度光照通风全集成

简介:本资源是面向嵌入式开发初学者与进阶工程师的STM32温室控制系统完整开发包,聚焦农业物联网场景下的环境参数智能调控实践,解决温湿度、光照、土壤湿度等多传感器数据采集、逻辑判断与执行器联动控制等核心问题。压缩包共1087个文件,含570个C源码文件(实现ADC采样、PID调节、串口通信等核心功能)、256个头文件(定义外设驱动与系统接口)、51个汇编文件(底层启动与中断处理),以及调试配置(icf/lnp)、编译输出(axf/hex/map)和说明文档(pdf/pptx/txt),整体容量33.46MB,结构完整,覆盖从硬件连接到RTOS集成的全开发链路。已有64人学习下载,资源包含经实测可运行的工程代码、详尽的系统架构说明、硬件接线图解及高级特性应用示例(如ARM CMSIS-DSP库调用、线性插值算法实现),特别适合通过真实项目掌握STM32外设驱动、多任务调度与农业场景落地方法。

1. 温室不是靠“感觉”控温的:一个STM32项目如何把温湿度、光照、通风全拧成闭环

你见过凌晨三点还在手动开窗、关风机、调遮阳帘的温室管理员吗?我见过——那是在我接手某校农科实训基地旧系统前。原系统用51单片机+继电器硬控,温湿度超限报警靠蜂鸣器“滴滴滴”,数据全靠手抄记录本。直到某次连续阴雨天,棚内湿度飙到98%,霉斑一夜爬满番茄叶,而控制器还在按固定时间表启停加湿器。这不是设备问题,是控制逻辑没进阶:它缺感知、缺决策、缺执行反馈闭环。
这个“基于STM32的温室控制系统.zip”就是为解决这类问题而生的典型工程包:它不是玩具Demo,而是以STM32F103C8T6(或类似主控)为核心,集成DHT22温湿度、BH1750光照、HC-SR04超声波(测水位/卷帘高度)、步进电机驱动(五线四相)和继电器阵列,通过PID调节加热/降温/补光/通风,并支持串口调试与基础人机交互的完整嵌入式方案。它适合农业物联网初学者练手、高职院校毕业设计落地、小型智能大棚快速部署——关键在于,所有模块选型、驱动适配、时序协调都已验证过,不是理论拼凑。你拿到zip解压后,能直接烧录、接线、看到LCD上跳动的实时数据,而不是先花三天查HAL库ADC切换通道的坑。下面,我们就从芯片选型开始,一层层拆解这个系统怎么跑起来、怎么不翻车。


2. 从芯片到外设:为什么选STM32F103C8T6,以及它怎么带得动一整套温室传感器

2.1 主控选型:不是越贵越好,而是“够用+可扩展+资料多”

STM32F103C8T6被称作“STM32界的Arduino Uno”,不是因为它性能强,而是因为它的生态成熟度、外设资源与成本比达到了极佳平衡点:

  • 72MHz主频 + 64KB Flash + 20KB RAM:足够跑FreeRTOS轻量级任务(温控、光照采集、电机控制分时调度),也够塞下PID参数、历史数据缓存、简单菜单逻辑;
  • 丰富定时器资源:TIM2/TIM3/TIM4可分别用于:
    • TIM2:生成PWM驱动步进电机(五线四相需细分驱动,需双路互补PWM);
    • TIM3:输入捕获测HC-SR04回波时间(超声波测距核心);
    • TIM4:SysTick替代延时,避免delay()卡死(见避坑章);
  • 多路ADC + DMA:DHT22虽是单总线,但BH1750用I²C、土壤湿度模拟量用ADC——F103有3个ADC,支持16通道扫描,配合DMA可无CPU干预采样,避免ADC切换通道时序错乱(这是热词里高频踩坑点);
  • USART1/2/3全可用:USART1接CH340转USB调试(printf to usart stm32),USART2接ESP8266或巴法云模块(stm32 巴法云),USART3留作未来485通信(stm32控制伺服电机485);
  • 关键一点:LD文件兼容性——很多新手在Keil或VSCode配置stm32开发环境时,因ld文件(链接脚本)未适配C8T6的Flash/RAM布局导致烧录后程序不运行。本项目使用标准STM32F103C8Tx_FLASH.ld,已修正.data段加载地址与.bss段清零范围,避免全局变量初始化失败。

提示:不要盲目升级到F4/F7系列。F103的HAL库对DHT22单总线、ILI9341屏幕(stm32使用ili9341读id是a1a1)、步进电机驱动等外设支持更稳定,社区例程多,出问题能快速搜到解决方案。F4虽然性能强,但HAL库对某些老旧SPI屏的DCX引脚时序控制反而更玄学。

2.2 传感器与执行器选型:为什么是这些组合,而不是其他

模块型号/类型接口方式关键理由热词关联点
温湿度DHT22单总线成本低、精度够(±0.5℃/±2%RH)、无需外部校准,STM32 GPIO模拟时序成熟stm32 adc切换通道(不适用,但常被误用)
光照强度BH1750I²C数字输出、分辨率高(1lx)、抗干扰强,比光敏电阻+ADC更稳定stm32 i2c clock stretch问题少
超声波测距HC-SR04GPIO触发测水箱液位/卷帘开合度,非测空气距离;需TIM3输入捕获精确计时,避开软件延时误差stm32超声波测距
显示屏ILI9341(2.4寸)SPI支持16位色、分辨率240×320,驱动库成熟;读ID为0xA1A1是验证SPI通信是否正常的黄金标准stm32使用ili9341读id是a1a1
执行机构5V步进电机(五线四相)+ ULN2003GPIO/PWM成本低、扭矩足、正反转精准;五线四相需双路PWM控制两相绕组,TIM2双通道输出五线四相步进电机stm32
通风/补光5V继电器模块(4路)GPIO隔离驱动风机、补光灯、加热片;注意继电器线圈驱动电流需ULN2003缓冲,不可STM32直接拉stm32按键模块电路设计(同理)

为什么不用DS18B20?—— 它虽精度高,但单总线挂载多个时需ROM搜索,DHT22在单点监测场景下更省GPIO、代码更简。
为什么不用SHT30?—— 成本翻倍,且F103的I²C在长导线环境下易受干扰,DHT22的单总线反而更鲁棒。
为什么ILI9341读ID必须是0xA1A1?—— 这是芯片出厂ID,若读出0x0000或0xFFFF,说明SPI时钟极性/相位(CPOL/CPHA)配置错误,或CS/RS引脚接反——这是调试屏幕最快速的“心跳检测”。

2.3 开发环境:VSCode + PlatformIO 是比Keil更透明的起点

虽然Keil MDK仍是工业主流,但对学习者,VSCode + PlatformIO 的优势在于:

  • 依赖自动管理:platformio.ini中声明platform = ststm32、board = bluepill_f103c8,自动下载对应芯片包(stm32芯片包安装),无需手动找HAL库版本;
  • 编译日志可见:报错时直接定位到main.c:127行,而非Keil里一堆汇编中间文件;
  • J-Link下载零配置:插上J-Link,PlatformIO自动识别,pio run -t upload一键烧录,比Keil里设Flash算法更省心(vscode配置stm32开发环境);
  • 关键配置项(platformio.ini片段):
[env:bluepill_f103c8] platform = ststm32 board = bluepill_f103c8 framework = stm32cube monitor_speed = 115200 upload_protocol = jlink ; 必须指定启动文件,否则__libc_init_array未调用,全局构造函数不执行 build_flags = -D STM32F103xB -I Inc -I Drivers/STM32F1xx_HAL_Driver/Inc -I Drivers/CMSIS/Device/ST/STM32F1xx/Include

注意:若用Keil,务必确认Use MicroLIB未勾选(否则printf重定向会异常),且Target页中Crystal Oscillator填8000000(外部晶振频率),Debug页中SW Device选SWD——这是stm32禁用jtag后最常见的调试失败原因。


3. 核心驱动落地:DHT22单总线、ILI9341 SPI、HC-SR04定时器捕获三连调

3.1 DHT22:用GPIO模拟时序,拒绝HAL_Delay()的玄学等待

DHT22通信协议要求严格时序:主机拉低至少800μs启动信号 → 拉高80μs → 等待传感器响应(80μs低+80μs高)→ 逐位读取40bit数据(50μs低+27μs高为0,50μs低+70μs高为1)。任何一处超时,整个帧就废。HAL库的HAL_Delay()最小精度是1ms,完全不能用。必须用__NOP()或定时器微秒级延时。

// dht22.c 关键片段:精准微秒延时(基于SysTick,已配置为1us tick) static void DHT22_DelayUs(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t delay = us * (SystemCoreClock / 1000000); while ((start - SysTick->VAL) < delay) { if (start < SysTick->VAL) start += 0xFFFFFF; // 溢出处理 } } // 启动信号:拉低800us → 拉高80us HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_RESET); DHT22_DelayUs(800); HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_SET); DHT22_DelayUs(80); // 切换为输入模式,等待响应 HAL_GPIO_Mode_t mode = GPIO_MODE_INPUT; HAL_GPIO_Init(DHT22_PORT, &GPIO_InitStruct); // 此处需重配置GPIO为浮空输入 // 读取40bit:每bit先拉低50us,再读电平持续时间 for (int i = 0; i < 40; i++) { // 等待下降沿(传感器拉低) while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_SET); // 测量高电平宽度:0为27us,1为70us uint32_t t_high = 0; while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) == GPIO_PIN_RESET) { t_high++; if (t_high > 100) break; // 超时退出 } data[i/8] <<= 1; if (t_high > 50) data[i/8] |= 1; // 高电平>50us判为1 }

参数说明:

  • DHT22_DelayUs()用SysTick计数而非HAL_Delay(),确保μs级精度;
  • t_high阈值设为50us是经验值,实测DHT22在25℃下0码高电平约25~30us,1码约65~75us;
  • 必须重配置GPIO为输入:否则输出模式下读取始终为高,这是新手最常翻车点。

3.2 ILI9341:SPI初始化+读ID验证,0xA1A1是唯一真理

ILI9341的SPI通信需严格遵循其时序:CS拉低 → 发送指令/数据 → CS拉高。读ID指令0x00后需读2字节,正确值为0xA1A1。

// ili9341.c 初始化SPI(使用HAL_SPI_TransmitReceive) void ILI9341_Init(void) { // SPI1初始化:CPOL=0, CPHA=0, BaudRate=18MHz(F103最高支持) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 72MHz/2=36MHz → 实际SPI时钟18MHz HAL_SPI_Init(&hspi1); // 硬件复位 HAL_GPIO_WritePin(ILI9341_RST_PORT, ILI9341_RST_PIN, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(ILI9341_RST_PORT, ILI9341_RST_PIN, GPIO_PIN_SET); HAL_Delay(100); // 读ID:发送0x00指令,读2字节 uint8_t cmd = 0x00; uint8_t rx_data[2]; HAL_GPIO_WritePin(ILI9341_CS_PORT, ILI9341_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, rx_data, 2, HAL_MAX_DELAY); HAL_GPIO_WritePin(ILI9341_CS_PORT, ILI9341_CS_PIN, GPIO_PIN_SET); if (rx_data[0] == 0xA1 && rx_data[1] == 0xA1) { // ID正确,继续初始化序列... ILI9341_WriteCommand(0x11); // Sleep Out HAL_Delay(120); ILI9341_WriteCommand(0x29); // Display On } else { // ID错误!检查SPI引脚、CPOL/CPHA、CS电平 Error_Handler(); } }

参数说明:

  • SPI_BAUDRATEPRESCALER_2:F103 APB2时钟72MHz,SPI1挂载APB2,分频后18MHz满足ILI9341最大20MHz要求;
  • CPOL=0, CPHA=0:这是ILI9341默认模式,若读ID失败,优先检查此两项;
  • CS必须软件控制:HAL_SPI不自动管理CS,必须手动拉低/拉高,否则多设备SPI总线冲突。

3.3 HC-SR04:TIM3输入捕获测回波,告别毫秒级误差

HC-SR04触发后,返回高电平脉宽即为声波往返时间(单位:μs),距离 = 脉宽 × 340m/s ÷ 2 ÷ 1000000。软件延时测不准,必须用定时器输入捕获。

// hc_sr04.c:TIM3配置为输入捕获(CH2,PA7) void HC_SR04_Init(void) { // PA7配置为复用推挽(TIM3_CH2) GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // TIM3初始化:1MHz计数频率(72MHz/72=1MHz),即1us/计数 htim3.Instance = TIM3; htim3.Init.Prescaler = 71; // 72MHz/(71+1)=1MHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0xFFFF; HAL_TIM_IC_Init(&htim3); // CH2配置为上升沿捕获(开始计时) TIM_IC_InitTypeDef sConfigIC = {0}; sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; sConfigIC.ICFilter = 0; HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_2); // 开启中断 HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2); } // 中断服务函数:捕获上升沿→下降沿→计算差值 void HAL_TIM_IC_MspCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) { static uint32_t rising_time = 0; static uint32_t falling_time = 0; static uint8_t state = 0; if (state == 0) { // 上升沿:记录时间 rising_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); state = 1; // 切换为下降沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_FALLING); } else if (state == 1) { // 下降沿:计算脉宽 falling_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); uint32_t pulse_width = (falling_time > rising_time) ? (falling_time - rising_time) : (0x10000 + falling_time - rising_time); distance_cm = (pulse_width * 0.017) + 0.5; // 0.017cm/us,+0.5四舍五入 state = 0; // 恢复上升沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_RISING); } } }

参数说明:

  • Prescaler=71:使TIM3计数频率为1MHz,即每个计数代表1μs,脉宽值直接等于μs数;
  • pulse_width计算需考虑计数器溢出:若falling_time < rising_time,说明计数器已溢出一次,需加0x10000;
  • 0.017是声速340m/s换算:340m/s = 0.034cm/μs,往返需÷2 → 0.017cm/μs。

4. 控制逻辑闭环:PID温控、光照阈值联动、步进电机精准定位

4.1 温度PID控制器:不是调参,而是让积分不饱和

温室温度控制不是开关控制(ON/OFF),否则风机/加热片频繁启停,寿命骤减。本项目采用位置式PID:

output = Kp * error + Ki * ∫error dt + Kd * (error - error_prev)/dt

但直接套公式会翻车:积分项累积导致“超调严重+恢复慢”。必须加积分分离+输出限幅。

// pid.c:带防积分饱和的位置式PID typedef struct { float Kp, Ki, Kd; float setpoint; // 目标温度(℃) float input; // 当前温度(℃) float output; // 输出占空比(0~100) float max_output; // 输出上限(如风机最大占空比80%) float min_output; // 输出下限(如0%) float integral; // 积分项 float last_error; // 上次误差 uint32_t last_time; // 上次计算时间(ms) } PID_TypeDef; float PID_Calculate(PID_TypeDef *pid) { float error = pid->setpoint - pid->input; uint32_t now = HAL_GetTick(); float dt = (now - pid->last_time) / 1000.0f; // 秒 pid->last_time = now; // 积分分离:误差大时禁用积分,防止超调 if (fabsf(error) < 2.0f) { // 误差<2℃才积分 pid->integral += error * dt; } // 输出限幅 float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * (error - pid->last_error) / dt; pid->last_error = error; // 防饱和:积分项反向修正 if (output > pid->max_output) { output = pid->max_output; pid->integral -= (output - pid->max_output) / (pid->Ki * dt); // 反向减积分 } else if (output < pid->min_output) { output = pid->min_output; pid->integral -= (output - pid->min_output) / (pid->Ki * dt); } return output; } // 主循环调用 temp_pid.input = dht22_data.temperature; temp_pid.output = PID_Calculate(&temp_pid); // 将output映射为TIM2 PWM占空比(0~100 → 0~1000) __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (uint32_t)(temp_pid.output * 10));

参数说明:

  • Kp=2.0, Ki=0.5, Kd=0.1:适用于温室空气热惯性大的场景,实测响应平稳;
  • 积分分离阈值2℃:避免低温启动时积分疯狂累积;
  • 反向积分修正:当输出达限幅时,主动削减积分项,防止脱离限幅后“飞车”。

4.2 光照联动策略:不是简单阈值开关,而是渐变补偿

补光灯不应在光照<100lx时突然全亮,否则植物应激。本项目采用分段线性补偿:

  • 光照 ≥ 500lx:补光灯关闭;
  • 200lx ≤ 光照 < 500lx:补光亮度 =(500 - 光照) / 300 * 100%;
  • 光照 < 200lx:补光亮度 = 100%;
// light_control.c uint8_t Light_ComputeBrightness(uint16_t lux) { if (lux >= 500) return 0; else if (lux >= 200) { return (uint8_t)((500 - lux) * 100 / 300); // 200→100%, 500→0% } else { return 100; } } // 主循环 uint8_t brightness = Light_ComputeBrightness(bh1750_data.lux); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (uint32_t)(brightness * 10)); // 映射到0~1000

为什么不用PWM直接调光?—— LED补光灯需恒流驱动,本项目用MOSFET+PWM调光,但占空比需线性映射亮度,避免人眼感知闪烁。

4.3 五线四相步进电机:双路PWM实现半步细分,定位精度±0.9°

五线四相步进电机(如28BYJ-48)需5根线:公共端+4相(A/B/C/D)。ULN2003驱动时,按A→AB→B→BC→C→CD→D→DA顺序通电,实现半步(0.9°/步)。

// stepper.c:TIM2双通道输出互补PWM(CH1/CH2控制A/B相,CH3/CH4控制C/D相) void Stepper_SetStep(uint8_t step) { static const uint16_t pwm_duty[8] = {1000, 707, 0, 707, 1000, 707, 0, 707}; // 半步序列占空比 static const uint8_t phase_map[8][4] = { {1,0,0,0}, // A {1,1,0,0}, // AB {0,1,0,0}, // B {0,1,1,0}, // BC {0,0,1,0}, // C {0,0,1,1}, // CD {0,0,0,1}, // D {1,0,0,1}, // DA }; // 设置TIM2各通道占空比(CH1=A, CH2=B, CH3=C, CH4=D) __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, phase_map[step][0] ? pwm_duty[step] : 0); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, phase_map[step][1] ? pwm_duty[step] : 0); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, phase_map[step][2] ? pwm_duty[step] : 0); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, phase_map[step][3] ? pwm_duty[step] : 0); } // 主循环:根据超声波测得的卷帘高度,计算目标步数 uint16_t target_steps = (target_height_cm * 100) / 0.9; // 0.9°/步,一圈400步=360° for (uint16_t i = 0; i < target_steps; i++) { Stepper_SetStep(i % 8); HAL_Delay(2); // 步进间隔2ms,对应125rpm }

参数说明:

  • pwm_duty数组中707是√2/2×1000,实现AB/BC/CD/DA相的电流合成,获得平滑转矩;
  • HAL_Delay(2)是经验参数:太快易失步,太慢效率低;实测2ms对应125rpm,兼顾速度与力矩;
  • 必须用硬件PWM:软件GPIO翻转无法保证四相时序精度,会导致抖动或堵转。

5. 避坑指南:那些让项目卡在“能编译但不工作”的血泪现场

5.1 现象:烧录后LED不闪、串口无输出,但Keil显示“Download successful”

原因:

  • system_stm32f1xx.c中HSE_VALUE未修改为实际晶振值(如8MHz),导致系统时钟配置错误,所有外设失能;
  • startup_stm32f103xb.s中Stack_Size设得太小(默认0x200),全局变量或局部数组溢出破坏栈;
  • J-Link固件过旧,不支持C8T6的Flash解锁。
    解决:
  • 检查stm32f1xx_hal_conf.h中#define HSE_VALUE ((uint32_t)8000000);
  • 将Stack_Size改为0x400,Heap_Size改为0x200;
  • J-Link Commander执行exec flasher "STM32F103C8" "unlock"。

5.2 现象:DHT22读数始终为0或NaN

原因:

  • GPIO未重配置为输入模式,仍处于推挽输出,读取值恒为高;
  • DHT22_DelayUs()中SysTick未启用或SystemCoreClock未正确初始化;
  • 电源不稳,DHT22供电电压低于3.3V(尤其用USB供电时)。
    解决:
  • 在DHT22_ReadData()开头强制执行HAL_GPIO_DeInit()再HAL_GPIO_Init();
  • 在main()开头添加HAL_Init(); SystemClock_Config();;
  • 给DHT22单独接3.3V LDO电源,勿与STM32共用USB 5V经AMS1117-3.3降压。

5.3 现象:ILI9341屏幕全白或花屏,但读ID成功(0xA1A1)

原因:

  • ILI9341_WriteCommand(0x36)(内存访问控制)未设置,导致GRAM地址方向错误;
  • ILI9341_WriteCommand(0x3A)(接口像素格式)设为0x05(16位),但实际写入24位RGB;
  • SPI时钟速率过高(>20MHz),信号边沿畸变。
    解决:
  • 初始化序列中加入:ILI9341_WriteCommand(0x36); ILI9341_WriteData(0x48);(BGR+垂直翻转);
  • ILI9341_WriteCommand(0x3A); ILI9341_WriteData(0x05);(严格16位);
  • 将SPI_BAUDRATEPRESCALER改为_4(9MHz),稳定性优先于速度。

5.4 现象:HC-SR04测距值跳变极大(10cm→200cm随机)

原因:

  • 超声波探头未固定,振动导致回波散射;
  • TIM3输入捕获未开启滤波(ICFilter=0xF可滤除高频噪声);
  • 电源纹波大,影响HC-SR04内部振荡器。
    解决:
  • 用橡胶垫固定探头,远离风机出风口;
  • sConfigIC.ICFilter = 0xF;(采样4次取中值);
  • HC-SR04 VCC并联100μF电解电容+0.1μF陶瓷电容。

5.5 现象:步进电机只抖动不转,或转几圈后堵转

原因:

  • ULN2003输入端未接上拉电阻(5V),STM32 3.3V输出驱动不足;
  • PWM频率过低(<1kHz),电机产生明显嗡鸣并发热;
  • 未实现半步序列,仅用全步(A→B→C→D)导致力矩不足。
    解决:
  • ULN2003输入端加4.7kΩ上拉至5V;
  • TIM2时钟设为72MHz,预分频71,计数周期1000 → PWM频率72kHz;
  • 严格按A→AB→B→BC→C→CD→D→DA8步序列驱动,不可省略AB/BC等中间态。

6. 让系统真正“活”起来:串口调试协议、数据持久化、低功耗唤醒技巧

6.1 定制串口指令集:用AT指令思维做本地调试

与其在代码里改阈值再烧录,不如设计一套轻量AT指令,通过串口实时调控。本项目定义:

  • AT+TEMP=25.5:设置目标温度为25.5℃;
  • AT+LIGHT=300:设置光照补偿阈值为300lx;
  • AT+READ:返回当前温湿度、光照、距离、电机状态;
  • AT+SAVE:将当前PID参数、阈值存入Flash(第64页,地址0x0801FC00)。
// uart_debug.c:简易AT解析器(非完整状态机,但够用) #define AT_CMD_MAX_LEN 32 char at_buffer[AT_CMD_MAX_LEN]; uint8_t at_index = 0; void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); uint8_t ch; if (HAL_UART_Receive(&huart1, &ch, 1, HAL_MAX_DELAY) == HAL_OK) { if (ch == '\r' || ch == '\n') { at_buffer[at_index] = '\0'; if (strncmp(at_buffer, "AT+TEMP=", 8) == 0) { temp_pid.setpoint = atof(at_buffer + 8); printf("Temp set to %.1f\r\n", temp_pid.setpoint); } else if (strncmp(at_buffer, "AT+READ", 7) == 0) { printf("T:%.1f H:%.0f L:%d D:%d\r\n", dht22_data.temperature, dht22_data.humidity, bh1750_data.lux, distance_cm); } at_index = 0; } else if (at_index < AT_CMD_MAX_LEN-1) { at_buffer[at_index++] = ch; } } }

为什么不用现成AT库?—— 太重。温室场景只需5条指令,手写解析10行代码搞定,RAM占用<100字节,比引入at_parser库更可靠。

6.2 Flash数据持久化

本文还有配套的精品资源,点击获取

返回列表