51单片机BH1750光强测量系统设计与I2C模拟实现
2026/9/16 17:41:22 网站建设 项目流程

简介:一套基于51单片机光强测量系统的完整工程资料包,面向电子爱好者、大学生及嵌入式入门开发者,主要用于实现环境光照的实时检测与LCD1602液晶显示,适合课程设计、毕业设计或电子竞赛备赛。压缩包内共18个文件,包含3张JPG电路原理图(如光强检测-原理图)、C/A51源代码、HEX烧录文件、UV2工程文件及备份文件等,整体仅671KB,结构清晰便于阅读。通过原理图可理清51单片机、光强传感器(如STC_BH1750)与LCD1602之间的电源、信号连接关系;通过源代码可系统性学习IO口初始化、定时器采样、光强数据换算与显示刷新等关键流程,还能掌握Keil工程编译与下载调试方法。目前已有1456人学习下载,对想快速上手单片机光敏检测项目并深入理解软硬件协同开发的开发者极具参考价值。

1. 51单片机光强测量设计:为什么直接跳过光敏电阻选BH1750

大多数51单片机光强测量课程设计的第一反应,是用光敏电阻搭一个分压电路,再经ADC转换得到电压,最后换算成照度。但这个项目换了一条更干净的路:传感器用BH1750数字光强芯片,光线强度直接以16位数字量通过I²C总线输出,STC89C52不需要依赖ADC外设,只用两个普通IO口模拟I²C时序,就能把环境光照强度以lux为单位显示在LCD1602上。压缩包里同时给出了完整的电路原理图和STC_BH1750工程,对正在做单片机课程设计、或者想把环境光检测模块并入现有嵌入式项目的读者来说,是一套可以直接抄作业的参考实现。后面按传感器驱动、LCD1602显示、原理图布线和调试技巧四个层面拆开讲,每个环节都给出参数选择和踩坑点。

2. BH1750的I²C时序解析与51单片机GPIO模拟实现

2.1 数字光强传感器的内部结构与选型理由

BH1750内部集成光电二极管、集成运算放大器和16位ADC。光线照射光电二极管后产生微弱电流,经运放放大后送入ADC量化,最终通过I²C接口输出16位数据。整个过程不需要外部ADC芯片,也不需要像光敏电阻那样做分压电路标定。

选它替代光敏电阻的核心原因在于,STC89C52RC本身没有片上ADC。如果使用光敏电阻方案,要么外扩ADC0809,要么用RC充放电测频率的办法近似,前者增加硬件复杂度,后者精度差。而BH1750把模拟链路全封装在了传感器内部,51单片机只需要两根IO线模拟I²C,就能拿到直接以lux为单位的数字量。器件功耗也很低,待机模式下电流只有几微安,适合电池供电的光照记录设备。

2.2 引脚定义、从机地址与关键指令集

BH1750的I²C从机地址由ADDR引脚电平决定:

  • ADDR接地,设备地址0x23,写地址0x46,读地址0x47
  • ADDR接VCC,设备地址0x5C,写地址0xB8,读地址0xB9

本项目中ADDR引脚直接接地,所以程序里所有读写操作都以0x46和0x47为地址。传感器支持连续测量和一次性测量两种方式,常用的指令码如下:

指令码功能测量时间分辨率
0x01上电
0x10连续高分辨率模式约120ms1 lx
0x11连续高分辨率模式2约120ms0.5 lx
0x13连续低分辨率模式约16ms4 lx
0x20一次性高分辨率模式约120ms1 lx

这里0x10和0x11的区别在于分辨率换算系数:0x10模式下,寄存器值除以1.2就是实际照度;0x11模式下,寄存器值除以2.4才是实际照度。低分辨率模式节省测量时间,用于快速扫描场景。

2.3 GPIO模拟I²C的代码实现与应答处理

STC89C52没有硬件I²C控制器,所以用P1.0做SCL、P1.1做SDA,完全用延时翻转电平来模拟时序。下面是基础的起始、停止和字节发送函数:

sbit SCL = P1^0; sbit SDA = P1^1; #define I2C_DELAY() { _nop_(); _nop_(); } void I2C_Start(void) { SDA = 1; SCL = 1; I2C_DELAY(); SDA = 0; // SCL高电平期间SDA拉低,产生起始信号 I2C_DELAY(); SCL = 0; } void I2C_Stop(void) { SDA = 0; SCL = 1; I2C_DELAY(); SDA = 1; // SCL高电平期间SDA拉高,产生停止信号 I2C_DELAY(); } void I2C_SendByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { if (dat & 0x80) SDA = 1; else SDA = 0; dat <<= 1; SCL = 1; // 数据稳定后拉高SCL,从机在上升沿采样 I2C_DELAY(); SCL = 0; I2C_DELAY(); } // 释放SDA,等待从机ACK SDA = 1; SCL = 1; I2C_DELAY(); SCL = 0; } bit I2C_WaitAck(void) { bit ack; SDA = 1; // 释放总线,让从机控制SDA SCL = 1; I2C_DELAY(); ack = SDA; // 从机拉低SDA表示ACK,拉高表示NACK SCL = 0; I2C_DELAY(); return ack; }

I2C_Start和I2C_Stop函数的时序要点是:在SCL为高电平期间改变SDA状态,才会被从机识别为起始或停止信号;而在正常数据传输过程中,SDA只能在SCL低电平期间变化。I2C_SendByte里每发送完8位数据后要释放SDA,让从机在第9个时钟周期拉低SDA作为应答,I2C_WaitAck就是读取这个应答位的函数。

2.4 完整读光强函数与lux换算

有了底层时序函数,读取光强的完整流程分两步:先写地址和指令,告诉传感器开始连续高分辨率测量;等待测量完成后,再发起读操作,连续读取两个字节数据。

unsigned int BH1750_GetLux(void) { unsigned char dat_h, dat_l; unsigned int lux; // 第1步:发送写命令,启动连续高分辨率测量 I2C_Start(); I2C_SendByte(0x46); // 写地址,ADDR引脚接地 if (I2C_WaitAck()) return 0xFFFF; I2C_SendByte(0x10); // 连续高分辨率模式,分辨率1lx if (I2C_WaitAck()) return 0xFFFF; I2C_Stop(); delay_ms(180); // 手册标称120ms,留1.5倍余量 // 第2步:读取16位测量结果 I2C_Start(); I2C_SendByte(0x47); // 读地址 if (I2C_WaitAck()) return 0xFFFF; dat_h = I2C_ReadByte(1); // 第一个字节读完回ACK dat_l = I2C_ReadByte(0); // 最后一个字节回NACK,结束读取 I2C_Stop(); lux = (dat_h << 8) | dat_l; return (unsigned int)((float)lux / 1.2); // 高分辨率模式除以1.2 }

I2C_ReadByte函数接收一个ack参数,用来决定读完这个字节后回ACK还是回NACK。BH1750只输出两个字节,所以读第一个字节时回ACK让传感器继续把第二个字节发出来,读第二个字节时必须回NACK。delay_ms等待时间至少要大于测量周期,如果用了连续模式但读取间隔小于120ms,读回来的数据可能是上一次未更新的旧值,这是新手容易忽略的时序问题。

3. LCD1602并行接口驱动:初始化顺序与字符显示程序设计

3.1 并行接口的信号线用途与读写时序

LCD1602并行接口有11根关键信号线:RS、RW、E三根控制线,DB0到DB7八根数据线。RS决定当前传输的是命令还是数据,RS为0时写入的是命令,为1时写入的是字符数据。RW为0表示写,为1表示读。E是使能信号,写操作时在E的下降沿,也就是E从高电平跳变到低电平的瞬间,显示器锁存DB0到DB7线上的内容。

很多51单片机项目里RW直接接地,因为程序只需要往LCD写命令和字符数据,不需要读取状态寄存器。这样一来可以为单片机省下一个IO口,代价是没法读忙标志bit7,必须用固定延时来保证每一条指令的执行时间。下面代码就是这种只写方案。

3.2 初始化序列解析与驱动代码

LCD1602上电后需要按固定顺序发送初始化命令,顺序错了屏幕就可能出现花屏或者只显示方块:

#define LCD_DATA P0 // 数据总线接P0口 sbit LCD_RS = P2^6; sbit LCD_RW = P2^7; sbit LCD_E = P2^5; void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; // 命令模式 LCD_DATA = cmd; LCD_E = 1; _nop_(); _nop_(); // 保持高电平,让LCD采样数据 LCD_E = 0; // 下降沿锁存命令 delay_ms(2); } void LCD_WriteData(unsigned char dat) { LCD_RS = 1; // 数据模式 LCD_DATA = dat; LCD_E = 1; _nop_(); _nop_(); LCD_E = 0; delay_ms(2); } void LCD_Init(void) { delay_ms(50); // 等待LCD上电稳定 LCD_WriteCmd(0x38); // 8位模式、2行显示、5x8点阵 delay_ms(5); LCD_WriteCmd(0x38); // 重复一次确保模式同步 delay_ms(5); LCD_WriteCmd(0x08); // 显示关闭 LCD_WriteCmd(0x01); // 清屏,执行需要约1.64ms LCD_WriteCmd(0x06); // 地址指针自动加1,光标右移 LCD_WriteCmd(0x0C); // 显示开启,光标不显示,不闪烁 }

初始化序列里0x38要发两次,原因是市面上的LCD1602模块大多数基于HD44780控制器或其兼容芯片,上电瞬间内部状态不确定,连续发送两到三次相同的功能设置命令能确保控制器稳定进入8位模式。0x08关闭显示、0x01清屏、0x06设置输入模式、0x0C打开显示,这四条的顺序不能打乱,否则会出现只有背光亮但字符不出来的现象。

3.3 光标定位与数字字符串格式化管理

LCD1602的DDRAM地址映射是固定的,第一行从0x00开始,第二行从0x40开始。写入DDRAM前需要先发地址设置命令,命令的高位固定为1,也就是第一行第一个字符的位置命令是0x80,第二行第一个字符是0xC0:

void LCD_SetCursor(unsigned char row, unsigned char col) { if (row == 0) LCD_WriteCmd(0x80 | col); else LCD_WriteCmd(0xC0 | col); }

显示光强数据时,先把测量值转成字符串,再逐字符写入:

void LCD_DisplayLux(unsigned int lux) { char buf[6]; unsigned char i; sprintf(buf, "%5d", lux); // 固定宽度5,右对齐 LCD_SetCursor(0, 0); LCD_WriteData('L'); LCD_WriteData('u'); LCD_WriteData('x'); LCD_WriteData(':'); LCD_WriteData(' '); LCD_SetCursor(0, 6); for (i = 0; i < 5; i++) { LCD_WriteData(buf[i]); } }

这里用sprintf做格式化,虽然StC89C52的256字节片内RAM看起来比较紧张,但5位数字的格式转换足够用。如果不用标准库想省空间,也可以自己写一个整数转字符串函数。注意LCD1602每个字符地址只能容纳一个ASCII字符,中文不能直接显示,所以标题用Lux而不是光强两个字。

3.4 刷新策略与显示闪烁问题的规避

一个很容易踩的坑是每轮循环里都调用清屏命令0x01再重新显示。清屏会触发LCD内部DFB的填充操作,执行时间超过1.5ms,频繁调用会让显示文字出现肉眼可见的闪烁。正确的做法是只在初始化时清一次屏,后续更新数据时只覆盖变化区域的字符。如果光照值始终在固定位置显示,可以直接定位到该位置后重写5个字符,不需要动其他内容。

另外,LCD_WriteCmd里的固定延时2ms已经覆盖了大多数命令的执行时间。如果把延时去掉,或者缩短到几百微秒,会出现写入偶尔丢失、显示字符错位的问题。因为读忙标志需要RW引脚配合,既然硬件上把RW接地了,延时就必须给足,这是只写模式必须接受的约束。

4. 原理图深度阅读:STC最小系统与BH1750外围电路设计

4.1 工程包文件结构与原理图对应关系

压缩包里的目录结构对应两套产出物:

光强监测/ ├── 光强检测-原理图/02 - 电路原理图 └── 光强检测-工程/STC_BH1750

原理图目录下是电路图源文件,打开后能看到整体电路由三块组成:STC89C52最小系统、BH1750光强采集电路、LCD1602显示接口。工程目录下的STC_BH1750是Keil工程文件。原理图和程序是配套的,引脚定义完全一致,P1.0和P1.1连BH1750,P0口和P2.5到P2.7连LCD1602,直接对照布线就能烧录运行。

4.2 STC89C52最小系统的关键节点

原理图里最小系统看起来就是几个元件,但每个节点都有讲究。晶振回路用11.0592MHz晶振配两个22pF负载电容,这个频率是为了兼容串口波特率,如果后续要加串口调试,9600波特率误差几乎为零。复位电路用10uF电解电容加10k电阻组成上电复位,复位引脚端到地之间接电容、到VCC之间接电阻的方式。电源部分在单片机VCC引脚旁边放一个0.1uF陶瓷电容做去耦,这个电容尽量靠近芯片电源脚,作用是滤除高频噪声。

STC89C52的P0口内部没有上拉电阻,原理图里必须给P0口接一个10k排阻上拉到5V,否则P0输出高电平时实际上处于高阻状态,LCD1602的数据线收不到稳定高电平。这个细节经常被人漏掉,漏掉的结果就是LCD显示乱码或者只有低电平能写进去。

4.3 BH1750外围电路与5V系统电平匹配

BH1750典型工作电压是3.3V,绝对最大额定值3.6V。原理图上一般会用ASM1117-3.3稳压芯片把5V降到3.3V供传感器使用。SCL和SDA两根I²C线是开漏输出,必须外接上拉电阻,常见做法是接两个4.7k电阻到3.3V。因为上拉电平是3.3V,而51单片机的IO口识别高电平的门槛大约2V,所以5V单片机直接连接3.3V传感器的I²C总线时,通信可以正常进行,不需要电平转换芯片。

ADDR引脚在原理图中直接接地,确定了从机地址为0x23,对应程序里的写地址0x46。如果设计中想挂两个BH1750分别测不同方向的光,第二个传感器的ADDR引脚要接VCC,地址变成0x5C,程序里也要对应改成写地址0xB8。

4.4 完整引脚分配表与常见的布线错误

对照原理图整理出的完整引脚分配关系如下:

单片机引脚连接目标信号说明
P0.0-P0.7LCD1602 DB0-DB7数据总线,需上拉排阻
P2.5LCD1602 E使能信号
P2.6LCD1602 RS命令/数据选择
P2.7LCD1602 RW读写选择,原理图接地
P1.0BH1750 SCLI²C时钟线,上拉到3.3V
P1.1BH1750 SDAI²C数据线,上拉到3.3V
VCCLCD1602第2脚5V背光电源,经电阻限流

反过来看几个高频布线错误。第一,LCD1602第3脚V0对地不接电位器,屏上显示效果要不全黑要不全淡,通常这里要接一个10k可调电阻做对比度调节。第二,BH1750的VCC和GND之间只放传感器本身的滤波电容也可以运行,但原理图上应该在电源入口再加一个10uF电解电容做低频滤波。第三,SCL和SDA如果没有上拉电阻,I²C通信会间歇性失败,表现是刚开机能读到数据,运行几分钟后读数变成0xFFFF。

5. 光强数据换算精度调整与显示稳定性的压轴技巧

5.1 通过MTreg寄存器微调测量灵敏度

BH1750除了固定分辨率模式外,还有一个容易被忽略的测量时间寄存器。发送命令0x42可以设置测量时间寄存器的低字节,0x44设置高字节,默认值是0x69,也就是十进制105。修改这个值可以改变传感器的积分时间,实际照度换算公式也要相应修正:

实际照度 = 原始寄存器值 / 1.2 × (MTreg当前值 / 105)

把MTreg从105调到210后,测量时间翻倍到约240ms,分辨率提高一倍,适合弱光环境测量。反过来调到63,测量时间缩短到约72ms,适合快速扫描场景。项目里如果发现读数跳变厉害,我一般先试加大MTreg而不是急着加软件滤波,因为延长积分时间本身就是硬件层面的平均。

调整示例代码如下:

void BH1750_SetMTreg(unsigned char mt) { I2C_Start(); I2C_SendByte(0x46); I2C_WaitAck(); I2C_SendByte(0x42); // MTreg高字节命令 I2C_WaitAck(); I2C_SendByte(mt); // 写入目标值 I2C_WaitAck(); I2C_Stop(); }

注意事项:调整MTreg之后,原先除以1.2的换算系数不再成立,必须按当前MTreg值重新计算。程序里可以定义两个全局变量,一个存MTreg当前值,一个存换算系数,避免散落的魔法数字。

5.2 连续采样滑动平均与局部刷新显示

如果MTreg已经调到210还是觉得读数有波动,常见处理是在软件层做滑动平均。取最近5次测量值求平均,每次测量只移入一个新值、移出一个旧值,开销小且响应不会太迟钝:

unsigned int BH1750_Filter(unsigned int new_val) { unsigned char i; unsigned int sum = 0; static unsigned int buf[5] = {0,0,0,0,0}; static unsigned char idx = 0; buf[idx] = new_val; idx = (idx + 1) % 5; for (i = 0; i < 5; i++) sum += buf[i]; return sum / 5; }

局部刷新的做法在3.4节提到过,这里给出具体实现:第一行固定显示Lux字符,第二行空出来做量程提示。当测量值超过40000 lx时,在第二行写入OverRange提示,否则写入当前量程状态。这样每次更新只重写第二行的几个字符,LCD工作稳定,人眼观感也流畅。最后再提醒一点:BH1750上电后必须先发0x01上电指令,如果省略直接发测量指令,部分批次的芯片不会响应,这也是拿到工程后烧录验证时最常遇到的第一个问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询