RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】824412014(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第十二篇 GPIO子系统_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第129章 GPIO控制和操作实验

GPIO软件编程方式有多种,可以写驱动程序调用GPIO函数操作GPIO,也可以直接通过操作寄存器的方式操作GPIO,还可以通过sysfs方式实现对GPIO的控制。本章节我们来学习使用sysfs方式实现对GPIO的控制。

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

129.1.1 内核配置

使用sysfs方式控制gpio,首先需要底层驱动的支持,需要在make menuconfig图形化配置界面中加入以下配置:

Device Drivers

->GPIO Support

->/sys/class/gpio/xxxx

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第1张图片

图 129-1

129.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  

引脚编号=控制寄存器的寄存器基数+控制引脚寄存器位数。 在 rk3568 中, GPIO_number的计算方法为: n*32 + (K-A)*8 + x; 括号里面的 A、 B、 C、 D 分别代表数值 0、 1、 2、 3, 在计算时候分别对应即可。 

下面演示LED9用户LED灯的GPIO0_PB7 pin脚计算方法:

bank = 0;       //GPIO0_B7=> 0, bank ∈ [0,4]
group = 1;      //GPIO0_B7 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 7;         //GPIO4_D7 => 5, X ∈ [0,7]
number = group * 8 + X = 1 * 8 + 7 =15
pin = bank*32 + number= 0 * 32 + 15 = 15;

图 129-2

/sys/class/gpio/export 用于将GPIO控制从内核空间导出到用户空间。/sys/class/gpio/unexport 用于取消GPIO控制从内核空间到用户空间的导出。 export 和 unexport,他们都是只写的。GpiochipX代表GPIO 控制器。

export用于将指定编号的 GPIO 引脚导出。在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。注意 export 文件是只写文件,不能读取,将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出,以GPIO0_PB7为例(pin计算值为15)使用export 文件进行导出(如果没有更换本章开始部分的内核设备树镜像,会导出不成功),导出成功如下图所示:

echo 15 > export

图 129-3

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

unexport将导出的 GPIO 引脚删除。当使用完 GPIO 引脚之后,需要将导出的引脚删除,同样该文件也是只写文件、不可读,使用unexport 文件进行删除GPIO0_PB7,删除成功如下图所示:

 echo 15 > unexport

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第2张图片

图 129-4

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

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

图 129-5

出现上图报错的原因是该GPIO已经被其他GPIO使用,需要在内核中找到使用GPIO的驱动,并取消该驱动才可以正常使用GPIO。在使用GPIO15时,需要取消Linux内核源码中LED灯的配置,如下所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第3张图片

图 129-6

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

echo 15 > export

图 129-7

  可以看到gpio15文件夹下分别有active_low、device、direction、edge、power、subsystem、uevent、value八个文件,需要关心的文件是 active_low、direction、edge 以及 value 这四个属性文件,接下来分别介绍这四个属性文件的作用:

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

在“/sys/class/gpio/gpio15”目录下使用cat命令查看direction输入输出模式,如下图所示:

cat direction

图 129-8

 默认状态下的输入输出状态为“in”,由于direction为可读可写,可以使用以下命令将模式配置为输出,配置完成如下图所示

echo out > direction

cat direction

图 129-10

当 active_low 等于 0 时, value 值若为1则引脚输出高电平,value 值若为0则引脚输出低电平。当 active_low 等于 1 时 ,value 值若为0则引脚输出高电平,value 值若为1则引脚输出低电平。

edge控制中断的触发模式,该文件可读可写。在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式,四种触发模式的设置如下所示:

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

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

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

边沿触发:  echo "both" > edge

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

echo 1 > value

反之,把GPIO设置成低电平,使用命令

echo 0 > value

图 129-11

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

129.2.1 控制GPIO输出实验

本小节代码在配套资料“iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\82_gpioctrl01”目录下。

实验要求:

通过GPIO输出应用程序控制GPIO口输出高低电平,以此来控制LED灯的亮灭。

实验步骤:

首先进入ubuntu的终端界面输入以下命令来创建 gpioctrl.c文件,如下图所示:

图 129-12

然后向该文件中添加以下内容:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int fd;                   // 文件描述符
int ret;                  // 返回值
char gpio_path[100];      // GPIO路径
int len;                  // 字符串长度

// 导出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[]) // 主函数
{
    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建GPIO路径,格式为“/sys/class/gpio/gpio引脚号”
    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表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH

aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

图 129-13

最后将交叉编译生成的gpioctrl文件拷贝到开发板目录下运行即可,如下所示:

 

图 129-14

输入“./gpioctrl 15 1”命令LED灯点亮,输入“./gpioctrl 15 0”命令LED灯熄灭。

到此,实验结束。

129.2.2 控制GPIO输入实验

本小节代码在配套资料“iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\83_gpioctrl02”目录下。

实验要求:

通过GPIO输入应用程序读取GPIO口的输入电平。

实验硬件连接:

使用迅为iTOP-RK3568开发板,使用导线连接开发板背面的引脚GPIO1_B2,另一端连接到电源或者GND。

实验步骤:

首先进入ubuntu的终端界面输入以下命令来创建 gpioctrl.c文件,如下图所示:

图 129-15

然后向该文件中添加以下内容:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int fd;              // 文件描述符
int ret;             // 返回值
char gpio_path[100]; // GPIO路径
int len;             // 字符串长度
char file_path[100]; // 文件路径
char buf[2];         // 用于读取 GPIO 值的缓冲区

// 导出 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)
{
    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); // 关闭文件
}

// 读取 GPIO 引脚的值
int gpio_read_value(char *arg)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径,格式为 "gpio_path/arg"
    fd = open(file_path, O_RDONLY);              // 打开文件
    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"); // GPIO 引脚值为高电平
        return 1;
    }
    else if (!strcmp(buf,"0"))
    {
        printf("The value is low\n"); // GPIO 引脚值为低电平
        return 0;
    }
    
    close(fd); // 关闭文件
    return -1;
    
}

int main(int argc, char *argv[]) // 主函数
{
    int value;
    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径,格式为 "/sys/class/gpio/gpio引脚号"
    if (access(gpio_path, F_OK))                           // 检查 GPIO 路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
    }
    else
    {
        gpio_unexport(argv[1]); // 存在则取消导出 GPIO 引脚
    }

    gpio_ctrl("direction", "in");       // 配置 GPIO 为输入模式
    
    value = gpio_read_value("value");   // 读取 GPIO 引脚的值
    printf("The value is %d\n", value); // 打印读取的 GPIO 引脚的值
    gpio_unexport(argv[1]);             // 最后取消导出 GPIO 引脚

    return 0; // 返回 0 表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH

aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

图 129-16

最后将交叉编译生成的gpioctrl文件拷贝到开发板目录下运行即可。

为了测试输入高电平的状况,作者使用了杜邦线将开发板背面的3.3V接到了GPIO1_PB2 pin脚上,然后再次使用以下命令来进行状态的检测,如下图所示:

chmod 777 gpioctrl

./gpioctrl 42

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第4张图片

图 129-17

可以看到GPIO1_PB2 pin脚打印的value值为高,所以gpio的状态打印正确。

同理,我们将GPIO1_PB2 pin脚接到GND,再次运行程序,如下图所示:

图 129-18

可以看到GPIO1_PB2 pin脚打印的value值为0,所以gpio的状态打印正确。

至此GPIO输入应用程序在开发板的测试就完成了。

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

本小节代码在配套资料“iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\84_gpioctrl03”目录下。

实验要求:

通过GPIO的输入中断程序,将中断触发方式设置为边沿触发,每当触发中断会打印value的值。

129.3.1编写应用程序 

实验步骤:

首先进入到ubuntu的终端界面输入以下命令来创建 gpioctrl.c文件,如下图所示:

图 129-19

然后向该文件中添加以下内容:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int fd;              // 文件描述符
int ret;             // 返回值
char gpio_path[100]; // GPIO路径
int len;             // 字符串长度
char file_path[100]; // 文件路径
char buf[2];         // 缓冲区

struct pollfd fds[1]; // poll结构体数组

// 导出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); // 写入引脚号到export文件
    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); // 写入引脚号到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),而不是返回固定的-1
    close(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表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH

aarch64-linux-gnu-gcc -o demo64_interrupt demo64_interrupt.c

图129-20

最后将交叉编译生成的gpioctrl文件拷贝到开发板目录下即可。

129.3.2开发板测试

我们使用导线一端连接开发板背面的GPIO1_B2,然后输入以下命令运行程序。

./gpioctrl 42&

由于中断并没有被触发,所以程序会阻塞,等待中断的进行,然后使用杜邦线的另一端将GPIO底座的3.3V接到GPIO1_PB2 pin脚,进行中断的测试,如下图所示:

 

图 129-21

可以看到中断就被触发了,相应的字符串也被打印了。

至此GPIO输入中断应用程序在开发板的测试就完成了。

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

129.4.1 IO命令

"io" 命令是一个用于 Linux 系统的命令行工具,用于读取和写入指定 I/O 端口的值。它主要用于与硬件设备进行低级别的交互和调试,在内核阶段读写寄存器。

该命令的语法如下:

io [选项] [地址] [操作] [数据]

  1. 其中,选项可以是以下之一:

-b:以字节为单位进行 I/O 操作(默认为字)。

-w:以字为单位进行 I/O 操作。

-l:以双字为单位进行 I/O 操作。

  1. 地址是要读取或写入的 I/O 端口的十六进制值。
  2. 操作可以是以下之一:

r:读取 I/O 端口的值。

w:写入数据到 I/O 端口。

  1. 数据是要写入 I/O 端口的十六进制值。

以下是一个使用 io命令的示例

以下是一些使用 `io` 命令的示例:

1. 读取 I/O 端口的值:

io -b -r 0x80

这将以字节为单位读取 I/O 端口 0x80的值,并将其显示在终端上。

2. 向 I/O 端口写入数据:

io -b -w 0x80 0xAB

这将向 I/O 端口 0x80 写入十六进制值 0xAB。

3. 以字为单位进行读取:

io -w -r 0x1000

这将以字为单位读取 I/O 端口 0x1000的值

4. 以双字为单位进行写入:

io -l -w 0x2000 0xDEADBEEF

这将以双字为单位向 I/O 端口 0x2000 写入十六进制值 0xDEADBEEF。

129.4.2 LED引脚寄存器查找

我们查询到了控制LED灯的GPIO为GPIO0_B7。在接下来的实验中需要对GPIO进行配置,一般情况下需要对GPIO的复用寄存器,方向寄存器,数据寄存器进行配置。接下来我们打开RK3568的参考手册part1查找这几个寄存器的地址。

129.4.2.1查找复用寄存器

打开参考手册part1的第三章,GPIOB的复用寄存器的偏移地址如下(图129-22)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第5张图片

图 129-22

搜索gpio0b7,如下图(图 129-23)所示,gpio0b7_sel在PMU_GRF_GPIO0B_IOMUX_H上,所以偏移地址为0x000C。gpio0b7可以通过控制[14:12]位来选择复用为哪个功能,我们要控制led灯,所以功能要复用为gpio。

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第6张图片

图 129-23

复用寄存器的基地址如下图(图 129-24)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第7张图片

图 129-24

所以复用寄存器地址=基地址+偏移地址=0xFDC2000C 。使用io命令查看此寄存器的地址:

io -r -4 0xFDC2000C

图 129-25

如上图(图 129-25)所示,寄存器值为00000001,[14:12]位为000,如下图(图 129-26)所示,所以默认设置的为gpio功能。

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第8张图片

图 129-26

129.4.2.2查找方向寄存器

打开参考手册part1的第16章节,数据寄存器的偏移地址如下图(图 129-24)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第9张图片

图 129-24

GPIO有四组GPIO,分别是GPIOA,GPIOB,GPIOC,GPIOD。每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分。GPIO0B7在GPIO_SWPORT_DDR_L上所以,方向寄存器的偏移地址为0x0008。接着查看GPIO_SWPORT_DDR_L寄存器的具体描述,如下图(图129-25)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第10张图片

图 129-25

如上图(图 129-25)所示,[31:16]位属性是WO,也就是只可写入。这[31:16]位是写标志位,是低16位的写使能。如果低16位中某一位要设置输入输入输出,则对应高位写标志也应该设置为1。 [15:0] 是数据方向控制寄存器低位,如果要设置某个GPIO为输出,则对应位置1,如果要设置某个GPIO为输入,则对应位置0。那么GPIO0 B7 ,我们要设置第15位为输入还是输出,那么对应的[31:16]位写使能也要置1。

打开参考手册part1的1.1小节Address Mapping。

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第11张图片

图 129-27

如上图(图129-27)所示,GPIO0的基地址为0xFDD60000。方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008

然后使用IO命令查看该寄存器的值,如下(图129-28)所示:

图 129-28

如下图(图 129-29)所示,第15位默认为1,设置GPIO0_B7为输出。

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第12张图片

图129-29

129.4.2.3查找数据寄存器

打开参考手册part1的1.1小节Address Mapping。

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第13张图片

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第14张图片

图 129-31

如上图(图18-13)所示,GPIO0的基地址为0xFDD60000。

数据寄存器的偏移地址如下(图129-32)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第15张图片

所以数据寄存器的地址为基地址+偏移地址=0xFDD60000。使用IO命令查看地址的值,如下(图129-33)所示:

图 129-33

我们来看一下这个数据寄存器的描述,如下图(图129-34)所示,

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第16张图片

图129-34

分析上图的方法和在分析方向寄存器的方法同理,由上图可知,如果要控制第15位为高电平(置1),需要设置31位为1,那么点亮灯,需要向数据寄存器写入0x8000c040,如下图(图129-35)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第17张图片

图 129-35

如果要灭灯,需要设置第15位为0 ,第31位为1,那么向数据寄存器中写入0x80004040,如下图(图 129-36)所示:

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第18张图片

总结

  • 复用关系寄存器的基地址为0xFDC20000 ,偏移地址为000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
  • GPIO的基地址为0xFDD60000,偏移地址为0x0008,所以方向寄存器要操作的地址为基地址+偏移地址=0xFDD60008,我们要给方向寄存器写入0x80000044设置为输出。
  • GPIO的基地址为0xFDD60000,偏移地址为0x0000,所以数据寄存器要操作的地址为基地址+偏移地址=0xFDD60000
  • 默认的数据寄存器的值:0x8000c040亮灯,0x80004040灭灯

129.4.3 IO命令点灯测试

默认GPIO0_B7是GPIO模式,然后输入以下命令将方向寄存器设置为输出。

io -w -4 0xFDD60008 0x80008044

接下来设置GPIO是输出高电平还是低电平,首先查看数据寄存器的值,输入以下命令:

io -r -4 0xFDD60000

图129-37

给数据寄存器写入0x80008040输出高电平,灯亮。

io -w -4 0xFDD60000 0x8000c040

给数据寄存器写入0x80008040输出高电平,灯灭。

io -w -4 0xFDD60000 0x80004040

129.5 通过mem设备控制GPIO

在上一节中,我们学习了如何通过输入输出命令(IO)来控制LED灯的GPIO寄存器,实现了简单的点灯效果。这种方法通过访问设备文件来直接读写寄存器,实现了对硬件的低级控制。

然而,在某些情况下,我们可能无法直接使用IO命令来访问GPIO寄存器,或者希望使用更高级的抽象来控制硬件。这时,可以使用/dev/mem设备来操作物理内存,以实现对GPIO寄存器的访问。

通过打开/dev/mem设备文件,并将其映射到用户空间的内存中,我们可以直接读写物理内存地址,从而实现对GPIO寄存器的控制。这种方法相对于IO命令更加灵活,可以使用更高级的编程语言(如C/C++)来编写控制逻辑。

在本小节中,我们将继续使用C语言,并通过/dev/mem设备来控制GPIO寄存器,实现LED灯的点灯效果。通过使用/dev/mem设备进行GPIO控制,我们可以更加灵活地操作硬件,并且能够使用更高级的编程语言和工具来进行开发和调试。接下来,让我们开始学习如何使用/dev/mem设备来控制GPIO,进一步扩展我们的硬件控制能力。

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

在Linux系统中,用户态可以通过多种方式访问内核态,包括:

1. 通过read/write/ioctl:使用这种方式,用户态程序可以通过读写文件描述符或使用ioctl系统调用与内核进行通信。例如,可以通过读写特定文件描述符来控制设备或获取设备状态。

2. 通过sysfs虚拟文件系统:sysfs是一种以文件的形式表示设备和内核信息的虚拟文件系统。通过在sysfs中的特定路径下读写文件,用户态程序可以与内核进行交互,例如控制GPIO引脚或获取系统信息。

3. 通过内存映射:内存映射是将用户空间的一段内存区域映射到内核空间的一种机制。通过内存映射,用户态程序可以直接修改内存区域的内容,从而与内核进行通信。这种方式可以实现高效的数据传输和共享。

4. 通过Netlink:Netlink是Linux内核提供的一种通信机制,用于用户态程序与内核之间的双向通信。通过创建Netlink套接字,用户态程序可以与内核进行交互,发送请求、接收事件通知等。这种方式适用于需要与内核进行复杂交互的场景,例如配置系统参数或发送命令。

这些方法提供了不同的方式和接口,用户态程序可以根据具体需求选择适合的方法与内核进行通信。

129.5.2 /dev/mem设备

/dev/mem是Linux系统中的一个虚拟设备,通常与mmap结合使用,可以将设备的物理内存映射到用户态,以实现用户空间对内核态的直接访问。无论是标准Linux系统还是嵌入式Linux系统,都支持使用/dev/mem设备。

然而,直接访问内核空间是一项潜在危险的操作,因此只有root用户才能访问/dev/mem设备。此外有些系统可能需要单独启动/dev/mem设备的功能。配置启动/dev/mem设备方法如下所示:

在Linux源码内核中配置以下选项。

Device Drivers --->

Character devices--->

[*] /dev/mem virtual device support

RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验_第19张图片

图129-38

在上一小节中,我们讲解了IO命令,IO命令实际上就是基于/dev/mem设备实现的。如果Linux内核源码没有配置支持/dev/mem,IO命令是不能使用的。

129.5.3 /dev/mem设备的使用方法。

使用/dev/mem设备需要具有root权限,并且谨慎操作,因为直接访问内核空间是一项潜在的危险操作。以下是使用/dev/mem设备的基本步骤:

步骤一:

使用open函数打开"/dev/mem"文件描述符,并指定访问权限和阻塞方式。访问权限可以是只读(O_RDONLY)、只写(O_WRONLY)或读写(O_RDWR)阻塞方式或非阻塞(O_NDELAY)。

int fd = 0;

fd = open("/dev/mem", O_RDWR | O_NDELAY); /* 读写权限,非阻塞 */

请注意,这里使用O_RDWR表示读写权限,并使用O_NDELAY表示非阻塞方式。你可以根据实际需求选择适当的访问权限和阻塞方式。

步骤二:

使用mmap函数将需要访问的物理地址与"/dev/mem"文件描述符建立映射。mmap函数将返回一个指向映射内存区域的指针。

char *mmap_addr = NULL;

mmap_addr = (char *)mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, MMAP_ADDR);

在这里,使用mmap函数将物理内存地址映射到mmap_addr指针所指向的内存区域。MMAP_SIZE表示映射的大小,PROT_READ | PROT_WRITE表示访问权限为读写,MAP_SHARED表示共享映射,fd是之前打开的/dev/mem文件描述符,MMAP_ADDR是要映射的物理地址。

步骤三:

对映射的地址进行访问,即对寄存器进行读写操作。

int a = 0;

*(int *)mmap_addr = 0xff; // 写地址

a = *(int *)mmap_addr; // 读地址

在这里,使用指针操作对mmap_addr指向的地址进行读写操作。*(int *)mmap_addr表示将mmap_addr解释为int类型的指针,对于写操作,将0xff写入该地址;对于读操作,将地址的值读取到变量a中。

通过上述三个步骤,我们可以使用/dev/mem设备。接下来我们来了解下mmap函数。

129.5.4 mmap函数

mmap函数解释如下所示:

函数原型:

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

函数参数:

start: 指定文件应被映射到进程空间的起始地址 ,一般被指定为一个空指针,选择起始地址的任务留给内核来完成。映射成功之后,函数返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

length: 是映射到调用进程地址空间的字节数。

prot: 参数指定共享内存的访问权限。可取如下几个值的或。PROT_READ(映射区域可读)、PROT_EXEC(映射区域可执行)、PROT_WRITE(映射区域可写)、PROT_NONE(映射区域不可访问)。

flags: 由以下几个常值指定,MAP_SHARED,MAP_PRIVATE,MAP_FIXED,其中MAP_SHARED,MAP_PRIVATE必选其一,MAP_FIXED不推荐使用。

fd: 有效的文件描述符。一般是由open()函数返回。

offset: 文件映射的偏移量,offset的大小必须是页的整数倍,如果设备为0代表从文件最前方开始映射。

函数返回值:成功执行时,mmap()返回被映射区的指针,失败时,mmap()返回-1.

129.5.5 LED灯实验

本小节代码在配套资料“iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\85_gpioctrl04”目录下。

实验要求:

通过编写mem设备控制GPIO(LED灯)的应用程序实现LED灯闪烁的效果。

实验硬件连接:

使用迅为iTOP-RK3568开发板上的用户灯LED9。

实验步骤:

首先进入ubuntu的终端界面输入以下命令来创建 gpioctrl.c文件,如下图所示:

图129-39

然后向该文件中添加以下内容:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH

aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

图129-40

最后将交叉编译生成的gpioctrl文件拷贝到开发板目录下运行即可。

chmod 777 gpioctrl

./gpioctrl 15

图129-41

程序运行之后,开发板上的用户灯LED实现了闪烁的效果。

你可能感兴趣的:(RK3568驱动开发指南,#,RK3568驱动指南,第十二期,驱动开发,linux)