基于STM32的大棚温湿度检测与蓝牙APP控制系统设计
简介面向单片机开发初学者与毕业设计学生的这套项目资料围绕STM32与DHT11传感器实现大棚温湿度采集并通过蓝牙APP完成远程监控与设备控制覆盖从底层驱动到手机端交互的完整链路。包内共159个文件压缩后约18.11MB主要包含H/C源码、工程配置文件、原理图schdoc与PCBpcbdoc设计文件、论文文档doc以及可直接烧录的hex文件和蓝牙调试APK便于从原理到实物运行全流程参考。已有170人浏览学习项目附有详细代码注释和论文支撑系统功能完善、界面直观既适合作为本科毕业设计也可用于期末大作业或课程设计的快速部署与演示。资源包含完整工程和设计图可帮助读者理解项目架构、掌握嵌入式开发流程并在此基础上进行二次开发与功能拓展。1. 基于STM32的大棚温湿度检测与蓝牙APP控制到底要解决什么大棚种植最怕的是温湿度失控。早上掀帘晚一小时棚内温度可能冲到40℃叶片萎蔫通风早了湿度骤降灰霉病又开始冒头。传统方案要么靠人工拿着温湿度计逐棚巡检要么拉一屋子RS485线节点一多布线成本比传感器还贵。用STM32做数据采集端通过蓝牙广播或透明传输把数据推给手机APP就能在值班室实时看十几个棚的状态还能远程控制排风扇、湿帘。这套设计从原理图、PCB到嵌入式代码和论文都包含正好覆盖了基于STM32的毕业设计里最典型的需求。适合想做完整项目的在校生也适合生产环境里需要快速搭一套低成本监测原型的工程师。它解决的核心问题是在不用WiFi、不依赖云平台的前提下用手机本地蓝牙把传感器数据变成可视化界面并保留控制通道。2. 基于STM32的大棚温湿度检测系统硬件选型与整体架构2.1 系统架构从传感器到APP的完整数据链路一套完整的蓝牙温湿度检测系统数据链路是传感器 → STM32微控制器 → 蓝牙模块 → 手机APP。传感器负责把温度、湿度转成数字信号或模拟电压STM32负责读取传感器数据、做简单滤波和错误处理然后通过串口把数据封装成协议帧发给蓝牙模块蓝牙模块在这里只做物理传输不做协议解释手机APP通过蓝牙适配器连接后按相同的协议解析字节流并刷新界面。我一般会把系统拆成三个独立模块数据采集端传感器、主控端STM32 蓝牙、显示控制端APP。这样做的好处是软硬件可以并行开发传感器坏了只换传感器蓝牙方案升级不用改主控代码。ARM Cortex-M3内核的STM32F103系列是这里最常见的选择因为它外设丰富、库函数资料多而且价格低。对于大棚这种对实时性要求不高的场景不需要上F4系列。2.2 主控与传感器选型STM32F103与SHT30/DHT22怎么选主控方面STM32F103C8T6这款芯片几乎成了这类项目的默认选项。它有64KB Flash、20KB RAM3个USART、2个I2C、2个SPI足够挂一个传感器和一个蓝牙模块。如果你要做的系统里还有继电器控制、OLED显示、或者多个传感器建议选STM32F103RCT6Flash扩大到256KB引脚也更多。两者的开发方式一致CubeMX里切换型号后重新生成代码就行。传感器才是决定数据精度的关键。DHT22也叫AM2302是最常见的温湿度传感器成本低单总线协议但数据更新的典型周期是2秒而且对时序要求非常苛刻稍有不慎就会读到无效位。SHT30是I2C接口的传感器精度更高典型±0.3℃、±2%RH有CRC校验读取速度也快代码写起来比DHT22干净。下面这张表是选型时常用的对比参数DHT22SHT30通信接口单总线自定义时序I2C标准/快速模式温度精度±0.5℃±0.1℃典型湿度精度±2%RH±1.5%RH采样周期2秒最快1ms价格低中代码复杂度高低如果只求能跑通、成本优先DHT22够用如果要做论文里的对比实验或者实际种植环境监测SHT30更方便。我这里后面会给出两种传感器都可以用的读取思路但代码示例以DHT22为主因为它在HC-05蓝牙调试时更容易暴露时序问题。2.3 蓝牙模块选型HC-05与BLE各自的适用场景蓝牙模块的选择决定了APP端走的是经典蓝牙Bluetooth Classic还是低功耗蓝牙BLE。HC-05是经典蓝牙SPP串口透传模块主从一体你把它当成一根无线串口线来用配置简单手机上用任何SPP串口工具就能直接收到数据。它适合原型验证但经典蓝牙协议在Android机上需要定位权限且在Android 12以上扫描受限较多。如果你做的是正式的毕业设计或者产品化项目我更推荐使用BLE模块比如JDY-08或AT-09它们内部是CC2541或nRF52832芯片通过串口与STM32通信手机用BLE API扫描和连接。BLE的优势是功耗低、连接速度快而且不需要像HC-05那样做配对码确认。缺点是手机APP需要自己实现GATT服务的收发逻辑。两种模块都能实现数据传输区别在于HC-05更像“串口替身”BLE更像“服务型通道”。后面章节我会分别说明协议设计和APP端的处理方式。2.4 硬件初始化代码示例CubeMX中的引脚分配与时钟配置无论用哪种传感器我都建议用STM32CubeMX生成初始化代码不要手写寄存器。下面是一个基于HAL库的串口和GPIO初始化片段假设在CubeMX中配置了PA2/PA3为USART2蓝牙PA1为DHT22的数据线PA5为LED指示灯。void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); /*Configure GPIO pin : PA1 (DHT22 data) */ GPIO_InitStruct.Pin GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出外部上拉 GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /*Configure GPIO pin : PA5 (LED indicator) */ GPIO_InitStruct.Pin GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); } void MX_USART2_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 9600; // 蓝牙模块默认波特率 huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; huart2.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart2) ! HAL_OK) { Error_Handler(); } }这里的关键参数是DHT22的数据引脚配成开漏输出并启用内部上拉。注意DHT22的时序要求高内部上拉电阻通常在几kΩ到几十kΩ之间如果信号边沿太缓需要外部加一个4.7kΩ的上拉电阻到3.3V。USART的波特率要跟蓝牙模块出厂默认值一致HC-05默认9600JDY-08默认9600或115200具体看模块手册。代码里的HAL_UART_Init函数会自动读取CubeMX生成的配置结构体所以改动波特率要在CubeMX中改不要直接改代码。3. 原理图与PCB设计从最小系统到蓝牙天线区域的布线规则3.1 STM32最小系统原理图晶振电容计算与复位电路最小系统主要包含电源、晶振、复位、BOOT引脚。STM32F103内部有8MHz的RC振荡器但内部RC精度不高蓝牙通信和串口波特率有轻微偏差容易出乱码所以外部一定要接8MHz晶振作为HSE。晶振两端各接一个负载电容到地电容大小由晶振本身的负载电容CL决定。常见公式是CL (C1 * C2) / (C1 C2) Cs其中Cs是PCB走线分布电容大概2~5pF。反过来算两个电容时可以粗略用C 2 * CL - Cs。比如你要用8MHz晶振负载电容标称18pF那么C1和C2可以取33pF。实际调试用公式计算后我会把同批次的晶振参数输入下面这个Python脚本快速评估def calc_load_cap(cl_crystal, stray_pf3.0): 根据晶振负载电容和PCB寄生电容计算匹配电容C1/C2 cl_crystal: 晶振手册中的负载电容单位pF stray_pf: PCB走线与引脚寄生电容一般取2~5pF c ( (cl_crystal - stray_pf) * 2 ); return c for cl in [12, 18, 20, 22]: c1 calc_load_cap(cl, stray_pf3.0) print(f负载电容 {cl}pF - 推荐C1C2{c1:.1f}pF取值 {int(c1 // 10 * 10 (5 if c1 % 10 else 0))}pF)脚本逻辑很简单直接计算2倍负载电容与寄生电容的差值得到匹配电容。如果计算结果不是标准容值就就近选取。比如18pF的负载电容算出来30pF就选33pF或30pF。注意晶振下面不要走任何信号线晶振外壳接地时钟走线要尽量短。复位电路用10kΩ上拉电阻加100nF电容到地即可NRST引脚内部有施密特触发器不需要太复杂。3.2 传感器接口与电源树避免电平冲突大棚现场通常使用12V适配器给整个系统供电板上需要一级DC-DC降压到5V给传感器如果传感器是5V供电再经过LDO降到3.3V给STM32和蓝牙模块。这里容易踩坑的是DHT22的供电范围是3.3V~5.5V当STM32以3.3V供电时如果直接把5V供电的DHT22数据引脚连到STM32的PA1就会形成电平冲突。我一般会统一用3.3V给所有数字部分供电DHT22也接3.3V。因为DHT22在3.3V下的数据时序和5V下基本一致只是上拉电阻需要更小一些。SHT30则只支持1.8V~3.6V所以也必须接3.3V。如果传感器需要5V供电数据线上加一个电平转换电路把5V的数据信号降到3.3V。最简单的做法是电阻分压串一个1kΩ再加一个2kΩ电阻到地但这种做法只适合低速信号DHT22的20~40微秒时序勉强可以SHT30的I2C在100kHz模式下也可以用。3.3 蓝牙模块部分HC-05的电平转换与BLE的走线要求HC-05模块的VCC典型是3.6V~6V很多模块板载LDO所以可以用5V供电。但它的UART引脚是3.3V电平这点要注意不少资料里直接拿5V单片机的TX接HC-05的RX长期工作会导致模块发热、通信不稳。STM32的TX/RX都是3.3V所以直连HC-05没问题。BLE模块比如JDY-08标准供电是2.2V~3.6V所以只能接3.3V。它的引脚间距更小建议在原理图中把TX、RX、STATE、RST全部引出来方便调试。布线时蓝牙模块的天线下方需要净空PCB天线区域不要铺铜、不要走电源线四层板时该区域对应地层挖空。如果使用外置天线天线座要靠近板边缘并保证50Ω阻抗的走线短而直。这些都是PCB设计时非常容易忽略的参数直接决定蓝牙通信距离是否稳定。3.4 PCB布局中的3个关键参数线宽、间距、天线净空做PCB布局时我最看重三个参数线宽、间距和天线净空。电源线按照电流来做宽度3.3V主干建议≥1mmGND铺铜全覆盖信号线最细可以用0.3mm传感器和蓝牙信号线建议0.5mm。间距方面普通信号线间距≥0.3mm晶振和蓝牙天线区域与其他走线间距≥0.7mm。天线净空指天线周围至少5mm内无覆铜、无器件这个距离越大越好。下面这个表是我自己做这类板子时的默认值对象线宽/间距备注3.3V电源线≥1.0mm如果走LED需按LED电流加粗GND铺铜全覆盖顶层底层都要过孔连接晶振走线线宽0.3mm间距≥0.7mm下方不能有信号线HC-05 UART线0.5mm尽量短远离晶振蓝牙天线净空5mm无铺铜天线正下方挖空这些参数不是绝对的但能满足大多数两层板EMC要求。底层铺铜时记得做栅格铺铜或者实心铺铜实心铺铜对信号回流的帮助更大但会增加部分工艺成本。制板之前用PCB软件的DRC检查一遍有无未连接的引脚和间距错误特别是蓝牙模块的细脚焊盘。4. 嵌入式代码实现STM32读取温湿度并通过串口发蓝牙4.1 开发环境STM32CubeMX生成工程与串口配置STM32开发环境的搭建几乎是固定的套路安装STM32CubeMX、Keil MDK或STM32CubeIDE以及ST-Link驱动。CubeMX里选择芯片型号后将外部晶振设为HSE把调试口设为Serial Wire避免占用PB3/PB4再配置USART1调试打印用和USART2蓝牙DHT22引脚设成输出开漏然后生成初始化代码。有一点要注意DHT22的数据引脚需要频繁切换方向输出拉低/释放输入读高电平所以在HAL库中不要把它配置成完全输出或完全输入而是在代码里用GPIO模式切换函数。很多新手在CubeMX里把它固定为输出模式后面直接读寄存器读不出数据。我建议在CubeMX里只初始化时钟和引脚方向由代码里设置。4.2 DHT22的驱动时序与代码DHT22的通信时序是典型的单总线协议主机发送起始信号拉低至少18ms再拉高20~40us然后释放总线。DHT22会先响应一个80us低电平和80us高电平然后连续输出40位数据湿度16位温度16位校验8位。每一位的读取逻辑是引脚先拉低50us再拉高高电平持续26~28us表示“0”持续70us表示“1”。下面的函数实现了这40位数据的读取uint8_t DHT22_ReadData(float *temp, float *hum) { uint8_t data[5] {0}; uint8_t bits[40] {0}; uint8_t i, j; uint32_t counter 0; /* 主机起始信号 */ HAL_GPIO_WritePin(DHT_GPIO_Port, DHT_Pin, GPIO_PIN_RESET); HAL_Delay(20); // 拉低至少18ms HAL_GPIO_WritePin(DHT_GPIO_Port, DHT_Pin, GPIO_PIN_SET); delay_us(30); // 拉高20~40us HAL_GPIO_WritePin(DHT_GPIO_Port, DHT_Pin, GPIO_PIN_RESET); /* 切换为输入模式 */ GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT_Pin; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT_GPIO_Port, GPIO_InitStruct); /* 等待响应低电平 */ counter 0; while (HAL_GPIO_ReadPin(DHT_GPIO_Port, DHT_Pin) GPIO_PIN_RESET) if (counter 100) return 1; // 超时 /* 等待响应高电平 */ counter 0; while (HAL_GPIO_ReadPin(DHT_GPIO_Port, DHT_Pin) GPIO_PIN_SET) if (counter 100) return 1; /* 读取40位数据 */ for (i 0; i 40; i) { counter 0; while (HAL_GPIO_ReadPin(DHT_GPIO_Port, DHT_Pin) GPIO_PIN_RESET) if (counter 100) return 2; // 超时 delay_us(40); // 在高电平中间采样 if (HAL_GPIO_ReadPin(DHT_GPIO_Port, DHT_Pin) GPIO_PIN_SET) bits[i] 1; else bits[i] 0; counter 0; while (HAL_GPIO_ReadPin(DHT_GPIO_Port, DHT_Pin) GPIO_PIN_SET) if (counter 100) return 3; } /* 拼装成5字节 */ for (i 0; i 5; i) { for (j 0; j 8; j) { data[i] (data[i] 1) | bits[i * 8 j]; } } /* 校验 */ if (data[0] data[1] data[2] data[3] ! data[4]) return 4; *hum ((uint16_t)(data[0] 8 | data[1])) / 10.0f; *temp ((uint16_t)(data[2] 8 | data[3])) / 10.0f; return 0; }这段代码里最关键的是采样时机。每一位的0或1是由高电平持续长度决定的我在高电平开始后延时40us如果这时引脚仍是高说明是1否则是0。默认的HAL_Delay是毫秒级不能用在这里所以需要实现delay_us微秒延时。最简单的办法是用定时器或SysTick做延时函数在CubeMX里把SysTick配置为1MHz时钟然后写一个空循环计数。注意读取完数据后要把GPIO重新配置为输出模式并拉高保持总线空闲。4.3 蓝牙通信协议设计帧头、长度、校验蓝牙串口是字节流APP端必须能从连续数据中找出每一帧。我常用的一套简易协议是帧头0xAA、命令字、数据长度、数据负载、CRC校验。下面定义了一个固定长度的温湿度数据帧字节序号内容说明00xAA帧头10x01命令字实时温湿度20x04数据长度温度和湿度各2字节3-4温度值有符号整数单位0.1℃5-6湿度值无符号整数单位0.1%RH7CRC8从0到6字节的异或和发送端代码很简单把温度扩大10倍后拆成高八位和低八位void SendTHData(float temp, float hum) { uint8_t txbuf[8]; int16_t t (int16_t)(temp * 10); uint16_t h (uint16_t)(hum * 10); uint8_t crc 0; txbuf[0] 0xAA; txbuf[1] 0x01; txbuf[2] 0x04; txbuf[3] (uint8_t)(t 8); txbuf[4] (uint8_t)(t 0xFF); txbuf[5] (uint8_t)(h 8); txbuf[6] (uint8_t)(h 0xFF); for (int i 0; i 7; i) crc ^ txbuf[i]; txbuf[7] crc; HAL_UART_Transmit(huart2, txbuf, 8, 100); }这样设计的好处是APP端不用逐字节积累只需要检测到0xAA且下个字节是0x01就按固定长度读取。异或校验比累加和更容易实现任何语言都是一行代码。如果以后要加控制命令比如启动排风扇只需扩展命令字和长度字段即可。4.4 完整的数据流代码定时采集与发送在主循环里我一般会每2秒调用一次DHT22_ReadData成功后调用SendTHData。如果读取失败连续失败3次可能说明传感器未连接或线路有问题此时发送一个错误帧给APP提示。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); float temp 0, hum 0; uint8_t fail_cnt 0; while (1) { if (DHT22_ReadData(temp, hum) 0) { fail_cnt 0; SendTHData(temp, hum); printf(temp:%.1f hum:%.1f\r\n, temp, hum); } else { fail_cnt; if (fail_cnt 3) { printf(DHT22 read error\r\n); fail_cnt 0; } } HAL_Delay(2000); } }这里把USART1接到ST-Link的虚拟串口PC端用串口助手可以同步看到调试信息。注意printf重定向要在工程里实现fputc函数否则输出无效。主循环中不建议做高精度等待因为DHT22的读取已经占用了约50ms剩下的2000ms足够完成蓝牙发送。4.5 调试时用到的串口日志蓝牙通信中最容易出问题的是波特率不匹配。HC-05模块默认9600但如果之前配置过命令把波特率改成115200而STM32不改就会出现收到的全是大段乱码。排查方法是在上电后先用串口调试助手连接STM32的调试串口确认DHT22数据正常再用USB-TTL直接连蓝牙模块的TXD和RXD用手机SPP工具发送AT指令验证模块波特率。常见AT指令有AT返回OK、ATUART查询波特率、ATROLE查询主从模式。注意AT模式需要模块上电前按住按键或者通过EN引脚拉高进入AT状态否则无法识别指令。5. 蓝牙APP控制端从连接扫描到温湿度界面显示5.1 经典蓝牙SPP与BLE的选择以及Android端权限APP端到底选HC-05还是BLE模块直接决定了代码写法。如果是HC-05APP需要走经典的BluetoothSocket连接Android在API 30以上需要动态申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限并且在扫描设备时要额外申请定位权限因为Beacon扫描逻辑可能涉及位置信息。如果是BLE方案APP使用BluetoothLeScanner扫描连接后用BluetoothGatt读写特征值权限也是BLUETOOTH_SCAN和BLUETOOTH_CONNECT但不需要定位权限。下面这段是Android 12环境下的权限声明清单uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /如果需要兼容Android 11及以下还要保留BLUETOOTH和BLUETOOTH_ADMIN。动态权限请求通常在Activity的onCreate里统一处理不要放在扫描回调里否则容易出现重复弹窗。5.2 最小BLE扫描绑定代码Kotlin/JavaBLE模块在上电后会广播一个设备名APP扫描到后发起连接。下面是最精简的扫描代码val scanResult BluetoothLeScannerCompat.getScanner() val scanSettings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult?) { val device result?.device val name device?.name ?: return if (name.startsWith(JDY-08)) { // 找到目标设备停止扫描并连接 scanResult.stopScan(this) connectToDevice(device) } } } scanResult.startScan(scanCallback)这里用到的是Android的BLE扫描接口startScan时传入ScanSettingsSCAN_MODE_LOW_LATENCY表示高占空比扫描适合首次搜索。找到设备后要立刻stopScan避免持续扫描耗电。connectToDevice方法内部用BluetoothGattCallback处理连接状态。HC-05则用BluetoothAdapter的startDiscovery但HC-05默认会被系统过滤掉设备名所以很多同学会碰到hc05蓝牙模块连接不上的问题具体在5.4节展开。5.3 数据解析与界面更新把协议帧变成温度数值BLE默认是字节流JDY-08模块内部把串口数据打包成WriteCharacteristic事件驱动每次STM32发送8个字节APP会在onCharacteristicChanged回调里收到这8个字节。解析逻辑和STM32端是对称的fun parsePacket(data: ByteArray): PairFloat, Float? { if (data.size 8) return null if (data[0] ! 0xAA.toByte()) return null if (data[1] ! 0x01.toByte()) return null val length data[2].toInt() if (length ! 4) return null val sum data.sliceArray(0..6).fold(0) { a, b - a xor b.toInt() } if (sum ! data[7].toInt()) return null val tempRaw (data[3].toInt() shl 8) or data[4].toInt() val humRaw (data[5].toInt() shl 8) or data[6].toInt() return (tempRaw / 10f) to (humRaw / 10f) }注意Kotlin里Byte是有符号的所以data[i].toInt()要再和0xFF做与运算避免负数扩展但这里直接用toInt()在特定情况下会出错严谨写法是(data[i].toInt() and 0xFF)。主要原因是STM32端温度是int16类型负数温度会以补码形式出现APP端必须用and 0xFF还原。解析完成后通过runOnUiThread把温度湿度设置到TextView或图表View上即可。如果要做控制可以在APP端发一个按钮事件写入同样的0xAA帧给STM32STM32在串口接收中断里解析即可。5.4 连接不上时的排查路径HC-05配对失败和BLE连接失败蓝牙连接不上是所有蓝牙项目中最高频的错误。HC-05连接不上最常见的原因是模块没有进入AT模式而是仍然处于数据模式手机上搜索到后无法配对。另一种情况是模块被其他手机连接过进入AT模式后用ATRESET恢复默认状态。还有一点是HC-05的密码默认1234但密秘籍可能在模块端被改过重置后才可恢复。BLE连接失败的表现更隐蔽扫描不到设备或者扫描到但connectGatt返回false。扫描不到设备先确认蓝牙模块是否上电、是否被其他设备占用connect成功但没有任何数据回调通常是STM32端没有在收到连接状态前发送数据或者串口波特率不一致。这些排查可以用手机自带的Nordic nRF Connect等工具但不要用网上所谓的“一键蓝牙修复”工具它们并不能解决问题反而可能干扰系统权限。6. 论文写作与系统验证把实物测试变成可信的数据图6.1 用Excel或Python记录长时间温湿度数据论文里最好的论据是一组长时间稳定的数据。我通常让STM32每隔10秒向串口发送一条CSV格式数据PC端用Python pyserial读取并保存到文件import serial, csv, time ser serial.Serial(COM3, 9600, timeout1) with open(log.csv, w, newline) as f: writer csv.writer(f) writer.writerow([timestamp, temp, hum]) while True: line ser.readline().decode(utf-8).strip() if line: t time.strftime(%Y-%m-%d %H:%M:%S) # 假设stm32发送格式: 25.3,60.1 temp, hum line.split(,) writer.writerow([t, temp, hum]) f.flush()这段脚本不用额外安装第三方库只依赖pyserial。运行前先确认串口号和波特率数据记录时间建议持续12小时以上。使用Excel打开CSV后插入折线图横轴是时间纵轴是温度和湿度就可以直接用在论文里。6.2 传感器校准与对比验证技巧很多论文评审老师会问“你的传感器数据准不准”。答案要给出对比实验。方法是用一个标准水银温度计或校准过的湿度计放在同一个环境中记录传感器读取值和标准值差值然后修正代码里的offset。SHT30可以用I2C命令写入校准寄存器DHT22只能在代码里加一个固定的偏差补偿。建议在论文中放一张温度对比表包含标准值、测量值、误差和补偿后的值。6.3 论文中最容易扣分的细节时序图与状态描述蓝牙通信原理部分不少学生会直接复制别人的文字但评审一眼就能看出时序有没有画对。最稳妥的做法是用Visio画一个简单的状态图包含“系统上电→传感器初始化→蓝牙等待连接→APP连接后循环发送数据→断开重连”这几个状态。重点强调APP与STM32的交互是“由APP发起连接请求STM32被动接受”不要写成“STM32向手机发送连接请求”这在经典蓝牙模式才是允许的BLE模式下是绝对错误的。最后再分享一个我常用的验证技巧在APP界面上增加一个“连续发送”开关让STM32每500ms发一次APP连续接收统计掉包率。理论上同一房间内的蓝牙丢包率应低于1%如果超过5%优先检查蓝牙天线区域是否有覆盖物其次检查串口波特率是否过高。推荐使用9600波特率虽然数据速率低但抗干扰能力强适合大棚这种电磁环境复杂的场景。本文还有配套的精品资源点击获取