基于GD32E230的AHT10温湿度传感器I2C驱动移植与数据采集实战
基于GD32E230的AHT10温湿度传感器I2C驱动移植与数据采集实战最近在做一个环境监测的小项目需要用到温湿度传感器。选来选去发现AHT10这个小家伙性价比很高精度也够用关键是它采用标准的I2C接口和咱们常用的单片机都能方便地连接。正好手头有一块立创的GD32E230C8T6开发板今天就带大家一步步把AHT10的驱动移植上去实现温湿度的实时采集。这篇文章特别适合刚开始接触嵌入式开发的朋友或者需要在GD32E230项目中使用AHT10的工程师。我会从最基础的I2C通信原理讲起手把手教你如何根据数据手册编写驱动最后把采集到的温湿度数据通过串口打印出来。跟着做一遍你就能掌握传感器驱动移植的完整流程。1. 认识我们的“新朋友”AHT10温湿度传感器在开始写代码之前咱们先来了解一下AHT10这个小模块。知道它是什么、能干什么、有什么特点后面的驱动编写才能心里有数。AHT10是一款数字输出的温湿度传感器它把测量芯片、信号处理电路都集成在一个小小的封装里。你拿到手的模块通常长这样一个比指甲盖还小的PCB板上面焊着传感器芯片和几个必要的电阻电容引出三个引脚VCC、GND、SDA、SCL其中SDA和SCL是I2C通信线。它的核心特点有几个数字输出直接可用传感器内部已经把模拟信号转换成了数字信号通过I2C接口直接输出数字值省去了我们做模数转换的麻烦。精度不错湿度测量精度在±2%RH左右温度精度在±0.3℃。对于大多数环境监测应用比如室内温湿度显示、仓库环境监控这个精度完全够用。超低功耗工作电流在微安级别特别适合电池供电的便携设备。标准I2C接口这是重点I2C是一种只需要两根线数据线SDA和时钟线SCL就能通信的协议可以挂载多个设备非常节省单片机的IO口。注意AHT10的工作电压范围是1.8V~3.6V咱们的GD32E230开发板通常是3.3V供电正好匹配直接连接就行不需要电平转换。2. I2C通信基础单片机如何与传感器“对话”既然AHT10用的是I2C接口那咱们就得先搞明白I2C是怎么工作的。你可以把I2C想象成两个人打电话SCL时钟线就像打电话时的节奏控制一方说“现在可以说话了”另一方才能说。SCL由主机我们的单片机产生控制着数据传输的节奏。SDA数据线就是实际说话的内容温湿度数据、控制命令都通过这根线传输。一次完整的I2C通信包含几个基本动作起始信号就像拿起电话说“喂我要开始说话了”。在SCL为高电平时SDA从高变低。停止信号就像说“好了我说完了挂电话”。在SCL为高电平时SDA从低变高。数据传输每个时钟脉冲SCL的一次高低变化传输1位数据。数据在SCL为低电平时可以变化在SCL为高电平时必须保持稳定。应答信号每收到8位数据1个字节接收方要在第9个时钟脉冲期间把SDA拉低表示“我收到了”。AHT10作为从机设备有一个固定的“电话号码”——器件地址。从数据手册我们知道AHT10的器件地址是0x38。但在I2C协议中实际发送的地址字节还要包含读写方向位最低位0表示写1表示读所以通常我们会把地址左移一位。在代码里我们用的是0x38 1也就是0x70。3. 动手写驱动软件模拟I2C时序GD32E230虽然有硬件I2C外设但很多初学者一开始用起来会遇到各种问题。所以咱们今天用更直观的方法软件模拟I2C。就是用普通的GPIO引脚通过程序控制电平变化来模拟出I2C的时序。这样虽然速度不如硬件I2C快但理解起来更透彻调试也方便。3.1 硬件连接与引脚初始化首先把AHT10模块和开发板连起来VCC → 3.3VGND → GNDSDA → PA2你可以选其他IO但代码要对应修改SCL → PA1在bsp_aht10.h头文件里我们要定义这些引脚//端口移植 #define RCU_SDA RCU_GPIOA #define PORT_SDA GPIOA #define GPIO_SDA GPIO_PIN_2 #define RCU_SCL RCU_GPIOA #define PORT_SCL GPIOA #define GPIO_SCL GPIO_PIN_1这里我选择了PA1和PA2你可以根据实际接线修改。接着是一些宏定义方便后面操作引脚//设置SDA输出模式 #define SDA_OUT() gpio_mode_set(PORT_SDA,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_SDA) //设置SDA输入模式 #define SDA_IN() gpio_mode_set(PORT_SDA,GPIO_MODE_INPUT,GPIO_PUPD_PULLUP,GPIO_SDA) //获取SDA引脚的电平变化 #define SDA_GET() gpio_input_bit_get(PORT_SDA,GPIO_SDA) //SDA与SCL输出 #define SDA(x) gpio_bit_write(PORT_SDA,GPIO_SDA, (x?SET:RESET)) #define SCL(x) gpio_bit_write(PORT_SCL,GPIO_SCL, (x?SET:RESET))初始化函数很简单就是配置这两个引脚为输出模式并且使能时钟void AHT10_GPIO_Init(void) { /* 使能时钟 */ rcu_periph_clock_enable(RCU_SCL); rcu_periph_clock_enable(RCU_SDA); /* 配置SCL为输出模式 */ gpio_mode_set(PORT_SCL,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_SCL); /* 配置为开漏输出 50MHZ */ gpio_output_options_set(PORT_SCL,GPIO_OTYPE_OD,GPIO_OSPEED_50MHZ,GPIO_SCL); /* 配置SDA为输出模式 */ gpio_mode_set(PORT_SDA,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_SDA); /* 配置为开漏输出 50MHZ */ gpio_output_options_set(PORT_SDA,GPIO_OTYPE_OD,GPIO_OSPEED_50MHZ,GPIO_SDA); }提示注意这里配置的是开漏输出GPIO_OTYPE_OD不是推挽输出。I2C总线需要开漏输出配合上拉电阻才能实现“线与”功能多个设备可以同时挂在总线上。3.2 编写I2C基础时序函数现在来写最核心的几个I2C时序函数。这些函数就像积木块后面组合起来就能完成完整的通信。起始信号函数void IIC_Start(void) { SDA_OUT(); // 确保SDA是输出模式 SDA(1); // SDA先拉高 delay_us(5); SCL(1); // SCL再拉高 delay_us(5); SDA(0); // 在SCL为高时SDA从高变低这就是起始信号 delay_us(5); SCL(0); // 拉低SCL为后续数据传输做准备 delay_us(5); }停止信号函数void IIC_Stop(void) { SDA_OUT(); SCL(0); // 先确保SCL是低的 SDA(0); // SDA拉低 SCL(1); // SCL拉高 delay_us(5); SDA(1); // 在SCL为高时SDA从低变高这就是停止信号 delay_us(5); }发送一个字节函数void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for(i 0; i 8; i) { // 先发送最高位I2C协议规定先传高位 SDA((dat 0x80) 7); // 取出最高位 delay_us(2); SCL(1); // 拉高时钟数据被采样 delay_us(5); SCL(0); // 拉低时钟准备下一位 delay_us(5); dat 1; // 左移准备发送下一位 } }读取一个字节函数unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // 设置为输入模式读取从机数据 for(i 0; i 8; i) { SCL(0); delay_us(5); SCL(1); // 拉高时钟从机放置数据到SDA delay_us(5); receive 1; // 左移为接收新数据腾出位置 if(SDA_GET()) // 读取SDA电平 { receive | 1; // 如果为高最低位置1 } delay_us(5); } SCL(0); return receive; }等待应答函数 这个函数特别重要用来检查从机是否收到了我们发送的数据。unsigned char I2C_WaitAck(void) { char ack 0; unsigned char ack_flag 10; SCL(0); SDA(1); SDA_IN(); // 切换为输入检测从机的应答 delay_us(5); SCL(1); delay_us(5); // 等待SDA被从机拉低应答信号 while((SDA_GET() 1) (ack_flag)) { ack_flag--; delay_us(5); } if(ack_flag 0) // 超时没有收到应答 { IIC_Stop(); return 1; // 返回1表示失败 } else { SCL(0); SDA_OUT(); // 切换回输出模式 } return ack; // 返回0表示成功 }4. AHT10数据采集全流程有了上面的基础函数现在可以按照AHT10数据手册的流程来读取温湿度了。整个过程就像和传感器进行一次完整的对话4.1 启动测量首先我们要告诉AHT10“开始测量吧”这个过程需要发送特定的命令。IIC_Start(); // 1. 先说喂我要开始说话了 Send_Byte(0x38 1 | 0); // 2. 拨通AHT10的电话地址写命令 I2C_WaitAck(); // 3. 等对方说喂请讲 Send_Byte(0xAC); // 4. 说开始测量触发测量命令 I2C_WaitAck(); // 5. 等对方确认 Send_Byte(0x33); // 6. 这是命令参数的一部分 I2C_WaitAck(); // 7. 等确认 Send_Byte(0x00); // 8. 命令参数的剩余部分 I2C_WaitAck(); // 9. 等确认 IIC_Stop(); // 10. 说好了先这样等会儿再联系发送完启动命令后AHT10需要一点时间来测量。数据手册说最多需要80ms所以我们要等一会儿。4.2 读取测量结果等传感器测量完成后我们再次联系它读取数据char timeout 0; unsigned char buff[6] {0}; // 用来存放读取到的6个字节数据 // 等待传感器准备好最多等5次 do { delay_1ms(1); // 每次等1ms timeout; IIC_Start(); // 重新开始通信 Send_Byte(0x38 1 | 1); // 这次是读命令最后一位是1 } while(I2C_WaitAck() 1 timeout 5); // 如果没应答重试 // 开始读取6个字节的数据 buff[0] Read_Byte(); // 第1字节状态字 IIC_Send_Ack(0); // 发送应答告诉传感器继续发 buff[1] Read_Byte(); // 第2字节湿度高8位 IIC_Send_Ack(0); buff[2] Read_Byte(); // 第3字节湿度低8位 IIC_Send_Ack(0); buff[3] Read_Byte(); // 第4字节湿度最低4位 温度高4位 IIC_Send_Ack(0); buff[4] Read_Byte(); // 第5字节温度中间8位 IIC_Send_Ack(0); buff[5] Read_Byte(); // 第6字节温度低8位 IIC_Send_Ack(1); // 最后一个字节发送非应答表示发完了 IIC_Stop(); // 结束通信4.3 数据解析与换算读回来的原始数据是20位的湿度值和20位的温度值分散在6个字节里。我们需要把它们拼起来然后按照公式换算成实际的温湿度值。湿度数据拼接 湿度数据占据第1、2、3字节的部分位。具体来说buff[1]湿度高8位buff[2]湿度低8位buff[3]的高4位湿度最低4位unsigned int dat 0; // 把三个部分拼成一个20位的数 dat (((buff[1] 12) | (buff[2] 4)) | (buff[3] 4)); // 换算公式湿度 (原始值 / 2^20) * 100% Humidity dat / 1048576.0 * 100.0; // 1048576就是2的20次方温度数据拼接 温度数据占据第3、4、5字节的部分位buff[3]的低4位温度高4位buff[4]温度中间8位buff[5]温度低8位dat 0; // 注意buff[3]的低4位是温度的高4位 dat ((buff[3] 0x0F) 16) | (buff[4] 8) | buff[5]; // 换算公式温度 (原始值 / 2^20) * 200 - 50 Temperature (dat / 1048576.0) * 200 - 50;注意这里有个细节要注意。buff[3]这个字节很特殊它的高4位是湿度的最低4位低4位是温度的最高4位。所以在拆分的时候要用buff[3] 4取高4位给湿度用buff[3] 0x0F取低4位给温度。5. 移植到你的工程并测试5.1 文件准备与工程配置在你的工程目录下新建一个文件夹比如AHT10把写好的bsp_aht10.c和bsp_aht10.h放进去。在IDE比如Keil中添加这两个文件到工程。确保包含了必要的头文件路径。根据你的实际硬件连接修改bsp_aht10.h中的引脚定义。如果你用的不是PA1和PA2记得改成你实际连接的引脚。5.2 主函数编写现在来写主函数测试一下我们的驱动是否工作正常#include gd32e23x.h #include systick.h #include stdio.h #include bsp_usart.h #include bsp_aht10.h int main(void) { systick_config(); // 滴答定时器初始化用于延时函数 usart_gpio_config(9600U); // 串口初始化用于打印数据到电脑 AHT10_GPIO_Init(); // AHT10引脚初始化 printf(AHT10温湿度传感器测试开始\r\n); while(1) { // 采集温湿度数据 AHT10_Read(); // 通过串口打印温度数据 printf(温度: %.2f℃\r\n, Get_Temperature()); // 通过串口打印湿度数据 printf(湿度: %.2f%%RH\r\n, Get_Humidity()); // 每隔1秒采集一次 delay_1ms(1000); } }5.3 常见问题与调试技巧在实际移植过程中你可能会遇到一些问题。这里分享几个我踩过的坑问题1读取的数据全是0xFF或0x00检查硬件连接VCC、GND是否接对SDA、SCL是否接反检查上拉电阻I2C总线需要上拉电阻通常4.7kΩ模块上一般已经集成如果没有需要外接。用逻辑分析仪或示波器看波形这是最直接的调试方法可以看到起始信号、地址、数据位的实际波形。问题2能读到数据但数值明显不对检查时序延时delay_us()函数的延时时间是否准确不同主频下需要调整。检查数据拼接特别是buff[3]这个字节的拆分是否正确。检查换算公式1048576.0不要写成整数否则除法会取整。问题3偶尔读取失败增加重试机制就像示例代码中的do-while循环给传感器足够的响应时间。检查电源稳定性传感器供电不稳会影响测量。降低I2C速度软件模拟I2C时适当增加delay_us()的延时时间。6. 进阶思考如何优化与扩展基本的驱动跑通后你可以考虑下面这些优化添加CRC校验AHT10的数据包含CRC校验位可以增加校验代码确保数据正确性。低功耗优化AHT10支持单次测量模式测量完成后自动进入休眠适合电池供电应用。多传感器支持I2C总线可以挂多个设备你可以尝试同时接多个AHT10注意地址冲突问题。移植到硬件I2C等软件模拟调通后可以尝试改用GD32E230的硬件I2C外设速度更快且不占用CPU时间。好了到这里整个AHT10的驱动移植就完成了。从最基础的I2C时序到完整的数据采集每一步我都尽量讲清楚原理和细节。实际项目中你可能还需要添加错误处理、数据滤波等功能但基本的框架就是这样。最后提醒一点嵌入式开发最重要的是动手实践。不要只看代码一定要亲手连上线、下载程序、观察现象。遇到问题就查数据手册、用示波器看波形这样进步最快。