
做RK3588平台的项目最常碰到的小功能需求之一就是按键。很多方案商会直接用GPIO直连按键一个IO对应一个键简单是简单但一旦按键数量上来IO资源直接告急。RK3588这种把PCIe、USB、MIPI、以太网、HDMI全部铺开的高密度芯片BGA封装的引脚虽然多真正留给你做功能扩展的GPIO往往没有想象中宽松。我这次在项目里需要增加四个功能键——上一曲、下一曲、音量加减设计方案评审时被反复质疑为什么不用GPIO为什么不用I2C扩展芯片最后定下来的方案是RK3588自带SARADC加电阻分压总共四颗电阻加一颗电容硬件成本不到一毛钱驱动层直接用内核自带的adc-keys驱动从设备树配置到Input事件上报整条链路一个上午就跑通了。这篇就把完整的开发过程和踩坑记录整理出来给后面做RK3588 ADC按键的朋友一个可复用的参考。文章会覆盖从芯片底层ADC资源分析、分压电路计算、设备树节点写法、内核驱动工作机制到实际调试命令的全流程最后还有我在噪声处理上遇到的坑。无论你手里是EVB开发板还是自己画的板子只要RK3588引出SARADC通道这套思路都能套用。1. 选型之前ADC按键方案到底适合什么场景1.1 为什么不用GPIO直连或IO扩展芯片GPIO直连按键的优点不用多说驱动层有现成的gpio-keys设备树写几个节点就完事响应快还能做中断唤醒。但GPIO按键有个硬性前提你得有足够多且空闲的GPIO。在RK3588的板卡设计里GPIO经常被HDMI的CEC/DDC、MIPI的复位与使能、PCIe的时钟请求甚至调试用的串口占得七七八八再加上很多核心板还要接EEPROM、TP、Sensor真的省不下几个干净的IO。I2C扩展芯片比如TCA6416、PCF8574是另一种常见思路一颗芯片轻松扩出八个甚至十六个IO驱动也有标准GPIO expander。问题在于成本、layout面积和复杂度一颗扩展芯片单价几块钱还要加去耦电容、上下拉电阻芯片本身得挂在I2C总线上如果系统里I2C设备多还得协调地址。对四五个按键这种轻量需求用I2C扩展芯片有点杀鸡用牛刀。ADC按键方案的本质是用电压换IO把按键的物理通断转换成电阻分压网络的不同电压点ADC引脚只需占用一个模拟通道按键数量再多也不占GPIO。RK3588片上SARADC最多提供八路模拟输入哪怕只用一个通道串联多个并联电阻也足够挂六个到八个按键。这个思路在消费电子和工控板上都很常见电视遥控器、空气净化器面板、考勤机键盘大量采用这种方案。1.2 RK3588的SARADC资源够不够RK3588片上的SARADC是逐次逼近型ADC挂在APB总线上地址在0xfec10000附近支持多路模拟输入分辨率12位参考电压一般由板级硬件接3.3V或1.8V。这颗ADC在SDK里的驱动已经非常成熟属于标准IIOIndustrial I/O子系统设备树里给saradc节点设好status和通道系统起来之后会自动在/sys/bus/iio/devices/下注册iio设备。对我们做驱动的人来说底层的寄存器操作、采样时序、FIFO处理都不用碰直接在设备树里引用io-channels即可。大部分RK3588底板的SARADC不会把八个通道全部引出一般留出两到三路用于Type-C检测、温敏电阻或者自定义模拟量。做按键之前先打开原理图确认哪个通道没被占。以我的经验通道0往往被用于Type-C的CC检测或者板载NTC温度采样通道1到通道3预留的概率更高。我用的是通道1设备和节点都起的adc-keys名字后面的配置也都按通道1来写。1.3 ADC按键的局限别以为它是万能的ADC按键有三件事做不了一是不能同时按多个键因为多个按键同时按下会改变整个分压网络的等效电阻读出来的电压不在任何单键档位附近驱动只能匹配到最接近的一个甚至误判成别的键二是不能做低功耗唤醒SARADC本身没有比较器中断机制adc-keys驱动是轮询方式读取系统挂起后CPU停转没人去采样按键自然唤不醒三是响应速度有限按键事件依赖轮询周期一般软件上设置20ms到50ms和GPIO按键的中断响应相比肯定有延迟但对菜单操作、音量调整这类场景完全够用。搞清楚这三点再去判断要不要用ADC按键。如果产品要求支持组合键、要求按键唤醒睡眠模式那就老老实实留GPIO或者上I2C扩展芯片。2. 电阻分压网络设计每个按键的电压到底怎么算2.1 电路拓扑与电压计算方法ADC按键的标准电路拓扑并不复杂ADC采样引脚通过一个上拉电阻接到参考电压VREF每个按键各自串联一个不同的电阻到地所有按键的公共端接到ADC引脚。没有按键按下时没有任何通路到地ADC引脚被上拉电阻拉到VREF读到的值接近满量程。按下某个按键后该按键的串联电阻和上拉电阻形成分压ADC引脚电压变成V VREF x Rn / (Rpull Rn)其中Rpull是上拉电阻Rn是当前按键串联的电阻。这个公式是整个电路设计的基础所有档位电压都是从这里推出来的。举个例子VREF是3.3VRpull取10kΩ按下串联1kΩ的按键时V 3.3 x 1000 / (10000 1000) 0.3VADC读到的原始值就是 0.3 / 3.3 x 4095 ≈ 372。这个原始值随后被IIO驱动换算成微伏也就是大约300000μV。2.2 12位ADC分辨率下的档位间距设计RK3588的SARADC是12位满量程4096档参考电压3.3V时每一步约0.8mV理论上精度绰绰有余。但真实系统里精度根本达不到理论值因为电阻本身有误差常见5%精度电阻实际偏差可能到5%电源有纹波PCB走线还会引入噪声。所以按键电压档位之间必须留足余量这才是设计的关键。我一般按两个原则来分配档位相邻档位电压差至少250mV最靠近参考电压的档位和松开阈值之间至少留500mV以上的安全距离。第一个原则用于抵抗电阻误差和采样噪声第二个原则是为了避免无按键状态被误判为某个高位按键。如果档位间距小于100mV实际生产时很容易出现一批板子正常、另一批板子按键错乱的问题。这是因为不同批次的贴片电阻阻值分布不同电压点整体偏移后落到了别的档位上。用1%精度的电阻能显著改善但成本略高批量大时值得考虑。2.3 一个具体的四按键设计实例我这次项目的VREF是3.3V上拉电阻Rpull取10kΩ四个按键的串联电阻分别选1kΩ、2.2kΩ、4.7kΩ、10kΩ。算出来的理论电压和ADC原始值如下表按键串联电阻理论电压ADC原始值(约)设置press阈值(μV)KEY_A1kΩ0.300V372300000KEY_B2.2kΩ0.594V737594000KEY_C4.7kΩ1.056V13101056000KEY_D10kΩ1.650V20481650000无按键-3.300V4095keyup 2800000选择这些阻值有几个考虑11kΩ1k10k和12.2kΩ2.2k10k这些组合都是市面上最常见的E24系列标准阻值料好找电压分布从0.3V到1.65V覆盖了量程的10%到50%没有把档位压到贴近满量程的高危区相邻档位之间的最小间距是0.594-0.3000.294V大于250mV的设计下限最靠近的KEY_D是1.65V和keyup阈值2.8V之间有1.15V的巨大余量即使电源波动0.2V也不会误判。最后硬件上别忘了在ADC引脚对地并联一颗10nF到100nF的电容用作高频滤波。这颗电容能显著改善后续采样抖动后面第6节我会详细说。3. 设备树配置从dtsi到dtb的完整链路3.1 adc-keys节点标准写法硬件分压网络设计好之后下一步就是把按键关系写进设备树。内核自带的adc-keys绑定文档在Documentation/devicetree/bindings/input/adc-keys.txt标准节点结构是把所有按键放在一个compatible为adc-keys的顶层节点下每个按键用子节点描述。我板子上的完整设备树节点是这样写的/ { adc-keys { compatible adc-keys; io-channels saradc 1; io-channel-names buttons; keyup-threshold-microvolt 2800000; poll-interval 30; debounce-interval 100; status okay; button-a { label KEY_A; linux,code KEY_PREVIOUSSONG; press-threshold-microvolt 300000; }; button-b { label KEY_B; linux,code KEY_NEXTSONG; press-threshold-microvolt 594000; }; button-c { label KEY_C; linux,code KEY_VOLUMEDOWN; press-threshold-microvolt 1056000; }; button-d { label KEY_D; linux,code KEY_VOLUMEUP; press-threshold-microvolt 1650000; }; }; };这里有几个字段非常关键第一个是io-channels它通过saradc 1指定使用SARADC的第1通道phandle指向soc dtsi里定义的saradc节点数字1对应通道索引。如果硬件接在通道1就写1别和节点名字里的adc-keys搞混。第二个是keyup-threshold-microvolt它告诉驱动电压高于这个值意思是所有键都松开了我写2800000μV即2.8V对应前文设计的余量。第三个是每个子节点里的press-threshold-microvolt这个值就是2.3节表格里的理论电压单位同样是微伏。3.2 关键属性逐项拆解io-channels属性的背后是IIO框架的channel映射机制。adc-keys驱动在probe阶段调用devm_iio_channel_get获取对应的IIO通道之后所有读取操作都通过这个channel句柄完成。io-channel-names是给这个channel起的别名目前驱动里用NULL作为name来获取所以这个属性可以不写但写上更规范。poll-interval驱动轮询间隔单位毫秒。我设置30ms这是权衡点太短如10msCPU频繁进入采样流程虽然RK3588性能强不在乎这点负载但对功耗不友好太长如100ms按键响应会有明显迟滞感用户会觉得按下去没反应。30ms在主观使用上几乎感觉不到延迟而且足够规避大部分机械按键的弹跳窗口。如果你对响应有更高要求可以试20ms但不要再低了。debounce-interval是按键消抖的持续时间单位同样是毫秒。新版内核的adc-keys支持这个属性逻辑是连续多次读到同一键值且持续时间超过该值才认为按键有效。我设100ms既能过滤掉电磁干扰引起的瞬时跳变又不会让快速连按丢失。如果你的产品需要极快的连击响应可以适当减小到50ms但要注意噪声。linux,code字段的值来自include/uapi/linux/input-event-codes.hKEY_PREVIOUSSONG、KEY_NEXTSONG这类宏都是标准Linux按键码应用层通过Input子系统收到的也是这些码。注意不要直接用数字一旦内核升级改了宏定义数字就会错位写宏名更安全。3.3 确认设备树有没有被编译进dtb设备树源文件写好后不能只盯着dts文件还得确认它真的进了最终烧录的dtb。RK3588平台的常见布局是内核源码arch/arm64/boot/dts/rockchip/目录下放各个板型的dts比如rk3588-evb1-lp4.dts、rk3588-evb7-lp4.dts或者你自己定制的rk3588-myboard.dts。编译时用以下命令之一make dtbs make rk3588-myboard.dtb如果改了dtsi里soc公共部分需要确保引用的板级dts确实包含你写的adc-keys节点很多时候板级dts会引用公共dtsi节点放在dtsi里更省事。编译完成后在arch/arm64/boot/dts/rockchip/下能看到对应的.dtb文件。RK的SDK通常会把dtb打包进resource.img或boot.img具体打包方式各家SDK脚本不同以你手里的SDK文档为准。板子起来之后验证设备树有没有生效最快的方法ls /proc/device-tree/adc-keys/如果能看到button-a、button-b这些子节点名称说明设备树已经被内核解析。还可以直接读取某个属性的十六进制内容cat /proc/device-tree/adc-keys/keyup-threshold-microvolt | od -An -td4读到2800000就对了。这里注意设备树里的属性值是大小端存储的大整数od按4字节有符号整数读出来才是人看的数字。4. 内核驱动工作机理adc-keys是轮询不是中断4.1 从设备树到驱动的匹配过程设备树节点只是一个静态描述真正跑逻辑的是内核里的drivers/input/keyboard/adc-keys.c。这段代码在Linux内核里已经维护了很多年非常稳定。匹配过程遵循标准的platform driver流程设备树里带compatibleadc-keys的节点会在系统初始化时生成platform_deviceadc-keys驱动声明了同样的compatible于是probe函数被调用。probe阶段主要做几件事解析io-channels拿到IIO通道遍历子节点把每个子节点的press-threshold-microvolt和linux,code解析出来组成一张按键电压映射表调用input_allocate_device分配Input设备设置设备名称、事件位掩码最后注册到输入子系统。注册完成后/proc/bus/input/devices里会多出一个名字为adc-keys的输入设备。4.2 按键识别算法最接近电压优先adc-keys的灵魂在它的按键识别逻辑我直接说代码层面的算法流程。驱动通过一个定时器定时触发adc_keys_poll函数每次调用都从IIO通道读取一次电压值然后遍历按键映射表计算每个按键press电压与实际读数的绝对差值取差值最小的那个按键作为当前匹配结果。如果当前读数大于keyup阈值则直接判定没有按键按下。这个取最接近的策略有个隐含前提硬件设计时各档位电压不能重叠且实际读数应该落在某个档位附近。如果有一个按键的press电压恰好是两个档位的中间值驱动会随机偏向读数值更近的那个这在批产时是隐患。所以我在第2节反复强调档位间距原因就在这里驱动算法本身不会帮你纠错它只在给定条件下做最近邻匹配。每次匹配完成后驱动会比较当前键和上一次键是否一致。如果不一致就先把上一次的键上报为释放再上报当前键为按下。这个状态机的设计保证了重复轮询时不会产生大量重复事件只有按键状态真正变化时应用层才能感知到。4.3 Input子系统事件上报链路接下来是Input事件上报链路。adc-keys调用input_report_key(input, keycode, 1)上报按键值调用input_report_key(input, last_key, 0)上报释放值每次上报之后紧跟input_sync(input)对事件进行同步。这三个调用会生成标准Input事件通过内核的input_event机制分发给所有打开该Input设备文件的应用。应用层的表现就是用evtest工具能看到type为EV_KEY、code为具体按键码、value为1或0的事件。Android系统里常见做法是直接用getevent观察节点或者让上层通过EventHub读取。整条链路从硬件分压到上层应用中间没有私有协议完全走Linux标准输入子系统这也是我推荐这个方案的原因之一上层不用写任何自定义库。5. 实测一把从raw值到内核事件5.1 通过IIO接口读取ADC原始值设备树和驱动都跑通后第一步不是直接测按键而是先确认ADC通道本身的读数是否正常。RK3588的SARADC通过IIO框架导出sysfs属性先看有哪些通道ls /sys/bus/iio/devices/iio:device0/能找到in_voltage0_raw到in_voltage7_raw等文件命名规则是in_voltageN_rawN对应通道号。我用的通道1就读in_voltage1_rawcat /sys/bus/iio/devices/iio:device0/in_voltage1_raw不按任何键时理论上读到接近4095的值也就是3.3V对应的满量程。实际板子上可能有微小偏差比如误读到4093或4094这属于正常范围。然后在每个按键分别按下瞬间观察读数是否和2.3节表格接近操作预期raw预期电压无按键约4095约3.3V按下KEY_A约3720.3V按下KEY_B约7370.594V按下KEY_C约13101.056V按下KEY_D约20481.65V如果实际读数偏离超过50优先怀疑上拉电阻和按键串联电阻的精度用万用表实测阻值重新计算不要盲目调设备树阈值。如果读数乱跳先把ADC引脚并联的滤波电容焊上再看噪声是否收敛。如果sysfs导出多个iio设备可以用下面命令确认哪个对应saradccat /sys/bus/iio/devices/iio:device0/name一般输出rockchip,saradc-8或类似名称确认无误后再操作。5.2 用evtest验证input事件通道读数正常之后切换到Input事件验证。先确认adc-keys设备在Input子系统里的注册情况cat /proc/bus/input/devices | grep -A 5 adc-keys输出里能看到HandlerseventX例如event3。然后直接上evtestevtest /dev/input/event3屏幕进入事件监听状态后按下KEY_A应该看到类似输出Event: time 1698743424.123456, type 1 (EV_KEY), code 163 (KEY_NEXTSONG), value 1 Event: time 1698743424.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0松开后needs看到value 0。如果事件根本不出先别急着怀疑驱动回头查设备树status是不是okay以及dmesg里有没有adc-keys相关的报错dmesg | grep -i adc-keys dmesg | grep -i saradc如果probe阶段没问题dmesg不会有错误但可能在选择matching时有失败需要仔细看完整启动日志。Android环境用getevent代替evtest用法类似。5.3 我把阈值改小之后发生的按住不放问题这里分享一个真实排错案例。第一次上电调试时KEY_D的电压实测是1.68V比理论值1.65V偏高了30mV。起因是我随手抓了一颗5%精度的10k电阻做上拉实际阻值9.7k导致1.65V变成了1.68V。当时的设备树里keyup-threshold-microvolt我粗心设成了1700000也就是1.70V。结果松开KEY_D之后ADC读数恰好还在1.68V小于1.70V驱动始终认为按键没松开表现就是音量一直加到底。排查过程并不复杂先evtest发现没有value 0事件再去读raw值发现松手后raw是2090左右换算成电压1.68V而keyup阈值是1.70V中间只差20mV。正是这20mV让驱动判定永远有键按下。后来我把keyup阈值改回2.8V同时把上拉电阻换成1%精度的10k现象立刻消失。这个坑给两个教训一是在设计阶段就把keyup阈值放得足够高不要卡在某个按键档位上方一点点二是现场如果发现阈值附近的案件粘连先怀疑电阻精度再怀疑软阈值。0.1V的余量在ADC按键里根本不够用。6. 踩坑实录噪声、抖动与误触发6.1 电源纹波导致的ADC值漂移ADC按键最容易翻车的地方不是逻辑而是采样值的稳定性。RK3588的SARADC参考电压直接取自板上的模拟电源如果这个电源纹波偏大采样值就会跟着抖。我在一块布局比较紧凑的板子上遇到过这种情况不按任何按键时in_voltage1_raw在4080到4095之间跳动按下KEY_A之后读数在340到410之间来回跳偶尔还会跳到600多触发了KEY_A和KEY_B之间的错误判定。第一板硬件上ADC引脚没有加任何滤波电容电源也直接借用了数字3.3V。排查时我不停地在sysfs里读raw值确认抖动的范围和频率然后用示波器量ADC引脚的波形看到约50mV的纹波叠加在DC电压上频率正好和板上DDR时钟附近的一个开关电源纹波吻合。解决方案分两步硬件上在VREF引脚和ADC引脚分别增加滤波电容VREF处放100nF陶瓷电容并联10uF钽电容ADC引脚对地放10nF电容形成一阶RC低通软件上把debounce-interval从0改成100ms。改完之后raw值的跳动范围从70个LSB缩小到5个LSB以内按键识别稳定多了。6.2 机械按键弹跳的影响机械按键按下和松开时内部的簧片并不是干净利落地接通或断开而是会在几毫秒内反复接触弹开这就是经典的按键抖动。GPIO按键方案需要在驱动里做消抖处理ADC按键同样存在这个问题但表现略微不同。抖动期间ADC引脚看到的是串入电阻和上拉电阻之间不断切换的分压值在轮询周期为30ms的前提下单次采样的瞬间可能正好落在弹跳窗口内读到一个不该出现的电压值。如果这个电压恰好跨越到相邻档位的判定范围就会出现一次错误的按键按下/释放事件。所以设计时相邻档位保留250mV以上的间距这个余量比机械按键抖动造成的电压变化大得多。机械抖动一般只引起几十mV级别的电压扰动不会跨档位只要档位间距满足要求抖动对ADC按键的影响就很有限。为了进一步保险我通常还在结构允许的情况下给按键并联一个100nF电容让电压变化更平滑。6.3 实际项目中抗干扰设计清单结合我在多个板卡上的调试经历整理一份ADC按键的抗干扰设计清单新板子设计阶段就可以对着检查VREF参考电压不要直接从主3.3V数字电源上拉尽量从模拟电源LDO引出没条件时至少加磁珠隔离。上拉电阻到ADC引脚之间串一个100Ω到1kΩ的小电阻配合ADC引脚对地10nF电容构成RC低通能有效滤除高频耦合噪声。每个按键串联电阻都用1%精度贴片电阻成本差异不大但批产稳定性提升非常明显。按键走线尽量远离时钟线和电源开关节点特别是MIPI信号和DDR布线避免容性耦合。ADC引脚的滤波电容不要放太远尽量贴近SoC引脚放置否则走线长度会引入新的天线效应。系统层面对saradc的采样时钟也要确认设备树saradc节点里的clocks属性需要正确配置采样时钟过快或过慢都会影响转换精度。这些点看起来琐碎但每一个都对应现场的一次返工。第一批板子回来如果发现批量按键错乱不要先怀疑驱动驱动逻辑就那么几十行大概率问题在硬件噪声和档位设计上。6.4 如果重新选型我会怎么权衡做完这个ADC按键项目后回头看对什么时候用ADC按键这个问题有了更清晰的判断。四到六个按键、不要求组合键、不要求低功耗唤醒、IO资源紧张这四个条件同时满足时ADC按键几乎是最优解。成本低驱动成熟可靠性经得住批量考验。但如果按键数量超过八个或者有组合键需求我宁愿重新规划GPIO分配或者专门留一颗I2C扩展芯片。多个按键同时按压会被ADC分压网络天然耦合强行在驱动里做解算会非常痛苦维护成本远高于芯片成本。所以选型的核心不是哪个方案更高级而是哪个方案让产品风险最低。ADC按键在它适合的场景里就是那个低风险方案。还有一个小技巧实际产品的结构件如果有金属外壳按键PCB区域靠近金属件时金属和走线之间可能形成寄生电容导致按下瞬间ADC读数有过冲。这种情况下除了RC滤波还可以在ADC驱动初始化后先做一次校准把无按键的基准值记录下来后续用这个基准值做归一化判断。RK3588的saradc本身就支持通道校准具体寄存器操作SDK里可能没有现成接口但驱动框架已经留了扩展空间需要时可以自己加一段校准逻辑。我这次因为硬件改版后噪声已经收敛没有走到这一步但下次遇到更恶劣的电磁环境我肯定会先上这个方案。