文章目录
- 1 概述
- 1.1 说明
- 2 基础知识
- 2.1 地址映射
- 2.1.1 ioremap函数
- 2.1.2 iounmap函数
- 2.2 I/O内存访问函数
- 2.2.1 读操作函数
- 2.2.2 写操作函数
- 3 硬件原理图分析
- 4 RK3568 GPIO驱动原理
- 4.1 引脚复用设置
- 4.2 引脚驱动能力配置
- 4.3 GPIO输入输出设置
- 4.4 GPIO引脚高低电平设置
- 5 实验程序编写
- 5.1 配置头文件路径
- 5.2 驱动代码
- 5.3 测试应用程序
- 5.4 驱动测试
- 5.4.1 关闭心跳灯
- 5.4.2 编写Makefile并编译
- 5.4.3 测试驱动程序
系列文章
Linux驱动开发——字符设备驱动开发
1 概述
1.1 说明
本文是学习rk3568开发板驱动开发的记录,代码依托于rk3568开发板。
Linux下外设驱动,最终都是配置相应的硬件寄存器,本文中的LED灯驱动也是对rk3568的io口进行配置。
2 基础知识
2.1 地址映射
在编写驱动之前,需要先了解一下MMU,MMU(Memory Manage Unit),是内存管理单元,老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能有:
- 完成虚拟空间到物理空间的映射
- 内存保护,设置寄存器的访问呢权限,设置虚拟存储空间的缓冲特性
首先看第1点,虚拟空间到物理空间的映射,也叫地址映射。首先了解两个地址概念:虚拟地址、物理地址。对于32位处理器来说,虚拟地址范围是 2^32=4GB(64 位的处理器则是 2^64=18.45 x 10^18 GB,即从 0 到 2^64-1 的范围。这个地址范围比 32 位处理器的地址范围要大得多,可以支持更大的内存空间,提高了计算机的性能)。例如我们的开发板上有 1GB 的 DDR3,这 1GB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,如下图所示:
物理内存只有1G,虚拟内存有4G,肯定存在多个虚拟地址映射同一个物理地址空间,这个会由处理器进行处理。
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好之后CPU访问的都是虚拟地址。比如 RK3568 的 GPIO0_C0 引脚的 IO 复用寄存器 PMU_GRF_GPIO0C_IOMUX_L 物理地址为 0xFDC20010。如果没有开启 MMU 的话直接向 0xFDC20010)这个寄存器地址写入数据就可以配置 GPIO0_C0 的引脚的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0xFDC20010 这个地址写入数据了。我们必须得到 0xFDC20010 这个物理地址在Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。
2.1.1 ioremap函数
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中,定义如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
实现如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap);
这些参数和返回值的含义如下:
- res_cookie:要映射的物理起始地址。
- size:要映射的内存空间大小。
- 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
2.1.2 iounmap函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:
void iounmap (volatile void __iomem *addr)
2.2 I/O内存访问函数
当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
2.2.1 读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
2.2.2 写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要写入的数值,addr 是要写入的地址。
3 硬件原理图分析
LED 接到了 GPIO0_C0(WORKING_LEDN_H)上,当 GPIO0_C0 输出高电平(1)的时候 Q1 这个三极管就能导通,LED (DS1)这个绿色的发光二极管就会点亮。当GPIO0_C0 输出低电平(0)的时候 Q1 这个三极管就会关闭,发光二极管 LED (DS1)不会导通,因此 LED 也就不会点亮。所以 LED 的亮灭取决于 GPIO0_C0 的输出电平,输出 1 就亮,输出 0 就灭。
4 RK3568 GPIO驱动原理
4.1 引脚复用设置
rk3568的一个引脚一般用多个功能,也就是引脚复用,比如 GPIO0_C0 这个 IO 就可以用作:GPIO,PWM1_M0,GPU_AVS 和 UART0_RX 这四个功能,这里使用的是GPIO功能。
rk3568芯片有5组GPIO,这里使用的是GPIO0这组中的C0这个端口,首先从芯片的参考手册中,找到对应寄存器的地址。
所有GPIO相关的寄存器都属于PMU_GRF,查询,其中的PMU是电源管理模块,GRF是通用寄存器。基地址是0xFDC20000。
这个就是GPIOC相关的两个寄存器,偏移地址是0x0010和0x0014,大小是4个字节。低寄存器控制GPIO0中的C0-C3这4个引脚的复用,高寄存器控制C4-C7这4个引脚的复用。
首先看该寄存器的地址,由基址+偏移地址,0xFDC20000+0x0010=0xFDC20010。
一共4字节,32位,其中的高16位是对于低16位的使能位,0对应16,15对应31。只有高16位对应的使能之后,低16位的设置才生效。
低16位,分4组,每组3位加1位预留,用于表示对应GPIO引脚的复用功能。3个bit可以表示8种功能,这里最多的是5种复用功能。
以现在要使用的GPIO0_C0引脚为例,该引脚有4种复用功能:
- 0 :GPIO0_C0
- 1 :PWM1_M0
- 2:GPU_AVS
- 3 :UART0_RX
如果要使用GPIO功能,则需要配置bit2:0为000,bit18:16为111,对应的这四个字节为0x00070000。
4.2 引脚驱动能力配置
引脚驱动能力的配置,在PMU_GRF_GPIOC_DS_0这个寄存器中
该寄存器的地址是:0xFDC20000 + 0x0090 = 0xFDC20090
该寄存器也分为两部分,高16位是对于低16位的使能位,低16位是驱动能力配置。0-5这6个bit用于定义C0的驱动能力,8-13这6个bit用于定义C1的驱动能力。
驱动能力一共有6级,这里设置为5级,具体原因不太清楚,可能跟硬件参数相关。
那么0-5设置为111111,同时需要设置使能位16-21为111111
4.3 GPIO输入输出设置
GPIO是双向的,既可以做输入,也可以做输出。这里是使用GPIO口来控制LED的亮灭,因此这里设置成输出。GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 这两个寄存器用于设置 GPIO 的输入输出功能。RK3568 一共有 GPIO0、GPIO1、GPIO2、GPIO3 和GPIO4 这五组 GPIO。其中 GPIO0-3 这四组每组都有 A0-A7、B0-B7、C0-C7 和 D0~D7 这 32个 GPIO。每个 GPIO 需要一个 bit 来设置其输入输出功能,一组 GPIO 就需要 32bit,GPIO_SWPORT_DDR_L 和GPIO_SWPORT_DDR_H 这两个寄存器就是用来设置这一组 GPIO所 有 引 脚 的 输 入 输 出 功 能 的 。 其 中 GPIO_SWPORT_DDR_L 设 置 的 是 低 16bit ,GPIO_SWPORT_DDR_H 设置的是高 16bit。一组GPIO引脚对应如下:
GPIO_SWPORT_DDR_H寄存器也是base+offset。其中基地址为:
对于GPIO0_C0来说,其对应寄存器地址为:0xFDD60000 + 0x000C = 0x
FDD6000C。
也是32位寄存器,其中高16位是控制低16位的使能位。低16位对应GPIO的16个端口的输入输出设置,1是输出,0是输入。
4.4 GPIO引脚高低电平设置
GPIO的引脚高低电平设置和输入输出设置的原理是一样的,只是使用的寄存器不同,使用的是GPIO_SWPORT_DR_L 和GPIO_SWPORT_DR_H。
5 实验程序编写
5.1 配置头文件路径
目前开发版刷入的版本不是Linux系统,是Android11系统,所以这里依赖的是Android11系统的kernel头文件,测试程序的编译则使用ndk工具进行编译
首先在vscode中配置头文件依赖路径,配置c_cpp_properties.json文件
{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/alientek/code/atk-rk3568-11/kernel/arch/arm64/include","/home/alientek/code/atk-rk3568-11/kernel/include","/home/alientek/code/atk-rk3568-11/kernel/arch/arm64/include/generated"],"defines": [],"compilerPath": "/usr/bin/gcc","cStandard": "c17","cppStandard": "gnu++17","intelliSenseMode": "linux-gcc-x64"}],"version": 4
}
5.2 驱动代码
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define LED_MAJOR 200
#define LED_NAME "led"#define LEDOFF 0
#define LEDON 1#define PMU_GRF_BASE 0xFDC20000
#define PMU_GRF_GPIO0C_IOMUX_L (PMU_GRF_BASE + 0x0010)
#define PMU_GRF_GPIO0C_DS_0 (PMU_GRF_BASE + 0x0090)#define GPIO0_BASE 0xFDD60000
#define GPIO0_SWPORT_DR_H (GPIO0_BASE + 0x0004)
#define GPIO0_SWPORT_DDR_H (GPIO0_BASE + 0x000C)static void __iomem* PMU_GRF_GPIO0C_IOMUX_L_PI;
static void __iomem* PMU_GRF_GPIO0C_DS_0_PI;
static void __iomem* GPIO0_SWPORT_DR_H_PI;
static void __iomem* GPIO0_SWPORT_DDR_H_PI;void led_switch(u8 sta)
{u32 val = 0;if (sta == LEDON){val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0x01 << 0);val |= ((0x01 << 16) | (0x1 << 0));writel(val, GPIO0_SWPORT_DR_H_PI);}else if (sta == LEDOFF){val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0x01 << 0);val |= ((0x01 << 16) | (0x0 << 0));writel(val, GPIO0_SWPORT_DR_H_PI);}
}void led_remap(void)
{PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);PMU_GRF_GPIO0C_DS_0_PI = ioremap(PMU_GRF_GPIO0C_DS_0, 4);GPIO0_SWPORT_DR_H_PI = ioremap(GPIO0_SWPORT_DR_H, 4);GPIO0_SWPORT_DDR_H_PI = ioremap(GPIO0_SWPORT_DDR_H, 4);
}void led_unmap(void)
{iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);iounmap(PMU_GRF_GPIO0C_DS_0_PI);iounmap(GPIO0_SWPORT_DR_H_PI);iounmap(GPIO0_SWPORT_DDR_H_PI);
}static int led_open(struct inode* inode, struct file* flip)
{return 0;
}static ssize_t led_read(struct file* flip, char __user* buf, size_t cnt, loff_t* offt)
{return 0;
}static ssize_t led_write(struct file* flip, const char __user* buf, size_t cnt, loff_t* offt)
{int retValue;unsigned char databuf[1];unsigned char ledstat;retValue = copy_from_user(databuf, buf, cnt);if (retValue < 0){printk("kernel write failed!\r\n");return EFAULT;}ledstat = databuf[0];printk("ledstat = %d", ledstat);if (ledstat == LEDON){led_switch(LEDON);}else if (ledstat == LEDOFF){led_switch(LEDOFF);}return 0;
}static int led_release(struct inode* inode, struct file* flip)
{return 0;
}static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.release = led_release,.read = led_read,.write = led_write,
};static int __init led_init(void)
{int retValue = 0;u32 val = 0;led_remap();val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);val &= ~(0x7 << 0);val |= ((0x7 << 16) | (0x0 << 0));writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);val = readl(PMU_GRF_GPIO0C_DS_0_PI);val &= ~(0x3F << 0);val |= ((0x3F << 16) | (0x3F << 0));writel(val, PMU_GRF_GPIO0C_DS_0_PI);val = readl(GPIO0_SWPORT_DDR_H_PI);val &= ~(0x1 << 0);val |= ((0x1 << 16) | (0x1 << 0));writel(val, GPIO0_SWPORT_DDR_H_PI);val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0x1 << 0);val |= ((0x1 << 16) | (0x0 << 0));writel(val, GPIO0_SWPORT_DR_H_PI);retValue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);if (retValue < 0) {printk("register chrdev failed!\r\n");goto fail_map;}return 0;fail_map:led_unmap();return EIO;
}static void __exit led_exit(void) {led_unmap();unregister_chrdev(LED_MAJOR, LED_NAME);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
5.3 测试应用程序
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"#define LEDON 1
#define LEDOFF 0int main(int argc, char *argv[]) {int fd, retValue;char *fileName;unsigned char deataBuf[1];if (argc != 3) {printf("arg num error!\r\n");return -1;}fileName = argv[1];fd = open(fileName, O_RDWR);if (fd < 0) {printf("open dev failed!\r\n");return -1;}printf("open dev success");deataBuf[0] = atoi(argv[2]);retValue = write(fd, deataBuf, 1);if (retValue < 0) {printf("write to dev failed!\r\n");return -1;}retValue = close(fd);if (retValue < 0) {printf ("close fd failed!\r\n");return -1;}return 0;
}
5.4 驱动测试
5.4.1 关闭心跳灯
目前系统中的led用作心跳灯,首先需要关闭才能进行本实验。
adb shell
echo none > /sys/class/leds/work/trigger
使用以上命令暂时关闭心跳灯,持续周期为本次启动,永久关闭心跳灯需要修改设备树。
5.4.2 编写Makefile并编译
然后编写Makefile文件
KERNELDIR := /home/alientek/code/atk-rk3568-11/kernel
CURRENT_PATH := $(shell pwd)
obj-m := led.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
Makefile和代码在同一个目录中,在该目录中执行编译命令
make ARCH=arm64
即可编译出内核模块文件led.ko
接下来使用ndk编译应用层程序
/home/alientek/code/android-ndk-r27/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang ledApp.c -o ledApp
使用ndk中的aarch64-linux-android30-clang工具编译,编译后产生ledApp可执行文件
5.4.3 测试驱动程序
将led.ko和ledApp推到设备中,其中led.ko推到vendor/lib/modules目录,ledApp可以随意找个目录
接下来是加载模块
adb shell
cd /vendor/lib/modules
insmod led.ko
mknod /dev/led c 200 0
加载完模块,并创建对应的设备节点之后,就可以通过应用程序进行测试了。
chmod +x ./ledApp
./ledApp /dev/led 1
./ledApp /dev/led 0
输入1是打开led灯,输入0是关闭led灯。