ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

51单片机驱动AM2302温湿度采集与蓝牙传输实战

51单片机驱动AM2302温湿度采集与蓝牙传输实战 简介这套基于51单片机的温湿度监测设计资料面向嵌入式初学者与电子设计竞赛备赛者以AM2302数字温湿度传感器采集环境数据经LCD1602实时显示并借助蓝牙模块实现无线远程查看适合作为物联网入门、单片机课程设计或毕业设计的参考方案。压缩包共17个文件包含C语言源程序、Keil工程文件、仿真辅助文件及hex烧录文件等其中uvproj和uvopt用于工程配置a51、c与lst对应启动代码和源码清单hex可直接烧录整体约3.12MB结构紧凑便于快速上手。目前已有927人学习下载资料内附源程序与仿真文件可帮助读者在无实物环境下验证逻辑、理解传感器时序、LCD1602驱动方式及蓝牙串口通信流程特别适合希望系统梳理单片机外设集成与调试思路的学习者。1. 硬件架构里的三块基石51单片机、AM2302与蓝牙的职责边界在STC89C52最小系统板上接一颗AM2302温湿度传感器数据同时推到LCD1602和HC-05蓝牙模块这是很多51单片机课程设计里“口袋气象站”的常见骨架。AM2302用单总线把温度和湿度打包在40位数据里51单片机用GPIO模拟时序把它“抠”出来1602负责本地显示而蓝牙模块负责把同一份数据透传给手机或PC。这三个模块都工作在5V/3.3V混合电平下接口类型完全不同因此工程里“源程序”和“仿真文件”的价值不在于点亮屏幕而在于把单总线读取、LCD初始化、串口发送和仿真调试这几套流程写到能复现的程度。读懂这个组合之后换成DHT11或加一块Wi-Fi模块也只是改数据解析的问题。2. 从AM2302数据手册到51单片机GPIO读取单总线时序与校验实现2.1 引脚定义、40位数据帧和与DHT11的差异AM2302也被称为DHT21引脚顺序如果买的是4针模块通常是VCC、SDA、GND、NCSDA即数据线。数据手册给出的供电范围是3.3~5.5V所以直接接51单片机开发板的5V是可行的。芯片内部已经做了校准校准系数存在OTP内存里读出来就是真实温湿度不需要再计算线性化。每次完整输出是40位16位湿度、16位温度、8位校验。湿度数据的二进制值除以10是带一位小数的相对湿度温度数据的最高位是符号位按16位补码解析后再除以10。校验字节是前四个字节相加后取低8位接收端必须判断这个值是否一致否则丢弃一帧。对比项AM2302DHT11湿度精度±2%RH±5%RH温度精度±0.5℃±2℃湿度分辨率0.1%RH1%RH温度分辨率0.1℃1℃采样周期约2s约1s单总线时序形式50us低电平高电平宽度判位50us低电平高电平宽度判位很多教程直接让用户把DHT11代码改个名字上传结果读取数据偶尔乱跳原因是DHT11库里的延时和采样周期参数是按1秒调的而AM2302上电后需要更长的空闲时间读完一帧立即再次开始信号大概率失败。要注意的还有模块上是否带4.7kΩ上拉电阻如果你的模块是焊好的“蓝头”一般都有如果是裸传感器就必须在SDA和VCC之间外接4.7kΩ电阻。2.2 51单片机模拟单总线读取的完整函数51没有硬件单总线控制器只能用GPIO模拟时序。下面的代码在STC89C52、晶振11.0592MHz下验证过普通AT89C52也能用但要删去STC特有的寄存器设置。读位的思路是每个位的低电平时长固定约50μs真正包含信息的是高电平的宽度高电平持续约26~28μs判为0约70μs判为1。因此我用两个while循环和超时计数器来测量高电平宽度。#include reg52.h #include intrins.h sbit AM2302_PIN P2^0; void delay_us(unsigned int us) { while (us--) { _nop_(); } } unsigned char AM2302_ReadBit(void) { unsigned char cnt 0; while (!AM2302_PIN) { // 等待当前位的50us低电平结束 if (cnt 100) return 0xFF; // 超时线上一直低 } cnt 0; while (AM2302_PIN) { // 高电平计时 delay_us(1); if (cnt 80) return 0xFF; // 高电平超时可能遇到末位或错误 } return (cnt 35) ? 1 : 0; // 高电平宽于35us判为1否则0 } unsigned char AM2302_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { unsigned char bit AM2302_ReadBit(); if (bit 0xFF) return 0xFF; dat (dat 1) | bit; } return dat; }这里的cnt每次循环加一配合delay_us(1)近似每步1μs。35us的阈值是按“0”高电平28us和“1”高电平70us的中间值取的即使晶振频率略有偏差35us依然有比较大的余量。若把阈值调得太小容易把0误判成1调得太大又会丢失连续bit边界。开始信号和5字节读取逻辑如下。读时序期间必须关中断因为中断处理会占用几十微秒一旦在50μs低电平期间进入中断单片机无法及时采样后续位全部错位。关中断的时间也就几毫秒对主循环影响很小。bit AM2302_ReadData(unsigned char *humi_hi, unsigned char *humi_lo, unsigned char *temp_hi, unsigned char *temp_lo, unsigned char *checksum) { unsigned char buf[5]; unsigned char i; EA 0; // 关总中断 AM2302_PIN 1; delay_us(5); AM2302_PIN 0; delay_us(1200); // 开始信号拉低1.2ms务必超过1ms AM2302_PIN 1; delay_us(30); // 释放总线等待传感器应答 if (AM2302_PIN) { // 没拉低表示传感器未响应 EA 1; return 0; } while (!AM2302_PIN); // 应答低电平结束 while (AM2302_PIN); // 应答后的80us高电平结束 for (i 0; i 5; i) buf[i] AM2302_ReadByte(); EA 1; // 重新开启中断 if (((buf[0] buf[1] buf[2] buf[3]) 0xFF) ! buf[4]) return 0; // 校验失败 *humi_hi buf[0]; *humi_lo buf[1]; *temp_hi buf[2]; *temp_lo buf[3]; *checksum buf[4]; return 1; }delay_us(1200)是刻意比手册要求更长的一段确保不同模块都能正确识别开始信号。如果改成500μs部分批次传感器确实不响应这与模块内部RC时间常数有关。等待应答的while循环里没有超时保护实际项目里应该加上最大循环次数否则传感器拔掉时程序会卡死在这行。读取成功后在main函数里用unsigned int humi (humi_hi 8) | humi_lo; int temp (temp_hi 8) | temp_lo;就能得到可直接运算的数值temp是补码负数会自然落到int的负区间。2.3 读取失败先查供电和采样周期常见现象是刚上电能读一次之后一直返回0。最大嫌疑是主循环里读得太频繁。AM2302要求两次读取间隔至少2秒如果while(1)里不加延时传感器还在恢复期开始信号自然无效。正确做法是用定时器计数达到2000ms再去采样。另一个高频问题是SDA引脚接触不良AM2302的开漏输出如果没有外部上拉高电平会被引脚寄生电容拖慢导致高电平宽度变宽误判成“1”。这时用示波器看波形会发现上升沿很缓换成4.7kΩ上拉即可。如果显示数值里湿度正常温度不变先看温度符号位在51上读回来的补码temp不能当作unsigned int直接显示要先转成int再除以10。3. 1602显示与数据格式化LCD驱动、小数位处理与刷新策略3.1 1602并行接口的连接与初始化顺序1602模块的接口除了电源最关键的是RS、RW、E和8根数据线DB0~DB7。在设计实物接线时我一般把RS接P2.6RW接P2.5E接P2.7DB0~DB7接P0口。P0口内部没有上拉要外接一个10kΩ排阻到VCC否则LCD读到的数据全是低电平屏幕只会显示一块黑条。1602引脚连接目标VSSGNDVDD5VVO10k电位器抽头RSP2.6RWP2.5EP2.7DB0~DB7P0.0~P0.7初始化代码顺序有讲究先延时40ms让LCD内部复位完成然后写0x388位接口、2行显示、5×7点阵再延时5ms写一次接下来0x08关显示、0x01清屏、0x06光标自增、0x0C开显示。整个顺序不能颠倒0x01清屏要稍等一会儿否则后续指令容易被忽略。#define LCD_DATA P0 sbit LCD_RS P2^6; sbit LCD_RW P2^5; sbit LCD_E P2^7; void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_E 1; _nop_(); LCD_E 0; LCD_Delay(2); } void LCD_WriteData(char dat) { LCD_RS 1; LCD_RW 0; LCD_DATA dat; LCD_E 1; _nop_(); LCD_E 0; LCD_Delay(2); } void LCD_Init(void) { LCD_Delay(40000); LCD_WriteCmd(0x38); LCD_Delay(5000); LCD_WriteCmd(0x38); LCD_Delay(5000); LCD_WriteCmd(0x08); LCD_WriteCmd(0x01); LCD_Delay(2000); LCD_WriteCmd(0x06); LCD_WriteCmd(0x0C); }注意LCD_WriteCmd和LCD_WriteData之间必须要有一定的延时至少几十微秒因为LCD本身处理一条指令也需要时间。如果写之后立刻读BF标志也可以但读标志要多接一根RW还要把P0口切到输入模式代码复杂度略增。对于课程设计级别延时法足够稳定。3.2 把AM2302原始数据转为“xx.x”格式并定位显示读取回来的湿度是两个字节组合成16位无符号整数除以10得到整数部分取余得到小数部分。温度也一样但可能为负数。下面给出一个不依赖stdio.h的格式化函数因为51的printf重定向到串口后很容易和蓝牙发送冲突而且占用大量代码空间。void LCD_ShowTemp(int temp) { unsigned char integer, decimal; if (temp 0) { LCD_WriteData(-); temp -temp; } integer temp / 10; decimal temp % 10; LCD_WriteData(0 integer / 10); LCD_WriteData(0 integer % 10); LCD_WriteData(.); LCD_WriteData(0 decimal); LCD_WriteData(C); }0 integer / 10实际上是先把整数的十位分离出来再加0x30变成ASCII字符。温度如果超过99.9℃只显示三位整数AM2302量程上限80℃这里够用。湿度函数同理只是不需要符号判断。1602的显示位置由地址决定第一行第一列是0x80第二行第一列是0xC0。先写LCD_WriteCmd(0x80 col)定位再写数据。一个容易踩的坑是“字符串拼接”。在51上用strcat拼接动态内容可能因为目标RAM不够导致程序跑飞。我习惯的做法是预先写好每行的前缀比如第一行固定“Temp: ”“Humi: ”然后把数字逐位写入固定的列清屏只清数字区域。3.3 刷新策略与对比度调节显示刷新不要用LCD_Clear()这条指令会把整个DDRAM清零并让光标归零每次刷新都会看到屏幕闪一下。更好的办法是维护两个变量保存上次显示的温度和湿度只有数值变化时才把新的ASCII写入对应位置。由于AM2302每2秒才能采一次显示更新频率设定在1~2Hz就够。频繁刷新还有一个副作用1602的总线时序占用P2口会干扰按键扫描虽然这里没有按键但在复杂项目里要留意共用的GPIO时间片。对比度调节是实物调试里最容易被忽略的。Vo引脚接一个10k电位器电位器两端接VCC和GND中间抽头接Vo。屏幕底色全黑说明对比度太低把Vo电压往0.5V方向调屏幕空白无显示则可能初始化代码没跑进去。如果你用的是带转接板的绿膜1602转接板上有I2C接口那就不能用上面的P0并口代码需要改用I2C的LCD驱动时序完全不同。源程序里一定要注明使用的是哪种版本。4. 蓝牙透传与串口中断HC-05配置、数据帧设计和连接失败排查4.1 HC-05的AT命令配置与电平匹配项目资料里的蓝牙模块最常见的是HC-05它支持SPP协议完全兼容HC-06引脚。第一次用之前必须设置串口波特率和模块名。把模块的KEY引脚拉高再上电指示灯进入慢闪用USB转TTL接到电脑交叉连接RX/TX打开任意串口助手发送AT应该能返回OK。常用命令是ATNAMEAM2302_NODE修改蓝牙名称ATUART9600,0,0把通信波特率从默认的38400改成9600ATROLE1设置角色ATPSWD1234改配对密码。设置完波特率后模块内部会保存参数但USB转TTL的上位机也要同步改成9600才能继续操作。51单片机侧的电路需要处理电平差。HC-05的RX引脚耐压不是5V如果直接把51的TXD接过去长期使用有烧毁风险。我一般用两个电阻分压R1取1kΩ串联在51的TXD和模块RX之间R2取1.5kΩ对地接在模块RX端这样高电平被分到约3V既高于3.3V逻辑高电平阈值又不会超过模块耐压。模块的TX输出3.3V接到51的RXD会被识别为高电平可以直连但为了保险也可以加一个1kΩ限流电阻。4.2 51单片机串口初始化与数据帧发送串口使用定时器1产生波特率晶振11.0592MHz下9600波特率误差为零。初始化代码void UART_Init() { TMOD 0x0F; TMOD | 0x20; // 定时器18位自动重装 TH1 0xFD; // 9600波特率重装值 TL1 0xFD; PCON 0x00; // 不倍频 SCON 0x50; // 模式18位UART使能接收 ES 1; TR1 1; EA 1; }为了调试方便我往往用查询方式发送单字节只在接收时开中断。发送温湿度需要把前面AM2302读到的原始数据按一定帧格式组装。一个比较简洁的帧定义是帧头0xAA、湿度高字节、湿度低字节、温度高字节、温度低字节、校验字节、帧尾0x55。校验字节取前5个字节异或接收端用同样计算判断是否有效。void Send_SensorFrame(unsigned int humi, int temp) { unsigned char frame[7]; unsigned char xor 0; frame[0] 0xAA; frame[1] humi 8; frame[2] humi 0xFF; frame[3] (unsigned char)(temp 8); frame[4] (unsigned char)(temp 0xFF); xor frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4]; frame[5] xor; frame[6] 0x55; for (unsigned char i 0; i 7; i) { SBUF frame[i]; while (!TI); TI 0; } }注意humi是unsigned int而温度用int高位字节转成unsigned char后发送的其实是补码。手机端如果需要显示负温度必须先把接收到的两个字节拼成short再做无符号转有符号不能直接按无符号解析否则零下温度会显示成一个大数。帧头帧尾可以防止粘包但如果发送频率较低串口助手一般不会丢包。真正的丢包发生在蓝牙无线传输过程中尤其是模块距离超过5米时会连续丢字节所以校验字节是必要的。4.3 HC-05蓝牙模块连接不上的5个排查点配网过程中最常见到的现象是手机能搜到模块却连不上或者连上后串口没数据。结合开发板调试经验可以优先排查以下位置现象可能原因处理办法手机搜索不到模块模块仍处于AT模式KEY被拉高断开KEY引脚重新上电能搜索但不能配对模块已绑定旧设备用ATRESET清空绑定重新配对配对成功但单片机收不到波特率不一致确认模块ATUART9600且单片机初始化波特率也是9600收发数据全是乱码电平不匹配或地线没共地检查TX/RX交叉连接单片机地必须连蓝牙模块地数据发送一帧后程序卡死发送查询与串口中断冲突查询TI前暂时关闭ES中断或改用中断发送需要特别提醒的是很多人把HC-05模块和CH340 USB转TTL同时接在电脑上电脑上只有一个COM口模块没有独立供电导致AT命令没响应。正确的做法是模块用3.3V LDO供电USB转TTL只负责通信二者共地。另外模块名称里不要有中文和空格旧固件对AT指令大小写敏感统一用大写。5. Proteus仿真验证整套设计源文件组织、虚拟终端与AM2302模型处理5.1 Proteus中找不到AM2302时怎么选单片机外设Proteus按AM2302或DHT21都搜不到官方模型最稳妥的替代品是元件名DHT11搜索“DHT11”即可。DHT11在Proteus里的模型同样采用单总线支持主机开始信号、应答和40位数据返回。AM2302和DHT11在时序的宏观结构上是一致的差异只在判位阈值和采样周期用DHT11模型仿真验证51代码的读位、校验、显示和串口发送逻辑完全够用。若连DHT11也没有可以用两个电位器加ADC实现模拟输入但那样就丢了单总线时序价值不做推荐。单片机型号方面Proteus自带AT89C51和AT89C52没有STC89C52RC。你可以在原理图里直接用AT89C52程序里如果出现了STC特有的定义就会编译失败所以源程序要尽量写成标准C51避免依赖STC寄存器。晶振频率务必设置为11.0592MHz和Keil工程中一致否则仿真中波特率会和代码预期不一样。5.2 用虚拟终端和COMPIM替代蓝牙通道Proteus仿真并不能真实模拟无线蓝牙协议栈但可以用两个方法验证串口数据。方法一是在原理图里放一个VIRTUAL TERMINAL把它的RXD接单片机TXDTXD接单片机RXD波特率设置为9600然后运行仿真就能看到单片机发出来的十六进制帧。虚拟终端适合看原始数据但不能模拟手机端回包。方法二是用COMPIM组件生成一对带名字的虚拟串口COMPIM的B接线端连接单片机PC端用串口助手打开另一个串口。这种方法能真实验证数据帧的校验算法因为串口助手收到的是和蓝牙透传一样的内容。无论哪种方法都不能证明HC-05能够正常配对只能证明单片机送出的串口数据是符合帧格式的。为了在同一套源码里区分仿真和实物可以在main.c顶部加一个宏/* main.c 中做仿真/实物切换 */ #define PROTEUS_SIM 1 #if PROTEUS_SIM /* 仿真时使用DHT11模型采样周期可以缩短到1s */ #else /* 实物AM2302必须保持2s采样间隔 */ #define SAMPLE_PERIOD_MS 2000 #endif这样编译仿真版本和实物版本时只需要改一行宏不需要动AM2302读取函数。仿真模型返回的数据时序和真实AM2302略有出入只要读到的湿度在20%~80%之间、温度在25℃左右就说明单总线读取逻辑已经跑通。5.3 仿真文件的调试技巧与最小工程清单交付的仿真文件里除了原理图建议在工程目录下保留Keil的src目录和output目录因为Proteus加载的hex文件路径如果变了就会找不到。在Proteus中双击AT89C52在ProgramFile指向hex路径。常见问题是明明重新编译了Keil但仿真还是旧程序原因是hex文件没有生成到Proteus指定的目录或者生成后没刷新。在仿真过程中观察AM2302时序需要用到Proteus的“调试”菜单里的逻辑分析仪把SDA引脚添加进去。如果读出来的高电平宽度始终偏大多半是因为仿真模型对51的引脚时序计算方式与真实器件不同不用过分纠结绝对宽度只要判位阈值正确即可。仿真中遇到“Program file not found”是路径问题“Simulation is not running”则要检查是否放置了电源VCC和GND网络。最后仿真不能代替实物调试即便仿真完美上电前还是要用万用表量一遍模块供电引脚电压。6. 把项目做扎实校验重传、掉电存储与采样节流6.1 给蓝牙数据加上超时重传机制前面用异或校验只能发现错误不能纠错。如果手机端要更可靠可以预留一个ACK字节单片机发送一帧后等待接收端回复0xAA 0x00 0x55收到后再发下一帧等待超过100ms就重发当前帧连续重发3次仍失败则丢弃。这个机制不需要引入复杂协议用两个全局变量就能完成。注意若用这个方式单片机串口中断里就要增加对0xAA开头的ACK判断与AM2302读时序的中断禁用要严格分开不能在单总线读取期间处理串口中断。6.2 定时器驱动的2秒采样节流把温湿度读取放进一个受定时器驱动的节拍里比纯while循环稳定得多。典型写法是定时器0每10ms溢出一次中断里对ms_cnt加1主循环判断ms_cnt 200时执行一次采集。这样AM2302有充足的恢复时间蓝牙发送也变成有节奏的周期发送。定时器初值以11.0592MHz、12T模式为例10ms需要计数约9216次装入TH0/TL0的十六进制值是0xDC和0x00。不同晶振下这个初值要重新计算如果用1T模式则要特别注意因为1T模式下同样计数周期快了12倍初值完全不一样。6.3 把校准值写进STC内部EEPROMAM2302在长期运行后会有些漂移最常见的是湿度偏高或偏低。如果项目中要求绝对精度可以在开机时检测到一个校准按键读取当前传感器值和标准表的差值写入STC89C52内部EEPROM的某个地址。运行中再读一次传感器如果EEPROM中的校准值有效就把差值加上再显示/发送。STC的内部EEPROM不存在地址总线读一个字节用ISP命令写一个字节前要先擦除扇区但代码开销不大。这个功能对普通课程设计不是必须的但放在“设计资料”里会让整个工程看起来更像一个完整产品原型。本文还有配套的精品资源点击获取
返回列表