28、Linux设备模型与引脚控制:深入解析与实践

AI助手已提取文章相关产品:

Linux设备模型与引脚控制:深入解析与实践

一、Linux设备模型基础

在Linux系统中,设备模型是管理和组织硬件设备的重要机制,而sysfs则是与之紧密相关的虚拟文件系统,它为系统提供了全局视图,并通过kobject展示内核对象的层次结构。

1.1 添加和移除属性组

在文件系统中添加或移除属性组可以使用以下内核函数:

int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
void sysfs_remove_group(struct kobject * kobj, const struct attribute_group * grp);

为了更高效地添加多个属性,可以将它们嵌入到 struct attribute_group 中,示例如下:

static struct d_attr foo = {
    .attr.name="foo",
    .attr.mode = 0644,
    .value = 0,
};
static struct d_attr bar = {
    .attr.name="bar",
    .attr.mode = 0644,
    .value = 0,
};
static struct attribute * attrs [] = {
    &foo.attr,
    &bar.attr,
    NULL,
};
static struct attribute_group my_attr_group = {
    .attrs = attrs,
};
sysfs_create_group(mykobj, &my_attr_group);

这样只需一次调用就能添加多个属性,比为每个属性单独调用函数更加高效。

1.2 sysfs文件系统

sysfs是一个非持久的虚拟文件系统,每个kobject在sysfs中表现为一个目录,目录中的文件代表内核变量,这些文件被称为属性,可以进行读写操作。sysfs的顶级目录位于 /sys/ 下,常见的目录及其功能如下:
| 目录名 | 功能描述 |
| ---- | ---- |
| block | 包含系统中每个块设备的目录,每个目录下还有设备分区的子目录 |
| bus | 包含系统中注册的总线 |
| class | 设备类的相关信息 |
| dev | 以原始方式包含注册的设备节点,每个节点是指向 /sys/devices 中真实设备的符号链接 |
| devices | 展示系统中设备的拓扑结构 |
| firmware | 显示系统特定的低级子系统树,如ACPI、EFI、OF (DT) |
| fs | 列出系统中实际使用的文件系统 |
| hypervisor | 与虚拟机管理程序相关的信息 |
| kernel | 保存内核配置选项和状态信息 |
| module | 列出已加载的模块 |
| power | 与电源管理相关的信息 |

这些目录对应的kobject部分被导出为内核符号,例如 kernel_kobj 对应 /sys/kernel power_kobj 对应 /sys/power 等。

1.3 kobject目录的创建位置

当使用 kobject_add 将kobject目录添加到sysfs时,其位置取决于kobject的父节点:
- 如果父指针已设置,则作为父目录的子目录添加。
- 如果父指针为NULL,则作为 kset->kobj 的子目录添加。
- 如果父和kset字段都未设置,则映射到sysfs的根目录 /sys

同时,可以使用以下函数在现有对象(目录)上创建或移除符号链接:

int sysfs_create_link(struct kobject * kobj, struct kobject * target, char * name);
void sysfs_remove_link(struct kobject * kobj, char * name);

这样可以让一个对象在多个位置存在,例如设备可以同时出现在 /sys/bus /sys/devices 中。

1.4 sysfs文件和属性

默认的属性集通过kobject和kset的 ktype 字段以及 kobj_type default_attrs 字段提供。但有时需要为特定的ktype实例添加自定义属性,添加或移除新属性(或属性组)的低级函数如下:

int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
void sysfs_remove_group(struct kobject * kobj, const struct attribute_group * grp);
1.5 当前接口

sysfs中存在多种接口层,除了创建自己的ktype或kobject添加属性外,还可以使用现有的接口,包括设备、驱动、总线和类属性。

  • 设备属性 :除了设备结构中嵌入的kobject提供的默认属性外,可以创建自定义属性。使用 struct device_attribute 结构,通过 DEVICE_ATTR 宏进行声明:
struct device_attribute {
    struct attribute attr;
    ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
    ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
};
#define DEVICE_ATTR(_name, _mode, _show, _store) \
   struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

使用 device_create_file device_remove_file 函数添加或移除设备属性:

int device_create_file(struct device *dev, const struct device_attribute * attr);
void device_remove_file(struct device *dev, const struct device_attribute * attr);

示例代码如下:

static ssize_t foo_show(struct device *child, struct device_attribute *attr, char *buf) {
    return sprintf(buf, "%d\n", foo_value);
}
static ssize_t bar_show(struct device *child, struct device_attribute *attr, char *buf) {
    return sprintf(buf, "%d\n", bar_value);
}
static DEVICE_ATTR(foo, 0644, foo_show, NULL);
static DEVICE_ATTR(bar, 0644, bar_show, NULL);
if ( device_create_file(dev, &dev_attr_foo) != 0 ) {
    /* handle error */
}
if ( device_create_file(dev, &dev_attr_bar) != 0 ) {
    /* handle error*/
}
device_remove_file(wm->dev, &dev_attr_foo);
device_remove_file(wm->dev, &dev_attr_bar);
  • 总线属性 :依赖于 struct bus_attribute 结构,通过 BUS_ATTR 宏声明:
struct bus_attribute {
    struct attribute attr;
    ssize_t (*show)(struct bus_type *, char * buf);
    ssize_t (*store)(struct bus_type *, const char * buf, size_t count);
};
#define BUS_ATTR(_name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)

使用 bus_create_file bus_remove_file 函数创建或移除总线属性:

int bus_create_file(struct bus_type *, struct bus_attribute *);
void bus_remove_file(struct bus_type *, struct bus_attribute *);
  • 设备驱动属性 :使用 struct driver_attribute 结构,通过 DRIVER_ATTR 宏声明:
struct driver_attribute {
    struct attribute attr;
    ssize_t (*show)(struct device_driver *, char * buf);
    ssize_t (*store)(struct device_driver *, const char * buf, size_t count);
};
#define DRIVER_ATTR(_name, _mode, _show, _store) \
struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store)

使用 driver_create_file driver_remove_file 函数创建或移除设备驱动属性:

int driver_create_file(struct device_driver *, const struct driver_attribute *);
void driver_remove_file(struct device_driver *, const struct driver_attribute *);
  • 类属性 :基于 struct class_attribute 结构,通过 CLASS_ATTR 宏声明:
struct class_attribute {
    struct attribute attr;
    ssize_t (*show)(struct device_driver *, char * buf);
    ssize_t (*store)(struct device_driver *, const char * buf, size_t count);
};
#define CLASS_ATTR(_name, _mode, _show, _store) \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)

使用 class_create_file class_remove_file 函数创建或移除类属性:

int class_create_file(struct class *class, const struct class_attribute *attr);
void class_remove_file(struct class *class, const struct class_attribute *attr);

需要注意的是, device_create_file bus_create_file driver_create_file class_create_file 函数内部都会调用 sysfs_create_file

1.6 使sysfs属性文件可轮询

为了避免CPU浪费的轮询来检测sysfs属性数据的可用性,可以使用 poll select 系统调用等待属性内容的变化。内核提供了 sysfs_notify 函数实现通知机制:

void sysfs_notify(struct kobject *kobj, const char *dir, const char *attr);

用户空间代码检测数据变化的步骤如下:
1. 打开属性文件。
2. 对所有内容进行一次虚拟读取。
3. 调用 poll 请求 POLLERR|POLLPRI select/exceptfds 也可)。
4. 当 poll select 返回时,读取数据发生变化的文件内容。
5. 关闭文件并回到循环顶部。

示例代码如下:

static ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len) {
    struct d_attr *da = container_of(attr, struct d_attr, attr);
    sscanf(buf, "%d", &da->value);
    printk("sysfs_foo store %s = %d\n", a->attr.name, a->value);
    if (strcmp(a->attr.name, "foo") == 0) {
        foo.value = a->value;
        sysfs_notify(mykobj, NULL, "foo");
    } else if (strcmp(a->attr.name, "bar") == 0) {
        bar.value = a->value;
        sysfs_notify(mykobj, NULL, "bar");
    }
    return sizeof(int);
}
二、引脚控制和GPIO子系统

在嵌入式Linux开发中,引脚控制和GPIO子系统是非常重要的部分,大多数嵌入式Linux驱动和内核工程师都会使用GPIOs或进行引脚复用操作。

2.1 引脚复用和引脚控制器

SoC可以对引脚进行复用,一个引脚可能有多种功能。选择引脚工作模式的机制称为引脚复用,负责该机制的系统称为引脚控制器。

2.2 引脚控制子系统

引脚控制(pinctrl)子系统允许管理引脚复用。在设备树(DT)中,需要以特定方式复用引脚的设备必须声明所需的引脚控制配置。

pinctrl子系统提供的功能包括:
- 引脚复用:允许同一引脚用于不同目的,如UART TX引脚、GPIO线或HSI数据线。
- 引脚配置:设置引脚的电子属性,如上拉、下拉、驱动强度、去抖周期等。

2.3 设备树中的引脚控制节点

在设备树中,通常需要两个嵌套节点来描述引脚组的配置。第一个节点描述组的功能,第二个节点保存引脚配置。引脚配置以 <PIN_FUNCTION> <PIN_SETTING> 的形式给出,例如:

MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09  0x80000000

其中 MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 表示引脚功能, 0x80000000 表示引脚设置。

设备可以通过 pinctrl-<ID> pinctrl-name 两个属性分配引脚配置节点:
- pinctrl-<ID> :给出设备特定状态所需的引脚控制配置列表,每个列表项是指向引脚配置节点的phandle。
- pinctrl-name :为列表中的每个状态命名,状态ID 0通常命名为 default

示例设备树节点如下:

usdhc@0219c000 {
   non-removable;
   vmmc-supply = <&reg_3p3v>;
   status = "okay";
   pinctrl-names = "default";
   pinctrl-0 = <&pinctrl_usdhc4_1>;
};
gpio-keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_io_foo &pinctrl_io_bar>;
};
iomuxc@020e0000 {
    compatible = "fsl,imx6q-iomuxc";
    reg = <0x020e0000 0x4000>;
    usdhc4 {
        pinctrl_usdhc4_1: usdhc4grp-1 {
            fsl,pins = <
                MX6QDL_PAD_SD4_CMD__SD4_CMD    0x17059
                MX6QDL_PAD_SD4_CLK__SD4_CLK    0x10059
                MX6QDL_PAD_SD4_DAT0__SD4_DATA0 0x17059
                ...
            >;
        };
    };
    uart3 {
        pinctrl_uart3_1: uart3grp-1 {
            fsl,pins = <
                MX6QDL_PAD_EIM_D24__UART3_TX_DATA 0x1b0b1
                MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1
            >;
        };
    };
    gpios {
        pinctrl_io_foo: pinctrl_io_foo {
            fsl,pins = <
                MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09  0x1f059
                MX6QDL_PAD_DISP0_DAT13__GPIO5_IO07  0x1f059
            >;
        };
        pinctrl_io_bar: pinctrl_io_bar {
            fsl,pins = <
                MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05  0x1f059
                ...
            >;
        };
    };
};
2.4 引脚控制的使用示例

在驱动初始化时,通常需要获取引脚控制并应用其配置。示例代码如下:

struct pinctrl *p;
struct pinctrl_state *s;
int ret;
p = pinctrl_get(dev);
if (IS_ERR(p)) {
    return p;
}
s = pinctrl_lookup_state(p, name);
if (IS_ERR(s)) {
    devm_pinctrl_put(p);
    return ERR_PTR(PTR_ERR(s));
}
ret = pinctrl_select_state(p, s);
if (ret < 0) {
    devm_pinctrl_put(p);
    return ERR_PTR(ret);
}

pinctrl_select_state 函数内部会调用 pinmux_enable_setting ,进而对引脚控制节点中的每个引脚调用 pin_request 。可以使用 pinctrl_put 函数释放引脚控制,也可以使用资源管理版本的API,如 pinctrl_get_select 函数:

static struct pinctrl *pinctrl_get_select(struct device *dev, const char *name);

综上所述,Linux设备模型和引脚控制子系统为嵌入式Linux开发提供了强大的功能和灵活的配置方式,开发者可以根据实际需求进行设备属性的管理和引脚的复用配置。

三、GPIO子系统详细解析

在嵌入式Linux开发中,GPIO(General Purpose Input Output)子系统扮演着至关重要的角色,它允许开发者灵活地控制通用输入输出引脚,实现各种硬件交互功能。

3.1 GPIO接口类型

GPIO子系统提供了两种主要的接口类型,分别是传统的基于整数的接口和新型的基于描述符的接口API。

  • 传统整数接口 :这是早期使用的接口方式,通过整数编号来标识每个GPIO引脚。开发者可以使用一系列的函数,如 gpio_request gpio_free gpio_direction_input gpio_direction_output 等函数来请求、释放、设置引脚输入输出方向。例如:
// 请求GPIO引脚
if (gpio_request(gpio_num, "my_gpio") < 0) {
    printk(KERN_ERR "Failed to request GPIO %d\n", gpio_num);
    return -ENODEV;
}
// 设置为输出模式
gpio_direction_output(gpio_num, 0); 
  • 新型描述符接口 :为了提高代码的可读性和可维护性,Linux内核引入了基于描述符的接口。这种接口使用 struct gpio_desc 结构体来表示GPIO引脚,通过描述符来操作引脚。例如:
struct gpio_desc *desc;
// 获取GPIO描述符
desc = gpio_to_desc(gpio_num);
if (!desc) {
    printk(KERN_ERR "Failed to get GPIO descriptor for %d\n", gpio_num);
    return -ENODEV;
}
// 设置为输入模式
gpiod_direction_input(desc);
3.2 GPIO映射到IRQ

在很多应用场景中,需要对GPIO引脚的电平变化进行实时响应,这就涉及到将GPIO映射到中断请求(IRQ)。当GPIO引脚的电平发生变化时,会触发相应的中断处理程序。

以下是一个简单的示例,展示了如何将GPIO映射到IRQ并设置中断处理程序:

static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
    // 中断处理逻辑
    printk(KERN_INFO "GPIO interrupt triggered!\n");
    return IRQ_HANDLED;
}

// 在驱动初始化时设置
int ret;
// 获取GPIO对应的IRQ号
int irq = gpio_to_irq(gpio_num);
if (irq < 0) {
    printk(KERN_ERR "Failed to get IRQ for GPIO %d\n", gpio_num);
    return irq;
}
// 注册中断处理程序
ret = request_irq(irq, gpio_irq_handler, IRQF_TRIGGER_RISING, "my_gpio_irq", NULL);
if (ret) {
    printk(KERN_ERR "Failed to request IRQ %d\n", irq);
    return ret;
}
3.3 处理sysfs接口的GPIOs

sysfs为GPIO子系统提供了用户空间和内核空间之间的交互接口,开发者可以通过读写sysfs文件来控制GPIO引脚。

  • 导出GPIO引脚 :在用户空间中,首先需要将GPIO引脚导出到sysfs接口,才能对其进行操作。可以通过向 /sys/class/gpio/export 文件写入GPIO编号来实现:
echo <gpio_num> > /sys/class/gpio/export
  • 设置引脚方向 :导出后,可以通过 /sys/class/gpio/gpio<gpio_num>/direction 文件设置引脚的输入输出方向:
# 设置为输出模式
echo out > /sys/class/gpio/gpio<gpio_num>/direction
# 设置为输入模式
echo in > /sys/class/gpio/gpio<gpio_num>/direction
  • 读写引脚电平 :对于输出引脚,可以通过 /sys/class/gpio/gpio<gpio_num>/value 文件写入0或1来设置引脚的高低电平;对于输入引脚,可以读取该文件获取引脚的当前电平:
# 设置输出引脚为高电平
echo 1 > /sys/class/gpio/gpio<gpio_num>/value
# 读取输入引脚电平
cat /sys/class/gpio/gpio<gpio_num>/value
  • 移除导出的GPIO :当不再需要使用某个GPIO引脚时,可以通过向 /sys/class/gpio/unexport 文件写入GPIO编号来移除导出:
echo <gpio_num> > /sys/class/gpio/unexport
四、总结与展望

通过对Linux设备模型和引脚控制及GPIO子系统的深入探讨,我们可以看到这些机制为嵌入式Linux开发提供了强大而灵活的功能。

在Linux设备模型方面,sysfs文件系统通过kobject和kset等数据结构,清晰地展示了内核对象的层次结构,使得设备、驱动、总线和类等对象的管理变得更加直观和高效。开发者可以方便地通过sysfs接口对设备属性进行添加、移除和查询操作,同时还能利用轮询机制实时监测属性的变化。

引脚控制子系统则为引脚复用和配置提供了统一的框架,通过设备树可以清晰地描述引脚的功能和设置,在驱动初始化时能够方便地获取和应用引脚控制状态。而GPIO子系统则为开发者提供了丰富的接口,无论是传统的整数接口还是新型的描述符接口,都能满足不同场景下对通用输入输出引脚的控制需求。

展望未来,随着嵌入式系统的不断发展,Linux设备模型和相关子系统也将不断完善和优化。例如,可能会进一步简化接口的使用,提高系统的性能和稳定性;同时,对于新的硬件特性和应用场景,也会有相应的支持和扩展。开发者可以充分利用这些强大的功能,开发出更加高效、稳定和灵活的嵌入式Linux驱动程序,推动嵌入式系统的发展和创新。

以下是一个简单的mermaid流程图,展示了从设备树配置到引脚控制应用的整体流程:

graph LR
    A[设备树配置] --> B[驱动初始化]
    B --> C[获取引脚控制]
    C --> D[查找引脚状态]
    D --> E[应用引脚状态]
    E --> F[设备正常运行]

总之,深入理解和掌握Linux设备模型、引脚控制和GPIO子系统,对于嵌入式Linux开发者来说是一项必备的技能,能够帮助他们更好地应对各种硬件平台和应用需求。

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值