资讯动态

基于C51的TCS3200颜色识别与LCD1602显示实现详解

发布时间:2026/9/12 11:03:32 来源:尧图企业网站定制
简介一份基于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 的每个下降沿。这样定时和计数互不占用同一个硬件资源。源码开头的IT01就是设置 INT0 为下降沿触发EX01打开外部中断initTimer()把 T0 初始化成 10ms 定时TR01启动定时器ET01允许定时器中断最后EA1打开总中断。这个顺序本身就是 C51 中断系统的标准启用流程少一步都可能出现只亮屏不计数的情况。有人会问为什么不用 T0 的计数器模式直接数脉冲原因是 T0 一旦用作计数器就失去了定时能力你没法在同一个 T0 上再产生 1 秒基准。用两个定时器分别做定时和计数当然可以但 51 只有两个定时器还要留一个给串口时就不够了。所以采用“INT0 计数 T0 定时”的配合是最省硬件资源的经典方案。2.3 定时初值计算与颜色通道切换以 12MHz 晶振为例机器周期是 1µsT0 工作在方式 1 的 16 位定时器要定时 10ms 就需要计数 10000 个机器周期于是初值是 65536-1000055536也就是 0xD8F0。Keil 里初始化代码通常写成下面这样void initTimer(void) { TMOD 0xF0; // 清空 T0 相关位 TMOD | 0x01; // T0 方式116位定时器 TH0 0xD8; // 装入初值高字节 TL0 0xF0; // 装入初值低字节 }TMOD 0xF0是为了不影响 T1 的配置这是一个容易被忽略的细节。如果不加这句直接给 TMOD 赋值串口波特率发生器可能被改坏。初值填入 TH0、TL0 后每次定时器溢出进入中断都要重新装入初值否则时间基准就从 0 开始数满 65536变成 65.536ms最后测出来的频率整体偏低。S2/S3 的真值表和颜色通道对应关系如下S2S3通道00红01蓝10绿11透明无滤波采样时按红、蓝、绿、透明依次切换每个通道各测一个 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.uvprojKeil 工程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表示外部中断 0interrupt 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 这三条指令对应关系如下指令含义0x388位数据、双行、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.5msHD44780 执行清屏指令比普通命令慢有些代码在清屏后直接写数据会丢首个字符这一点是 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%这样能在分辨率与中断负载之间找到平衡点。调整这个参数就能在不改硬件的前提下找到当前光源下的最佳工作点。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价