ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

VS Code + Jupyter Notebook:打造个人本地知识库

VS Code + Jupyter Notebook:打造个人本地知识库 简介面向嵌入式开发者和物联网设备调试人员该压缩包提供AW9523 GPIO扩展芯片的完整I2C驱动源码用于通过两线制I2C总线读写芯片内部寄存器从而在微控制器上扩展GPIO引脚数量。驱动采用C/C编写包内仅含1个主要C源文件整体约3KB结构紧凑适合直接阅读和移植到不同平台。已有244人学习下载适合有一定嵌入式基础、正在调试I2C外设的工程师参考。代码覆盖I2C控制器初始化、寄存器映射读写、GPIO方向与电平控制、中断配置及错误处理等关键环节并体现与Linux设备模型交互的思路开发者结合AW9523数据手册可快速定位驱动中的初始化流程、寄存器操作和中断响应逻辑也能从中学习I2C从设备驱动的通用编写框架为后续在RTOS或裸机环境中实现类似外设驱动提供参考。1. 提到 gpio-aw9523 这个包先搞清楚它到底解决什么问题提到 gpio-aw9523 这个包做嵌入式的第一反应通常是又一颗 I2C 接口的 GPIO 扩展芯片而且驱动得自己写。AW9523 的核心价值不在“IO 多”而在于它用一条两线 I2C 总线带起 16 路 IO其中 P0-P7 还能进 LED 模式做电流可调的呼吸灯效果省掉专门的 PWM 定时器。对 MCU 引脚不够、又要接按键加指示灯的板子这个方向很容易落地。这篇笔记按 Linux 用户态和 MCU 裸机两条路线把 AW9523 的寄存器模型、C/C 驱动结构、参数配置和容易翻车的点一次讲透。刚拿到芯片不知道怎么下手的新手能跟着走熟手可以直接看边界和坑。2. AW9523 寄存器模型与 I2C 时序先读明白地址和位域再动手2.1 从机地址不是固定的A0 引脚决定 0x58 还是 0x59AW9523 的 I2C 地址不是写死的芯片的 A0 引脚电平会改变 7 位从机地址。常见的默认配置是 A0 接地地址落在 0x58如果把 A0 拉高地址会变成 0x59。这个细节最容易在第一步卡住用 i2cdetect 扫描总线发现 0x58 和 0x59 都不出现第一反应是芯片坏了实际上只是原理图上 A0 接到了 VCC而你代码里写死了 0x58。我一般会在拿到板子后先查原理图确认 A0 接法再决定驱动里用哪个地址。如果原理图还没定建议默认按 0x58 写同时把地址定义做成宏这样后面改起来只动一行。还有一点要注意AW9523 的地址是 7 位地址在 Linux i2c-dev 里 ioctl 设置从机地址时直接传 0x58 这种 7 位值不要左移一位很多刚从单片机转过来的同事在这里会把地址写成 0xB0结果怎么都通信不上。地址确认后下一步就是过一遍寄存器映射。以下寄存器表是根据这颗芯片的公开手册整理的常见布局具体到某个批次还是以你手里的 datasheet 为准。2.2 核心寄存器映射输入、输出、方向、LED 模式与中断AW9523 的寄存器是 8 位宽偏移地址从 0x00 开始。写驱动前先把下面这张表背下来比反复查手册快得多。偏移名称位宽作用0x00Input16读引脚电平bit0 对应 P00x01Output16写引脚电平bit0 对应 P00x02Direction161 表示输入0 表示输出0x03Initial State16定义上电时输出脚的默认电平0x04LED Mode Select8只对 P0-P7 有效置 1 进入 LED 模式0x11/0x12LED Current8每 4 位一组控制 LED 电流档位0x13Global Control8输出模式、翻转速率等全局配置0x14/0x15Interrupt Enable/Status8引脚变化中断使能与状态这里最容易误解的是 0x00 到 0x03 都是 16 位寄存器但 AW9523 的 I2C 寄存器地址是 8 位偏移一次读写可以按两个字节处理也可以按两个地址分别读写高低字节。我习惯把它拆成两个 8 位访问逻辑更简单踩的坑更少。0x04 的 LED 模式选择只对低 8 位有效也就是 P0-P7。置 1 之后对应引脚从普通 GPIO 变成 LED 驱动通道输出电流由 0x11 和 0x12 里的 4 位档位决定不再受方向寄存器控制。这一点在后面的踩坑章节会专门讲很多翻车现场就是在这里发生的。中断部分同样容易误用。0x14 是中断使能0x15 是中断状态状态寄存器读完后必须写 1 清除否则 INT 引脚会一直拉低主机侧永远等不到下一次中断。2.3 时序与总线速率100kHz 和 400kHz 怎么选SLEW 控制在管什么I2C 总线速率对 AW9523 这种芯片来说不是越快越好。芯片本身支持标准模式 100kHz 和快速模式 400kHz但实际板级走线长度、上拉电阻值、总线上挂了多少设备都会影响高速下的通信可靠性。我之前在一款主板上总线上同时挂了 AW9523 和一颗 EEPROM走线超过 15 厘米跑 400kHz 时 EEPROM 读写正常AW9523 却偶发返回错误数据。原因在于 AW9523 的输入引脚有一个 slew rate 控制位在 0x13 全局控制寄存器里。slew rate 调低一点能抑制信号边沿的过冲对长走线和弱上拉有帮助。默认值不一定适合你的板子所以驱动初始化时最好显式写入这个寄存器而不是依赖上电默认值。如果总线距离很短、上拉电阻 4.7k 到 10k、设备少400kHz 没问题但凡有一点不确定先用 100kHz 把功能调通再尝试提速。读时序也是新手容易写错的地方。AW9523 读寄存器的标准流程是先发一个字节的寄存器地址然后重新产生起始条件再读数据。用 i2cdetect 时看到设备地址出现只说明芯片在线不代表读写逻辑正确。真正验证时序最好用逻辑分析仪抓一次写和一次读对比手册里的时序图这一步省掉的话后面排错全靠猜。3. Linux 用户态驱动用 C/C 写一个可用的 aw9523ctl 工具3.1 用户态还是内核态i2c-dev 和设备树各自的边界在 Linux 下控制 AW9523有两条路一条是在设备树里声明这颗芯片写内核态 I2C 驱动通过 sysfs 或 gpio 子系统暴露给应用层另一条是直接访问 /dev/i2c-N 节点在用户态用 C 程序读写寄存器。对大部分项目来说用户态方案就够了理由很实际AW9523 这类扩展芯片的实时性要求不高寄存器操作频率低用户态读写引入的调度延迟完全在可接受范围内。而且用户态代码调试方便编译快出错不会把整个内核带崩。内核态驱动的好处是能接入 Linux 的 gpiochip 框架让上层用标准 gpiod 接口操作适合需要和其它 gpio 设备统一管理的场景。但如果只是点亮几颗灯、读几个按键为它写一个 platform_driver 再配设备树投入产出比不划算。我通常的做法是原型阶段一律用户态把寄存器行为摸清楚后面如果确实要接入 gpio 子系统再把这层寄存器逻辑原封不动搬进内核驱动测试周期会短很多。用户态方案有一个前提/dev/i2c-N 节点要有访问权限。开发板上一般直接用 root 跑如果产品里是普通用户运行需要在 udev 规则里给 i2c 设备节点加权限组这个后面验证章节会提到。3.2 最小读写封装两个 C 函数覆盖全部寄存器访问用户态访问 AW9523本质就是打开 /dev/i2c-1用 ioctl 设置从机地址然后 write 和 read 寄存器。下面的代码是一个最小可用的封装头文件部分包含了 Linux i2c-dev 的用户态接口。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/i2c-dev.h #include stdint.h #define AW9523_ADDR 0x58 #define AW9523_REG_OUTPUT 0x01 #define AW9523_REG_DIR 0x02 static int aw_write_reg(int fd, uint8_t reg, uint8_t val) { uint8_t buf[2] { reg, val }; if (write(fd, buf, 2) ! 2) { perror(i2c write fail); return -1; } return 0; } static int aw_read_reg(int fd, uint8_t reg, uint8_t *val) { if (write(fd, reg, 1) ! 1) { perror(i2c write reg fail); return -1; } if (read(fd, val, 1) ! 1) { perror(i2c read fail); return -1; } return 0; }这段代码的逻辑很直接写寄存器时把寄存器地址和数据拼成一个两字节 buffer 一次发出读寄存器时先单独发地址再等芯片把数据送到总线上。注意读操作里 write 和 read 之间不需要重新设置从机地址因为 ioctl 设置的地址在 fd 上一直有效。参数说明打开设备节点时用 O_RDWR不能只读因为写寄存器是双向操作。ioctl 的第二个参数 I2C_SLAVE_FORCE 会强制把 fd 绑定到指定从机地址即使这个地址已经被内核驱动占用用它调试比 I2C_SLAVE 更不容易失败。但这个特性也意味着地址配错了不会立刻报错数据会发给一个不存在的设备所以地址宏一定要核对原理图。3.3 初始化与命令行动作把寄存器配置变成可调参数有了读写封装剩下的就是把寄存器配置翻译成命令行动作。下面这段代码实现了三个子命令init 完成基本初始化set 控制某一路输出read 读取所有引脚电平。这里刻意把寄存器地址和参数都做成宏方便换板子时调整。#define AW9523_REG_INIT_ST 0x03 #define AW9523_REG_LED_MODE 0x04 #define AW9523_REG_GCR 0x13 int main(int argc, char *argv[]) { int fd open(/dev/i2c-1, O_RDWR); if (fd 0) { perror(open /dev/i2c-1); return 1; } if (ioctl(fd, I2C_SLAVE_FORCE, AW9523_ADDR) 0) { perror(set slave addr); return 1; } if (argc 2) { printf(usage: aw9523ctl init|set|read\n); return 1; } if (strcmp(argv[1], init) 0) { // 先写初始电平再配方向P0-P7 全部输出高P8-P15 输入 aw_write_reg(fd, AW9523_REG_INIT_ST, 0xFF); aw_write_reg(fd, AW9523_REG_DIR, 0xFF); aw_write_reg(fd, AW9523_REG_LED_MODE, 0x00); aw_write_reg(fd, AW9523_REG_GCR, 0x00); printf(aw9523 init done\n); } else if (strcmp(argv[1], set) 0 argc 4) { int pin atoi(argv[2]); int val atoi(argv[3]); uint8_t cur; aw_read_reg(fd, AW9523_REG_OUTPUT, cur); if (val) cur | (1 pin); else cur ~(1 pin); aw_write_reg(fd, AW9523_REG_OUTPUT, cur); printf(pin P%d - %d\n, pin, val); } else if (strcmp(argv[1], read) 0) { uint8_t lo, hi; aw_read_reg(fd, 0x00, lo); aw_read_reg(fd, 0x00, hi); // 实际应读 0x00 的低字节和高字节这里按两段读取 printf(input: 0x%02X%02X\n, hi, lo); } close(fd); return 0; }这段代码里的 init 顺序是有讲究的先写 0x03 初始电平再写 0x02 方向寄存器。这样配置的引脚从上电开始就保持确定的电平不会在初始化瞬间乱跳。方向寄存器写成 0xFF表示高 8 位是输入低 8 位是输出P0-P7 输出高P8-P15 悬空输入。set 子命令里先读回当前输出寄存器再改对应位避免把其它引脚的状态冲掉。这个“读-改-写”操作是 GPIO 驱动里最常见的模式很多人第一次写会直接整字节覆盖输出寄存器导致其它引脚状态被错误复位。read 子命令目前没有区分高低字节实际工程里应该读取偏移 0x00 和 0x01或者一次读两个字这点在拿到芯片手册后按实际位宽修正即可。这个工具编译只需要一条命令gcc -o aw9523ctl aw9523ctl.c。它不依赖任何第三方库直接在开发板或 Linux 主机上编。如果板子上的 I2C 控制器编号不是 1把 open 里的 /dev/i2c-1 改成实际节点用 i2cdetect -l 可以列出所有总线。4. MCU 裸机驱动把 AW9523 封装成可复用的 C 库4.1 硬件抽象层设计为什么驱动不该直接绑死 HAL_I2C在 STM32 或其它 MCU 上写 AW9523 驱动最容易犯的错是把驱动代码直接调 HAL_I2C_Mem_Write 或寄存器操作写满整个文件。这样做在单板子上确实能跑一旦换平台或者从 HAL 库换成 LL 库全部代码要重写。C/C 驱动该有的样子是底层只留两个函数指针上层逻辑完全不知道 I2C 控制器是哪家的。我一般会先定义一个设备结构体把从机地址和两个 I2C 收发函数指针放进去。初始化时传入平台的读写函数驱动内部所有寄存器操作都通过这两个指针完成。这样 AW9523 驱动变成一个纯 C 的静态库换平台只是换底层两个函数的实现芯片逻辑一行不用改。这个思路对 I2C 从机都适用不只是 AW9523。结构体的定义并不复杂如下所示。核心是让上层拿着 aw9523_t 指针就能完成所有操作而不需要关心底层是阻塞还是中断传输。4.2 初始化和方向控制的核心代码先配初值再配方向下面的代码展示了设备结构体和初始化函数的完整实现。重点是初始化顺序和方向配置的细节。typedef struct { uint8_t addr; int (*i2c_write)(uint8_t addr, uint8_t reg, uint8_t val); int (*i2c_read)(uint8_t addr, uint8_t reg, uint8_t *val); } aw9523_t; static int aw9523_write_reg(aw9523_t *dev, uint8_t reg, uint8_t val) { return dev-i2c_write(dev-addr, reg, val); } static int aw9523_read_reg(aw9523_t *dev, uint8_t reg, uint8_t *val) { return dev-i2c_read(dev-addr, reg, val); } int aw9523_init(aw9523_t *dev) { if (aw9523_write_reg(dev, 0x03, 0xFF) 0) return -1; // 初始电平全高 if (aw9523_write_reg(dev, 0x02, 0x00) 0) return -1; // 全部输出 if (aw9523_write_reg(dev, 0x04, 0x00) 0) return -1; // 关闭 LED 模式 if (aw9523_write_reg(dev, 0x13, 0x00) 0) return -1; // 推挽输出默认翻转速率 return 0; } int aw9523_set_pin(aw9523_t *dev, uint8_t pin, uint8_t level) { uint8_t cur; if (pin 15) return -1; if (aw9523_read_reg(dev, 0x01, cur) 0) return -1; if (level) cur | (uint8_t)(1 pin); else cur (uint8_t)~(1 pin); return aw9523_write_reg(dev, 0x01, cur); }初始化时先写 0x03 再写 0x02这个顺序和 Linux 用户态版本一致。方向寄存器 0x02 的位定义是1 为输入0 为输出。示例里直接写成 0x00让 16 路全部输出。实际项目里通常会结合硬件设计把部分引脚配置成输入这时应该按位准备方向字节。set_pin 函数里的读-改-写是关键。AW9523 的输出寄存器是 16 位分布在两个地址上如果直接写一个字节另一个字节的引脚状态就丢了。示例代码为了可读性只处理了 8 位的情况真实使用时应该按高低两个字节分别处理或者封装一个 aw9523_set_pin_mask 函数一次性把需要变更的位和掩码传进来。这部分是驱动里最容易埋雷的地方后面避坑章节还会展开。4.3 LED 呼吸模式不用 PWM 也能做出来的亮度渐变AW9523 真正值钱的功能是 LED 模式。进入 LED 模式后P0-P7 不再受方向寄存器控制而是由 LED 电流寄存器决定输出电流。这意味着不需要 PWM 定时器只需要周期性更新电流档位就能做出呼吸灯效果。下面这段代码演示了如何在 P0 上做 0 到 15 档的渐变。#define AW9523_REG_LED_CUR 0x11 void aw9523_led_breath_step(aw9523_t *dev, uint8_t step) { uint8_t cur, target; if (step 15) step 15; // 读回当前电流配置只改低 4 位避免影响 P4-P7 aw9523_read_reg(dev, AW9523_REG_LED_CUR, cur); target (uint8_t)((cur 0xF0) | step); aw9523_write_reg(dev, AW9523_REG_LED_CUR, target); }调用方只需要在定时器中断或主循环里每隔 30 到 50 毫秒改变 step 的值从 0 递增到 15 再递减回 0就能看到呼吸效果。电流档位和实际毫安值的对应关系要查芯片手册里的曲线不同批次可能略有差异。0x11 寄存器每 4 位控制一组 LED 通道直接整体写入会误伤相邻引脚所以必须先读后写。这个方案替代 PWM 的意义在于MCU 可以继续睡在低功耗模式不需要一个硬件定时器专门跑 PWM。呼吸节奏完全由软件控制改起来灵活。但副作用是 LED 模式的引脚不能再读输入状态所以在设计按键和指示灯共用引脚时要提前想清楚。5. AW9523 驱动踩坑记录五个最容易翻车的点5.1 引脚编号和寄存器位对应错位输出写到别的脚上现象是控制 P5 输出高结果 P9 亮了或者完全没反应。原因几乎都是把引脚编号直接当成了寄存器位号写了 (1 pin)但 AW9523 的位映射是 bit0 对应 P0bit15 对应 P15中间的 P8-P15 在高字节P0-P7 在低字节。如果 pin 值大于 7位号已经超出低字节范围但驱动只写了低字节高字节压根没动。解决方法是先分清楚高低字节pin 小于 8 操作 0x01 的低字节pin 大于等于 8 操作 0x01 的高字节实际寄存器地址为 0x01 和 0x011按手册确认。或者定义一个 pin_to_reg(pin) 宏把端口号映射到正确的寄存器和掩码。我在驱动里统一用掩码不用裸的位移从根上杜绝错位。5.2 进了 LED 模式后P0-P7 读不到输入电平现象是把 0x04 寄存器的某位置 1 后原来配置为输入的 P0 变成了永远读到低电平。原因是 LED 模式会把引脚内部结构从输入缓冲切换成电流源驱动引脚不再具备数字输入功能。这不是芯片故障而是模式本身互斥。解决方法是把需要做输入检测的引脚全部放在 P8-P15或者在使用前把 0x04 对应位清零并重新配置方向寄存器。设计阶段就定好分工P0-P7 优先给 LED 和指示灯P8-P15 做按键输入和普通输出。如果布局已经固定就要在驱动里加一个状态变量记录哪些引脚处于 LED 模式避免上层误读。5.3 初值寄存器没先写上电瞬间输出乱跳现象是系统上电后接在 AW9523 输出脚上的继电器或 LED 闪了一下然后才进入正常状态。原因是方向寄存器配置成输出之后输出寄存器默认值是不确定的芯片在上电到初始化完成的窗口期内可能输出随机电平。初值寄存器 0x03 如果没被写入这个窗口期的行为不可控。解决方法是严格按照先写 0x03 初值、再写 0x02 方向的顺序初始化。如果硬件上不允许输出脚在上电瞬间有不确定电平还需要在 AW9523 的 INT 脚或使能电路上加一个 RC 延时让芯片输出脚在外围电路稳定后再生效。软件和硬件两手都要做这一点在量产产品上尤其重要。5.4 中断一直拉低读状态寄存器后没有清标志现象是 AW9523 的 INT 引脚触发一次后电平就一直保持低后续引脚变化不再产生中断。原因通常是中断状态寄存器 0x15 只读不清。读取状态寄存器之后必须把读到的值原样写回把中断标志清掉INT 引脚才会释放。很多人读完之后直接忽略返回值中断就永远卡死在第一次触发。解决方法是把清标志和读状态做成一个函数每次中断处理末尾强制写回。即使只有一个中断源也建议写回完整状态值而不是写 1避免不同位之间有粘连逻辑。用逻辑分析仪抓 INT 波形能直接看到清标志前后引脚电平的变化是最快的验证方式。5.5 400kHz 快速模式下偶发读写失败线长和上拉要背锅现象是 AW9523 在 400kHz 下工作运行一段时间后偶发写入失败或读到 0xFF降到 100kHz 后完全正常。原因不是芯片性能不行而是板级 I2C 总线的上拉电阻和走线寄生电容不匹配。上拉电阻太大信号上升沿变缓走线太长振铃和过冲叠加在 SDA/SCL 上毛刺会被芯片当成错误起始或停止位。解决方法是先看示波器抓 SCL 的上升沿如果边沿超过 300ns把上拉电阻调小比如从 10k 换成 4.7k 或者 2.2k。同时把 0x13 寄存器里的 slew rate 控制位调低让输出驱动器本身不那么激进。如果板子已经量产没法改电阻驱动里强制用 100kHz 跑AW9523 这类扩展芯片的实时性要求通常不高慢一点完全可以接受。6. 验证方法与调试环境让 AW9523 驱动跑得明明白白拿到一块带 AW9523 的板子先不要急着写驱动。我有几个固定的验证步骤能省掉大半排错时间。第一步用 i2cdetect 确认设备在线。下面的命令会扫描总线 1 上所有 I2C 地址输出列表里出现 58 或 59说明芯片通信链路没问题。如果扫不到先查 A0 引脚电平再量 SCL/SDA 波形最后检查 VCC 和 GND。i2cdetect -y 1 i2cdump -y 1 0x58第二步用 i2cset 手动改写方向寄存器和输出寄存器看对应引脚电平是否变化。这一步绕过所有代码直接证明硬件链路和寄存器行为符合预期。命令格式是把 0x58 地址处的 0x02 寄存器写成 0x00全部输出再把 0x01 写成测试值。i2cset -y 1 0x58 0x02 0x00 i2cset -y 1 0x58 0x01 0xFF第三步如果功能正常但代码跑起来不对重点检查工具链架构匹配。在 vscode 里配置 C/C 环境时遇到过扩展报“二进制文件不兼容或不匹配”十有八九是 tasks.json 里用的编译器架构和开发板不一致比如在 x86 主机上编出了 x86 的 ELF拿到 ARM 板上当然跑不了。用交叉编译工具链时编译、链接、调试器三者的目标架构必须一致不能主机 gcc 和 arm-gdb 混用。想快速验证算法逻辑在 termux 里装 gcc 也可以但操作硬件设备节点需要真实硬件环境这点绕不开。验证顺序建议是i2cdetect 确认在线i2cset 确认可控逻辑分析仪确认时序最后再编译驱动代码。我自己的习惯是每换一颗新芯片先花半小时把寄存器表抄成头文件再写读写函数最后才碰业务逻辑。这个顺序救过我很多次每次觉得“时序没问题直接写吧”结果都在省掉的那步上翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表