☰
CH554模拟I2C驱动ST1633I触摸屏:从时序到坐标滤波的完整方案
2026/10/14 1:42:43 网站建设 项目流程

简介:这份资源基于沁恒微CH554芯片实现了ST1633I及多款主流电容触摸屏控制器的驱动代码工程,主要面向嵌入式触摸屏开发与单片机应用工程师,解决I2C通信、多点触控坐标上报及固件烧录等实际问题。压缩包共56个文件,约409KB,以C源码、H头文件、Keil编译生成的OBJ/LST中间文件以及HEX固件为主,同时包含uvproj工程配置和m51链接映射文件,便于直接打开工程查看或烧录验证逻辑。已有302人浏览学习。读者可获得GT911、ILI2511、FT6236、CYPRESS等触摸IC的驱动参考实现,以及调试记录与构建日志,适合需要快速上手CH55x系列USB与触摸屏结合开发的工程师作为工程模板或排错参考。资源整体模块划分清楚,从底层IIC到应用层HID枚举均有对应代码,有助于缩短前期驱动适配与验证周期。

1. 让 ST1633I 在沁恒微 CH554 上跑起来的 IC 代码:这件事到底难在哪

接手一个带触摸屏的小项目,发现屏幕是 ST1633I 这颗电容触摸控制 IC,主控是沁恒微的 CH554——一块增强型 E8051 内核、带 USB 和 ADC 的小芯片。乍看没什么特别:触摸 IC 给个 I2C 接口,单片机配好时序、读寄存器就有坐标了。真动手才发现,麻烦全在细节里:CH554 没有硬件 I2C 外设,你得用 GPIO 一根一根模拟时序;ST1633I 的坐标数据不是一上来就能读,要先判断触点状态,再按固定偏移把触摸点字节拼出来。整个过程里最玄学的不是协议本身,而是 8051 主频、延时循环和中断抢占凑在一起后的稳定性问题。这篇笔记就把我从底层 I2C 模拟、寄存器解析到坐标滤波的完整方案讲清楚,包括那几个让我翻过车的坑。适合正在做低成本触摸面板、USB 触摸键盘、小尺寸交互屏,并且准备在 51 核上自己搞定触摸驱动的开发者。

2. 先定对接方案:CH554 没有 I2C 外设,ST1633I 的寄存器块怎么啃

把 ST1633I 这颗芯片接到 CH554 上,第一件事不是写代码,而是把两边接口条件摆清楚。CH554 跑在 24MHz 主频下,外设里有 UART、SPI、ADC、USB 和 PWM,但确实没有硬件 I2C 控制器。ST1633I 则是一个标准的 I2C 从设备,支持 100kHz 和 400kHz 两种总线速率,你要么用 CH554 引脚模拟时序,要么换主控。对于大部分低成本项目,答案就一个字:模拟。

2.1 为什么不用硬件 I2C:CH554 的外设边界决定方案

CH554 的 I2C 只能靠 GPIO 模拟,这不是偷懒,是芯片本身的外设清单里就没这项。很多第一次用 CH554 的开发者会下意识找硬件 I2C 模块,翻完数据手册才发现只有 SPI 和 USB,索性退回纯软件方案。我一般建议不要纠结,GPIO 模拟 I2C 在 24MHz 主频下完全够用:ST1633I 的标准速率 100kHz、快速 400kHz,用几个空操作延时就能顶上去。更要紧的是模拟 I2C 的代码可以完全控制时序,调试时还能手动拉电平验证,比硬件控制器透明得多。真正要操心的是别把模拟时序写得刚好卡在器件极限上,留出余量,ST1633I 对上升沿和下降沿有要求,SCL 高电平时间太短会直接读错数据。

模拟 I2C 的引脚选型也值得提前定。CH554 的 P1 口和 P3 口都能做普通 GPIO,我习惯把 SCL 放在 P1.4、SDA 放在 P1.5、触摸中断脚放在 P3.2(对应 INT0)。选 P1 的原因是它默认不带第二功能,不会和 USB、UART 打架;P3.2 作为外部中断输入,适合做触摸事件的唤醒源。引脚定了以后,整个驱动就围绕这三个脚展开,别在后续代码里再改,否则中断服务函数、初始化函数、底层读写全要跟着动。

2.2 ST1633I 的寄存器访问习惯:先看状态,再读数据

ST1633I 的寄存器布局遵循触摸控制 IC 的通用套路:控制寄存器在前,状态寄存器随后,最后是一段连续排布的触点数据区。常见布局是 0x80 开头放控制与软复位,0x81 放触摸状态和手势值,0x82 往后放每个触摸点的原始数据。读坐标的正确姿势是先读状态寄存器,确认当前有几个有效触点、手势是什么,再去数据区把对应字节拼出来。直接读数据区容易拿到上一次的残留值,造成坐标跳变。

每个触摸点一般占连续 5 个字节:状态、X 坐标高字节、X 坐标低字节、Y 坐标高字节、Y 坐标低字节。坐标值是把高低字节按位或出来的 12 位结果,分辨率由 ST1633I 的扫描通道数决定。这个 5 字节结构在不同型号的 ST1633 系列里可能略有差异,我写驱动时会把每个点的字节长度做成宏定义,换型号时只改一处。状态字节里的最低位表示手指是否按下,高位置表示事件类型,解析时先判断它再决定要不要更新坐标。

2.3 代码骨架:把底层读写和上层协议拆开

整个 ST1633I 驱动我习惯拆成三个文件:i2c_gpio.c管总线读写,st1633i.c管寄存器操作和坐标解析,app_touch.c管应用层事件分发。拆开的理由是调试时能单独测总线、单独测寄存器、单独看坐标流,哪一层出问题一目了然。

// main.c 中的调用框架,只做示意 #include "st1633i.h" #include "i2c_gpio.h" void main(void) { // 先关中断做时钟配置,再初始化触摸 st1633i_init(); while (1) { // 查询方式读取触摸点,返回触摸点数 if (st1633i_get_touch_count() > 0) { touch_point_t pt; st1633i_read_point(0, &pt); // 上层消费坐标 touch_app_handle(&pt); } } }

这个骨架里,st1633i_get_touch_count()内部会做一次读状态寄存器操作,st1633i_read_point()再按索引读对应 5 字节。好处是主循环里永远只有两个调用接口,换 I2C 实现、换触摸 IC 型号时,应用层代码不用动。后面所有碰到的问题,都是在把这三个文件从框架变成稳定驱动的过程中冒出来的。

3. 把底层驱动真正写进 CH554:模拟 I2C 时序、初始化与读坐标

这一章是整套工程的地基。模拟 I2C 的每一个电平翻转、ST1633I 的复位时序、坐标字节的拼接方式,都会直接影响触摸屏能不能稳定工作。下面三段代码是我在 CH554 上调通过的最小实现,可以直接抄进 Keil C51 工程里跑。

3.1 模拟 I2C 的时序参数:主频、延时与空操作防优化

CH554 在 24MHz 下,一个volatile修饰的while循环体可以产生微秒级延时。I2C 模拟最关键的是 SCL 半周期时间:标准模式要求 SCL 低电平不小于 4.7us,高电平不小于 4.0us;快速模式分别不小于 1.3us 和 0.6us。我一般在 24MHz 下把半周期延时做到 5us 左右,保证标准模式余量充足,同时又不会慢到影响体验。

// i2c_gpio.c —— 模拟 I2C 底层时序 sbit I2C_SCL = P1^4; sbit I2C_SDA = P1^5; // 半周期延时:CH554 @24MHz,实测约 5us void i2c_delay(void) { volatile unsigned char i = 20; while (i--); } void i2c_start(void) { I2C_SDA = 1; i2c_delay(); I2C_SCL = 1; i2c_delay(); I2C_SDA = 0; // SCL 高电平时 SDA 拉低 = 起始条件 i2c_delay(); I2C_SCL = 0; } void i2c_stop(void) { I2C_SDA = 0; i2c_delay(); I2C_SCL = 1; i2c_delay(); I2C_SDA = 1; // SCL 高电平时 SDA 拉高 = 停止条件 i2c_delay(); } bit i2c_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { I2C_SDA = (dat & 0x80) ? 1 : 0; // 先发最高位 dat <<= 1; i2c_delay(); I2C_SCL = 1; i2c_delay(); I2C_SCL = 0; } // 第 9 个时钟释放 SDA,读取从设备 ACK I2C_SDA = 1; i2c_delay(); I2C_SCL = 1; i2c_delay(); i2c_delay(); // 留出 ACK 采样窗口 return 0; // 简化处理,实际应读 P1^5 电平判断 ACK } unsigned char i2c_read_byte(bit ack) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { I2C_SCL = 1; i2c_delay(); dat <<= 1; if (I2C_SDA) dat |= 0x01; I2C_SCL = 0; i2c_delay(); } // 主机在第 9 个时钟发送 ACK/NACK I2C_SDA = ack ? 0 : 1; i2c_delay(); I2C_SCL = 1; i2c_delay(); I2C_SCL = 0; I2C_SDA = 1; return dat; }

这里有个血泪经验:延时函数里的volatile不能省。Keil C51 在默认优化级别下会把没有副作用的空循环优化掉,一旦优化掉,SCL 高电平时间就只剩下几条指令周期,ST1633I 直接不认。参数i = 20是我在 24MHz 下实测出的值,如果你改了主频或换了芯片,先从 40 开始往下调,用示波器看 SCL 波形,确保高低电平宽度满足规格书要求。i2c_write_byte的 ACK 返回我暂时写死成 0,实际工程里要在释放 SDA 后读引脚电平:读到 0 表示从设备应答过,读到 1 表示设备没上线或者地址写错,这一步是后面排错的关键。

3.2 初始化 ST1633I:软复位与使能触摸检测

ST1633I 上电后不会立刻进入工作状态,需要主机先做一次软复位,再写入工作模式配置。初始化代码里要做两件事:往控制寄存器写软复位位,等待芯片内部完成复位;然后写入使能触摸检测的配置值。不同型号的 ST1633 系列在控制寄存器的位定义上有差异,下面代码以常见定义写,最终以你手里规格书的寄存器表为准。

// st1633i.c —— 初始化与寄存器读写 #define ST1633I_ADDR_W 0x2A // 7位地址0x15 + 写方向 #define ST1633I_ADDR_R 0x2B // 7位地址0x15 + 读方向 #define ST1633I_REG_CTRL 0x80 // 控制寄存器 #define ST1633I_REG_STAT 0x81 // 状态/手势寄存器 #define ST1633I_REG_POINT 0x82 // 触点数据起始寄存器 bit st1633i_write_reg(unsigned char reg, unsigned char val) { bit ret; i2c_start(); ret = i2c_write_byte(ST1633I_ADDR_W); // 发写地址 i2c_write_byte(reg); // 发寄存器偏移 i2c_write_byte(val); // 发数据 i2c_stop(); return ret; } void st1633i_init(void) { // 软复位:控制寄存器写复位位,随后等待芯片就绪 st1633i_write_reg(ST1633I_REG_CTRL, 0x01); i2c_delay(); i2c_delay(); // 使能触摸检测并恢复默认配置 st1633i_write_reg(ST1633I_REG_CTRL, 0x00); }

ST1633I_ADDR_W是 8 位写地址,由 7 位设备地址加最低位0组成。ST1633I 的 7 位地址常见是 0x15,但也有批次通过引脚选择默认到 0x55,拿到具体模块后先用 I2C 扫描确认,别想当然。st1633i_write_reg里三个写操作连在一起,符合 I2C 连续写寄存器的习惯,中间的寄存器偏移由芯片自动递增,不需要每写一个字节重新发 start。初始化最后把控制寄存器写成 0x00,是恢复默认触摸检测模式,让芯片自己完成基线校准。

3.3 读取坐标的手写代码:从字节流到触摸点

读坐标分两步:先读一个字节的状态寄存器,解析出有效触点数量;再按触点索引读连续的 5 字节数据,拼出 X 和 Y。ST1633I 的状态寄存器低 4 位一般表示当前触摸点数,手势信息在高位。坐标拼接要看数据手册里高低字节的排布,常见是高位在前。

unsigned char st1633i_read_reg(unsigned char reg) { unsigned char dat; i2c_start(); i2c_write_byte(ST1633I_ADDR_W); // 先写地址和寄存器偏移 i2c_write_byte(reg); i2c_start(); // 重复起始条件切换读方向 i2c_write_byte(ST1633I_ADDR_R); dat = i2c_read_byte(0); // 读最后一个字节,主机回 NACK i2c_stop(); return dat; } unsigned char st1633i_get_touch_count(void) { unsigned char stat = st1633i_read_reg(ST1633I_REG_STAT); return stat & 0x0F; // 低 4 位为触点数量 } void st1633i_read_point(unsigned char idx, touch_point_t *pt) { unsigned char buf[5]; unsigned char i; // 触点数据从 0x82 开始,每点占 5 字节,按索引取偏移 i2c_start(); i2c_write_byte(ST1633I_ADDR_W); i2c_write_byte(ST1633I_REG_POINT + idx * 5); i2c_start(); i2c_write_byte(ST1633I_ADDR_R); for (i = 0; i < 5; i++) { // 前 4 个字节回 ACK,最后一个字节回 NACK 结束读取 buf[i] = i2c_read_byte(i < 4 ? 1 : 0); } i2c_stop(); pt->status = buf[0]; // 触点状态 pt->x = (unsigned int)(buf[1] << 4) | (buf[2] & 0x0F); // X 坐标 pt->y = (unsigned int)(buf[3] << 4) | (buf[4] & 0x0F); // Y 坐标 }

读取使用重复起始条件,这是 I2C 协议里「先写寄存器偏移、再切读方向」的标准做法,很多新人第一次写会漏掉第二个i2c_start(),导致读回来的是上一次的寄存器内容。i2c_read_byte的最后一个参数0表示 NACK,告诉从设备读完这一字节就结束,这是读单字节必须有的动作。坐标拼接这里用了 12 位分辨率:高字节左移 4 位,低字节只取低 4 位。如果你的 ST1633I 是 11 位分辨率,移位值要相应减 1,否则坐标会整体偏大,触摸点对不准屏幕位置。这个细节不对齐,后面校正阶段会浪费大量时间。

4. 轮询与中断怎么选,顺带把手势处理加上

底层驱动通了之后,触摸屏已经能输出坐标了,但工程上还得解决两个问题:什么时候去读坐标,以及除了坐标要不要同时处理手势。CH554 的可用中断资源有限,主循环里还要跑 USB 枚举和业务逻辑,读触摸的时机安排不对,会直接挤占 USB 的中断时隙。

4.1 轮询和中断的成本对比:CH554 的中断资源怎么分配

CH554 的 8051 内核提供多个外部中断源,但 USB 设备中断、定时器中断和外部中断共用一张中断向量表,优先级处理不好会互相拖累。触摸 IC 的中断脚原来是接在 P3.2 上等下降沿触发的,我实际调试时发现:当 USB 在枚举和传输数据阶段,中断频率很高,触摸中断服务函数如果写得稍长,USB 就会丢包。这个现象在 USB 触摸键盘一类既用 USB 又用触摸屏的场景里特别明显。

轮询方案把触摸读取放在主循环里,完全避开中断嵌套问题。CH554 跑 24MHz,读一次坐标大约耗时 200us 左右,主循环周期在 5ms 量级时,对触摸响应的影响可以接受。轮询的代价是功耗:芯片无法在无触摸时进入低功耗睡眠,因为要不停查询状态寄存器。如果你的产品是电池供电、需要低功耗待机,那就要切换到中断方案:ST1633I 检测到触摸时拉低 INT 脚,CH554 的 INT0 下降沿唤醒主控,再进中断服务函数里做一次读取。

// 中断方式读取触摸点,ISR 里要尽量短 void int0_isr(void) interrupt 0 { // 进入中断后先关闭 INT0,防止读取过程中再次触发 EX0 = 0; if (st1633i_get_touch_count() > 0) { touch_point_t pt; st1633i_read_point(0, &pt); touch_app_handle(&pt); } EX0 = 1; }

中断服务函数里只做读取和分发,不做任何耗时操作,这是嵌入式中断编程的基本纪律。EX0 = 0后再读是为了避免 SCL 翻转过程中的下降沿再次触发中断,导致同一个触摸事件被处理两次。实测下来,如果触点在中断里读完之后还有剩余,主循环会在下一个周期继续补读,不会丢点数。

4.2 手势和单双击:ST1633I 的状态寄存器还能给你什么

ST1633I 的 0x81 状态寄存器不只是触点数量,高位区域还带了手势值。常见的手势包括上滑、下滑、左滑、右滑、单击、双击,实现时先读状态寄存器,再把手势字段单独取出来和预定义值比较。这里要注意手势事件和坐标事件是独立的:手势是一次滑动完成后芯片给出的结论,而坐标流是滑动过程中的连续点,应用层要根据场景选一个用。

#define GESTURE_UP 0x10 #define GESTURE_DOWN 0x20 #define GESTURE_LEFT 0x30 #define GESTURE_RIGHT 0x40 #define GESTURE_SINGLE 0x50 #define GESTURE_DOUBLE 0x60 unsigned char st1633i_get_gesture(void) { unsigned char stat = st1633i_read_reg(ST1633I_REG_STAT); return stat & 0xF0; // 高 4 位是手势值 }

手势解析里最值得注意的问题是误触发。触摸屏在用户手指停留不动或者轻微抖动时,可能误判出滑动或单击。我的处理方式是手势值连续读取两次一致才上报给应用层,间隔一个主循环周期。代价是手势响应延迟了一个周期,但对点击类操作完全无感,误触发率能明显降下来。还有一点,手势值的定义在不同触摸 IC 上不一定相同,换用同系列其他型号时务必回查数据手册,直接复用比较值容易翻车。

5. 移植 ST1633I 到 CH554 的排查记录:四个让我翻过车的坑

这一章是把前四章的代码真正烧到板子上之后,我最常撞见的四个问题。每一条都是现象、原因、解决三连写,按这个顺序排查,能省下大量盲调时间。

5.1 现象:上电后第一次读坐标全是 0,复位后恢复正常

原因:ST1633I 的电源稳定时间比 CH554 上电时间晚,主控执行初始化时触摸 IC 还没完成内部上电复位,写入控制寄存器的配置被丢弃。解决:初始化之前加 50ms 以上的上电等待,或者把触摸初始化放到主循环里重试 3 次,直到读状态寄存器返回非 0 数据。

// st1633i_init 增加重试,确保触摸 IC 真正就绪 void st1633i_init(void) { unsigned char retry = 3; while (retry--) { st1633i_write_reg(ST1633I_REG_CTRL, 0x01); // 软复位 delay_ms(10); st1633i_write_reg(ST1633I_REG_CTRL, 0x00); // 使能 delay_ms(10); if (st1633i_read_reg(ST1633I_REG_STAT) != 0xFF) break; // 读回非 FF 说明芯片已就绪 } }

5.2 现象:模拟 I2C 读一半 SCL 卡在低电平,程序死循环

原因:从设备在某个时钟周期拉低了 SCL 做时钟拉伸,或者总线被异常状态卡死,软件代码里没有任何超时保护,进入死等。解决:给 I2C 的 SCL 等待加超时计数,超过阈值就执行 9 次 SCL 脉冲加 STOP 的总线恢复序列,然后重新初始化。

// i2c_wait_scl 带超时,避免总线卡死 bit i2c_wait_scl(void) { unsigned int timeout = 1000; while (timeout--) { if (I2C_SCL) return 1; } return 0; // 超时后调用方执行总线恢复 }

5.3 现象:触点坐标跳动大,松开手指后还有残留点

原因:坐标解析时没有严格过滤状态字节,把已经抬起的手指状态当成有效触点;同时触摸屏电源纹波较大,导致坐标值在边缘区域抖动。解决:在st1633i_read_point里增加状态位判断,只有按下状态的触点才更新坐标;应用层再做一次 3 点滑动平均,把随机抖动抹平。

// 坐标滤波:三点滑动平均,抑制随机抖动 static unsigned int filter_x(unsigned int new_val) { static unsigned int buf[3] = {0, 0, 0}; static unsigned char idx = 0; buf[idx] = new_val; idx = (idx + 1) % 3; return (buf[0] + buf[1] + buf[2]) / 3; }

5.4 现象:CH554 枚举 USB 之后,触摸屏完全不响应

原因:USB 枚举过程中芯片发生了多次复位,触摸 IC 的 GPIO 配置被复位默认值覆盖,I2C 引脚失去输出功能;还有一种情况是 USB 中断频繁进入后,主循环里的模拟 I2C 时序被长时间打断,SCL 波形出现毛刺导致通信错乱。解决:在 USB 初始化完成后重新调用st1633i_init(),并把触摸读取放回主循环避开中断高峰期。我在某次实际项目里还遇到过更隐蔽的翻车现场——USB 的 D+ 引脚和 SCL 引脚挨得太近,PCB 走线串扰导致 I2C 时序被干扰,问题在重排布线后才彻底消失。

6. 验证驱动是否稳的两个技巧:事件日志法和坐标圆滑度检查

调试触摸驱动到最后一公里,靠的不是看波形就是看日志。我习惯在 CH554 的 UART 上挂一组调试信息,每次触摸事件上报时打印状态、坐标和手势值,格式类似T01: 120,340 G=0x10。跑一个手指滑动测试,观察日志里坐标是否连续、有没有跳点、有没有重复上报。这套验证法成本极低,能把玄学问题变成可定位的数据。另一步是坐标圆滑度检查:在屏幕上画一个已知直线路径,把采集到的坐标点拉成散点图,如果轨迹明显偏离直线,先查解析出的坐标高低字节有没有拼错,再查电源纹波,最后查机械结构上的贴合偏移。这里送一个我踩过的坑:焊盘虚接会导致 Y 轴坐标周期性跳变,用万用表量通断完全正常,但触摸时信号反射就出问题,最后是补焊解决。

做完这两步,整套 ST1633I 驱动在 CH554 上就能放心跑了。后边如果要加双击唤醒,就把 INT0 配置成下降沿唤醒并保留轮询兜底。希望这篇笔记能帮你少走一段弯路,把触摸驱动一次做稳。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询