
简介这是一套基于C51单片机的TCS3200颜色识别与LCD1602显示完整软件源码面向正在学习单片机接口编程、传感器数据采集与显示驱动的开发者尤其适合课程设计或电子竞赛的软硬件联调阶段。代码中main函数完成系统初始化后开启总中断INT0下降沿触发计数定时器T0负责计时利用TCS3200输出频率与颜色分量的关系计算RGB数值再通过LCD1602显示出对应颜色信息中断服务与主循环配合紧密适合理解传感器测频与显示刷新的完整流程。压缩包共16个文件容量仅46KB主要包含C语言源文件、Keil工程文件uvproj/uvopt、编译生成的HEX烧录文件、LST列表文件及工程备份能够直接打开工程重新编译也可用HEX文件烧录验证。另有PLG、M51等辅助文件便于查看链接映射和项目构建日志。目前已有278人学习下载对于需要快速搭建TCS3200颜色识别显示系统的读者具有直接参考和复用价值。1. 手头这份源代码包到底在做什么手头这份『基于C51单片机设计的TCS3200颜色识别LCD10602显示的软件源代码.zip』是典型的单片机课程设计或软件综合实践选题用8051系列单片机读一颗颜色传感器再把识别结果送到一块字符屏上。标题里的LCD10602按市面上能买到的器件习惯对应的是最常见的LCD1602字符液晶下文的驱动和接线都以LCD1602为准。整包源码的价值不在传感器本身的电路而在C51工程里怎么把TCS3200输出的频率信号量化成RGB三个数再把这个过程嵌进一个不卡死的显示循环。适合正在做课程设计、刚接触Keil C51的读者也适合想把手头的C51开发板快速接到TCS3200的老手。2. TCS3200颜色传感器的频率输出原理与C51定时器计数2.1 引脚配置S0/S1决定频率缩放S2/S3决定通道颜色TCS3200内部把光电二极管排列成8x8阵列等效为16个红色、16个绿色、16个蓝色、16个无滤光通道。芯片对外只有一个频率输出引脚OUT频率高低与所选通道的光强近似线性。选哪个通道由S2/S3两个引脚决定频率缩放倍率由S0/S1两个引脚决定这四根线正好接在C51的四个普通IO上。S0S1输出频率缩放适用场景LL关断不建议用于测量LH2%弱光环境、需要更精细的低频测量HL20%常规室内光源推荐起始档HH100%强光、高速采样S2S3选通通道LL红色滤波通道HH绿色滤波通道LH蓝色滤波通道HL无滤波通带通道我一般把S0/S1设为20%档起步。原因很直接C51的T0计数器是16位如果直接用100%档强光下输出频率可达数百kHz10ms窗口内计数可能逼近65535容易溢出2%档又会让暗色物体读数太小。20%档在室内光照下通常落在10kHz到120kHz之间10ms窗口计数范围是100到1200分辨率足够判断颜色大类。接线方式也很固定TCS3200的OUT接P3.4也就是T0外部计数输入引脚S0接P1.0S1接P1.1S2接P1.2S3接P1.3。这样做的原因是T0专门负责数脉冲T1专门负责开时间窗口两个定时器分工不占用CPU计算频率。2.2 T0外部计数加T1时间窗口的测量代码C51测量频率的常见做法是“定时计数”T1产生一个固定宽度的时间窗口T0在这个窗口内对外部脉冲计数。窗口结束时读出TH0和TL0拼接出来的计数值再除以窗口时间就得到频率。下面这段是可运行的骨架我会把它独立成tcs3200_core.c。/* tcs3200_core.c - 适用于STC89C52/AT89S52等C51单片机 */ #include reg52.h sbit S0 P1^0; sbit S1 P1^1; sbit S2 P1^2; sbit S3 P1^3; #define TCS_SCALE_20 0 /* S01,S10: 输出频率为原频率的20% */ #define TCS_SCALE_2 1 /* S00,S11: 输出频率为原频率的2% */ void Delay_Ms(unsigned int ms); void TCS_Init(unsigned char scale) { if (scale TCS_SCALE_20) { S0 1; S1 0; } else { S0 0; S1 1; } } unsigned int TCS_ReadChannel(unsigned char ch) { unsigned int cnt; /* 切换光电二极管通道: 0红, 1绿, 2蓝, 3通带 */ switch (ch) { case 0: S2 0; S3 0; break; /* 红 */ case 1: S2 1; S3 1; break; /* 绿 */ case 2: S2 0; S3 1; break; /* 蓝 */ default: S2 1; S3 0; break; /* 无滤波 */ } Delay_Ms(2); /* 通道切换后留2ms让输出稳定 */ /* T0作16位外部计数, T1作16位定时器, 开一个10ms窗口 */ TMOD 0x15; /* 0001 0101: T116位定时器, T016位计数器 */ TH0 0; TL0 0; /* 11.0592MHz晶振, 12时钟/机器周期, 10ms窗口需9216个计数 */ TH1 (65536 - 9216) / 256; TL1 (65536 - 9216) % 256; TR0 1; TR1 1; while (TF1 0); /* 等待T1溢出, 10ms窗口结束 */ TR0 0; TR1 0; TF1 0; cnt ((unsigned int)TH0 8) | TL0; return cnt; }这段代码的操作逻辑很直观先切换通道等输出稳定清空T0给T1装上10ms窗口的初值然后同时启动两个定时器。T0在P3.4引脚上数外部脉冲T1在内部按机器周期跑。T1溢出时窗口结束停止计数后把TH0和TL0拼成一个16位无符号整数返回。参数上要注意三个点。第一TMOD0x15的低4位0101表示T0是16位计数器高4位0001表示T1是16位定时器如果把T0配成定时器而T1配成计数器读出来会是完全无规律的数字。第二9216这个数字只在11.0592MHz晶振、10ms窗口下成立换成12MHz晶振时要改成10000因为12MHz的机器周期正好是1us。第三返回的cnt除以0.01秒就是OUT引脚的实际频率单位Hz如果要做绝对光谱分析再除以0.2得到传感器的原始频率但单纯比颜色时直接用cnt就够。2.3 三通道轮询与RGB归一化颜色识别最少要读红绿蓝三个通道。我一般把三个读数放进一个结构体里然后按最大值归一化到0到255这样光源强弱变化时颜色比例仍然稳定。typedef struct { unsigned char r; unsigned char g; unsigned char b; } RGB_Value; void TCS_ReadRGB(RGB_Value *rgb) { unsigned int cr, cg, cb, cmax; cr TCS_ReadChannel(0); cg TCS_ReadChannel(1); cb TCS_ReadChannel(2); /* 取三通道最大值做归一化基准, 防止暗场下三个数都偏小 */ cmax (cr cg) ? cr : cg; cmax (cb cmax) ? cb : cmax; if (cmax 0) cmax 1; rgb-r (unsigned char)((unsigned long)cr * 255 / cmax); rgb-g (unsigned char)((unsigned long)cg * 255 / cmax); rgb-b (unsigned char)((unsigned long)cb * 255 / cmax); }为什么不除以固定的校准值而用动态最大值因为TCS3200读到的绝对计数值受环境光强度影响很大同一个红色物体在白天和晚上可能差三倍但红绿蓝之间的相对比例变化不大。按当天当前光源下的最大值归一化相当于把亮度信息丢掉只保留色度比例这对识别“偏红还是偏绿”最有效。需要提醒的是三个通道不是同一时刻采样的红绿蓝各占一个10ms窗口再加上通道切换延时一组数据实际跨越了大约40ms。对静止物体这没有问题对快速移动的物体三通道会受运动模糊影响。更稳的做法是每个通道连续读三次取平均或者把窗口时间从10ms拉到50ms后者适合弱光但会降低刷新率。3. LCD1602LCD10602在C51上的4位驱动与显示格式3.1 4线接线与引脚分配LCD1602的标准接口有16个引脚但控制字符显示只需要数据线、RS、RW、E四类。8位模式要占8根数据线4位模式只占4根而且速度差异在字符屏上完全感受不到。我一般直接省略RW把它接地这样能再省一个IO只保留RS和E两个控制脚。这里采用一组比较省线的分配LCD的D4到D7接P0.4到P0.7RS接P2.5RW接P2.6E接P2.7。P0口是开漏输出必须外接10k上拉排阻否则高电平驱动能力不足字符会忽亮忽暗。背光LED的限流电阻用一个100欧到330欧的贴片电阻视供电电压而定。LCD引脚接C51引脚说明D4~D7P0.4~P0.74位数据线高4位有效RSP2.50写命令1写数据RWP2.6只写模式接地EP2.7下降沿锁存数据选4位模式还有一层原因C51的P0口经常同时挂外部存储器或其它外设8位全占用会让系统扩展性变得很差。4位模式只占半个端口剩下的引脚可以留给按键、蜂鸣器或者串口。3.2 4位初始化时序和读写函数LCD1602的4位初始化有一个固定套路先发三次0x30让液晶无论处于什么状态都回到8位模式的初始位置第四次发0x20切换成4位模式。之后才能用完整的命令字节配置显示参数。我把它写成一个单独的lcd1602.c核心函数如下。/* lcd1602.c - 4位总线驱动, 适用于STC89C52/AT89S52 */ #include reg52.h #define LCD_DB P0 sbit LCD_RS P2^5; /* 寄存器选择: 0命令 1数据 */ sbit LCD_RW P2^6; /* 读写选择: 0写 1读 */ sbit LCD_E P2^7; /* 使能脉冲 */ void Delay_Ms(unsigned int ms); static void LCD_WriteHalf(unsigned char dat) { LCD_DB (LCD_DB 0x0F) | (dat 0xF0); /* 只改高4位 */ LCD_E 1; Delay_Ms(1); LCD_E 0; } static void LCD_WriteByte(unsigned char dat) { LCD_WriteHalf(dat); /* 先高4位 */ LCD_WriteHalf(dat 4); /* 后低4位 */ } void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_WriteByte(cmd); } void LCD_WriteData(unsigned char dat) { LCD_RS 1; LCD_RW 0; LCD_WriteByte(dat); } void LCD_Init(void) { Delay_Ms(20); LCD_RS 0; LCD_RW 0; /* 三次0x30握手进入4位模式 */ LCD_WriteHalf(0x30); Delay_Ms(5); LCD_WriteHalf(0x30); Delay_Ms(1); LCD_WriteHalf(0x30); LCD_WriteHalf(0x20); /* 正式切到4位 */ LCD_WriteCmd(0x28); /* 4位, 2行, 5x7点阵 */ LCD_WriteCmd(0x0C); /* 开显示, 不显示光标 */ LCD_WriteCmd(0x01); /* 清屏 */ LCD_WriteCmd(0x06); /* 地址自动加一 */ Delay_Ms(2); }LCD_WriteHalf每次只向P0的高4位写数据低4位用原来的值兜住避免影响同一端口上其它外设。写命令和写数据唯一的区别就是RS的电平在命令状态时RS拉低在数据状态时RS拉高。E引脚用下降沿锁存所以先拉高再拉低形成一个完整的使能脉冲。初始化时前三次0x30看起来是同一个值实际是在逐步确认8位模式下的同步边界。如果直接发0x28液晶在上电时序不完整时可能停留在2行模式的某一侧导致后面每行只显示半个字符。0x28把我前面发送的半个字节拆成两次0x30中的一次然后在4位模式下正确配对。3.3 把RGB值转成颜色名称的映射判据归一化之后接下来要做的不是打印三个数字而是给出一个人类能读的颜色名。判据基于一个简单事实纯红色物体的r值会明显大于g和b纯绿色物体的g值会明显大于另外两个蓝色同理。亮度很低的物体一律归为黑色三个值接近且都偏大的归为白色或灰色。unsigned char code *MapColor(unsigned char r, unsigned char g, unsigned char b) { unsigned char max; max (r g) ? r : g; max (b max) ? b : max; if (max 60) return BLACK; /* 三通道接近且都大于180, 视为白或浅灰 */ if (max 180 max - r 30 max - g 30 max - b 30) return WHITE; /* 用整数比较代替浮点, 避免C51的浮点库开销 */ if (r max (unsigned int)g * 10 (unsigned int)max * 6 (unsigned int)b * 10 (unsigned int)max * 6) return RED; if (g max (unsigned int)r * 10 (unsigned int)max * 6 (unsigned int)b * 10 (unsigned int)max * 6) return GREEN; if (b max (unsigned int)r * 10 (unsigned int)max * 6 (unsigned int)g * 10 (unsigned int)max * 6) return BLUE; return MIX; }这里的阈值6/10表示次要通道不超过主通道的60%是室内光源下比较宽松的判断。如果你把物体放在暗处60%可能会把棕色误判成红色这时把阈值收紧到45%左右。比较时用g * 10 max * 6而不是g max * 0.6是因为C51的浮点运算要额外链接浮点库在8位机上会拖慢显示刷新。值得注意的是上面函数返回的字符串常量在C51里默认放在code段所以返回类型写成unsigned char code *。如果你的Keil工程报“指针类型不匹配”多半是因为字符串被放在code段而函数声明成了普通的char *这是C51和标准C差异最明显的地方。4. 完整软件工程的文件划分与Keil5工程配置4.1 main.c、tcs3200.c、lcd1602.c的接口职责拿到一个软件源代码包先看文件划分是否合理比先看代码更重要。我一般会把C51工程拆成四个文件main.c负责主循环tcs3200.c负责传感器采样lcd1602.c负责显示delay.c提供延时。头文件只放函数声明和宏定义不放全局变量定义。文件对外接口内部职责main.cmain()初始化、轮询、显示刷新节奏tcs3200.cTCS_Init、TCS_ReadRGB通道切换、定时器窗口、归一化lcd1602.cLCD_Init、LCD_ShowRGB4位驱动、字符格式化delay.cDelay_Ms毫秒延时软实现这样拆分有个看得见的好处调试颜色识别时不需要翻液晶代码调显示时不需要碰传感器。压缩包里如果只有一个巨长的main.c建议先按这个结构拆开后再去改算法否则每一个小改动都要重新编译整个文件验证周期会拖长。4.2 主循环的采样节奏与刷新节流主循环不需要写成复杂状态机颜色识别和显示天然是顺序关系。为了让屏幕上的数字可读而不是飞快跳变我一般把每次完整刷新的间隔控制在300ms左右也就是大约3帧每秒。#include tcs3200.h #include lcd1602.h void main(void) { RGB_Value rgb; unsigned char code *color_name; Delay_Ms(100); LCD_Init(); TCS_Init(TCS_SCALE_20); LCD_WriteStr(0, 0, TCS3200 Ready); Delay_Ms(500); LCD_WriteCmd(0x01); Delay_Ms(2); while (1) { TCS_ReadRGB(rgb); color_name MapColor(rgb.r, rgb.g, rgb.b); LCD_ShowRGB(rgb, color_name); Delay_Ms(300); } }这段循环里TCS_ReadRGB本身会消耗三组10ms窗口加两次通道切换延时大概40ms剩下的300ms是纯显示节流。如果你把Delay_Ms去掉屏幕会以每秒25帧以上刷新但由于LCD1602本身没有硬件缓冲快速刷新时字符会闪烁人的眼睛反而不舒服。主循环里没有中断这是刻意为之。TCS3200的采样靠定时器同步等待LCD1602的写操作也依赖延时整个流程是顺序的。如果给采样加中断反而会出现中断里改通道、主循环里读计数值的竞态问题。对这个项目来说轮询比中断更不容易踩坑。4.3 Keil5中添加C51芯片包与链接器控制文件Keil5默认安装时只勾选ARM工具链的话新建工程会找不到C51芯片。解决路径不是重新下载整个IDE而是打开Pack Installer找到对应芯片厂商的设备包后点击Install或者在安装Keil5时勾上C51 Compiler组件。同一台机器上同时装C51和ARMSTM32两套工具链是完全常见的它们共用一个uVision界面工程文件后缀都是.uvprojx创建工程时靠芯片型号区分编译工具链。芯片选型后在Options for Target里有两处必须确认。第一是Memory Model一般选Small变量放data区这样访问速度快但data区总共只有128字节如果程序里RGB缓冲区不多Small完全够用。第二是Code Rom Size选Large 64K避免链接器因为代码超过默认段而产生L55类错误。Keil C51会为每个工程自动生成一个链接器控制文件STARTUP.A51它负责在main之前把可重定位段清零、设置堆栈指针。如果程序里用了XDATA的大数组链接器会在映射文件.M51里报告XDATA段的起始地址和大小调试RAM溢出时先看这个文件。不要手动去改STARTUP.A51里的堆栈位置除非你确定XDATA或IDATA的地址冲突改了反而容易把中断向量顶掉。5. 白平衡校准与三处易错点定位5.1 黑白两点标定与归一化公式动态最大值归一化能扛住光源强度波动但扛不住光源色温偏移。白炽灯偏黄、LED偏蓝同样一张白纸在不同光源下读到的RGB比例会变。如果要让颜色名称稳定我会上电后先做黑白两点校准白参考采白纸黑参考采镜头盖或直接把传感器贴近不透光物体。unsigned int cal_white[3] {200, 200, 200}; /* 白参考 */ unsigned int cal_black[3] {10, 10, 10}; /* 黑参考 */ unsigned char ScaleChannel(unsigned int raw, unsigned int white, unsigned int black) { unsigned long v; if (raw black) return 0; v (unsigned long)(raw - black) * 255 / (white - black); if (v 255) v 255; return (unsigned char)v; }ScaleChannel做的是线性拉伸把黑色读数当作0白色读数当作255中间的读数按比例映射。这样处理后即使换了一个色温完全不同的环境只要重新校准白参考和黑参考颜色名称映射就能保持不变。校准值建议放在main函数里在开机时提示用户把白纸贴上传感器延时两秒后读取并暂存。5.2 采样稳定不下来怎么办如果屏幕上的数值在固定场景下仍然跳变优先怀疑三个地方环境光频闪、采样窗口太短、物体距离远近变化。荧光灯每半个工频周期亮度变化一次10ms窗口正好可能横跨暗段和亮段解决办法是把窗口从10ms改成50ms让读数覆盖多个频闪周期。距离问题更常见TCS3200没有透镜传感器表面和被测物体距离超过2cm后杂散光会明显抬高三个通道我一般尽量把传感器贴在物体表面或者用黑色遮光套管固定1cm左右的工作距离。读数彻底为零时先检查OUT有没有接到P3.4而不是P3.5再确认S0/S1组合不是关断状态。5.3 字符显示异常的定位顺序LCD1602显示乱码或白屏按顺序排查先量E引脚有没有下降沿再确认P0上拉到VCC的电阻已经接好最后看初始化时序里三次0x30是否被精简成一次。只发一次0x30的代码看起来省事但遇到上电慢的液晶屏就会漏同步表现为第一行正常、第二行错位或者全是方块。另一个常见问题是RS和E的引脚定义和实际接线不一致代码里写P2^5板上却接到了P2^4这类问题看代码看不出直接拿示波器量引脚电平最快。如果只是刷新时新字符覆盖残留在写第二行前先输出16个空格清行再把颜色名居中就不会出现长短名交替时末尾残留的情况。本文还有配套的精品资源点击获取