光敏电阻环境感知系统是蓝桥杯单片机竞赛中的经典题型,它考察选手对模拟信号采集、数字信号处理和动态显示的综合运用能力。这个项目的核心在于通过光敏电阻感知环境光照强度,并将实时数据以直观的数字形式呈现在数码管上。我当年第一次做这个项目时,最让我兴奋的就是用手电筒照射光敏电阻时,看到数码管数值跟着光线变化而跳动的瞬间。
这个系统主要包含三个关键技术点:首先是光敏电阻的模拟信号采集,它需要配合PCF8591这类模数转换芯片;其次是I2C通信协议的实现,这是连接单片机与外围器件的重要桥梁;最后是数码管的动态扫描显示,需要处理好刷新频率与人眼视觉暂留效应的关系。这三个环节环环相扣,任何一个环节出问题都会导致整个系统无法正常工作。
光敏电阻的型号选择很关键,我推荐使用GL5528这类通用型光敏电阻,它的阻值范围在黑暗环境下约1MΩ,强光下约10KΩ,响应曲线比较平缓,适合教学使用。在实际连接时,需要给它串联一个10KΩ的固定电阻组成分压电路,这样就能将电阻变化转化为电压变化。
PCF8591是飞利浦推出的8位AD/DA转换芯片,它内置4个模拟输入通道和1个模拟输出通道。我特别喜欢它的I2C接口设计,只需要两根线(SCL和SDA)就能完成通信,大大简化了布线复杂度。需要注意的是,它的I2C设备地址是固定的0x90(写)和0x91(读),这个地址在代码中会直接用到。
在面包板上搭建电路时,最容易出错的是I2C总线的上拉电阻。根据我的经验,SCL和SDA线上都需要接4.7KΩ的上拉电阻到VCC,否则通信时会出现波形畸变。有一次调试时我忘了接上拉电阻,结果读取的数据全是乱码,排查了好久才发现这个问题。
数码管部分建议使用共阳型四位一体数码管,它的段选信号通过74HC138译码器控制,位选信号可以直接用单片机的IO口驱动。记得在每个段选线上串联100Ω的限流电阻,防止电流过大烧坏LED。我见过有同学忘记加限流电阻,结果调试时数码管越来越暗,最后完全熄灭。
I2C协议的时序要求非常严格,下面是经过实战验证的代码实现要点:
c复制void IIC_Start(void) {
SDA = 1; // 先拉高数据线
SCL = 1; // 再拉高时钟线
IIC_Delay(DELAY_TIME); // 保持一段时间
SDA = 0; // 在时钟高电平时拉低数据线
IIC_Delay(DELAY_TIME);
SCL = 0; // 最后拉低时钟线
}
这个启动序列看似简单,但每个步骤的顺序和延时都不能错。我在初期调试时经常遇到通信失败的情况,后来用逻辑分析仪抓取波形才发现是延时时间不够。建议DELAY_TIME设置在5-10个_nop_()之间,具体值要根据单片机主频调整。
接收数据时有个细节需要注意:PCF8591在发送完一个字节后会自动释放SDA线,所以读取时要先左移数据再判断SDA状态,否则会丢失最高位:
c复制da <<= 1; // 先左移
if(SDA) da |= 1; // 再判断
直接从PCF8591读取AD值会有一定的波动,我总结出两个优化方法:一是连续读取两次,第一次的值通常不太准确;二是加入软件滤波算法。下面是改进后的读取函数:
c复制unsigned char ad_read_filter(unsigned char ch){
unsigned char temp, sum=0;
for(int i=0; i<5; i++){ // 采样5次
temp = ad_read(ch);
sum += temp;
IIC_Delay(10);
}
return sum/5; // 取平均值
}
对于光照强度的标定,我建议先用手机的光强检测APP获取几个基准点,比如室内正常光照约200lux,台灯直射约2000lux,然后记录下对应的AD值,建立简单的线性映射关系。
四位一体数码管实际上是8个LED的集合体,要显示不同数字就需要快速切换位选信号。根据人眼视觉暂留特性,刷新频率需要保持在50Hz以上,也就是每个数码管的显示时间不超过5ms。我的做法是使用定时器中断,每1ms刷新一位:
c复制void timer_0() interrupt 1 {
TH0 = (65536-1000)/256; // 1ms定时
TL0 = (65536-1000)%256;
digital(); // 调用数码管刷新函数
}
为了平滑显示变化,我建立了显示缓冲区dspbuf[8],主程序只需要更新这个缓冲区的内容,显示刷新由中断服务程序自动完成。这种设计避免了直接操作硬件带来的闪烁问题:
c复制void re_dsp(uchar *arr){
for(int i=0;i<8;i++){
dspbuf[i] = arr[i]; // 批量更新显示内容
}
}
在显示光照强度时,我做了数值归一化处理,将0-255的AD值转换为0-500的显示范围,这样更符合人们对光照强度的直观感受:
c复制cache_light = (float)light/255*500; // 线性映射
dsp[0] = cache_light/100 + 10; // 百位数带小数点
dsp[1] = cache_light%100/10; // 十位数
dsp[2] = cache_light%10; // 个位数
在调试过程中,最常遇到的问题是I2C通信失败。我总结了一个排查清单:
数码管显示异常时,首先要确认共阳/共阴类型是否匹配,然后检查段选和位选信号是否正常。可以用万用表测量每个引脚的电平变化,或者直接给固定值测试每段LED是否都能点亮。
在实际环境中,光敏电阻容易受到各种干扰。我采用的方法包括:
c复制#define FILTER_LEN 8
unsigned char filter_buf[FILTER_LEN];
unsigned char filter_index = 0;
unsigned char moving_average(unsigned char new_val){
filter_buf[filter_index++] = new_val;
if(filter_index >= FILTER_LEN) filter_index = 0;
unsigned int sum = 0;
for(int i=0; i<FILTER_LEN; i++){
sum += filter_buf[i];
}
return sum/FILTER_LEN;
}
在蓝桥杯比赛中,这个基础功能往往需要与其他模块配合。比如可以增加光强阈值报警功能,当环境光低于某个值时点亮LED警示灯:
c复制if(light < LIGHT_THRESHOLD){
hc138(2, 0x00); // 点亮所有LED
} else {
hc138(2, 0xff); // 关闭LED
}
还可以将光强数据通过串口发送到上位机,用曲线图展示光照变化趋势。我常用的串口初始化配置如下:
c复制void uart_init(){
SCON = 0x50; // 模式1,允许接收
AUXR |= 0x40; // 定时器1时钟为Fosc
AUXR &= 0xFE; // 串口1选择定时器1为波特率发生器
TMOD &= 0x0F; // 清除定时器1模式位
TMOD |= 0x20; // 设定定时器1为8位自动重装方式
TH1 = 0xFD; // 波特率9600
TL1 = 0xFD;
TR1 = 1; // 启动定时器1
}
在实际比赛中,建议将各个功能模块封装成独立的.c和.h文件,比如iic.c、pcf8591.c、digital_tube.c等,这样既方便调试也利于代码复用。我见过有选手把所有代码都写在main.c里,结果调试时手忙脚乱,一个小小的改动就可能引发连锁问题。