Chinaunix首页 | 论坛 | 博客
  • 博客访问: 374808
  • 博文数量: 116
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 894
  • 用 户 组: 普通用户
  • 注册时间: 2018-10-15 14:13
个人简介

搭建一个和linux开发者知识共享和学习的平台,QQ群 925939366,欢迎加入

文章分类

全部博文(116)

文章存档

2026年(2)

2025年(13)

2024年(15)

2023年(24)

2022年(27)

2019年(8)

2018年(27)

分类: 嵌入式

2025-11-19 16:58:54

在嵌入式Linux系统中,通用输入输出(General Purpose Input/Output,简称GPIO) 是{BANNED}最佳基础也是{BANNED}最佳常用的外设接口之一。它允许软件直接控制硬件引脚的电平状态或读取外部信号,实现与简单传感器、按钮、LED灯等外设的交互。Linux GPIO框架的设计经历了从各厂商独立实现到内核统一管理的演进过程,形成了一套既保证硬件兼容性又提供统一接口的完善子系统。

GPIO是集成在微处理器或微控制器上的可编程引脚,其功能可以通过软件灵活配置。从硬件视角看,GPIO引脚通常有以下特性:

  • 可配置性:每个GPIO引脚可以独立配置为输入或输出模式
  • 电平控制:输出模式下可以设置高电平或低电平,输入模式下可以读取当前电平状态
  • 中断支持:多数GPIO支持中断机制,用于检测引脚状态变化
  • 复用功能:许多GPIO引脚可以复用为其他外设功能(如I2C、SPI等)

在Linux操作系统中,由于安全性和稳定性考虑,应用程序不能直接访问硬件寄存器,必须通过内核提供的GPIO子系统来访问这些引脚。这种设计既保护了硬件资源不被误操作,又为不同硬件平台提供了统一的编程接口。


Linux GPIO框架的设计遵循了内核驱动的通用设计原则,其核心设计目标可以概括为:

  • 硬件抽象:通过定义标准的接口和数据结构,将具体的硬件操作封装在统一的API之下。这样,上层驱动开发者无需关心底层硬件的具体实现细节。
  • 资源管理:GPIO框架负责跟踪每个GPIO引脚的使用状态,防止多个驱动同时访问同一个GPIO而引发的冲突。
  • 接口统一:为内核其他子系统、内核驱动以及用户空间应用提供一致的GPIO访问方式。
  • 设备树集成:与现代Linux设备树机制紧密集成,实现硬件配置与驱动代码的分离。

GPIO核心层承上启下,负责管理所有GPIO控制器和GPIO引脚的状态,并提供API给上层使用。 GPIO控制器驱动是特定于SoC硬件平台的驱动,负责直接操作GPIO控制器的寄存器。 而Pinctrl子系统则负责管理引脚的复用功能[migration:5],确保一个物理引脚在多个可能的功能(如GPIO、UART、I2C等)中正确配置。

struct gpio_chip是描述GPIO控制器的核心数据结构,可以将其比喻为GPIO控制器的"身份证"和"能力清单"。每个GPIO控制器(SoC内部通常有多个GPIO控制器)都有一个对应的gpio_chip实例,用于向系统宣告自己的能力和操作方法。

关键字段说明:

  • label:控制器的名称,用于标识
  • direction_input/direction_output:设置GPIO方向的回调函数
  • get/set:读取/设置GPIO电平值的回调函数
  • ngpio:该控制器管理的GPIO数量
  • base:在全局GPIO编号空间中的起始编号

GPIO控制器驱动通过gpiochip_add_data()函数将gpio_chip注册到系统中,此后该控制器的GPIO引脚就可以被其他驱动或应用程序使用了。


在现代GPIO框架中,struct gpio_desc是代表单个GPIO引脚的主要数据结构,它采用面向对象的设计思想,将GPIO引脚抽象为独立的对象:

它通过chip指针关联到所属的控制器,所有的操作{BANNED}最佳终都通过该指针调用控制器特定的操作方法。这种设计实现了抽象与实现的分离,消费者驱动只需要操作gpio_desc对象,而不需要关心底层的硬件细节。

消费者驱动是从GPIO框架使用引脚的驱动,如LED驱动、按键驱动等。现代GPIO框架推荐使用基于描述符的API(gpiod_*系列函数)。

设备树中的消费者配置

在设备树中,消费者节点通过phandle引用GPIO控制器:
消费者驱动通过gpiod_get()系列函数获取GPIO描述符,这个过程完成了GPIO资源的映射和预留,防止多个驱动同时使用同一个GPIO引脚。

GPIO中断的实现依赖于irq_domain机制,它在GPIO子系统与通用中断子系统之间建立了桥梁。 当调用gpiod_to_irq()时,GPIO控制器驱动通过irq_domain将GPIO偏移量转换为全局中断号。

Pinctrl(引脚控制)子系统与GPIO子系统紧密协作,负责管理SoC引脚的多功能配置。 当一个引脚被配置为GPIO功能时,实际上经历了以下过程:

  1. 1. 引脚复用:Pinctrl子系统将引脚从默认功能切换到GPIO功能
  2. 2. 电气特性配置:设置引脚的上下拉电阻、驱动强度等参数
  3. 3. GPIO控制:GPIO子系统接管引脚,进行方向设置和电平控制
通过pinctrl-0属性引用了引脚配置节点,确保在GPIO驱动使用引脚前,硬件状态已正确配置。

Sysfs GPIO接口是历史{BANNED}最佳悠久、使用{BANNED}最佳广泛的用户空间GPIO控制方法,它通过虚拟文件系统暴露GPIO操作接口。

4.1.1 Sysfs GPIO目录结构

/sys/class/gpio/目录下,可以看到以下核心文件和目录:

/sys/class/gpio/
├── export          # 写-only,用于导出GPIO到用户空间
├── unexport        # 写-only,用于取消导出GPIO
├── gpiochip0/      # GPIO控制器0
├── gpiochip32/     # GPIO控制器32  
└── gpioN/          # 已导出的GPIO引脚
    ├── direction   # 读写,引脚方向:in/out
    ├── value       # 读写,引脚电平:0/1
    ├── edge        # 读写,中断触发:none/rising/falling/both
    └── active_low  # 读写,电平反转:0/1

对于输入和中断检测,操作也很直观:
# 导出并设置为输入
echo 12 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio12/direction

# 设置中断触发方式
echo rising > /sys/class/gpio/gpio12/edge

# 读取当前值
cat /sys/class/gpio/gpio12/value

使用libgpiod库

字符设备接口的底层操作相对复杂,推荐使用libgpiod库来简化开发:

#include 
#include 
#include 

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line *led_line, *button_line;
    int ret;
    
    // 打开GPIO控制器
    chip = gpiod_chip_open("/dev/gpiochip0");
    if (!chip) {
        perror("Open chip failed");
        return -1;
    }
    
    // 获取GPIO线
    led_line = gpiod_chip_get_line(chip, 5);    // GPIO1_5
    button_line = gpiod_chip_get_line(chip, 3); // GPIO1_3
    
    if (!led_line || !button_line) {
        perror("Get line failed");
        gpiod_chip_close(chip);
        return -1;
    }
    
    // 配置LED为输出,初始低电平
    ret = gpiod_line_request_output(led_line, "led-example", 0);
    if (ret < 0) {
        perror("Request LED as output failed");
        gpiod_chip_close(chip);
        return -1;
    }
    
    // 配置按钮为输入
    ret = gpiod_line_request_input(button_line, "button-example");
    if (ret < 0) {
        perror("Request button as input failed");
        gpiod_line_release(led_line);
        gpiod_chip_close(chip);
        return -1;
    }
    
    // 主循环:按钮按下时点亮LED
    while (1) {
        int value = gpiod_line_get_value(button_line);
        gpiod_line_set_value(led_line, value);
        usleep(10000); // 10ms
    }
    
    // 清理资源
    gpiod_line_release(led_line);
    gpiod_line_release(button_line);
    gpiod_chip_close(chip);
    return 0;
}

DebugFS是内核提供的调试文件系统,可以展示GPIO子系统的内部状态信息。

如果系统中还没有挂载debugfs,可以手动挂载:
mount -t debugfs none /sys/kernel/debug/

查看系统中所有GPIO控制器的状态:
cat /sys/kernel/debug/gpio

输出显示了:

  • GPIO控制器的名称和GPIO范围
  • 每个GPIO的当前状态:方向(in/out)、电平(hi/lo)、使用者标签
  • 中断状态:是否配置为中断

查看Pinctrl状态

对于引脚复用问题,可以查看pinctrl子系统的状态:

# 查看引脚复用状态
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins

# 查看引脚配置
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinconf-pins

# 查看GPIO范围映射
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/gpio-ranges

gpio-utils包

许多嵌入式Linux发行版提供了gpio-utils工具包:

# 检测系统中的GPIO控制器
gpiodetect

# 查看GPIO信息
gpioinfo

# 读取GPIO值
gpioget gpiochip0 1 2 3  # 读取gpiochip0的1,2,3引脚

# 设置GPIO值
gpioset gpiochip0 4=1 5=0  # 设置引脚4为高,引脚5为低

GPIO申请失败

当遇到gpio_request失败时,可能的原因包括:

  • GPIO已被占用:通过cat /sys/kernel/debug/gpio检查使用者
  • GPIO编号错误:确认硬件原理图和GPIO编号计算正确
  • 设备树配置错误:检查pinctrl配置和GPIO控制器定义

电平异常问题

当GPIO电平与预期不符时:

  • 检查上下拉电阻:硬件设计可能影响了电平
  • 确认active_low设置:某些驱动可能使用了反向逻辑
  • 测量实际电平:使用万用表或示波器确认硬件状态

中断不触发

GPIO中断无法触发时的诊断步骤:

  1. 1. 确认中断配置:检查/proc/interrupts中对应的中断计数
  2. 2. 检查触发方式:确认edge设置正确(rising/falling/both)
  3. 3. 验证硬件连接:确保物理连接可靠,信号无毛刺
GPIO翻转速度测试
#include 
#include 

void gpio_speed_test(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    struct timespec start, end;
    int i;
    
    chip = gpiod_chip_open("/dev/gpiochip0");
    line = gpiod_chip_get_line(chip, 5);
    gpiod_line_request_output(line, "speed-test", 0);
    
    clock_gettime(CLOCK_MONOTONIC, &start);
    
    for (i = 0; i < 1000; i++) {
        gpiod_line_set_value(line, 1);
        gpiod_line_set_value(line, 0);
    }
    
    clock_gettime(CLOCK_MONOTONIC, &end);
    
    double duration = (end.tv_sec - start.tv_sec) * 1e9 + 
                     (end.tv_nsec - start.tv_nsec);
    printf("平均翻转周期: %.2f ns\n", duration / 2000.0);
    
    gpiod_line_release(line);
    gpiod_chip_close(chip);
}

阅读(12) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~