ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STK3310距离传感器驱动实战:光学串扰与阈值设计要点

STK3310距离传感器驱动实战:光学串扰与阈值设计要点

简介:面向MTK平台嵌入式开发者的STK3310距离传感器驱动源码包,解决硬件缺乏I2C控制器时通过GPIO模拟I2C时序完成接近感应通信的问题,适用于智能手机、平板等移动设备的屏幕亮度调节与通话熄屏场景,也适合驱动开发入门者对照学习。压缩包内共4个文件,由两个C源文件与两个头文件组成,整体仅6KB,代码结构紧凑,包含初始化、传感器识别、寄存器读写、错误处理等关键模块。驱动已在MTK平台实测可用,阅读后可掌握GPIO模拟I2C的时序实现思路、STK3310寄存器操作流程,以及将驱动接入设备树和上层API的要点。已有1247人学习下载,可作为嵌入式驱动调试与传感器移植的参考。

1. STK3310 距离传感器驱动:先别急着读寄存器,你要解决的是光学串扰

STK3310 距离传感器驱动,做嵌入式的人一听就明白,I2C 读写本身不难,难的是贴在屏幕后面那点光学串扰,它能让所有阈值算法瞬间变成玄学。这颗芯片常出现在手机防误触、手表入耳检测、接近唤醒灯光这些场景里,同时提供接近检测(PS)和环境光(ALS)两个通道,但真正让人熬夜的是 PS 通道的底噪和误触发。我会从芯片检测原理讲到设备树与中断处理,再给出 sysfs 接口和五条踩坑记录。适合手里有屏幕项目、正在为贴屏接近检测投入时间的嵌入式开发者,也适合评估这颗传感器到底值不值得用在自己的方案里。

2. 看懂 STK3310 的检测原理与寄存器:接近值为什么不能只看原始数据

2.1 红外发射与反射光接收:PS 数据的物理来源在哪里

STK3310 内部结构并不复杂:一颗红外 LED、一个接收红外光的光电二极管、一个把光电流转成数字的 ADC,外面再包一层 I2C 接口。驱动对它做的事情,本质上就是对光源和 ADC 做配置,然后周期性地把转换结果读回来。和超声波、激光测距不同,它没有飞行时间或相位差概念,它测量的就是反射光的强度,所以它回答的问题是“有没有东西靠近”,而不是“那个东西离我几厘米”。这一点决定了整个驱动设计方向:我们要做的是稳定地区分贴近和没贴近,而不是追求精确的物理距离。

从光链路看,LED 发出的红外光要穿过玻璃盖板,到目标物表面再反射回来,再次穿过盖板才进入光电二极管。这段路程里损耗最大、最不可控的环节不是目标物,而是盖板本身。玻璃和空气的折射率差异会带来固定的界面反射,屏幕模组的层叠结构会让红外光在内部来回乱窜。于是你会看到一种非常典型的现场:芯片在裸板上读数很低,装进整机、贴上盖板后,无物体状态的 PS 读数从十几跳到两三百。这个固定抬升的部分,就是串扰。

PS 通道接收的还不只是目标反射和串扰。太阳光和室内照明里都含有红外成分,它们会直接叠加进 PS 读数里。STK3310 内部有环境光抑制机制,但并不是万能的,所以芯片单独设计了一个 ALS 通道,用来读出当前环境光强度。成熟的驱动会同时读 PS 和 ALS,在 ALS 变化剧烈时对 PS 结果做一次补偿,或者用 ALS 的变化判断“是不是光照变化导致误报”。低成本驱动往往忽略这一步,结果就是同一套阈值,白天正常、晚上乱报,最后只能靠调高阈值牺牲灵敏度来掩盖问题。

另一个影响原始读数稳定性的因素是采样噪声。PS 数据在读数末位通常有 2 到 3 个 LSB 的随机跳动,加上系统里其他信号的串扰,短时间内的原始值波动会明显超过预期。我一般会连续读 4 到 8 次,去掉最大最小值后取平均,再做状态判断。这个习惯看起来多花了几次 I2C 传输,但能省掉后面大量调试阈值的时间,尤其是量产阶段,这点毛刺往往就是误报的来源。

2.2 寄存器地图:配置、数据、中断三类寄存器先分清

STK3310 的寄存器按作用可以分成三类:配置类、数据类、中断状态类。配置类负责决定 LED 电流大小、PS 增益档位、采样率、芯片是否进入睡眠;数据类保存最近一次 PS 和 ALS 转换结果;中断状态类记录哪个事件发生了,需要驱动读取后主动清除。下面是我习惯的分类表,具体寄存器偏移以你手里那版数据手册为准,不同批次的地址定义可能不完全一样,驱动里不要写死一堆十六进制魔数。

类别寄存器作用关键位域
配置PS_CONFPS功能开关、增益档位、采样率PS_gain、PS_rate、PS_IT
配置ALS_CONFALS增益与采样率ALS_gain、ALS_rate
配置INT_CONF中断使能与事件选择ALS_INT_EN、PS_INT_EN
配置PS_THD接近阈值与释放阈值PS_THD_HIGH、PS_THD_LOW
数据PS_DATA最近一次PS转换结果无符号数,有效位见数据手册
数据ALS_DATA最近一次ALS转换结果无符号数,低端读数可能不可靠
状态INT_FLAG中断标志位PS_FLAG、ALS_FLAG

PS 增益和 LED 电流是最先要定下来的两个参数。LED 电流越大,出射红外越强,检测距离越远,但串扰也水涨船高;增益可以放大微弱反射,同时把噪声一起放大。我一般先把增益调到中等档位,再从小到大调整 LED 电流,目标是找一个“无物体底噪不超过满量程 15%、贴近 5 毫米时读数能翻两倍以上”的工作点,而不是一味追求最大读数。这个工作点每个项目都要单独找,屏幕模组一变,参数就得重测。

采样率代表芯片每秒做多少次 PS 转换。一次转换不只有 ADC 采样时间,还有 LED 点亮时间,功耗大头就烧在这里。做屏幕防误触的项目,每秒 10 到 20 次已经足够;做入耳检测或唤醒类应用,可以降到每秒 2 到 5 次。采样率低的代价是响应变慢,物体快速靠近时要等一两个采样周期才能被捕捉到,所以采样率要和业务响应要求一起定。比如唤醒类场景必须保证从靠近到触发不超过 200 毫秒,那就至少按 10Hz 来配。

中断机制值得单独强调。STK3310 的 INT 引脚低电平有效,芯片检测到接近事件后会拉低 INT 或产生脉冲,驱动收到中断后要去读 INT_FLAG 判断具体事件,再通过写清除位的方式把标志清掉,INT 引脚才会恢复高电平。这个流程如果漏了清除步骤,中断会反复触发,看起来像芯片坏了,实际上只是没按它的握手协议办事。中断标志一旦积压,读数据、清标志就要在一个处理函数里连续做完,直到确认引脚恢复。

2.3 阈值与滞回:为什么直接拿 PS_DATA 判断不可靠

许多人第一次接触距离传感器,习惯写“读数大于 X 就认为接近”,这个思路在 STK3310 上会翻车。因为无物体底噪不是固定不变的,它随温度、电压、盖板应力漂移,环境红外的分量也在变化。如果只设一个阈值,物体停在阈值附近时,读数每抖动几个 LSB,状态就在接近和远离之间来回横跳,上层看到的就是高频抖动的事件流,这种问题用去抖根本无法根治,只能靠阈值结构解决。

正确做法是用一对阈值做滞回。比如无物体底噪是 100,贴近后的读数是 400,那把接近阈值设为 250,释放阈值设为 180。物体靠近时只有读数超过 250 才上报接近;离开时必须跌到 180 以下才能上报离开。中间 180 到 250 这段区间就是滞回带,噪声抖动被它吸收掉了。两段阈值的间隔,建议至少留出满量程的 10%,太窄起不到滞回去抖作用,太宽则会让离开响应显得迟钝,用户握持手机时会出现“已经离开屏幕了,状态还停在接近”的感受。

阈值之上还要加一道软件去抖。硬件只能保证单次读数越过阈值,不能保证这不是瞬时毛刺。驱动里我维护一个 3 到 5 次的计数窗口,连续 N 次读数都越过阈值才切换状态,任何一次没达到就把计数清零。这个 N 配合采样率可以换算成毫秒级的去抖时间,比如采样率每秒 10 次、N 取 3,意味着持续 300 毫秒的稳定信号才被认可,足以滤掉大部分瞬时遮挡干扰,比如衣角快速掠过。

另一个容易被忽略的点是 PS_DATA 的读取时机。芯片正在转换过程中读数据,可能读到上一帧旧值或者新旧拼接的脏值。可靠做法是先查状态寄存器里的转换完成位,或者连续读两遍取一致值。状态寄存器位域在数据手册里有明确说明,驱动里的读数据函数不要直接拿i2c_smbus_read_word_data的返回值就用,至少要加一个二次确认逻辑。看到这里应该已经明白,STK3310 驱动的工作量不在 I2C,而在配置、去抖、补偿这三件事上,后面章节的实现就围绕它们展开。

3. 驱动方案选型与最小环境搭建:从I2C探测到设备树配置

3.1 三种驱动路线对比:裸机轮询、RTOS事件驱动、内核IIO/Input

拿到 STK3310 之后先别写代码,要看板子上的软件环境。常见做法无非三种:裸机轮询、RTOS(或裸机+中断)事件驱动、Linux 内核框架驱动。我见过不少项目在这个选择上反复改版,所以先把对比放在这里。三种路线没有绝对好坏,关键在于你的主控资源、功耗预算和量产维护成本。

方案适用环境时序与功耗控制代码量调试难度
裸机轮询8位/32位MCU无RTOS简单,忙等,功耗偏高少低
RTOS事件驱动FreeRTOS等RTOS中断+任务,低功耗友好中等中
Linux IIO/InputSoC跑内核依赖子系统与睡眠机制多偏高

如果你的目标是在一个跑 Linux 的主控上做接近检测,我不建议自己从零写一个操作 I2C 的字符设备驱动。内核已经有 I2C 框架、设备树、中断系统和 input 子系统,正确路线是写一个符合内核接口的驱动,注册成 i2c_driver,在 probe 里申请资源,把事件上报给 input 子系统,上层复用现成的事件接口。早期快速验证阶段可以用用户态 i2c-dev 直读寄存器,但那只适合调硬件,不适合量产,用户态轮询既慢又费电。

裸机或 RTOS 场景里,我推荐加中断。STK3310 带 INT 引脚,就是为了让主控在无事件时睡大觉,只有接近事件发生才唤醒。轮询虽然代码少,但 CPU 一直在跑,功耗表现差,电池类应用里会被直接砍掉。用 RTOS 时把 INT 接到 GPIO,中断服务函数里只发事件给线程,线程里完成读数据、清中断、去抖、上报四件事,这是最稳也最容易排查的结构。

3.2 最小硬件检查:供电、上拉、I2C地址与中断引脚

软件开工之前先做最小硬件检查,这一步能省掉后面一大半排错时间。STK3310 通常分开 VDD 和 VIO,VDD 给内部模拟电路,VIO 给 I2C 接口电平,两个电压不一定相同,缺哪个都会出现“读写时好时坏”的怪现象。上电时序也要留意,常见要求是 VDD 稳定后再等若干毫秒才允许发起 I2C 访问,驱动里 probe 一上来最好先msleep(10)再读 ID。这个动作看起来多余,但在量产后电源纹波偏大时,它就是救命的。

I2C 地址默认一般是 0x23,如果芯片的 ADDR 引脚被拉到不同电平,地址会变。先别急着在驱动里写死地址,用 I2C 工具扫一遍最可靠。Linux 下最直接的方式是:

i2cdetect -y 0

输出里出现 0x23 是默认地址,出现在其他格子说明 ADDR 被硬件绑到了不同电平,驱动里 reg 要跟着改。扫描不到时不要立刻怀疑芯片坏了,优先量 I2C 两根线的上拉电阻是否存在,以及上拉电压是不是 VIO 那一路。上拉电阻太小会增大功耗、压塌信号上升沿;太大则上升沿过慢,400k 高速模式读写会失败。具体阻值和总线电容有关,我的经验是从 4.7k 起步,总线上挂的设备超过 3 颗就换成 2.2k,再配合示波器看边沿。

INT 引脚要确认空闲状态。STK3310 的 INT 通常是开漏输出,板子上必须有上拉电阻把它拉高,芯片事件发生时拉低。如果接到 SoC 的 GPIO,设备树里要把该引脚配成带内部上拉的输入,中断类型声明为低电平触发。用万用表量一下空闲电平,发现是低电平,说明上拉没接或者引脚被复用到了别处,这在样板阶段很常见。

3.3 设备树配置:一段最小可跑通的 DTS 片段

在内核方案里,一个最小可跑通的 I2C 设备树节点要包含 compatible、reg、中断三大要素。下面这段是常见写法,注意 compatible 字符串必须和驱动里 of_match_table 写的一致,否则设备树挂上了,驱动却不 probe,这是新手最容易卡住的地方。

&i2c0 { status = "okay"; clock-frequency = <400000>; stk3310@23 { compatible = "somevendor,stk3310"; reg = <0x23>; interrupt-parent = <&gpio0>; interrupts = <5 IRQ_TYPE_LEVEL_LOW>; vdd-supply = <&vcc_3v3>; vio-supply = <&vcc_1v8>; }; };

简单说明参数:clock-frequency 设 400k 是启用 fast mode,总线不稳定就退回 100k;reg 的 0x23 是前面扫描出来的 I2C 地址;interrupt-parent 和 interrupts 指定 GPIO0 的第 5 脚作为中断输入,IRQ_TYPE_LEVEL_LOW 表示低电平有效;vdd-supply 和 vio-supply 是为了让电源管理框架在 suspend/resume 时自动开关供电。如果方案不依赖 regulator 框架,这两个属性可以去掉,但 compatible、reg 和 interrupts 必须保留。

系统起来后用这个命令确认驱动有没有识别:

ls /sys/bus/i2c/devices/0-0023/

目录存在且里面出现自定义 sysfs 属性,说明 probe 成功。卡在这一步时先看内核日志,没有 device found 就查两件事:compatible 是否匹配,reg 是否和实际地址一致。设备树阶段最常见的坑是把 7 位 I2C 地址当成 8 位地址写进 dts,结果驱动永远 probe 不到。这个最小环境搭完之后,真正的工作才刚开始,核心逻辑是初始化、数据读取和上报,也就是下一章的内容。

4. 核心驱动实现:初始化、去抖、中断上报与 sysfs 接口

4.1 初始化流程:probe 里到底要做哪几件事

STK3310 的 probe 函数不建议一上来就写一堆寄存器。我的习惯是分四步:先等供电稳定,再读 ID 确认芯片在线,然后软复位并按业务配置寄存器,最后申请中断和注册上报接口。顺序反了,比如先注册中断再初始化,中间夹着没配置好的状态,容易产生一次误报,而误报一旦上报到应用层,用户可能刚开机就收到一次假的接近事件。

下面是一个基于内核 i2c_driver 框架的初始化骨架,寄存器名以你手头数据手册为准:

static int stk3310_power_on(struct stk3310_data *data) { int ret; /* 1. 先等VDD稳定,再访问I2C */ msleep(20); /* 2. 读产品ID,确认芯片真的在总线上 */ ret = i2c_smbus_read_byte_data(data->client, STK3310_REG_PRODUCT_ID); if (ret < 0) return ret; if ((ret & 0x0f) != STK3310_EXPECT_ID) return -ENODEV; /* 3. 软复位并等待内部稳定,保证从一个确定状态开始 */ i2c_smbus_write_byte_data(data->client, STK3310_REG_SOFT_RESET, 0x01); msleep(5); /* 4. 配置PS增益、LED电流和采样率,先关中断避免乱报 */ i2c_smbus_write_byte_data(data->client, STK3310_REG_PS_CONF, STK3310_PS_GAIN_MID | STK3310_PS_RATE_10HZ); i2c_smbus_write_byte_data(data->client, STK3310_REG_INT_CONF, STK3310_INT_PS_EN); return 0; }

这里的关键点有三处。第一,msleep(20) 放在访问之前,解决的是上电时序问题,很多量产后的偶发读写失败就是因为时序余量不足。第二,读 ID 后要掩码过滤无关位,把结果和期望值比较,不能只判断返回值大于零,否则地址错误时可能把总线上其他芯片的寄存器值当成 ID。第三,先软复位再配置寄存器,避免上一次休眠留下的配置干扰新状态。i2c_smbus_write_byte_data的函数签名是int i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value),command 是寄存器偏移,value 是写入的字节,它只适合单字节寄存器,多字节配置要用 i2c_transfer 配合缓冲区写完整块。

4.2 中断与工作队列:中断里只做标记,读数据放到线程上下文

STK3310 的中断处理和 GPIO 按键驱动是同一套路:中断服务函数里绝对不能做耗时的 I2C 读操作。I2C 是慢速总线,一次完整传输可能拉长几百微秒到毫秒级,放在硬中断里会阻塞整个系统的中断调度。常见做法是用request_threaded_irq申请线程化中断,把完整的数据读取放在线程上下文执行,代码写起来和普通中断处理几乎一样,但不会阻塞其他中断,也不容易引发内核警告。

static irqreturn_t stk3310_irq_handler(int irq, void *dev_id) { struct stk3310_data *data = dev_id; int flag, ps; /* 1. 读中断标志,判断是哪类事件 */ flag = i2c_smbus_read_byte_data(data->client, STK3310_REG_INT_FLAG); if (flag < 0) return IRQ_HANDLED; /* 2. 有PS事件才读数据,没有就清标志退出 */ if (flag & STK3310_FLAG_PS) { ps = i2c_smbus_read_word_data(data->client, STK3310_REG_PS_DATA); if (ps >= 0) stk3310_process_ps(data, ps & STK3310_PS_MASK); } /* 3. 写INT_FLAG清除位,完成握手 */ i2c_smbus_write_byte_data(data->client, STK3310_REG_INT_FLAG, flag); return IRQ_HANDLED; }

这段代码有三个容易被忽略的细节。第一,中断标志读取后不能立刻清除,否则可能在数据转换完成前丢掉同一事件的新数据,正确顺序是读数据再清标志。第二,中断处理的返回值要符合实际,如果用了线程化中断,某些平台要求返回 IRQ_WAKE_THREAD 来触发线程函数,中断频率异常高时先怀疑这一步写没写对。第三,i2c_smbus_read_word_data返回的是 16 位整数,但 PS 数据的有效位可能只有低 12 位或 16 位,要根据数据手册做掩码,否则状态位会被当成高位数据混进去,导致读数虚高。

关于多字节读取,还有一个常见坑:SMBus 的 word 读取在部分 I2C 控制器上有字节序问题。稳妥做法是分两次读 LSB 和 MSB 两个寄存器,再手动拼(msb << 8) | lsb。内核里i2c_smbus_read_word_data已经做了字节序封装,但如果你直接调 i2c_transfer 读两字节,返回的排列顺序会跟寄存器真实布局有关,所以驱动里能用 SMBus 封装就用封装,少一层心智负担,也就少一个踩坑点。

4.3 数据读取与去抖:连续 N 次越过阈值才切换状态

状态判断放在一个独立函数里,结构是维护一个计数器,每次拿到新 PS 读数都与高低阈值比较,连续 N 次满足条件才切换当前状态,并上报给上层。下面这段是去抖状态机的核心逻辑,不涉及具体平台,可以直接套用到裸机或 RTOS:

static void stk3310_process_ps(struct stk3310_data *data, u16 ps_raw) { bool above = (ps_raw >=>static ssize_t ps_raw_show(struct device *dev, struct device_attribute *attr, char *buf) { struct stk3310_data *data = dev_get_drvdata(dev); int ps; mutex_lock(&data->lock); ps = i2c_smbus_read_word_data(data->client, STK3310_REG_PS_DATA); mutex_unlock(&data->lock); if (ps < 0) return ps; return sysfs_emit(buf, "%d\n", ps & STK3310_PS_MASK); } static DEVICE_ATTR_RO(ps_raw);

这里加 mutex 的原因很实际:sysfs 读函数可能在中断线程读数据的同时被用户态访问,两个 I2C 访问交叉会让芯片状态错乱。sysfs_emit 是内核提供的格式化输出函数,它保证缓冲区不越界,相比sprintf更安全。调试时在板子上执行:

cat /sys/bus/i2c/devices/0-0023/ps_raw

读到的是当前真实 PS 原始值,配合状态属性可以确认接近和远离切换是否正常。这一步是最有价值的定位手段:先确认每次事件上报都对应一次读数变化,再往上排查应用逻辑,不要把问题直接丢给上层。

5. STK3310 驱动避坑:串扰、误触发、低功耗三座大山

5.1 无物体读数上百:串扰让底噪直接抬到阈值附近

现象:芯片在裸板上工作正常,无物体时 PS_DATA 只有十几,装进屏幕整机后变成两三百,甚至和贴近读数差不多高,接近检测基本失效。

原因:红外光在盖板内表面和屏幕模组层间反射,形成固定串扰。这是光学问题,不是寄存器配置错误,反复调中断阈值没用,因为底噪本身已经被抬到了信号区附近。

解决:第一步降低 LED 电流,电流减小对串扰的抑制比调增益更明显;第二步开启厂商提供的串扰消除或基线跟踪机制,如果没有这种机制,就用软件基线:上电后 2 秒内不判断接近,只采集底噪存下来当基线;第三步把工作点改成“目标反射量 = 当前读数 - 设备基线”,用差值去和阈值比较。我踩过这个坑后养成的习惯是:任何光照相关传感器驱动,都留一个基线校准接口,不直接拿原始值判决。这个习惯在换屏、换盖板、换整机结构时都能用上。

5.2 阈值只设单边:接近和离开在临界点反复横跳

现象:物体停在阈值附近不动,应用层每隔几十毫秒收到一次接近和离开事件,日志刷得飞快,用户体感就是屏幕防误触功能在抽搐。

原因:只设了一个接近阈值,没有释放阈值,也就没有滞回带。任何噪声让读数越过阈值就触发切换,掉回来又触发切换,状态紧跟噪声走。

解决:按第二章的方式设一对阈值。接近阈值取“底噪 + 信号增量的一半”,释放阈值取“底噪 + 信号增量的三分之一”,两条线之间至少留出满量程 10% 的落差。同时把去抖计数加进来,连续 3 次越过才切换。我常用的参数组合是:PS 增益中档、LED 电流初始档、阈值间隔 15% 满量程、去抖 3 次。这套组合覆盖了大多数贴屏接近项目,量产波动再单独调整,但结构始终是双阈值加计数,不要退化成单阈值。

5.3 中断频繁触发:清了又触发,读数据却没超阈值

现象:INT 引脚不断拉低,中断函数被反复调用,但读回 PS_DATA 并没有越过阈值,甚至读数很低,系统 CPU 占用率肉眼可见地升高。

原因:中断标志没有按协议清除,或者中断类型配置错误。STK3310 的 INT 是开漏低有效,如果驱动配成边沿触发又没完成“读标志-清标志”握手,电平一直低着,系统会认为事件不断发生,持续重入中断处理函数。

解决:把中断类型确认清楚,申请IRQF_TRIGGER_LOW并使用线程化中断。处理函数严格按“读标志 → 读数据 → 清标志”的顺序执行,清标志后最好再读一次 INT 引脚确认恢复高电平。如果仍然是低电平,说明有事件堆积,要继续按流程读,直到清干净为止。量产测试时,我曾经靠逻辑分析仪抓 INT 波形才定位到这个问题,驱动看起来没问题,但中断引脚波形一直在低电平抖动。

5.4 休眠唤醒失败:系统醒了,芯片却还没醒

现象:系统进入 suspend 后,物体靠近屏幕不产生任何反应;resume 后第一次读 PS_DATA 返回异常值,甚至直接读写失败。

原因:STK3310 在 suspend 时被切断了 VDD,寄存器状态全部丢失;或者中断没有配置为唤醒源,SoC 在睡眠状态下根本收不到 INT 变化。

解决:在驱动里实现 suspend/resume 回调,suspend 时关闭中断、保存必要的寄存器值,resume 时重新走一遍上电初始化和恢复配置。如果硬件设计不允许断电,就用 regmap 的寄存器缓存,让内核在 resume 时自动恢复。同时确认 DTS 节点里加了wakeup-source属性,INT 所接 GPIO 要支持唤醒,否则内核会把中断直接屏蔽掉。这是休眠问题里最容易翻车的点:不是驱动没写完,而是设备树根本没有告诉内核这个引脚要当唤醒源用。

5.5 十台设备十种表现:量产一致性差

现象:同一批 10 台机器,同一距离下 PS_DATA 从 300 到 900 分布,用同一套阈值,有的误报有的漏报,产线测试员一天能挑出好几台“手感不对”的机器。

原因:贴片偏移、LED 与光电管之间装配公差、盖板油墨厚度不同,导致每台的串扰和反射效率都不一样。软件阈值不可能在手工调试阶段统一拟合所有机器。

解决:产线上加一道光学校准。用一个标准反射块放在固定距离,比如 5mm,每台机器读出 base_data 和 target_data,写入设备分区保存。驱动启动时读取这两个值,动态计算阈值:接近阈值 = base_data + (target_data - base_data) * 0.5,释放阈值 = base_data + (target_data - base_data) * 0.3。这样每台机器的阈值都跟着自身光学特性走,量产一致性才能真正收敛。校准失败时驱动可以上报一个标志,让产线知道这块模组需要返修,而不是带着缺陷流到用户手里。

6. 进阶:把驱动做稳的四个习惯——动态阈值、底噪测量、产线校准与唤醒验证

6.1 动态阈值与底噪测量

静态阈值在量产场景里不够用,我习惯在驱动里加一个轻量动态基线:每秒采样一次底噪,当检测到物体离开且状态稳定时,把当前读数滑入基线,用一阶低通更新。这样温度漂移、盖板应力变化、元器件老化都会被慢慢吸收,阈值不再是写死的数值,而是一个跟随硬件状态漂移的参考线。底噪测量要把毛刺剔除,采样 8 次去掉最大最小值再取平均,平均值的更新步长限制在每 10 秒不超过 2%,防止把真实遮挡误吃进基线。实现上就是多一个定时器和几个变量,跑在同一个工作线程里,不需要额外加锁。

6.2 产线校准与阈值计算

产线校准是量产项目最后一道防线。标准反射块放在固定位置,读出 base 和 target,驱动在初始化时按比例生成阈值,并暴露一个calibration_status节点,让产线脚本自动判断是否通过。如果 base 太高或者信号增量太小,说明模组光学特性不合格,直接标记 NG。这个流程看似简单,但它能避免超过一半的线下误报投诉。我的经验是校准节点不要等到量产才加,样机阶段就把这个接口留在驱动里,因为屏幕供应商换一批材料,你就要重新校准一批设备。

6.3 低功耗下的唤醒验证方法

休眠策略我现在的做法是:短睡不断 VDD,进 suspend 前把中断保持使能,让 INT 引脚直接作为唤醒源;长睡断电,resume 里全量恢复寄存器。验证唤醒链路时,我专门做过一组测试:顶灯强光照射、物体遮挡、静置不同时间组合,方向发现有一版驱动在 resume 后第一帧数据是旧的,导致误报一次接近。修复办法是 resume 后强制丢弃前两帧数据,等芯片重新完成采样再启用判断逻辑。这个细节不实际跑组合测试很难发现,建议任何睡眠唤醒项目都把手动测试清单列全。

到这里,STK3310 距离传感器驱动的基本功已经过了一遍。回头看我自己项目记录,最值钱的一条教训是:不要跟原始 PS_DATA 较劲,把精力放在基线和阈值上,先把“无物体读数稳定”做到,再谈检测灵敏度。真遇到玄学问题,十有八九出在光学链路,而不是 I2C。希望这篇笔记能帮到你,少走我当时走的弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进