简介:本资源是面向嵌入式开发初学者与进阶工程师的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切换通道(不适用,但常被误用) |
| 光照强度 | BH1750 | I²C | 数字输出、分辨率高(1lx)、抗干扰强,比光敏电阻+ADC更稳定 | stm32 i2c clock stretch问题少 |
| 超声波测距 | HC-SR04 | GPIO触发 | 测水箱液位/卷帘开合度,非测空气距离;需TIM3输入捕获精确计时,避开软件延时误差 | stm32超声波测距 |
| 显示屏 | ILI9341(2.4寸) | SPI | 支持16位色、分辨率240×320,驱动库成熟;读ID为0xA1A1是验证SPI通信是否正常的黄金标准 | stm32使用ili9341读id是a1a1 |
| 执行机构 | 5V步进电机(五线四相)+ ULN2003 | GPIO/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数据持久化
本文还有配套的精品资源,点击获取