CW32L012的BH1750照度传感器实验案例分享
前言本实验用CW32L012作为主控BH1750为照度传感器实现测量环境光的Lux值。一 、BH1750模块介绍BH1750是一款数字型光强度传感器集成芯片。BH1750的内部由光敏二极管、运算放大器、ADC采集、晶振等组成。PD二极管通过光生伏特效应将输入光信号转换成电信号经运算放大电路放大后由ADC采集电压然后通过逻辑电路转换成16位二进制数存储在内部的寄存器中进入光窗的光越强光电流越大电压就越大所以通过电压的大小就可以判断光照大小但是要注意的是电压和光强虽然是一一对应的但不是成正比的所以这个芯片内部是做了线性处理的这也是为什么不直接用光敏二极管而用集成IC的原因。BH1750引出了时钟线和数据线单片机通过I2C协议可以与BH1750模块通讯可以选择BH1750的工作方式也可以将BH1750寄存器的光照度数据提取出来。下附BH1750实物图cw32主板BH1750原理图。BH1750光照传感器模块CW32L012主板BH1750电路工作原理图二、IIC通讯协议介绍IIC总线含SCL 时钟线、SDA 数据线SCL 管控通讯时序SDA 负责双向传数据。通俗理解就是SCL 像红绿灯输出方波脉冲定节拍SDA 高低电平代表 1、0一个时钟周期传 1 位8 位组成 1 字节逐字节完成设备通信。主从架构是单片机为主设备BH1750 为从设备SCL 仅由主机输出标准最大通信 400kHz时钟周期不小于 2.5μs延时越长速率越慢。通信起止规则为先发起始信号SCL 高、SDA 由高变低结束发停止信号SDA 拉高。通讯流程为主机先发7 位器件地址 1 位读写位合成 1 字节选中对应从设备从设备回复应答位表示接收成功之后按字节逐发 / 逐收数据SDA 支持双向收发器件地址用于一条总线挂载多个从设备精准指定通信对象。下附引脚定义。什么是软件IIC答用单片机普通 IO 口手动模拟 SCL、SDA 的高低电平模拟出 I2C 时序。就像你手动掰开关一下高一下低模仿出 I2C 的通讯波形。优点任意 IO 口都能用不受硬件引脚限制移植超级方便代码拿到哪都能用时序完全可控调试简单适合低速设备BH1750、OLED、EEPROM 完全够用缺点占用 CPUCPU 必须一直参与发脉冲速度比硬件 I2C 慢一点高频通信不稳定什么是硬件IIC答单片机内部自带专门的 I2C 外设控制器自动产生时序不用 CPU 管。就像装了自动开关机器你告诉它要发什么它自己完成所有波形。优点不占用 CPU发完数据 CPU 就去干别的速度快、标准、稳定支持高速通信400kHz 以上缺点只能用固定的硬件引脚移植麻烦不同单片机配置不一样调试比软件 I2C 复杂一点为什么我用软件IIC答1.硬件 IIC 只能用单片机指定固定引脚有时候布线不方便、引脚被占用了就没法用。软件 IIC 随便拿两个普通 IO 口当 SCL、SDA想用哪根就哪根接线、画板子都灵活。2.BH1750 对 IIC 时序要求不苛刻软件 IIC 是代码延时模拟高低电平延时想调多少调多少适配任何单片机、任何主频不挑芯片、不挑系统时钟。硬件 IIC 受总线速率、外设寄存器配置影响有时候时序不匹配容易通讯失败、读不出数据、乱码。3.软件 IIC 底层时序代码是纯 IO 操作STM32、CW32、51、 直接复制就能用不用改寄存器配置。硬件 IIC 每个单片机库、寄存器配置都不一样换个板子就要重配麻烦还容易出错。4.软件 IIC 每一步起始、停止、应答、发字节都是肉眼能看懂的代码哪里时序不对一眼就能改。硬件 IIC 是外设自动发时序底层黑盒一旦卡死、不应答、通讯异常很难排查原因。5.BH1750 只需要低速 IIC标准 100k~400kHz 就行软件 IIC 完全跑满富余根本瓶颈不在速度。硬件 IIC 的高速优势在这个传感器上完全浪费。6.很多单片机硬件 IIC 容易出现总线卡死、引脚电平拉不起来、仲裁出错、一直无应答等玄学问题。软件 IIC 没有这些底层 bug稳定耐用做项目少踩坑。三、BH1750通讯过程第一步发送上电指令。指令固定为0x01。通信流程和发测量命令一致只是把指令换成上电指令 0x01走一遍标准 IIC 写流程即可。第二步发送测量命令。IIC 流程起始信号 ST → 从机地址 写位 → 等待从机应答 → 发送测量指令 0x10 → 再次应答 → 停止信号 SP。第三步等待测量完成。程序里加延时确保采样完成、读到有效新数据。第四步读取光照数据。先发 IIC 起始信号 ST。发送从机地址 读位等待从机应答。单片机把 SDA 由输出切换为输入模式先接收高 8 位数据。主机发送应答继续接收低 8 位数据。收完两个字节后不应答通知从机不再接收。发送停止信号 SP结束本次 IIC 通信。第五步读出 2 个字节先高 8 位、后低 8 位拼接成 16 位原始寄存器值。固定公式为光照强度 (lx) (16 位合并值 × 分辨率) ÷ 1.2四、BH1750照度传感器在Keil中的代码实现BH1750.c示例 #include BH1750.h #include cw32l012.h // 根据实际型号选择 #include cw32l012_gpio.h #include cw32l012_sysctrl.h // --- 引脚配置 PB06 和 PB07 --- #define SCL_PIN GPIO_PIN_6 #define SDA_PIN GPIO_PIN_7 #define GPIO_PORT CW_GPIOB // ---CW32L012 的宏定义 --- // 直接使用 1 和 0避免 GPIO_PIN_SET 报错 #define SCL_H GPIO_WritePin(CW_GPIOB, GPIO_PIN_6, 1) #define SCL_L GPIO_WritePin(CW_GPIOB, GPIO_PIN_6, 0) #define SDA_H GPIO_WritePin(CW_GPIOB, GPIO_PIN_7, 1) #define SDA_L GPIO_WritePin(CW_GPIOB, GPIO_PIN_7, 0) #define READ_SDA GPIO_ReadPin(CW_GPIOB, GPIO_PIN_7) void BH1750_Delay(void) { uint16_t i 20; while(i--); } void BH_Start(void) { SDA_H; SCL_H; BH1750_Delay(); SDA_L; BH1750_Delay(); SCL_L; BH1750_Delay(); } void BH_Stop(void) { SDA_L; SCL_H; BH1750_Delay(); SDA_H; BH1750_Delay(); } void BH_SendByte(uint8_t byte) { for (uint8_t i 0; i 8; i) { if (byte 0x80) SDA_H; else SDA_L; BH1750_Delay(); SCL_H; BH1750_Delay(); SCL_L; byte 1; } SDA_H; SCL_H; BH1750_Delay(); SCL_L; // 忽略 ACK } uint8_t BH_ReadByte(uint8_t ack) { uint8_t byte 0; SDA_H; for (uint8_t i 0; i 8; i) { SCL_H; BH1750_Delay(); byte 1; if (READ_SDA) byte | 0x01; SCL_L; BH1750_Delay(); } if (ack) SDA_L; else SDA_H; SCL_H; BH1750_Delay(); SCL_L; return byte; } void BH1750_Init(void) { // 1. 开启 GPIOB 时钟 (注意是 SYSCTRL) __SYSCTRL_GPIOB_CLK_ENABLE(); // 2. 配置 GPIO GPIO_InitTypeDef GPIO_InitStructure {0}; GPIO_InitStructure.Pins GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStructure.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 // 如果没有外部上拉电阻建议加上内部上拉 // GPIO_InitStructure.IT GPIO_IT_NONE; GPIO_Init(CW_GPIOB, GPIO_InitStructure); // 默认拉高 SCL_H; SDA_H; // 上电序列 BH_Start(); BH_SendByte(BH1750_ADDR); BH_SendByte(0x01); BH_Stop(); } void BH1750_StartMeasure(void) { BH_Start(); BH_SendByte(BH1750_ADDR); BH_SendByte(0x10); // H-Resolution Mode BH_Stop(); } float BH1750_ReadLux(void) { uint16_t raw; BH_Start(); BH_SendByte(BH1750_ADDR | 0x01); raw BH_ReadByte(1); raw (raw 8) | BH_ReadByte(0); BH_Stop(); float lux (float)raw / 1.2f; if (lux 5.0f) return 0.0f; return (float)raw / 1.2f 250.0f; } BH1750.h示例 #ifndef __BH1750_H #define __BH1750_H #include cw32l012.h // 或者是 cw32l012.h根据你的工程决定 #define BH1750_ADDR 0x46 void BH1750_Init(void); void BH1750_StartMeasure(void); float BH1750_ReadLux(void); #endif main.c示例 #include cw32l012.h // 根据实际型号选择 #include OLED.h #include BH1750.h #include stdio.h // CW32 自定义简易延时 void Delay_Simple(uint32_t ms) { uint32_t i ms * 3000; while(i--); } int main(void) { char str[20]; float lux; // 1. CW32 系统时钟初始化 // 默认通常为内部源确保开启了相关外设时钟 // 2. 初始化外设 OLED_Init(); BH1750_Init(); BH1750_StartMeasure(); OLED_Clear(); OLED_Printf(0, 0, OLED_6X8, System Init OK); OLED_Update(); while (1) { // 2. 读取数据 float lux_val BH1750_ReadLux(); // 3. 动态数据显示 OLED_Printf(0, 50, OLED_6X8, Lux: %.2f , lux_val); // 4. 刷新屏幕必须调用否则看不见变化 OLED_Update(); // 适当延时 for(volatile int i0; i100000; i); } } /** brief 断言失败函数 param file: 发生错误的文件名指针 param line: 发生错误的行号 note 当库函数检测到输入参数非法时会调用此函数 */ void assert_failed(uint8_t *file, uint32_t line) { while (1) { } }五、最终效果图存在些许误差实属正常。六、最后总结在驱动 BH1750 这类 IIC 接口芯片时正确的学习顺序一定是先吃透 IIC 通信时序和原理再去编写对应的驱动代码。驱动程序的编写逻辑也非常清晰整体可以拆成三个核心部分第一部分是IIC 底层基础协议代码这部分是通用固定的直接复用现成代码即可第二部分是芯片专属的读写流程必须按照芯片本身的通信规则来实现第三部分是指令控制函数BH1750 结构简单只需要实现测量和计算功能。复杂一些的芯片还会包含多种模式配置、数据校验等函数但本质都是通过发送不同指令来完成对应操作。谢谢大家