Linux驱动开发(速记版)--GPIO子系统

第105章 GPIO 入门

105.1 GPIO 引脚分布

        RK3568 有 5 组 GPIO:GPIO0 到 GPIO4。

        每组 GPIO 又以 A0 到 A7,B0 到 B7,C0 到C7,D0 到 D7,作为区分的编号。

        所以 RK3568 上的 GPIO 是不是应该有 5*4*8=160 个呢?但是为什么在数据手册中有 152 个 GPIO 呢?

        实际上 RK3568 一 共 有 152 个 GPIO ,

        其中 GPIO0_D2 , GPIO0_D7 , GPIO2_C7 ,GPIO4_D3~GPIO4_D7 是没有的,

        所以是 152 个 GPIO。

105.2 GPIO 电气属性

        我们以 RK3568 为例,以具体 CPU 的数据手册为准。

        RK3568 上的 GPIO 可以设置为 3.3V,也可以设置为 1.8V。

        在实际编程时,高电平(3.3V 或 1.8V)用 1 表示,低电平用 0 表示。

那么我们如何确定 RK3568 的 GPIO 电平是 3.3V 还是 1.8V 呢?

        具体操作方法如下所示:

        1 首先打开 RK3568 的底板原理图,在底板原理图上查找使用的引脚,查找到引脚对应到核心板连接器上的网络标号。

        2 然后打开 RK3568 的核心板原理图,在核心板原理图上查找在上一步骤中找到的引脚网络标号,如下图所示,查找到引脚对应的 GPIO 和引脚所连接的电源域。

 3 然后查找对应的电源域,如下图所示,对应的电压值就是 GPIO 引脚的电压。

第106章 GPIO 控制和操作

        GPIO 软件编程方式有多种,

        可以写驱动程序调用 GPIO 函数操作 GPIO

        也可以直接通过操作寄存器的方式操作 GPIO

        还可以通过 sysfs 方式实现对 GPIO 的控制

106.1 使用命令通过 sysfs 文件系统控制 GPIO

106.1.1 内核配置

        使用 sysfs 方式控制 gpio,首先需要底层驱动的支持,需要在 make menuconfig 图形化配置.

106.1.2 GPIO 编号计算

        iTOP-RK3568 有 5 组 GPIO bank:GPIO0~GPIO4,每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分,常用以下公式计算引脚:

        GPIO pin 脚计算公式:pin = bank * 32 + number //bank 为组号,number 为小组编号

        GPIO 小组编号计算公式:number = group * 8 + X

106.1.3 使用 sysfs 控制接口控制 GPIO

        sysfs 控制接口/sys/class/gpio/export/sys/class/gpio/unexport

sysfs 的控制接口

        /sys/class/gpio/export 用于将 GPIO 控制从内核空间导出到用户空间

        /sys/class/gpio/unexport 用于取消 GPIO 控制从内核空间到用户空间的导出

        export 文件和 unexport文件,他们都是只写的。GpiochipX 代表 GPIO 控制器。

        export文件:用于将指定编号的 GPIO 引脚导出。

        在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。

        注意 export 文件是只写文件,不能读取,将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出,

        会发现在/sys/class/gpio 目录下生成了一个名为 gpio15 的文件夹(gpioX,X 表示对应的编 号),该文件夹就是导出来的 GPIO 引脚对应的文件夹,用于管理、控制该 GPIO 引脚。

        unexport文件:将导出的 GPIO 引脚删除。

        当使用完 GPIO 引脚之后,需要将导出的引脚删除,同样该文件也是只写文件、不可读,

        可以看到之前生成的 gpio15 文件夹就会消失!

        需要注意的是,并不是所有 GPIO 引脚都可以成功导出,如果对应的 GPIO 已经被导出或者在内核中被使用了,那便无法成功导出。

        出现上图报错的原因是该 GPIO 已经被其他 GPIO 使用,需要在内核中找到使用 GPIO 的驱动,并取消该驱动才可以正常使用 GPIO。

        比如在使用 GPIO15 时,需要取消 Linux 内核源码中LED 灯的配置,如下所示:

         再次使用以下命令导出 GPIO0_PB7 引脚,导出成功之后进入 gpio15 文件夹如下图所示:

        可以看到 gpio15 文件夹下分别有 active_low、device、direction、edge、power、subsystem、uevent、value 八个文件,

        需要关心的文件是 active_low、direction、edge 以及 value 这四个属性文件。

direction:配置 GPIO 引脚为输入或输出模式

        该文件可读、可写,读表示查看 GPIO 当前是输入还是输出模式,写表示将 GPIO 配置为输入或输出模式;读取或写入操作可取的值为"out"(输出模式)和"in"(输入模式)。

active_low:用于控制极性的属性文件。

        当 active_low 等于 0 时, 属性文件value 值若为 1 则引脚输出高电平,value 值若为 0 则引脚输出低电平。否则相反。

edge:控制中断的触发模式,该文件可读可写。

        在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式,

        非中断引脚:echo "none" > edge

        上升沿触发:echo "rising" > edge

        下降沿触发:echo "falling" > edge

        边沿触发: echo "both" > edge

value: 设置高低电平

        如果我们要把这个管脚设置成高电平,我们只需要给 value 设置成 1即可,反之,则设置成 0。

106.2 使用 C 程序通过 sysfs 文件系统控制 GPIO

        其实就是文件io去读写 export出来的sysfs文件系统下的属性文件。

int fd; // 文件描述符
int ret; // 返回值
char gpio_path[100]; // GPIO 路径
int len; // 字符串长度int fd; // 文件描述符
// 导出 GPIO 引脚
int gpio_export(char *argv)
{fd = open("/sys/class/gpio/export", O_WRONLY); // 打开 export 文件if (fd < 0){printf("open /sys/class/gpio/export error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取参数字符串的长度ret = write(fd, argv, len); // 将参数字符串写入文件,导出 GPIO 引脚if (ret < 0){printf("write /sys/class/gpio/export error \n"); // 写入文件失败return -2;}close(fd); // 关闭文件
}// 取消导出 GPIO 引脚
int gpio_unexport(char *argv)
{fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开 unexport 文件if (fd < 0){printf("open /sys/class/gpio/unexport error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取参数字符串的长度ret = write(fd, argv, len); // 将参数字符串写入文件,取消导出 GPIO 引脚if (ret < 0){printf("write /sys/class/gpio/unexport error \n"); // 写入文件失败return -2;}close(fd); // 关闭文件
}// 控制 GPIO 引脚的属性
int gpio_ctrl(char *arg, char *val)
{char file_path[100]; // 文件路径sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径,格式为“gpio_path/arg”fd = open(file_path, O_WRONLY); // 打开文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}len = strlen(val); // 获取参数字符串的长度ret = write(fd, val, len); // 将参数字符串写入文件,控制 GPIO 引脚的属性if (ret < 0){printf("write file_path error\n"); // 写入文件失败return -2;}close(fd); // 关闭文件
}int main(int argc, char *argv[]) // 主函数
{// 构建 GPIO 路径,格式为“/sys/class/gpio/gpio 引脚号”sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);if (access(gpio_path, F_OK)) // 检查 GPIO 路径是否存在{gpio_export(argv[1]); // 不存在则导出 GPIO 引脚}else{gpio_unexport(argv[1]); // 存在则取消导出 GPIO 引脚}gpio_ctrl("direction", "out");    // 配置 GPIO 为输出模式gpio_ctrl("value", argv[2]);     // 控制 GPIO 输出高低电平gpio_unexport(argv[1]);            // 最后取消导出 GPIO 引脚return 0;                         // 返回 0 表示程序正常退出
}

106.3 使用 C 程序通过 sysfs 文件系统使用 GPIO 中断

        中断引脚设置为输入,用杜邦线接到3.3v。由于触发模式为边沿模式,因此会触发中断。

        使用 poll监听输入引脚的 value文件。

    memset((void *)fds, 0, sizeof(fds)); // 清空 poll 结构体数组fds[0].fd = fd; // 设置 poll 结构体的文件描述符fds[0].events = POLLPRI; // 设置 poll 结构体的事件类型为 POLLPRI,表示有紧急数据可读read(fd, buf, 2); // 读取文件内容,清除中断事件ret = poll(fds, 1, -1); // 调用 poll 函数等待中断事件发生,阻塞直到事件发生
int fd; // 文件描述符
int ret; // 返回值
char gpio_path[100]; // GPIO 路径
int len; // 字符串长度
char file_path[100]; // 文件路径
char buf[2]; // 缓冲区
struct pollfd fds[1]; // poll 结构体数组
// 导出 GPIO 引脚,向/sys/class/gpio/export写入引脚号
int gpio_export(char *argv)
{fd = open("/sys/class/gpio/export", O_WRONLY); // 打开 export 文件if (fd < 0){printf("open /sys/class/gpio/export error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取字符串长度ret = write(fd, argv, len); // 写入引脚号到 export 文件if (ret < 0){printf("write /sys/class/gpio/export error \n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 取消导出 GPIO 引脚,向/sys/class/gpio/unexport写入引脚号
int gpio_unexport(char *argv)
{fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开 unexport 文件if (fd < 0){printf("open /sys/class/gpio/unexport error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取字符串长度ret = write(fd, argv, len); // 写入引脚号到 unexport 文件if (ret < 0){printf("write /sys/class/gpio/unexport error \n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 控制 GPIO 引脚的属性
int gpio_ctrl(char *arg, char *val)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建属性文件的路径fd = open(file_path, O_WRONLY); // 打开属性文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}len = strlen(val); // 获取字符串长度ret = write(fd, val, len); // 写入属性值到属性文件if (ret < 0){printf("write file_path error\n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 监听 GPIO 引脚的中断事件
int gpio_interrupt(char *arg)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径fd = open(file_path, O_WRONLY); // 打开文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}memset((void *)fds, 0, sizeof(fds)); // 清空 poll 结构体数组fds[0].fd = fd; // 设置 poll 结构体的文件描述符fds[0].events = POLLPRI; // 设置 poll 结构体的事件类型为 POLLPRI,表示有紧急数据可读read(fd, buf, 2); // 读取文件内容,清除中断事件ret = poll(fds, 1, -1); // 调用 poll 函数等待中断事件发生,阻塞直到事件发生if (ret <= 0){printf("poll error \n"); // 调用 poll 失败或超时return -1;}if (fds[0].revents & POLLPRI){lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头read(fd, buf, 2); // 读取文件内容,获取中断事件的值buf[1] = '\0';printf("value is %s\n", buf); // 输出中断事件的值}
}// 读取 GPIO 引脚的值
int gpio_read_value(char *arg)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径fd = open(file_path, O_WRONLY); // 打开文件,以只写模式打开是一个错误,应该使用只读模式if (fd < 0){printf("open file_path error\n"); // 打开文件失败return -1;}ret = read(fd, buf, 1); // 读取文件内容,获取引脚的值if (!strcmp(buf, "1")){printf("The value is high\n"); // 引脚值为高电平}else if (!strcmp(buf, "0")){printf("The value is low\n"); // 引脚值为低电平}return -1; // 这里应该返回读取到的引脚值(0 或 1),而不是返回固定的-1close(fd); // 关闭文件(这行代码无法执行到,应该放在 read 之前)
}int main(int argc, char *argv[]) // 主函数
{int value;sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径if (access(gpio_path, F_OK)) // 检查 GPIO 路径是否存在{gpio_export(argv[1]); // 不存在则导出 GPIO 引脚}else{gpio_unexport(argv[1]); // 存在则取消导出 GPIO 引脚}gpio_ctrl("direction", "in"); // 设置 GPIO 引脚为输入模式gpio_ctrl("edge", "both"); // 设置 GPIO 引脚的中断触发方式为上升沿和下降沿gpio_interrupt("value"); // 监听 GPIO 引脚的中断事件gpio_unexport(argv[1]); // 最后取消导出 GPIO 引脚return 0; // 返回 0 表示程序正常退出
}

106.4 使用 IO 命令操作寄存器控制 GPIO

106.4.1 IO 命令

        io命令是Linux下用于低级硬件交互的工具,能读写I/O端口值

io [选项] [地址] [操作] [数据]/*
选项:-b:按字节操作。-w:按字操作(默认)。-l:按双字操作。地址:I/O端口的十六进制值。操作:r:读取。w:写入。数据:要写入I/O端口的十六进制值。
*/
io -b -r 0x80                   //从 0x80读取一个字节
io -l -w 0x2000 0xDEADBEEF      //向 0x2000写入一个双字

106.4.2 LED 引脚寄存器查找

        我们查询到了控制 LED 灯的 GPIO 为 GPIO0_B7。

        在接下来的实验中需要对 GPIO 进行配置,

        一般情况下需要对 GPIO 的复用寄存器方向寄存器数据寄存器进行配置。

 

         所以        复用寄存器地址=基地址+偏移地址=0xFDC2000C.

        使用 io 命令查看此寄存器的地址:

io -r -4 0xFDC2000C //从地址读4字节数据

        寄存器值为 00000001,[14:12]位为 000,表示默认为GPIO功能。

        数据寄存器的偏移地址如下图,

        GPIO 有四组 GPIO,分别是 GPIOA,GPIOB,GPIOC,GPIOD。

        每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分。

        GPIO0B7 在 GPIO_SWPORT_DDR_L 上所以,方向寄存器的偏移地址为 0x0008。

        [31:16]位属性是 WO,也就是只可写入。

        这[31:16]位是写标志位,是低 16 位的写使能。

        如果低 16 位中某一位要设置输入输入输出,则对应高位写标志也应该设置为 1。

        [15:0] 是数据方向控制寄存器低位,

        如果要设置某个 GPIO 为输出,则对应位置 1,

        如果要设置某个 GPIO 为输入,则对应位置 0。

        那么 GPIO0 B7 ,我们要设置第 15 位为输入还是输出,那么对应[31:16]位写使能也要置 1。

        GPIO0 的基地址为 0xFDD60000。

        方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008。

        然后使用 IO 命令查看该寄存器的值,

         第 15 位默认为 1,设置 GPIO0_B7 为输出。

        接着查找数据寄存器地址,

         所以数据寄存器的地址为 基地址+偏移地址=0xFDD60000。

        如果要控制第 15 位为高电平(置 1),需要设置 31 位为 1,那么点亮灯,需要向数据寄存器写入 0x8000c040。

        直接使用 Io命令向寄存器写入值就可以实现点灯灭灯。

106.5 通过 mem 设备控制 GPIO

        在某些场合,直接使用IO命令可能受限,我们更倾向于采用更高级的抽象手段。

        此时,/dev/mem设备成为了一个选择,它允许我们操作物理内存以间接访问GPIO寄存器

        通过/dev/mem,我们能在用户空间中映射物理内存地址,进而读写GPIO寄存器

106.5.1 Linux 系统用户态访问内核态方式

在Linux系统中,用户态与内核态的通信方式多样:

        read/write/ioctl通过文件描述符读写或使用ioctl系统调用,用户态程序可与内核通信,控制或查询设备。

        sysfs:这是一个虚拟文件系统,用户态程序可通过读写sysfs中的文件与内核交互,如控制GPIO或获取系统信息。

        内存映射:用户空间内存区域映射到内核空间,允许用户态直接修改内存以与内核通信,适用于高效数据传输和共享。

        Netlink:用于用户态与内核间的双向通信,通过Netlink套接字发送请求、接收事件通知,适用于复杂交互场景。

106.5.2 /dev/mem 设备

        /dev/mem是Linux中的虚拟设备,常与mmap结合将物理内存映射至用户空间,实现用户态对内核态的直接访问。

        此功能需root权限,且某些系统需特别启用。在Linux内核源码中,需配置“/dev/mem虚拟设备支持”以启用此功能。IO命令即基于/dev/mem实现,若内核未配置支持,则无法使用IO命令。

106.5.3 /dev/mem 设备的使用方法

使用/dev/mem需root权限,操作需谨慎。基本步骤如下:

1、打开/dev/mem:

        使用open函数获取文件描述符

        指定读写权限(O_RDWR)和非阻塞模式(O_NDELAY)。

int fd = open("/dev/mem", O_RDWR | O_NDELAY);

2、建立内存映射:

        使用mmap将物理地址映射到用户空间。指定映射大小、读写权限、映射类型及文件描述符。

char *mmap_addr; // 声明一个指向字符的指针,用于接收映射地址  mmap_addr = mmap(  NULL,        // 将 NULL 传递给 addr 参数,表示让系统自动选择映射的起始地址  MMAP_SIZE,   // 指定映射区域的大小(以字节为单位)  PROT_READ | PROT_WRITE, // 指定映射区域的保护属性,这里设置为可读可写  MAP_SHARED,  // 指定映射类型,MAP_SHARED 表示对映射区域的修改会反映到底层的物理内存中,且修改对所有进程可见(如果有多个进程映射了同一物理内存区域)  fd,          // 打开 /dev/mem 获得的文件描述符  PHYS_ADDR    // 要映射的物理内存地址的起始点  
);

3、访问映射地址:

        通过指针操作读写映射的内存区域,即访问寄存器

*(int *)mmap_addr = value; // 写  
int read_value = *(int *)mmap_addr; // 读

106.5.4 mem设备操作控制 led

#define GPIO_REG_BASE 0xFDD60000
#define GPIO_SWPORT_DDR_L_OFFSET 0x0008
#define GPIO_SWPORT_DR_L_OFFSET 0x0000
#define SIZE_MAP 0x1000
// 打开 LED 灯
void LED_ON(unsigned char *base)
{// 设置 LED 灯的方向为输出*(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;// 将 LED 灯打开*(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80008040;
}// 关闭 LED 灯
void LED_OFF(unsigned char *base)
{// 设置 LED 灯的方向为输出*(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;// 将 LED 灯关闭*(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80000040;
}int main(int argc, char *argv[])
{int fd;unsigned char *map_base;// 打开/dev/mem 设备fd = open("/dev/mem", O_RDWR);if (fd < 0){printf("open /dev/mem error \n");return -1;}// 将物理地址映射到用户空间(重点!!!)map_base = (unsigned char *)mmap(NULL, SIZE_MAP, PROT_READ | PROT_WRITE, MAP_SHARED,     fd, GPIO_REG_BASE);if (map_base == MAP_FAILED){printf("map_base error \n");return -2;}while (1){// 打开 LED 灯LED_ON(map_base);// 等待 1 秒usleep(1000000);// 关闭 LED 灯LED_OFF(map_base);// 等待 1 秒usleep(1000000);}// 解除映射munmap(map_base, SIZE_MAP);// 关闭文件描述符close(fd);return 0; // 返回 0 表示程序正常退出
}

        运行后 led闪烁。

第107章 GPIO 的调试方法

        GPIO 的调试方法除了使用 IO 命令去查看寄存器,还可以使用其他方法进行 GPIO 的调试。

107.1 方法一 debugfs文件系统

        debugfs 是 Linux 内核提供的一个调试文件系统,可以用于查看和调试内核中的各种信息,包括 GPIO 的使用情况。

        通过挂载 debugfs 文件系统,并查看/sys/kernel/debug/目录下的相关文件,可以获取 GPIO 的状态,配置和其他调试信息。

        如果上图目录/sys/kernel/debug 目录下没有文件,需要在 Linux 内核源码的menuconfig配置 debugfs。

107.2 方法二

        当你进入/sys/kernel/debug/pinctrl 目录时,你可以获取有关 GPIO 控制器的调试信息。

        在该目录下,通常会有以下文件和目录:

        1. /sys/kernel/debug/pinctrl/*/pinmux-pins:

                这些文件列出了每个 GPIO 引脚的引脚复用配置。

        你可以查看每个引脚的功能模式、引脚复用选择以及其他相关的配置信息。

        2. /sys/kernel/debug/pinctrl/*/pins:

                这些文件列出了 GPIO 的引脚编号,可以查看 GPIO 编号。

        3. /sys/kernel/debug/pinctrl/*/gpio-ranges:这些文件列出了每个 GPIO 控制器支持的 GPIO 范围。你可以查看GPIO编号范围和对应的控制器名称 。

        4. /sys/kernel/debug/pinctrl/*/pinmux-functions:

                这些文件列出了每个功能模式的名称以及与之关联的 GPIO 引脚。你可以查看各个功能模式的名称和对应的引脚列表。

        5. /sys/kernel/debug/pinctrl/*/pingroups:

                该路径提供有关用于配置和控制系统上的 GPIO 引脚的引脚组的信息。

        6. /sys/kernel/debug/pinctrl/*/pinconf-pins:

                这些文件包含了 GPIO 引脚的配置信息,如输入/输出模式、上拉/下拉设置等。你可以查看和修改 GPIO 的电气属性,以便进行 GPIO 的调试和配置。

第108章 GPIO 子系统 API 函数

        在Linux内核中,GPIO(通用输入/输出)子系统分为新旧两个版本。旧版基于整数实现,新版则基于描述符

        为保持兼容性,旧版接口仍被支持,但新版接口(以"gpiod_"为前缀)正逐渐完善并终将取代旧版。

        新版GPIO子系统需与设备树结合使用,提供了更高的灵活性和可扩展性。

关键结构体包括:

        struct gpio_desc描述GPIO,含标志位、名称等。

struct gpio_desc {    struct gpio_device gdev;  // GPIO设备相关的结构体,包含设备信息  unsigned long flags;     // 标志位,用于控制GPIO的行为或状态  const char *label;       // GPIO的标签,用于标识或分类  const char *name;        // GPIO的名称,通常用于唯一标识  
};

        struct gpio_device描述GPIO设备,关联GPIO芯片等。

struct gpio_device {    int id;            // GPIO设备的唯一标识符  struct device *dev; // 指向设备结构体的指针,包含设备的一般信息  struct gpio_chip *chip; // 指向GPIO芯片结构体的指针,包含芯片的具体信息  int base;          // GPIO编号的基准值,用于计算实际GPIO号  u16 ngpio;         // 此设备管理的GPIO引脚数量  const char *label; // GPIO设备的标签,用于标识或分类  
};

        struct gpio_chip描述GPIO芯片,含操作函数等,由芯片原厂完成。

struct gpio_chip {    const char *label;          // GPIO芯片的标签,用于标识  int (*request)(...);        // 请求GPIO引脚的函数指针  void (*free)(...);          // 释放GPIO引脚的函数指针  int (*direction_input)(...); // 设置GPIO引脚为输入方向的函数指针  int (*direction_output)(...); // 设置GPIO引脚为输出方向的函数指针,并设置输出值  int (*get)(...);            // 读取GPIO引脚电平的函数指针  void (*set)(...);           // 设置GPIO引脚电平的函数指针  int base;                   // GPIO编号的基准值  u16 ngpio;                  // 此芯片管理的GPIO引脚数量  
};

        使用新版GPIO子系统API(如gpiod_set_value, gpiod_get_value等)可灵活配置和管理GPIO资源。

        这些API基于gpio_desc和gpio_device,通过gpio_chip中的函数指针实现对GPIO的控制。

        用户无需关心gpio_chip函数的具体实现,只需掌握API的使用即可。

第109章 获取单个 gpio 描述

109.1 函数介绍

109.1.1 获取 GPIO 描述符

        gpiod_get()函数用于获取与给定设备和连接标识符相关联的 GPIO 描述符

#include <linux/gpio/consumer.h>struct gpio_desc *gpiod_get(struct device *dev,  //与GPIO关联的设备的指针const char *con_id,  //GPIO的连接标识符,用于设备树上标识唯一GPIO //就是GPIO名称enum gpiod_flags flags); //GPIO描述符的选项标志,//用于指定GPIO的配置(如输入、输出、激活电平等)。

gpiod_get_index():通过索引获取 GPIO 描述符。

gpiod_get_optional():可选获取 GPIO 描述符,失败时返回 NULL。

gpiod_get_index_optional():通过索引可选获取 GPIO 描述符。

109.1.2 释放 GPIO 描述符

        gpiod_put() 函数用于释放之前获取的 GPIO 描述符

#include <linux/gpio/consumer.h>//释放之前获取的GPIO描述结构体
void gpiod_put(struct gpio_desc *desc);

109.2 设备树的修改

        新版本的 gpio 子系统 api 接口要与设备树结合才能使用,所以需要在设备树中将用于获取 GPIO 描述符的引脚复用为 GPIO 模式。

        首先根据核心板原理图的引脚配置查看设备树中是否已经对该引脚进行了复用,在确保该引脚无任何复用之后,对 rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的添加,在根节点的结尾添加以下内容:

my_gpio:gpiol_a0 {compatible = "mygpio";                        //兼容性字符串my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;  //引脚属性配置pinctrl-names = "default";                    //GPIO控制器名称列表,//default代表&mygpio_ctrl。pinctrl-0 = <&mygpio_ctrl>;                   //GPIO控制器配置
};/*compatible: 设备的兼容性字符串,与驱动匹配。my-gpios: 指定GPIO,含控制器句柄(&gpiol)、资源描述符(RK_PA0)及默认高电平。pinctrl-names: 引脚控制器配置名,此处为"default"。pinctrl-0: 与"default"配置关联的引脚控制器句柄(&mygpio_ctrl)。
*/

        然后找到 pinctrl 节点,在节点尾部添加以下内容,

mygpio {mygpio_ctrl: my-gpio-ctrl {rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>;};
};/*
在第三行的内容中,1 表示引脚索引,RK_PA0 表示资源描述符,用于标识与该引脚相关联的物理资源,表示引脚所属的功能组,RK _FUNC_GPI0 表示将引脚的功能设置为 GPIO,&pcfg_pull_none 表示引脚配置为无上下拉。
*/

        至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的 boot.img镜像烧写到开发板上即可。

109.3 驱动程序的编写

        设备树给了GPIO节点名称为"mygpio",因此驱动注册时可以通过匹配规则匹配到设备,从而调用probe函数。

平台设备初始化函数(my_platform_probe):

        当驱动与设备树中的设备节点匹配并加载时,此函数被调用。

        使用gpiod_get_optional尝试获取与设备相关联的GPIO描述符。

        将GPIO设置为输出模式,并初始化为低电平,随后设置为高电平。

        通过gpiod_get_direction获取GPIO的方向,并打印出来。

        使用gpiod_get_value读取GPIO的当前值,并打印。

        调用gpiod_to_irq将GPIO转换为中断号(若支持),并打印出来。

struct gpio_desc *mygpio1; // GPIO 描述符指针
int dir, value, irq; // 方向、值和中断号变量//平台设备初始化函数
static int my_platform_probe(struct platform_device *dev) {printk("This is mydriver_probe\n");// 获取 GPIO 描述符mygpio1 = gpiod_get_optional(&dev->dev, "my", 0);if (mygpio1 == NULL) {printk("gpiod_get_optional error\n");return -1;}gpiod_direction_output(mygpio1, 0); // 将 GPIO 设置为输出模式并设置初始值为低电平gpiod_set_value(mygpio1, 1); // 设置 GPIO 为高电平dir = gpiod_get_direction(mygpio1); // 获取 GPIO 的方向if (dir == GPIOF_DIR_IN) {printk("dir is GPIOF_DIR_IN\n"); // 输出方向为输入} else if (dir == GPIOF_DIR_OUT) {printk("dir is GPIOF_DIR_OUT\n"); // 输出方向为输出}value = gpiod_get_value(mygpio1); // 获取 GPIO 的值printk("value is %d\n", value); // 输出 GPIO 的值irq = gpiod_to_irq(mygpio1); // 将 GPIO 转换为中断号printk("irq is %d\n", irq); // 输出中断号return 0;
}// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_remove: Removing platform device\n");// 清理设备特定的操作// ... return 0;
}/*驱动-设备匹配规则*/
const struct of_device_id of_match_table_id[] = {{.compatible="mygpio"}, };// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {.probe = my_platform_probe, .remove = my_platform_remove, .driver = {.name = "my_platform_device", .owner = THIS_MODULE, .of_match_table = of_match_table_id, }, 
};// 模块初始化函数
static int __init my_platform_driver_init(void)
{int ret;// 注册平台驱动ret = platform_driver_register(&my_platform_driver);if (ret) {printk(KERN_ERR "Failed to register platform driver\n");return ret;}printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");return 0;
}// 模块退出函数
static void __exit my_platform_driver_exit(void)
{// 注销平台驱动platform_driver_unregister(&my_platform_driver);printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

第110章 GPIO 操作函数

110.1 函数介绍

        gpiod_get_direction()用来获取GPIO方向

        gpiod_direction_input()配置为输入。 

        gpiod_direction_output()配置为输出

        gpiod_get_value()用来读取GPIO电平状态

        gpiod_set_value()用来设置GPIO电平状态

        gpiod_to_irq()用来将GPIO描述符转换为中断号

int gpiod_get_direction(struct gpio_desc *desc);  
// 返回值:
//GPIO_LINE_DIRECTION_IN(0) 表示输入,
//GPIO_LINE_DIRECTION_OUT(1) 表示输出,负数表示错误。int gpiod_direction_input(struct gpio_desc *desc);  // 配置为输入  
int gpiod_direction_output(struct gpio_desc *desc, int value);  // 配置为输出,并设置初始值int gpiod_get_value(const struct gpio_desc *desc);  //获取GPIO电平状态
void gpiod_set_value(struct gpio_desc *desc, int value);  //设置GPIO电平状态int gpiod_to_irq(const struct gpio_desc *desc);  //GPIO描述符转为中断号

110.2 驱动程序编写

        设备树给了GPIO节点名称为"mygpio",因此驱动注册时可以通过匹配规则匹配到设备,从而调用probe函数。

struct gpio_desc *mygpio1; // GPIO 描述符指针
int dir, value, irq; // 方向、值和中断号变量//平台设备初始化函数
int mydriver_probe(struct platform_device *dev) {printk("This is mydriver_probe\n");// 获取 GPIO 描述符mygpio1 = gpiod_get_optional(&dev->dev, "my", 0);if (mygpio1 == NULL) {printk("gpiod_get_optional error\n");return -1;}gpiod_direction_output(mygpio1, 0); // 将 GPIO 设置为输出模式并设置初始值为低电平gpiod_set_value(mygpio1, 1); // 设置 GPIO 为高电平dir = gpiod_get_direction(mygpio1); // 获取 GPIO 的方向if (dir == GPIOF_DIR_IN) {printk("dir is GPIOF_DIR_IN\n"); // 输出方向为输入} else if (dir == GPIOF_DIR_OUT) {printk("dir is GPIOF_DIR_OUT\n"); // 输出方向为输出}value = gpiod_get_value(mygpio1); // 获取 GPIO 的值printk("value is %d\n", value); // 输出 GPIO 的值irq = gpiod_to_irq(mygpio1); // 将 GPIO 转换为中断号printk("irq is %d\n", irq); // 输出中断号return 0;
}// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_remove: Removing platform device\n");// 清理设备特定的操作// ... return 0;
}//驱动-设备匹配规则
const struct of_device_id of_match_table_id[] = {{.compatible="mygpio"}, };// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {.probe = my_platform_probe,.remove = my_platform_remove,.driver = {.name = "my_platform_device",.owner = THIS_MODULE, .of_match_table = of_match_table_id, }, 
};// 模块初始化函数
static int __init my_platform_driver_init(void)
{int ret;// 注册平台驱动ret = platform_driver_register(&my_platform_driver);if (ret) {printk(KERN_ERR "Failed to register platform driver\n");return ret;}printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");return 0;
}// 模块退出函数
static void __exit my_platform_driver_exit(void)
{// 注销平台驱动gpiod_put(mygpio2);platform_driver_unregister(&my_platform_driver);printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

第111章 三级节点操作函数

        新版本 GPIO 子系统中的 GPIO 操作实验,而在进行操作之前首先要获取相应的 gpio 描述,

        在前面的示例中获取的都是二级节点的 GPIO 描述,

        如果仍旧使用 gpiod_get 来获取三级节点的 gpio 描述会发现是获取不成功的

/* 定义名为my_gpio的节点,代表gpio1_a0这个GPIO控制器或引脚组 */  
my_gpio: gpio1_a0 {  compatible = "mygpio"; /* 表明这个节点兼容"mygpio"这个设备或驱动 */  /* 定义led1的子节点,包含GPIO配置 */  led1 {  /* my-gpios属性列出了led1所使用的GPIO引脚及其配置 */  /* &gpio1表示GPIO控制器的引用,RK_PA0和RK_PB1是引脚编号,GPIO_ACTIVE_HIGH表示高电平有效 */  my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>, <&gpio1 RK_PB1 GPIO_ACTIVE_HIGH>;  /* pinctrl-names属性定义了引脚控制组(pinctrl)的名称 */  pinctrl-names = "default";  /* pinctrl-0属性引用了具体的引脚控制组配置,&mygpio_ctrl是配置节点的引用 */  pinctrl-0 = <&mygpio_ctrl>;  };  /* 定义led2的子节点,包含GPIO配置 */  led2 {  /* my-gpios属性列出了led2所使用的GPIO引脚及其配置 */  /* 这里只使用了一个引脚,&gpio1表示GPIO控制器的引用,RK_PB0是引脚编号,GPIO_ACTIVE_HIGH表示高电平有效 */  my-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;  /* 注意:led2没有定义pinctrl-names和pinctrl-0属性,可能意味着它使用默认配置或不需要特定的引脚控制配置 */  };  
};

111.1 函数介绍

        device_get_child_node_count() 用于计算给定设备节点的子节点数量

#include <linux/device.h>  //返回设备节点 dev 的子节点数量 
unsigned int device_get_child_node_count(struct device *dev);

fwnode_get_named_gpiod() 通过指定节点的对象地址获取子节点中的 GPIO 结构描述

#include <linux/gpio/consumer.h>  struct gpio_desc *fwnode_get_named_gpiod(struct fwnode_handle *fwnode,//const char *propname, //指定要获取的GPIO的属性名称。int index,      //指定要获取的GPIO在属性中的索引。enum gpiod_flags dflags,  //GPIO的初始化配置标志。const char *label);//用于标识GPIO的描述标签。/*
fwnode:指向struct fwnode_handle的指针,表示要获取GPIO的节点对象地址。
propname:指定要获取的GPIO的属性名称。
index:指定要获取的GPIO在属性中的索引。
dflags:GPIO的初始化配置标志。
label:用于标识GPIO的描述标签(节点名称)。
*/

        device_get_next_child_node() 用于获取给定父设备节点的下一个子设备节点

struct fwnode_handle *device_get_next_child_node(struct device *dev,  //父设备节点。struct fwnode_handle *child);/*
第二个参数
指向struct fwnode_handle的指针,表示当前子设备节点。如果为NULL,则函数返回父设备节点的第一个子节点。
*/

111.2 设备树的修改

        由于本章节要获取到三级节点的 GPIO 描述,所以要对 rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的修改,将根节点中的 gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {compatible = "mygpio";led1{my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>, <&gpio1 RK_PB1 GPIO_ACTIVE_HIGH>;pinctrl-names = "default";pinctrl-0 = <&mygpio_ctrl>;};led2{my-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;};
};

        保存退出之后,编译内核,然后将生成的 boot.img镜像烧写到开发板上。

        至于驱动和上一章类似,只是用到的API不同。

第112章 GPIO 子系统与 pinctrl 子系统相结合

        实验的设备树示例如下所示,是一个二级节点。

my_gpio:gpio1_a0 {compatible = "mygpio";my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;pinctrl-names = "myled1";pinctrl-0 = <&mygpio_ctrl>;
};

        参数 pinctrl-names 并不是 default,这就需要用到我们前面 pinctrl 子系统中的知识来查找并设置相应的 pinctrl 状态了。

/*pinctrl结构体*/
struct pinctrl {  // 引用计数  struct kref kref;  // 设备指针(指向与这个pinctrl实例相关联的设备)  struct device *dev;  // 状态列表(包含与这个pinctrl实例相关联的所有pinctrl_state实例)  struct list_head states;  // 其他成员(如操作函数、引脚组等)  // ...  
};  /*pinctrl_state结构体*/
struct pinctrl_state {  // 状态名称  const char *name;  // 引用计数  struct kref kref;  // 引脚配置(包含在这个状态下每个引脚应该如何配置的详细信息)  // 这可能是一个数组、链表或其他数据结构  void *pin_configs;  // 指向pinctrl实例的指针(这个状态是从哪个pinctrl实例中获取的)  struct pinctrl *pctl;  // 其他成员(如状态标志等)  // ...  
};

112.1 pinctrl相关函数介绍

        pinctrl_get()用于获取与给定设备对象相关联的 pinctrl 实例

        pinctrl_put()用于释放获得的 pinctrl 实例

        pinctrl_lookup_state()用于在给定的 pinctrl 实例中查找指定名称的 pinctrl 状态

        pinctrl_select_state()用于将指定的 pinctrl 状态设置到 pinctrl 上

#include <linux/pinctrl/pinctrl.h>
/*获取与设备相关的pinctrl实例*/
struct pinctrl* pinctrl_get(struct device *dev);
/*释放由pinctrl_get获得的pinctrl实例*/
void pinctrl_put(struct pinctrl *p);
/*在pinctrl实例中查找指定名称的状态*/
struct pinctrl_state *pinctrl_lookup_state(struct pinctrl *p, const char *name);
/*将pinctrl状态设置到指定的pinctrl上*/
int pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *s);

112.2 设备树的修改

        由于本章节要使用 pinctrl 子系统相关的接口来查找并设置相应的 pinctrl 状态,所以要对 rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的修改,将根节点中的 gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {compatible = "mygpio";                      //匹配规则my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;//gpio属性设置pinctrl-names = "myled1";                   //pinctrl状态的名称pinctrl-0 = <&mygpio_ctrl>;    //指定与pinctrl-names中第一个名称相关联的pinctrl状态。//mygpio_ctrl标签见前几章的设备树修改
};
mygpio {mygpio_ctrl: my-gpio-ctrl {rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>;};
};
/*在第三行的内容中,1 表示引脚索引,RK_PA0 表示资源描述符,用于标识与该引脚相关联的物理资源,表示引脚所属的功能组,RK _FUNC_GPI0 表示将引脚的功能设置为 GPIO,&pcfg_pull_none 表示引脚配置为无上下拉,即浮空*/
struct pinctrl *led_pinctrl;    // pinctrl 实例指针
struct pinctrl_state *led_state;// pinctrl 状态指针//平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{printk("This is mydriver_probe\n");led_pinctrl = pinctrl_get(&dev->dev);// 获取 pinctrl 实例if (IS_ERR(led_pinctrl)) {printk("pinctrl_get is error\n");return -1;}led_state = pinctrl_lookup_state(led_pinctrl, "myled1");// 查找状态if (IS_ERR(led_state)) {printk("pinctrl_lookup_state is error\n");return -2;}ret = pinctrl_select_state(led_pinctrl, led_state);// 设置状态到硬件if (ret < 0) {printk("pinctrl_select_state is error\n");return -3;}return 0;
}//设备-驱动匹配规则
const struct of_device_id of_match_table_id[] = {{.compatible="mygpio"}, };// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {.probe = my_platform_probe, .remove = my_platform_remove, .driver = {.name = "my_platform_device", .owner = THIS_MODULE,.of_match_table = of_match_table_id, }, 
};// 模块初始化函数
static int __init my_platform_driver_init(void)
{int ret;// 注册平台驱动ret = platform_driver_register(&my_platform_driver);if (ret) {printk(KERN_ERR "Failed to register platform driver\n");return ret;}printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");return 0;
}

112.3 测试

        启动开发板,首先使用以下命令查看 gpio1 RK_PA0 引脚的复用功能,

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

        可以看到在没有加载驱动之前,gpio1 RK_PA0 引脚是没有进行复用的,

        加载模块后再查看,发现引脚已经配置了复用

        根据打印信息可以得到 gpio1 RK_PA0 已经被设置为了 GPIO 功能,功能引脚组正是我们在 pinctrl 节点中添加的信息,证明已经成功使用了添加的 pinctrl-names 状态。

        设备树加载启动时,由于设备树节点 my_gpio没有对应的驱动,所以不会被加载,因此pinctrl状态也不会被加载到引脚上。

第113章 实战:实现动态切换引脚复用功能

        这里仍旧使用 RK3568 底板背面的 20 pin GPIO 底座的 1 号管脚来完成本章节要进行的动态切换引脚复用的功能,该引脚的核心板原理图内容如下所示:

        左侧为该引脚的一些其他复用功能,在前面的章节中复用的都是 GPIO 功能,而本章节中将实现 I2C3_SDA 和 GPIO 两个复用功能的动态切换。

113.1 设备树的修改

        首先根据上图中的复用功能查看设备树中是否已经对该引脚进行了复用,在确保该引脚无任何复用之后,rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的添加,将根节点中的 gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {compatible = "mygpio";my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;pinctrl-names = "mygpio_func1", "mygpio_func2";pinctrl-0 = <&mygpio_ctrl>;    //第一个pinctrl名称对应的设置pinctrl-1 = <&i2c3_sda>;       //第二个pinctrl名称对应的设置
};

        pinctrl-names 表示引脚控制器配置的名称,这里有两个值,分别对应复用 1 和复用 2。

        pinctrl-0 指定了与该配置相关联的引脚控制器句柄,这里为 &mygpio_ctrl,表示复用为 gpio功能。

        pinctrl-1 指定了与该配置相关联的引脚控制器句柄,这里为 &i2c3_sda,表示复用为i2c3_sda 功能。

        然后找到 pinctrl 节点,在节点尾部进行修改和添加,具体内容如下所示:

mygpio_func1 {mygpio_ctrl: my-gpio-ctrl {rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>;};
};
mygpio_func2 {i2c3_sda: i2c3_sda {rockchip,pins = <1 RK_PA0 1 &pcfg_pull_none>;};
};/*
1:通常表示引脚的数量,这里只有一个引脚被配置。
RK_PA0:引脚的标识,表示这是Rockchip平台上PA0这个GPIO引脚。
RK_FUNC_GPIO:引脚的功能,这里设置为GPIO模式。
&pcfg_pull_none:引脚的电气配置,这里表示没有上拉或下拉电阻。
*//*
第三个参数是 1 而不是 RK_FUNC_GPIO。这是功能编号,代表i2c3_sda
*/

        至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的 boot.img镜像烧写到开发板上即可。

113.2 驱动程序的编写

struct pinctrl *gpio_pinctrl; // GPIO pinctrl 实例指针
struct pinctrl_state *func1_state; // 功能 1 状态
struct pinctrl_state *func2_state; // 功能 2 状态
int ret;ssize_t selectmux_store(struct device *dev, struct device_attribute *attr, const char *buf,size_t count)
{unsigned long select;select = simple_strtoul(buf, NULL, 10);//将字符串转无符号长整型数值if (select == 1) {pinctrl_select_state(gpio_pinctrl, func1_state); // 选择功能 1 状态} else if (select == 0) {pinctrl_select_state(gpio_pinctrl, func2_state); // 选择功能 2 状态}return count;
}DEVICE_ATTR_WO(selectmux); // 定义可写的设备属性 selectmux//获取pinctrl实例并查看状态
int pinctrl_get_and_lookstate(struct device *dev)
{gpio_pinctrl = pinctrl_get(dev); // 获取 GPIO pinctrl 实例if (IS_ERR(gpio_pinctrl)) {printk("pinctrl_get is error\n");return -1;}func1_state = pinctrl_lookup_state(gpio_pinctrl, "mygpio_func1"); // 查找功能 1 状态if (IS_ERR(func1_state)) {printk("pinctrl_lookup_state is error\n");return -2;}func2_state = pinctrl_lookup_state(gpio_pinctrl, "mygpio_func2"); // 查找功能 2 状态if (IS_ERR(func2_state)) {printk("pinctrl_lookup_state is error\n");return -2;}return 0;
}// 平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{printk("This is mydriver_probe\n");pinctrl_get_and_lookstate(&dev->dev); // 获取并查找 GPIO pinctrl 实例和状态// 在设备上创建属性件device_create_file(&dev->dev, //设备&dev_attr_selectmux);//dev_attribute结构体指针,dev_attr结构体有store和load俩方法return 0;
}// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_remove: Removing platform device\n");// 清理设备特定的操作// ... return 0;
}const struct of_device_id of_match_table_id[] = {{.compatible="mygpio"}, };// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {.probe = my_platform_probe, .remove = my_platform_remove, .driver = {.name = "my_platform_device", .owner = THIS_MODULE, .of_match_table = of_match_table_id, }, 
};// 模块初始化函数
static int __init my_platform_driver_init(void)
{int ret;// 注册平台驱动ret = platform_driver_register(&my_platform_driver);if (ret) {printk(KERN_ERR "Failed to register platform driver\n");return ret;}printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");return 0;
}// 模块退出函数
static void __exit my_platform_driver_exit(void)
{// 注销平台驱动platform_driver_unregister(&my_platform_driver);printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

        效果就是向设备的属性文件store写1或者0达到切换设备的pinctrl状态的效果,进而切换引脚复用状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/440075.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL高阶2004-职员招聘人数

目录 题目 准备数据 分析数据 实现 题目 一家公司想雇佣新员工。公司的工资预算是 70000 美元。公司的招聘标准是&#xff1a; 雇佣最多的高级员工。在雇佣最多的高级员工后&#xff0c;使用剩余预算雇佣最多的初级员工。 编写一个SQL查询&#xff0c;查找根据上述标准雇…

男单新老对决:林诗栋VS马龙,巅峰之战

听闻了那场激动人心的新老对决&#xff0c;不禁让人热血沸腾。在这场乒乓球的巅峰之战中&#xff0c;林诗栋与马龙的对决无疑是一场视觉与技术的盛宴。 3:3的决胜局&#xff0c;两位选手的每一次挥拍都充满了策略与智慧&#xff0c;他们的每一次得分都让人心跳加速。 林诗栋&am…

Linux自动化构建工具Make/Makefile

make是一个命令 makefile是一个文件 touch 创建并用vim打开makefile 写入依赖对象和依赖方法 mycode是目标文件 第二行数依赖方法 以tab键开头 make makefile原理 makefile中写的是依赖关系和依赖方法 clean英语清理文件 后不用加源文件。.PHONY定义clean是伪目标。 make只…

动态SLAM总结二

文章目录 Mapping the Static Parts of Dynamic Scenes from 3D LiDAR Point Clouds Exploiting Ground Segmentation&#xff1a;&#xff08;2021&#xff09;RF-LIO&#xff1a;&#xff08;2022&#xff09;RH-Map&#xff1a;&#xff08;2023&#xff09;Mapless Online …

[C++]使用纯opencv部署yolov11-pose姿态估计onnx模型

【算法介绍】 使用纯OpenCV部署YOLOv11-Pose姿态估计ONNX模型是一项具有挑战性的任务&#xff0c;因为YOLOv11通常是用PyTorch等深度学习框架实现的&#xff0c;而OpenCV本身并不直接支持加载和运行PyTorch模型。然而&#xff0c;可以通过一些间接的方法来实现这一目标&#x…

POLYGON Nature - Low Poly 3D Art by Synty 树木植物

一个低多边形资源包,包含可以添加到现有多边形风格游戏中的树木、植物、地形、岩石、道具和特效 FX 资源。 为 POLYGON 系列提供混合样式树这一新增功能。弥合 POLYGON 与更传统的层级资源之间的差距。还提供了一组经典的 POLYGON 风格的树木和植被以满足你的需求。 该包还附带…

系统安全 - Linux /Docker 安全模型及实践

文章目录 导图Linux安全Linux 安全模型用户层权限管理的细节多用户环境中的权限管理文件权限与目录权限 最小权限原则的应用Linux 系统中的认证、授权和审计机制认证机制授权机制审计机制 小结 内网安全Docker安全1. Docker 服务隔离机制Namespace 机制Capabilities 机制CGroup…

JavaWeb - 8 - 请求响应 分层解耦

请求响应 请求&#xff08;HttpServletRequest&#xff09;&#xff1a;获取请求数据 响应&#xff08;HttpServletResponse&#xff09;&#xff1a;设置响应数据 BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程…

Oracle中MONTHS_BETWEEN()函数详解

文章目录 前言一、MONTHS_BETWEEN()的语法二、主要用途三、测试用例总结 前言 在Oracle数据库中&#xff0c;MONTHS_BETWEEN()函数可以用来计算两个日期之间的月份差。它返回一个浮点数&#xff0c;表示两个日期之间的整月数。 一、MONTHS_BETWEEN()的语法 MONTHS_BETWEEN(dat…

水下声呐数据集,带标注

水下声呐数据集&#xff0c;带标注 水下声呐数据集 数据集名称 水下声呐数据集 (Underwater Sonar Dataset) 数据集概述 本数据集是一个专门用于训练和评估水下目标检测与分类模型的数据集。数据集包含大量的水下声呐图像&#xff0c;每张图像都经过专业标注&#xff0c;标明…

vSAN05:vSAN延伸集群简介与创建、资源要求与计算、高级功能配置、维护、故障处理

目录 vSAN延伸集群延伸集群创建延伸集群的建议网络配置vSAN延伸集群的端口见证主机的资源要求vSAN延伸集群中见证节点带宽占用vSAN延伸集群的允许故障数vSAN延伸集群不同配置下的空间占用 vSAN延伸集群的HA配置vSAN延伸集群的DRS配置vSAN存储策略以及虚拟机/主机策略的互操作vS…

华为最新业绩出炉!上半年营收4175亿元,同比增长34%!

华为2024年上半年经营业绩分析:稳健发展,符合预期 [中国,深圳,2024年8月29日] 今日,华为发布了其2024年上半年的经营业绩,整体表现稳健,结果符合预期。在复杂多变的全球市场环境下,华为凭借强大的创新能力和市场洞察力,实现了销售收入和净利润的显著增长。 上半年,华…

C语言:预编译过程的剖析

目录 一.预定义符号和#define定义常量 二.#define定义宏 三.宏和函数的对比 四、#和##运算符 五、条件编译 在之前&#xff0c;我们已经介绍了.c文件在运行的过程图解&#xff0c;大的方面要经过两个方面。 一、翻译环境 1.预处理&#xff08;预编译&#xff09; 2.编译 3…

Spring Boot 整合 Keycloak

1、概览 本文将带你了解如何设置 Keycloak 服务器&#xff0c;以及如何使用 Spring Security OAuth2.0 将 Spring Boot 应用连接到 Keycloak 服务器。 2、Keycloak 是什么&#xff1f; Keycloak 是针对现代应用和服务的开源身份和访问管理解决方案。 Keycloak 提供了诸如单…

Unity MVC框架演示 1-1 理论分析

本文仅作学习笔记分享与交流&#xff0c;不做任何商业用途&#xff0c;该课程资源来源于唐老狮 1.一般的图解MVC 什么是MVC我就不说了&#xff0c;老生常谈&#xff0c;网上有大量的介绍&#xff0c;想看看这三层都起到什么职责&#xff1f;那就直接上图吧 2.我举一个栗子 我有…

人工智能新闻和发展 (24001)- By 10/4/2024

1. 谷歌增强了搜索中的人工智能&#xff0c;允许对图像进行语音提问。 Google adding AI to answer voiced questions about images | AP NewsGoogle is pumping more artificial intelligence into its search engine. New features will enable people to voice questions a…

15分钟学 Python 第39天:Python 爬虫入门(五)

Day 39&#xff1a;Python 爬虫入门数据存储概述 在进行网页爬虫时&#xff0c;抓取到的数据需要存储以供后续分析和使用。常见的存储方式包括但不限于&#xff1a; 文件存储&#xff08;如文本文件、CSV、JSON&#xff09;数据库存储&#xff08;如SQLite、MySQL、MongoDB&a…

无神论文解读之ControlNet:Adding Conditional Control to Text-to-Image Diffusion Models

一、什么是ControlNet ControlNet是一种能够控制模型生成内容的方法&#xff0c;能够对文生图等模型添加限制信息&#xff08;边缘、深度图、法向量图、姿势点图等&#xff09;&#xff0c;在当今生成比较火的时代很流行。 这种方法使得能够直接提供空间信息控制图片以更细粒…

PCL 1.8.1 + VTK 1.8.0 + QT5.14.2+ VS2017 环境搭建

先看看效果: PCL 1.8.1下载安装: Tags PointCloudLibrary/pcl GitHub 安装完成后: 如果VTK想重新编译的,可以看我的这篇博客:

Spring14——案例:利用AOP环绕通知计算业务层接口执行效率

前面介绍了这么多种通知类型&#xff0c;具体该选哪一种呢? 我们可以通过一些案例加深下对通知类型的学习。 34-案例&#xff1a;利用AOP环绕通知计算业务层接口执行效率 需求分析 这个需求也比较简单&#xff0c;前面我们在介绍AOP的时候已经演示过: 需求:任意业务层接口…