
简介基于STM32F103的TCS34725颜色传感器驱动例程面向嵌入式开发者和单片机学习者解决颜色识别、RGB光强度检测中I2C通信与寄存器配置等关键问题适合课程设计与毕业设计参考。压缩包内含完整驱动源码如TCS34725_I2C_Write/Read函数实现以及寄存器操作宏定义与配置示例并整合Keil MDK工程可直接打开编译下载验证。压缩包共135个文件以c源文件与h头文件为主另有uvprojx工程文件、o目标文件、crf交叉引用文件、axf可执行文件、hex烧录文件、bat批处理脚本等覆盖源码编写、编译中间过程与最终烧录产物工程目录分组清晰整体仅3.13MB。已有660人学习下载适合用于智能小车巡线、颜色分拣、环境光检测等项目快速移植驱动也可作为学习STM32硬件I2C时序的参考资料。1. TCS34725 和 stm3F102 组合真正的难点不在 I2C 协议TCS34725 是一颗带红外滤光片的颜色传感器通过 I2C 输出 RGB 和 Clear 四路 ADC 数据。标题里的 stm3F102 显然对应 STM32F103但同一份源码例程在不同人手里表现完全不同原因不在传感器而在驱动里对寄存器、积分时间和数据处理的理解。常见现象是读回全 0、颜色偏紫、数值跳变这些多数和 I2C 硬件关系不大。接下来的内容从寄存器表出发给出 STM32F103 上可编译的 HAL 骨架再把原始值变成颜色判断、色温和亮度的工程做法最后用两个现场验证技巧收尾。新手可以按顺序直接复用有经验的人重点看第 4 章的校准和第 5 章的超时恢复思路。2. TCS34725 的寄存器、通道与时序驱动例程先要定下 4 个参数2.1 四个数据通道Red 寄存器里存的是“曝光后的原始积分值”TCS34725 内部有 4 组光电二极管分别是 R、G、B 和 Clear。Clear 不经过颜色滤光片接收经过红外滤光后的全光谱在很多例程里起着“环境光总量”的作用。四路输出都换算成 16 位计数存放在 0x14、0x16、0x18、0x1A 开始的连续寄存器中。移植的时候要记住Red 寄存器的值不是“红色”它是在指定积分时间、指定增益下对红色波长段的积分结果。同一个红色物体积分时间从 2.4ms 改成 24ms数值会放大 10 倍左右增益从 1x 改成 4x数值又放大 4 倍。所以任何基于绝对阈值的判断都必须和积分时间、增益绑定否则换一个配置就失效。Clear 通道的意义在于归一化。把 R、G、B 各自除以 Clear得到的比例基本能抵消积分时间和增益带来的公共因子。这也是后面颜色判断的入口。例程里最常犯的错误是把 R、G、B 当作三个独立变量去卡阈值结果换个光照强度整个逻辑全乱。2.2 常用寄存器表初始化代码只操作 5 个地址TCS34725 的寄存器读写不是裸地址第一个字节必须带上 0x80 命令位如果需要连续读两个字节再加 0x20 自动递增位。例如读红色数据 0x16真正发送的寄存器地址是 0x80 | 0x16读红和绿两组可以发 0x80 | 0x20 | 0x16然后连续取 4 字节。驱动源码例程中常见的0x29 1就是 I2C 地址左移7bit 地址 0x29 在 STM32 HAL 里要变成 8bit 写地址 0x52。寄存器名地址作用常见值ENABLE0x00PON 上电、AEN RGBC 使能0x01 后 0x03ATIME0x01积分时间2.4ms/计数0xFF2.4ms0xF624msWTIME0x03休眠等待时间例程常设为 0xFFCONTROL0x0F增益低 2 位0x001x0x014x0x0216x0x0360xCDATA 低字节0x14Clear 数据起始只读RDATA 低字节0x16Red 数据起始只读GDATA 低字节0x18Green 数据起始只读BDATA 低字节0x1ABlue 数据起始只读这里要特别说明 WTIME 和 ATIME 不是同一个东西。ATIME 控制 RGB/C 四个通道的积分时长决定灵敏度WTIME 是 RGBC 使能后的一个节流等待用来降低功耗不影响积分公式。例程里如果只想要一次性读数可以把 WTIME 设大减少模块平均电流但如果要连续采集做动态监测WTIME 太大会让数据刷新率变低。2.3 最小初始化序列PON、延时、AEN顺序不能颠倒初始化 TCS34725 的顺序在例程里经常被压缩成一句复制粘贴但真正动手时一定要拆开看。芯片默认 POR 后 PON0所有模块不转换。此时读数据寄存器只能拿到 0。必须先置 PON1 等内部振荡器起振一般延时 10ms再设置积分时间和增益最后把 AEN 置 1 打开 ADC。AEN 置 1 之后至少要等一个积分周期才能读到有效数据。下面是最小的初始化代码#define TCS_I2C_ADDR 0x29 #define TCS_CMD_BIT 0x80 #define TCS_ENABLE 0x00 #define TCS_ATIME 0x01 #define TCS_CONTROL 0x0F static void tcs_write8(uint8_t reg, uint8_t value) { uint8_t buf[2] { TCS_CMD_BIT | reg, value }; // HAL_I2C_Master_Transmit(hi2c1, TCS_I2C_ADDR 1, buf, 2, 50); } void tcs_init(void) { tcs_write8(TCS_ENABLE, 0x01); // PON 1只上电不动 ADC HAL_Delay(10); tcs_write8(TCS_ATIME, 0xF6); // 24ms 积分数值约是 2.4ms 的 10 倍 tcs_write8(TCS_CONTROL, 0x00); // AGAIN1x tcs_write8(TCS_ENABLE, 0x03); // PON | AEN打开 RGBC HAL_Delay(30); // 等至少一个积分周期 留余量 }注意这里的tcs_write8先发命令位再发数据这是 TCS34725 和普通 I2C 温湿度传感器最大的差异。如果直接按寄存器裸地址发送模块会拒绝响应表现为主机读回来全是 0xFF 或总线 ACK 异常。参数上我把 ATIME 设成 0xF6 而不是最常用的 0xFF原因很简单24ms 在绝大多数室内光下能把白纸的 Clear 拉过 1000 以上0xFFFF 饱和风险也低。如果环境光特别暗再把 CONTROL 调成 0x0216x 增益或 0x0360x。3. 在 STM32F103 上用 HAL 编写 TCS34725 驱动源码例程最小骨架3.1 引脚分配、I2C 地址与 4.7kΩ 上拉STM32F103 上跑 I2C 最简单的是用硬件 I2C1引脚固定在 PB6/PB7。TCS34725 模块多为 3.3V 供电注意不要接到板子 5V 排针。7bit I2C 地址一般是 0x29部分模组会把地址选择脚拉高变成 0x39拿到手先看丝印和原理图。HAL 库里HAL_I2C_Mem_Read的 DevAddress 参数需要左移一位写成0x29 1很多人在这里多写了一次左移结果地址变成 0xA4 导致 No ACK。STM32F103 引脚TCS34725 引脚说明PB6SCL硬件 I2C1 时钟开漏输出PB7SDA硬件 I2C1 数据开漏输出3.3VVCC不要接 5VGNDGND共地PB5可选INT中断输出低有效上拉电阻我一般选 4.7kΩ 到 3.3V。TCS34725 的 SCL/SDA 是开漏模块板上通常会带如果模块上没有上拉电阻STM32 的 GPIO 配置成开漏之后必须外部加否则总线会卡在低电平状态。内部上拉在标准库和 HAL 里都能配但阻值偏大、脉冲边沿慢400kHz 模式下容易出现数据错所以不建议省外部电阻。I2C 速度配 100kHz 或 400kHz 都行先 100kHz 调通再提速度。3.2 底层读写函数命令位、16 位组合和错误返回下面是 HAL 下的两个底层函数。第一个用于单字节寄存器读第二个用于连续读 16 位数据返回值采用小端序组合。注意所有读操作都要带上TCS_CMD_BIT | reg作为 mem address。#define TCS_ADDR (0x29 1) #define TCS_CMD_BIT 0x80 uint8_t tcs_read8(uint8_t reg) { uint8_t val 0; HAL_I2C_Mem_Read(hi2c1, TCS_ADDR, TCS_CMD_BIT | reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); return val; } uint16_t tcs_read16(uint8_t reg) { uint8_t buf[2] {0, 0}; HAL_I2C_Mem_Read(hi2c1, TCS_ADDR, TCS_CMD_BIT | reg, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); return (uint16_t)buf[1] 8 | (uint16_t)buf[0]; }第一个函数用来读 ID 和状态寄存器第二个读颜色数据。TCS34725 的数据寄存器低字节在前所以buf[1]是高字节组合时用左移。HAL_I2C_Mem_Read的第 4 个参数I2C_MEMADD_SIZE_8BIT表示寄存器地址是 8bit这点在 STM32F103 的 HAL 里必须明确给出否则内部时序会对不上。工程上还要检查HAL_I2C_Mem_Read的返回值。如果返回HAL_BUSY很可能是 I2C 总线被其他任务占用如果返回HAL_ERROR基本是 No ACK。调试时我会在函数里加一句if (HAL_I2C_Mem_Read(...) ! HAL_OK) return 0xFF;这样主程序至少能区分通信失败和数据异常。3.3 初始化、ID 校验和单次读色可直接复用的完整函数上一个例程的tcs_init没有校验芯片是否在线。实际工程里我会加 ID 读取TCS3472x 系列的 ID 寄存器在 0x12高 4 位固定为 0x4。这样 I2C 地址、接线错误都能在上电第一时间暴露而不是等到读颜色数据时靠观察判断。#define TCS_ID_REG 0x12 #define TCS_ENABLE 0x00 #define TCS_ATIME 0x01 #define TCS_CONTROL 0x0F typedef struct { uint16_t c; uint16_t r; uint16_t g; uint16_t b; } tcs_rgbc_t; uint8_t tcs_init(void) { uint8_t id tcs_read8(TCS_ID_REG); if ((id 0xF0) ! 0x40) return 0; // 通信失败或芯片型号不匹配 tcs_write8(TCS_ENABLE, 0x01); // PON 上电 HAL_Delay(10); tcs_write8(TCS_ATIME, 0xF6); // 积分 24ms tcs_write8(TCS_CONTROL, 0x00); // 1x 增益 tcs_write8(TCS_ENABLE, 0x03); // 打开 ADC HAL_Delay(30); return 1; } void tcs_read(tcs_rgbc_t *v) { v-c tcs_read16(0x14); v-r tcs_read16(0x16); v-g tcs_read16(0x18); v-b tcs_read16(0x1A); }这段代码中tcs_read16每个通道单独发起一次 I2C 传输读四个通道要 4 次约 1ms 的额外开销在 50Hz 的轮询频率下完全可接受。如果需要更高效率可以改成连读 8 个字节起始地址 0x80|0x20|0x14一次取回四个通道然后组合。自动递增位 0x20 的作用就是让模块内部把地址从低到高推下去不需要主机反复发命令。HAL 版例程还有个容易忽略的配置hi2c1的ClockSpeed字段。CUBE MX 默认生成 100kHz 时可以工作但如果代码里hi2c1.Init.ClockSpeed 400000同时 GPIO 又没有外部 4.7kΩ 上拉波形上升沿就会明显变缓高电平可能达不到判定阈值数据随机跳变时先把ClockSpeed改回 100000 测试一次能复现就是上拉不完整。4. 原始值怎么变成颜色判断Clear 归一化、校准和 Lux 估算4.1 用 Clear 通道做归一化摆脱积分时间影响拿到了四个原始值接下来就是算法。第一步不是算色温而是判断当前数据是否可信。所谓可信有两个条件第一Clear 值不能太小否则信噪比很低第二R、G、B 任一值不能等于 0xFFFF否则已饱和当前积分时间或增益太高。这两个条件必须在归一化之前检查。归一化最简单的方法是计算各通道占 RGB 总和的比例例如红色比例是r / (rgb)。Clear 参与比例计算会稀释颜色信息因为 Clear 的值通常比 RGB 任何一个都大。颜色判断用 RGB 之和即可。下面的代码用整数实现红色判断int tcs_is_red(const tcs_rgbc_t *v) { uint32_t sum (uint32_t)v-r v-g v-b; if (sum 200) return 0; // 太暗不可判 if (v-r 0xFFFF || v-g 0xFFFF || v-b 0xFFFF) return 0; // 饱和先用短积分 return ((uint32_t)v-r * 100 / sum) 50; }这里* 100 / sum是为了避免浮点算出来的比例整数部分超过 50 就认为是红色主导。阈值 50 对红、绿、蓝三色比较对称实际应用里建议改成可调参数。室内 LED 光源下白色物体三个通道几乎相等红色物体比例通常在 55 以上如果物体表面有反光比例会下降这时就需要配合白平衡校准再做判断。4.2 白平衡校准用一张白纸把通道比例拉平颜色传感器不是相机三通道响应不均匀所以同一张白纸在不同光源下读出的 R/G/B 比例并不相等。标准做法是在目标环境下先采集白纸数据得到校准系数然后对后续测量值做通道增益修正。校准原理是把白色读数的 G 当作基准把 R 和 B 乘到与 G 相等。typedef struct { uint16_t kr, kb; } tcs_white_cal_t; void tcs_calibrate(const tcs_rgbc_t *white, tcs_white_cal_t *cal) { if (white-g 0) { cal-kr 1000; cal-kb 1000; return; } cal-kr (uint16_t)((uint32_t)white-g * 1000 / white-r); cal-kb (uint16_t)((uint32_t)white-g * 1000 / white-b); } void tcs_apply_white(volatile tcs_rgbc_t *v, const tcs_white_cal_t *cal) { v-r (uint16_t)((uint32_t)v-r * cal-kr / 1000); v-b (uint16_t)((uint32_t)v-b * cal-kb / 1000); }校准系数用 1000 倍整数保存避免浮点。调用顺序是采集白纸原始值算好cal以后每次读色后调用tcs_apply_white再做比例判断。如果环境光源换了比如从白炽灯换成荧光灯白纸校准必须重新做否则颜色判断会整体偏黄或偏蓝。tcs_apply_white里我把参数声明成volatile是因为这种函数经常直接在传感器采集线程里调用防止编译器把变量优化掉。4.3 Lux 估算给亮度一个绝对参考值如果项目不只要识别颜色还要衡量环境亮度可以在积分时间和增益已知时把 Clear 转成 Lux。常见做法是先算 counts per second即每秒计数再乘一个由标准照度计标定出来的系数。注意系数不是芯片出厂统一的因为模块窗口的透光率、安装位置都会影响绝对值。uint32_t tcs_get_lux(const tcs_rgbc_t *v, uint8_t atime_reg, uint8_t gain_reg) { float atime_ms (256 - atime_reg) * 2.4f; float again; switch (gain_reg 0x03) { case 1: again 4.0f; break; case 2: again 16.0f; break; case 3: again 60.0f; break; default: again 1.0f; break; } float cps v-c / (atime_ms / 1000.0f) / again; return (uint32_t)(cps * 0.42f); }0.42f不是固定参数是标准模块上的经验值不同模组换壳或换遮光片后需要用照度计在 1000、500、100 Lux 三档重新标定。至少能得到单调可用的亮度值。不用浮点的话可以把系数变成整数例如(cps * 42) / 100但要注意cps在暗光下可能只有几千整数不会溢出。4.4 数据异常定位先区分 I2C 问题还是光路问题现象优先怀疑方向检查动作Read 全 0使能寄存器没写进去读回 ENABLE 看是否 0x03某一通道恒 0xFFFF饱和增大 ATIME 或降低增益RGB 都随电源周期性波动光源频闪积分时间改为 20ms 的整数倍I2C 读返回 HAL_BUSY总线被长占用示波器看 SCL 是否有毛刺颜色判断有时对有时错白平衡未重校准换成白纸重新测量这表里最容易被忽视的就是第四行。STM32F103 的硬件 I2C 在总线上有异常时序时不会自动恢复经常卡在 HAL_BUSY 上。我的做法是在主循环里加一个 200ms 超时超过就调用HAL_I2C_DeInit再HAL_I2C_Init重新初始化。虽然粗暴但现场比一直挂死强。5. 例程跑不顺时先抓时序而不是继续改寄存器5.1 逻辑分析仪抓两段波形就能定位 90% 的问题第一个验证是上电后 100ms 内的初始化波形。逻辑分析仪两路分别接 PB6/PB7触发沿设为下降沿抓到 STM32 向 0x29 发送的第一个字节。正确情况下第一次传输的地址字节应该是 0x520x29 左移一位后写方向随后跟命令字节 0x80再跟 ENABLE 0x01。如果逻辑分析仪显示地址是 0xA4 或 0x52 之后直接 NAK说明地址换算或地址引脚接错。第二个验证是连续读取时的波形。调用tcs_read16(0x16)后波形上应该有START、0x52、0x960x80|0x16、重复 START、0x53读地址、读 2 字节。缺少重复 START 的波形多半是 HAL 配置里I2C_MEMADD_SIZE_8BIT设置不对或者用了模拟 I2C 但没有正确处理读写方向位。5.2 用 INT 引脚省掉高频轮询如果主程序需要在睡眠状态下等待颜色变化可以把 TCS34725 的比较器阈值打开。设置AILT0x04 和 0x05和AIHT0x06 和 0x07然后把ENABLE的AIEN位置 1再把 INT 脚接到 STM32 的外部中断输入。Clear 小于低阈值或大于高阈值时模块会把 INT 拉低。这个机制比主循环主动读值更省电适合分拣设备在传送带到位时才唤醒的场景。注意阈值比较用的是 16 位原始值如果对 Clear 做了 Lux 换算阈值要换算回原始值才能填寄存器。5.3 澄清一个“驱动”概念的坑这里的“驱动程序”指的是设备固件和上位机例程和 Windows 设备管理器里“由于 Windows 无法加载这个设备所需的驱动程序代码 31、代码 39”这类系统驱动不是一回事。嵌入式驱动不需要数字签名也不需要 inf 文件它本质上是把传感器寄存器访问序列封装成.c和.h。网上类似的 TCS34725 例程压缩包解压后常见结构就是tcs34725.c、tcs34725.h和main.c其中main.c只负责初始化 I2C 和调用颜色读取函数。最后补一个实用技巧把tcs_init()的返回值判断写成if (!tcs_init()) { while(1); }上电先确认通信正常再读数据否则例程里所有报错都会指向光路问题。本文还有配套的精品资源点击获取