首页 / 资讯中心 / 文章详情

基于AS7341的光谱强度显示:STM32驱动与颜色识别实战

基于AS7341的光谱强度显示:STM32驱动与颜色识别实战 ★ FEATURED ARTICLE
简介基于AS7341的光谱强度显示程序是一份面向STM32开发者与颜色识别、环境光测量等应用场景的嵌入式源码工程。程序通过标准库软件模拟IIC协议读取AS7341多通道光谱数据并利用转换函数将光谱值映射为RGB565色彩实时显示在1.44寸TFT屏上可用于颜色识别、植物光照监测等教学与项目原型开发。压缩包共222个文件5.75MB包含STM32标准外设库源文件与头文件如stm32f10x_tim.c、stm32f10x_i2c.c、编译输出文件axf、hex、map以及Keil工程配置文件uvprojx、uvoptx等结构完整可直接在Keil中打开编译调试。已有769人学习下载适合对光谱传感与嵌入式显示感兴趣的开发者参考。资源价值在于可直接复用的驱动与显示代码省去从零配置IIC和TFT的步骤同时提供光谱到RGB的转换思路便于理解多通道传感器数据可视化的实现方法有助于快速搭建光谱检测原型。1. 基于AS7341的光谱强度显示程序它和你玩过的RGB颜色传感器不在一个级别调过RGB颜色传感器的人都有过这种经历同一个目标物换个光源读出来的R、G、B三个值立马变样靠阈值判色基本靠猜。基于AS7341的光谱强度显示程序能带来一种完全不同的玩法——它把可见光切成8个窄带通道加上近红外和Clear通道输出的是“光谱能量分布”而不是被压缩过的三原色。换句话说它能让你“看见”颜色的构成。这份资源适合做颜色识别、物料分选、液体浓度检测或光是拿来做光谱可视化Demo的人。不需要昂贵的分光计一块STM32加一个AS7341模块就能把光谱条形图刷到OLED上。接下来我把拆解它的过程完整写出来包括寄存器配置、SMUX切换、积分时间计算、显示刷新和几个能让你少掉头发的坑。2. 先搞清楚AS7341读回来的到底是什么通道、SMUX与积分时间2.1 十一个物理单元不等于十一个真实“通道”AS7341内部的探测器被划分为多个光谱单元公开资料里常看到的“11通道”指的是两组探测器的总数量第一组是8个可见光窄带通道F1~F8覆盖大约410nm到680nm的可见光范围第二组包括一个近红外通道NIR和一个全波段通道Clear。再加上一组不参与光谱输出的冗余探测单元凑成11这个数字。实际可用的光谱数据点是10个其中F1~F8是颜色识别的主力。单元中心波长/覆盖典型用途F1约415nm紫蓝区域适合区分蓝紫F2约445nm蓝色区域F3约480nm蓝绿过渡F4约515nm绿色区域F5约555nm绿黄区域F6约590nm黄色区域F7约630nm橙红区域F8约680nm深红区域NIR近红外宽带红外补偿、环境光混入检测Clear整个可见波段总亮度参考这10个数据点组合起来就是一份迷你光谱。相比TCS34725那种只有红绿蓝三色加Clear的芯片它的优势在于波长分辨率高出一个量级。比如两个长相接近的红色RGB传感器可能给出几乎一样的数值而AS7341在F6、F7、F8三个通道的响应会出现明显差异这就是光谱测量和颜色传感器最大的区别。2.2 为什么默认配置一次读不出全部通道很多人第一次上手时有个困惑我把使能寄存器打开了ADC数据寄存器也读了怎么只有6个有效值这不是芯片坏了而是它内部的SMUX传感器复用矩阵在起作用。AS7341内部的ADC通道数量少于光谱探测单元数量硬件上通过一个类似开关矩阵的结构把不同波长的探测器分时接入ADC。常见的做法是分两组测量第一组把F1、F2、F3、F4加上Clear接进ADC读出5个值第二组再把F5、F6、F7、F8加上NIR接进ADC再读出5个值。两次测量拼起来才是完整的10通道光谱。资源包里的驱动把这一层封装成了类似as7341_read_all()的接口但你在做二次开发时一定要知道这个高一层的逻辑——否则你自己写寄存器配置时很可能漏掉一组SMUX切换导致得到的“光谱”永远是残缺的一半。2.3 积分时间才是控制灵敏度的真正旋钮AS7341的积分时间不像普通I2C传感器那样写一个寄存器就完事它由两个寄存器配合控制ASTEP和ATIME。芯片内部的振荡频率决定了每个LSB对应的时间基准实际积分时间的计算公式为T_INT (ASTEP 1) × (ATIME 1) × 2.78µs举例来说ASTEP写入999、ATIME写入0时积分时间是1000 × 1 × 2.78µs约2.78ms。如果你把ATIME也改成9积分时间就变成27.8ms。这个参数直接影响低光照下的读数稳定性也直接影响显示刷新的频率。资源包中默认的一组参数是ASTEP999、ATIME0适合室内照明环境。做高灵敏度场景时我一般会先把ATIME拉大而不是急着在代码里做多次采样平均——前者是硬件层面的积累后者只是软件滤波两者效果差距很大。一次完整测量的流程通常是这样先将SMUX切到第一组映射写入命令寄存器启动测量轮询状态寄存器直到数据就绪读取第一组ADC值再切到第二组映射重复上述动作。下面这段伪代码展示了基本流程void read_spectrum_group1(uint16_t *buf, uint8_t len) { // 切换SMUX到 F1-F4 Clear smux_select(GROUP_1); // 写命令寄存器启动一次测量 write_reg(REG_CMD, CMD_START); // 等待状态寄存器bit0置1表示数据就绪 while (!(read_reg(REG_STATUS) 0x01)); // 从ADC数据寄存器依次读取 for (uint8_t i 0; i len; i) { buf[i] read_adc_channel(i); } // 停止测量准备下一次切换 write_reg(REG_CMD, CMD_STOP); }这里的smux_select()只是在内部把一组SMUX配置表写入寄存器这一组配置表在资源包的smux_table.c里可以直接拿到。寄存器地址和命令字我建议不要凭记忆写直接对照包内附的数据手册片段来核对因为不同芯片版本个别保留位可能不一样。反正我每次换一个批次的芯片都会先读一遍ID寄存器确认器件版本。3. 硬件接线与驱动初始化I2C时序决定数据上限3.1 接线表和电平匹配这份资源的代码基于STM32的HAL库I2C通信采用7位地址0x39。接线本身不复杂信号模块引脚接STM32引脚备注电源VDD3.3V2.7V~3.6V不要接5V地GNDGND共地必须接数据SDAPB7示例需要上拉电阻时钟SCLPB6示例需要上拉电阻LED电源VLED3.3V串限流电阻部分模块板载LED已接好中断INT可选GPIO不需要时悬空最容易翻车的地方是VLED。AS7341本身不自带光源需要外部LED照明。如果买的是裸片或最小模块VLED引脚要接一颗LED及限流电阻到电源而不是直接当电源引脚用。在“光谱强度显示”这个场景里照明光源对测量结果的影响非常大后面第5章会展开讲。3.3V电平的STM32和AS7341直连没有电气问题但如果你拿5V的单片机开发板去接SDA和SCL的高电平就可能超标。遇到过有人把SCL接到5V后芯片能通信但读数异常测了半天最后发现是电平问题。3.2 初始化序列可以照抄但建议理解资源包里的初始化函数做了四件事上电、等待稳定、配置SMUX映射、设置积分参数。我从包里抽出一段核心代码做了精简注释// as7341_init.c static void as7341_write_reg(uint8_t reg, uint8_t val) { uint8_t data[2] {reg, val}; // 这里的i2c_write是HAL库封装的写函数对寄存器地址做一次字节编址 i2c_write(AS7341_ADDR, data, 2); } static uint8_t as7341_read_reg(uint8_t reg) { uint8_t val 0; i2c_write_read(AS7341_ADDR, reg, 1, val, 1); return val; } void as7341_init(void) { // 1. 先给芯片上电 as7341_write_reg(REG_ENABLE, 0x01); // 等待内部上电复位完成常见做法是延时5ms以上 delay_ms(5); // 2. 写入SMUX映射表把探测单元切换到ADC输入 // 映射表根据数据手册寄存器0x00~0x2F的配置生成 for (uint8_t i 0; i SMUX_TABLE_LEN; i) { as7341_write_reg(REG_SMUX_BASE i, smux_table[i]); } // 3. 设置积分时间相关寄存器 // ASTEP低字节寄存器 as7341_write_reg(REG_ASTEP_LOW, (uint8_t)(ASTEP_VALUE 0xFF)); // ASTEP高字节寄存器 as7341_write_reg(REG_ASTEP_HIGH, (uint8_t)(ASTEP_VALUE 8)); // ATIME寄存器 as7341_write_reg(REG_ATIME, ATIME_VALUE); // 4. 使能光谱测量等待进入稳定工作状态 as7341_write_reg(REG_CMD, CMD_START); delay_ms(10); as7341_write_reg(REG_CMD, CMD_STOP); }这里有个细节值得注意smux_table[]并不是随便一组数值它的每一位都对应某个探测单元是否接入ADC而且高位和低位的含义和通道顺序有关。资源包中这个表已经按顺序排好我不建议在你还没跑通基础例程时去修改它。ASTEP_VALUE和ATIME_VALUE在头文件里定义默认值分别是999和0。如果你发现室内光下读数偏低优先调ATIME到2或3而不是改SMUX表。3.3 上电时序的几种异常表现初始化后读寄存器如果一直是0xFF大概率不是芯片坏而是通信根本没建立。先量SDA和SCL有没有3.3V上拉电压再确认STM32的I2C时钟速率。我的习惯是初始化为100kHz等所有功能跑通后再提高到400kHz。有时候把速率从400kHz降到100kHz那些偶发通信失败直接消失。AS7341对时序的要求不像部分传感器那么苛刻但总线上如果还挂了OLED或者其他I2C设备就没那么乐观了。资源包的例程里默认I2C速率是100kHz这是稳妥选择。4. 把原始读数变成光谱条归一化计算与显示实现4.1 ADC原始值的含义AS7341的ADC输出是16位无符号数。读出来的原始值直接受积分时间、照明强度、目标反射率三个因素影响。它不像普通温度传感器那样有个固定换算公式真实物理含义是“在某段波长范围内接收到的光子数量经过电荷积分后的数字表示”。所以比较两个传感器的原始值没有意义有意义的比较是同一个传感器、同一种照明条件下不同目标物之间的相对响应差异。在显示程序里第一步是把两组数据拼成一个完整的10通道数组。这一步看似简单但顺序错是常见问题。我见过有人把第二组数据接到第一组前面结果F5~F8的数据跑到F1~F4的位置光谱条形图看起来莫名其妙。资源包里的as7341_read_all()返回的数组顺序是固定的索引0~3对应F1~F4索引4是Clear索引5~8对应F5~F8索引9是NIR。你拿到数据后先打印一遍每个索引对应的原始值确认和你的预期波段一致再往上层走。4.2 归一化与OLED条形图直接显示原始值有个问题不同积分时间下数值范围变化极大条形图要么顶格要么只有短短一截。常用的做法是做最大值归一化把最大通道的值映射到显示器的最大高度。下面这段代码展示了一个精简的采样和显示流程// spectrum_display.c #define SPECTRUM_CHANNELS 10 void spectrum_sample_and_display(void) { uint16_t raw[SPECTRUM_CHANNELS] {0}; uint8_t bar[SPECTRUM_CHANNELS] {0}; uint16_t max_value 1; // 一次完整的10通道采样内部完成两次SMUX分组读取 as7341_read_all(raw); // 找最大值用于归一化 for (uint8_t i 0; i SPECTRUM_CHANNELS; i) { if (raw[i] max_value) max_value raw[i]; } // 把每个通道映射到0~63的条形高度 for (uint8_t i 0; i SPECTRUM_CHANNELS; i) { bar[i] (uint32_t)raw[i] * 63 / max_value; } // 在OLED上按通道顺序画条形图 oled_clear_bars(); for (uint8_t i 0; i SPECTRUM_CHANNELS; i) { oled_draw_bar(i, bar[i]); // 第i根条高度bar[i] } }这段代码逻辑很直接采样、找最大值、归一化、画条。有一个参数需要注意桶高度63不是拍脑袋定的它是OLED屏64像素高度留出顶部一行做边框的余量。如果你用的显示屏是128×32的那63就改成31。另外oled_draw_bar()内部如果用了矩形填充注意把单根条的宽度控制在6~8像素10根条加间距才能在128像素宽的屏幕上放得舒服。4.3 显示刷新频率怎么定刷新率最高不能超过测量周期的倒数。假设积分时间2.78ms两组测量就是约5.56ms加上I2C传输和OLED绘制的时间20Hz刷新率很容易做到。但实际使用中我建议把刷新率限制在10Hz以下。原因是人眼对光谱条形图这类渐变信息有视觉暂留刷新太快反而会看到闪烁和频条跳动。更关键的是光源频闪——普通室内LED灯其实在以100Hz或120Hz的频率闪烁如果积分时间和刷新周期跟它拍频读数就会周期性波动。资源包例程里的主循环用了delay_ms(100)也就是接近10Hz刷新率。这个数值不是随手写的它避开了常见电源频率的拍频区间也让OLED显示的视觉体验比较稳定。做高精度测量时我一般会把连续4次采样的值做平均再显示而不是单次裸奔。平均窗口不要太大否则程序对这个颜色变化的响应会迟钝。4.4 串口输出一个可读的文本光谱调试阶段OLED显示效率可能不高串口打印反而能帮你更快发现问题。用一个简单的循环把通道名和原始值打印出来就够了。看数据时先做两步确认用手机闪光灯照一下F1~F8的原始值应该呈现一个从蓝到红的平滑变化再把手指放到传感器窗口上所有通道值都会大幅下降。如果这两条都符合说明你的采集链路是通的。5. 实测中踩过的五个坑现象、原因与寄存器玄学5.1 读数全是0xFFFF这个现象一般出现在刚上电或者复位后不久。原因有两类第一类是SMUX还没配置完成就开始读ADC寄存器返回默认复位值第二类是I2C地址不对读回来的是总线上的垃圾数据。解决方法是初始化后强制等待20ms再做第一次采样同时用I2C扫描工具确认地址。我之前遇到过一次芯片焊在转接板上地址被一个电阻拉成了另一个值排查了很久。如果你手头有逻辑分析仪直接抓I2C波形是最快的。5.2 同一块色卡两次测量结果差异大两次间隔几秒钟同一目标读数差了20%甚至更多这通常不是芯片问题。最常见的原因是照明不稳定尤其是普通LED灯和市电频率的拍频另一个原因是积分时间设置太短ASTEP0或ATIME0时内部电荷积累太少信噪比不足。解决方法是先按第2章的公式把积分时间调到10ms以上再看波动是否减小。如果波动依然很大就要考虑是不是有环境光从侧面漏进传感器窗口。5.3 条形图顶格但颜色信息丢失显示上所有条都撞到顶部说明信号饱和。原因大概率是积分时间过长或照明过强。AS7341的ADC输出超过65535就会饱和。硬件上可以减小照明LED的电流软件上优先调小ASTEP而不是ATIME。资源包里的ASTEP999在强光下确实会饱和我通常会准备一份“强光配置”把ASTEP降到249ATIME保持0积分时间缩到约0.7ms专门应对白色物体近距离测试。5.4 两组数据拼出来有明显断带F1~F4一组读出来正常F5~F8一组明显偏低或偏高。这是SMUX切换时序引起的。如果两组测量之间间隔太久环境光发生变化前后两组的基线就不同。解决方法是在完成两组读取之前不要做其他耗时操作更不要在两次切换之间去刷OLED。资源包里的实现是连续读完两组再统一处理数据这个顺序不要动。5.5 I2C偶发通信失败现象是偶尔某次采样全部读到0x00程序卡在等待状态寄存器那里。原因通常是总线电容过大或干扰。解决步骤有三步把I2C速率降回100kHz检查SDA/SCL上拉电阻是不是太大模块上用10kΩ上拉在长跳线场景下容易出问题换成4.7kΩ会更稳最后把传感器和STM32之间的杜邦线缩短到10cm以内。这一步做完绝大多数偶发通信问题会消失。6. 让光谱数据更有说服力白平衡与标定系数6.1 白平衡系数的计算原始的光谱数据能看出相对趋势但没法稳定地表达“这是什么颜色”。原因在于LED光源本身的光谱就不是均匀的再加上传感器每个通道的响应率不同直接比较F2和F7的数值没有意义。白平衡校准可以解决这个问题找一块白色参考面在固定照明下采集一组数据记录为white[i]。之后每次测量都乘以一个系数// white_calib.c #define CHANNEL_NUM 10 static float calib_coeff[CHANNEL_NUM] {0}; static uint16_t white_ref[CHANNEL_NUM]; void calibrate_from_white(void) { uint16_t raw[CHANNEL_NUM]; as7341_read_all(raw); // 找到白色参考采集中的最大值 uint16_t max_ref 1; for (uint8_t i 0; i CHANNEL_NUM; i) { if (raw[i] max_ref) max_ref raw[i]; } // 每个通道的系数 最大值 / 该通道白参考值 for (uint8_t i 0; i CHANNEL_NUM; i) { if (raw[i] 0) { calib_coeff[i] (float)max_ref / raw[i]; } else { calib_coeff[i] 0.0f; } white_ref[i] raw[i]; } } void apply_calibration(uint16_t *raw, float *out) { for (uint8_t i 0; i CHANNEL_NUM; i) { out[i] (float)raw[i] * calib_coeff[i]; } }校准后的out[i]中白色物体在响应最好的通道附近会接近最大值而其他通道按比例放大。这样测不同颜色时几个可见光通道之间的相对关系才真实反映目标物本身的光谱特性。6.2 标准色卡校准流程只用白平衡还不够因为芯片每一颗都有制造差异。批量做产品时我建议做两层校准第一层用白色参考面做白平衡第二层用一组标准色卡做线性校正比如白、灰、黑、红、蓝、绿六色。对每个标准色卡采集后记录每个通道的实测值和标准值用分光计或者色卡出厂数据得到建立线性映射关系y a*x b。资源包里没有现成的大批量标定脚本但如果你做单机展示白平衡已经能极大改善颜色识别的一致感。6.3 校准系数存进EEPROM校准只在设备出厂前做一次因此系数需要掉电保存。STM32内部Flash可以复用也可以外挂一颗AT24C02。每次都重新校准不现实毕竟LED会老化、镜头会积灰。我通常在校准函数后加一个保存流程把系数数组写成float存储。读取端在初始化时先检查EEPROM里有没有有效标志位有就直接加载没有就用默认系数1.0。从那以后我每次写AS7341的Demo都会强制走一遍“白色参考校准→保存系数→验证校准效果”的流程哪怕只是做实验室里的一次性验证。这个习惯帮我省掉了至少三次肉眼反复对比颜色的麻烦也让最后拿出来的光谱条形图真正能说明问题。希望这篇拆解能让你少走几步弯路拿到资源后照着顺序跑一遍再根据你的照明条件调一调ASTEP和时间参数基本就能稳定输出一块让外行也能看懂的光谱图。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站