前阵子我在做一块智能温控器的控制面板,主控选了一颗很常见的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显示和按键扫描如何共用同一组IO
2.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 | 开/关显示、调节亮度、极性翻转控制 |
| 显示RAM | 0x10~0x1F | LCD段码显示数据,每位对应一段 |
| 扫描配置寄存器 | 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,别省这一步。把这三个点吃透,基本上不会翻车。