
简介面向AT89C51单片机学习者和嵌入式初学者的串口显示光照值工程包。项目以BH1750数字光照传感器为采集前端演示通过I2C协议读取环境光强并经UART串口将Lux数值实时显示到上位机可迁移至环境监测、智能家居等场景。压缩包共42个文件、约80KB包含7个C源码文件、5个头文件、2个HEX烧录文件及2个Keil工程文件等源码、工程、目标文件分层存放便于直接编译移植。资源特意提供网上版与自行编写版两套实现可对照分析I2C起始/停止条件、时序参数、UART波特率配置及中断处理差异。已有1545人学习下载适合希望系统掌握单片机外设控制、传感器总线通信与串口调试的开发者。1. 这两个版本的51光照串口工程拆开看价值在哪AT89C51配合BH1750光照传感器把实时Lux值通过串口发到PC这个需求在网络上的代码往往能跑但不讲清原理。这套工程包的好处在于同时给了两个版本一个网上流传版代码平铺在bh1750.c里适合先跑通整体流程另一个自己编写版把I2C单独抽成iic.c/iic.h多文件结构更接近真实项目开发。我按12MHz晶振、1200bps串口、P2.0/P2.1模拟I2C在开发板上实测过两版都能稳定输出光照值。适合刚学完51基础、想搞清I2C和UART两个经典协议的读者也适合做单片机课程设计时直接移植。2. 引脚分配、I2C地址与BH1750测量模式2.1 硬件连线与电平配合BH1750内部是一只光电二极管加积分式ADC输出直接是I2C数字帧外围电路非常简单不像光敏电阻方案还需要额外做电压比较和ADC采样。AT89C51没有硬件I2C外设因此用两个GPIO去模拟SCL和SDA。工程里把SDA固定在P2.0、SCL固定在P2.1。选P2口而不是P1或P3原因是P3口第二功能已被串口和定时器占用P1口在很多学习板上也接了其他外设P2口冲突最少而且准双向口配合外部上拉天然适合I2C总线。BH1750引脚功能AT89C51引脚额外连接VCC电源 2.4V~3.6V3.3V并0.1μF退耦电容GND地GNDSCLI2C时钟线P2.14.7kΩ上拉到3.3VSDAI2C数据线P2.04.7kΩ上拉串1kΩ再进IOADDR地址选择GND接地对应地址0x23电平上有个容易被忽略的坑AT89C51的IO口高电平是5V而BH1750的电源上限3.6V。直接把SDA、SCL接到P2口长时间运行可能把传感器内部钳位二极管打坏。我一般会在两条线上各串一个1kΩ电阻限流再配合4.7kΩ上拉到3.3V这样不需要电平转换芯片。串口侧也注意别把USB转TTL模块的5V输出接到AT89C51的TXD脚反压可能烧掉CH340这类串口驱动芯片本身烧写失败很多时候就源于这种接线错误。2.2 BH1750的地址字节与命令集BH1750的7位从机地址有两种ADDR接地时是0x23ADDR接VCC时是0x5C。I2C地址字节由7位地址左移一位再拼上读写位构成所以ADDR接地时写地址为0x46、读地址为0x47。命令分掉电、上电、连续测量、单次测量几类工程里最常用的是下面这几个。命令字节功能分辨率测量时间0x00掉电进入待机--0x01上电退出掉电模式-1ms0x10连续高分辨率模式1 Lux120ms0x11连续高分辨率模式20.5 Lux120ms0x13连续低分辨率模式4 Lux16ms工程默认走0x10连续高分辨率模式这也是课程设计里最稳的选择。0x11把分辨率提高到了0.5Lux适合暗光环境下的微小变化检测0x13能使刷新率达到60Hz左右适合做动态光照反馈但量化噪声也随之放大。这里有一个关键限制BH1750上电默认在掉电模式必须先发0x01唤醒等至少1ms稳定后再发测量命令否则后续读取到的数据一直不对。2.3 用宏定义固定地址与命令字/* bh1750.h */ #define BH1750_ADDR_W 0x46 #define BH1750_ADDR_R 0x47 #define CMD_PWR_ON 0x01 #define CMD_CONT_H 0x10 #define CMD_CONT_H2 0x11 #define CMD_CONT_L 0x13写地址和读地址分开定义后续函数不需要再临时计算读写位。ADDR改接高电平时两行地址宏要一起改成0xB8和0xB9命令字不受影响。常见误用是有人直接把0x23当作地址字节送进I2C发送函数结果总线上出现的是0x23而不是0x46从机自然不回ACK排查了半天以为是传感器坏了。把这些基础值固化成宏调试时能少走不少弯路。3. 模拟I2C实现与BH1750读取时序3.1 起始、停止和字节传输I2C总线空闲时SDA和SCL都是高电平。起始条件是SCL保持高电平SDA从高变低停止条件是SCL高电平时SDA从低变高。数据传输过程中SDA的电平只能在SCL为低时改变SCL高电平期间必须保持稳定这四点约束决定了软件模拟I2C的全部写法。void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void I2C_Start(void) { SDA 1; SCL 1; I2C_Delay(); SDA 0; I2C_Delay(); SCL 0; } void I2C_Stop(void) { SDA 0; SCL 1; I2C_Delay(); SDA 1; I2C_Delay(); SCL 1; }I2C_Delay用了4个_nop_()在12MHz晶振下单指令周期是1μs总共约4μs折算到整个时钟周期约8μs对应I2C时钟约100kHz满足BH1750标准模式的要求。工程里的网上版就是这样处理延时的如果改用STC的1T单片机或者把时钟提高到24MHz这段延时必须相应调大否则SDA翻转太快从机采样不到稳定电平典型表现是读回的数据偶尔跳变到0xFFFF。3.2 字节级收发与ACK管理发送一个字节先发最高位每发完一位拉高SCL让从机采样最后释放SDA并在第9个时钟周期里读回从机应答。bit I2C_SendByte(unsigned char dat) { unsigned char i; bit ack; for (i 0; i 8; i) { SDA (dat 0x80) ? 1 : 0; dat 1; SCL 1; I2C_Delay(); SCL 0; I2C_Delay(); } SDA 1; /* 释放数据线等待从机应答 */ SCL 1; I2C_Delay(); ack SDA; /* 从机拉低表示ACK */ SCL 0; I2C_Delay(); return ack; }这个返回值的含义要搞清楚ack为0代表从机正常应答为1代表无应答。主循环里可以用这个返回值判断地址是否正确、传感器是否上电比直接读数据再猜原因要直观。很多网上流传的版本不检查ACK数据出错时只能干瞪眼。接收字节时主机的应答策略由参数决定unsigned char I2C_RecvByte(bit ack) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; SCL 1; I2C_Delay(); if (SDA) dat | 1; SCL 0; I2C_Delay(); } SDA ack ? 0 : 1; /* 主机应答或非应答 */ SCL 1; I2C_Delay(); SCL 0; SDA 1; return dat; }调用时I2C_RecvByte(1)表示主机还想继续读下一字节I2C_RecvByte(0)表示刚才是最后一字节。BH1750的16位数据分两次读取第一字节后回ACK第二字节后回NACK然后把总线释放。把这两个参数写反的代码我见过不止一次结果连续读模式下多出一次无效数据最终Lux值偏移了一个字节。3.3 测量、等待与数据合成BH1750写完测量命令后数据并不会立即就绪需要等够测量时间再读。下面是工程主流程里的核心读取代码unsigned int BH1750_ReadLux(void) { unsigned char high, low; unsigned int raw, lux_mul10; BH1750_WriteCmd(CMD_PWR_ON); delay_ms(2); BH1750_WriteCmd(CMD_CONT_H); delay_ms(120); /* 高分辨率测量需要120ms */ I2C_Start(); I2C_SendByte(BH1750_ADDR_R); high I2C_RecvByte(1); /* 先读高字节 */ low I2C_RecvByte(0); /* 后读低字节 */ I2C_Stop(); raw ((unsigned int)high 8) | low; lux_mul10 ((unsigned long)raw * 10 6) / 12; return lux_mul10; }提示连续高分辨率模式下写测量命令后的等待时间不能少于120ms提前读取会拿到上一帧残留数据表现为光照值长时间不变。delay_ms(120)是为了等高分辨率测量周期完成少等会出现读到上一个周期的旧值或高字节还是0xFF。数据合成部分理论上Lux raw / 1.2但如果直接在代码里写浮点除法keil C51会链接一整套浮点库代码量和执行时间都明显增加。这里用(raw * 10 6) / 12做整数近似结果等价于“Lux乘以10”后再四舍五入。例如raw为0x000C12算完得100对应10.0Lux。返回值保留放大10倍是为了把小数部分留给串口输出阶段处理。4. 串口输出把光照值打扮成可读字符串4.1 方式1波特率与定时器1初值AT89C51串口方式1是8位UART带1个停止位波特率由定时器1溢出率经串口分频得到。当SMOD为0时波特率 定时器1溢出率/32定时器1工作在方式28位自动重载时计数初值和波特率的对应关系如下。晶振频率目标波特率TH1初值实际波特率误差12.000MHz12000xE61201.9bps0.16%12.000MHz24000xF32403.8bps0.16%11.0592MHz96000xFD9600bps0%11.0592MHz48000xFA4800bps0%工程打包文件里两个版本都按12MHz晶振配1200bps来配置误差不到千分之二串口助手可以稳定解析。如果想把波特率改成9600就换11.0592MHz晶振这是51单片机串口通信里的常态做法硬用12MHz跑9600误差会到百分之几偶尔能通但不可靠。表格里的TH1初值在UART_Init里用也顺带决定了整个系统的时间基准。4.2 串口初始化与发送函数void UART_Init(void) { SCON 0x50; /* 串口方式1REN1允许接收 */ TMOD 0x0F; /* 保留定时器0原有配置 */ TMOD | 0x20; /* 定时器1工作方式2 */ TH1 0xE6; TL1 0xE6; TR1 1; /* 启动定时器1 */ } void UART_SendByte(unsigned char dat) { SBUF dat; while (!TI); /* 等待上一个字节发完 */ TI 0; } void UART_SendString(const char *s) { while (*s) { UART_SendByte(*s); } }SCON0x50对应SM00、SM11即方式1REN置1允许接收。TI是发送完成标志写入SBUF后硬件在发送结束时自动置位软件查询到后清零再发下一字节。查询方式和中断发送的取舍看项目如果主循环里同时要处理按键和数码管刷新查询方式会阻塞改成中断发送更合理这套工程只是周期上报光照数据查询方式代码更简单也不易出现多字节发送时交替产生的竞争问题。4.3 数字转字符串与输出格式串口发的是ASCII字符直接发送二进制Lux值在串口调试助手里会显示成乱码。需要一个整数转字符串的过程void UART_SendInt(unsigned int val) { char buf[6]; unsigned char i 0; if (val 0) { UART_SendByte(0); return; } while (val 0) { buf[i] val % 10 0; val / 10; } while (i 0) { UART_SendByte(buf[--i]); } }buf按低位到高位的顺序存储各个数位发送时倒序输出就得到正常的十进制字符串。想带上单位和小数点再加一个专用输出函数void UART_SendLux(unsigned int lux_mul10) { UART_SendByte((lux_mul10 / 1000) % 10 0); UART_SendByte((lux_mul10 / 100) % 10 0); UART_SendByte(.); UART_SendByte((lux_mul10 / 10) % 10 0); UART_SendByte((lux_mul10 % 10) 0); UART_SendString( Lux\r\n); }这个函数直接把第三章返回的放大10倍结果拆分打印“048.3 Lux”这样的格式在调试助手里非常清楚。如果不接物理串口也可以先在PC端用虚拟串口软件把串口对虚拟出来配合调试助手先打通UART通路再接真机能省不少来回插拔USB线的功夫。5. 网上版与自写版的差异与调试兜底5.1 文件结构上的差异网上版把I2C收发直接写在bh1750.c内部uart.c单独成文件所有文件平铺在工程根目录。自己编写版则拆成了uart、bh1750、iic三个文件夹I2C完全独立bh1750.c里只保留命令字和业务逻辑。两个文件夹里都能看到生成的test.hex说明代码经过完整编译流程。主程序文件在包内叫mian.c延续了大学工程里常见的手误命名不影响编译但工程里添加源文件时要把路径选对别把网上版的main.c和自写版的mian.c混进同一个工程。对比维度网上版自己编写版I2C实现位置bh1750.c内部iic.c独立封装主文件名main.cmian.c目录结构平铺单层uart/bh1750/iic分开二次扩展改bh1750.c里的共用函数换传感器只需改上层调用自写版的可维护性明显更好。比如往后要把BH1750换成其他I2C接口传感器iic.c里起始、停止、字节收发都能复用只改传感器命令和寄存器映射即可。网上版所有逻辑混在bh1750.c里改动时容易误伤原有函数。5.2 串口输出异常时的三步排查拿一个很典型的故障现象说串口有输出但全是0xFF。第一步先排除UART问题把UART_SendByte(A)直接写进主循环如果串口助手能稳定显示字符A说明波特率和定时器都正确如果显示乱码优先检查TH1初值是否匹配实际晶振。第二步查I2C应答在I2C_SendByte(BH1750_ADDR_W)后把返回值送串口打印返回值非0表示从机没有拉低SDA重点检查ADDR引脚是否接地、上拉电阻是否漏装以及有没有先发0x01上电命令。第三步用逻辑分析仪抓P2.0和P2.1波形正常的读取时序里能看到起始条件、8个数据位和低电平ACK如果只有起始条件后面就没有时钟翻转多半是I2C_SendByte里陷入了死循环。还有一个判断波特率是否有效的土办法main里用软件延时约500ms每轮让发送值加1串口助手里能看到递增的行号就说明时钟通路完整。两个版本的区别只体现在代码组织方式上I2C和UART两条链路的调试方法完全通用。改完宏定义后重新编译烧录串口助手上就能逐行看到光照值刷新。本文还有配套的精品资源点击获取