gpio-aw9523驱动详解:I2C扩展GPIO的寄存器配置与Linux实现

发布时间:2026/10/5 17:41:07
gpio-aw9523驱动详解:I2C扩展GPIO的寄存器配置与Linux实现 简介面向嵌入式与物联网开发者的AW9523 GPIO扩展芯片驱动源码基于I2C两线制串行协议解决微控制器GPIO引脚数量不足、需外扩输入输出接口的常见问题。压缩包内共1个c源码文件大小仅3KB但代码内容完整涵盖I2C控制器初始化、从机地址配置、寄存器映射与读写、GPIO方向/电平控制、中断触发及超时检测等关键模块并考虑了与操作系统设备模型对接的方式。已有244人学习该驱动既可直接移植到Linux或RTOS环境作为外设驱动参考也可结合AW9523数据手册逐行研读帮助嵌入式开发者深入理解I2C通信时序、寄存器位操作、中断服务程序编写和系统调用接口封装等底层技能。无论用于快速实现功能验证还是作为学习I2C驱动框架的教学样例都能有效缩短硬件驱动开发与调试的时间适合中初级嵌入式工程师及物联网设备开发者参考学习。1. gpio-aw9523 驱动源码包I2C 扩展 GPIO 的开箱即用参考MCU 引脚不够用是嵌入式开发里最现实的问题传感器占两根、按键占四根、LED 再占三根一个 48 引脚的芯片就捉襟见肘了。gpio-aw9523.zip 里那份 gpio-aw9523.c就是专为解决这类问题的——用两线 I2C 总线挂一颗 AW9523把 GPIO 扩展出 16 路驱动代码用 C/C 写成能直接看到 I2C 初始化、寄存器读写、方向配置、中断回调这些完整链路。不像某些芯片厂商的例程只给个读写函数敷衍了事这份源码把从总线探测到用户态 ioctl 的路径都串了起来适合三种人一是板子引脚下个月就要投产的底层工程师照它改个地址和时钟就能跑二是第一次接触 GPIO 扩展芯片、想搞懂寄存器怎么编排的新手三是做物联网网关、工控板这类需要大量数字 IO 的老手拿它当模板快速落地。下面从芯片底层开始拆再落到编译和排错。2. AW9523 与 I2C 扩展 GPIO寄存器编排和通信协议先吃透2.1 AW9523 寄存器地图0x40 地址背后藏着 16 路 IO 的编排逻辑AW9523 的 I2C 从机地址是 7 位的 0x40换算成 8 位读写地址就是 0x80 写、0x81 读。这颗芯片的一大特点是 16 路 GPIO 分成 P0 和 P1 两组每组 8 位。Gpio-aw9523.c 里所有的操作本质上都是对着下面这批寄存器做读写寄存器地址功能说明复位值REG_IN_P00x00P0 输入电平只读外部电平REG_IN_P10x01P1 输入电平只读外部电平REG_OUT_P00x02P0 输出电平写 1 拉高0x00REG_OUT_P10x03P1 输出电平写 1 拉高0x00REG_DIR_P00x04P0 方向1输入0输出0xFFREG_DIR_P10x05P1 方向1输入0输出0xFFREG_MODE_P00x06P0 模式1GPIO0LED 模式0xFFREG_MODE_P10x07P1 模式1GPIO0LED 模式0xFFREG_INT_P00x10P0 中断状态读后清零0x00REG_INT_P10x11P1 中断状态读后清零0x00REG_ID0x12芯片 ID读回 0x230x23方向寄存器默认全是 0xFF也就是上电后 16 个引脚全是高阻输入这点必须记住——很多人拿到新片子直接写输出电平发现没反应就是忘了先把方向位改成输出。模式寄存器决定了引脚是普通 GPIO 还是 LED 恒流模式LED 模式下芯片内部会限流可以直接推 LED 不用串电阻但此时读输入是无效的。2.2 I2C 通信与驱动框架选型Linux gpiolib 与裸机两条路I2C 本身是两线制SCL 和 SDA 加上拉电阻时钟频率 AW9523 最高支持 400kHz 快速模式。驱动代码里真正要关心的不是 I2C 协议本身而是挂在哪个框架下。Linux 下最规范的做法是写成 i2c client 驱动通过i2c_driver结构体注册再调用gpiochip_add_data注册成 gpiolib 的 gpiochip这样应用层就能用/sys/class/gpio或者libgpiod操作扩展出来的引脚不用自己写字符设备。裸机方案则是把 i2c 读写封装成platform_i2c_write_reg/platform_i2c_read_reg两个函数往 STM32、ESP32 上移植时把底层换成对应 HAL 库即可。gpio-aw9523.c 同时给这两种路径留了接口空位我一般建议先在 Linux 用户态用i2c-tools把寄存器读写摸清楚再决定要不要下沉到内核模块。如果是做量产固件裸机驱动反而更可控没有内核调度延迟中断响应更及时。3. gpio-aw9523.c 驱动代码拆解初始化、读写、中断三条主路径3.1 初始化与设备探测从 i2c_driver 注册到 gpiochip 挂载源码里最值得抄的一段是初始化流程。常见做法是先用i2c_master_send发一个字节的地址去探测设备读回 REG_ID 判断 0x23然后再往下走。下面是我从工程里抽出来的核心骨架static int aw9523_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct aw9523_chip *chip; int ret; u8 id_val; chip devm_kzalloc(client-dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; chip-client client; i2c_set_clientdata(client, chip); /* 读取芯片 ID确认通信链路正常 */ ret aw9523_read_reg(client, REG_ID, id_val); if (ret 0 || id_val ! AW9523_ID_EXPECT) { dev_err(client-dev, AW9523 ID read failed: 0x%02x\n, id_val); return -ENODEV; } /* 全部引脚默认设为 GPIO 模式避免 LED 模式带来的读输入异常 */ aw9523_write_reg(client, REG_MODE_P0, 0xFF); aw9523_write_reg(client, REG_MODE_P1, 0xFF); /* 方向寄存器初始化为全输入安全默认 */ aw9523_write_reg(client, REG_DIR_P0, 0xFF); aw9523_write_reg(client, REG_DIR_P1, 0xFF); /* 注册 gpiolib chip应用层可通过 /sys/class/gpio 访问 */ ret gpiochip_add_data(chip-gc, chip); if (ret) { dev_err(client-dev, gpiochip_add_data failed: %d\n, ret); return ret; } dev_info(client-dev, AW9523 GPIO expander probed OK\n); return 0; }读写寄存器函数aw9523_read_reg/aw9523_write_reg内部走的是i2c_transfer这点注意I2C 读寄存器是先写寄存器地址再读数据一次 complete 要发两个 message一个写地址、一个读数据。很多新手直接调i2c_master_recv去收收回来的是从设备当前输出缓冲的内容不是目标寄存器。参数上REG_MODE_P0写 0xFF 是确保 16 路都在 GPIO 模式如果你要用 LED 恒流驱动这里要改成 0x00。方向默认全输入是最稳妥的上电姿态避免意外拉低外部电路。3.2 GPIO 读写与中断回调电平翻转和边沿触发的实现细节GPIO 读写部分输出口要把方向寄存器对应位置 0再写输出寄存器。输入口则是设置方向为 1 后读输入寄存器。这里容易踩的坑是P0 和 P1 的方向寄存器是分开的一个引脚一个 bit但很多人图省事把 0x04 当总方向寄存器整字节写结果操作 P1 时把 P0 的方向全带偏了。中断设计上AW9523 的 INT 引脚是开漏输出多个扩展芯片可以共用一个中断线靠读 REG_INT_P0/P1 判断具体是哪个引脚触发。中断触发方式在芯片里是默认边沿敏感的电平变化产生中断但中断状态寄存器里的位需要软件写 0 清除或者读寄存器后自动清零——AW9523 是读后清零所以中断服务函数里必须先读再处理业务否则临界区之外的第二次触发直接丢。static irqreturn_t aw9523_irq_handler(int irq, void *data) { struct aw9523_chip *chip data; u8 int_p0, int_p1; int pin; /* 读中断状态寄存器同时完成自动清零 */ aw9523_read_reg(chip-client, REG_INT_P0, int_p0); aw9523_read_reg(chip-client, REG_INT_P1, int_p1); for (pin 0; pin 8; pin) { if (int_p0 BIT(pin)) handle_nested_irq(irq_find_mapping(chip-gc.irq.domain, pin)); } for (pin 0; pin 8; pin) { if (int_p1 BIT(pin)) handle_nested_irq(irq_find_mapping(chip-gc.irq.domain, pin 8)); } return IRQ_HANDLED; }这段逻辑说明中断处理前半段无条件读寄存器把状态拿下来后半段才做派发顺序反了会有两个问题——读晚了中断标志被新触发覆盖或者读早了新旧中断混在一起。参数上BIT(pin)的 pin 范围P0 对应 0~7P1 对应 8~15映射到 gpiolib 的全局编号时要注意偏移。3.3 系统集成两种方式内核 gpiolib 与裸机函数指针内核方式集成是用gpiochip_add_data把芯片挂进系统然后struct gpio_chip里实现direction_input、direction_output、get、set四个回调这套接口对应用层来说就是标准/sys/class/gpio/gpiochipXXX目录或者用gpiod_get在设备树里直接绑定。裸机方式则是把这些回调抽象成结构体函数指针你在board_setup里填入具体平台的 I2C 读写函数。参数上唯一的区别是内核方式下 I2C 传输可能睡眠所以如果芯片接了中断不能在线程化 irq 里直接调i2c_transfer否则会触发 BUG 报“ sleeping function called from invalid context ”。Gpio-aw9523.c 里如果看到它用了request_threaded_irq包一层那就是在规避这个约束。我通常建议量产固件直接裸机移植省掉内核框架那层间接调用代码更直白。4. 编译与移植把驱动跑起来的三种落地路径4.1 内核模块编译Makefile、设备树与模块加载把 gpio-aw9523.c 编成内核模块最省事的方式是写一个外置模块 Makefile配合内核源码树里的 KDIR 变量。下面是我常用的一套KDIR ? /lib/modules/$(shell uname -r)/build obj-m gpio-aw9523.o all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean install: $(MAKE) -C $(KDIR) M$(PWD) modules_install编译前要确认目标内核开启了 CONFIG_I2C 和 CONFIG_GPIOLIB这两个没开代码写再好也编不过。设备树里需要加一个 i2c 子节点指定地址和中断号i2c1 { aw952340 { compatible awinic,aw9523; reg 0x40; interrupt-parent gpio0; interrupts 23 IRQ_TYPE_EDGE_FALLING; gpio-controller; #gpio-cells 2; }; };reg 0x40是 7 位 I2C 地址#gpio-cells 2表示引用时第一个参数是引脚号、第二个是标志位。加载顺序是modprobe i2c-dev、insmod gpio-aw9523.ko然后看dmesg有没有 “AW9523 GPIO expander probed OK”。如果一直没 probe先查设备树 compatible 是否和驱动里of_match_table完全一致再查 I2C 总线编号是否对得上。4.2 裸机移植底层 I2C 读写函数的接口适配裸机平台移植最核心的是填充平台 I2C 钩子。Gpio-aw9523.c 里底层读写常用的封装是这样static int aw9523_i2c_write_reg(struct aw9523_chip *chip, u8 reg, u8 val) { u8 buf[2] { reg, val }; return chip-platform_write(chip-bus_handle, AW9523_ADDR, buf, 2); } static int aw9523_i2c_read_reg(struct aw9523_chip *chip, u8 reg, u8 *val) { int ret; ret chip-platform_write(chip-bus_handle, AW9523_ADDR, reg, 1); if (ret) return ret; return chip-platform_read(chip-bus_handle, AW9523_ADDR, val, 1); }参数说明AW9523_ADDR是 0x40 左移一位后的 8 位地址 0x80写寄存器时把寄存器地址和值一起发给从机读寄存器时先发寄存器地址再发一个重复起始信号读一个字节。芯片驱动不关心底层是 I2C 控制器还是软件模拟只要platform_write/platform_read返回非零代表失败即可。我在 STM32 上移植时直接把chip-platform_write换成 HAL 的HAL_I2C_Master_Transmit参数顺序改成(hi2c, AW9523_ADDR1, buf, 2, 100)三处替换就完事。ESP32 则换成i2c_master_write_to_device注意地址不要再次移位。4.3 用户态验证与 VSCode 调试不等内核版本直接摸寄存器如果不想编内核模块用户态也能验证这包代码的核心逻辑。用 i2c-tools 直接测i2cdetect -y 1 i2cget -y 1 0x40 0x12 i2cset -y 1 0x40 0x04 0x00 i2cset -y 1 0x40 0x02 0xFF第一行扫描总线上有哪些设备第二行读 REG_ID 应该看到 0x23第三行把 P0 方向全设为输出第四行把 P0 全部拉高。这三条命令跑通说明芯片链路没问题代码里的问题就被缩小到驱动框架适配上了。调试环境我习惯用 VSCode 配置 C/C 环境配好 launch.json 挂在 QEMU 或者 JTAG 上单步看i2c_transfer返回值和寄存器内容裸机则开一个printf串口重定向在aw9523_read_reg里打断点观察 buf 内容。这比反复烧固件看现象快得多。C 语言的优势在这个场景体现得很明显没有 C 构造析构的顺带开销一个结构体加函数指针就完成了设备抽象。5. 避坑与排查AW9523 驱动最常见的五个翻车点5.1 现象读 ID 永远返回 0xFF 或 0x00原因I2C 地址搞错了。很多人把数据手册里的 0x40 直接当 8 位地址传给i2c_master_send结果地址变成了 0x80从机不响应。AW9523 的 7 位地址是 0x40但在 Linux i2c-dev 里ioctl的 addr 参数要左移一位变成 0x80裸机 HAL 里则要看 API 是否已经帮你移位。解决统一换算规则——凡是代码里出现(AW9523_ADDR 1)就说明 API 接收的是未移位地址凡是直接写0x80说明 API 自己会处理。不要混用否则换平台必翻车。5.2 现象写输出电平后引脚电压纹丝不动原因方向寄存器没配置成输出。AW9523 复位后 REG_DIR_P0/P1 全是 0xFF所有引脚是高阻输入这时候写 REG_OUT_P0 的值会被芯片忽略。解决先写方向寄存器再写输出寄存器顺序必须保证。aw9523_write_reg(chip, REG_DIR_P0, 0x00); /* P0 全部输出 */ aw9523_write_reg(chip, REG_OUT_P0, 0x55); /* 再设置电平 */写输出的函数里最好内置一个方向检查避免调用顺序出错。我在给产线治具写测试固件时就吃过这个亏烧了三个板子才意识到是复位后默认输入的问题。5.3 现象中断触发一次后再也不触发原因中断状态寄存器没清零或者中断服务函数里读寄存器时机不对。AW9523 的中断状态是读后自动清零但如果你的 ISR 先处理业务逻辑再读状态寄存器芯片在此期间又产生了新中断状态寄存器里的值已经是新旧混合。解决ISR 入口第一件事就是把 REG_INT_P0/P1 读出来保存然后再做业务分发。5.4 现象引脚配置成输入后读到的电平始终是 0原因芯片处于 LED 模式。REG_MODE_P0/P1 默认是 0xFFGPIO 模式但如果初始化时图省事没写这个寄存器而芯片之前被别的代码配置成了 LED 模式写 0x00LED 模式下引脚的输入缓冲是关闭的。解决probe 函数里强制写一次 REG_MODE_P00xFF、REG_MODE_P10xFF。5.5 现象编译报错implicit declaration of function i2c_transfer原因内核模块缺少头文件包含或者 I2C 框架版本不匹配。解决文件头补齐#include linux/i2c.h如果用了gpiochip_add_data还要包含linux/gpio/driver.h。另外老内核里gpiochip_add_data可能不存在只有gpiochip_add需要按内核版本条件编译#if LINUX_VERSION_CODE KERNEL_VERSION(4, 5, 0) ret gpiochip_add(chip-gc); #else ret gpiochip_add_data(chip-gc, chip); #endif这个坑在把源码从新内核往老内核移植时必现提前加宏判断比事后打补丁省事。6. 进阶实战用 AW9523 同时做按键扫描和 LED 呼吸AW9523 一个很有价值的特性是 P0 和 P1 可以分别配置成 GPIO 和 LED 模式这意味着同一颗芯片既能读按键又能驱动 LED不需要额外扩展。做法是把 P0 设为 GPIO 输入接按键矩阵P1 设为 LED 模式接 8 个 LEDLED 模式下芯片内置恒流源每个灯的亮度由 REG_LED_P1 的 8 位 PWM 值控制范围 0~255不需要外部限流电阻。按键扫描用轮询还是中断取决于系统负载低功耗场景把 AW9523 的 INT 引脚接到 MCU 的 EXTI按下按键触发中断ISR 里读 REG_INT_P0 定位到具体引脚高实时性场景则建议 5ms 轮询一次 P0 输入寄存器加 20ms 消抖。/* LED 模式下设置 P1 第 0 脚亮度为 128半亮 */ aw9523_write_reg(chip, REG_MODE_P1, 0x00); /* P1 切到 LED 模式 */ aw9523_write_reg(chip, REG_LED_P1, 0x01); /* 选中第 0 脚 */ aw9523_write_reg(chip, REG_LED_PWM, 0x80); /* 亮度 128 / 255 */第一行把 P1 模式切到 0第三行写的是全局 PWM 寄存器它影响当前选中的那一脚所以第二行必须先选中目标引脚。做呼吸灯效果就是循环写 REG_LED_PWM 从 0 加到 255 再减回来间隔 10ms肉眼看到的就是平滑呼吸。按键部分我习惯把 P0 的四根线接一个 4x1 矩阵用 P0 的低四位读高四位留作输出轮询扫描列线这样 8 个引脚能扫出 16 个按键。整套逻辑用 gpio-aw9523.c 的寄存器读写函数就能完成不需要额外驱动框架。从那以后我每次拿到一块新的 IO 扩展芯片都会强制自己先用手头的 i2c-tools 把寄存器过一遍再写代码——芯片手册里的默认值和复位状态永远是第一个要确认的东西。这包源码的价值不在于能直接编译进你的内核而在于它把 I2C 扩展 GPIO 的注册、读写、中断、错误处理这几段必经之路都走了一遍你照着改比从零看数据手册省半天时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询