Linux驱动开发——LED驱动开发
文章目录
-
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 验证驱动程序功能
专栏
1 概述
1.1 说明
本文是学习RK3568开发板驱动开发的笔记,并基于RK3568开发板进行编码。在Linux环境下配置外设驱动时会主要涉及配置相应的硬件寄存器,在本研究中提到的LED灯驱动主要涉及对RK3568 IO口的配置。
2 基础知识
2.1 地址映射
编写驱动之前应先熟悉MMU(Memory Management Unit),它是内存管理单元,在旧Linux版本通常要求处理器配备MMU功能;然而现代Linux内核已可兼容不带MMU的处理器。其主要职责包括实现地址转换、执行段保护策略以及管理虚拟内存机制以提升系统运行效率。
- 实现地址转换过程:完成虚拟空间到物理空间的映射。
- 内存保护措施:设定寄存器的数据访问权限,并定义虚拟存储区域的数据缓存特性。
首先理解两个基本概念:虚拟地址和物理地址。
对于32位处理器而言:
虚拟地址范围为 2^{32}=4GB(64位处理器则为 2^{64}=18.45 x 10^18 GB)。
这个范围远超32位处理器的能力,
显著提升了计算机的整体性能。
在开发板上,
比如我们使用的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);
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);
c
这些参数和返回值的含义如下:
- res_cookie:对应于计算机上运行该程序时所使用的物理起始地址。
- size:对应于计算机上运行该程序时所占用的内存空间大小。
- 返回值:一个指向... 区域内存位置的指针类型变量;其中该指针变量所指向的位置即为... 区域内存位置。
2.1.2 iounmap函数
在卸载驱动的过程中,通常会调用 iounmap 函数以解除占用由 ioremap 函数所造成的内存映射关系。
iounmap函数的原型如下:
void iounmap (volatile void __iomem *addr)
c
2.2 I/O内存访问函数
当外部寄存器或内存对应于I/O空间时,则被称为I/O端口;而当外部寄存器或内存对应于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)
c
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)
c
这三个函数分别对应着 8bit、16bit 和 32bit 的写操作,在程序中我们可以通过调用 writeb(value, addr) 来实现一个字节的数据 writes;调用 writew(value, addr) 来实现一个字的数据 writes;调用 writel(value, addr) 来实现一个四字节的数据 writes。其中 parameter value 表示待写的数值,在不同 bit 数量下其数据类型也会有所变化;parameter addr 则表示存储该数值的具体地址位置。
3 硬件原理图分析

该设备连接到 GPIO 引脚 0-C-Working_Ledn-H。
当 GPIO 引脚 C-Working-Ledn-H 状态变为高电平时, Q1 型晶体管就会导通,
该发光二极管将呈现绿色并点亮。
当 C-Working-Ledn-H 状态变为低电平时,
Q1 型晶体管将被关闭,
该发光二极管不会导通,
因此 LED 将无法点亮。
由此可知,
LED 的点亮与熄灭状态受其工作状态控制,
仅在接收高电压信号时会亮起,
反之则熄灭。
4 RK3568 GPIO驱动原理
4.1 引脚复用设置
rk3568的一个引脚通常会使用多个功能进行综合配置(也就是引脚复用功能)。其中IO0_C0端口可以配置为:GPIO、PWM1_M0、GPU_AVS以及UART0_RX四种不同的功能组合。这里采用的是GPIO接口来实现该功能的配置设置。rk3568芯片共有5组GPIO资源可用(这里指的是 GPIO0 这一组中的 C0 端口)。首先需要从芯片的数据手册或参考手册中找到对应寄存器的地址信息。

所有与GPIO接口相关的寄存器均归类于PMU_GRF区域。该区域用于执行相关操作并完成功能查询。其中的PMU模块负责执行电源管理功能。而GRF区域则被指定为通用寄存器区域。其基地址位于十六进制地址空间中的具体位置

我们可以看到这些涉及GPIOC的两个相关联的寄存器。其偏移地址分别为8和12。每个占用了四个字节的空间。较低的寄存器负责控制GPIO端子组中的C₀至C₃引脚的复用配置,而较高的寄存器则负责管理 GPIO端子组中 C₄ 至 C₇ 引脚的状态设置。

首先查看该寄存器的地址值为基址加上偏移地址之和即0xFDC20000 + 0x0010 = 0xFDC221C。
总共占用4个字节的空间其中每个字节包含8位二进制数据因此总计32位数据。
其中高位16位用于指定低位16位的数据是否启用当高位指定为启用时低位的数据才会被有效读取。
而低位16位则被划分为四组每组包含3个二进制位并保留最后一位作为备用不参与数据设置。
这样每组最多可以表示8种不同的复用功能因此总共有最多8种不同的复用模式可供选择。
以当前使用的GPIO引脚为例比如GPIO引脚X7C4E9E5这个引脚就支持最多5种不同的复用模式。
- 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这个寄存器中

该寄存器的存储位置为:FDC2\texttt{A}FF + 9\texttt{A} = FDC2\texttt{A}FF\texttt{A}
4.3 GPIO输入输出设置
该设备具备双向功能不仅可以接收信号还可以发送信号因此在本设计中将其中一个GPIO口配置为输出端口用于控制连接的LED指示灯状态。这两个寄存器的作用是配置整个GPIO资源的输入输出状态RK3568拥有五个独立的GPIO资源组从(GPIO-0到PIN-4)。其中前四个资源组(从A系列到D系列)各自包含八个通道(A0-A7等)每个通道又细分为两个部分:低16位和高16位两部分。

GPIO_SWPORT_DDR_H寄存器也是base+offset。其中基地址为:

对于GPIO0_C0来说,其对应寄存器地址为:0xFDD60000 + 0x000C = 0x
FDD6000C。

同样是32位寄存器,在其高16位字段中设置了控制低位寄存器各端点启用状态的功能。对应的低位字段负责配置GPIO各端口输入输出模式设置:当数值为" 1"时表明该端口被设为输出模式;当数值为" 0"时则表示该端口被设为输入模式。
4.4 GPIO引脚高低电平设置
GPIO引脚的高低电平配置与输入输出模式设定基于相同的原理实现;然而它们所使用的寄存器有所区别,并具体采用的是GPIO_SWPORT_DR_L 和GPIO_SWPORT_DR_H。

5 实验程序编写
5.1 配置头文件路径
目前开发版本导入的系统为Android 11而非Linux系统,在此项目中将基于Android 11系统的内核构建相应的支持代码因此本项目基于Android 11系统的内核构建相应的支持代码对于测试程序而言,则采用NDK工具链完成编译过程随后,在Visual Studio Code中设置项目相关的包含目录并配置CMakeLists.cmake以指定项目属性
{
"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
}
c

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");
c

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;
}
c

5.4 驱动测试
5.4.1 关闭心跳灯
目前系统中的led用作心跳灯,首先需要关闭才能进行本实验。
adb shell
echo none > /sys/class/leds/work/trigger
c
通过前述指令临时熄灭心跳灯,并限定其作用时间为当前启动周期;若需永久关闭,则需更新设备配置以完成相应操作
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
c

Makefile和代码在同一个目录中,在该目录中执行编译命令
make ARCH=arm64
c
即可编译出内核模块文件led.ko
接下来使用ndk编译应用层程序
/home/alientek/code/android-ndk-r27/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang ledApp.c -o ledApp
c
通过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
c
完成模块加载,并建立相应的设备节点即可通过应用程序来进行测试。
chmod +x ./ledApp
./ledApp /dev/led 1
./ledApp /dev/led 0
c
输入1是打开led灯,输入0是关闭led灯。
