光敏电阻测试数码管显示:51单片机ADC0832采样与动态扫描实现
2026/9/14 20:24:31 网站建设 项目流程

简介:面向51单片机入门者与嵌入式开发者的实战源码包,围绕光敏电阻阻值随光照变化这一物理特性,演示如何利用ADC将模拟电压转为数字量,并通过数码管动态刷新实时显示光照强度,适用于环境光检测、智能家居光照采集等场景。压缩包共收录23个文件,主要以C源码与对应头文件构成程序逻辑,辅以Keil工程文件、编译中间文件、烧录文件及备份文件,便于读者对照源码、查看编译过程或直接下载测试。文件总数不多但功能链完整,从初始化、ADC采样到数码管驱动均有明确分工。已有417人学习下载,资源体积仅36KB,适合作为单片机外设驱动与模拟量采集的入门实验参考,也可为后续扩展光控电路或光照记录项目提供基础代码。

1. 光敏电阻测试与数码管显示的硬件连接基础

光敏电阻测试数码管显示这个项目,本质上是把光强度变化转化为数字量,再通过数码管把结果呈现出来。很多初学者第一次接触这个题目,会误以为难点在数码管驱动,实际上真正决定测量精度的是光敏电阻的分压电路设计和 ADC 采样值的换算逻辑。51 单片机内部没有 ADC,所以常见做法是外接 PCF8591 或者 ADC0832 这类并行或串行接口的转换芯片;如果手头没有专用 ADC,也可以用 RC 充放电法配合定时器测量电阻值,虽然线性度差一些,但胜在电路极简。

这个项目的实用场景很广:环境光检测、循迹小车的光电传感器标定、智能窗帘的光强阈值判断,都能复用同一套代码结构。适合的人群从准备课程设计的在校学生,到需要快速验证光敏探头是否正常的硬件工程师都算——前者需要跑通流程,后者需要知道误差从哪来、怎么消除。我下面给出的方案是基于 ADC0832 串行 ADC 加四位数码管动态扫描的常见做法,接线少、代码量可控,也方便你移植到其他 51 内核芯片上。

在动笔写代码之前,先把硬件链路拆清楚:光敏电阻和一个固定电阻串联分压,分压点送入 ADC0832 的 CH0 通道,ADC 转换结果通过 CLK、DO、DI 三根线跟单片机通信,最后把处理后的数据送到 P0 口驱动数码管的段码,P2 口的低四位做位选。这套架构是 51 单片机硬件设计的标准套路,无论你用 STC89C52 还是 AT89S52,引脚定义基本一致。

2. 光敏电阻分压电路与 ADC0832 采样的参数设计

2.1.1 分压电阻选型与测量范围标定

光敏电阻的阻值随光照变化范围很大,暗态可达 MΩ 级,强光下可能只有几百欧。直接把这个电阻接入 ADC 分压电路,输出电压变化会严重非线性。我一般会在 5V 供电下选用 10kΩ 固定电阻作为下分压电阻,光敏电阻接 VCC,分压点输出电压 Vout = VCC * R_fixed / (R_light + R_fixed)。当环境光从暗到亮变化时,R_light 减小,Vout 升高,ADC 读数增大,这个方向符合直觉。

分压电阻的取值不能随意拍脑袋。以 5V 供电为例,如果目标测量范围是 100lx 到 10000lx,查光敏电阻的 datasheet 会发现这区间阻值大约从 50kΩ 变到 1kΩ。用 10kΩ 固定电阻时,输出范围是 5V * 10 / (50 + 10) = 0.83V 到 5V * 10 / (1 + 10) = 4.55V,正好落进 ADC0832 的 0~5V 输入范围,而且 8 位分辨率的每个 LSB 对应约 20mV,在这个电压区间内能区分出约 180 个有效等级,对显示光强百分比来说足够了。

不想做模拟计算的话,直接实测:用万用表测光敏电阻在你要的最低亮度和最高亮度下的阻值,然后取几何平均值作为固定电阻值。比如暗态 80kΩ、亮态 2kΩ,几何平均约 12.6kΩ,取标准值 10kΩ 或 15kΩ 都可以。注意固定电阻的精度建议选 1% 金属膜电阻,温度系数比碳膜电阻稳定得多,后级标定会少很多麻烦。

2.1.2 ADC0832 的接线与时序初始化

ADC0832 是 8 位逐次逼近型 ADC,支持两路单端输入和一路差分输入,最高时钟频率约 500kHz。跟 51 的接口只有三根线:CLK(时钟)、DO(数据输出)、DI(数据输入)。由于 DO 和 DI 在实际电路中经常短接成一根数据线,时序上需要处理半双工通信的切换。

连接方式可以固定为:ADC0832 的 CS 接 P1.0,CLK 接 P1.1,DI 接 P1.2,DO 接 P1.3。我习惯把 DI 和 DO 分开接,这样读时序更清晰,代价是多个引脚;如果你要省引脚,可以把 DO 和 DI 并在一起接 P1.2,代码里先写后读,注意方向切换时先拉低时钟再切换引脚模式。下面是 ADC0832 读取单通道 CH0 的常用代码,带注释说明每个时序步骤:

// 51 读取 ADC0832 通道 0,单端输入模式 sbit ADC_CS = P1^0; sbit ADC_CLK = P1^1; sbit ADC_DI = P1^2; sbit ADC_DO = P1^3; unsigned char read_adc0832(unsigned char channel) { unsigned char i, dat = 0; ADC_CS = 1; ADC_CLK = 0; ADC_CS = 0; // 拉低片选,启动转换 // 起始位 + 通道选择位,共 3 个时钟周期 ADC_DI = 1; ADC_CLK = 1; ADC_CLK = 0; // 起始位 S ADC_DI = 1; ADC_CLK = 1; ADC_CLK = 0; // 通道选择 D0 = 1 ADC_DI = channel; ADC_CLK = 1; ADC_CLK = 0; // 单端模式选择 D1 ADC_DI = 1; // 释放数据线 for (i = 0; i < 8; i++) { // 读取 8 位转换结果 ADC_CLK = 1; ADC_CLK = 0; dat <<= 1; if (ADC_DO) dat |= 0x01; } ADC_CS = 1; // 结束转换 return dat; }

片选拉低后的前三个时钟周期是配置阶段,单片机把起始位和通道号按顺序送给 ADC;之后 ADC 在后续 8 个时钟上沿逐个输出转换结果的最高位到最低位。你如果发现连续两次读取同一光照下的值波动超过 ±3 LSB,优先检查 CLK 高电平时间是否过短,51 在 12MHz 晶振下一条ADC_CLK = 1; ADC_CLK = 0;至少需要 2μs,不要用循环嵌套去延时,直接顺序写高低电平即可。

ADC0832 还要注意一点:它没有内部参考电压源,VREF 脚必须接稳定的基准电压。很多人直接接 VCC,但 VCC 上如果有数码管动态扫描的电流毛刺,参考电压会跟着抖。更稳妥的做法是 VREF 接一个 5.1V 稳压管并联 100μF 电容,或者用 TL431 搭 2.5V 基准并相应调整分压电阻,这样光敏电阻测试读数长期漂移会小很多。对于课程设计而言直接接 VCC 通常能接受,但你要知道这个误差源存在。

2.2 光敏电阻测试的多次采样与数字滤波

8 位 ADC 的单次采样结果受环境光工频干扰、光敏电阻自身噪声影响,读数跳动少则 ±2 个 LSB,多则 ±8 个 LSB。为了拿到稳定的显示值,我一般会做两次处理:先连续采样 8 次去掉最大值和最小值再取平均,得到原始值;然后对这个平均结果做一阶低通滤波,平滑系数取 0.2~0.3 左右。

// ADC 多次采样取中平均 + 低通滤波,滤波系数 0.25 unsigned int adc_filter(void) { unsigned char i, j, temp; unsigned char buf[8]; unsigned int sum = 0, avg; for (i = 0; i < 8; i++) { buf[i] = read_adc0832(0); // 连续读 8 次 } // 冒泡排序去掉一个最大值一个最小值,工程中最简单直接 for (i = 0; i < 7; i++) { for (j = 0; j < 7 - i; j++) { if (buf[j] > buf[j + 1]) { temp = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = temp; } } } // 去掉 buf[0] 和 buf[7],累加中间 6 个值 for (i = 1; i <= 6; i++) { sum += buf[i]; } avg = sum / 6; // 一阶低通滤波,防止显示跳变 static unsigned int smooth_val = 0; if (smooth_val == 0) { smooth_val = avg; // 首次直接赋初值 } else { smooth_val = (unsigned int)(smooth_val * 0.75f + avg * 0.25f); } return smooth_val; }

这段代码里冒泡排序虽然效率低,但 8 个数据元素用时极短,在 12MHz 下不到 0.5ms,不会拖累主循环。滤波系数 0.25 的意义是:新采样值只占最终结果的四分之一,快速的光强跳变会以指数形式逼近真实值,约 4~5 次采样后显示值稳定到目标的 90% 以上。要更快的响应就调大系数,要更平滑就调小系数,但注意系数太小会让显示看起来反应迟钝。

一个隐藏问题:smooth_val声明为 static 后,如果环境光突然从极暗变成极亮,显示值需要较长时间才能跟上。如果你想做"用手遮住再松开"这类演示,建议把低位光照下的滤波系数放大到 0.4,高位光照下缩小到 0.15,用 ADC 读数本身作为分档条件,效果会自然很多。

2.3 数码管动态扫描与显示映射

2.3.1 段码表与位选连接方式

数码管显示部分,我先定义一个标准段码表,共阴数码管的结构决定了段码的编码方式。P0 口经过 74HC245 驱动段选(段码表按共阴方式定义);如果直接驱动,电流可能不够,显示亮度会发暗。

// 共阴数码管段码表:0~9 及空位 unsigned char code seg_code[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x00 // 熄灭 };

位选我接在 P2 的低四位,分别对应千位、百位、十位、个位。动态扫描的核心是逐位点亮、快速轮询、利用视觉暂留让人眼感觉四位数同时亮着。扫描频率不能太低,不然能看到闪烁;也不能太高导致驱动电路来不及响应。实测 12MHz 晶振下每 2ms 切换一位,四位数码管完整刷新周期 8ms,对应 125Hz 刷新率,非常稳定。

2.3.2 将滤波后的电压值映射为光照百分比

光敏电阻测试显示的内容可以有两种:直接显示 0~255 的 ADC 原始值,或者显示换算后的光照强度百分比。课程设计里绝大多数要求是后者,所以需要一个映射函数,把 ADC 值映射到 0~100 的范围。这里强烈不建议用简单的线性映射,因为光敏电阻阻值和光照强度的对数关系是物理特性决定的;更好的方法是先测出暗态和强光下的 ADC 值,再按对数关系建立查表或分段拟合。

一个小例子:暗态实测 ADC 读数为 15,强光下读数为 230。ADC 值每增加 1,对应的光照增量并不相同。用分段线性插值效果不错:把 15~230 这段拆成 4 个区间,每个区间用不同斜率映射到 0~100。每个区间对应 ADC 读数 15~60、60~110、110~170、170~230,映射到 0~25、25~55、55~85、85~100。代码如下:

// 分段线性映射:ADC 值转光照百分比 unsigned char adc_to_light_percent(unsigned int adc_val) { if (adc_val <= 15) return 0; if (adc_val >= 230) return 100; if (adc_val <= 60) { return (unsigned char)((adc_val - 15) * 25 / 45); // 0~25 } else if (adc_val <= 110) { return 25 + (unsigned char)((adc_val - 60) * 30 / 50); // 25~55 } else if (adc_val <= 170) { return 55 + (unsigned char)((adc_val - 110) * 30 / 60);// 55~85 } else { return 85 + (unsigned char)((adc_val - 170) * 15 / 60);// 85~100 } }

分段边界值必须由你手里的实际电路标定,不要照抄。光照条件下的 ADC 值会因分压电阻、光敏电阻型号和供电电压不同而有明显差异。标定方法是:在目标暗环境下记下 ADC 值,再在手机闪光灯直射下记下 ADC 值,然后把中间区间均匀切 3 段,按上式的结构填进去。

2.3.3 数码管显示驱动的完整主循环示例

把采样和显示串起来的主循环不长。下面这个示例每轮循环调用一次滤波采样,把百分比拆成 4 位数字输出到数码管,最高位做灭零处理,避免显示"0015"这种不自然的格式。

// 主循环:采样 → 映射 → 拆分 → 动态扫描 void main(void) { unsigned int adc_raw; unsigned char percent; unsigned char digit[4]; while (1) { adc_raw = adc_filter(); // 获取稳定的 ADC 值 percent = adc_to_light_percent(adc_raw); // 映射到百分比 digit[0] = percent / 1000; // 千位 digit[1] = percent % 1000 / 100; // 百位 digit[2] = percent % 100 / 10; // 十位 digit[3] = percent % 10; // 个位 // 四位动态扫描,显示 8ms 后再取下一次采样 display_digit(0, digit[0]); delay_ms(2); display_digit(1, digit[1]); delay_ms(2); display_digit(2, digit[2]); delay_ms(2); display_digit(3, digit[3]); delay_ms(2); } }

注意采样频率和扫描频率之间的关系。上面循环每次采样后要执行 8ms 的扫描,滤波函数里还要连续读 8 次 ADC,所以实际采样周期大约 10ms,对于光强变化来说已经很快了。如果你把adc_filter()放在扫描的间隙执行而不是整轮执行,可以避开数码管切换时的电流干扰,ADC 读数会更稳。具体做法是设置一个 10ms 定时中断,在中断里关数码管显示、读 ADC、开显示。

// 定时器 0 中断服务函数,10ms 间隔采样 void timer0_isr(void) interrupt 1 { static unsigned char count = 0; TH0 = 0xDC; // 12MHz 晶振,10ms 重载值 TL0 = 0x00; if (++count >= 5) { // 每 50ms 更新一次显示值 count = 0; P0 = 0x00; // 关段选,避免干扰 adc_raw_buffer = adc_filter(); percent_buffer = adc_to_light_percent(adc_raw_buffer); P0 = 0xFF; } }

用中断采样后,主循环只管数码管扫描,两者互不阻塞。这种分离方式在 51 单片机项目中属于比较成熟的做法,为后面扩展按键设置阈值、串口输出调试信息都留了余量。

3. 光敏电阻测试数码管显示程序的下载与验证方法

3.1.1 用 Keil 编译源码并生成可烧录的 HEX 文件

拿到别人写的 51 单片机源码程序,第一步不是急着烧录,而是先建工程编译一遍确认没有语法错误。哪怕是 Keil5 安装教程里那些细节没做对,比如没选对芯片型号、没勾选生成 HEX 文件选项,后面都白搭。常见做法是打开 Keil 新建工程,选择 AT89C52 或 STC89C52RC,把源码文件夹里的.c文件添加进工程。

关键点在于右击 Target 选择 Options for Target,在 Output 选项卡里勾选 Create HEX File,否则编译通过也得不到可烧录文件。编译器版本我用的是 C51 V9.60 以上,低版本对某些语法支持不好,比如sbit声明本身就存在,但部分老编译器对static局部变量初值处理的坑不少。编译无误后,工程文件夹下会出现.hex文件,这就是给烧录软件用的最终产物。

有些源码可能使用了寄存器头文件里的特殊位定义,例如P1^0这种写法必须在文件头部包含reg52.h。如果缺失,编译报错全是 undefined identifier,比较常见。检查源码第一处预处理指令即可确认。

3.1.2 烧录与硬件测试时的关键观察点

常见的烧录工具是 STC-ISP 或普中科技自带的烧录软件。如果你用的是 STC89C52RC,要先把单片机断电,点击下载按钮后再上电,冷启动时序不对会提示握手失败。波特率我一般选 2400 或 4800,最稳。

硬件上电后,确认以下几点:

  1. 数码管是否有暗亮迹象。P0 口内部没有上拉,若未接外部上拉电阻,段码输出高电平时电压可能不足,表现为显示亮度低。这种情况要把 10kΩ×8 排阻接到 P0 口到 VCC,不用换芯片。
  2. 遮挡光敏电阻时,数码管数值应明显下降;用手电照射时,数值应上升。如果反向,说明分压电阻接到了 VCC 而光敏电阻接到了 GND,改变方向即可。
  3. 数值跳变范围是否异常大。若完全遮挡时仍有 20 以上的波动,检查光敏电阻引脚是否虚焊,以及 ADC_CLK 和 ADC_DO 的线是否过长,试验板上超过 15cm 的杜邦线在高频切换时会串入噪声。

一个容易被忽视的问题:数码管动态扫描会让 51 的 VCC 电流在几十 mA 级别波动,如果开发板的稳压芯片余量不足,ADC 的 VREF 会跟着波动,导致测量值周期性跳动。观察方法是固定光照强度下让显示值静止,如果最后一位以固定的周期上下漂移,优先改善电源而非改代码。在 VCC 和 GND 之间加一个 100μF 电解电容和 0.1μF 瓷片电容并联,算是最经济的稳定手段。

4. 光敏电阻测试的标定、误差分析与显示精度提升

4.1.1 环境光与人工光源的差异对数据的影响

光敏电阻对不同波长的光敏感度不同,白炽灯的偏红光谱和荧光灯的偏蓝光谱即使照度相同,测出的 ADC 值也会有差异。这是物理特性,不是 bug。因此标定时使用的光源要跟你实际应用场景一致。例如做一个判断"环境是否够亮"的装置,在自然光下标定好阈值,拿到普通的 60W 白炽灯下测,阈值可能需要重调。为了避免这种误差,可以在光敏电阻前面加一个滤光片,把光谱响应修正到接近人眼视见函数,但这在课程设计里成本偏高了。更实际的做法是:程序里定义 3 个阈值档位(暗、柔和、明亮),每个档位通过 2 个可调电位器设置上下限,这样即使换光源,也能现场粗调。

4.1.2 ADC 参考电压波纹的影响与改善手段

ADC0832 的 8 位分辨率只有 256 个等级,对应 5V 参考电压,每个 LSB 约 19.5mV。如果参考电压有 10mV 的纹波,结果就直接跳半个 LSB;如果纹波到 50mV,跳 2~3 个 LSB。你在实际测试中如果发现数值小幅快速抖动,尽管加了滤波,原始抖动还是偏大,优先怀疑 VCC 上的数码管扫描噪声。

我测试过不同供电方式下的效果:直接用 USB 供电,纹波通常 30~80mV;改用线性稳压 7805 供电并加大电容后,纹波降到 5mV 以下,显示值肉眼可见稳定下来。如果你的项目要求 0.5% 以内的显示精度,建议 VREF 单独用 TL431 稳压到 2.5V,分压网络的满量程输出电压相应降到 2.5V,同时调整固定电阻让光敏电阻在最亮时的分压输出不超过 2.45V。这样 ADC 的量化分辨率变成 2.5V/256 ≈ 9.8mV,反而提升了相对精度,代价是映射表需要重新标定。

4.1.3 数码管刷新时序与 ADC 采样时刻的冲突处理

动态扫描与 ADC 采样的时序冲突是这类混合信号项目最常见的难点。扫描切换的瞬间,段选位选同时翻转,导致瞬间电流增大、电源电压跌落。如果采样恰好发生在扫描切换的前后 100μs 内,ADC 结果容易偏大。解决方案就是让所有 ADC 转换过程在数码管全灭的几十微秒内完成,或者靠定时中断错开两者相位。

具体做到什么程度?看你对显示稳定性的要求。如果只要求最后一位数字不快速跳动,多采几次滤波就够了;如果要求连续读值基本静止,必须做时序分离。我用过一个比较巧的方案:数码管扫描一位的时间 2ms,而一次 ADC0832 转换大约需要 40μs(12MHz 下实测约 45μs),所以在每一位扫描的末尾插入一次 ADC 采样,正好利用切换间隙。实际上由于 ADC 时钟是我们自己产生的,可以完全放在中断里处理。

// 数码管显示与 ADC 采样时序错开:每次显示一位后采样一次 void display_and_sample(void) { display_digit(0, digit[0]); // 显示千位 delay_500us(); adc_raw_buf = read_adc0832(0); // 此时段选导通电流已稳定 display_digit(1, digit[1]); delay_500us(); adc_raw_buf = read_adc0832(0); display_digit(2, digit[2]); delay_500us(); adc_raw_buf = read_adc0832(0); display_digit(3, digit[3]); delay_500us(); adc_raw_buf = read_adc0832(0); }

这个循环每轮调用 4 次 ADC 读操作,把采样分散在整个扫描周期里,避免了集中在同一瞬间造成电源冲击。显示切换到新位后先延时 500μs 再采样,是为了避开位选瞬间的电源跌落,因为线路寄生电感和电容的暂态响应大约持续几十微秒到 200μs 之间。

4.1.4 测量结果的一致性验证方法

做完标定和滤波,还不能直接验收。最简单的验证方法是记录 5 个不同光照条件下的显示值,每个条件手动读取 10 次,计算偏差。如果偏差在 ±2% 以内,对大多数 51 项目来说是合格的。另一个快速验证是:用一个可调亮度的手电筒从远到近缓慢靠近光敏电阻,数码管显示值应当平滑上升,不应该出现跳变或回头。出现跳变说明分段映射的斜率在某一段过大,导致相邻两个映射结果差异明显;出现回头说明 ADC 采样毛刺穿越了分段的边界,滤波不够。前者减小映射斜率,后者增大滤波系数或者把分段的边界改成迟滞比较(升序和降序使用不同边界),都可以改善。

5. 用串口输出调试信息校准光敏电阻测量曲线的进阶方法

数码管只有 4 位,显示细腻的标定数据有限。调试时我更倾向于通过串口把 ADC 原始值和映射后的百分比一起发到上位机,在 PC 上画出曲线来,确认分压电路是否工作在最佳区间。51 单片机用 UART 输出调试数据是很成熟的套路,不需要额外硬件,一根 USB 转 TTL 线就能搞定。

// 串口初始化:9600 baud,12MHz 晶振,定时器 1 作为波特率发生器 void uart_init(void) { SCON = 0x50; // 模式 1,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器 1 工作于模式 2 TH1 = 0xFD; // 9600 波特率重装值 TL1 = 0xFD; TR1 = 1; TI = 0; } void uart_send_hex(unsigned char value) { unsigned char buf[3]; buf[0] = '0' + value / 100; buf[1] = '0' + value % 100 / 10; buf[2] = '0' + value % 10; SBUF = buf[0]; while (!TI); TI = 0; SBUF = buf[1]; while (!TI); TI = 0; SBUF = buf[2]; while (!TI); TI = 0; }

在标定阶段,每 100ms 调用一次uart_send_hex(adc_value)uart_send_hex(percent),然后在 PC 上打开串口监视器,把收到的数据复制到 Excel 里生成散点图。通过散点图你能一眼看出映射函数是否线性、有没有异常拐点。如果散点呈明显的下弯或上翘,说明分段边界没选对,或者某一段的斜率过大放大了噪声。

最高效的标定流程是:用手电筒模拟从暗到亮的连续变化,同时把 ADC 值和显示百分比都发出来,结束后对比两者曲线。ADC 值曲线应当无明显台阶,显示百分比曲线应当在各个分段的衔接处连续。如果发现某个分段起点和终点有跳跃,调整该分段两个边界对应的映射系数即可。最终确定的边界值写回主程序里的adc_to_light_percent(),然后用数码管显示做最终验收。

把这段标定代码留在工程里不删也没有问题,仅在#ifdef DEBUG条件下编译,量产时去掉这个宏就能省掉串口输出的时间开销。这种用串口验证数值、用数码管发布值的组合,比单纯看 4 位数码管猜问题高效得多,尤其当你的分压电阻选得不合适导致整个测量范围只落在 ADC 的一小段区间时,串口曲线能立即暴露问题根源——这也是在 51 单片机项目里值得一直保留的调试习惯。

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

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

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

立即咨询