简介面向单片机学习者、家电电子设计及维修人员的电容感应触摸按键技术文档聚焦玻璃面板式家电中触摸按键的原理分析与编程实现帮助读者理解从电容变化检测到ADC判别的完整思路。文档共1个docx文件压缩包约344KB内容围绕STC单片机方案展开涉及电路原理图、PWM控制充放电、ADC0电压采样、串口联调与C源程序改编等环节。文中结合实测说明无触摸时ADC端约2.7V、触摸时约2.3V利用约400mV差异供8位ADC识别并讨论电磁干扰、元件参数离散性对即时采样的影响程序部分基于STC12C4052AD的8位ADC应用包含电源控制位、转换速度、通道选择等寄存器配置示例以及通过串口观察转换数据完成参数调试的方法。已有2155人学习适合需要低成本替代专用触摸芯片、将按键与控制功能综合设计的开发者参考也可为故障排查与参数调试提供思路。1. 从一块 5mm 玻璃板说起这套电容触摸按键方案到底解决了什么问题拆过几台玻璃面板的小家电最让人头疼的不是功率部分而是面板下面那几个按键——按下去没反应拆开一看是一块金属片被弹簧顶着贴在玻璃背面没有触点、没有可调的东西坏了只能整块板换。电容感应式触摸按键在小家电里铺开十几年了高灵敏度、面板免开孔、寿命长这些优点大家都认可真到维修和自研的时候专用芯片型号杂、换件只能找原厂反倒成了一个黑匣子。这份资料走的是另一条路不依赖专用触摸芯片直接用单片机做充放电检测把按键判定和其它控制逻辑塞进同一颗芯片。它适合手上有 STC12 系列、想低成本做几键面板的人也适合打算把这套判定逻辑搬到 STM32 上去、先拿 8 位机把原理跑通的人。2. 硬件链路拆解两只二极管、两只 560K 电阻和一个 C0 如何把手指变成电压这套电路最反直觉的地方在于它没有用到任何触摸芯片检测手段就是一颗普通单片机的 ADC。整条链路上只有七个元件两只二极管、两只电阻、一只电容、一块金属片外加一根弹簧。元件少到这种程度反而要求每一处的取值和装配都不能含糊因为没有任何专用芯片帮你做补偿和滤波灵敏度和稳定性全靠这几个器件的配合。先把位号和作用列清楚后面所有推算都基于这张表。位号取值/型号作用D1小信号开关二极管充电通路PWM 高电平期间电源经 R2、D1 给 C0 充电D2小信号开关二极管放电通路PWM 低电平期间 C0 经 R1、D2 对地放电R1560K放电限流与 C0 决定放电时间常数R2560K充电限流与 C0 决定充电时间常数C0nF 级常见做法 1nF10nF充放电电容与感应电容形成分压关系电极Φ12mm 金属片感应电极贴在玻璃下表面弹簧普通导电弹簧把电极压紧在玻璃上同时把信号引到板子上提示原始图纸上标的 R560Ω 是笔误实际应为 560K。这个数量级差了一千倍照 560Ω 焊出来C0 上的电压会被充放到电源轨附近ADC 读数不是 0 就是满量程根本没法判键。2.1 感应电容的形成与电荷通路先看电极这一侧。Φ12mm 的金属片被弹簧压紧在 5mm 厚的玻璃板下表面玻璃板另一面正对的位置就是手指的触摸区。金属片、玻璃、手指三者构成一个典型的平行板电容结构玻璃就是介质。按平行板电容公式粗算一下量级电极面积 S π × (6mm)² ≈ 1.13×10⁻⁴ m²玻璃厚度 d 5mm玻璃相对介电常数取 45代入 C ε₀εᵣS/d得到的结果在零点几 pF 到一两个 pF 这个量级。这个电容非常小手指按上去引起的变化量更小可能只有几百 fF。关键在于这个感应电容并不是孤立存在的它通过金属片和走线等效地与充放电电容 C0 并联在一起。C0 通常是 nF 级比感应电容大两三个数量级所以手指带来的变化被严重稀释——这正是为什么不能简单测一下电压看看有没有变而必须让 C0 反复充放电、让这个微小的电容变化在充放电平衡点上被放大出来。也正因为信号这么微弱后面程序里即时采样四个字才变得特别重要任何一点环境扰动都能把几百 fF 的变化淹掉。从电荷通路看整个电路在 PWM_IN 的两个半周期里走的是两条不同的路。高电平段电流从电源出发经过 R2、D1 灌进 C0此时 D2 反偏截止放电路径断开低电平段PWM_IN 被拉到地C0 上积累的电荷经 R1、D2 泄放到地此时 D1 反偏截止充电路径断开。两只二极管的作用就是把充和放这两条路彻底分开让两个方向的时间常数可以各自独立地由 R2 和 R1 决定。2.2 充放电稳态与 2.7V/2.3V 的由来PWM_IN 持续跑起来之后C0 上会建立一个动态平衡一个周期内充进去的电荷和放掉的电荷相等端电压稳定在某个中间值。这个稳态值由两路的阻抗比和 PWM 占空比共同决定而不是简单地等于 VCC/2。实测下来这个值是 2.7V 左右比一半电源电压略高说明充电通路和放电通路并不完全对称——二极管压降、占空比偏差、电阻精度都会往里掺一脚。这也说明一件事这个电压只能实测标定不能靠公式算完就写进程序。手指按上玻璃板之后感应电容相当于在 C0 上并联了一个小电容等效的充放电电容变大同样的电荷量对应的电压就低了一截ADC0 上读到的稳态电压掉到 2.3V 左右。两个状态之间的差值大约 400mV这个差值就是整个方案的判据来源。再看 ADC 这一侧的分辨能力。电源 5V、8 位 ADC一个 LSB 对应 5V/256 ≈ 19.5mV取整说 20mV。400mV 的落差折算下来大概是 20 个码值余量相当充足不是那种差一两个码全靠猜的临界状态。把这两个状态换算成码值放在一起对比会更直观状态ADC0 电压理论码值VCC5V与松手状态的差无触摸约 2.7V约 138—手指按下约 2.3V约 118约 20 个码20 个码的间距意味着阈值可以直接取在中间比如 128两边各留 10 个码的余量。实际调试时我一般会把阈值再往按下那侧偏一点因为误触发没碰却判成按下比漏触发更让人难受尤其是在有电机、有继电器的小家电里。PWM 频率也要配合 R、C 的取值来定。R1、R2 都是 560KC0 取 1nF 时时间常数 τ 560K × 1nF ≈ 0.56ms那么 PWM 周期最好远小于这个数常见做法是把频率取在几 kHz 到几十 kHz让 C0 上的纹波足够小ADC 每次采到的都是平均之后的直流电平而不是正在爬升的斜坡。2.3 电极、弹簧与玻璃面板的机械装配这部分最容易被当成随便装装就行但实际翻车大多出在这里。金属片必须是面接触地压在玻璃下表面中间不能夹胶、不能有气隙。气隙等于在电容串路上又串了一个小电容总容量直接掉一截灵敏度会明显变差。弹簧的作用有三个导电、提供持续压力、把电极和 PCB 隔开一段距离。弹簧压力不够设备用一段时间、受热胀冷缩影响接触就会变差表现为新装的时候很灵敏装进壳子里就不灵了。还有一处细节电极周围和背面尽量不要走其他信号线尤其是驱动继电器、蜂鸣器这类带脉冲的线。这些线跟电极之间存在寄生电容会跟感应电容并联在一起等于给按键加了一个固定的假手指。走线实在绕不开时STM8S 系列里有个叫 Driven Shield 的做法可以参考——在电极走线旁边平行布一根屏蔽线给它灌上和按键管脚同相位的驱动信号两边电位同步跳变寄生电容就不会被反复充放电灵敏度能保住不少。这套思路在 STC 上也能手工实现只是要多占一个 IO。焊完之后先别急着上电写程序用万用表二极管档量一遍D1、D2 的方向有没有装反R1、R2 是不是真的 560K拿错成 560Ω 是最常见的低级错误C0 焊盘有没有虚焊。这三项确认完再进下一步。3. 单片机侧的程序骨架STC12C4052AD 的 8 位 ADC 配置与采样函数硬件给出的是一个随手指变化的模拟电压剩下的事全在单片机里。这颗 STC12C4052AD 自带 8 位 ADC通道 0 和通道 1 分别对应 P1.0 和 P1.1程序结构非常简单初始化 ADC、初始化串口、主循环里反复读通道 0 并把结果从串口发出去。简单归简单寄存器配置上有几个地方是必须写对的写错了不会报错只会给你一个恒定不变的读数。3.1 头文件里必须补的宏与 ADC 初始化第一个坎在编译阶段。有些版本的头文件里并没有定义 ADC 相关的寄存器位宏直接写 ADC_POWER 会提示未定义。这几个宏必须补齐而且要在包含头文件之前补或者直接改进头文件里。/* STC12C2052AD.H 中若缺少以下宏定义需补齐否则编译直接报错 */ #define ADC_POWER 0x80 /* ADC 模块电源控制位 */ #define ADC_SPEEDLL 0x00 /* 每次转换 1080 个时钟周期最慢 */ #define ADC_SPEEDL 0x20 /* 每次转换 810 个时钟周期 */ #define ADC_SPEEDH 0x40 /* 每次转换 540 个时钟周期 */ #define ADC_SPEEDHH 0x60 /* 每次转换 270 个时钟周期最快 */ #define ADC_FLAG 0x10 /* ADC 转换完成标志 */ #define ADC_START 0x08 /* ADC 开始转换控制位 */ #define ADC_CHS1 0x02 /* ADC 通道选择位 1 */ #define ADC_CHS0 0x01 /* ADC 通道选择位 0 */这些宏其实就是 ADC_CONTR 寄存器各个位的掩码后面所有配置都是把它们按位或起来写进去。速度档位那四个宏的注释值得留意转换速度和精度是反着来的越快越省时间但采样保持网络来不及跟上。初始化函数只有三行但每一行都有讲究void InitADC() { ADC_DATA 0; /* 数据寄存器清零 */ ADC_CONTR ADC_POWER | ADC_SPEEDLL; /* 开 ADC 电源选最慢转换档 */ Delay(2); /* 等 ADC 模拟部分电源稳定 */ }选最慢档 ADC_SPEEDLL 不是保守是必需。这个电路的信号源内阻是 560K 量级属于典型的高阻节点ADC 内部采样保持电容要在这点时间内充到和外部一致需要相当长的采样窗口。用最快档读出来会系统性偏低而且抖得厉害很多人以为是硬件没做好其实是速度档选错了。Delay(2) 那一下也别省ADC 模拟电路上电到稳定需要一点时间省掉的话头几次读数是不可信的。3.2 GetADCResult一次转换的完整时序真正干活的函数是这一个它把选通道、启动、等待、清标志、取数五步串成一次完整转换。uint GetADCResult(uchar ch) { /* 电源、速度、通道、启动位一次性写入 CONTR */ ADC_CONTR ADC_POWER | ADC_SPEEDLL | ch | ADC_START; _nop_(); _nop_(); _nop_(); _nop_(); /* 必要的等待让 START 位被硬件识别 */ while (!(ADC_CONTR ADC_FLAG)); /* 轮询等待转换完成 */ ADC_CONTR ~ADC_FLAG; /* 软件清零完成标志 */ return ADC_DATA; /* 返回 8 位结果0~255 */ }参数ch是通道号传 0 就是 ADC0P1.0传 1 就是 ADC1P1.1。返回值是 0 到 255 的原始码值不做任何换算——这个阶段保持原始值最有利于观察。这里有个容易踩的细节ADC_CONTR 是写配置、读状态共用同一个寄存器所以每次调用都必须把 ADC_POWER、ADC_SPEEDLL 这些配置位重新或进去不能只写通道位和 START 位。如果偷懒写成ADC_CONTR ch | ADC_START;ADC 电源位就被清掉了读回来永远是一个固定值。那四个_nop_()也是同理START 位写下去到硬件真正响应之间需要几个时钟的延迟全删掉在高主频下会丢启动。转换时间可以粗算最慢档 1080 个时钟周期12MHz 晶振下大约 90μs。这个耗时决定了后面的采样节奏——不能把它塞进高频中断里。3.3 主循环、端口高阻配置与采样节奏主函数里除了初始化最要紧的是两行端口模式配置void main() { P1M0 0x03; P1M1 0x00; /* P1.0/P1.1 配成高阻输入供 ADC0/ADC1 使用 */ InitADC(); InitUart(); while (1) { ShowResult(0); /* 先只观察通道 0 */ } }STC12 系列的 IO 上电默认是准双向口做 ADC 输入时必须切到高阻模式否则内部那两个弱上拉/强下拉的管子会把外部高阻节点的电压拽偏读数要么贴 0 要么贴满量程。P1M0 和 P1M1 的组合编码每颗芯片可能略有差异照着自己那颗片的数据手册核对一遍最稳妥别照抄。主循环里只读通道 0是有意为之。第一次上电的时候目标是先确认手指按上去数字会变这一件事多通道、多按键、消抖、状态机统统往后放。一次转换 90μs串口发两个字节 2400bps 大约 8ms函数里那两个 Delay(50) 加起来已经远超实际需要纯粹是给串口助手留出显示刷新的余量。等确认硬件链路通了再把延时压下来提高采样率。4. 串口联调把 ADC 数据变成能看的数字程序能跑起来之后接下来这一步决定了整个项目是能用还是能调。很多人的第一反应是拿万用表怼在 ADC0 上量电压量到 2.7V 和 2.3V 就以为大功告成直接把这两个值换算成十六进制写进程序当阈值。这条路走不通而且不是理论上走不通是实际接上系统就会出问题。串口联调这段代码只有三十来行但它是后面所有判据的数据来源。4.1 2400bps 串口初始化的三个寄存器串口部分用的是定时器 1 做波特率发生器工作在方式 28 位自动重装这样不需要在中断里手动重装初值。void InitUart() { SCON 0x50; /* 串口方式 18 位 UART允许接收 */ TMOD 0x20; /* T1 方式 28 位自动重装做波特率发生器 */ TH1 0xF3; /* 12MHz 晶振下对应 2400bps */ TR1 1; /* 启动定时器 1 */ } void SendData(uint dat) { SBUF dat; /* 写入发送缓冲硬件自动开始发送 */ while (!TI); /* 等发送完成标志 */ TI 0; /* 软件清零 */ } void ShowResult(uchar ch) { SendData(ch); /* 先发通道号多通道时用于区分 */ Delay(50); SendData(GetADCResult(ch)); /* 再发 8 位转换结果 */ Delay(50); }TH1 0xF3 这个值不是随便填的可以倒推一遍12MHz 晶振、12 分频后定时器计数频率 1MHzT1 方式 2 的溢出率是 1MHz/(256−TH1)。串口方式 1 的波特率 溢出率/32SMOD0 时要得到 2400bps则 256−TH1 1×10⁶/(32×2400) ≈ 13.02取整得 TH1 243 0xF3实际误差不到 0.2%2400bps 下完全够用。换个晶振这个值就得重算比如 11.0592MHz 下算出来是整数误差为零这也是为什么老项目偏爱 11.0592MHz。TMOD 0x20这一句要留神它是一条赋值语句不是按位或。如果程序里还有别的地方在配置 T0这一句会把 T0 的模式一起清掉顺序放错了就会出现串口好了、定时器不跑了的玄学现象。要么统一在一处配置 TMOD要么用TMOD (TMOD 0x0F) | 0x20;只改高四位。SendData的参数类型是 uint但 SBUF 只有 8 位超过 255 的值会被截断。这里只发原始码值0~255没问题但哪天想直接发十进制文本给串口助手看就得自己写一个把字节拆成三个 ASCII 字符的函数否则串口助手上看到的永远是乱码。4.2 为什么万用表读数不能直接当阈值第一个理由是测量工具本身的差异。万用表内部的 ADC 性能远好于单片机片内的 8 位 ADC同一路模拟量两者读出来再换算数值很难对得上。用万用表量到 2.70V按 5V 参考换算成码值 138但片内 ADC 的参考电压实际可能不是精确的 5.00V加上零点误差和增益误差读回来可能是 133 或者 143。差这几个码阈值就偏了。第二个理由更根本万用表读的是慢平均值片内 ADC 抓的是某一瞬间。感应按键周围本来就存在各种扰动——开关电源的纹波、继电器动作的瞬间、人手靠近但没接触、甚至板子布局带来的分布参数这些在万用表上被平均掉了在 ADC 上却是实打实的跳变。所以程序里的判据只能基于即时采样值万用表量出来的那个数只能当作中心值参考告诉你大概在这个范围不能当作阈值本身。还有一个很隐蔽的问题万用表表笔本身就带分布电容探上去的那一刻相当于给这个高阻节点并了一个电容电路状态已经变了。你量到的 2.7V是表笔接上去之后的 2.7V跟实际运行时的值未必是一回事。4.3 采样数据的记录方法与判据串口助手设成 HEX 显示波特率 2400数据位 8停止位 1无校验。上电后应该能看到成对的字节在滚第一个是通道号 00第二个是 ADC 码值。手指悬空不接触时记录一到两分钟取最小值和最大值手指实按在玻璃上再记录一到两分钟同样取最小最大值。把两组数放在一起看中间那段空档就是阈值可以落脚的地方。判断硬件是否正常的标准很朴素按下和松开两组数据的区间不能重叠中间至少要有十来个码的间隔。如果两组数几乎糊在一起先别改程序回头查硬件——八成是金属片没贴平玻璃、弹簧压力不够、或者 R1/R2 拿错了值。这个顺序很重要我见过太多次在软件里加了一堆滤波算法最后发现是电极根本没压紧。5. 避坑与常见问题排查那几个让我返工的细节这套东西元件少、代码短看起来很好复现但真正焊出来调通的人比例并不高。下面这几条都是实际会撞上的按现象 → 原因 → 解决记下来遇到时能少绕几圈。5.1 装配与硬件环节的坑现象一上电后串口一直在发 00 FF 或者 00 00手指怎么按都不变。原因基本锁定在端口模式上IO 还停在默认的准双向状态内部上拉把高阻节点拽死了ADC 读到的永远是轨值。解决是把 P1M0/P1M1 按芯片手册配成高阻输入配置语句放在 InitADC 之前。另一种可能是金属片根本没接到 ADC 那个管脚上弹簧顶偏了量一下通断就能确认。现象二手指按上去码值只掉三五个跟噪声分不开。先查 R1、R2 的实际阻值560Ω 当成 560K 用是最常见的翻车点阻值小了充放电太猛工作点跑到饱和区感应电容那点变化完全体现不出来。再查金属片和玻璃之间有没有气隙用手压紧金属片再试一次如果压紧后变化明显变大就是弹簧压力不足换弹力大一点的或者把弹簧压缩量加大。还有一种是 C0 取值太大nF 级往上走到几十 nF感应电容被稀释得更厉害把 C0 降到 1nF 附近试试。现象三装进外壳就失灵裸板测试一切正常。外壳带来的分布电容变化或者外壳上的金属装饰件、螺丝柱离电极太近。解决思路是把电极周围留出一圈禁布区螺丝柱换成塑料的实在避不开就参考前面说的屏蔽驱动做法在电极走线旁边布一根同步驱动的屏蔽线。另外面板胶如果溢到金属片和玻璃之间等于加了一层额外介质也会让灵敏度掉下来。5.2 程序与调试环节的坑现象四编译直接报 ADC_POWER、ADC_FLAG 未定义。原因就是头文件版本对不上这几组宏在部分版本里没有。解决是把第 3 章那十个宏定义补齐位置放在#include之前或者直接写进头文件里。补的时候对照数据手册确认位定义别凭记忆写0x80 和 0x08 差一位效果天差地别。现象五串口助手收到的全是乱码。第一顺位查晶振频率和 TH1 是否匹配12MHz 配 0xF3 才是 2400bps换了晶振没改 TH1 必然乱码。第二顺位查串口助手的波特率是不是也被改过有时候软件会自动记忆上次的设置。第三顺位查 TMOD 有没有被后面的代码覆盖掉特别是程序里还用了 T0 做延时的情况TMOD 0x20这一句会把 T0 的配置清空。还有一个偏门原因SendData 传了大于 255 的值SBUF 截断后发出的字节完全对不上看起来就像乱码。6. 二次开发从读得到数到判得准再顺手搬到 STM32把这套东西调通之后真正的活儿才开始怎么从一串跳动的数字里稳定地判出按下了。最简单的写法是if (v 128) key 1;但环境温湿度变化、电源波动、器件老化都会让基线慢慢漂移跑几天之后要么一直判按下要么怎么按都没反应。我一般会加一层基线跟踪和回差代码也就十来行#define TH_ON 10 /* 低于基线 10 个码判为按下 */ #define TH_OFF 5 /* 回升到基线 5 个码以内判为松开 */ static uchar base 0; /* 基线上电时用前若干次采样取平均得到 */ static bit key 0; void KeyScan() { uchar v GetADCResult(0); if (!key) { if (v TH_ON base) { key 1; /* 明显低于基线才置位 */ } else { base (base * 7 v) / 8; /* 空闲时才缓慢跟随环境漂移 */ } } else { if (v TH_OFF base) { key 0; /* 回到回差带内才释放 */ } } }两个细节值得说清楚。基线只在未按下状态下更新按下期间冻结——如果无脑更新手指按住不放的那几秒基线会被一点一点拉到按下的电平上然后按键自己松开了这个血泪经验我吃过不止一次。TH_ON 和 TH_OFF 取不同值是为了形成回差防止在临界点附近来回抖。用移位平均而不是直接赋值是为了抗单次干扰脉冲。想搬到 STM32 上判定逻辑可以原样保留变的是底层采样。STM32 有两条路一是用片内触摸感应外设硬件做电荷转移直接给出电容变化量抗干扰和一致性都比自己搭电路强代价是引脚和库的配置复杂一些二是继续用 ADC 加定时器触发靠多次采样求平均把信噪比拉上来。两者的取舍大致是这样维度STC12 分立元件方案STM32 触摸外设方案成本极低几个阻容加弹簧芯片本身较贵但省掉外围灵敏度调节改 R、C 和阈值全手调寄存器配置充电电流和采样数抗干扰靠布线、屏蔽和软件滤波硬件滤波加软件基线余量大适用场景三五个键、成本敏感的小家电按键多、要做滑条或接近感应移植之后验证方法还是老一套先把原始采样值从串口打出来手指按下松开各录一段看两组数据的间隔够不够再决定阈值和回差。上电老化测试也别省让板子连续跑几个小时看基线漂移的幅度有多大。从那以后我每次做这类按键都强制先跑一遍裸板录数据—装壳再录一遍—烤机两小时的流程不跳步。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?