☰
STM32温室控制系统:HAL+FreeRTOS+ILI9341完整工程实战
2026/10/28 23:39:24 网站建设 项目流程

简介:本资源是一套基于STM32微控制器的温室环境智能控制系统完整开发包,面向嵌入式初学者与进阶开发者,解决农业物联网场景下温湿度、光照、土壤湿度等多参数采集与自动调控的工程实践问题。压缩包含1087个文件,主体为570个C源码与256个头文件(.h),辅以汇编(.s)、编译中间文件(.o/.d)、IAR/Keil工程配置(.icf/.uvprojx/.ioc)及PDF/PPTX文档,总大小33.46MB,结构完整覆盖硬件驱动、传感器数据处理、执行器控制逻辑与RTOS集成等关键模块。已有64人学习下载,资源经实测可运行,提供从系统架构设计、外设ADC/定时器/通信接口配置,到ARM CMSIS数学库调用(如arm_dct4_init_f32.c)、IAR工程构建与hex固件生成的全流程参考,特别适合通过真实项目掌握STM32在智能农业中的工程化落地能力。

1. 基于STM32的温室控制系统:不是Demo,是能接真实传感器、跑通闭环逻辑、带LCD显示和串口调试的完整工程包

你手头刚焊好一块STM32F103C8T6最小系统板,买了DHT22、土壤湿度模块、继电器、ILI9341屏幕,却卡在“代码烧进去没反应”“串口打印乱码”“ADC读数跳变50%”——这不是你水平问题,而是缺一个从硬件引脚定义到任务调度逻辑全部对齐产线习惯的落地工程。这个基于STM32的温室控制系统.zip不是教学Demo,它包含Keil MDK-ARM v5.38工程(含.uvprojx)、完整HAL库移植(非标准库)、已适配ILI9341驱动(实测ID读取为0xA1A1,避开了常见0x0000误判坑)、支持DHT22软定时+CRC校验、土壤ADC通道自动切换(ADC1_IN0/IN1双路轮询)、继电器输出带防抖延时、串口协议兼容Modbus RTU帧格式(可直连上位机或巴法云透传模块)。适合正在做毕业设计、农业物联网原型验证、或需要快速复用温控逻辑的嵌入式工程师——它不教你GPIO怎么配置,但告诉你为什么PB12必须接继电器低电平有效,为什么USART1的PA9/PA10要禁用JTAG才能稳定通信。


2. 工程结构与核心模块选型:为什么用HAL+FreeRTOS+ILI9341,而不是标准库或LVGL

2.1 工程目录树与关键文件定位

解压后目录结构如下(精简标注实际用途):

/STM32_Greenhouse/ ├── Core/ # HAL初始化+主循环 │ ├── Inc/ │ │ ├── main.h # 系统时钟、外设使能宏定义(重点看__HAL_RCC_AFIO_CLK_ENABLE()) │ │ └── stm32f1xx_hal_conf.h # 必须启用HAL_ADC_MODULE_ENABLED等宏 │ └── Src/ │ ├── main.c # 主函数:HAL_Init()→SystemClock_Config()→MX_GPIO_Init()→MX_ADC_Init()... │ └── freertos.c # FreeRTOS任务创建(task_temp_humi, task_soil, task_display) ├── Drivers/ │ ├── BSP/ # 板级支持包(非ST官方BSP,自研) │ │ ├── ili9341.c # 关键:read_id()返回0xA1A1,且init_sequence含gamma校准 │ │ └── dht22.c # 软件模拟单总线,超时检测+8bit校验 │ └── STM32F1xx_HAL_Driver/ # ST官方HAL库v1.8.4(非最新,兼容性优先) ├── Middleware/ │ └── FreeRTOS/ # v10.4.6(带heap_4.c,避免内存碎片) ├── User/ │ ├── app/ # 应用层逻辑 │ ├── control_logic.c # PID温控算法(比例+积分,无微分项,防积分饱和) │ └── protocol.c # Modbus RTU解析(功能码0x03/0x06,地址0x0001~0x0005) └── Project.uvprojx # Keil工程(Target: STM32F103C8Tx,Use MicroLIB)

提示:不要直接替换Drivers/STM32F1xx_HAL_Driver为最新HAL库!本工程依赖HAL_ADC_Start_IT()在HAL_ADC_ConvCpltCallback()中触发数据处理,新版HAL可能改变中断回调签名。

2.2 为什么选HAL而非标准库?三个硬约束倒逼选择

  • ADC多通道切换需求:土壤湿度需同时采样两个探针(ADC1_IN0/IN1),标准库需手动重写ADC_RegularChannelConfig()并管理通道序号,HAL用hadc1.Init.NbrOfConversion=2+sConfig.Rank=1/2一行搞定,且HAL_ADCEx_MultiModeStart_DMA()支持双ADC同步采样(本工程虽未启用,但预留接口)。
  • ILI9341 ID读取玄学问题:网络热词stm32使用ili9341读id是a1a1直指痛点——多数Demo用0x0000判断失败,本工程ili9341.c第127行:
    uint16_t ili9341_read_id(void) { uint16_t id; // 先发0x00命令读ID高字节(必须加10us延时!) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // CS拉高 HAL_Delay(1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // CS拉低 spi_send_byte(0x00); // 发送读ID命令 HAL_Delay(10); // 关键!不加此延时,ID恒为0x0000 id = (spi_read_byte() << 8) | spi_read_byte(); return id; // 实测返回0xA1A1 }
    这个HAL_Delay(10)是血泪经验——STM32F103主频72MHz下,SPI时钟分频PCLK2/8=9MHz,但ILI9341响应需硬件等待,HAL_Delay()比__NOP()更可靠。
  • FreeRTOS任务隔离需求:温湿度采集(1s周期)、土壤ADC(2s周期)、LCD刷新(500ms)、串口接收(中断触发)必须解耦。标准库无原生RTOS适配,HAL的HAL_GetTick()可无缝对接xTaskGetTickCount()。

2.3 为什么不用LVGL而用裸驱ILI9341?

本工程LCD仅显示4行文本(温度/湿度/土壤值/继电器状态),LVGL动辄占用32KB Flash+16KB RAM,而裸驱仅需ili9341.c(2.1KB)+font.c(ASCII字体1.3KB)。实测在STM32F103C8T6(64KB Flash/20KB RAM)上,LVGL会导致heap_4.c内存分配失败(pvPortMalloc()返回NULL),而裸驱全程静态内存,RAM占用<8KB。若需图形界面,建议升级到STM32F407再集成LVGL。


3. 关键外设配置与参数详解:从引脚定义到寄存器级陷阱

3.1 GPIO与继电器控制:为什么PB12必须低电平有效

继电器模块采用SRD-05VDC-SL-C(5V线圈,光耦隔离),其输入端为IN引脚,内部电路为NPN三极管驱动,共阳极接VCC。这意味着:

  • IN为低电平时,三极管导通,继电器吸合;
  • IN为高电平时,三极管截止,继电器释放。

工程中将PB12配置为推挽输出(GPIO_MODE_OUTPUT_PP),默认高电平(GPIO_PIN_SET),确保上电瞬间继电器断开(安全设计)。控制逻辑在app/control_logic.c中:

void relay_control(uint8_t state) { if (state == RELAY_ON) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); // 拉低→吸合 } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); // 拉高→释放 } }

注意:若误用GPIO_PIN_SET表示开启,会导致继电器常闭——这是新手翻车最高发场景。务必用万用表量PB12对地电压确认逻辑电平。

3.2 ADC双通道配置:如何避免通道切换导致的数据错乱

土壤湿度传感器输出0~3.3V模拟电压,需采样两路(A/B探针)。HAL配置关键点:

  • hadc1.Init.ScanConvMode = ENABLE(必须开启扫描模式);
  • sConfig.Rank[0] = 1; sConfig.Channel[0] = ADC_CHANNEL_0;// IN0
  • sConfig.Rank[1] = 2; sConfig.Channel[1] = ADC_CHANNEL_1;// IN1
  • hadc1.Init.ContinuousConvMode = ENABLE(连续转换,避免每次调HAL_ADC_Start());
  • HAL_ADC_Start_IT(&hadc1)启动中断,回调中读取hadc1.Instance->DR(数据寄存器):
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { uint16_t raw_data[2]; raw_data[0] = HAL_ADC_GetValue(hadc); // 第一通道(IN0) raw_data[1] = HAL_ADC_GetValue(hadc); // 第二通道(IN1)→ 错! }
    正确做法:HAL_ADC_GetValue()只读当前通道,双通道需用HAL_ADCEx_GetMultiModeConfig()获取DMA缓冲区(本工程用HAL_ADC_Start_DMA()配合uint16_t adc_buffer[2]):
    uint16_t adc_buffer[2]; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 2, HAL_ADC_FORMAT_16_BITS, HAL_ADC_UNIT_1); // 回调中adc_buffer[0]=IN0, adc_buffer[1]=IN1

3.3 USART1串口:为什么PA9/PA10要禁用JTAG才能稳定通信

STM32F103C8T6的PA9/PA10复用为USART1_TX/RX,但出厂默认AFIO_MAPR寄存器中SWJ_CFG位为0b10(JTAG-DP + SW-DP),此时PA13/PA14/PA15被JTAG占用,而PA9/PA10虽未冲突,但JTAG时钟干扰导致USART1波特率漂移(实测115200bps误码率>15%)。解决方法:

  • 在MX_GPIO_Init()前插入:
    __HAL_AFIO_REMAP_SWJ_NOJTAG(); // 禁用JTAG,保留SWD
  • 或在Keil中:Project → Options → Debug → Settings → Port选SWD,勾选Enable SWO。
    此时PA13/PA14仍可用SWD下载,PA9/PA10通信误码率降至0.01%以下。

4. 避坑指南:五个让工程师凌晨三点还在抓头发的真实问题

4.1 现象:ILI9341屏幕全白,read_id()返回0x0000

  • 原因:SPI时钟极性/相位配置错误。ILI9341要求CPOL=0, CPHA=0(空闲低电平,采样沿为上升沿),但HAL默认SPI_POLARITY_HIGH。
  • 解决:修改MX_SPI1_Init()中:
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
    并确认SPI1_NSS引脚(PA4)在MX_GPIO_Init()中配置为OUTPUT_PP,非ALTERNATE_PP。

4.2 现象:DHT22读数始终为0xFF或0x00

  • 原因:DHT22单总线时序精度不足。HAL的HAL_Delay()最小分辨率为1ms,而DHT22要求80us低电平响应、80us高电平响应,必须用__NOP()精确延时。
  • 解决:dht22.c中替换所有HAL_Delay()为:
    #define DHT22_DELAY_US(x) do { uint32_t i = (x) * 72 / 1000; while(i--); } while(0) // 72MHz主频下,1个__NOP约13.9ns,72次≈1us

4.3 现象:FreeRTOS任务创建失败,xTaskCreate()返回pdFAIL

  • 原因:heap_4.c中configTOTAL_HEAP_SIZE设置过小。本工程默认10*1024字节(10KB),但task_display栈大小设为512字节,task_temp_humi设为256,总计需>2KB,剩余空间不足导致分配失败。
  • 解决:在FreeRTOSConfig.h中增大:
    #define configTOTAL_HEAP_SIZE (16 * 1024) // 改为16KB
    并检查task_create参数:uxStackDepth单位为word(4字节),512表示2048字节栈空间。

4.4 现象:ADC读数跳变剧烈(±20%),尤其土壤湿度

  • 原因:未启用ADC校准。STM32F103出厂校准值存储在0x1FFFF7AC地址,需调用HAL_ADCEx_Calibration_Start()。
  • 解决:在MX_ADC1_Init()末尾添加:
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED); HAL_ADCEx_Calibration_GetValue(&hadc1); // 获取校准系数

4.5 现象:串口发送Modbus帧后,上位机收不到响应

  • 原因:Modbus RTU帧末尾需加3.5字符时间静默期(本工程波特率115200,即3.5*10*1000/115200≈304us),但HALHAL_UART_Transmit()无此功能。
  • 解决:在protocol.c发送完帧后插入:
    HAL_UART_Transmit(&huart1, tx_buffer, tx_len, 100); // 手动延时304us for(volatile int i=0; i<22; i++) __NOP(); // 22*13.9ns≈306us

5. 温控逻辑实战:PID参数整定、继电器防抖、数据持久化到Flash

5.1 温度PID控制器:为什么去掉微分项?

温室环境热惯性大(空气升温慢、土壤储热强),微分项会放大噪声(DHT22温度波动±0.5℃),导致继电器频繁开关(缩短寿命)。本工程采用PI控制:

typedef struct { float Kp; // 比例增益,设为2.0(实测:温度偏差1℃→继电器开启时间增加2s) float Ki; // 积分时间,设为60.0(单位:秒,防止积分饱和) float integral; // 积分项累加值 float last_error; } pid_t; float pid_compute(pid_t* pid, float setpoint, float feedback) { float error = setpoint - feedback; pid->integral += error * 0.1f; // 0.1s采样周期 // 防积分饱和:限制integral在[-100, 100] if (pid->integral > 100.0f) pid->integral = 100.0f; if (pid->integral < -100.0f) pid->integral = -100.0f; float output = pid->Kp * error + pid->Ki * pid->integral * 0.1f; return output > 0 ? output : 0; // 输出非负(继电器仅支持ON/OFF) }

参数整定法:先设Kp=0, Ki=0,观察自然降温曲线;再逐步加大Kp至系统临界振荡(如温度在25±2℃波动),取Kp_critical=1.5,则Kp=0.6*Kp_critical=0.9;最后加Ki消除静差。

5.2 继电器防抖策略:硬件+软件双保险

  • 硬件:在PB12与继电器IN间串联1kΩ电阻,降低驱动电流尖峰;
  • 软件:control_logic.c中定义RELAY_DEBOUNCE_MS=500,继电器状态变更后强制锁定500ms:
    static uint32_t last_relay_time = 0; void set_relay_state(uint8_t state) { if (HAL_GetTick() - last_relay_time < 500) return; // 500ms内不重复操作 relay_control(state); last_relay_time = HAL_GetTick(); }

5.3 数据持久化:把设定温度存到Flash,掉电不丢

STM32F103内置Flash支持页擦除(1KB/页),地址范围0x08000000~0x0800FFFF。本工程将设定温度存在0x0800F000(最后一页):

#define FLASH_TEMP_ADDR 0x0800F000 void save_setpoint(float temp) { HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); HAL_FLASHEx_Erase(&eraseInitStruct, &SECTORError); // 擦除整页 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_TEMP_ADDR, *(uint32_t*)&temp); HAL_FLASH_Lock(); } float load_setpoint(void) { return *(float*)FLASH_TEMP_ADDR; // 直接读取 }

注意:Flash编程需先擦除页,且HAL_FLASH_Program()仅支持WORD(32bit)或DOUBLEWORD(64bit),不能写单字节。

5.4 串口协议扩展:兼容巴法云透传的Modbus帧改造

巴法云要求透传数据以{}包裹,如{"temp":25.3,"humi":45}。本工程protocol.c中新增:

void send_to_baifa_cloud(float temp, float humi) { char json_buf[64]; sprintf(json_buf, "{\"temp\":%.1f,\"humi\":%.0f}", temp, humi); // 巴法云要求帧头0x00+长度+数据 uint8_t frame[64]; frame[0] = 0x00; frame[1] = strlen(json_buf); memcpy(&frame[2], json_buf, strlen(json_buf)); HAL_UART_Transmit(&huart1, frame, strlen(json_buf)+2, 100); }

实测可直连ESP8266(AT指令AT+CIPSEND=64),无需额外解析。

从那以后我每次做STM32项目,都会先确认三点:read_id()是否返回0xA1A1、HAL_ADCEx_Calibration_Start()是否执行、__HAL_AFIO_REMAP_SWJ_NOJTAG()是否调用——这三步做完,80%的硬件通信问题就消失了。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询