
最近有个刚量产的音箱项目回来找我用的就是杰理AC696X平台反馈的问题很有代表性ADKEY复用了指示灯引脚之后不定时出现灯不亮或者按键失灵。不是批次报废也不是必现就是十天半月偶尔来那么一次客户能容忍功能差但容忍不了“说不准什么时候就按键没反应”的随机故障。这个案子折腾了我将近两周最后定位到的问题其实很有意思——既不是芯片坏了也不是单纯电路画错而是ADKEY和LED在同一个引脚上复用之后电气状态与软件时序打架。如果你也在做杰理AC696X或者AC692X、AC701N这类低成本蓝牙项目手里只有一个IO引脚却又要接按键又要接指示灯那这篇整理你最好看完再画板或者写驱动。1. 这个案子是怎么来的现象、场景和复用关系1.1 先看现象故障是怎么“不定时”出现的客户反馈的具体现象有三类指示灯该亮的时候不亮过一会儿自己又恢复按键按下去灯没反应实际按键也没有被系统识别到更隐蔽的一种按键A在灯亮的时候按偶尔会被识别成按键B。这三种情况不是每次操作都出现而是随机出现。生产线上用测试治具连续按压几个小时可能一次复现不出来用户拿回家正常用偏偏在某个组合操作下就翻车了。这种“最难复现的bug”最折磨人因为你不能光靠肉眼盯得先想清楚机制再设计复现方案。项目本身是一台便携蓝牙小音箱用了3个ADKEY按键音量加、音量减、播放/暂停。指示灯有两颗LED一颗做电源状态、一颗做蓝牙连接状态其中电源指示灯所在的引脚就是和ADKEY复用的那个管脚。供电是锂电池升压到3.6V左右给芯片供电按键扫描和LED驱动都在主控内部完成外部电路非常简。1.2 ADKEY复用指示灯到底是怎么一个“复用”法ADKEY在杰理AC696X上属于比较常用的按键方案原理不复杂芯片某个ADC通道的引脚内部有上拉电阻外部每个按键通过不同阻值的电阻接地。按下不同按键时引脚电压被拉到不同电平ADC采样得到不同的数值软件根据数值区间判断按下了哪个键。一个ADC引脚能识别好几个按键这在蓝牙SoC这种引脚紧张的应用里非常划算。而“复用指示灯”的做法在低成本方案里也很常见同一个引脚平时工作在推挽输出模式驱动LED亮灭需要检测按键时把引脚切换成ADC输入模式读取电压判断按键。这样既省了一个MCU引脚又保住了按键检测和指示灯两个功能。示意电路就是LED阳极通过限流电阻到VCC阴极接在芯片的ADKEY/LED复用脚上按键则通过各自的电阻接到同一个芯片引脚按键另一端接地。需要亮灯时芯片引脚输出低电平LED形成回路点亮需要扫描按键时引脚停止输出切成高阻输入状态ADC开始采集电压。真正的问题就是出在这个“切成输入状态”的过程上。1.3 为什么“不定时”才是最难搞的地方不定时故障背后的核心原因有三个第一个是时间轴冲突。按键扫描通常是主循环定时调用比如每10ms扫一次LED驱动如果是呼吸灯或者闪烁模式则由PWM或者定时器在背后不断改变占空比。两个逻辑各自为政大多数时候恰好错开但总会有那么几个毫秒的重合窗口一旦在这个窗口里既切换了GPIO模式又执行了ADC采样结果就会异常。第二个是电气状态存在多个“临界余量小”的状态。ADC按键识别本身就是靠电压区间判断的如果按下按键后的分压电压处在两个区间的边界附近LED点亮和熄灭造成的引脚电压偏移就能轻松把结果推过边界导致错判或者漏判。第三个是人为制造出来的随机性。机械按键本身有接触抖动按键老化之后的接触电阻会变大电池电压也会随着电量下降从3.6V慢慢跌到3.2V甚至更低这些变量叠加起来问题就表现出“时好时坏”。你以为它随机其实它一直在按物理规律悄悄积累只是没到触发条件。2. 根因推演从硬件示意到软件时序2.1 硬件上LED支路是怎么破坏ADC分压的要搞清楚按键为什么失灵先得把LED对ADKEY那条支路的影响算明白。假设芯片内部上拉电阻Rup为10kΩVCC为3.6V。外部3个按键通过1kΩ、3.3kΩ、10kΩ电阻接地。按下第一个键时引脚电压为V1 VCC × R1 / (Rup R1) 3.6 × 1 / 11 ≈ 0.33V按下第二个键V2 3.6 × 3.3 / 13.3 ≈ 0.89V按下第三个键V3 3.6 × 10 / 20 1.8V未按键时引脚被内部上拉到3.6V左右。软件里根据ADC读到的电压值划分出几个区间比如0~0.6V判KEY10.6~1.3V判KEY21.3~2.5V判KEY3超过2.8V判无键。这些区间看起来挺宽正常状态完全够用。但LED一旦介入就不一样了。LED导通压降大约是2.0V到2.6V限流电阻如果是510Ω点亮时的电流在几个毫安到十几毫安之间。LED阳极接VCC阴极接复用引脚。引脚作为推挽输出低电平时LED正常亮此时引脚电压只有0.2V左右这个状态没问题。问题出在把引脚切到ADC输入模式后引脚变成了高阻输入但LED支路并没有被断开VCC会通过LED和限流电阻向引脚反向施加电压。如果此时恰好按下了第三颗键理论上引脚应该被拉到1.8V但LED支路的存在会把这个电压往前推。当LED两端电压接近导通压降时LED就会进入微弱导通状态向引脚注入额外电流把ADKEY采样电压抬高。抬高多少取决于LED的V-I特性、限流电阻阻值和芯片内部上拉阻值一旦抬高超过上一档按键的判定阈值按键就错乱或者失灵。更极端的场景是呼吸灯。呼吸灯意味着LED一直在从灭到亮渐变PWM占空比不断变化。LED在低占空比阶段并没有完全截止而是处于微导通状态这个状态下LED对外表现更像一个还是电阻对ADKEY分压网络的破坏比单纯亮灭还要大。所以我在项目里看到的现象常常是灯在呼吸渐变过程中按按键最容易失灵灯完全灭掉或者完全常亮的时候按键反而正常。“灯不亮”的现象也跟这个有关。引脚被切到ADC输入模式之后如果软件切回去的动作出了岔子特别是某次扫描按键时把GPIO方向位改成了输入却没有在扫描结束后恢复成输出低那LED就一直灭着。用户看到的表象是“灯坏了”实际是“GPIO没切回来”。2.2 软件上模式切换和扫描时序的坑硬件有了隐患软件必须靠时序把它“兜住”。但很多代码恰恰在最关键的时刻打开了潘多拉盒子。我见过最典型的写法是这样的定时器每10ms触发一次按键扫描扫描函数里直接调用ADC读取接口却忘了先把引脚从LED驱动模式切到ADC输入模式。这种代码大多数时候能跑是因为ADC通道使能后芯片内部的模拟开关可能在物理上切断了数字输出驱动但问题是切断的时机是模拟开关决定的不是你写代码时决定的。如果ADC采样正好发生在GPIO方向切换和模拟开关稳定之间的那几十微秒里采到的电压就是非稳态值可能落在任何按键区间。另一种更隐蔽的坑是主循环里的状态恢复逻辑用了“读-改-写”的方式操作GPIO寄存器。比如LED状态寄存器某一位已经被定时器改成了新的目标状态但按键扫描函数在恢复LED输出时使用了旧的状态快照把寄存器里刚更新的位又盖回去了。这个过程如果恰好和定时器的LED刷新交错就会造成LED状态丢失表现为该亮的时候不亮。去抖逻辑也容易引入偶发故障。很多人把去抖做成了“连续采样N次一致才上报”但连续的N次采样都发生在LED亮度切换的同一个窗口里采样值是稳定的“错误值”去抖照样通过反而把错判变成了“有效判定”。2.3 别忘了芯片内部的模拟开关和采样保持AC696X的ADC是逐次逼近型SAR ADC内部有采样保持电容。每次ADC转换之前采样开关需要把外部引脚电压充到采样保持电容上这个过程需要时间。如果外部引脚上还并联了滤波电容或者走线寄生电容比较大采样保持时间不够读到的是前一状态或者中间状态的值。在复用场景下这个问题会被放大。因为引脚在LED输出模式时电压要么是接近VCC的高电平要么是0.2V的低电平而切到ADC模式后的按键电压可能是0.3V到2.5V中间某个值。如果切换后立即采样采样保持电容上的电压可能还停留在“LED刚熄灭时的高电平”ADC结果自然不对。这就解释了为什么许多故障是在灯灭后又马上按按键时出现软件把LED关掉立即切到ADC模式并开始采样采样电容还没完成放电读数偏高按键被漏判。芯片内部上拉电阻也分了多档不同档位阻值可能从5k到几十k不等。外部按键分压电阻设计时没有针对实际档位去核对量产时芯片批次不同键值就会漂移。LED支路再叠加一点扰动整片产品的故障率就变得不可控。3. 现场是怎么定位出来的复现、量波形、加日志3.1 复现先别急着换芯片把现象逼出来拿到故障报告后第一件事不是拆机换芯片而是想尽一切办法在实验室把问题复现出来。复现不到后面所有分析都是空谈。我当时的做法是做一个组合压力测试LED强制进入呼吸灯模式亮度周期设为2秒一个来回用一个小电机加偏心轮做成自动按压装置以大约每秒2次的频率轮流按压3个按键电源电压调低到3.1V让ADC余量更小持续跑4到8小时通过串口把每次按键的ADC原始值记录下来。这样跑了一晚上第二天看日志终于抓到几十次异常点。对比正常点之后发现异常几乎全部集中在呼吸灯从暗到亮的前半段也就是PWM占空比从0开始爬升的区间。这就给了我明确的排查方向——问题跟LED亮度切换高度相关。3.2 示波器抓ADKEY引脚的电压波形锁定方向后用示波器同时抓两路信号一路是复用引脚的电压波形另一路是LED驱动端的PWM波形。注意探头一定要靠近芯片引脚接地线尽量短不然探头寄生电容会把本来就微弱的ADC电压扰动掩盖掉。抓到几个关键波形现象按键按下瞬间引脚电压并不是干净地跌到目标电平而是在LED的PWM高电平边沿附近出现明显的台阶和缓慢爬升在呼吸灯占空比较低时引脚电压会顶在一个比正常分压值更高的平台上持续十几毫秒才恢复当按键按压时刻和LED高脉冲重合时电压波形出现毛刺明显不是平滑的RC充放电曲线。这些现象可以用来做快速判断波形特征可能的结论按键按下后电压缓慢爬升几百us到1ms才稳定采样保持时间或外部RC时间常数偏大电压在LED高电平边沿附近出现台阶LED支路向ADKEY网络注入电流LED亮/灭状态下的按键电压不一致分压工作点被LED状态耦合波形出现高频毛刺电源纹波或LED开关噪声进入采样通道示波器只能定性要定量还缺一组数据LED灭、LED常亮、LED呼吸中三种状态下每个按键的ADC值分别是多少。3.3 串口打印把ADC读数暴露在灯光切换下这里我用了自己平时一直留着的调试组合一块基于STC15F104复刻的强制下载工具配合串口转发直接在量产样机上抓日志。杰理平台的SDK可以通过串口把自定义调试信息打出来关键是打原始ADC码值而不是打处理后的按键结果。因为按键结果已经被阈值逻辑“整过容”原始码值才保留真相。在工程里临时加了这样一个调试输出void debug_dump_key_adc(void) { u16 v read_adc_single(ADKEY_CH); printf(adc%d led_state%d duty%d\n, v, led_status(), led_get_duty()); }然后配合自动按压装置让LED分别工作在灭、常亮、呼吸三种状态各采集2000组数据。整理出来的统计结果非常直接按键LED灭时ADC范围LED常亮时ADC范围呼吸灯中间亮度ADC范围KEY185~11095~120110~140KEY2230~270245~280270~315KEY3480~520500~540540~585无按键1000~1023980~1010940~990注意KEY2和KEY3的区间KEY2的判定上限设在300LED常亮时KEY2最大到280还能接受但呼吸灯中间亮度时已经到315明显越界到KEY3的区间去了这就是按键错乱的根本原因。更糟的是KEY3在呼吸灯下已经逼近540而KEY3的判定阈值如果设在560还有一点余量但如果电池电压再低一些或者电阻精度差一点就直接漏判了。拿到这本“原始账本”根因就非常清楚了LED支路的电流会让复用引脚的电压发生同比偏移偏移量在呼吸灯中间亮度时达到最大足以盖过按键区间的余量。后面所有的改动都是把这个偏移压到可接受范围。4. 最终修正方案软硬件一起改4.1 硬件改动用最小成本保住ADC工作点纯软件改能不能解决能缓解但不能根治。因为这个问题的物理基础是LED支路对ADKEY网络的工作点干扰只要电路结构不变干扰就永远存在。所以最终方案是软硬件一起改。先看硬件三个关键改动第一个LED驱动从直接引脚驱动改成三极管隔离驱动。ADKEY复用脚不再直接作为LED的阴极而是控制一颗NPN三极管比如SS8050或者常见的3904的基极LED的电流路径从VCC经过LED、经过三极管到GND和ADKEY采样网络彻底断开。这样LED再怎么呼吸电流都不会倒灌进ADC引脚。第二个保留ADKEY引脚的RC滤波。在ADKEY引脚处加一个对地的0.1uF电容串联一颗100Ω电阻用于滤除高频的LED开关噪声和线缆耦合干扰。注意这个电容不能加太大否则会增加ADC采样稳定时间软件得同步把采样前延时拉长。第三个重新校准分压电阻。把3个按键的分压电阻从原来的1k/3.3k/10k改成1k/4.7k/15k使按键电压区间拉开尽量避开关干扰最大的区域。改完之后重新测一轮三种状态下的ADC分布确保相邻按键最小间隔不小于80个ADC码值。按12位ADC、3.6V供电算1个码大约0.88mV80个码差不多70mV的实测电压差这个量级足够抵抗正常的温度漂移和电阻精度误差。推荐升压型电源供电的系统按键分压电阻的精度至少选1%精度千万别用20%的碳膜电阻大批量上省的那几分钱会在产线上加倍还回来。4.2 软件改动分时复用必须做到三个“隔离”硬件把干扰源切掉之后软件依然不能掉以轻心。因为只要还是同一个引脚做时分复用那么“模式切换瞬间”的软件逻辑仍然会影响稳定性。我最后落地的软件方案核心是三个隔离第一时间隔离。按键扫描开始时先把GPIO方向切到输入等一段时间再进行ADC采样。这个时间由外部RC常数决定我这边用的是0.1uF滤波电容配内部上拉实测500us之后读数就稳定了保险起见延时加到1ms。第二状态隔离。LED的当前亮灭状态和PWM占空比在按键扫描期间不允许被其他代码修改。做法是扫描函数进入前先关闭LED刷新定时器中断扫描完再恢复。如果扫描不是关中断而是用标志位互斥一定要保证标志位设置和清除成对出现否则会出现漏开中断的灾难。第三采样隔离。第一次ADC采样结果直接丢弃连续采4次去掉最大值和最小值取中间两个值的平均值。这样能滤掉机械抖动和瞬间脉冲造成的偶发异常码。最后是去抖按键判定结果要连续读到3次一致才上报每次扫描间隔10ms也就是说一次有效的按键判断至少需要30ms的连续稳定时间对用户手感来说完全是可接受的换来的是系统稳定。4.3 参考代码按键扫描的完整时序下面是基于杰理AC696X常见SDK风格写的示意代码重点在时序结构具体接口名以你手上的SDK为准#define ADKEY_GPIO GPIO_PH4 #define ADKEY_CH ADC_CH_4 static u8 s_led_snapshot_saved 0; static u8 s_led_snapshot_on; static u8 s_led_snapshot_duty; static u16 adkey_read_avg(void) { u16 raw[4]; u16 sum 0; for (u8 i 0; i 4; i) { raw[i] adc_read_channel(ADKEY_CH); delay_us(50); } // 去掉最大最小值取中间两个的平均 for (u8 i 0; i 4; i) { sum raw[i]; } u16 min raw[0], max raw[0]; for (u8 i 1; i 4; i) { if (raw[i] min) min raw[i]; if (raw[i] max) max raw[i]; } return (sum - min - max) / 2; } void adkey_enter_scan_mode(void) { // 保存LED状态 s_led_snapshot_on led_is_on(); s_led_snapshot_duty led_get_duty(); s_led_snapshot_saved 1; // 关闭LED刷新防止扫描期间被定时器改值 led_refresh_timer_disable(); // 切换GPIO为ADC输入 gpio_set_direction(ADKEY_GPIO, GPIO_DIR_INPUT); gpio_set_mode(ADKEY_GPIO, GPIO_MODE_ADC); adc_channel_select(ADKEY_CH); // 等待RC稳定 delay_us(1000); } void adkey_exit_scan_mode(void) { // 恢复GPIO为LED输出 gpio_set_mode(ADKEY_GPIO, GPIO_MODE_GPIO); gpio_set_direction(ADKEY_GPIO, GPIO_DIR_OUTPUT); // 恢复LED状态 if (s_led_snapshot_saved) { led_set_on(s_led_snapshot_on); led_set_duty(s_led_snapshot_duty); s_led_snapshot_saved 0; } // 重新使能LED刷新 led_refresh_timer_enable(); } u8 key_scan_once(void) { u16 v; u8 key KEY_NULL; adkey_enter_scan_mode(); // 第一次采样丢弃 (void)adc_read_channel(ADKEY_CH); v adkey_read_avg(); if (v KEY1_TH_HIGH) { key KEY1; } else if (v KEY2_TH_HIGH) { key KEY2; } else if (v KEY3_TH_HIGH) { key KEY3; } else if (v NOKEY_TH_LOW) { key KEY4; } adkey_exit_scan_mode(); return key; }主循环里这样调用u8 key_scan_debounced(void) { static u8 last_key KEY_NULL; static u8 count 0; u8 cur key_scan_once(); if (cur last_key) { if (cur ! KEY_NULL) { count; if (count 3) { count 0; last_key KEY_NULL; return cur; } } } else { last_key cur; count 0; } return KEY_NULL; }整套流程跑起来之后我在同一套样品上重新做了一晚上的自动按压测试呼吸灯模式下按键错乱彻底消失连续按键5000次无一次漏判。再拿到温度箱里做-10℃到50℃的扫描ADC值的最大漂移也在阈值区间之内。5. 踩坑记录和排查速查表5.1 这次踩过的三个坑第一个坑是轻信了“多用内部上拉就行”的说法。ADKEY内部上拉在数据手册上标着一个范围不是一个精确值。我用一个批次芯片测出来是9.8k换一个批次直接变成11.6k同样的外部分压电阻ADC值偏了不少。后来我把所有分压电阻的设计都改成“按内部上拉最差值算余量”而不是按典型值算。第二个坑是改软件时忘了关中断的连锁反应。我一开始没有关LED刷新中断只是用标志位等待结果在GPIO切换过程中定时器中断正好进来把LED状态改了扫描完之后恢复了一个错误的旧状态灯不亮的问题反而更频繁了。改成扫描期间关中断后这个问题消失。第三个坑是滤波电容加得太大。我想着滤得越干净越好直接把0.1uF换成了1uF结果按键按下后ADC电压要接近5ms才稳定去抖时间又不敢太长造成按键明显延迟。后来重新算RC时间常数0.1uF加1ms延时刚刚好。5.2 问题排查速查表现象可能原因定位方法解决方向按键偶尔失灵重启后恢复引脚切ADC后采样电容未稳定示波器抓按键瞬间电压延长切换后延时并丢弃首次采样按键错判成其他键LED支路电流改变分压打印LED各状态下的ADC原始值隔离LED驱动重选分压电阻指示灯不定时不亮GPIO扫描后未恢复输出状态检查LED状态变量和恢复代码用快照保存/恢复LED状态呼吸灯过程中按键乱跳LED低电流区非线性扰动统计呼吸各亮度点ADC值软件避开临界亮度段硬件隔离低温下故障率变高RC稳定时间不够温度箱自动按压测试增大滤波电容或延长延时批量产品ADC值不一致内部上拉和外部电阻精度差多批次芯片测分压改用1%电阻按最差余量设计5.3 这套经验对杰理其他平台同样适用AC696X、AC692X、AC701N这几个杰理平台的ADKEY模块大同小异内部的逐次逼近型ADC和GPIO复用机制基本一致。这篇文章里所有定位思路和代码时序拿到其他型号上完全可以直接套用。唯一的差别是SDK接口名和寄存器地址不同有些新平台可能直接提供了ADC数字滤波和GPIO模式切换的库函数用那些现成接口比自己操作寄存器更稳。但核心原则不会变同一个引脚做ADKEY和LED复用必须保证LED电流不参与ADC分压必须保证模式切换后有足够稳定时间必须保证LED状态在扫描结束后能准确恢复。我后来在另一个项目里干脆把引脚预算重新梳理了一遍发现多花一个GPIO把LED独立出来软件复杂度能降低不少量产Debug成本也低。如果硬件设计阶段有条件优先用独立引脚驱动LEDADKEY脚老老实实做模拟采样后续能省掉一大半的偶发问题。实在省不出引脚再回来用这篇文章的方案兜底。