基于CW32F030C8T6的MLX90614红外测温模块SMBus驱动移植与实战
基于CW32F030C8T6的MLX90614红外测温模块SMBus驱动移植与实战最近在做一个非接触测温的小项目用到了MLX90614红外测温传感器主控芯片选的是国产的CW32F030C8T6。很多朋友在初次接触这类传感器时对它的“类IIC”通信也就是SMBus以及如何把温度原始数据换算成摄氏度感到头疼。今天我就把自己在实际项目中的驱动移植过程、代码编写和调试心得整理出来手把手带你搞定MLX90614与CW32的对接让你能快速上手读取物体温度和环境温度。1. 认识MLX90614与SMBus通信MLX90614是一个用起来非常方便的红外测温传感器。它最大的优点就是“非接触”你不用碰到物体就能测出它的温度而且出厂前厂家已经帮你校准好了我们拿过来直接用就行。它的测温原理是检测物体发出的红外辐射能量所以响应速度比接触式的快很多也不容易受环境干扰。它和单片机通信的方式资料里常说是“类IIC”。这其实指的是SMBus系统管理总线。你可以把它理解为IIC总线的一个“亲戚”或“特定版本”也是用两根线时钟线SCL和数据线SDA进行通信。对于CW32F030C8T6这类没有硬件SMBus接口的单片机我们完全可以用两个普通的GPIO引脚通过软件模拟时序的方式来和MLX90614对话这正是我们下面要做的。注意MLX90614模块的工作电压是4.5V~5.5V而我们的CW32开发板GPIO输出电平是3.3V。这是一个关键点后面配置GPIO时必须设置为开漏输出模式并依靠模块内部或外部上拉电阻拉到5V才能实现电平兼容和可靠通信。2. 理解MLX90614的数据存储与读取逻辑想要正确读出温度得先明白MLX90614内部是怎么存数据的。它内部有两块存储区域对我们用户来说主要关心RAM。EEPROM存储了厂家的校准参数和配置信息。除非你需要修改传感器的一些默认设置比如发射率否则我们一般不用动它直接使用出厂值即可。RAM这里存放着实时测量得到的数据。我们需要的温度数据就在这里面环境温度 (Ta)存储在RAM地址0x06。物体温度 (To)存储在RAM地址0x07。我们要做的就是通过SMBus协议去读取这两个地址里的数据。几个关键概念器件地址MLX90614默认的7位器件地址是0x5A。在通信时我们需要将其左移一位并在最低位加上读写位来构成一个完整的8位字节。命令字告诉传感器我们要读RAM还是EEPROM以及具体的地址。对于读RAM中的温度数据命令字就是温度数据地址本身0x06或0x07。数据格式读回来的温度原始数据是一个16位的数。这里有个细节低8位在前高8位在后。我们在代码里需要把它们组合起来。温度换算读到的16位原始数据是经过内部ADC转换后的数值。换算成摄氏度的公式是温度(℃) (原始数据 × 0.02) - 273.15这个公式对物体温度(To)和环境温度(Ta)都适用。比如读到0x3AF7(十进制15095)计算就是15095 * 0.02 - 273.15 28.75℃。3. 动手移植GPIO初始化与SMBus模拟理解了原理咱们就开始写代码。我一般会把驱动分成一个.c文件和一个.h文件方便管理。3.1 引脚与宏定义 (bsp_mlx90614.h)首先在头文件里我们把用到的引脚、端口和操作宏定义好。这里我选择的是CW32F030C8T6的PA0和PA1你可以根据自己板子的情况修改。#ifndef _BSP_MLX90614_H_ #define _BSP_MLX90614_H_ #include board.h // 端口与引脚定义 #define RCC_MLX90614_ENABLE() __RCC_GPIOA_CLK_ENABLE() // 使能GPIOA时钟 #define PORT_MLX90614 CW_GPIOA // 使用GPIOA端口 #define GPIO_SDA GPIO_PIN_1 // SDA数据线接PA1 #define GPIO_SCL GPIO_PIN_0 // SCL时钟线接PA0 // 设置SDA为开漏输出模式关键为了兼容5V模块 #define SDA_OUT() { \ GPIO_InitTypeDef GPIO_InitStruct; \ GPIO_InitStruct.Pins GPIO_SDA; \ GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; \ GPIO_InitStruct.Speed GPIO_SPEED_HIGH; \ GPIO_Init(PORT_MLX90614, GPIO_InitStruct); \ } // 设置SDA为上拉输入模式读取数据时用 #define SDA_IN() { \ GPIO_InitTypeDef GPIO_InitStruct; \ GPIO_InitStruct.Pins GPIO_SDA; \ GPIO_InitStruct.Mode GPIO_MODE_INPUT_PULLUP; \ GPIO_InitStruct.Speed GPIO_SPEED_HIGH; \ GPIO_Init(PORT_MLX90614, GPIO_InitStruct); \ } // 读取SDA引脚电平 #define SDA_GET() GPIO_ReadPin(PORT_MLX90614, GPIO_SDA) // 控制SDA和SCL引脚输出高低电平 #define SDA(x) GPIO_WritePin(PORT_MLX90614, GPIO_SDA, (x?GPIO_Pin_SET:GPIO_Pin_RESET) ) #define SCL(x) GPIO_WritePin(PORT_MLX90614, GPIO_SCL, (x?GPIO_Pin_SET:GPIO_Pin_RESET) ) // 函数声明 void MLX90614_GPIO_Init(void); float MLX90614_Read(unsigned char SlaveAddr, unsigned char RegAddr); #endif提示GPIO_MODE_OUTPUT_OD开漏输出模式是必须的。因为开漏模式下引脚只能主动拉低输出0或高阻态相当于断开。当我们需要输出高电平时实际是靠外部上拉电阻模块内部通常已集成拉到5V这样就实现了3.3V MCU与5V模块的电平转换。3.2 GPIO初始化与基础时序函数 (bsp_mlx90614.c)接下来是C文件。我们先完成引脚的初始化和最基础的SMBus时序模拟函数。这些函数是通信的基石一定要保证时序正确。#include bsp_mlx90614.h #include stdio.h // 一个简单的微秒级延时函数需要你根据CW32的系统时钟来实现 extern void delay_us(uint32_t us); // 毫秒延时通常由系统滴答定时器提供 extern void delay_ms(uint32_t ms); /****************************************************************** * 函数名称MLX90614_GPIO_Init * 函数说明初始化连接MLX90614的GPIO引脚 * 函数形参无 * 函数返回无 ******************************************************************/ void MLX90614_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_MLX90614_ENABLE(); // 打开GPIO时钟 // 初始化SCL和SDA都为开漏输出模式并先置高 GPIO_InitStruct.Pins GPIO_SCL | GPIO_SDA; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Speed GPIO_SPEED_HIGH; GPIO_Init(PORT_MLX90614, GPIO_InitStruct); SCL(1); SDA(1); } /****************************************************************** * 函数名称IIC_Start * 函数说明产生SMBus起始信号 * 形 参无 * 返 回 值无 ******************************************************************/ void IIC_Start(void) { SDA_OUT(); // 确保SDA为输出模式 SDA(1); // SDA高 SCL(1); // SCL高 delay_us(5); // 保持一段时间 SDA(0); // 在SCL高期间SDA由高变低 - 起始信号 delay_us(5); SCL(0); // 拉低SCL准备发送数据 delay_us(5); } /****************************************************************** * 函数名称IIC_Stop * 函数说明产生SMBus停止信号 * 形 参无 * 返 回 值无 ******************************************************************/ void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); // 先确保SDA为低 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(1); // 在SCL高期间SDA由低变高 - 停止信号 delay_us(5); } /****************************************************************** * 函数名称Send_Byte * 函数说明主机向从机发送一个字节数据 * 形 参dat - 要发送的数据 * 返 回 值无 ******************************************************************/ void Send_Byte(uint8_t dat) { uint8_t i; SDA_OUT(); SCL(0); // 拉低时钟线准备数据变化 for(i 0; i 8; i) { // 从最高位(MSB)开始发送 SDA((dat 0x80) ? 1 : 0); // 取出最高位并输出 delay_us(1); SCL(1); // 拉高时钟从机在此时采样数据 delay_us(5); SCL(0); // 拉低时钟为下一位数据做准备 delay_us(5); dat 1; // 左移处理下一位 } } /****************************************************************** * 函数名称Read_Byte * 函数说明主机从从机读取一个字节数据 * 形 参无 * 返 回 值读取到的数据 ******************************************************************/ uint8_t Read_Byte(void) { uint8_t 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 * 函数说明主机等待从机的应答信号 * 形 参无 * 返 回 值0 - 收到应答1 - 未收到应答超时 ******************************************************************/ uint8_t I2C_WaitAck(void) { uint8_t ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机先释放SDA线 SDA_IN(); // 设置SDA为输入检测从机应答 delay_us(5); SCL(1); // 拉高时钟从机应在此时将SDA拉低 delay_us(5); // 等待SDA被拉低应答 while((SDA_GET() 1) (ack_flag)) { ack_flag--; delay_us(5); } if(ack_flag 0) // 超时未收到应答 { IIC_Stop(); return 1; } else // 收到应答 { SCL(0); SDA_OUT(); // 恢复SDA为输出模式以便后续操作 } return 0; } /****************************************************************** * 函数名称IIC_Send_Ack * 函数说明主机发送应答(ACK)或非应答(NACK)信号 * 形 参ack - 0发送应答1发送非应答 * 返 回 值无 ******************************************************************/ void IIC_Send_Ack(uint8_t ack) { SDA_OUT(); SCL(0); if(!ack) SDA(0); // 发送应答(ACK)拉低SDA else SDA(1); // 发送非应答(NACK)保持SDA高 delay_us(5); SCL(1); // 产生一个时钟脉冲 delay_us(5); SCL(0); SDA(1); // 释放SDA线 }3.3 核心温度读取函数基础时序函数写好之后我们就可以按照SMBus的读数据流程来封装一个最终的温度读取函数了。这个函数会完成完整的通信协议起始信号 - 发送器件地址写命令 - 发送要读的RAM地址 - 重复起始信号 - 发送器件地址读命令 - 读取两个字节数据 - 停止信号。/****************************************************************** * 函数名称MLX90614_Read * 函数说明读取MLX90614的温度值 * 形 参SlaveAddr - 器件地址默认0x5A * RegAddr - 寄存器地址0x06读环境温度0x07读物体温度 * 返 回 值计算后的摄氏温度值如果通信失败返回0.0 ******************************************************************/ float MLX90614_Read(unsigned char SlaveAddr, unsigned char RegAddr) { // 1. 发送起始信号 IIC_Start(); // 2. 发送器件地址 写位(0) Send_Byte((SlaveAddr 1) | 0); if(I2C_WaitAck()) { // 等待应答 IIC_Stop(); return 0.0f; // 通信失败 } // 3. 发送要读取的RAM地址 Send_Byte(RegAddr); if(I2C_WaitAck()) { IIC_Stop(); return 0.0f; } // 4. 发送重复起始信号相当于StopStart但更高效 IIC_Start(); // 5. 发送器件地址 读位(1) Send_Byte((SlaveAddr 1) | 1); if(I2C_WaitAck()) { IIC_Stop(); return 0.0f; } // 6. 读取数据低8位在前 uint8_t lowByte Read_Byte(); IIC_Send_Ack(0); // 读取第一个字节后发送ACK uint8_t highByte Read_Byte(); IIC_Send_Ack(1); // 读取最后一个字节后发送NACK // 7. 发送停止信号结束本次通信 IIC_Stop(); // 8. 组合数据并换算温度 uint16_t tempRaw (highByte 8) | lowByte; // 高8位在前低8位在后组合 float temperature (tempRaw * 0.02f) - 273.15f; return temperature; }4. 在工程中测试与验证驱动代码写好了接下来就是在主函数里调用它。我们初始化串口用于打印结果然后在一个循环里每隔1秒读取一次物体温度。#include board.h #include stdio.h #include bsp_uart.h // 你的串口驱动头文件 #include bsp_mlx90614.h int32_t main(void) { board_init(); // 开发板基础初始化系统时钟等 uart1_init(115200); // 初始化串口1波特率115200用于打印 MLX90614_GPIO_Init(); // 初始化MLX90614的GPIO printf(MLX90614 Test Start!\r\n); while(1) { // 读取物体温度RAM地址0x07器件地址0x5A float object_temp MLX90614_Read(0x5A, 0x07); if(object_temp ! 0.0f) // 简单判断通信是否成功 { printf(Object Temperature %.2f C\r\n, object_temp); } else { printf(Read Error!\r\n); } // 你也可以读取环境温度 // float ambient_temp MLX90614_Read(0x5A, 0x06); // printf(Ambient Temperature %.2f C\r\n, ambient_temp); delay_ms(1000); // 延时1秒 } }将代码编译下载到CW32F030C8T6开发板连接好MLX90614模块VCC接5VGND接GNDSCL接PA0SDA接PA1打开串口助手。你应该能看到每秒打印一次的温度值。用手靠近传感器温度会明显上升把手心对着传感器读数大概在36℃左右这说明驱动移植成功了常见问题与调试建议读不到数据或数据为0首先检查硬件连接确保电源是5VSCL和SDA线没有接反。然后用逻辑分析仪或示波器抓一下SCL和SDA的波形看看起始信号、地址、数据位的时序是否符合要求。最可能的原因是GPIO模式没有设置为开漏输出。温度值跳变或不准MLX90614需要一点时间进行温度稳定。确保传感器前方没有强烈的热源或气流干扰。读取间隔不要太快给传感器足够的测量时间。关于PEC校验原始资料里提到了PEC包错误校验这是一个可选的CRC校验用于提高通信可靠性。在要求不高的场合我们可以暂时不用它。如果你需要更稳定的通信可以参照资料中的PEC_Calculation函数示例代码中已注释在通信帧末尾加上校验和比对。这个驱动代码我已经在实际项目中用过稳定性不错。掌握了这个基础你不仅可以用来测温还能举一反三理解如何通过SMBus/IIC协议去操作其他类似的传感器。