基于立创开发板与R7FA6E2BB3CNE的BH1750FVI光照传感器I2C驱动移植与数据采集实战
基于立创开发板与R7FA6E2BB3CNE的BH1750FVI光照传感器I2C驱动移植与数据采集实战
最近在做一个智能环境监测的小项目,需要用到光照传感器。我手头正好有一块立创的开发板,主控是瑞萨的R7FA6E2BB3CNE,还有一个ROHM原装的BH1750FVI光照传感器模块。今天我就来分享一下,如何把BH1750的驱动移植到这块开发板上,并实现稳定的数据采集。
这篇文章适合正在使用或学习立创开发板的嵌入式开发者,特别是那些需要在项目中集成环境光传感功能的朋友。我会从硬件连接到软件驱动,一步步带你完成整个移植过程,中间遇到的坑也会详细说明。
1. 认识BH1750FVI光照传感器
BH1750FVI是一款数字式环境光强度传感器,它最大的优点就是使用简单,不需要复杂的计算和校准。咱们先来了解一下它的基本特性。
1.1 传感器核心特点
BH1750FVI内部集成了16位的AD转换器,可以直接输出数字信号,省去了我们自己做模数转换的麻烦。它的测量范围很宽,从0到65535勒克斯(lx),这个范围覆盖了从黑暗环境到强烈日光下的各种光照条件。
注意:勒克斯(lx)是光照强度的单位,可以简单理解为光线有多亮。普通室内照明大约在100-500 lx,晴朗的室外中午能达到10000 lx以上。
传感器内部还做了特殊处理,能够模拟人眼对不同光源的敏感度,这样测出来的数据更接近我们人眼实际感受到的亮度。
1.2 关键参数一览
| 参数 | 规格 |
|---|---|
| 工作电压 | 3.3V - 5V |
| 工作电流 | 约200uA(很省电) |
| 测量范围 | 1 - 65535 lx |
| 模块尺寸 | 32.6mm × 15.2mm × 11.6mm |
| 输出方式 | I2C通信 |
| 引脚数量 | 5个 |
BH1750采用标准的I2C通信协议,这意味着我们只需要两根线(SCL时钟线和SDA数据线)就能和它通信。模块内部还集成了电平转换电路,所以可以直接和3.3V的单片机IO口连接,不需要额外的电平转换芯片。
2. 硬件连接准备
在开始写代码之前,咱们先把硬件连接好。我用的立创开发板上有丰富的GPIO资源,这里选择P408和P409两个引脚来连接BH1750。
2.1 引脚对应关系
| BH1750模块引脚 | 立创开发板引脚 | 说明 |
|---|---|---|
| VCC | 3V3 | 电源正极,接3.3V |
| GND | GND | 电源地 |
| SCL | P408 | I2C时钟线 |
| SDA | P409 | I2C数据线 |
| ADDR | 悬空或接地 | 地址选择引脚,这里悬空使用默认地址 |
提示:BH1750的ADDR引脚用来设置I2C从机地址。当ADDR接低电平(接地)时,地址是0x46;接高电平(接VCC)时,地址是0xB8。我们这里悬空,模块内部有下拉电阻,所以默认使用0x46地址。
2.2 连接注意事项
- 电源要稳定:虽然BH1750工作电流很小,但还是要确保3.3V电源稳定,避免电压波动影响测量精度。
- 上拉电阻:I2C总线需要上拉电阻,一般用4.7kΩ或10kΩ。好在立创开发板的I2C引脚内部已经集成了上拉电阻,我们不需要外接。
- 走线尽量短:如果传感器离开发板较远,建议使用屏蔽线或者尽量缩短连接线长度,减少干扰。
3. 开发环境配置
3.1 图形化引脚配置
立创的开发环境支持图形化配置引脚,这个功能对初学者特别友好。咱们先来配置一下要用到的两个GPIO引脚:
- 打开开发环境的图形化配置界面
- 找到P408和P409这两个引脚
- 将它们配置为普通的GPIO功能(因为我们用软件模拟I2C时序)
- 按
Ctrl + S保存配置 - 点击右上角的
Generate Project Content生成代码
这样配置后,开发环境会自动生成引脚初始化的代码,我们就不用自己手动写寄存器配置了。
3.2 创建驱动文件
接下来咱们创建两个文件来存放BH1750的驱动代码:
bsp_bh1750.c- 驱动源文件bsp_bh1750.h- 驱动头文件
创建好后,记得把头文件路径添加到编译器的包含目录中。具体怎么添加,可以参考开发板资料里DHT11温湿度传感器章节的说明,方法是一样的。
4. 驱动代码编写
现在进入核心部分——编写驱动代码。BH1750的驱动主要分为几个部分:I2C时序模拟、命令发送、数据读取。
4.1 头文件定义(bsp_bh1750.h)
头文件里主要定义一些宏和函数声明,让代码更清晰:
#ifndef BSP_CODE_BSP_BH1750_H_ #define BSP_CODE_BSP_BH1750_H_ #include "hal_data.h" #include <stdio.h> // 定义一些常用的数据类型,方便移植 #ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif // 延时函数宏定义 #ifndef delay_ms #define delay_ms(x) R_BSP_SoftwareDelay(x, BSP_DELAY_UNITS_MILLISECONDS) #endif #ifndef delay_1ms #define delay_1ms(x) R_BSP_SoftwareDelay(x, BSP_DELAY_UNITS_MILLISECONDS) #endif #ifndef delay_us #define delay_us(x) R_BSP_SoftwareDelay(x, BSP_DELAY_UNITS_MICROSECONDS) #endif #ifndef delay_1us #define delay_1us(x) R_BSP_SoftwareDelay(x, BSP_DELAY_UNITS_MICROSECONDS) #endif // 引脚定义 #define Module_SCL_PIN BSP_IO_PORT_04_PIN_08 // SCL时钟线 #define Module_SDA_PIN BSP_IO_PORT_04_PIN_09 // SDA数据线 // SDA引脚输入模式配置 #define SDA_IN() { \ fsp_err_t err = R_IOPORT_PinCfg(&g_ioport_ctrl, Module_SDA_PIN, \ (uint32_t) IOPORT_CFG_PORT_DIRECTION_INPUT); \ if(err != FSP_SUCCESS) { \ printf("GPIO Mode Set INPUT Failed!!\r\n"); \ } \ } // SDA引脚输出模式配置 #define SDA_OUT() { \ fsp_err_t err = R_IOPORT_PinCfg(&g_ioport_ctrl, Module_SDA_PIN, \ ((uint32_t) IOPORT_CFG_DRIVE_HIGH \ | (uint32_t) IOPORT_CFG_NMOS_ENABLE \ | (uint32_t) IOPORT_CFG_PORT_DIRECTION_OUTPUT \ | (uint32_t) IOPORT_CFG_PORT_OUTPUT_HIGH)); \ if(err != FSP_SUCCESS) { \ printf("GPIO Mode Set OUTPUT Failed!!\r\n"); \ } \ } // SCL和SDA引脚输出控制 #define SCL(BIT) R_IOPORT_PinWrite(&g_ioport_ctrl, Module_SCL_PIN, BIT) #define SDA(BIT) R_IOPORT_PinWrite(&g_ioport_ctrl, Module_SDA_PIN, BIT) // 获取SDA引脚电平状态 static inline bsp_io_level_t SDA_GET(void) { bsp_io_level_t p_pin_value; fsp_err_t err = R_IOPORT_PinRead(&g_ioport_ctrl, Module_SDA_PIN, &p_pin_value); if(err != FSP_SUCCESS) { printf("GPIO Input Read Failed!!\r\n"); } return p_pin_value; } // 函数声明 char Multiple_read_BH1750(float *bh1750_value); char Single_Write_BH1750(uint8_t REG_Address); void BH1750_Init(void); #endif /* BSP_CODE_BSP_BH1750_H_ */4.2 I2C时序模拟
由于我们用的是普通GPIO模拟I2C时序,所以需要自己实现I2C的起始、停止、发送、接收等基本操作。这种软件模拟的方式虽然效率不如硬件I2C,但更灵活,也更容易调试。
// I2C起始信号 void IIC_Start(void) { SDA_OUT(); // 设置SDA为输出模式 SDA(1); // SDA拉高 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(0); // SDA拉低,产生起始条件 delay_us(5); SCL(0); // SCL拉低,准备数据传输 delay_us(5); } // I2C停止信号 void IIC_Stop(void) { SDA_OUT(); SCL(0); // 确保SCL为低 SDA(0); // SDA拉低 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(1); // SDA拉高,产生停止条件 delay_us(5); } // 发送应答信号 void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); // SCL拉低,准备发送应答 SDA(0); // 先拉低SDA delay_us(5); if(!ack) SDA(0); // ack=0表示应答,SDA保持低电平 else SDA(1); // ack=1表示非应答,SDA拉高 SCL(1); // SCL拉高,应答信号生效 delay_us(5); SCL(0); // SCL拉低,结束应答 SDA(1); // 释放SDA线 } // 等待从机应答 unsigned char I2C_WaitAck(void) { char ack = 0; unsigned char ack_flag = 10; // 超时计数 SCL(0); SDA(1); // 释放SDA线 SDA_IN(); // 设置SDA为输入模式,准备读取 delay_us(5); SCL(1); // SCL拉高,从机应该在这时拉低SDA 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(); // 重新设置SDA为输出模式 } return 0; // 返回0表示成功收到应答 }4.3 字节发送与接收
有了基本的I2C时序函数,接下来实现字节级别的发送和接收:
// 发送一个字节 void Send_Byte(uint8_t dat) { int i = 0; SDA_OUT(); SCL(0); // SCL拉低,开始数据传输 for( i = 0; i < 8; i++ ) { // 从最高位开始发送 SDA( dat & 0x80 ); // 取出最高位 delay_us(1); SCL(1); // SCL拉高,数据被采样 delay_us(5); SCL(0); // SCL拉低,准备发送下一位 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); // SCL拉低 delay_us(5); SCL(1); // SCL拉高,从机在这时输出数据 delay_us(5); receive <<= 1; // 左移一位,为接收新数据做准备 if( SDA_GET() ) // 读取SDA电平 { receive |= 1; // 如果SDA为高,对应位设为1 } delay_us(5); } SCL(0); // SCL拉低,结束读取 return receive; }4.4 BH1750专用函数
现在来实现BH1750的专用操作函数。BH1750支持几种测量模式,我们主要用连续高分辨率模式。
// BH1750的I2C地址(ADDR引脚悬空时的地址) #define SlaveAddress 0x46 // 向BH1750发送单字节命令 char Single_Write_BH1750(uint8_t REG_Address) { IIC_Start(); // 发送起始信号 Send_Byte(SlaveAddress); // 发送设备地址+写标志 if( I2C_WaitAck() != 0 ) return 1; // 等待应答,失败返回1 Send_Byte(REG_Address); // 发送命令字节 if( I2C_WaitAck() != 0 ) return 2; // 等待应答,失败返回2 IIC_Stop(); // 发送停止信号 return 0; // 成功返回0 } // 读取BH1750的光照强度数据 char Multiple_read_BH1750(float *bh1750_value) { uint16_t dis_data = 0; uint8_t dat_buff[2]; // 发送连续高分辨率测量命令 Single_Write_BH1750(0x10); delay_ms(180); // 等待测量完成,手册建议120ms,这里给足余量 // 开始读取数据 IIC_Start(); // 起始信号 Send_Byte(SlaveAddress + 1); // 设备地址+读标志 I2C_WaitAck(); // 等待应答 dat_buff[0] = Read_Byte(); // 读取高8位数据 IIC_Send_Ack(0); // 发送应答,继续读取 dat_buff[1] = Read_Byte(); // 读取低8位数据 IIC_Send_Ack(1); // 发送非应答,停止读取 IIC_Stop(); // 停止信号 // 合并两个字节的数据 dis_data = ((uint16_t)dat_buff[0] << 8) + dat_buff[1]; // 转换为光照强度值(单位:lx) // 公式:光照强度 = 测量值 / 1.2 *bh1750_value = (float)dis_data / 1.2f; return 0; // 成功返回0 } // BH1750初始化 void BH1750_Init(void) { // 初始化I2C引脚状态 SCL(1); SDA(1); delay_ms(100); // 等待传感器稳定 // 发送上电命令 Single_Write_BH1750(0x01); delay_ms(100); // 等待传感器准备好 }5. 主程序调用与测试
驱动写好了,现在咱们在主程序里调用它,看看效果如何。
5.1 修改主程序
打开src\Applay\app.c文件,在合适的位置添加BH1750的初始化和读取代码:
#include "app.h" #include "stdio.h" #include "bsp_uart.h" #include "bsp_bh1750.h" void Run(void) { /* 初始化调试串口 */ UART0_Debug_Init(); printf("\r\n= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =\r\n"); printf("\r\n=== Welcome to use the DQX-R7FA6E2BB3CNE development board ====\r\n"); printf("\r\n======================= www.lckfb.com =========================\r\n"); printf("\r\n======================= wiki.lckfb.com ========================\r\n"); printf("\r\n======================= [Debug Uart0] =========================\r\n"); printf("\r\n=================== | RX:P100 | TX:P101 | =====================\r\n"); printf("\r\n= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =\r\n"); /* BH1750传感器初始化 */ BH1750_Init(); printf("\r\nBH1750 Sensor Init Success!\r\n"); while(1) { float lux = 0; // 读取光照强度 if(Multiple_read_BH1750(&lux) == 0) { printf("\r\nBH1750 Lux: %.2f lx\r\n", lux); } else { printf("\r\nBH1750 Read Error\r\n"); } delay_1ms(500); // 延时500ms再进行下一次读取 } }5.2 编译与下载
代码写好后,按以下步骤操作:
- 编译工程:确保没有语法错误
- 连接硬件:用USB线连接开发板和电脑
- 下载程序:通过调试器将程序下载到开发板
- 打开串口助手:设置波特率(通常是115200)
- 观察输出:应该能看到光照强度的数值
5.3 测试结果分析
正常情况下,串口助手会每隔500ms输出一次光照强度值。你可以用手电筒照射传感器,或者用手遮挡传感器,观察数值的变化:
- 完全黑暗环境:接近0 lx
- 室内正常照明:100-500 lx
- 靠近窗户:1000-5000 lx
- 室外晴天:10000 lx以上
调试技巧:如果读取失败,首先检查硬件连接是否正确,特别是VCC、GND、SCL、SDA这四根线。然后检查I2C地址是否正确(我们用的是0x46)。还可以用逻辑分析仪或示波器查看I2C波形,确保时序正确。
6. 常见问题与解决方案
在实际移植过程中,我遇到过几个典型问题,这里分享给大家:
6.1 读取数据总是失败
现象:程序能运行,但总是返回读取错误。
可能原因和解决:
- I2C地址错误:确认ADDR引脚的连接状态,我们这里悬空使用0x46地址
- 时序问题:检查延时时间是否足够,BH1750需要时间完成测量
- 电源问题:确保3.3V电源稳定,可以用万用表测量一下
- 上拉电阻:虽然开发板内部有上拉,但如果线太长,可能需要外接4.7kΩ上拉电阻
6.2 测量值不稳定
现象:数值跳动比较大。
解决:
- 增加软件滤波:连续读取多次取平均值
- 检查环境光:避免传感器附近有闪烁的光源(如PWM调光的LED)
- 稳定供电:确保电源没有大的纹波
6.3 响应速度慢
现象:每次读取都要等很久。
原因:BH1750在高分辨率模式下测量需要120ms,这是传感器本身的特性。
优化:如果对实时性要求高,可以考虑使用低分辨率模式(命令0x13),测量时间会缩短到16ms,但精度会降低。
7. 实际应用建议
在实际项目中使用BH1750时,我有几个经验分享:
功耗考虑:如果项目是电池供电,记得在不需要测量时让传感器进入断电模式(发送0x00命令),这样可以节省电能。
测量模式选择:
- 连续高分辨率模式(0x10):适合连续监测,精度最高
- 单次高分辨率模式(0x20):适合间歇性测量,测量后自动进入断电模式
- 低分辨率模式(0x13):响应快,适合快速变化的场景
校准问题:BH1750出厂已经校准过,一般不需要用户再校准。但如果对精度要求特别高,可以用标准光源进行对比测试,如果有偏差可以在软件里加一个修正系数。
安装位置:传感器要避免被直接光源照射,最好有半透明的漫射罩,这样测出来的环境光更准确。
这个驱动我在好几个项目里都用过,稳定性还是不错的。最关键的是理解了I2C通信的基本原理和BH1750的工作方式,这样即使换到其他平台,移植起来也会很快。
