MLX90614红外测温传感器在天空星GD32F407上的SMBus驱动移植与温度读取实战最近在做一个智能家居项目需要非接触测温功能MLX90614这款传感器进入了我的视线。它体积小、精度高而且不用接触物体就能测温度非常适合用在一些需要卫生安全或者测量运动物体的场景。不过它的通信协议是SMBus和常见的I2C很像但又不完全一样很多朋友在移植驱动时容易卡在时序和数据解析上。今天我就以立创的天空星GD32F407开发板为例手把手带大家完成MLX90614的驱动移植并成功读取物体和环境温度。整个过程我会把原理讲清楚把代码掰开揉碎确保你跟着做一遍就能用起来。1. 认识我们的主角MLX90614传感器在动手写代码之前咱们先花几分钟了解一下MLX90614到底是何方神圣以及它怎么工作的。知其然更要知其所以然理解了原理调试起来心里才有底。MLX90614是一个红外测温传感器。它的原理是检测物体发出的红外辐射能量然后根据能量大小换算出物体的温度。这和我们平时用的水银温度计或者DS18B20那种接触式测温完全不同。接触式测温需要传感器和被测物体达到热平衡反应慢还容易受环境影响。而红外测温是“非接触”的速度快也不怕高温物体用在比如耳温枪、工业设备测温这些场合非常合适。这个传感器内部已经帮我们做好了校准和线性化处理我们拿到手直接用就行非常方便。它会把测到的**物体温度To和传感器周围的环境温度Ta**都计算好存起来我们通过通信接口去读出来就行。那么怎么和单片机通信呢数据手册里说它是“类IIC”通信官方名字叫SMBus。你可以把它理解为I2C的一个“亲戚”或者“变种”也是两根线时钟线SCL和数据线SDA通信方式很像。对于GD32F407这种没有硬件SMBus接口的单片机我们完全可以用两个普通的GPIO引脚通过软件模拟它的时序来和传感器对话这就是我们常说的“软件模拟”或者“GPIO模拟”。2. 硬件连接与引脚配置硬件连接很简单但有一个关键点必须注意弄错了可能烧芯片。MLX90614的工作电压是4.5V到5.5V而我们的天空星GD32F407开发板的GPIO引脚输出电压是3.3V。如果直接用3.3V去驱动5V的器件电平不匹配通信可能会失败。更稳妥的做法是将连接传感器的两个GPIO引脚PB8和PB9配置为开漏输出Open-Drain模式然后在模块的SDA和SCL线上各自加上一个4.7kΩ的上拉电阻到5V。这样引脚输出低电平时能拉低线路输出高电平时靠外部上拉电阻拉到5V实现了3.3V单片机与5V器件的安全通信。注意务必确保MLX90614模块的VCC接5VGND接开发板GND。SCL和SDA分别接PB9和PB8并且这两个引脚必须配置为开漏模式。在代码里我们专门用一个函数来初始化这两个引脚void MLX90614_GPIO_Init(void) { /* 使能GPIOB的时钟 */ rcu_periph_clock_enable(RCU_GPIOB); /* 配置PB9 (SCL) 为开漏输出模式上拉速度50MHz */ gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_9); gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_9); /* 配置PB8 (SDA) 为开漏输出模式上拉速度50MHz */ gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_8); gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_8); }这里的关键是GPIO_OTYPE_OD它指定了输出类型为开漏。速度设为50MHz是为了让引脚电平翻转更快保证通信时序稳定。3. 理解SMBus通信与数据读取原理要正确读出温度我们必须看懂MLX90614的数据手册。别怕我帮你把核心部分提炼出来了。传感器内部有两种存储器EEPROM存储了出厂校准参数我们一般不用改。RAM存储了实时数据我们关心的温度数据就在这里。我们需要读取的是RAM中的两个地址0x06存放环境温度Ta的原始数据。0x07存放物体温度To的原始数据。读取数据的SMBus命令格式有点特别。命令Command字节由两部分组成高3位BIT7~BIT5表示操作类型。读RAM是000读EEPROM是001以此类推。低5位BIT4~BIT0就是我们要读的RAM地址的低5位。举个例子我们要读环境温度Ta地址0x06。0x06的二进制是0000 0110取低5位是0 0110。因为我们是读RAM操作高3位是000。所以组合起来的命令字节就是0000 0110也就是十六进制的0x06。同理读物体温度To地址0x07命令字节就是0x07。通信过程可以概括为以下几步单片机主机发送起始信号。发送传感器地址默认0x5A加写位0表示要写命令。发送命令字节比如0x06读环境温度。重新发送起始信号。发送传感器地址加读位1表示要读数据。连续读取3个字节温度低8位、温度高8位、PEC校验码。单片机发送停止信号。这里有个细节数据是低字节在前高字节在后。比如读到的两个字节是0xF7和0x3A那么完整的16位数据应该是0x3AF7。读到的原始数据怎么变成我们熟悉的摄氏度呢公式很简单实际温度 (原始数据 × 0.02) - 273.15原始数据0x3AF7转换成十进制是15095。代入公式15095 × 0.02 301.9301.9 - 273.15 28.75℃。这个公式对物体温度To和环境温度Ta都适用。最后提一下PEC它是一个CRC-8校验码用于验证数据传输过程中有没有出错。我们的代码里提供了校验功能可以通过宏定义选择开启或关闭。为了通信可靠建议在正式项目里开启它。4. 手把手编写驱动代码理解了原理咱们就开始写代码。我会把核心函数逐个拆解并解释每一行代码的作用。首先我们需要一些宏定义来方便操作引脚这些写在头文件bsp_mlx90614.h里#ifndef _BSP_MLX90614_H_ #define _BSP_MLX90614_H_ #include gd32f4xx.h // 引脚定义使用PB8和PB9 #define RCU_SDA RCU_GPIOB #define PORT_SDA GPIOB #define GPIO_SDA GPIO_PIN_8 #define RCU_SCL RCU_GPIOB #define PORT_SCL GPIOB #define GPIO_SCL GPIO_PIN_9 // 便捷的引脚操作宏 #define SDA_OUT() gpio_mode_set(PORT_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_SDA) #define SDA_IN() gpio_mode_set(PORT_SDA, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_SDA) #define SDA_GET() gpio_input_bit_get(PORT_SDA, GPIO_SDA) #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 MLX90614_GPIO_Init(void); float MLX90614_Read(unsigned char SlaveAddr, unsigned char RegAddr); #endif接下来是重头戏在bsp_mlx90614.c中实现SMBus的底层时序和读取函数。4.1 基础时序函数这些函数模拟了SMBus的起始、停止、发送应答、等待应答、发送一个字节和接收一个字节的时序。时序图上的每一个高低电平变化和延时都体现在这几段代码里。/* 起始信号SCL高电平时SDA从高变低 */ void IIC_Start(void) { SDA_OUT(); SDA(1); delay_us(5); SCL(1); delay_us(5); SDA(0); delay_us(5); SCL(0); delay_us(5); } /* 停止信号SCL高电平时SDA从低变高 */ void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); SCL(1); delay_us(5); SDA(1); delay_us(5); } /* 发送应答(ack0)或非应答(ack1) */ void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); if(!ack) SDA(0); else SDA(1); delay_us(5); SCL(1); delay_us(5); SCL(0); SDA(1); // 释放SDA线 } /* 等待从机应答超时返回1有应答返回0 */ unsigned char I2C_WaitAck(void) { unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机释放SDA线 SDA_IN(); // 切换SDA为输入准备读取 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; } SCL(0); SDA_OUT(); // 切换回输出模式 return 0; }发送和接收字节的函数是按位操作数据的核心/* 发送一个字节高位先发 */ void Send_Byte(uint8_t dat) { int i; SDA_OUT(); SCL(0); // 拉低时钟开始 for(i 0; i 8; i) { // 取出最高位发送 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); delay_us(5); // 时钟上升沿数据被采样 SCL(0); delay_us(5); dat 1; // 左移准备发送下一位 } } /* 读取一个字节高位先收 */ unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // SDA设为输入读取从机数据 for(i 0; i 8; i) { SCL(0); delay_us(5); SCL(1); delay_us(5); // 时钟上升沿主机读取数据 receive 1; // 左移为新数据位腾出空间 if( SDA_GET() ) { receive | 1; // 如果SDA为高最低位置1 } delay_us(5); } SCL(0); return receive; }4.2 PEC校验函数这个函数用于计算CRC-8校验码多项式是X8X2X11。如果你暂时不想深究CRC算法可以直接把它当做一个黑盒函数使用知道它是用来验证数据正确性的就行。static unsigned char PEC_Calculation(unsigned char *dat, unsigned char len) { unsigned char i; unsigned char crc 0; while(len--) { crc ^ *dat; // 与数据字节异或 for(i 0; i 8; i) { if(crc 0x80) { crc (crc 1) ^ 0x07; // 多项式为0x07 } else { crc (crc 1); } } } return crc; }4.3 核心温度读取函数终于来到最关键的MLX90614_Read函数了。它根据传入的器件地址和寄存器地址完成一次完整的温度读取和换算。#define CRC_VERIFY_ENABLE 1 // 1启用校验0关闭校验 float MLX90614_Read(unsigned char SlaveAddr, unsigned char RegAddr) { unsigned char buff[3] {0}; // 存放读到的3个字节低8位、高8位、PEC unsigned char arr[5] {0}; // 存放用于PEC计算的全部数据帧 uint16_t temp 0; // 合并后的16位原始温度数据 float T 0.0; // 计算后的实际温度值 // 1. 发送起始信号 IIC_Start(); // 2. 发送器件地址写位(0) Send_Byte((SlaveAddr 1) | 0); if(I2C_WaitAck()) return 0; // 等待应答失败则退出 // 3. 发送要读取的寄存器地址命令 Send_Byte(RegAddr); if(I2C_WaitAck()) return 0; // 4. 重新发送起始信号准备读数据 do { delay_ms(1); // 稍作延时等待传感器准备数据 IIC_Start(); // 5. 发送器件地址读位(1) Send_Byte((SlaveAddr 1) | 1); } while(I2C_WaitAck()); // 如果从机忙无应答则重复发送 // 6. 连续读取3个字节 buff[0] Read_Byte(); // 温度数据低8位 IIC_Send_Ack(0); // 主机发送应答要求继续读 buff[1] Read_Byte(); // 温度数据高8位 IIC_Send_Ack(0); // 主机发送应答要求继续读 buff[2] Read_Byte(); // PEC校验码 IIC_Send_Ack(1); // 主机发送非应答表示读取结束 IIC_Stop(); // 发送停止信号 // 使用PEC校验推荐 #if CRC_VERIFY_ENABLE // 构造用于校验的数据帧地址写命令地址读数据低数据高 arr[0] (SlaveAddr 1) | 0; // 器件地址 写 arr[1] RegAddr; // 命令寄存器地址 arr[2] (SlaveAddr 1) | 1; // 器件地址 读 arr[3] buff[0]; // 数据低8位 arr[4] buff[1]; // 数据高8位 // 计算PEC并与读到的校验码比较 if(PEC_Calculation(arr, 5) buff[2]) { // 校验正确合并数据并换算温度 temp (uint16_t)(buff[1] 8) | buff[0]; // 高8位在前低8位在后 T (temp * 0.02) - 273.15; } else { // 校验错误可以通过串口打印错误信息 printf(PEC Error!\r\n); return -999.0; // 返回一个错误值 } // 不使用PEC校验 #else temp (uint16_t)(buff[1] 8) | buff[0]; T (temp * 0.02) - 273.15; #endif return T; }这个函数的设计考虑得很周全。它支持PEC校验通过一个宏定义就能开关。校验能极大提高通信的可靠性特别是在有干扰的环境里。函数最后返回的就是计算好的摄氏温度值。5. 在工程中测试与验证代码都写好了现在来实际测试一下。在你的主函数main.c里可以这样调用#include board.h #include bsp_mlx90614.h #include stdio.h // 用于printf打印 int main(void) { // 开发板初始化时钟、延时等 board_init(); // 串口初始化用于打印结果 bsp_uart_init(); // 初始化MLX90614的GPIO引脚 MLX90614_GPIO_Init(); printf(MLX90614 Test Start...\r\n); while(1) { // 读取物体温度传感器地址0x5A RAM地址0x07 float object_temp MLX90614_Read(0x5A, 0x07); // 读取环境温度传感器地址0x5A RAM地址0x06 float ambient_temp MLX90614_Read(0x5A, 0x06); printf(Object Temp: %.2f C, Ambient Temp: %.2f C\r\n, object_temp, ambient_temp); delay_ms(1000); // 每秒读取一次 } }把代码编译下载到天空星开发板连接好传感器打开串口助手。你应该能看到类似这样的输出MLX90614 Test Start... Object Temp: 36.50 C, Ambient Temp: 25.80 C你可以用手靠近传感器物体会升高对着传感器吹气或者用风扇吹环境温度可能会有轻微变化。这说明我们的驱动移植成功了6. 常见问题与调试心得最后分享几个我调试时踩过的坑希望能帮你节省时间通信完全没反应读到的值一直是0或者错误值。首先检查硬件VCC是不是5VGND接好了吗SCL和SDA的上拉电阻4.7kΩ到5V加了吗这是最容易出错的地方。检查引脚配置务必确认MLX90614_GPIO_Init函数里将PB8和PB9设置成了开漏输出GPIO_OTYPE_OD并且使能了上拉。用逻辑分析仪抓时序这是最强大的调试工具。抓一下SCL和SDA的波形对照数据手册的时序图看起始、停止、应答信号是否正确数据位有没有发对。延时delay_us(5)是否合适如果单片机主频很高可能需要调整。温度值跳变很大或者明显不准。检查PEC校验尝试开启CRC_VERIFY_ENABLE。如果开启后数据稳定了说明之前通信有误码可能是干扰或时序太临界。如果开启后反而读不到数据可能是PEC计算有误检查PEC_Calculation函数和数据拼接顺序。注意传感器视角MLX90614有一个测量视场角要确保被测物体充满它的视野并且没有其他强红外源如太阳、白炽灯干扰。公式计算确认原始数据拼接是否正确buff[1]是高8位buff[0]是低8位以及公式(temp * 0.02) - 273.15是否正确应用。只能读一次第二次就卡死了。重点检查I2C_WaitAck()函数和MLX90614_Read函数中的流程。确保每次通信结束后都正确发送了停止信号IIC_Stop()并且SDA线被正确释放比如在IIC_Send_Ack函数最后有SDA(1)。在MLX90614_Read函数中发送读命令前的do...while循环是为了等待传感器准备好数据。如果传感器一直忙可能会死循环。可以增加一个最大重试次数限制。按照上面的步骤一步步来你肯定能在天空星GD32F407上成功驱动MLX90614。这套代码框架也稍作修改就能用到其他单片机平台上核心的SMBus时序和数据处理逻辑都是通用的。