本文是《从零到一:把一块 RK3568 开发板移植到 OpenHarmony 6.1 全记录》系列的第 8 篇。完整记录一次"从零写驱动"的过程:找协议 → 套模板 → 写探测 → 写读点 → 接入构建。
上篇讲了 HDF 框架和"改驱动"。这篇是从零写:这块板的触摸芯片是 TW3106,OpenHarmony 官方没有它的驱动,只有 Android 源码里有一份。
我的做法:从 Android 驱动里"翻译"出芯片协议,套进 HDF 驱动模板。
一、写驱动的第一步:不是写代码,是读协议
芯片不会说话,它和主芯片之间用固定格式的数据交流。这个格式就是"协议",写驱动之前必须先搞清三件事:
- 怎么确认芯片存在? —— 读芯片 ID 寄存器
- 怎么读坐标数据? —— 读数据寄存器的格式
- 读到的数据怎么解析? —— 每字节代表什么
答案都在 Android 源码的 TW3106 驱动里(原厂把协议写进了代码)。我做的就是把它"翻译"出来:
1. 芯片 ID:读寄存器 0x10FC
static int32_t ChipDetect(ChipDevice *device)
{
uint8_t reg[2] = { 0x10, 0xFC }; /* ID 寄存器地址 */
uint8_t buf[2] = {0};
InputI2cRead(i2cClient, reg, 2, buf, 2); /* I2C 读 2 字节 */
deviceId = (buf[0] << 8 | buf[1]) & 0xFF | 0x3100;
if (deviceId == 0x3106) { /* TW3106 的 ID */
return HDF_SUCCESS; /* 芯片在,我上岗 */
}
return HDF_FAILURE; /* 芯片不在,干净退出 */
}
这个"探测"函数是 HDF 触摸驱动的标配,也是下一篇"多芯片共存"的关键:芯片在板上,读 ID 就有应答;没焊,就没应答,驱动自动跳过。
2. 读点数据:两段式读取 + CRC 校验
TW3106 的数据读取是两段式:
第一段:读事件寄存器 0x0000,16 字节"头部"
头部结构: { event事件类型; address数据地址; length数据长度; ...; headCrc16 }
头部自带 CRC16 校验,算不对说明读错了
第二段:如果 event==5(触摸事件),按 head.address/head.length 读点数据
点数据: { scrInfo手指数; point[10]坐标数组; crc16 }
CRC16 校验是协议里的"防错码"——数据在总线上传输可能出错,接收方用同样的算法算一遍,对不上就丢弃重读。算法是标准的 CRC-16/CCITT(多项式 0x1021,初值 0xFFFF),Android 源码里就有实现,照搬即可:
static uint16_t Tw3106GetCrc16(const uint8_t *ptr, int32_t len)
{
uint8_t crch = 0xFF, crcl = 0xFF;
while (len--) {
index = crch ^ *ptr++;
crch = crcl ^ g_crcTabH[index];
crcl = g_crcTabL[index];
}
return (crch << 8) | crcl;
}
3. 解析坐标:4 字节一个点,位域打包
最有意思的是坐标格式——为了省空间,一个触摸点用 4 字节位域打包:
struct Tw3106Point {
uint32_t y : 13; /* y 坐标,13 位 */
uint32_t x : 13; /* x 坐标,13 位 */
uint32_t id : 5; /* 手指编号,5 位(区分多点触控的第几根手指) */
uint32_t stat : 1; /* 状态:按下/抬起 */
};
13+13+5+1 = 32 位,正好 4 字节,一点不浪费。解析时直接按位取:
frame->fingers[i].x = (int32_t)data.point[i].x;
frame->fingers[i].y = (int32_t)data.point[i].y;
frame->fingers[i].trackId = (int32_t)data.point[i].id;
frame->fingers[i].valid = true;
二、第二步:套 HDF 模板
HDF 触摸驱动的骨架是固定的,每个芯片驱动要实现 7 个函数:
static struct TouchChipOps g_tw3106ChipOps = {
.Init = ChipInit, /* 初始化 */
.Detect = ChipDetect, /* 探测:读芯片 ID */
.Resume = ChipResume, /* 休眠唤醒 */
.Suspend = ChipSuspend, /* 进入休眠 */
.DataHandle = ChipDataHandle,/* 读数据:中断来了读坐标 */
.UpdateFirmware = NULL, /* 固件升级(暂无) */
.SetAbility = SetAbility, /* 声明能力:支持哪些事件类型 */
};
其中 SetAbility 声明"我这个输入设备支持什么"——绝对坐标、多点触控:
static void SetAbility(ChipDevice *device)
{
inputDev->abilitySet.eventType[0] = SET_BIT(EV_SYN) | SET_BIT(EV_KEY) | SET_BIT(EV_ABS);
inputDev->abilitySet.absCode[1] = SET_BIT(ABS_MT_POSITION_X) |
SET_BIT(ABS_MT_POSITION_Y) | SET_BIT(ABS_MT_TRACKING_ID);
inputDev->attrSet.axisInfo[ABS_X].max = device->boardCfg->attr.resolutionX - 1;
...
}
新手提示:
SetAbility里的分辨率要和屏幕一致(1024x600),这来自 HCS 配置文件,和 C 代码分离。
三、第三步:接入构建与配置(4 处)
驱动写完了,还要在 4 个地方"登记",漏一处就不生效:
- Kconfig + Makefile(内核编译开关):
config DRIVERS_HDF_TP_CHESHIT10_TW3106
bool "Enable HDF tp TW3106 driver for CHISHI CHESHIT10 board"
- defconfig:
CONFIG_DRIVERS_HDF_TP_CHESHIT10_TW3106=y - HCS 配置(芯片地址/中断方式/上电时序):
chip1 :: touchChip {
match_attr = "chishi_tw3106_cheshit10";
chipName = "tw3106";
deviceAddr = 0x0C; /* I2C 地址 */
irqFlag = 1; /* 上升沿中断 */
...
}
- HDF 设备表(声明驱动模块):
device1 :: deviceNode {
moduleName = "HDF_TOUCH_TW3106";
deviceMatchAttr = "chishi_tw3106_cheshit10";
}
四、从零写驱动的完整方法论
复盘这次经历,方法论四步:
- 读协议,别急着写代码:协议在同类芯片的既有驱动里(Android 源码/内核源码/芯片手册),先翻译清楚"ID 怎么读、数据怎么来、字节怎么解"
- 套模板:HDF 驱动骨架固定,找官方类似芯片(如 gt911)的驱动当模板,只改芯片相关的三个函数(Detect/DataHandle/SetAbility)
- 四处登记:Kconfig、defconfig、HCS、设备表,缺一不可
- 真机验证预期:编译通过 ≠ 能用,CRC 校验、坐标映射这类细节必须真机验证——所以驱动里我保留了详细日志,方便上板后对照
写在最后
写驱动最难的从来不是 C 语言,而是理解芯片的通信协议。协议吃透了,剩下的都是体力活。
下一篇是本系列我最想聊的一篇:如何设计一个框架,让一块板子兼容 4 种触摸芯片和 3 种 4G 模组。
📚 本系列文章导航
《从零到一:把一块 RK3568 开发板移植到 OpenHarmony 6.1 全记录》
- Ubuntu 24.04 编译 OpenHarmony 6.1:我踩过的 11 个坑
- 我把一块跑 Android 11 的开发板,换成了 OpenHarmony
- 零基础拆解:一块 RK3568 开发板里有什么
- 按下电源后发生了什么?嵌入式开机全流程
- 给 Linux 内核写一份"硬件说明书":设备树 DTS 入门
- 设备树翻译实战:把 Android 4.19 的 DTS 搬到 OpenHarmony 6.6
- OpenHarmony HDF 驱动框架入门:屏幕驱动"参数化"改造
- 从零写一个 OpenHarmony 触摸驱动:TW3106 移植实录
- 一块板子如何兼容 4 种触摸芯片和 3 种 4G 模组?
- 移植全景复盘:从 0 到可烧写镜像,我做了什么