RK3568学习笔记12:LED驱动开发实验

Dr.Guo
发布于 2024-01-20 / 78 阅读
0
0

RK3568学习笔记12:LED驱动开发实验

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系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremapiounmap

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_Lbit2: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_LGPIO_SWPORT_DDR_H这 两个寄存器用于设置 GPIO的输入输出功能。

RK3568一共有 GPIO0GPIO1GPIO2GPIO3GPIO4这五组 GPIO。 其中 GPIO0~3这四组每组都有 A0~A7B0~B7C0~C7D0~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");

评论