简介:一份基于C51单片机实现TCS3200颜色识别与LCD1602显示的完整软件源代码包,面向单片机初学者、电子设计竞赛备赛者以及需要快速实现颜色检测功能的开发者。资源围绕TCS3200驱动、信号采集与LCD显示展开,提供中断初始化、定时器装初值、LCD液晶驱动等核心代码,可直接用于课设或产品原型验证。整个压缩包共16个文件、约46KB,包含C源文件、Keil工程文件(uvproj/uvopt)、编译生成的hex烧录文件以及obj、lst等中间产物,并附带3个bak备份文件,方便对照恢复工程状态。目前已有278人学习下载,代码结构简洁、注释清晰,适合作为颜色识别模块开发时的参考模板,也能帮助理解C51中断与定时器配合实现脉冲计数的典型用法。
1. 颜色识别没有“颜色”,只有频率
拿到这份“TCS3200 颜色识别+LCD1602 显示”的 C51 源码,先别急着烧录看效果,你会发现整个工程里根本没有现成的 RGB 数值,有的是定时器初值、外部中断函数和一堆脉冲计数变量。TCS3200 的工作方式是把当前滤波通道下的光强转成方波频率,单片机要做的只是用固定的时间窗口数脉冲,再把频率查表映射成颜色。这种设计让 51 不必内建 ADC,普通 I/O 加两个中断就能完成识别。对正在做软件综合实践选题、课程设计的人,或者想搞清传感器底层驱动原理的开发者,这套代码都是一个短小但完整的参考实现。
2. TCS3200 测色原理与 C51 的中断定时配合
2.1 为什么要用脉冲频率表达颜色
TCS3200 的感光区域把红、绿、蓝、透明四种滤波器的光电二极管均匀排列,保证任何一色光落在传感器上都有对应的二极管响应。芯片内部把光电流转换成脉冲输出,所以软件看到的不是模拟量,也不是 I2C 寄存器,而是一串方波。你只有两个控制维度:S0/S1 决定输出频率的缩放比例,S2/S3 决定让哪一组光电二极管接入转换电路。
选型上这种方案和 BH1750、TCS34725 这类数字传感器不同。TCS3200 的优势是响应快、成本低、驱动逻辑透明,劣势是颜色识别结果完全依赖外部光源和校准。对 C51 课程设计来说,正好可以把中断、定时器、查表这几个核心知识点串起来,这也是这道题目常被选作软件综合实践选题的原因。这类传感器不需要软件模拟 I2C,在低主频的 51 上反而更干脆。
2.2 INT0 数脉冲、T0 定时间:两个中断的分工
要让 C51 测出脉冲频率,最直接的方法是 T0 定时 1 秒,同时让外部中断 INT0 记录 TCS3200 的每个下降沿。这样定时和计数互不占用同一个硬件资源。源码开头的IT0=1就是设置 INT0 为下降沿触发;EX0=1打开外部中断;initTimer()把 T0 初始化成 10ms 定时;TR0=1启动定时器;ET0=1允许定时器中断;最后EA=1打开总中断。这个顺序本身就是 C51 中断系统的标准启用流程,少一步都可能出现只亮屏不计数的情况。
有人会问,为什么不用 T0 的计数器模式直接数脉冲?原因是 T0 一旦用作计数器,就失去了定时能力,你没法在同一个 T0 上再产生 1 秒基准。用两个定时器分别做定时和计数当然可以,但 51 只有两个定时器,还要留一个给串口时就不够了。所以采用“INT0 计数 + T0 定时”的配合是最省硬件资源的经典方案。
2.3 定时初值计算与颜色通道切换
以 12MHz 晶振为例,机器周期是 1µs,T0 工作在方式 1 的 16 位定时器,要定时 10ms 就需要计数 10000 个机器周期,于是初值是 65536-10000=55536,也就是 0xD8F0。Keil 里初始化代码通常写成下面这样:
void initTimer(void) { TMOD &= 0xF0; // 清空 T0 相关位 TMOD |= 0x01; // T0 方式1:16位定时器 TH0 = 0xD8; // 装入初值高字节 TL0 = 0xF0; // 装入初值低字节 }TMOD &= 0xF0是为了不影响 T1 的配置,这是一个容易被忽略的细节。如果不加这句,直接给 TMOD 赋值,串口波特率发生器可能被改坏。初值填入 TH0、TL0 后,每次定时器溢出进入中断都要重新装入初值,否则时间基准就从 0 开始数满 65536,变成 65.536ms,最后测出来的频率整体偏低。
S2/S3 的真值表和颜色通道对应关系如下:
| S2 | S3 | 通道 |
|---|---|---|
| 0 | 0 | 红 |
| 0 | 1 | 蓝 |
| 1 | 0 | 绿 |
| 1 | 1 | 透明(无滤波) |
采样时按红、蓝、绿、透明依次切换,每个通道各测一个 1 秒脉冲数。切换后要等光电二极管稳定,后面主循环里的 1 秒等待窗口已经覆盖了这个时间差。
为什么用 1 秒而不是更短?TCS3200 在弱光下输出频率可能只有几百赫兹,如果门控 10ms,一次采样的脉冲数只有几个到几十个,统计误差非常大。取 1 秒可以获得更稳定的平均频率,代价是颜色识别刷新率只有 1Hz,对静态色块识别完全够用。如果要做流水线动态检测,可以改成 100ms 门控再乘以 10,但要接受随机误差变大,之后加数字滤波。
3. 从源码文件到实际数据流:MyColorTest.c 中断和主循环
3.1 压缩包里那些文件是干什么的
工程文件列表里有 MyColorTest.c、MyColorTest.uvproj、MyColorTest.uvopt、MyColorTest.M51、MyColorTest.LST、MyColorTest.hex 以及若干 .bak。.c是唯一需要改的源码,.hex是编译产物,直接交给 STC-ISP 烧录即可。.uvproj是 Keil 工程文件,.uvopt保存窗口布局和调试器配置,.bak是上一次保存的备份。.M51是链接器生成的存储器映射文件,.LST是编译器生成列表,这两个文件在工程报错和跑飞时最有用。文件说明见下表:
| 文件 | 作用 |
|---|---|
| MyColorTest.c | 源码主文件 |
| MyColorTest.hex | 烧录文件 |
| MyColorTest.M51 | 链接存储映射 |
| MyColorTest.uvproj | Keil 工程 |
| MyColorTest.uvgui_*.bak | 窗口布局备份,可删除 |
如果你用 Keil 打开工程发现器件型号显示不正常,可以手动选 AT89C52,驱动代码不依赖特定器件。烧录时如果目标板是 STC89C52,打开 STC-ISP,选择对应单片机型号,载入 .hex,波特率不要超过 9600,否则老芯片可能下载失败。如果提示写超时,先断电重新上电再点下载,STC 系列需要上电冷启动进入 ISP 模式。
3.2 中断服务函数和主循环的数据交接
源码摘要里给出了 main 函数开头的初始化序列,但没有展示中断函数。按此类工程最常见写法,两个中断服务函数应该是这样的:
volatile unsigned int pulse_count; volatile unsigned char timer_tick; bit second_flag; void int0_service(void) interrupt 0 { pulse_count++; } void timer0_service(void) interrupt 1 { TH0 = 0xD8; // 重装10ms初值 TL0 = 0xF0; if (++timer_tick >= 100) // 100次10ms = 1秒 { timer_tick = 0; second_flag = 1; } }interrupt 0表示外部中断 0,interrupt 1表示定时器 0 中断。函数名后面不带任何返回值类型,这是 Keil C51 中断函数的标准写法。volatile关键字必须要加,否则编译器可能把 pulse_count 优化成寄存器变量,导致主循环读到的永远是 0。bit类型只占一位,适合做标志位。
主循环在读完一个通道后要主动清标志:
pulse_count = 0; second_flag = 0; while (!second_flag);这段放在切换通道之后。如果不清 second_flag,下一次循环一进来 while 就立刻通过,测到的是上一秒的脉冲数,颜色数据不会更新。中断服务函数里不要写 LCD 刷新或者乘除运算,只改标志和计数器,这是保证 1 秒门控准确的前提。
3.3 通道选择与采样函数
颜色通道的切换函数可以封装成传递模式参数的形式。结合上文的真值表,S2、S3 可以用两个位变量控制:
sbit S2 = P1^1; sbit S3 = P1^0; unsigned int sample_channel(unsigned char mode) { S3 = mode & 0x01; // mode bit0 -> S3 S2 = (mode >> 1) & 0x01; // mode bit1 -> S2 pulse_count = 0; second_flag = 0; while (!second_flag); return pulse_count; }调用时 mode 取 0 红、1 蓝、2 绿、3 透明。这里有一点要注意:TCS3200 的 S2 和 S3 排布并不符合人的直觉,所以代码里把 S3 作为 bit0、S2 作为 bit1,再按红蓝绿透明顺序传入参数,比直接操作引脚更容易维护。实际硬件上如果你把 S2、S3 接反了,识别结果会表现为红蓝互换,这类错接用白纸校准后仍然无法纠正。
这样写的好处是主循环可以连续采样四次,把四个频率保存到数组里,再做白平衡归一化和颜色判定。采样顺序没有严格规定,但通常先测一次透明通道用来估算环境光强度,再测 RGB 三个通道。
3.4 把频率值换算成 RGB 并送 LCD 显示
拿到四个通道的 pulse_count 后,先把它换算成频率,再经过白平衡归一化得到 RGB 字节。LCD 显示部分只负责把三个数字显示在第二行:
unsigned char display_buf[16]; sprintf(display_buf, "R:%3d G:%3d", r, g); lcd_puts(display_buf);C51 的 sprintf 占资源较多,小工程没有问题,但如果代码空间紧张,就改用整数拆分函数自己拼字符。这里%3d表示至少占 3 位右对齐,LCD 第一行显示颜色名,第二行显示 RGB 值,调试时很直观。同时注意,每一个 sample_channel 调用耗时 1s,四个通道就是 4 秒一轮。想要缩短周期,可以把每个通道门控改为 250ms 并乘以 4,但计数误差会增大,需要看实际脉冲数量决定。上面说的这些调整都不需要动中断服务函数,只要改门控累加次数和主循环的等待条件。
4. LCD1602 字符屏驱动与 Keil5 的 C51 工程配置
4.1 LCD10602 到底是不是 1602
项目标题里的 LCD10602 并不是常见的型号,摘要里的 init_LCD() 和关键词里的 LCD1602 说明实际使用的就是字符型 1602 液晶。1602 的意思是 16 列 2 行,每行最多 16 个字符,和 10602 这个数字对不上。这个差异大概率是资料上传时手误,驱动时序还是走 HD44780 兼容协议。用 8 位数据总线和 RS、RW、E 三个控制脚即可,初始化时依次发送 0x38、0x0C、0x01 这三条指令,对应关系如下:
| 指令 | 含义 |
|---|---|
| 0x38 | 8位数据、双行、5x7点阵 |
| 0x0C | 开显示、关光标 |
| 0x01 | 清屏、光标回原点 |
void init_LCD(void) { P0 = 0x38; // 8位总线,两行显示 lcd_cmd(); P0 = 0x0C; // 显示开,光标关 lcd_cmd(); P0 = 0x01; // 清屏 lcd_cmd(); }这是最精简的 1602 初始化。lcd_cmd()内部会产生 E 下降沿,把 P0 上的命令写入液晶控制器。注意每次命令之间要等待约 1.5ms,HD44780 执行清屏指令比普通命令慢,有些代码在清屏后直接写数据会丢首个字符,这一点是 LCD1602 驱动常见的坑。
4.2 Keil5 安装 C51 与 ARM 并存的正确方式
很多人电脑上先装了 MDK-ARM 用来写 STM32,再打开 C51 工程时发现编译按钮是灰的,找不到 CX51 编译器。这是因为 Keil5 的 C51 和 ARM 是两套独立工具链,不能装进同一个目录。常见做法是把 C51 版安装到C:\Keil_v5,把 MDK-ARM 安装到D:\Keil_MDK,两个目录完全分开。安装时勾选全部器件包,装完后桌面会出现一个统一的 Keil μVision5 图标,它自己会根据打开的工程类型选择编译器。
如果双击 .uvproj 被 MDK 关联打开,可以先用“文件-打开工程”来加载 C51 工程。还有一个小坑:.uvopt 文件保存了上一次调试器配置,如果是从别的电脑拷来的工程,打开后提示找不到 FLM 文件,直接删掉 .uvopt 和 .bak 文件,重新打开即可。Keil5 怎么添加 c51 芯片包这个问题,本质上是 C51 版软件自带的 Device Pack,不需要单独下载芯片包,只要安装 C51 版本时没取消勾选即可。
4.3 用 M51 链接器控制文件判断资源占用
.M51文件不是让你手写的,它是 Keil 链接完成后输出的存储器分布图。打开 MyColorTest.M51 能看到类似下面的段落:
LINK MAP OF MODULE: MyColorTest (MYCOLORTEST) IDATA 0010H 0003H ... BIT 0000H 0001H ...这里 IDATA 表示内部数据存储器占用。8051 的 IDATA 只有 256 字节,其中低 128 字节可以直接寻址。如果工程里定义了较大的全局数组,例如颜色特征表:
code unsigned char color_table[][3] = { {255, 0, 0}, {0, 255, 0}, {0, 0, 255}, };code关键字把数组放进了程序存储器(ROM),不占用宝贵的 IDATA。这一点对 51 非常关键,因为一个 4 色的 3 维表就要占 12 字节,如果不用 code,再叠加 LCD 缓冲区,RAM 就很容易超限。Keil 编译后如果报 “DATA/IDATA OVERFLOW”,第一件事就是检查是否有大型数组漏加 code 关键字。打开 M51 后搜索OVERLAP,如果出现多个段覆盖在同一地址区间,说明编译器认为这些函数不会同时执行,这对中断和主循环之间的共享变量是危险的。只要中断服务函数里也访问了主循环的变量,编译器并不能自动识别异步访问,所以前面的 volatile 是底线。
5. 白平衡校准与识别稳定性调整
5.1 白色校准:让频率变成相对值
直接用原始频率判断颜色会受光源强度和传感器差异影响,所以第一步是白平衡。把传感器对准白色纸面,分别读取红、蓝、绿三个通道的脉冲数,记为 white_r、white_g、white_b。之后每一次采样都用同样的白值做归一化:
unsigned char r = (unsigned char)(freq_r * 255UL / white_r); unsigned char g = (unsigned char)(freq_g * 255UL / white_g); unsigned char b = (unsigned char)(freq_b * 255UL / white_b);这里的 255 是把结果映射到 8 位灰度。255UL防止乘法溢出。如果白色读数里有某个通道特别小,比如蓝光 LED 照明下 blue 频率很低,归一化后噪声会被放大,这时就要重新调整光源或降低频率缩放比例。
5.2 用最小距离而不是相等判断颜色
颜色表里存的是标准色归一化后的 RGB,实际读取值由于抖动不会完全相等。常用办法是计算欧氏距离或曼哈顿距离:
unsigned char c = 0, min = 255, result = 0; for (c = 0; c < 4; c++) { unsigned char diff = abs((int)r - color_table[c][0]) + abs((int)g - color_table[c][1]) + abs((int)b - color_table[c][2]); if (diff < min) { min = diff; result = c; } }曼哈顿距离的运算量远小于欧氏距离,51 上没有浮点单元,开方会拖慢主循环,所以工程里更常见的是这种绝对值求和形式。注意(int)强转,防止 unsigned char 相减出现负数借位。
5.3 输出频率比例和遮光的影响
最后一个技巧是频率缩放。前文提到 S0/S1 可以设置输出比例,很多照抄网上的代码都是 11,也就是 100% 频率输出。但在强光下,频率可能冲到几十千赫兹,C51 的 INT0 中断来不及处理,导致计数丢失,颜色值偏小。这时把 S0/S1 改成 10 或 01,分别对应 20% 和 2% 比例,能明显改善测量线性度。
我一般会在遮光管里测白纸校准,并在代码里把 S0/S1 定义为两个位变量,方便现场切换。切换比例后记得重新校准白色基准,因为通道频率整体变了。实际调试中,先用 20% 比例扫一遍记录最大频率,如果低于 10kHz 再切回 100%,这样能在分辨率与中断负载之间找到平衡点。调整这个参数,就能在不改硬件的前提下找到当前光源下的最佳工作点。
本文还有配套的精品资源,点击获取