前阵子我在做一块智能温控器的控制面板主控选了一颗很常见的8位通用MCU面板上要显示温度、时间、工作状态图标另外还得接6个轻触按键。按我过去习惯的做法LCD段码屏交给一颗专用LCD驱动芯片去推按键就在主控上拿GPIO搭矩阵扫描。画原理图的时候才发现IO位被占得七七八八按键消抖、连按识别这些逻辑又把定时器资源刮走不少。后来换成FZH1643这颗带键盘扫描的LCD驱动芯片整块面板的硬件和软件都清爽了很多I2C接口一条总线搞定显示数据下发和按键状态读取双模式切换也省掉了主控侧的调度负担。这篇文章就把这次改用的完整过程整理出来重点说清双模式切换的底层逻辑、I2C寄存器读写细节和键盘扫描链路怎么配合适合正在做小家电面板、仪器仪表或者智能家居控制器的朋友参考。1. 为什么非得把LCD驱动和键盘扫描塞进同一颗芯片1.1 段码LCD屏的驱动原理与主控直驱的死角段码LCD屏本质上是通过两片玻璃之间的液晶分子受电场控制偏转来显黑或者显灰电场要加在不同电极之间所以面板背后要引出COM公共端和SEG段端。我们常听到的1/2 duty、1/4 duty说的就是COM端有多少路。驱动芯片靠轮询的方式给各COM端加电压同时驱动SEG端让每一段在正确的位置获得压差从而实现显示。段数一多SEG引脚数量会非常可观。如果用主控直接驱动会面临至少三个问题。第一引脚不够用。一块带8字形和小数点的4位数码管屏至少需要4乘8等于32个段电极加上几个COM端直接吃掉三十几个IO一般小封装主控根本扛不住就算勉强够用也几乎没有余量给按键、蜂鸣器、传感器了。第二刷新调度非常烦。LCD是需要交流波形驱动的如果两端的直流成份长期存在液晶材料会老化甚至损坏所以驱动芯片要周期性翻转电压极性。这个翻转频率一般不能太低低了人眼会看到闪烁也不能太高高了功耗上升。让主控在业务逻辑里同时维护这个刷新时序就是给自己找罪受。第三电平匹配问题。有些LCD屏的工作电压和主控IO电平不一致比如屏需要4.5V左右的驱动电压而主控是3.3V系统那就需要额外的电平处理电路徒增成本。所以大家早就改用专用LCD驱动器了主控用串行接口把显示数据送进去后面的刷新、极性翻转、COM扫描全部由驱动芯片自己完成。这个思路本身没有毛病问题出在键盘上。1.2 键盘扫描在MCU端造成的资源挤压面板上除了显示几乎必有按键。按键扫描的典型做法是矩阵扫描行线输出扫描电平列线读回电平根据行列组合判断哪个键被按下。硬件上还得分出几个IO给行和列软件上要做防抖、连按识别、组合键判断、长按计时这些逻辑看着不复杂但写起来一点都不省心。按键少的时候问题不明显比如两三个按键主控直接轮询电平就行。但一旦超过四个矩阵扫描就是必然选择IO占用和扫描代码复杂度同时上来了。我记得之前做过一个带12键的仪表项目主控40个引脚几乎用满中断资源和定时器资源也被消抖逻辑瓜分到最后加一个功能都战战兢兢。更麻烦的是在很多传统方案里LCD驱动芯片和键盘矩阵是两套独立系统一个走串口一个走GPIO互不相通。单片机代码里显示任务和按键任务各自抢占CPU时间片逻辑一多就很容易出现按键卡顿或者显示刷新不及时的问题。这个状态持续了很久直到我注意到FZH1643这类把LCD驱动和键盘扫描整合在一起的二合一芯片。1.3 二合一方案真正省掉的开销FZH1643做的事是把两件高频事情收敛到一个从机上LCD显示数据通过I2C写入键盘扫描由芯片内部完成主控通过I2C直接读回键值。主控这边只留一个I2C接口显示数据往里写按键状态往里读CPU大部分时间可以睡觉整机功耗好控制尤其在电池供电的设备里这个特性很值钱。它真正的价值不是把两个芯片拼成一个而是把系统级开销砍掉了。硬件上少了键盘扫描的GPIO分配压力PCB走线大幅简化软件上不用写矩阵扫描状态机不用维护防抖定时器显示刷新和按键读取共用一套I2C读写函数。代码量保守估计少了三分之一而代码量减少意味着出bug的面也小了。2. 双模式切换机制LCD显示和按键扫描如何共用同一组IO2.1 底层原理显示帧与扫描时隙的分时复用FZH1643引脚的物理数量是固定的要在同样的引脚上既跑LCD段驱动又跑键盘扫描核心就是分时复用。这个机制理解透了后续调试会顺手很多。LCD显示刷新是周期性的假设一块1/4 duty的屏每路COM在1个显示帧周期内只会有1/4的时间处于导通驱动状态其余时间对这颗COM来说其实是空闲的。而键盘扫描也是一样行线逐行拉低或拉高每一行只有很短暂的电平变化列线大部分时间处于高阻或保持状态。两种工作在时间上天然可以互相穿插。FZH1643内部有一个模式配置寄存器写入对应配置字之后芯片会在一个显示周期里自动插入按键扫描时段扫描完成再切回LCD驱动整个切换过程由芯片内部状态机调度不需要MCU干预。这也是“双模式”这个叫法的由来用户从宏观上看到芯片在不同工作状态间切换实际上内部是由很多微小时隙交替组成的。理解这一点后就不会纠结“芯片到底是怎么从显示瞬间切到按键的”这种问题了。2.2 模式切换的完整动作序列与关键参数从我实际使用的这颗芯片来看模式切换的完整动作大概是这样的主控通过I2C写配置寄存器将工作模式设为“LCD显示键盘扫描”共同使能。芯片不会立刻中断正在进行的COM扫描而是等到下一个显示同步点才执行切换避免画面撕裂。切换到键盘扫描态时原先作为SEG输出的引脚转为行输出或列输入。扫描完成之后自动恢复SEG输出显示RAM里的数据不需要重新写入。按键事件被锁存到键值寄存器如果INT脚使能还会输出一个中断脉冲提醒主控来读。实际操作时有几个参数需要结合自己的面板来定。显示刷新率我习惯设在80Hz到160Hz之间太低了能感觉到闪烁太高了功耗增加。按键扫描周期典型值4ms到10ms扫描周期太短会增加主控读键频率太长则按键响应变慢。每次扫描占用的时间片这个由芯片内部决定通常在微秒级到几十微秒级对显示亮度的影响很小。这些参数一般在“扫描配置寄存器”里调整不同封装或者不同批次可能略有差异务必以手头这颗芯片的数据手册为准。2.3 切换时LCD闪烁问题到底出在哪用分时复用方案大家最关心的就是显示会不会闪。从原理上讲人眼视觉暂留时间大约是0.05到0.1秒也就是人眼能感知到的最短间隔大概在50毫秒左右。FZH1643显示刷新周期如果是12.5毫秒按键扫描插入的时隙只有几十微秒远低于视觉暂留阈值所以正常情况下你盯着一块设置好的屏看是察觉不到切换动作的。我踩过的坑是按键扫描周期被我调得太激进之后显示出现了轻微变暗的现象。排查下来不是芯片坏了而是键盘扫描时间片插入得过于频繁挤压了LCD驱动时间。解决办法是把扫描周期从4ms调回8ms亮度立刻恢复正常。所以如果你遇到显示变暗或者出现细微横条纹不用急着怀疑硬件先看看扫描配置是不是设得太极端了。另外有一个值得注意的操作细节模式切换后显示RAM的内容理论上不受影响但我实际测过某些批次的芯片在频繁切换时偶尔会出现显示偏暗的情况。稳妥的做法是切换完成后主动重写一次显示RAM也就是把已经保存在主控里的显示缓存整体刷一遍这个习惯能帮你省掉很多定位问题的时间。3. I2C从机通信细节寄存器读写和多字节传输不再翻车3.1 器件地址分配与总线共存FZH1643在I2C总线上作为从机工作7位器件地址通常由A0、A1引脚的电平组合决定常见主地址落在0x70写地址、0x71读地址这个范围内。不同封装的引脚可能会有差异所以第一步就是去数据手册确认自己这颗芯片的默认地址和可配置范围。如果总线上不止挂这一颗芯片比如同时挂了EEPROM或者温湿度传感器就一定要把地址错开。我见过一个案子工程师把FZH1643的A0A1都接地结果和板上EEPROM的地址撞了写显示数据的时候顺带把EEPROM里的校准参数给改了设备当场抽风。这种问题查起来很费劲但根源往往特别简单就是原理图阶段没有仔细做地址分配。3.2 寄存器地图和读写命令格式以我项目里用的这颗FZH1643为例寄存器可以大致归成几类模式配置、显示控制、显示RAM、扫描配置、键值寄存器。我整理了一个简化版的寄存器地图方便你对照理解。寄存器类别寄存器地址功能模式配置寄存器0x00设置工作模式、使能LCD/键盘扫描显示控制寄存器0x01开/关显示、调节亮度、极性翻转控制显示RAM0x10~0x1FLCD段码显示数据每位对应一段扫描配置寄存器0x20键盘行列数、扫描周期、去抖时间键值寄存器0x30保存最近一次扫描到的按键值读写命令格式一般遵循“从机地址寄存器地址数据”的结构。写寄存器时主控先发从机写地址然后发寄存器地址再发一个或多个数据字节读键值时先写寄存器地址然后用重复起始位切换为读模式再读回数据。3.3 显示RAM写入与键值读取的代码骨架直接上代码更直观。下面是一段写显示RAM的示例假设寄存器地址支持自动递增。#define FZH1643_ADDR_W 0x70 #define FZH1643_ADDR_R 0x71 void fzh1643_write_ram(uint8_t start_addr, uint8_t *buf, uint8_t len) { i2c_start(); i2c_send_byte(FZH1643_ADDR_W); // 器件写地址 i2c_send_byte(0x80 | start_addr); // 命令头指示从哪个寄存器开始写 for (uint8_t i 0; i len; i) { i2c_send_byte(buf[i]); // 地址自动递增连续发送即可 } i2c_stop(); }读键值寄存器的代码也不复杂uint8_t fzh1643_read_key(void) { uint8_t val 0xFF; i2c_start(); i2c_send_byte(FZH1643_ADDR_W); i2c_send_byte(0x30); // 键值寄存器地址 i2c_restart(); i2c_send_byte(FZH1643_ADDR_R); // 器件读地址 val i2c_read_byte(0); // 最后一个字节发NACK i2c_stop(); return val; }需要注意一点有些LCD驱动芯片的显示RAM地址不会自动递增必须逐地址写。我第一次用类似芯片时吃了这个亏写进去的数据错位显示内容完全乱套。所以真正写代码前建议先用逻辑分析仪抓一次写时序确认地址是不是按预期递增的。3.4 上拉电阻、总线速率和卡死恢复I2C总线是需要上拉电阻的阻值选择与总线电容和通信速率有关。标准模式下100kHz用4.7k到10k的上拉基本没问题如果跑400kHz高速模式建议上拉电阻降到2.2k到4.7k之间否则边沿变缓通信容易偶发错误。走线越长总线电容越大上拉电阻就要选得更小但也不能太小否则功耗和驱动能力都会出问题。I2C最让人头疼的是总线锁死。一个常见场景是主控在从机还没释放SCL或SDA的时候异常复位导致总线被拉低。恢复方法不复杂把SCL连续翻转9个时钟脉冲让从机状态机回到空闲态。很多MCU的硬件I2C外设没有自动恢复功能所以我通常在驱动层加一个软件恢复函数初始化时调用一次确保总线是干净状态。另外如果芯片提供INT中断输出脚建议接到主控的外部中断输入上。按键按下时INT脚会拉低主控从低功耗模式被唤醒然后才去读键值寄存器这样就不用在主循环里轮询读键了。这个设计对电池供电设备意义很大可以把待机功耗压得很低。4. 键盘扫描链路实战从行列矩阵到防抖和事件上报4.1 扫描矩阵与LCD引脚的复用关系FZH1643的键盘扫描部分通常支持最大8乘8矩阵也就是64键对于日常面板应用完全够用。行列扫描脚实际上复用的是LCD的SEG引脚原理图上会看到某些SEG端既连着LCD的段电极又通过按键连到另一根SEG线上。这个接法最容易让人困惑LCD驱动时这些脚是段输出扫描时变成了行输出和列输入。这里有一个关键设计点按键不能直接把两个SEG脚短在一起完事因为LCD驱动波形和键盘扫描电平会互相干扰。稳妥的做法是在按键回路里串一个330欧姆到1k欧姆的电阻把按键短路对LCD段驱动的负载影响降到最低。这一点最好在原理图阶段就按官方评估板的接法来做不要自己发挥。实际画图时按键的一端接到一个SEG脚复用为行另一端通过电阻接到另一个SEG脚复用为列芯片扫描时靠内部开关切换电气方向。我第一次画这块电路时没串电阻按键按下瞬间LCD的段会出现微弱的亮度变化后来补上串联电阻就正常了。4.2 芯片自动扫描与主控读键流程在FZH1643的方案里键盘扫描由芯片内部自动完成主控只需要读回键值。这个和传统“主控自己拉IO扫描矩阵”的思路是两回事。读按键的标准流程如下确认模式配置已经打开“LCD键盘扫描”共同使能。等待INT脚触发或采用定时器周期读取。读回键值寄存器。如果键值不是空键标志0xFF说明有按键事件。软件再做一次防抖确认两次读值相同才认为有效。将有效按键通过事件队列交给上层应用处理。下面是带防抖的读键示例#define KEY_NONE 0xFF uint8_t fzh1643_read_key_debounced(void) { uint8_t k1 fzh1643_read_key(); delay_ms(5); uint8_t k2 fzh1643_read_key(); if (k1 k2 k1 ! KEY_NONE) { return k1; } return KEY_NONE; }4.3 防抖、长按、组合键的处理边界防抖通常做两层硬件层在按键并联一个0.1uF的电容滤掉高频抖动软件层在读键后延时5到10ms再确认一次。两层配合起来触发的可靠性是非常高的。如果面板所处的电磁环境恶劣也可以再加TVS管或者RC滤波但常规家用产品不加也能稳定工作。需要明确一个边界FZH1643报告的是单个键值组合键和长按逻辑需要主控自己处理。组合键的做法是连续读键把多次读到的键值合在一起来判断长按则是按键按下后开始计时达到阈值就触发重复上报。芯片负责的是底层的矩阵扫描和键值锁存上层的业务逻辑全部在主控端。我通常会在主控里维护一个简单的按键事件结构体包含键值和事件类型短按、长按、重复触发都从这个结构体往外抛应用层只关心事件不关心底层怎么扫到的。4.4 实测按键响应与常见误触排查我在这块温控器面板上实测6键扫描加LCD同时工作按键响应时间在10毫秒以内手感和直接用主控扫描没有区别。快速滑动按键时偶尔会连续读到相邻键值原因不是芯片异常而是扫描周期比较短同一列相邻行在很短间隔内都被扫到了应用层根据自己的需求决定是否过滤。如果出现按键误触排查顺序一般是这样先看硬件按键是否串了电阻、是否并了电容再看配置扫描周期是否太短行列数是否配置正确最后看软件读键时序是不是和LCD刷新抢总线。这三个层面都排查过误触基本就能解决。5. 把FZH1643集成进项目画板、写驱动、踩坑记录5.1 原理图设计的几个细节原理图阶段最值得花时间的是把电源和复位处理好。FZH1643驱动LCD屏的时候瞬间电流会比静态时大不少VDD上至少放一组0.1uF加10uF的去耦电容并且要尽量靠近芯片电源脚。有些朋友习惯把所有电容都堆在电源入口这种做法对主控可能够用但对LCD驱动芯片来说效果会打折扣因为走线电感会削弱滤波效果。I2C上拉电阻要就近放靠近主控或者芯片都行但不要拉得很远以防引入不必要的干扰。INT脚如果要用记得加上拉电阻因为这类芯片的中断输出通常是开漏结构。LCD背光如果走LED不要用芯片的某个GPIO直接驱动建议用三极管或者MOS管做开关否则背光电流会拉低芯片供电电压。5.2 PCB布线时容易忽略的干扰源PCB布线最需要注意的是LCD走线和I2C走线的隔离。LCD的COM和SEG是周期性翻转的电压信号如果这些线和I2C数据线长距离并行段码翻转的边沿容易耦合到I2C线上引起通信错误。我一般会让I2C走线尽量短并且在两层板时用GND包住I2C信号线。按键复用SEG脚之后按键矩阵走线也会变成动态信号线所以按键行和列的线也不要贴着晶振、复位电路这些敏感节点走。如果面板空间紧张宁可靠远一点绕线也不要让按键线穿过LCD玻璃引脚密集的区域。5.3 软件架构与驱动分层建议软件层面把驱动拆成几个独立模块后面维护会轻松很多。i2c_hal.c底层I2C读写包括总线恢复函数。fzh1643.c芯片操作层包括模式切换、写显示RAM、读键值寄存器。lcd_display.c显示缓冲区管理把待显示的数据整理成屏幕需要的位图格式。key_event.c按键事件处理包括短按、长按、重复触发。显示缓冲区建议固定定义一个数组来保存当前显示内容例如uint8_t lcd_ram[16];要改变显示内容时先改这个缓存数组再整体刷进芯片。这样做有两个好处一是减少I2C的零散写入降低总线负载二是模式切换后需要重刷显示RAM时数据源就在主控里直接再写一遍就行不用重新计算。我在代码里习惯保留一份“影子显存”在芯片的RAM和主控各一份所有上层逻辑只操作主控这版影子数据芯片端的数据只是影子数据的一次完整镜像。这样一来无论芯片是意外复位还是模式切换把显示弄丢主控都能随时恢复不需要重建整个显示对象。5.4 三个真实踩坑记录坑一模式切换后LCD内容丢失。我最初把扫描周期调到4ms后显示出现轻微闪动切换回8ms之后显示稳定了但偶尔还是会出现某一段莫名其妙不亮的情况。后来发现是模式切换时显示RAM偶发没保存好。解决办法就是每次模式切换完成后把主控里的显示缓存整体重刷一遍现象彻底消失。坑二I2C地址冲突。某块板上同时还挂了一颗EEPROM地址撞在一起结果写显示数据的时候把EEPROM里的配置参数改了。排查过程很费劲一开始以为是EEPROM写保护没开后来用逻辑分析仪抓波形才发现从机地址段根本不对。从此以后我养成了一个习惯画原理图时先把总线上所有器件的地址列一张表确认全部错开再往下做。坑三按键误触。最初按键没串电阻、没并电容主控在LCD刷新期间读键值偶发误判。补上330欧姆串联电阻和0.1uF并联电容之后问题彻底解决。这个锅不在芯片而在复用引脚的电平干扰属于原理图阶段就可以避免的问题。5.5 上电时序别省那50毫秒主控上电后不要立刻访问芯片芯片内部上电复位需要一点时间建议延时50毫秒以上再去写配置。我见过不少朋友在初始化函数里不延时就开始写寄存器结果第一帧显示不出来又回头怀疑芯片是坏的。实际上就是上电时序不满足要求。初始化顺序也建议固定下来先延时保证芯片上电稳定然后调I2C总线恢复函数确保总线空闲再写模式配置寄存器使能LCD和键盘扫描最后把显示缓存整体刷新一遍。这个顺序踩熟了之后基本可以做到一次点亮。//这次做温控器面板最终测试下来LCD刷新稳定按键响应迅速整机待机电流比之前用两颗芯片的方案低了不少。说实话从画原理图到调完驱动整个过程比我预想中顺利主要收益是把“显示加输入”这两件高频事收敛到一个I2C从机上主控代码简单了出问题的面反而小了。如果你接下来要选这类带键盘扫描的LCD驱动芯片我建议重点确认三件事第一数据手册里的寄存器地址别拿错版本第二键盘矩阵的复用引脚到底是不是和SEG共用原理图要按官方推荐接法来第三模式切换完成后主动刷新一次显示RAM别省这一步。把这三个点吃透基本上不会翻车。