Linux驱动开发——LED驱动开发


系列文章
Linux驱动开发——字符设备驱动开发

1 概述

1.1 说明

本文是学习rk3568开发板驱动开发的记录,代码依托于rk3568开发板。
Linux下外设驱动,最终都是配置相应的硬件寄存器,本文中的LED灯驱动也是对rk3568的io口进行配置。

2 基础知识

2.1 地址映射

在编写驱动之前,需要先了解一下MMU,MMU(Memory Manage Unit),是内存管理单元,老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能有:

  1. 完成虚拟空间到物理空间的映射
  2. 内存保护,设置寄存器的访问呢权限,设置虚拟存储空间的缓冲特性
    首先看第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 0

int 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.o

build: kernel_modules

kernel_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灯。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值