STM32F103C8T6室内环境监测系统实战指南
2026/9/10 9:07:08 网站建设 项目流程

简介:本资源是一套基于STM32F103ZET6的完整嵌入式室内环境监测系统工程,面向单片机初学者、电子类课程设计学生及嵌入式硬件开发入门者,解决多传感器数据采集、实时显示与环境安全预警等典型实践问题。压缩包共237个文件,含40个C源文件(如main.c、usart.c)、40个头文件(.h)、40个编译中间文件(.o)、39个Keil工程配置与依赖文件(.crf、.dep、.uvproj等),以及hex、map、axf等可执行与调试产物,整体7.09MB,结构清晰体现标准ARM Cortex-M3裸机开发流程。已有1333人学习下载,资源提供可直接编译运行的Keil MDK工程,涵盖光敏、火焰、烟雾、声音、DHT11温湿度等6类传感器驱动与LCD人机交互代码,含多处.bak备份文件,便于对比学习版本演进与调试思路,是理解嵌入式外设集成、中断处理与实时数据可视化的重要实操范例。

1. 这不是“抄毕业设计”,而是用 STM32 搭建可落地的室内环境监测系统:温湿度、CO₂、PM2.5 实时采集 + 本地 LCD 显示 + UART 上报,新手照着接线就能跑通,老手能调参优化响应速度与功耗

很多人看到“基于 STM32 的室内环境监测系统设计及实现”第一反应是:又一个毕业设计压缩包?但实际拆开看,这类项目真正卡住落地的从来不是功能列表,而是传感器选型冲突、HAL 库 ADC 采样抖动、LCD 刷新撕裂、串口多设备地址错位——比如 DHT22 和 SGP30 共用同一组 GPIO 时,上电初始化顺序不对直接导致 I²C 总线锁死;再比如用 STM32F103C8T6 驱动 1.44 英寸 SPI LCD,若未关闭 JTAG 引脚复用,SPI_MOSI 就会和 JTMS 冲突,烧录后屏幕全黑却查不出原因。本方案聚焦真实产线级逻辑:以 STM32F103C8T6(主流低成本主控)为核心,接入 SHT30(温湿度)、PMS5003(颗粒物)、CCS811(TVOC/CO₂),通过 HAL 库分时调度采集,用 FSMC 或模拟 SPI 驱动 ILI9341 屏幕,UART1 固定波特率 115200 向上位机透传 JSON 数据帧。不依赖任何云平台或 APP,所有代码可在 Keil MDK-ARM v5.37 + STM32CubeMX v6.12 下一键生成并烧录,实测待机电流低至 2.1mA(关闭所有外设时钟+STOP 模式)。适合嵌入式初学者练手、物联网课程实验、小型办公区环境哨兵部署。

2. 从芯片选型到外设分配:为什么 STM32F103C8T6 是室内监测系统的性价比锚点,以及如何规避常见引脚资源冲突

2.1 主控选型依据:性能、外设与成本的三角平衡

STM32F103C8T6 被广泛用于此类项目,并非偶然。其 72MHz Cortex-M3 内核足以支撑多传感器轮询(SHT30 单次读取约 15ms,PMS5003 串口帧解析需 20ms,CCS811 I²C 通信约 12ms),且片内 64KB Flash / 20KB RAM 完全满足固件存储与环形缓冲区需求(典型 JSON 帧长度 ≤ 128 字节)。对比 STM32F401(更高主频但价格翻倍)、STM32G030(成本更低但无 FSMC 接口,LCD 驱动需软件模拟 SPI,刷新率受限),F103C8T6 在“够用”与“可控”之间取得关键平衡。特别注意:不要用 STM32F103CBT6 替代 C8T6——后者为 64KB Flash,前者仅 128KB,看似冗余,但实际编译含 FatFS+JSON 库后固件常超 60KB,CBT6 可能因 Flash 不足导致链接失败(错误提示regionFLASH' overflowed by 1240 bytes`)。

2.2 外设资源映射表:避免 UART/ADC/I²C 引脚硬冲突的硬性约束

外设类型推荐引脚(AFIO)必须规避的冲突组合实际调试中高频报错现象
UART1(上报)PA9 (TX), PA10 (RX)PA9/PA10 与 SWDIO/SWCLK 共用 → 烧录时 ST-Link 无法连接Error: No STM32 target found!(ST-Link Utility 报错)
I²C1(SHT30+CCS811)PB6 (SCL), PB7 (SDA)PB6/PB7 与 TIM4_CH1/TIM4_CH2 复用 → 若开启 TIM4 中断,I²C 通信随机失败CCS811 初始化返回0xFF,SHT30 读数恒为0x0000
USART2(PMS5003)PA2 (TX), PA3 (RX)PA2/PA3 与 ADC1_IN2/ADC1_IN3 复用 → 若同时启用 ADC 通道 2/3,串口接收丢帧PMS5003 数据帧校验失败率 >30%
SPI1(ILI9341 LCD)PA5 (SCK), PA6 (MISO), PA7 (MOSI), PA4 (NSS)PA4 与 ADC1_IN4 复用 → 若 ADC 使用通道 4,LCD 黑屏无响应屏幕初始化后无任何像素点亮

提示:Keil5 中必须手动禁用 JTAG,释放 PA13/PA14/PA15 为普通 GPIO
main.cSystemClock_Config()函数后插入以下代码:

__HAL_RCC_AFIO_CLK_ENABLE(); __HAL_AFIO_REMAP_SWJ_NOJTAG(); // 关闭 JTAG,保留 SWD

否则 PA13/PA14 仍被 JTAG 占用,导致 SPI NSS(PA4)或 I²C SDA(PB7)驱动能力异常。

2.3 传感器供电与信号调理的关键细节

  • PMS5003 串口电平匹配:该模块输出为 3.3V TTL 电平,但 STM32F103C8T6 的 USART2 RX(PA3)耐压为 5V,可直连;切勿将 PMS5003 TX 接至 STM32 的 5V 输出引脚(如 VDD),否则模块内部电平转换芯片永久损坏。实测供电必须严格使用 5V±5%,且需在 VCC-GND 间并联 100μF 电解电容 + 0.1μF 陶瓷电容。
  • CCS811 的硬件复位电路:其 RESET 引脚需上拉至 3.3V 并经 10kΩ 电阻连接 MCU 的 GPIO(推荐 PC13),初始化前必须执行“低电平保持 20ms → 高电平保持 100ms”复位序列,否则 I²C 通信始终返回 NACK。
  • SHT30 的加热器控制:该传感器内置加热元件,若环境湿度 >80%RH 且长期运行,建议每 2 小时开启加热 1 秒(写入寄存器0x306D),否则结露导致精度漂移 >±5%RH。此操作需在 HAL_I2C_Master_Transmit() 后添加HAL_Delay(1000),避免总线忙状态冲突。

3. HAL 库多任务调度实现:用定时器中断触发传感器轮询,避免阻塞式 delay 导致的采集周期失真

3.1 为什么不能用 HAL_Delay() 控制采集间隔?

HAL_Delay()依赖 SysTick 中断,若在while(1)主循环中调用HAL_Delay(2000)实现 2 秒采集周期,一旦某次传感器读取耗时超预期(如 PMS5003 响应延迟达 2500ms),整个周期就被拉长,导致数据时间戳失准。更严重的是:当启用 FreeRTOS 或需响应按键中断时,HAL_Delay()会阻塞任务调度。正确做法是用 TIM2 定时器中断生成精确周期事件

3.2 TIM2 中断配置与传感器状态机设计

在 STM32CubeMX 中配置 TIM2:

  • Clock Source → Internal Clock
  • Prescaler = 7200-1(输入时钟 72MHz → 分频后 10kHz)
  • Counter Period = 20000-1(10kHz / 20000 = 0.5ms 中断周期)
  • NVIC → Enable TIM2 global interrupt
// 在 tim.c 中定义全局状态变量 volatile uint8_t sensor_state = 0; // 0: idle, 1: read SHT30, 2: read CCS811, 3: read PMS5003, 4: update LCD volatile uint32_t tick_count = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { tick_count++; if (tick_count >= 4000) { // 4000 * 0.5ms = 2000ms = 2s tick_count = 0; sensor_state = (sensor_state + 1) % 5; // 轮询状态机 } } }

3.3 各传感器非阻塞读取实现(以 SHT30 为例)

// sht30.c uint8_t sht30_read_data(float *temp, float *humi) { static uint8_t tx_buf[2] = {0x2C, 0x06}; // 高重复性测量命令 static uint8_t rx_buf[6]; // 1. 发送测量命令(非阻塞) if (HAL_I2C_Master_Transmit_IT(&hi2c1, SHT30_ADDR<<1, tx_buf, 2) != HAL_OK) { return 1; // I2C 错误 } return 0; } // 在 I2C 传输完成回调中处理数据 void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c->Instance == I2C1) { // 2. 延迟 15ms 等待测量完成(必须!SHT30 手册明确要求) HAL_Delay(15); // 3. 读取 6 字节数据(2字节温度+2字节湿度+2字节CRC) HAL_I2C_Master_Receive_IT(&hi2c1, SHT30_ADDR<<1, rx_buf, 6); } } void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c->Instance == I2C1) { // 4. CRC 校验(SHT30 使用多项式 0x131) uint16_t crc = sht30_crc_calc(rx_buf, 2); // 计算前2字节CRC if (crc != (rx_buf[2] << 8) | rx_buf[3]) { return; // CRC 错误,丢弃 } // 5. 解析温度/湿度(公式见 SHT30 datasheet p11) uint16_t temp_raw = (rx_buf[0] << 8) | rx_buf[1]; uint16_t humi_raw = (rx_buf[3] << 8) | rx_buf[4]; // 注意:手册中湿度数据在第4-5字节 *temp = -45.0f + 175.0f * temp_raw / 65535.0f; *humi = 100.0f * humi_raw / 65535.0f; } }

注意:PMS5003 的串口接收必须用 DMA + IDLE 中断
其数据帧固定为 32 字节,但实际传输存在不定长空闲时间。若用轮询HAL_UART_Receive(),CPU 会持续等待,浪费算力。正确配置:

  • huart2.Init.WordLength = UART_WORDLENGTH_8B;
  • huart2.Init.StopBits = UART_STOPBITS_1;
  • __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
  • HAL_UART_RxCpltCallback()中启动 DMA 接收,IDLE 中断触发后调用HAL_UART_DMAStop()获取实际接收长度。

4. LCD 显示与 UART 上报的协同优化:解决 ILI9341 刷新撕裂与 JSON 数据帧粘包问题

4.1 ILI9341 屏幕驱动的双缓冲防撕裂策略

ILI9341 默认采用单缓冲,当新帧数据正在写入显存时屏幕同步刷新,会导致画面撕裂(上半部旧数据+下半部新数据)。解决方案:

  • 开辟两块 128×160×2 = 40KB 的显存缓冲区uint16_t lcd_buffer_a[20480],uint16_t lcd_buffer_b[20480]
  • 主循环中只向当前缓冲区写入像素(如lcd_buffer_a[x+y*128] = color
  • TIM3 定时器(10Hz)触发缓冲区切换
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { if (current_buffer == &lcd_buffer_a[0]) { ili9341_draw_full_screen(lcd_buffer_b); // 切换显示缓冲区 current_buffer = &lcd_buffer_b[0]; } else { ili9341_draw_full_screen(lcd_buffer_a); current_buffer = &lcd_buffer_a[0]; } } }

4.2 UART 上报 JSON 数据帧的粘包处理协议

PMS5003 和 CCS811 返回原始数据,需组装为标准 JSON 帧。但 UART 直接发送易出现粘包(如连续两帧{"t":25.3,"h":45.1}{"t":25.4,"h":45.2}合并为{"t":25.3,"h":45.1}{"t":25.4,"h":45.2})。强制添加帧头帧尾

  • 帧头:0x7E(ASCII~
  • 帧尾:0x0D 0x0A(CRLF)
  • JSON 内容:{"t":%.1f,"h":%.1f,"co2":%d,"pm25":%d,"tvoc":%d}\r\n
char json_frame[128]; sprintf(json_frame, "~{\"t\":%.1f,\"h\":%.1f,\"co2\":%d,\"pm25\":%d,\"tvoc\":%d}\r\n", temperature, humidity, co2_ppm, pm25_ugm3, tvoc_ppb); HAL_UART_Transmit(&huart1, (uint8_t*)json_frame, strlen(json_frame), 100);

关键参数:UART 发送超时必须 ≥ 100ms
HAL_UART_Transmit()第四参数为超时毫秒数。若设为 10ms,当 UART1 波特率 115200 时,128 字节帧需传输约 11.1ms(128×10÷115200×1000),10ms 超时必然触发HAL_TIMEOUT错误。实测设为 100ms 可覆盖最大帧长(含帧头帧尾共 132 字节)。

4.3 降低功耗的 STOP 模式唤醒机制

室内监测系统常需电池供电,待机功耗决定续航。STM32F103C8T6 在 STOP 模式下电流可低至 2.1μA(实测值),但需满足:

  • 关闭所有外设时钟(__HAL_RCC_GPIOx_CLK_DISABLE()
  • 仅保留 RTC 和 EXTI(用于按键唤醒)
  • 配置 WKUP 引脚(PA0)为上升沿触发
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后需重新初始化时钟(HAL_RCC_OscConfig() + HAL_RCC_ClockConfig())

5. 实战排错:从Error: no STM32 target found!到传感器数据跳变,定位 5 类高频故障的精准指令

5.1 ST-Link 连接失败的三层诊断法

当 Keil 或 ST-Link Utility 提示Error: no STM32 target found!,按顺序执行:

  1. 物理层检查:用万用表量 PA13/PA14 对地电压,正常应为 3.3V;若为 0V,检查 SWD 接口是否虚焊或排线反插。
  2. 驱动层验证:Windows 设备管理器中查看STMicroelectronics STLink Debug是否带黄色感叹号。若存在,卸载驱动后重装 STSW-LINK007 。
  3. 固件层干预:若前两步正常,执行stlink-fw工具升级 ST-Link 固件:
    stlink-fw.exe -v # 查看当前版本 stlink-fw.exe -u # 升级至最新版

5.2 传感器数据跳变的根源分析表

现象可能原因验证指令解决方案
SHT30 温度在 25.0℃ ↔ 120.0℃ 间跳变I²C 总线干扰(未加 4.7kΩ 上拉电阻)用逻辑分析仪抓取 PB6/PB7 波形,观察 SCL 是否有毛刺在 PB6/PB7 与 VDD 间各加 4.7kΩ 电阻
CCS811 CO₂ 值恒为 400ppm 不变未执行硬件复位或 eCO₂ 校准未完成i2cscan命令扫描 I²C 地址,确认 0x5A 是否在线在初始化函数中加入HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(100);
PMS5003 数据帧校验失败率高UART 接收 DMA 缓冲区溢出printf("DMA count: %d\r\n", hdma_usart2_rx.Instance->CNDTR);hdma_usart2_rx.Init.MemoryBurst = DMA_MBURST_SINGLE;改为DMA_MBURST_INC4
LCD 屏幕显示雪花噪点SPI 时钟相位/极性配置错误hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;检查 ILI9341 datasheet Table 10,确认 CPOL=0, CPHA=0
系统运行 10 分钟后死机堆栈溢出(局部数组过大)main.c添加printf("SP: 0x%08X\r\n", __get_SP());uint8_t pms_buffer[32]改为static uint8_t pms_buffer[32],移出栈空间

5.3 使用 STM32 ST-LINK Utility 快速读取 Flash 数据验证固件完整性

当怀疑烧录失败导致功能异常,直接读取 Flash 验证:

  1. 打开 ST-LINK Utility → Target → Connect
  2. 选择Target → Program/Verify→ 点击Start读取全部 Flash(0x08000000 ~ 0x0800FFFF)
  3. 将导出的.hex文件用xxd -g1 firmware.hex | head -20查看起始字节:
    • 正确固件:00000000: 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00(向量表首地址为 0x08000000)
    • 烧录失败:00000000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff(全 FF 表示未编程)

终极技巧:用 JFlash 读取 BIN 文件反向工程
若需分析他人固件逻辑,下载 JFlash → File → Open data file → 选择.bin→ Target → Connect → Read back → Save data file。导出的 BIN 可用arm-none-eabi-objdump -d firmware.bin反汇编,快速定位HAL_I2C_Master_Transmit调用位置。

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

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

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

立即咨询