1. LED灯驱动原理
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的LED 灯驱动最终也是对RK3568 的IO 口进行配置,与裸机实验不同的是,在Linux 下编写驱动要符合Linux的驱动框架。开发板上的LED 连接到RK3568 的GPIO0_C0 这个引脚上,因此本章实验的重点就是编写Linux 下RK3568 引脚控制驱动。
1.1. MMU地址映射
MMU 全称叫做Memory Manage Unit,也就是内存管理单元。在老版本的Linux 中要求处理器必须有MMU,但是现在Linux 内核已经支持无MMU 的处理器了。
MMU的功能如下:
- 完成虚拟空间到物理空间的映射
- 内存保护,设置寄存器的访问权限,设置虚拟存储空间的缓冲特性
地址映射:首先了解两个地址概念:
虚拟地址
(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于32位的处理器来说,虚拟地址范围$2^32=4GB$(64 位的处理器则是2^64=18.45 x 10^18 GB,即从 0 到 2^64-1 的范围。这个地址范围比 32 位处理器的地址范围要大得多,可以支持更大的内存空间,提高了计算机的性能)。开发板上有1GB 的DDR3,这1GB 的内存就是物理内存,经过MMU 可以将其映射到整个4GB 的虚拟空间,
如图所示:
物理内存只有 1GB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究。
Linux内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU访问的都是虚拟地址。比如 RK3568的 GPIO0_C0引脚的 IO复用 寄存器PMU_GRF_GPIO0C_IOMUX_L 物理地址为 0xFDC20010。如果没有开启 MMU的话直接向 0xFDC20010)这个寄存器地址写入数据就可以配置 GPIO0_C0的引脚的复用功能 。现在开启了 MMU,并 且设置了内存映射,因此就不能直接向 0xFDC20010这个地址写入数据了。我们必须得到 0xFDC20010这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap
和 iounmap
。
ioremap 函数
ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文件中,定义如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
函数的实现是在 arch/arm/mm/ioremap.c文件中,实现如下:
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类型的指针,指向映射后的虚拟空间首地址。
要获取 RK3568的 PMU_GRF_GPIO0C_IOMUX_L寄存器对应的虚拟地址,使用如下代码:
#define PMU_GRF_GPIO0C_IOMUX_L (0xFDC20010)
static void __iomem* PMU_GRF_GPIO0C_IOMUX_L_PI; PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);
宏 PMU_GRF_GPIO0C_IOMUX_L是寄存器物理地址,PMU_GRF_GPIO0C_IOMUX_L_PI是映射后的虚拟地址。对于 RK3568来说一个寄存器是 4字节 (32位 ),因此映射的内存长度为4。映射完成以后直接对PMU_GRF_GPIO0C_IOMUX_L_PI 进行读写操作即可。
iounmap函数
卸载驱动的时候需要使用 iounmap函数释放掉 ioremap函数所做的映射,定义如下:
void iounmap (volatile void __iomem *addr)
iounmap
只有一个参数 addr
,此参数就是要取消映射的虚拟地址空间首地址。
1.2 I/O内存访问函数
这里说的 I/O是输入 /输出的意思,并不是我们学习单片机的时候讲的 GPIO引脚。这里涉及到两个概念: I/O端口和 I/O内存。
当外部寄存器或内存映射到 IO空间时,称为 I/O端口。当外部寄存器或内存映射到内存空间时,称为 I/O内存。但是对于 ARM来说没有 I/O空间这个概念,因此 ARM体系下只有 I/O内存 (可以直接理解为内存 )。使用 ioremap函数将寄存器的物
理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读内存函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
写内存函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
2. 硬件原理图
开发板上的LED等硬件原理如图:
可以看出, LED 接到了 GPIO0_C0(WORKING_LEDN_H)上,当 GPIO0_C0输
出 高 电平 (1)的时候 Q1这个三极管就能导通, LED (DS1)这个 绿 色的发光二极管就会点亮。 当GPIO0_C0输出低电平 (0)的时候 Q1这个三极管就会关闭, 发光二极管 LED (DS1)不会导通,
因此 LED 也就不会点亮。所以 LED 的亮灭取决于 GPIO0_C0的输出电平,输出 1就亮,输出 0就灭。
3 RK3568 GPIO驱动原理
3.1 引脚复用设置
RK3568的一个引脚一般用多个功能,也就是引脚复用,比如 GPIO0_C0这个 IO就可以用作: GPIO PWM1_M0 GPU_AVS和 UART0_RX这 四 个功能,这里要设置位CPIO0功能。
在 03、核心板资料\03、核心板板载芯片资料
中打开Rockchip RK3568 TRM Part1 V1.1-20210301.pdf文件,找到 PMU_GRF_GPIO0C_IOMUX_L
寄存器
从图 6.3.1.1可以看出 PMU_GRF_GPIO0C_IOMUX_L寄存器地址为: base+offset,其中 base就是 PMU_GRF外设的基地址,为 0xFDC20000 offset为 0x0010,所以PMU_GRF_GPIO0C_IOMUX_L寄存器地址为0xFDC20000+0x0010=0xFDC20010。
可以看出:PMU_GRF_GPIO0C_IOMUX_L寄存器分为2部分:
bit31:16
低 16位写使能位,这 16个 bit控制着寄存器的低 16位写使能。比如 bit16就对应着 bit0的写使能,如要要写 bit0,那么 bit16要置 1,也就是允许对 bit0进行写操作。
bit15:0
功能设置位
bit2:0用于设置 GPIO0_C0的复用功能,有四个可选功能:
- 0 GPIO0_C0
- 1 PWM1_M0 2 GPU_AVS
- 3 UART0_RX
将GPIO0_C0设置为 GPIO,所以 PMU_GRF_GPIO0C_IOMUX_L
的 bit2:0
这 三 位设置 000
。另外 bit18:16
要设置为 111
,允许写 bit2:0
。
3.2 引脚驱动能力设置
RK3568的 IO引脚可以设置不同的驱动能力, GPIO0_C0的驱动能力设置寄存器为
PMU_GRF_GPIO0C_DS_0
,如下图
PMU_GRF_GPIO0C_DS_0
寄存器地址为:base+offset=0xFDC20000+0X0090=0xFDC20090。
PMU_GRF_GPIO0C_DS_0
寄存器也分为 2部分:
bit31:16
低 16位写使能位,这 16个 bit控制着寄存器的低 16位写使能。比如 bit16就对应着 bit15:0的写使能,如要要写 bit15:0,那么 bit16要置 1,也就是允许对 bit15:0进行写操作。
bit15:0
功能设置位。
可以看出, PMU_GRF_GPIO0C_DS_0寄存器用于设置 GPIO0_C0~C1这 2个 IO的驱动能力,其中 bit5:0用于设置 GPIO0_C0的驱动能力,一共有 6级。
GPIO0_C0的驱动能力设置为 5级,所以 GRF_GPIO3D_DS_H的 bit5:0这六 位设置 111111。另外 bit21:16要设置为 111111,允许写 bit5:0。
3.3 GPIO输入输出
GPIO是双向的,也就是既可以做输入,也可以做输出。本章我们使用 GPIO0_C0
来控制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里面这 32给引脚对应的 bit如下所示:
GPIO0_C0很明显要用到 GPIO_SWPORT_DDR_H寄存器, 寄存器描述如下图所示
GPIO_SWPORT_DDR_H寄存器地址也是 base+offset,其中 GPIO0~GPIO4的基地址如下
所以 GPIO0_C0对应的 GPIO_SWPORT_DDR_H基地址就是0xFDD60000+0X000C=0X FDD6000C。
GPIO_SWPORT_DDR_H寄存器也分为 2部分:
bit31:16
低 16位写使能位,这 16个 bit控制着寄存器的低 16位写使能。比如 bit16就对应着 bit0的写使能,如要要写 bit0,那么 bit16要置 1,也就是允许对 bit0进行写操作。
bit15:0
功能设置位。
GPIO0_C0设置为输出,所以 GPIO_SWPORT_DDR_H
的 bit0要置 1,另外 bit16要设置为 1,允许写 bit16。
3.4 GPIO引脚高低电平设置
GPIO配置好以后就可以控制引脚输出高低电平了,需要用到 GPIO_SWPORT_DR_L和
GPIO_SWPORT_DR_H这两个寄存器,原理与上述几个寄存器相同。
GPIO0_C0需要用到 GPIO_SWAPORT_DR_H寄存器,寄存器描述如下图
同样的, GPIO0_C0
对应 bit0,如果要输出低电平,那么 bit0置 0,如果要输出高电平 bit0置 1。 bit16也要置 1,允许写 bit0
4. 实验程序
新建led文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
//#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : led.c
作者 : 正点原子
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/02 正点原子团队创建
***************************************************************/
#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;
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,
bit0,高电平*/
writel(val, GPIO0_SWPORT_DR_H_PI);
}else if(sta == LEDOFF) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,
bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
}
}
/*
* @description : 物理地址映射
* @return : 无
*/
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);
}
/*
* @description : 取消映射
* @return : 无
*/
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);
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, 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]; /* 获取状态值 */
if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* 初始化LED */
/* 1、寄存器地址映射 */
led_remap();
/* 2、设置GPIO0_C0为GPIO功能。*/
val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);
val &= ~(0X7 << 0); /* bit2:0,清零 */
val |= ((0X7 << 16) | (0X0 << 0)); /* bit18:16 置1,允许写bit2:0,
bit2:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);
/* 3、设置GPIO0_C0驱动能力为level5 */
val = readl(PMU_GRF_GPIO0C_DS_0_PI);
val &= ~(0X3F << 0); /* bit5:0清零*/
val |= ((0X3F << 16) | (0X3F << 0)); /* bit21:16 置1,允许写bit5:0,
bit5:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_DS_0_PI);
/* 4、设置GPIO0_C0为输出 */
val = readl(GPIO0_SWPORT_DDR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,
bit0,高电平 */
writel(val, GPIO0_SWPORT_DDR_H_PI);
/* 5、设置GPIO0_C0为低电平,关闭LED灯。*/
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,
bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
/* 6、注册字符设备驱动 */
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;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
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");