简介:这是一份面向Linux内核驱动开发学习者的Intel ICH系列芯片GPIO驱动源码包,聚焦ICH6至ICH10及Series 5/6芯片组的通用输入输出控制。压缩包内仅有1个C语言源文件,整体仅4KB,短小精悍,非常适合研读单个驱动的完整实现。目前已有142人学习过该资源。通过阅读这份源码,可以清晰了解GPIO控制器结构、引脚方向与上下拉电阻的初始化配置,掌握gpio_get_value/gpio_set_value等操作接口以及中断处理、电源管理等系统级任务的具体写法。同时,代码展示了如何遵循Linux内核GPIO子系统标准接口(如gpio_request、gpio_direction_input等)进行驱动设计,并能帮助开发者理解设备树配置与驱动解析的配合方式,以及不同ICH版本间的兼容处理思路,是入门芯片驱动开发和调试的实用参考。
1. 为什么还在折腾gpio-ich:Intel ICH 平台 GPIO 的最后一公里
在服务器主板上调试 BMC 之外的 GPIO,最难受的不是看 datasheet,而是明明内核已经识别出 gpiochip,写进去的电平就是不动。很多人第一反应是怀疑自己代码写错,其实多半是驱动没找对对象。网上流传的 gpio-ich.rar 解压出来,其实就是内核里那个老资格的 driver/gpio/gpio-ich.c。它专门伺候 Intel ICH 系列南桥的 GPIO,从 ICH6 一路跟到 Atom 时代的某些 SoC,解决的是 x86 平台上最基础的一组信号:电源使能、复位、LED、开机检测。我下面不打算复述内核文档,而是按实际调板子的顺序,从寄存器映射讲到用户态控制,最后把 GPIO 编号偏移和 ACPI 占用这两个最容易翻车的点一次说清。
2. gpio-ich 驱动模型:ICH 芯片组 GPIO 寄存器映射与驱动加载逻辑
2.1 ICH GPIO 硬件基础:GPIOBASE 与 8 组寄存器
ICH 系列南桥的 GPIO 控制器不是一个独立 PCI 设备,而是挂在 LPC 桥里的功能块。驱动要做的事,是先拿到 GPIOBASE。这个基地址由系统固件在 PCI 配置空间的 0x58 偏移处写入,芯片内部所有 GPIO 寄存器都在这 64 字节窗口内偏移得到。gpio-ich 驱动在 probe 阶段会调用 pci_read_config_dword 去读这个值,读不到就直接返回 -ENODEV。
#define GPIOBASE_IO 0x58 #define GPIO_USE_SEL 0x00 #define GP_IO_SEL 0x04 #define GP_LVL 0x0c这段代码里的偏移意思很直白:GPIO_USE_SEL 决定哪几个引脚作为 GPIO 使用,GP_IO_SEL 决定方向,GP_LVL 是数据寄存器。ICH 芯片组的 GPIO 分成 8 组,每组理论上 32 个位,但实际可用的引脚数量随芯片变化。ICH10 之前很多组只有 8 位或 16 位,所以驱动内部维护一张表记录每组有效引脚数和偏移。这也就是为什么你在数据手册上看到的 GPIO33,和你在软件里看到的 line 33 往往不是一回事——中间隔了保留位和不可用位。
这里要提一句 GPIO 的 8 种工作模式。这个概念来自 STM32 这类 MCU,x86 南桥没有那么多花样,只有输入、输出、开漏和内部上拉/下拉可选。调 ICH 时如果拿 MCU 思维去套,很容易把方向寄存器理解反,明明已经设置成输出,读回来还是输入状态。
| 偏移 | 寄存器 | 作用 |
|---|---|---|
| 0x00 | GPIO_USE_SEL | 每个位对应一个引脚,1 表示作为 GPIO 使用 |
| 0x04 | GP_IO_SEL | 1 表示输入,0 表示输出 |
| 0x0C | GP_LVL | 输出电平或输入状态 |
2.2 驱动如何确定它管的是哪一代 ICH
gpio-ich 本质上是一个 platform driver,但绑定方式却依赖 PCI ID。内核里维护了一张 PCI 设备表,匹配 ICH6 到 ICH10、以及 Atom 平台常见的 NM10 等 LPC 控制器。以 ICH7 为例,它的 LPC 设备 ID 是 0x27bc,VENDOR 是 Intel 的 0x8086。
static const struct pci_device_id ich_pci_tbl[] = { { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICH6_GPIO) }, { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICH7_GPIO) }, { } };这段代码的作用是让驱动在 PCI 探测阶段发现 LPC 桥。找到设备之后,驱动读取 0x58 处的 GPIOBASE,然后调用 platform_device_register 注册一个 gpiochip。这个注册动作是异步的,所以 dmesg 里有时会看到 gpiochip 出现在驱动 probe 日志之后。加载失败时,错误信息也有规律:报告 "failed to get gpio base" 说明缓存基址读出来是 0,报告 "no PCI BDF" 说明 PCI 设备表就没匹配上。
2.3 与通用 GPIO 框架的接口:irq_chip 和 gpio_chip
gpio-ich 不只提供输入输出,还支持中断。驱动通过 gpiochip_add_data 注册,同时设置 irq_chip,使得每个 GPIO 都能作为中断源使用。ICH 的内部中断路由比较特殊,把 GPI 组的中断合并到 8259 的某个 IRQ 上,所以 dmesg 里经常看到类似 "gpio_ich: GPIO controller on ICH7: 64 pins" 的信息。
在实现层面,驱动注册了 .request 回调来检查引脚是否已经被 ACPI 占用,.get_direction 和 .direction_input/output 负责方向控制。与新的 gpiod API 对接后,用户态程序不需要关心寄存器地址,内核会通过 line 号到寄存器位的映射表完成转换。
提示:ICH 的 GPIO 引脚里有很大一部分是复用脚。比如 GPIO33 同时可能是 SMBus 的 ALERT#,使用前必须确认它没有被别的子系统占走,再往 GP_LVL 里写。
2.4 与同代 PCH 上其它 GPIO 驱动的边界
很多人会把 gpio-ich 和 gpio-pch、intel-pinctrl 混为一谈。gpio-pch 针对的是日本电产的 Egg Harbor 芯片组和一些老 PCH,而 intel-pinctrl 是新一代 Intel SoC 的 GPIO 驱动,使用完全不同的一套寄存器。ICH 平台的特点是没有 pin control 子系统,GPIO 的方向、复用、上下拉都挤在这 64 字节窗口里,因此 gpio-ich 必须自己处理所有细节。
判断一块板子该用哪个驱动,最直接方式是看 LPC 桥的设备 ID。0x27bc 属于 ICH7,0x27b8 是 ICH6,0x3b00 左右是 5 系列 PCH 的 LPC,后者往往已经在 intel-pinctrl 的覆盖范围内。调 Intel i210 这类网卡时,如果 PHY 的中断接到了某个 GPIO,先确认那个 GPIO 属于 LPC 桥还是 PCIe 端口,再决定是不是该找 gpio-ich 负责。
3. 编译与加载 gpio-ich:最小可复现的内核配置与模块参数
3.1 确认你的南桥是不是 ICH:lspci 与 ACPI 表
调板子第一件事不是敲 make menuconfig,而是确认南桥型号。ICH 系列广泛存在于 Atom N2xxx、老款凌动以及一批工控主板里。执行:
lspci -nn | grep -i LPC输出类似00:1f.0 ISA bridge [0601]: Intel Corporation NM10 Family LPC Controller [8086:27bc]。这个 27bc 就是 ICH7 的 LPC 设备 ID,gpio-ich 的匹配表里收录了它。如果显示的是 Intel 8 系列 C220 或 9 系列 PCH,那已经不是 ICH,gpio-ich 不会绑定,应该去看 intel-pinctrl 或 gpio-pch。
接着看 ACPI 是否已经声明了这个资源:
grep -i gpio /sys/firmware/acpi/tables/DSDT 2>/dev/null多数情况下输出为空,说明 GPIO 控制器没有被 ACPI 接管,可以放心使用。如果 DSDT 里有 _CRS 方法指定了 GPIO 资源,内核的 ACPI GPIO 子系统可能会先注册,gpio-ich 再 probe 就会失败。这种情况通常需要靠模块参数把冲突绕开,具体见 3.3。
3.2 内核配置:GPIO_ICH 依赖 GPIO 基础框架
内核里 GPIO 驱动编译选项在Device Drivers -> GPIO Support下面。需要打开 CONFIG_GPIO_ICH,同时确认 CONFIG_GPIOLIB 已经选上。检查当前内核是否包含:
zcat /proc/config.gz | grep GPIO_ICH输出CONFIG_GPIO_ICH=m说明可以按模块加载。没有 config.gz 时,看/lib/modules/$(uname -r)/modules.alias里有没有 gpio_ich 条目。下表是我常用的最小配置集:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| CONFIG_GPIOLIB | y | GPIO 基础框架,所有 GPIO 驱动都依赖它 |
| CONFIG_GPIO_ICH | m | ICH 平台 GPIO 驱动 |
| CONFIG_GPIO_SYSFS | y | 老 sysfs 接口,调试阶段建议打开 |
| CONFIG_GPIOD | y | 字符设备接口,libgpiod 需要 |
拿到内核源码后,编译单个驱动目录再拷贝到目标机,速度比整编内核快很多:
make M=drivers/gpio/ cp drivers/gpio/gpio-ich.ko /lib/modules/$(uname -r)/kernel/drivers/gpio/ depmod -a modprobe gpio-ichdepmod -a用于刷新模块依赖,否则 modprobe 可能找不到新拷贝的 .ko。如果模块提示版本 magic 不匹配,说明内核源码版本和目标机内核不是同一套,需要重新编译整个内核或调整配置。
3.3 模块参数:gpio_base、reserve 与 use_acpi
gpio-ich 的模块参数很少,但每个都直接关系到能否在奇怪的主板上跑起来。第一个是 gpio_base,手动指定 GPIOBASE 地址,跳过 PCI 配置读取。这主要用于 BIOS 没有正确设置 0x58 偏移的场合:
modprobe gpio-ich gpio_base=0x500第二个参数是 reserve。当驱动发现 GPIOBASE 已经由 ACPI 占用时,可以用位掩码把特定引脚强制保留。比如要保留 0 到 7 号引脚:
modprobe gpio-ich gpio_base=0x500 reserve=0xffreserve 是位掩码,置 1 的位会跳过 request 检查。要注意的是,这个参数不能把已经被 gpiolib 认领的 line 抢回来,它只能让驱动在初始化时忽略这部分引脚的 ACPI 状态。第三个参数 use_acpi 默认值是 1,表示优先使用 ACPI 的 _CRS 方法获取资源。如果固件里的 ACPI 表写的基地址是错的,反而应该置 0:
echo "options gpio-ich use_acpi=0 gpio_base=0x500" > /etc/modprobe.d/gpio-ich.conf加载后立刻看 dmesg 尾部:
dmesg | tail -20正常会看到gpio_ich: GPIO controller on ICH7: 64 pins和gpiochip_add: registered GPIOs 0..63 on device gpio_ich。如果只有前者没有后者,说明 gpiochip 注册失败,多半是 reserve 和 ACPI 冲突。
3.4 模块加载失败时按哪三步排查
第一步确认设备树是否命中。dmesg | grep gpio_ich没有输出,说明驱动的 PCI 表没匹配,检查 lspci 看到的设备 ID。第二步检查 gpio_base 是否有效。把gpio_base=0x0手动设置去加载,如果 dmesg 报 invalid,那基本可以排除驱动本身问题,回到主板 BIOS 设置。第三步看 ACPI 冲突。gpio_ich: failed to request gpio往往出现在子设备申请特定 line 的时候,而不是驱动加载阶段。
很多人在这一步会反复尝试不同的 reserve 值,其实更好的做法是先查/sys/kernel/debug/gpio,看具体哪个 line 被谁占用,再决定保留哪几位。盲试位掩码只适合快速验证,不适合作为长期配置写入 modprobe.d。
4. 通过 gpio-ich 操作 GPIO:libgpiod 与 sysfs 的新旧两套用户态接口
4.1 先看 gpiochip 是否注册:gpiodetect 与 /sys/class/gpio
装好 libgpiod 之后,第一步是列出系统中的所有 gpiochip:
gpiodetect输出里应当包含类似gpiochip0 [gpio_ich] (64 lines)的一行。没有 libgpiod 的老系统里,可以看 /sys/class/gpio。gpiochipN 目录下存在 base 和 ngpio 两个文件:
cat /sys/class/gpio/gpiochip0/base cat /sys/class/gpio/gpiochip0/ngpio这两个值一个代表起始编号,一个代表引脚数量。gpio-ich 注册的 base 通常是 0,如果系统里还有其它 gpiochip,base 会从上一个 chip 的结束位置继续排。ngpio 根据芯片组不同从 64 到 128 不等。
gpiodetect 只给出 chip 总览。要查看每个 line 是否可用、当前默认电平,用 gpioinfo:
gpioinfo gpiochip0输出里每行是一个 line,name列通常是空的,consumer列只有被 request 之后才有内容。调试之前先跑一遍这个命令,能直观看到哪些 line 已经被 ACPI 或其它驱动占住。
4.2 用 gpioset/gpioget 搭一套最小上电时序
有了 chip 之后,控制电平就很简单。比如要通过 GPIO24 给外设使能信号,先拉低再延时再拉高:
gpioset gpiochip0 24=0 sleep 0.1 gpioset gpiochip0 24=1这里 24 是 chip 内部的 line 号,不需要加上 base。gpioset 的-m wait选项可以指定保持时间,比如输出 1 后保持 10ms 自动释放:
gpioset -m wait -t 10ms gpiochip0 24=1模拟上电时序时,把多路 GPIO 按顺序放进去。先使能主电源,再释放复位:
gpioset gpiochip0 12=1 gpiochip0 13=0 gpioget gpiochip0 12这样两条命令就完成了一组基本的时序验证。之前调 x86 工控板的上电时序,我都是先用这套命令确认每一路电平,再去看示波器波形,比反复修改内核模块高效得多。读取时如果指定多个引脚,输出格式是gpiochip0 12=1;配合-b选项可以去掉 chip 名,只显示数值。
4.3 从 C 代码调用 libgpiod 做 GPIO 互锁
用户态脚本适合调试,但真正的逻辑控制建议写在 C 里,尤其是电源互锁这种不能出错的场景。libgpiod 提供 gpiod_chip_open_by_name 和 gpiod_chip_get_line 接口。下面是一段最小示例:
#include <gpiod.h> #include <stdio.h> #include <unistd.h> int main(void) { struct gpiod_chip *chip; struct gpiod_line *line_in, *line_out; int v; chip = gpiod_chip_open_by_name("gpiochip0"); if (!chip) { perror("open"); return 1; } line_in = gpiod_chip_get_line(chip, 5); line_out = gpiod_chip_get_line(chip, 6); gpiod_line_request_input(line_in, "interlock"); gpiod_line_request_output(line_out, "interlock", 0); while (1) { v = gpiod_line_get_value(line_in); gpiod_line_set_value(line_out, v == 1 ? 1 : 0); usleep(5000); } gpiod_chip_close(chip); return 0; }这段代码把 line 5 当输入、line 6 当输出,输入为高时输出置高,5ms 轮询一次。gpiod_line_request_input 的第二个参数是 consumer 名称,会显示在 /sys/kernel/debug/gpio 里,便于排查占用。编译命令是cc -o interlock interlock.c -lgpiod。注意 ICH 的 GPIO 读周期通常在微秒级,轮询间隔不小于 1ms 才比较稳定,太快的忙循环反而会把 CPU 占满。
5. 三个容易踩的坑:gpio-ich 的 GPIO 编号偏移、ACPI 占用与上电时序验证
5.1 为什么原理图上的 GPIO 编号和系统里差了一大截
ICH 数据手册里写的 GPIO33,在 gpiochip 里未必是 33。原因有两层。首先,驱动注册的 base 不一定是 0,如果系统里已经有一个 gpiochip,新的 chip base 会顺延到下一个连续编号,可能在 400 甚至 1000。其次,ICH 的每组引脚之间存在保留位,驱动内部把原始寄存器位映射到连续 line 号时会跳过这些位,所以数据手册的编号和系统 line 号完全对不上。
查映射最直接的方法是看驱动源码里的ichx_gpio描述表,或者通过 gpioinfo 查看每个 line 的 names。日常调试时,我会把原理图上用到的 GPIO 号先列出来,再用 gpioset 挨个输出高电平,拿万用表量引脚确认。这一步能省掉后面大半的排错时间。
5.2 被 ACPI 和 PCH 内部逻辑占用的 IO
ICH 里有一部分 GPIO 在默认状态下直接作为其它功能使用,比如 GPIO21 在 ICH7 上连接 LAN 的 PHY 中断,GPIO51 可能关联 PCIe 的 wake。ACPI 表如果声明了这些引脚,gpio-ich 在 request 时就会返回 busy。遇到这种情况,先看 debugfs:
cat /sys/kernel/debug/gpio这里会列出每个 line 的占用者,被 ACPI 占用的 line 显示 consumer 为 "ACPI"。确认引脚没有被固件实际使用后,可以通过 3.3 里的 reserve 参数把它从 ACPI 占用列表里摘掉。但要注意,有些引脚和 PCH 内部逻辑共享,即使驱动能操作,外部也读不到预期状态,比如和 TCO 定时器复用的引脚。排查时拿万用表量一下引脚电压,不响应就换一个引脚。
5.3 验证时序的测量点选择:别把 3.3V 电平当开漏
最后验证时序时,逻辑分析仪的测量点要选在负载端,而不是直接卡在 CPU 旁边的排阻上。ICH GPIO 输出往往是推挽,但很多板子外部又加了一级开漏转换。如果测点放在转换器前面,会把内部上拉的下沉脉冲当成平直高电平。
另外,ICH 的 GPIO 驱动能力很弱,手册上典型值只有几毫安。外接 LED 时串一个 1k 电阻,别直接灌到 MCU 引脚。用示波器看时序时,触发条件最好设成下降沿,因为上电时序里最重要的是使能信号撤掉的时刻。验证命令可以用 gpioget 持续采样确认稳态,但真正的时序宽度必须靠示波器,用户态轮询的抖动太大,不能作为验收依据。
本文还有配套的精品资源,点击获取