拓冰建站拓冰建站
首页 / 资讯中心 / 正文

51单片机BH1750光强测量系统设计与I2C模拟实现

简介一套基于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读地址0x47ADDR接VCC设备地址0x5C写地址0xB8读地址0xB9本项目中ADDR引脚直接接地所以程序里所有读写操作都以0x46和0x47为地址。传感器支持连续测量和一次性测量两种方式常用的指令码如下指令码功能测量时间分辨率0x01上电无无0x10连续高分辨率模式约120ms1 lx0x11连续高分辨率模式2约120ms0.5 lx0x13连续低分辨率模式约16ms4 lx0x20一次性高分辨率模式约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第二行第一个字符是0xC0void 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连BH1750P0口和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.3VP1.1BH1750 SDAI²C数据线上拉到3.3VVCCLCD1602第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上电指令如果省略直接发测量指令部分批次的芯片不会响应这也是拿到工程后烧录验证时最常遇到的第一个问题。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门