MLX90614红外测温传感器在天空星HC32F4A0开发板上的SMBus驱动移植与实战
MLX90614红外测温传感器在天空星HC32F4A0开发板上的SMBus驱动移植与实战
最近在做一个非接触测温的小项目,用到了MLX90614这款红外测温传感器。我发现很多朋友在把它和天空星HC32F4A0开发板搭配使用时,对SMBus通信和开漏模式配置有点摸不着头脑。今天我就把自己完整的移植过程和调试心得分享出来,手把手带你搞定这个传感器。
MLX90614最大的优点就是非接触测温,不用碰到物体就能知道它的温度,而且出厂就校准好了,精度不错。它和单片机通信用的是SMBus协议,你可以把它理解为I2C的“兄弟”,时序很像,但有些细节不同。咱们的天空星开发板没有硬件SMBus接口,所以需要用GPIO引脚来模拟时序,也就是常说的“软件模拟”或“GPIO模拟”。
这篇文章,我会从传感器原理、硬件连接、软件模拟SMBus、再到完整的代码移植和温度读取,一步步讲清楚。只要你跟着做,就能让你的天空星开发板成功读取到物体温度和环境温度。
1. 认识MLX90614与SMBus通信
1.1 传感器简介与工作原理
MLX90614是一个红外测温传感器模块。它内部有个红外热电堆探测器,能检测物体发射的红外辐射能量,再通过内部的DSP和信号调理电路,直接算出温度值。
和传统的接触式测温(比如热敏电阻)相比,红外测温有三大优势:
- 非接触:不干扰被测物体,适合测运动物体、高温或危险物体。
- 响应快:几乎瞬间就能读出温度。
- 测量范围广:咱们这个模块大概能测-70℃到380℃。
模块出厂前已经完成了校准和线性化处理,所以我们拿到手直接用就行,不需要自己再做复杂的标定。
1.2 什么是SMBus?
MLX90614用的通信协议是SMBus(系统管理总线)。它和I2C非常像,也是两根线:
- SCL:时钟线
- SDA:数据线
通信方式也是主从结构,靠时钟同步数据。那它和I2C有什么区别呢?主要在一些时序参数(比如超时时间、时钟频率范围)和协议细节上。不过对于MLX90614的基本读写操作,我们可以用标准的I2C时序来模拟,绝大部分情况下都能正常工作。
关键点:因为天空星HC32F4A0开发板没有硬件SMBus外设,所以我们必须用两个普通的GPIO引脚,通过软件控制它们的高低电平变化,来“模仿”出SCL和SDA的时序。这就是“软件模拟SMBus”。
2. 硬件连接与关键配置
2.1 模块引脚与开发板连接
首先,看看MLX90614模块(以常见的GY-906模块为例)通常有4个引脚:
- VCC:电源,接5V
- GND:地
- SCL:时钟线
- SDA:数据线
这里有个非常重要的细节:MLX90614模块的工作电压是4.5V~5.5V,而咱们天空星HC32F4A0开发板的GPIO引脚输出电压是3.3V。如果直接用3.3V去驱动5V的器件,可能会出现逻辑电平不匹配,导致通信失败。
怎么办?答案就是使用开漏输出模式。
2.2 为什么必须用开漏模式?
开漏模式就像是一个只有“下拉开关”的电路。当单片机输出0时,引脚内部连接到GND,输出低电平;当单片机输出1时,这个开关断开,引脚状态由外部电路决定。
我们可以在SDA和SCL线上各加一个上拉电阻(通常4.7kΩ~10kΩ)到5V。这样:
- 当单片机输出0时,线被拉低到0V。
- 当单片机输出1(即断开)时,5V的上拉电阻把线拉到5V的高电平。
这样,数据线的高电平就是5V,符合MLX90614的要求,而低电平0V双方都能识别。所以,在初始化GPIO时,必须将引脚配置为开漏输出模式,并且使能内部上拉(或外部接上拉电阻)。原文代码里特别强调了这一点,我一开始没注意,调了半天都没数据。
2.3 引脚选择与电路连接
在提供的例程中,选择了开发板的PB8作为SDA,PB9作为SCL。你可以按照这个来,也可以换成其他GPIO,只要在代码里改一下宏定义就行。
连接方式很简单:
- 模块VCC → 开发板5V引脚
- 模块GND → 开发板GND
- 模块SCL → 开发板PB9(并接一个4.7kΩ电阻到5V)
- 模块SDA → 开发板PB8(并接一个4.7kΩ电阻到5V)
如果开发板GPIO内部上拉电阻足够强(通常几十kΩ),也可以尝试直接使能内部上拉,但为了稳定,我建议还是外接4.7kΩ电阻到5V最靠谱。
3. 软件模拟SMBus驱动详解
驱动代码主要包含两个文件:bsp_mlx90614.c和bsp_mlx90614.h。我们一点点拆开看。
3.1 引脚宏定义与模式切换(bsp_mlx90614.h)
头文件里最关键的是几个宏定义,它们封装了引脚操作,让代码更清晰。
//端口移植 #define PORT_MLX90614 GPIO_PORT_B #define GPIO_SDA GPIO_PIN_08 #define GPIO_SCL GPIO_PIN_09 //SDA输入模式 #define SDA_IN() { stc_gpio_init_t stcGpioInit; \ GPIO_StructInit(&stcGpioInit); \ stcGpioInit.u16PinState = PIN_STAT_SET; \ stcGpioInit.u16PinDir = PIN_DIR_IN; \ stcGpioInit.u16PullUp = PIN_PU_ON; \ GPIO_Init(PORT_MLX90614, GPIO_SDA, &stcGpioInit); \ } //SDA输出模式 #define SDA_OUT() { stc_gpio_init_t stcGpioInit; \ GPIO_StructInit(&stcGpioInit); \ stcGpioInit.u16PinState = PIN_STAT_SET; \ stcGpioInit.u16PinOutputType = PIN_OUT_TYPE_NMOS; \ stcGpioInit.u16PinDir = PIN_DIR_OUT; \ stcGpioInit.u16PullUp = PIN_PU_ON; \ GPIO_Init(PORT_MLX90614, GPIO_SDA, &stcGpioInit); \ } //获取SDA引脚的电平 #define SDA_GET() GPIO_ReadInputPins(PORT_MLX90614, GPIO_SDA) //控制SDA与SCL输出高低电平 #define SDA(x) ( x ? GPIO_SetPins(PORT_MLX90614,GPIO_SDA) : GPIO_ResetPins(PORT_MLX90614,GPIO_SDA) ) #define SCL(x) ( x ? GPIO_SetPins(PORT_MLX90614,GPIO_SCL) : GPIO_ResetPins(PORT_MLX90614,GPIO_SCL) )重点看SDA_OUT()宏:它把引脚方向设为输出(PIN_DIR_OUT),输出类型设为NMOS开漏(PIN_OUT_TYPE_NMOS),并且开启了内部上拉(PIN_PU_ON)。这就是实现前面讲的“开漏带上拉”的关键配置。
SDA_IN()宏则在需要读取SDA线状态时(比如等待应答),将引脚临时切换为输入模式。
3.2 GPIO初始化函数
这个函数在bsp_mlx90614.c里,主要就是调用上面的SDA_OUT()配置,并且把SCL和SDA线初始化为高电平(空闲状态)。
void MLX90614_GPIO_Init(void) { stc_gpio_init_t stcGpioInit; // 关闭寄存器保护(对HC32系列,操作外设前有时需要解锁) LL_PERIPH_WE(LL_PERIPH_ALL); (void)GPIO_StructInit(&stcGpioInit); stcGpioInit.u16PinState = PIN_STAT_SET; // 初始状态设为高 stcGpioInit.u16PinOutputType = PIN_OUT_TYPE_NMOS;// 开漏 stcGpioInit.u16PinDir = PIN_DIR_OUT; // 输出模式 stcGpioInit.u16PullUp = PIN_PU_ON; // 上拉开启 // 同时初始化SDA和SCL引脚 (void)GPIO_Init(PORT_MLX90614, GPIO_SCL|GPIO_SDA, &stcGpioInit); // 将两个引脚都设置为高电平 GPIO_SetPins(PORT_MLX90614,GPIO_SCL|GPIO_SDA); }注意:
LL_PERIPH_WE(LL_PERIPH_ALL);这行是HC32系列芯片特有的操作,目的是解锁外设寄存器,允许写入。如果你移植到其他芯片平台,可能需要删除或修改这一行。
3.3 SMBus基础时序模拟
SMBus/I2C通信有几个基本时序单元:起始信号、停止信号、发送一个字节、接收一个字节、应答。我们的驱动需要自己用代码把这些时序“画”出来。
起始信号 (IIC_Start):在SCL高电平期间,SDA产生一个下降沿。
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); }停止信号 (IIC_Stop):在SCL高电平期间,SDA产生一个上升沿。
void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); delay_us(5); SCL(1); delay_us(5); SDA(1); // 在SCL高时拉高SDA,产生停止条件 delay_us(5); }发送一个字节 (Send_Byte):从最高位(MSB)开始,在SCL低电平时设置SDA数据,然后拉高SCL,再拉低SCL,如此循环8次。
void Send_Byte(uint8_t dat) { int i = 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for( i = 0; i < 8; i++ ) { // 取出最高位,右移7位得到0或1,然后设置SDA线 SDA( (dat & 0x80) >> 7 ); delay_us(1); SCL(1); // 拉高时钟,数据被采样 delay_us(5); SCL(0); // 拉低时钟,准备下一位 delay_us(5); dat<<=1; // 左移,处理下一位 } }接收一个字节 (Read_Byte):主机控制SCL产生时钟,在SCL高电平时读取SDA线的状态。
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() ) // 读取SDA引脚电平 { receive|=1; // 如果为高,则该位为1 } delay_us(5); } SCL(0); return receive; }等待应答 (I2C_WaitAck):主机发送完8位数据后,会释放SDA线(设为输入),并产生一个时钟脉冲(SCL高电平)。在这个脉冲期间,从机应该把SDA线拉低,表示应答。
unsigned char I2C_WaitAck(void) { char ack = 0; unsigned char ack_flag = 10; // 超时计数 SCL(0); SDA(1); // 主机释放SDA线 SDA_IN(); // 设置为输入,检测从机应答 delay_us(5); SCL(1); // 产生第9个时钟脉冲 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表示成功 }3.4 MLX90614数据读取核心函数
这是最核心的函数,它按照MLX90614的SMBus协议,组合使用上面的基础时序,完成一次温度数据的读取。
MLX90614内部有两个重要的温度数据寄存器(在RAM中):
- 地址0x06:存放环境温度 (Ta)
- 地址0x07:存放物体温度 (To)
器件的默认从机地址是0x5A(7位地址)。在通信时,需要将这个地址左移一位,并根据读写操作补上最低位(0为写,1为读)。
float MLX90614_Read(unsigned char SlaveAddr, unsigned char RegAddr) { // 1. 发送起始信号 IIC_Start(); // 2. 发送器件地址 + 写命令 (地址左移1位,最低位写0) Send_Byte((SlaveAddr << 1) | 0); // 3. 等待从机应答 if(I2C_WaitAck()) { IIC_Stop(); return 0.0; // 应答失败,返回0 } // 4. 发送要读取的寄存器地址(比如0x07读物体温度) Send_Byte(RegAddr); if(I2C_WaitAck()) { IIC_Stop(); return 0.0; } // 5. 发送重复起始信号(Sr),准备读数据 IIC_Start(); // 6. 发送器件地址 + 读命令 (地址左移1位,最低位写1) Send_Byte((SlaveAddr << 1) | 1); if(I2C_WaitAck()) { IIC_Stop(); return 0.0; } // 7. 读取数据(低8位在前) uint8_t lowByte = Read_Byte(); // 读低8位 IIC_Send_Ack(0); // 发送应答(ACK),请求下一个字节 uint8_t highByte = Read_Byte(); // 读高8位 IIC_Send_Ack(1); // 发送非应答(NACK),表示读取结束 // 8. 发送停止信号 IIC_Stop(); // 9. 合并数据并换算温度 uint16_t tempRaw = (highByte << 8) | lowByte; float temperature = (tempRaw * 0.02) - 273.15; return temperature; }关键点解析:
- 数据顺序:MLX90614返回的数据是低8位在前,高8位在后。所以先读出来的是低字节(
lowByte),后读出来的是高字节(highByte)。合并时是(highByte << 8) | lowByte。 - 温度换算:原始数据是一个16位的数字。根据数据手册,换算公式是:温度(℃) = 原始数据 × 0.02 - 273.15。这个公式对物体温度(To)和环境温度(Ta)都适用。
- 应答机制:读取两个字节过程中,读完第一个字节后要发送应答(ACK),读完第二个字节后要发送非应答(NACK),然后停止。
4. 移植到你的工程并测试
4.1 文件添加与工程配置
- 在你的工程目录下(比如
Drivers/BSP文件夹),添加下载好的或自己编写的bsp_mlx90614.c和bsp_mlx90614.h文件。 - 在IDE(如Keil、IAR)的工程中,将
.c文件添加到对应的分组,并设置好头文件包含路径。 - 确保工程里包含了HC32F4A0的标准外设库(例如
hc32_ll.h),因为驱动代码里用到了它的GPIO操作函数。
4.2 编写主函数进行测试
在主函数里,初始化串口(用于打印结果)和MLX90614,然后循环读取温度即可。
#include "board.h" #include "bsp_uart.h" #include "stdio.h" #include "bsp_mlx90614.h" int32_t main(void) { // 板级初始化(系统时钟、外设等) board_init(); // 初始化串口1,波特率115200,用于打印调试信息 uart1_init(115200U); // 初始化MLX90614的GPIO(配置为开漏模式) MLX90614_GPIO_Init(); printf("MLX90614 Test Start\r\n"); while(1) { // 读取物体温度(从机地址0x5A,寄存器地址0x07) float object_temp = MLX90614_Read(0X5A, 0X07); if( object_temp != 0.0) // 读取成功 { printf("Object Temperature = %.2f C\r\n", object_temp); } else { printf("Read object temp failed!\r\n"); } // 读取环境温度(寄存器地址0x06) float ambient_temp = MLX90614_Read(0X5A, 0X06); if( ambient_temp != 0.0) { printf("Ambient Temperature = %.2f C\r\n", ambient_temp); } // 延时1秒 delay_ms(1000); } }4.3 现象与调试
将代码编译下载到天空星开发板,连接好MLX90614模块,打开串口助手(波特率115200)。你应该能看到每秒打印一次物体温度和环境温度。
正常现象:用手靠近传感器,物体温度会迅速上升到30多度(接近体温),环境温度则显示室温。
如果没数据或数据为0,可以按以下步骤排查:
- 检查硬件连接:VCC、GND、SCL、SDA四根线是否接对、接牢。务必确认SCL和SDA线上拉了4.7kΩ电阻到5V。
- 检查GPIO配置:确认代码中
MLX90614_GPIO_Init函数确实将引脚配置为了开漏输出(PIN_OUT_TYPE_NMOS)并开启了上拉。 - 用逻辑分析仪或示波器抓时序:这是最有效的调试方法。查看SCL和SDA线上是否有符合SMBus协议的波形,起始信号、地址、数据位、应答信号是否正常。
- 检查延时:
delay_us函数的实际延时时间是否准确。时序太快或太慢都可能导致通信失败。可以适当增大delay_us(5)中的延时值试试。 - 检查地址:确认使用的从机地址是
0x5A。有些模块可以通过命令修改地址,但默认都是0x5A。
按照上面的步骤,你应该就能成功在天空星HC32F4A0开发板上驱动MLX90614了。这个软件模拟SMBus的驱动代码框架是通用的,理解了之后,移植到其他单片机平台(如STM32、GD32等)也基本是改改GPIO操作函数就行。希望这篇教程能帮你少走弯路。
