简介:面向单片机开发初学者与毕业设计学生的这套项目资料,围绕STM32与DHT11传感器实现大棚温湿度采集,并通过蓝牙APP完成远程监控与设备控制,覆盖从底层驱动到手机端交互的完整链路。包内共159个文件,压缩后约18.11MB,主要包含H/C源码、工程配置文件、原理图(schdoc)与PCB(pcbdoc)设计文件、论文文档(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 RAM,3个USART、2个I2C、2个SPI,足够挂一个传感器和一个蓝牙模块。如果你要做的系统里还有继电器控制、OLED显示、或者多个传感器,建议选STM32F103RCT6,Flash扩大到256KB,引脚也更多。两者的开发方式一致,CubeMX里切换型号后重新生成代码就行。
传感器才是决定数据精度的关键。DHT22(也叫AM2302)是最常见的温湿度传感器,成本低,单总线协议,但数据更新的典型周期是2秒,而且对时序要求非常苛刻,稍有不慎就会读到无效位。SHT30是I2C接口的传感器,精度更高(典型±0.3℃、±2%RH),有CRC校验,读取速度也快,代码写起来比DHT22干净。下面这张表是选型时常用的对比:
| 参数 | DHT22 | SHT30 |
|---|---|---|
| 通信接口 | 单总线(自定义时序) | 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默认9600,JDY-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_pf=3.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_pf=3.0) print(f"负载电容 {cl}pF -> 推荐C1=C2={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主干建议≥1mm,GND铺铜全覆盖;信号线最细可以用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校验。下面定义了一个固定长度的温湿度数据帧:
| 字节序号 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 |
| 1 | 0x01 | 命令字:实时温湿度 |
| 2 | 0x04 | 数据长度(温度和湿度各2字节) |
| 3-4 | 温度值 | 有符号整数,单位0.1℃ |
| 5-6 | 湿度值 | 无符号整数,单位0.1%RH |
| 7 | CRC8 | 从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)、AT+UART(查询波特率)、AT+ROLE(查询主从模式)。注意AT模式需要模块上电前按住按键,或者通过EN引脚拉高进入AT状态,否则无法识别指令。
5. 蓝牙APP控制端:从连接扫描到温湿度界面显示
5.1 经典蓝牙SPP与BLE的选择,以及Android端权限
APP端到底选HC-05还是BLE模块,直接决定了代码写法。如果是HC-05,APP需要走经典的BluetoothSocket连接,Android在API 30以上需要动态申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限,并且在扫描设备时要额外申请定位权限,因为Beacon扫描逻辑可能涉及位置信息。如果是BLE方案,APP使用BluetoothLeScanner扫描,连接后用BluetoothGatt读写特征值,权限也是BLUETOOTH_SCAN和BLUETOOTH_CONNECT,但不需要定位权限。下面这段是Android 12+环境下的权限声明清单:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />如果需要兼容Android 11及以下,还要保留BLUETOOTH和BLUETOOTH_ADMIN。动态权限请求通常在Activity的onCreate里统一处理,不要放在扫描回调里,否则容易出现重复弹窗。
5.2 最小BLE扫描绑定代码(Kotlin/Java)
BLE模块在上电后会广播一个设备名,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时传入ScanSettings,SCAN_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): Pair<Float, 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帧给STM32,STM32在串口接收中断里解析即可。
5.4 连接不上时的排查路径:HC-05配对失败和BLE连接失败
蓝牙连接不上是所有蓝牙项目中最高频的错误。HC-05连接不上,最常见的原因是模块没有进入AT模式,而是仍然处于数据模式,手机上搜索到后无法配对。另一种情况是模块被其他手机连接过,进入AT模式后用AT+RESET恢复默认状态。还有一点是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, timeout=1) 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波特率,虽然数据速率低,但抗干扰能力强,适合大棚这种电磁环境复杂的场景。
本文还有配套的精品资源,点击获取