
简介这份资源是面向嵌入式Linux驱动开发者的PCA9698 GPIO扩展芯片驱动参考实现针对Linux 2.6.28内核编写解决I2C接口GPIO扩展芯片在系统中的通信与控制问题。PCA9698提供8路可独立配置为输入或输出的双向GPIO支持上拉/下拉电阻与中断功能常用于工业自动化、智能家居和物联网设备中需要灵活监测数字输入输出的场合。压缩包共2个文件包含1个c源文件和1个h头文件整体约3KB其中c文件实现设备注册、I2C读写、中断处理及用户空间控制接口h文件定义寄存器、错误码与API函数原型。目前已有172人学习下载。读者可据此理解I2C子系统设备注册流程、GPIO方向与中断触发配置方法并参考其寄存器读写与内存映射思路为其他内核版本或类似扩展芯片定制驱动适合具备一定内核基础的中高级开发者查漏补缺。1. 从一份 2.6.28 内核的 PCA9698 驱动包说起它到底能解决什么如果你手上有一块 PCA9698 扩展板或者正在维护一套跑 Linux 2.6.28 的老工控设备大概率会遇到同一个尴尬芯片手册能看懂寄存器也能背下来但真要在内核里把它跑起来缺的往往不是原理而是一份能直接编译、能对着改的驱动骨架。pca9698_gpio.rar里就三样东西——pca9698.c、pca9698.h以及一个打包目录看起来朴素但它把 I2C 设备注册、寄存器读写、GPIO 方向配置、中断上报这几件事串成了一条完整链路。PCA9698 是 NXP 那顆 40 路5 组 × 8 位I2C GPIO 扩展芯片常被拿来补主控 GPIO 不够用的场景工业控制柜、智能家居中控、老式物联网网关里都能见到它。这份驱动不是给你讲芯片原理的它是给你一个能落地的起点把芯片挂到 I2C 总线上让内核认得它再让用户空间能读能写能收中断。适合谁适合手里有 2.6.28 内核源码树、需要快速验证 PCA9698 硬件、又不想从零写 I2C 客户端的嵌入式工程师。下面我按“先看懂结构、再动手编译、最后避坑”的顺序拆一遍。2. 拆开 pca9698.c 与 pca9698.h驱动骨架与寄存器映射怎么对上2.1 头文件里藏着的寄存器地址与数据结构拿到pca9698.h第一件事不是看函数声明而是找寄存器偏移。PCA9698 的寄存器布局是分组的每组 8 位 GPIO 对应输入端口寄存器、输出端口寄存器、极性反转寄存器、配置寄存器各一个地址从 0x00 开始按组递增。头文件里通常会把这些偏移定义成宏比如PCA9698_REG_INPUT0、PCA9698_REG_OUTPUT0、PCA9698_REG_CONFIG0后面每组加 0x02 或 0x08 的步进。你要做的是拿芯片手册的 Table 对一遍确认驱动作者用的步进和手册一致——这一步偷懒后面读写全错位。除了寄存器宏头文件里还会定义struct pca9698_chip这类私有结构里面一般包含struct i2c_client *client、struct gpio_chip gpio、一个u8 reg_output[5]缓存、一个struct mutex lock。缓存数组的作用是避免每次改一个引脚都去读整组寄存器直接改内存再写回减少 I2C 事务。互斥锁则是防止并发写寄存器时把配置冲掉。这些字段不是摆设你在改驱动适配新板子时如果发现中断里也去写 I2C而没有拿锁大概率会翻车。/* pca9698.h 中常见的寄存器与结构定义片段 */ #define PCA9698_REG_INPUT0 0x00 #define PCA9698_REG_OUTPUT0 0x02 #define PCA9698_REG_POLARITY0 0x04 #define PCA9698_REG_CONFIG0 0x06 #define PCA9698_REG_GROUP_STEP 0x08 /* 每组寄存器间隔需对照手册确认 */ struct pca9698_chip { struct i2c_client *client; struct gpio_chip gpio; struct mutex lock; u8 reg_output[5]; /* 5 组输出缓存掉电前保持状态 */ u8 reg_config[5]; /* 方向配置缓存1输入 0输出 */ };上面这段代码的关键参数是PCA9698_REG_GROUP_STEP。不同批次的 PCA9698 手册里组间偏移可能是 0x08也可能是 0x02 的连续排列必须以你手上芯片的 datasheet 为准。reg_output和reg_config用 5 元素数组对应 5 组端口每组 8 位正好 40 路。如果你只用到其中两组也别把数组改小否则后面gpio_chip的ngpio对不上内核注册时会报引脚号越界。2.2 驱动初始化i2c_driver 注册与 gpio_chip 挂载pca9698.c的入口通常是一个module_i2c_driver或者老式的module_init/module_exit对。核心是probe函数I2C 匹配成功后先devm_kzalloc分配pca9698_chip把client存进去然后初始化互斥锁接着读一遍芯片的配置寄存器确认通信正常。确认通信这一步很多人省掉结果后面 GPIO 操作全失败回头查半天才发现是 I2C 地址设错或者上拉电阻没焊。通信确认之后填充gpio_chip结构label设成pca9698base可以设 -1 让内核动态分配ngpio设 40direction_input、direction_output、get、set这几个回调指向驱动里实现的函数。最后调gpiochip_add把 GPIO 控制器注册进内核。2.6.28 内核里gpiochip_add的签名和后来版本不同没有devm_版本所以出错时要手动gpiochip_remove这一点在写remove函数时要注意。static int pca9698_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct pca9698_chip *chip; int ret; chip devm_kzalloc(client-dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; chip-client client; mutex_init(chip-lock); i2c_set_clientdata(client, chip); /* 读一次配置寄存器确认 I2C 链路通 */ ret pca9698_read_reg(chip, PCA9698_REG_CONFIG0); if (ret 0) { dev_err(client-dev, PCA9698 not responding\n); return ret; } chip-gpio.label pca9698; chip-gpio.base -1; chip-gpio.ngpio 40; chip-gpio.direction_input pca9698_direction_input; chip-gpio.direction_output pca9698_direction_output; chip-gpio.get pca9698_get; chip-gpio.set pca9698_set; chip-gpio.can_sleep 1; /* I2C 操作会睡眠必须置位 */ ret gpiochip_add(chip-gpio); if (ret) { dev_err(client-dev, gpiochip_add failed: %d\n, ret); return ret; } return 0; }这段代码里最容易被忽略的是can_sleep 1。PCA9698 走 I2CI2C 读写会睡眠如果不置这个标志内核在中断上下文里调用gpio_set_value时会触发警告甚至死锁。另一个参数是base -1让内核自动分配 GPIO 编号避免和板上其他 GPIO 控制器冲突。如果你非要固定 base先cat /sys/class/gpio/gpiochip*/base看看哪些号段被占了。2.3 寄存器读写与 GPIO 方向切换的实现细节pca9698_read_reg和pca9698_write_reg是底层 I2C 封装。2.6.28 内核里用i2c_smbus_read_byte_data和i2c_smbus_write_byte_data就够了不需要自己拼i2c_msg。读的时候直接返回寄存器值写的时候先更新缓存再写芯片。注意 PCA9698 的输入端口寄存器是只读的你写它没意义输出端口寄存器可读可写读回来的是当前输出锁存值不是引脚实际电平。引脚实际电平要看输入端口寄存器这一点在调试时经常搞混。方向切换函数pca9698_direction_input和pca9698_direction_output的逻辑是根据传入的 offset 算出属于第几组、组内第几位然后读-改-写配置寄存器。配置寄存器里 1 表示输入0 表示输出。写输出时还要顺便把输出值设好避免切换瞬间产生毛刺。下面这段是方向设置的典型写法static int pca9698_direction_output(struct gpio_chip *gc, unsigned offset, int value) { struct pca9698_chip *chip gpiochip_get_data(gc); u8 group offset / 8; u8 bit offset % 8; u8 reg PCA9698_REG_CONFIG0 group * PCA9698_REG_GROUP_STEP; int ret; mutex_lock(chip-lock); /* 先设输出值再切方向减少毛刺 */ if (value) chip-reg_output[group] | BIT(bit); else chip-reg_output[group] ~BIT(bit); ret pca9698_write_reg(chip, PCA9698_REG_OUTPUT0 group * PCA9698_REG_GROUP_STEP, chip-reg_output[group]); if (ret) goto out; chip-reg_config[group] ~BIT(bit); /* 0 输出 */ ret pca9698_write_reg(chip, reg, chip-reg_config[group]); out: mutex_unlock(chip-lock); return ret; }参数offset是 0 到 39 的全局引脚号group offset / 8算出组号bit offset % 8算出组内位号。BIT(bit)是内核里的位操作宏。先写输出寄存器再写配置寄存器是为了让引脚在变成输出之前就处于目标电平否则先切输出再写值中间会有一个短暂的不确定状态驱动继电器或 LED 时可能看到闪一下。这个顺序在血泪经验里排前三。3. 把驱动塞进 2.6.28 内核编译、加载与设备树之前的板级配置3.1 老内核的编译方式Makefile 与 Kconfig 怎么写2.6.28 内核还没有设备树普及驱动要么编进内核要么编成模块。最省事的做法是编成.ko用insmod加载方便反复调试。你需要一个 Makefile指定内核源码路径和交叉编译器。假设你的内核源码在/home/eng/linux-2.6.28交叉编译器是arm-linux-gccMakefile 大概长这样# Makefile for pca9698 driver obj-m pca9698.o pca9698-objs : pca9698.o KERNELDIR ? /home/eng/linux-2.6.28 PWD : $(shell pwd) CROSS_COMPILE ? arm-linux- all: $(MAKE) -C $(KERNELDIR) M$(PWD) \ ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean这里obj-m表示编成模块pca9698-objs如果只有一个源文件其实可以省略但保留着方便以后拆多个文件。KERNELDIR必须指向已经make modules_prepare过的内核源码树否则编译时会报找不到autoconf.h或Module.symvers。交叉编译器前缀要和内核编译时用的一致混用会导致insmod时报invalid module format。编译命令就是make成功后当前目录会出现pca9698.ko。用modinfo pca9698.ko看一眼 vermagic确认和你的内核版本字符串完全匹配。2.6.28 的 vermagic 包含内核版本、SMP 标志、抢占模式等差一个字符都加载不了。3.2 板级 I2C 设备注册i2c_board_info 的填法模块编好了但insmod之后probe不一定被调用因为 I2C 总线还不知道有个 PCA9698 挂在那里。2.6.28 里需要在板级初始化代码里注册i2c_board_info。假设 PCA9698 挂在 I2C-1 上7 位地址是 0x20由 A0-A2 引脚决定板级代码里加static struct i2c_board_info pca9698_info { I2C_BOARD_INFO(pca9698, 0x20), }; static int __init board_i2c_init(void) { struct i2c_adapter *adap; struct i2c_client *client; adap i2c_get_adapter(1); if (!adap) return -ENODEV; client i2c_new_device(adap, pca9698_info); i2c_put_adapter(adap); if (!client) return -ENODEV; return 0; }I2C_BOARD_INFO的第一个参数pca9698必须和驱动里i2c_device_id表的name字段完全一致否则匹配不上。地址 0x20 是 7 位地址不是 8 位读写地址别把手册上的 0x40 写进来。i2c_get_adapter(1)里的 1 是总线编号用i2cdetect -l可以列出系统里有哪些 I2C 总线。注册完板级代码重新编译内核或板级模块启动后dmesg | grep pca9698应该能看到 probe 成功的日志。3.3 用户空间怎么用sysfs 与 gpiolib 的对接驱动注册成功后内核的 gpiolib 会在/sys/class/gpio/下生成gpiochipN目录N 是动态分配的编号。你可以echo N /sys/class/gpio/export导出某个引脚然后echo out direction、echo 1 value控制输出。读输入则是cat value。这套 sysfs 接口在 2.6.28 上已经可用但要注意 export 的编号是全局 GPIO 号不是 PCA9698 的 0-39。全局号 gpiochip的 base 偏移。用cat /sys/class/gpio/gpiochipN/base拿到 base再加偏移。# 假设 gpiochipN 的 base 是 192 echo 192 /sys/class/gpio/export # 导出 PCA9698 第 0 路 echo out /sys/class/gpio/gpio192/direction echo 1 /sys/class/gpio/gpio192/value # 输出高电平 cat /sys/class/gpio/gpio192/value # 读回输出锁存值 echo 193 /sys/class/gpio/export # 导出第 1 路做输入 echo in /sys/class/gpio/gpio193/direction cat /sys/class/gpio/gpio193/value # 读实际引脚电平这套操作不需要写 C 代码适合快速验证硬件。但 sysfs 方式每次 export/unexport 有开销高频读写不建议。量产代码里还是用ioctl或者直接在内核里调gpio_set_value。另外注意2.6.28 的 sysfs GPIO 接口对中断支持不完整如果你要用 PCA9698 的中断引脚得在驱动里自己实现irq_chip或者用request_threaded_irq注册中断处理再把事件通过sysfs_notify或字符设备上报。这部分原驱动如果没实现就需要你补。4. 避坑与排查PCA9698 驱动在 2.6.28 上最容易翻车的五个点4.1 现象insmod 成功但 /sys/class/gpio 下没有新 gpiochip原因通常是gpiochip_add失败但错误码被忽略或者ngpio设成了 0。2.6.28 的gpiochip_add在 base 冲突时返回-EBUSY如果驱动里没检查返回值模块照样加载但 gpiochip 没注册进去。解决方法是看dmesg里有没有gpiochip_add failed的打印没有就自己在 probe 里加dev_info输出 base 和 ngpio。另外确认chip-gpio.ngpio确实赋了 40别被结构体清零后忘了赋值。4.2 现象能导出 GPIO但写 value 没反应读回来永远是 0先查 I2C 通信是否真的成功。用i2cdetect -y 1看 0x20 地址是否出现UU或数字如果是--说明芯片没应答检查上拉电阻和供电。如果 I2C 通了再查方向配置PCA9698 的配置寄存器默认值可能是全 1输入你写 value 之前必须先把方向设成输出。sysfs 里direction写out会触发驱动的direction_output回调如果这个回调里没写配置寄存器引脚就还是输入写 value 自然无效。用i2cget -y 1 0x20 0x06读配置寄存器确认对应位是 0。4.3 现象中断引脚一直触发或者完全不触发PCA9698 的中断是开漏输出需要外部上拉电阻。如果中断一直触发先读输入端口寄存器看是哪一路在跳变可能是输入引脚悬空导致电平抖动。给悬空输入加上拉或下拉。如果完全不触发检查中断触发方式配置寄存器通常叫INT相关寄存器PCA9698 支持电平触发和边沿触发配置错了就不会产生中断。另外 2.6.28 的 I2C 中断处理里不能睡眠而 PCA9698 读寄存器会睡眠所以中断处理函数必须用request_threaded_irq把实际处理放到线程里否则内核会报scheduling while atomic。4.4 现象编译报错 “unknown field ‘can_sleep’ in struct gpio_chip”这是内核版本差异。can_sleep字段在 2.6.28 的某些小版本里可能叫cannot_sleep或者根本没有需要看include/linux/gpio.h里的实际定义。如果没有这个字段说明该版本 gpiolib 还不支持睡眠 GPIO你得自己在set/get回调里判断上下文或者干脆不用 gpiolib直接实现字符设备接口。别硬抄后来版本的代码2.6.28 的 gpiolib 接口和 3.x 差别很大。4.5 现象同时操作多个 GPIO 时偶发数据错乱这是并发写 I2C 导致的。PCA9698 的 I2C 事务不是原子的两个线程同时读-改-写同一个寄存器后写的会覆盖先写的。驱动里必须用互斥锁把整个读-改-写序列包起来不能只锁 I2C 传输函数。上面pca9698_direction_output里的mutex_lock就是干这个的。如果你在中断线程里也操作 GPIO同样要拿这把锁否则和用户空间的 sysfs 操作撞车。锁的粒度要覆盖“读缓存-改缓存-写芯片”三步少一步都可能出错。5. 进阶把 PCA9698 中断接进 Linux 输入子系统与一个验证习惯原驱动如果只实现了 GPIO 读写中断部分往往是个半成品。实际项目里PCA9698 的 40 路输入经常用来接按键、限位开关、传感器报警这些事件需要上报到用户空间。2.6.28 上最稳的做法是把中断线程里读到的输入状态通过input_report_key或input_report_switch上报到输入子系统这样用户空间用evtest或read /dev/input/eventX就能拿到事件不用自己写字符设备。具体做法在probe里调input_allocate_device分配一个输入设备设置evbit支持EV_KEY或EV_SWkeybit里把 40 路对应的键码都置位。中断线程里读输入端口寄存器和上一次的状态做异或找出变化的位对每一位调input_report_key最后input_sync。注意输入子系统的键码是全局的40 路要映射到不冲突的KEY_*宏或者用SW_*开关码。如果只是内部逻辑用也可以注册一个miscdevice用read阻塞等待中断事件实现更轻量。static irqreturn_t pca9698_irq_thread(int irq, void *data) { struct pca9698_chip *chip data; u8 current[5]; int i, bit; mutex_lock(chip-lock); for (i 0; i 5; i) { current[i] pca9698_read_reg(chip, PCA9698_REG_INPUT0 i * PCA9698_REG_GROUP_STEP); } mutex_unlock(chip-lock); for (i 0; i 5; i) { u8 changed current[i] ^ chip-last_input[i]; for (bit 0; bit 8; bit) { if (changed BIT(bit)) { int offset i * 8 bit; input_report_key(chip-input, keymap[offset], current[i] BIT(bit)); } } chip-last_input[i] current[i]; } input_sync(chip-input); return IRQ_HANDLED; }这段代码里keymap是一个 40 元素的数组把每一路映射到具体的键码。last_input缓存上一次的输入状态用来做边沿检测。input_sync必须调否则用户空间收不到完整事件包。中断线程里拿锁是必要的因为读输入寄存器也是 I2C 操作和用户空间的输出操作会竞争。验证方法上我习惯在加载驱动后先做一轮“全路自检”把 40 路全部配成输出依次输出高电平再用另一路输入读回确认没有相邻引脚短路或虚焊。然后配成输入用一根杜邦线依次短接到 GND看evtest里对应键码是否上报。这套自检跑一遍能提前发现 80% 的硬件问题比后面在应用层猜来猜去省时间。从那以后我每次拿到新的 PCA9698 板子都强制走一遍全路自检和 I2C 地址扫描确认i2cdetect里地址稳定出现再写业务代码。希望帮到你。本文还有配套的精品资源点击获取