简介:面向STM32F407嵌入式开发者的TCS230颜色传感器驱动资源,基于STM32F407ZGT6编写验证,适合需要快速集成颜色识别、色彩检测等功能的项目开发。压缩包共179个文件,核心为48个C源码与48个头文件,涵盖GPIO控制、UART串口通信、定时器中断等关键模块;同时附带Keil工程文件及编译生成的axf、map、o等辅助文件,总大小5.47MB,目录结构完整,便于直接对照学习和二次开发。已有1031人学习下载。驱动代码实现了TCS230传感器的底层时序控制,包括滤波器通道切换、频率读取、RGB数据解算,并通过串口将颜色信息打印输出;此外还提供中断与错误处理思路,可帮助开发者理解颜色传感器工作原理,缩短嵌入式色彩检测功能的调试周期,适配各类颜色识别应用场景。
1. 基于 STM32F407 颜色传感器驱动:拆开这个标题,先解决的不是 I2C
做颜色检测、色卡比对、墨水浓度判断或者小型分拣机的人,大概率踩过同一个坑:传感器明明读出了 R、G、B 数值,可同一张色卡在室内日光灯下和窗边阳光下读出来完全不一样,甚至两个传感器之间读数差得离谱。标题里的“颜色传感器驱动”,真正要解决的不是“能不能读到数”,而是“读到的数能不能稳定反映真实颜色”。这篇笔记围绕 STM32F407 上最主流的 I2C 颜色传感器 TCS34725 展开,从测量原理、硬件接线、HAL 库驱动代码到白平衡校准,完整走一遍落地路径。适合第一次接触颜色传感器的单片机新手,也适合已经从传感器里读到 RGB 原始值、但发现颜色不准的老手——后半部分的校准方法和避坑点,才是这个标题值钱的地方。
2. 从 RGBC 数据到颜色感知:TCS34725 的测量原理与选型对比
2.1 颜色传感器为什么能“看见”颜色:RGBC 四通道与 ADC 积分
颜色传感器并不像人眼那样“识别”颜色,它做的事情是把入射光拆成几个光谱段,分别测量每个段的强度,然后再通过算法还原出颜色。TCS34725 内部有四组光电二极管:R、G、B 三组前面分别覆盖对应波段的滤光片,C 组不加滤光片,所有可见光都能穿过。C 通道就是 Clear 通道,测量的是总光强。
四组光电二极管把光转成电流,电流给积分电容充电,ADC 在一个积分周期结束时读出电容上的电压。这个“积分时间”就是寄存器里的 ATIME 字段,类比成相机曝光时间最合适:曝光时间短,适合强光环境,不然 ADC 直接饱和;曝光时间长,适合弱光环境,让电容多攒一点电荷,提高信噪比。增益则像相机 ISO,放大微弱信号,但也会把噪声一起放大。
C 通道的作用很多人忽略。做颜色识别时,如果直接拿 R、G、B 原始值去判断,光源一亮一暗三个值全都在变。正确的做法是先把 R、G、B 分别除以 C,得到三个比值,再做后续处理。这个归一化过程能消除大部分光强变化带来的影响,比单独读 R、G、B 再设阈值稳定得多。我见过不少方案跳过这一步直接设定 RGB 范围,结果到了现场换个光源整套阈值全部失效,这就是没有理解 Clear 通道存在的原因。
TCS34725 还把颜色传感器的常见痛点前置处理掉了:芯片自带一个 LED 驱动引脚,可以直接接一个白光 LED 做主动补光,把环境光的影响压到最低。这个特性在色卡识别、快递分拣这类需要稳定光源的场景里非常实用,后面硬件章节会专门讲怎么接。
2.2 三种主流方案怎么选:TCS34725、TCS3200 与 ISL29125 对比
颜色传感器按输出形式分成两大阵营:数字量输出和频率输出。TCS34725 是数字量输出的代表,芯片内部完成滤波、积分、ADC 转换,MCU 直接通过 I2C 读寄存器;TCS3200 是频率输出的代表,芯片把颜色强度转成方波频率,MCU 需要用定时器测量频率;ISL29125 则是另一颗 I2C 接口的数字传感器,量程窄但功耗低。
| 对比项 | TCS34725 | TCS3200 | ISL29125 |
|---|---|---|---|
| 输出形式 | I2C 寄存器 | 方波频率 | I2C 寄存器 |
| 通道类型 | R、G、B、Clear 四通道 | R、G、B、Clear 四通道 | R、G、B 三通道 |
| MCU 侧成本 | 少量代码,HAL 库即可 | 需要定时器输入捕获 | 少量代码 |
| 弱光表现 | 积分时间可调,表现好 | 频率低,测量时间长 | 量程偏窄 |
| 功耗 | 中等 | 中等 | 较低 |
| 典型场景 | 色卡识别、屏幕校色、分拣 | 低成本颜色识别 | 电池供电的简单颜色检测 |
TCS3200 有个优势是读取逻辑极其简单,只要测频率就行,而且传感器本体便宜;但它的滤光片切换是让 MCU 通过 GPIO 控制的,需要轮流把 R、G、B 三组通道切换出来测频率,一轮完整测量耗时明显更长,而且在弱光下频率很低,测频周期会拖到几十毫秒甚至上百毫秒。做产品时如果被要求“识别速度要快”,TCS3200 这条路会比较吃力。
我一般直接默认用 TCS34725,原因很实际:I2C 读寄存器比测频率省心,RGBC 四通道同时输出,不会像 TCS3200 那样分时切换,配合中断引脚还能做到颜色变化时主动通知 MCU。ISL29125 适合低功耗场景,但三通道缺少 Clear,光强归一化要多写一些补偿逻辑。如果你的应用是电池供电、只需要粗略区分颜色,ISL29125 合理;否则 TCS34725 是综合代价最低的选择。
2.3 数据手册里必须先圈出的三个参数:积分时间、增益与 I2C 地址
拿到 TCS34725 的数据手册,别从头翻到尾,先找三个地方:电气特性里的供电范围、I2C 地址配置、以及 RGBC 的 ADC 相关寄存器。供电范围决定了硬件方案,TCS34725 工作在 2.7V 到 3.6V,典型 3.3V,STM32F407 也是 3.3V 逻辑,两者可以直接对接,不需要电平转换,这一点对新手很友好。
I2C 地址分两部分:器件固定地址 0x29,再加上两个地址选择引脚 ADDR 和 ADDR_1 的电平组合,能偏移出一共 8 个可选地址。大多数场景两个引脚都接地,用默认的 0x29。很多驱动代码里写的是 0x52,那是把读写位直接拼进地址的 8 位写法,I2C 通信时实际地址还是 0x29 左移一位。接线上如果这个细节没注意,读设备号那一步就会卡住。
增益和积分时间是一对需要配合调整的参数。TCS34725 的增益有 1x、4x、16x、60x 四档,对应 CONTROL 寄存器的 AGAIN 字段;积分时间通过 ATIME 寄存器设置,范围从 2.4ms 到 700ms 左右。基本原则是:弱光环境优先加积分时间,其次再加增益;强光环境下积分时间和增益都要收着用,否则 ADC 输出直接饱和。两者的配合决定了读数的有效位数,也是后面偏色问题最常见的根源。
3. 硬件接线与 PCB 布局:把传感器“喂饱”才能读准
3.1 最小系统接线:I2C 上拉、供电与电平配合
TCS34725 的供电和通信加起来只需要 6 根线,但每一根都不省心。VIN 接 3.3V,在传感器引脚附近放一个 0.1uF 退耦电容;GND 和 MCU 共地,这没什么好说的。SCL 和 SDA 是 I2C 总线,两根线都必须接上拉电阻,注意 STM32F407 内部虽然也有上拉,但阻值太大且可配置性差,直接依赖内部上拉做 400kHz 传输很容易出现偶发通信失败,外部电阻不能省。
上拉电阻的阻值选择影响总线时序:400kHz 快速模式我常用 2.2kΩ,100kHz 标准模式用 4.7kΩ 就够。如果开发板上已经带了 I2C 上拉,外接传感器板时上拉相当于并联,阻值减半,驱动的翻转速度变化不大,但功耗会翻倍,排查功耗异常时要把这个并联关系算进去。
| 信号 | MCU 引脚示例 | 传感器引脚 | 说明 |
|---|---|---|---|
| VCC | 3.3V | VIN | 电压必须稳定,靠近 VIN 放 0.1uF + 10uF 电容 |
| GND | GND | GND | 与 MCU 共地,尽量避免长回路 |
| I2C1_SCL | PB6 | SCL | 加上拉电阻到 3.3V |
| I2C1_SDA | PB7 | SDA | 加上拉电阻到 3.3V |
| 中断 | PB0(可选) | INT | 开漏输出,需要上拉;不做中断可不接 |
| LED 驱动 | 无 | LED | 可驱动一个白光 LED 作为主动补光 |
还有一个容易翻车的是传感器直接靠近大电流器件的情况。TCS34725 的 ADC 积分对电源纹波敏感,如果它和电机、继电器共用一个电源,电机启动瞬间电压跌落会导致 ADC 采样值整体偏移。我一般会把传感器的 3.3V 单独走线,必要时用一个 LC 滤波或者磁珠隔离。电平配合方面,TCS34725 是 3.3V 器件,如果 MCU 是 5V I/O,直接接上去会让传感器的 ES D 保护二极管长期处于微导通状态,时间长了真的会烧;5V 系统必须加电平转换芯片或者用开漏方式配合外部上拉。
3.2 布线注意:传感器离 LED 远一点,离 MCU 近一点
PCB 布局最核心的一句话:传感器面向被测物体,背面尽量远离板上的发热器件和补光 LED。实测中我遇到过两种情况,第一种是补光 LED 距离传感器太近且中间没有遮光挡墙,LED 的光直接从传感器封装侧面漏进去,读数里混进大量固定偏置,白平衡怎么校准都偏色;第二种是传感器旁边走过一根 PWM 控制的 LED 驱动线,PWM 频率几十 kHz,通过寄生电容耦合到传感器模拟前端,低增益时看不出来,增益调到 16x 后读数出现规律的波纹跳动。
解决办法很简单:LED 和传感器之间留出 3mm 以上间距,中间加一个不透光的隔挡,可以是 PCB 上的开槽,也可以直接打一个焊盘孔;PWM 线别从传感器下方穿过;传感器尽量靠近 MCU,I2C 走线控制在 10cm 以内。超过 10cm 的 I2C 线缆建议降低总线速率到 100kHz,并且在上拉电阻旁边并联几十 pF 的电容做滤波,否则波形过冲会带来通信偶发错误。
3.3 用现有开发板快速验证:接线检查清单
如果不想自己做 PCB,直接用开发板飞线验证是最快的方式。TCS34725 模块一般自带 2.2kΩ 上拉电阻,这一项可以省心。飞线时注意杜邦线长度不要超过 15cm,I2C 线越短越好。接好以后先别写代码,上电用万用表量三处:VIN 到 GND 是不是 3.3V;SCL 和 SDA 对地电压是不是高电平(说明上拉正常);模块供电脚对地有没有持续抖动。这三项过了再烧程序,九成能直接读到设备 ID。
我给新手一个我的检查顺序:电压确认 → 静态电平确认 → 读 ID 确认 → 开 ADC 看原始值 → 做白平衡。前面两步如果没确认就写代码调通信,出问题的时候很难区分是硬件还是软件的问题。我曾经就因为一个模块的上拉电阻虚焊,折腾了一下午 I2C 时序,最后却是焊接问题,这个亏不值得再吃一次。
4. 用 STM32F407 读写 TCS34725:HAL 库驱动从初始化到取色
4.1 I2C 通信基础:写寄存器与读数据的时序
TCS34725 的寄存器操作和普通 I2C 传感器有一个关键差别:寄存器地址需要加命令位。芯片手册里要求每次访问寄存器时把最高位置 1,表示这是一次命令操作,同时可以用 bit5 开启动寄存器地址自动递增。刚开始写驱动的人很常犯的一个错误是直接拿寄存器地址去读,比如要读空闲寄存器 0x92,直接发 0x92 而不是 0xB2,整条 I2C 通信其实是成功的,但读回来的永远是设备 ID 寄存器的值,因为命令位没置位,芯片不认。
用 STM32 HAL 库的话,这个细节落在一个地方:控制寄存器地址的传参。比如使能寄存器 ENABLE 的物理地址是 0x00,访问它就要用 0x80 命令位,也就是传 0x80;连续读 RGBC 数据寄存器时,起始地址是空闲寄存器 0x14,加上命令位和自动递增位就是 0x94。数据传输本身没有特殊之处,HAL_I2C_Mem_Write 和 HAL_I2C_Mem_Read 的时序完全覆盖。
I2C 时钟频率建议初始设置成 100kHz,读到设备 ID 之后再拉高到 400kHz。这倒不是芯片支持不了 400kHz,而是飞线情况下 400kHz 容易受寄生电容影响,先把通信跑通,再逐步提速,排错范围会小很多。如果总线上有多个 I2C 设备,尤其里面夹杂着不支持快速模式的器件,整个总线都会被拖慢到 100kHz,这个是总线协议决定的。
4.2 初始化序列:使能 ADC、设积分时间与增益的寄存器值
初始化顺序有一个标准套路,顺序不能乱:先发使能寄存器打开 PON 电源开关,等待芯片内部振荡器稳定,然后设置积分时间和增益,最后把 ADC 使能位打开。很多人会把“开电源”和“开 ADC”混成一步做,结果 ADC 在振荡器还没稳的时候就开始积分,读出来的值要么是零,要么反复横跳。
// TCS34725 初始化:返回 0 成功,-1 为设备 ID 校验失败 int TCS34725_Init(I2C_HandleTypeDef *hi2c) { uint8_t id = 0; // 1. 读取设备 ID,地址 0x92 加上命令位 0x80 -> 0x92 // 设备型号在寄存器 bit0~2,不等于 0x1D 说明通信或型号有问题 if (HAL_I2C_Mem_Read(hi2c, 0x29 << 1, 0x92, I2C_MEMADD_SIZE_8BIT, &id, 1, 100) != HAL_OK) return -1; if ((id & 0x33) != 0x01) // 只比对型号字段,屏蔽版本位 return -1; // 2. 打开电源:ENABLE(0x00) 写 0x03,bit1->PON, bit0->AEN 先不使能 uint8_t en = 0x03; // 后面使能 ADC 时会改写成 0x03 HAL_I2C_Mem_Write(hi2c, 0x29 << 1, 0x80, I2C_MEMADD_SIZE_8BIT, &en, 1, 100); HAL_Delay(400); // 等待内部 RC 振荡器起振,400ms 很稳妥 // 3. 积分时间:ATIME(0x01) 写 0xFF,对应约 2.4ms,适合室内强光 uint8_t atime = 0xFF; // 值越大积分越短;户外强光建议用这一档 HAL_I2C_Mem_Write(hi2c, 0x29 << 1, 0x81, I2C_MEMADD_SIZE_8BIT, &atime, 1, 100); // 4. 增益:CONTROL(0x0F) 的 AGAIN 字段,0x00 表示 1x 增益 uint8_t gain = 0x00; // 室内光线下 1x 一般够,弱光再改成 0x01/0x02 HAL_I2C_Mem_Write(hi2c, 0x29 << 1, 0x8F, I2C_MEMADD_SIZE_8BIT, &gain, 1, 100); // 5. 使能 ADC:重新写 ENABLE,开启 AEN(bit1) 和 PON(bit0) en = 0x03; // PON 保持打开,新增 AEN=1 HAL_I2C_Mem_Write(hi2c, 0x29 << 1, 0x80, I2C_MEMADD_SIZE_8BIT, &en, 1, 100); HAL_Delay(50); // 等第一次积分完成 return 0; }这段代码里几个参数值得单独解释。ATIME 寄存器写入的是“256 减去实际积分点数”,0xFF 是让 ADC 只做很少的积分,整体时间约 2.4ms,适合室内光照充足的环境。如果把 atime 改成 0x65,积分时间大约 50ms,适合光线偏弱的场景,但每次读取的等待时间也变长,快速分拣场景就要权衡。延时 400ms 是参考振荡器起振时间给的保守值,实际大多数情况下 100ms 就够,但初始化只跑一次,多等几百毫秒换稳定性值得。增益位写在 CONTROL 寄存器的低 2 位,这里贴的代码默认 1x,后续如果发现白纸读数太小再调大。
4.3 读取 RGBC 数据与格式转换:从原始值到可用于白平衡的数值
初始化完成后,读取颜色数据的路径是固定的:从 RGBC 数据寄存器起始地址 0x14 连续读 8 个字节,每两个字节一组拼成一个 16 位整数,分别对应 C、R、G、B。读操作本身只需一条 HAL_I2C_Mem_Read,关键是把命令位和自增位都放在起始地址里。
// 读取 RGBC 原始值,c 为 Clear,r/g/b 为三通道,十六位无符号数 void TCS34725_ReadRGBC(I2C_HandleTypeDef *hi2c, uint16_t *r, uint16_t *g, uint16_t *b, uint16_t *c) { uint8_t buf[8]; // 数据按顺序排列:C、R、G、B 各占两字节 // 0x94 = 命令位0x80 | 自增位0x20 | 寄存器地址0x14 // 一次读 8 字节,寄存器地址自动递增,不用发 8 次单字节读 HAL_I2C_Mem_Read(hi2c, 0x29 << 1, 0x94, I2C_MEMADD_SIZE_8BIT, buf, 8, 100); *c = (buf[1] << 8) | buf[0]; // 低字节在前,别颠倒 *r = (buf[3] << 8) | buf[2]; *g = (buf[5] << 8) | buf[4]; *b = (buf[7] << 8) | buf[6]; // 简单光强归一化:输出 0.0 ~ 1.0 的浮点比例,方便后续算法使用 // 除零保护:C 通道接近 0 说明没有有效入射光 if (*c == 0) { *r = 0; *g = 0; *b = 0; return; } // 浮点归一化结果由调用方自行处理,这里保留原始值 }这段读取代码有四个注意点:寄存器地址必须是 0x94,漏了自增位就只能读到一帧数据;字节序是低字节在前,拼 16 位整数时别把高低字节放反,RGB 三个通道会互换;每次读完之后芯片会自动清掉中断状态标志,如果走的是中断模式,这相当于“我来处理了”的回执;最后,C 通道在无光时为 0,需要额外判断并直接置零输出,否则后面做除法时会是一个分母为零的非法值。
读取完成后做归一化时也有讲究。R/C、G/C、B/C 三个比值会受到光源色温影响,比如在暖黄色白炽灯下,即使看同一张白纸,R/C 也会明显高于 G/C 和 B/C。这部分偏色不是传感器的问题,是光源的光谱分布本来就偏红。建议在驱动层只做原始值和归一化输出,把色温补偿放到白平衡校准阶段处理,这样驱动适配不同灯具时不需要改代码。
5. 驱动避坑与常见问题排查:从“读不到数据”到“颜色整体偏掉”
5.1 读不到设备地址:上拉、地址位与总线死锁
现象:调用 HAL_I2C_Mem_Read 读 ID 寄存器时一直返回超时,用示波器抓住 SCL 有波形但 SDA 没有 ACK 信号。这通常是三个原因叠加:SCL/SDA 两根线接反了、外部上拉没接或虚焊、I2C 地址写错。
如果是接反,示波器上一眼就能看出问题,但很多人没有示波器,用万用表量 SCL 和 SDA 的静态电平也能量出来——都应该是高电平,而且上拉电源稳定。排除接反之后再核对地址:0x29 是 7 位地址,HAL 库的 I2C 地址参数要求传入左移后的值,直接传 0x29 就会去访问一个不存在的地址,这是从 STM32F103 Hal 时代就开始的老坑。还有一个隐蔽问题是 I2C 总线死锁:MCU 上电瞬间 SDA 被拉低,而此时传感器还没完全初始化完成,总线就卡在一种谁都释放不了的状态。解决方法是给 SCL 连续发 9 个时钟脉冲,让从机复位状态机,再重新发起通信;严重时需要断电重启整个模块。驱动里建议加一个错误恢复函数,检测到 HAL 库返回 HAL_ERROR 时自动执行这个复位序列。
5.2 数据全为 0:寄存器写入顺序错误导致 ADC 未真正使能
现象:设备 ID 能读到,初始化也不报错,但 RGBC 四个值永远是 0。问题出在 ENABLE 寄存器写入顺序。TCS34725 的 ENABLE 寄存器里 PON 和 AEN 两个位,PON 负责给芯片内部电路供电,AEN 负责启动 ADC 转换。正确顺序是先置 PON 等待振荡器稳定,再置 AEN;反过来操作的话 AEN 为 1 但 PON 还没立起来,ADC 无法运行,读出来的四个寄存器全是默认值零。
解决是把初始化函数改成两段写:第一段 ENABLE 写 0x01(仅 PON),延时 400ms;第二段再写 0x03(PON + AEN)。注意不能用一句“先写 0x03 再等 400ms”代替,因为等待必须发生在两次写操作之间。另外,如果系统上电后 MCU 复位导致传感器还在积分中途,重新初始化时也需要先清掉 ENABLE 里的 AEN,否则可能出现 ADC 状态混乱,读回的值不是满量程就是零。
5.3 数据全为 0xFFFF 或接近满量程:积分时间与增益的饱和配合
现象:白纸放在传感器前面,三个颜色通道直接顶到 0xFFFF,或者盖上黑纸读数依然很高。这是典型的 ADC 饱和,两个原因叠加时会特别明显:积分时间设得太长,增益又开得太大。两者都会让积分电容电压提前冲到满量程,其中增益占主导,因为增益是模拟放大,饱和后信息完全丢失,没有恢复手段。
解决方式是先把增益降到 1x,再把 ATIME 调整到合适区间。有一个简单的迭代步骤:从 ATIME=0xFF(最短积分)和增益 1x 开始读数,如果 C 通道小于几千,就逐步把积分时间延长一个档位,直到白纸的 C 通道落在满量程的 60% 到 80% 之间。这个区间给颜色变化留出了正负余量,又不会因为信号太弱导致低位噪声被放大。记住一个经验:宁可让积分时间短一点、增益低一点,也不要让数据顶到饱和,饱和带来的偏色后期校准都很难拉回来。
5.4 同一种颜色每次读数漂移:光源、遮挡与积分时长的影响
现象:传感器固定不动,同一个物体连续读 10 次,数值上下浮动超过 5%。先排除最基础的因素:传感器正上方有没有手指、外壳或其他遮挡物,这些东西反射的光会进入传感器,相当于环境光变了。然后是光源因素,50Hz 交流供电的荧光灯和 LED 驱动都有频闪,频率在 100Hz 左右,如果积分时间只有几毫秒,等于每次都拍一张曝光时间极短的“快照”,亮度自然跳来跳去。这种情况下把积分时间调整到 20ms 以上,让积分窗口覆盖多个频闪周期,读数就平滑了。如果机构上不方便加遮光,代码上可以做多次平均,但对快速分拣来说,平均次数带来的延时往往是接受不了的,所以优先从光源稳定性和遮挡角度解决。
另一个容易忽略的是传感器和物体的距离。颜色传感器的测量依赖反射光,距离变化 1mm,反射光强可能变化百分之几十。做固定工位检测时,物体必须有限位结构,保证每次出现在传感器正前方的同一高度。同一个角度摆放,否则 RGB 比例不会大变但总光强变化会被 C 通道正常吸收掉,问题不大;可如果物体表面有方向性纹理(比如磨砂金属、布料),角度稍微偏一点反射特性就变了,这时候就算归一化也无能为力,必须用结构件把物体姿态锁死。
5.5 中断引脚悬空导致系统卡顿:复用功能与浮空输入
现象:程序跑着跑着突然进不了主循环,卡死在某个 HAL 库函数里。排查半天发现初始化时把传感器 INT 引脚配成了外部中断输入,但模块上 INT 引脚是开漏输出,默认需要上拉电阻。模块自带上拉时不会有问题,可如果在自制 PCB 上忘了给 INT 加 1kΩ 到 3.3V 的上拉,它就会在浮空电平附近徘徊,MCU 的外部中断被电平抖动反复触发,一直执行回调函数,主循环根本得不到 CPU。
解决方式分两条路。如果不需要中断功能,把 INT 引脚直接不初始化,或者配成 GPIO 输出模式并输出低电平,彻底避免干扰;如果需要用中断,硬件上必须加上拉,代码里在中断回调函数中要等至少几十微秒再读取 RGBC 状态寄存器,因为开漏输出的上升沿本身比较缓,加上线缆电容后边沿会更慢,在边沿不稳定期间去读寄存器容易读到中间状态。
6. 进阶:白平衡校准与颜色判别阈值——把“读数”变成“判断”
6.1 白平衡校准的最小实现
驱动层把 RGB 原始值读出来后,颜色识别的最后一公里是白平衡校准。校准思路很简单:在真实使用环境的光源下,放一张标准白纸,读取此时归一化后的 R/C、G/C、B/C 三个比值,记录为 rBase、gBase、bBase。之后每次测量,把三个通道除以当时的 C 做一次测量归一化,再分别除以 rBase、gBase、bBase,就能把光源色温和反射率的差异压平。校准数据要保存到非易失存储里,上电时加载,不要每次初始化都做。
// 校准函数示例:传入白纸的 RGBC 原始值,保存三个基准系数 void TCS34725_CalibrateWhite(uint16_t r, uint16_t g, uint16_t b, uint16_t c) { // 先做光强归一化,再存基准;之后测颜色就除以这三项 if (c > 0) { calibR = (float)r / c; // 原始比例,通常在 0.3 附近 calibG = (float)g / c; calibB = (float)b / c; } } // 颜色判别函数:待测值除以基准值后,用欧氏距离和阈值比较 uint8_t TCS34725_MatchColor(uint16_t r, uint16_t g, uint16_t b, uint16_t c, float refR, float refG, float refB, float threshold) { float nR = ((float)r / c) / calibR; // 校准后的 R,白色应接近 1.0 float nG = ((float)g / c) / calibG; float nB = ((float)b / c) / calibB; float dist = sqrtf((nR - refR) * (nR - refR) + (nG - refG) * (nG - refG) + (nB - refB) * (nB - refB)); return (dist < threshold) ? 1 : 0; }校准后的数值有一个实用特征:白色物体的 nR、nG、nB 都接近 1.0,其他颜色则分布在 1.0 的上下。这意味着可以在调色板上放几种标准色,分别测一次存成基准,再设置同一个阈值范围做判别。阈值太大会把邻近颜色误判成同一种,太小则允许的色差太小,工业现场一般留 0.1 到 0.2 的余量比较稳。
6.2 色差计算与最简单稳定的判别方式
实际项目里最稳的做法不是算色块的欧氏距离,而是先算三个校准后比值的大小关系。比如做红色油墨检测,校准后 R 明显大于 B,那就只需要判断 rB 和 nR 的差值,而不需要同时比对三个分量,这样单个通道的噪声不会影响整体判断。真正到多色分类时,欧氏距离配合合适的阈值通常比神经网络之类的方式稳定得多——颜色传感器输出的信息量本来就有限,三到六种颜色的分类用距离法已经足够,反而引入模型会带来泛化问题。
联调时建议把校准值、实时 nR/nG/nB、当前距离这三个量通过串口打印出来,做一次产品验证后再固化参数。打印不仅是为了调试,也是记录产品出厂时的颜色特征,方便售后复现问题。我早期做颜色识别项目时,以为传感器读回来就能用,结果每次换一个实验室的灯颜色就偏得离谱,后来把白平衡校准和光源固定做成标准流程,才真正把读数变成“可复现的判断”。第一天调通的驱动不算数,能连续跑一周读数稳定的驱动才叫落地。希望帮到你。
本文还有配套的精品资源,点击获取