前言:本实验用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 完全够用)
缺点:
占用 CPU,CPU 必须一直参与发脉冲
速度比硬件 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. 配置 GPIOGPIO_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 ModeBH_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 0x46void BH1750_Init(void);void BH1750_StartMeasure(void);float BH1750_ReadLux(void);#endifmain.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 i=0; i<100000; i++);}}/**@brief 断言失败函数@param file: 发生错误的文件名指针@param line: 发生错误的行号@note 当库函数检测到输入参数非法时会调用此函数*/void assert_failed(uint8_t *file, uint32_t line){while (1){}}
五、最终效果图:
存在些许误差实属正常。
六、最后总结:
在驱动 BH1750 这类 IIC 接口芯片时,正确的学习顺序一定是:先吃透 IIC 通信时序和原理,再去编写对应的驱动代码。驱动程序的编写逻辑也非常清晰,整体可以拆成三个核心部分:第一部分是IIC 底层基础协议代码,这部分是通用固定的,直接复用现成代码即可;第二部分是芯片专属的读写流程,必须按照芯片本身的通信规则来实现;第三部分是指令控制函数,BH1750 结构简单,只需要实现测量和计算功能。复杂一些的芯片还会包含多种模式配置、数据校验等函数,但本质都是通过发送不同指令来完成对应操作。谢谢大家!
CW32生态社区微信交流群
扫码加入QQ群3群| 610403240
106