字符设备点亮led灯实验rk3568
笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正!!!
裸机驱动开发与带有操作系统的驱动开发最大的区别是是否要单一的操作寄存器还是说要符合操作系统的胃口进行设计。
带有操作系统的驱动开发势必要求设备驱动附加更多的代码和功能,把单一的驱动变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API。
一、内存管理单元mmu
流程:
当没有启用MMU的时候,CPU在读取指令或者访问内存时便会将地址直接输出到芯片的引脚上,此地址直接被内存接收,这段地址称为物理地址, 如下图所示。

简单地说,物理地址就是内存单元的绝对地址,好比你电脑上插着一张8G的内存条,则第一个存储单元便是物理地址0x0000, 内存条的第6个存储单元便是0x0005,无论处理器怎样处理,物理地址都是它最终的访问的目标。
当CPU开启了MMU时,CPU发出的地址将被送入到MMU,被送入到MMU的这段地址称为虚拟地址, 之后MMU会根据去访问页表地址寄存器(这个寄存器是在cpu内部的,为 MMU 提供页表在物理内存中的起始地址)然后去内存中找到页表(假设只有一级页表)的条目,从而翻译出实际的物理地址, 如下图所示。

作用:
- 保护内存
- 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换
- 突破物理内存限制,如一个程序运行要10gb,但是真实内存只有4gb依旧跑的起来,因为mmu对不使用的部分进行了换出换入。操作系统会把暂时用不到的物理页保存到硬盘的交换空间(虚拟内存文件)中,释放物理内存给当前需要的程序使用;当程序需要访问这些页时,操作系统再把它们从硬盘加载回物理内存(这个过程叫缺页异常处理)。
- Mmu在现代计算机中是集成在cpu内部。
二、快表tlb
上述说到mmu是通过页表查找到物理地址的,但是假如有多层页表的时候,如果没有tlb,,MMU 每次进行虚拟地址到物理地址的转换,都必须直接访问内存中的页表,会带来显著的性能下降和总线带宽浪费,影响如下:
- 地址转换延迟暴增,CPU 效率大幅降低。因为内存的访问延迟远高于 CPU 内部硬件(比如寄存器、MMU 电路)的操作延迟,两者相差数百倍甚至上千倍。
- 内存总线带宽被大量占用。因为内存总线是 CPU 和内存之间传输数据的 “通道”,带宽是有限的。(本人操作系统有点摆以前,建议大家去豆包一下就知道了,把计算过程看一下立马懂)
- 多级页表的优势几乎完全丧失。操作系统引入多级页表的核心目的,是节省页表本身占用的内存空间(按需分配页表,而非一次性分配完整的一级页表)。但多级页表的代价是增加了内存访问次数,这个代价原本是靠 TLB 来抵消的 ——TLB 命中时可以跳过所有页表访问步骤。
- 权限检查效率同步下降。TLB 中不仅缓存了虚拟页→物理页的映射关系,还会缓存页的权限信息(读 / 写 / 执行、内核态 / 用户态访问权限)。
有了tlb之后,在CPU传出一个虚拟地址时,MMU最先访问TLB,假设TLB中包含可以直接转换此虚拟地址的地址描述符(页表项之类的), 则会直接使用这个地址描述符检查权限和地址转换,如果TLB中没有这个地址描述符, MMU才会去访问页表并找到地址描述符之后进行权限检查和地址转换, 然后再将这个描述符填入到TLB中以便下次使用,实际上TLB并不是很大, 那TLB被填满了怎么办呢?如果TLB被填满,则会使用round-robin算法找到一个条目并覆盖此条目。
地址转换函数:包括ioremap()地址映射和取消地址映射iounmap()函数。用于实现物理地址到虚拟地址的转换。
![]()
ioremap函数:
paddr: 被映射的IO起始地址(物理地址);
size: 需要映射的空间大小,以字节为单位;(一般size填写paddr对应寄存器的字节,如32位寄存器则填写4字节)
返回值: 一个指向__iomem类型的指针,当映射成功后便返回一段虚拟地址空间的起始地址,我们可以通过访问这段虚拟地址来实现实际物理地址的读写操作。(__iomem的作用是告诉程序员,这个是用于区分 “普通内存指针” 和 “外设寄存器的内存映射 IO 地址指针”,也称mmio。普通内存地址是可以直接解引用读写的,但是寄存器内存映射io地址是不可以直接解引用指针读写的,必须用readl/writel等函数。并且__iomem指针的地址来自ioremap,用完必须用iounmap释放。之所以不可以直接解引用是因为会不符合硬件时序要求)
需要ioremap函数的原因:在 ARM/ARM64 架构(如 RK3568)中,CPU 有内存管理单元(MMU),要求所有地址访问都必须是虚拟地址,而外设寄存器的地址是物理地址(如 0xFE660000),无法直接访问。所以ioremap 的核心作用是把外设寄存器的物理地址,在 CPU 的内存管理单元(MMU)中建立与内核虚拟地址的映射关系,并返回这段虚拟地址的起始指针(__iomem 标记)。最终效果是:你操作这段虚拟地址,就等同于操作对应的物理地址,但这个过程会遵循外设访问的硬件规则。

iounmap函数:
addr: 需要取消ioremap映射之后的起始地址(虚拟地址)。
返回值: 无。
我们先按照野火教程rk3568鲁班猫2的走一遍如何点亮板卡的led
我们先根据原理图找到对应的led,发现是由gpio0_c7驱动的,低电平则点亮,反之。

对于LED灯的控制进行控制,也就是对上述GPIO的寄存器进行读写操作。可大致分为以下几个步骤:
1、使能GPIO时钟(默认开启,不用设置)
2、设置引脚复用为GPIO(复位默认为GPIO,不用配置)
3、设置引脚属性(上下拉、速率、驱动能力,默认)
4、控制GPIO引脚为输出,并输出高低电平
因为GPIO的时钟默认开启,引脚默认复用为GPIO,我们只需要配置GPIO的引脚输入输出模式及电平即可。
关于引脚电平控制:
参考Rockchip_RK35xx_TRM_Part1,GPIO_SWPORT_DR_L:低位引脚数据寄存器,设置高低电平,GPIO_SWPORT_DR_H:高位引脚数据寄存器,设置高低电平。


在rk3568种gpio是由控制器控制,一共4组控制器0123,每组控制器又有abcd4组,每个abcd组中又有8个gpio引脚,所以例如gpio0_c7则是gpio控制器0的c组的第七个脚,由此可知,一个gpio控制器有32个引脚。上述的这个gpio电平控制器是通用的,我们操作的时候只需要将基地址+偏移地址就可以操作gpio了,例如如果我想操控gpio0,那么基地址就是gpio0的地址,偏移地址则是手册表格中的偏移地址,还得看你想操控什么寄存器就填谁的。操作这些寄存器的时候,像gpio0_c7则是高16位,所以控制电平则是操作GPIO_SWPORT_DR_H这个寄存器。这个寄存器中高16位是控制是否允许某位读写,而低16位则是控制引脚电平。你对某个引脚电平操作的同时必须设置对应的读写位为1,否则操作无效。如果设置gpio0_c7为高电平,则不仅GPIO_SWPORT_DR_H的第7位(从0开始)为1,第7+16位也要为1。
关于引脚输入输出方向控制:

GPIO_SWPORT_DDR_L:低位引脚数据方向寄存器,控制输入或者输出。
GPIO_SWPORT_DDR_H:高位引脚数据方向寄存器,控制输入或者输出。
操作要点和电平控制一致。
关于引脚上下拉:


这个寄存器是控制引脚上下拉,操作的时候的基地址不是对应gpio控制器的地址了,而是pmu_grf的基地址。我们可以看到15和14位是针对gpio0c7的上下拉的,那么13到12就是针对gpio0c6的,以此类推。因为每个引脚的控制位占用两位所以读写控制位每个引脚也占用两位。Gpio0c7假如15和14位设置位weak1,则第31和30位都要设置为1才生效。
1、高阻态:上拉和下拉电阻的开关都断开,引脚相当于 “悬浮” 在电路中,既不被强制拉到高电平,也不被强制拉到低电平;相当于悬空了。
2、上拉(Weak 1):上拉电阻的开关闭合,引脚被弱电阻拉到高电平(无外部输入时,引脚为高);
3、下拉(Weak 0):下拉电阻的开关闭合,引脚被弱电阻拉到低电平(无外部输入时,引脚为低);
4、保留(2'b11):硬件未实现该功能,禁止使用。
以下是完整的rk3568驱动led代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define DEV_NAME "led_chrdev"
#define DEV_CNT (1)
#define GPIO0_BASE (0xfdd60000) //GPIO0的基地址
//一个寄存器32位,其中高16位都是写使能位,控制低16位的写使能;低16位对应16个引脚,控制引脚的输出电平
#define GPIO0_DR_L (GPIO0_BASE + 0x0000) // GPIO0的低十六位引脚的数据寄存器地址
#define GPIO0_DR_H (GPIO0_BASE + 0x0004) // GPIO0的高十六位引脚的数据寄存器地址
//一个寄存器32位,其中高16位都是写使能位,控制低16位的写使能;低16位对应16个引脚,控制引脚的输入输出模式
#define GPIO0_DDR_L (GPIO0_BASE + 0x0008) // GPIO0的低十六位引脚的数据方向寄存器地址
#define GPIO0_DDR_H (GPIO0_BASE + 0x000C) // GPIO0的高十六位引脚的数据方向寄存器地址
static dev_t devno;
struct class *led_chrdev_class;
struct led_chrdev {
struct cdev dev;
unsigned int __iomem *va_dr;
unsigned int __iomem *va_ddr;
unsigned int led_pin; // 引脚
};
static int led_chrdev_open(struct inode *inode, struct file *filp)
{
unsigned int val = 0;
struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);
filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);
printk("open\n");
// 设置输出模式
val = ioread32(led_cdev->va_ddr);
val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
val |= ((unsigned int)0X1 << (led_cdev->led_pin));
iowrite32(val,led_cdev->va_ddr);
//输出高电平
val = ioread32(led_cdev->va_dr);
val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
val |= ((unsigned int)0x1 << (led_cdev->led_pin));
iowrite32(val, led_cdev->va_dr);
return 0;
}
static int led_chrdev_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_chrdev_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
char ret = 0;
struct led_chrdev *led_cdev = (struct led_chrdev *)filp->private_data;
get_user(ret, buf);
val = ioread32(led_cdev->va_dr);
if (ret == '0'){
val |= ((unsigned int)0x01 << (led_cdev->led_pin+16));
val &= ~((unsigned int)0x01 << (led_cdev->led_pin)); /*设置GPIO引脚输出低电平*/
}
else{
val |= ((unsigned int)0x01 << (led_cdev->led_pin+16));
val |= ((unsigned int)0x01 << (led_cdev->led_pin)); /*设置GPIO引脚输出高电平*/
}
iowrite32(val, led_cdev->va_dr);
return count;
}
static struct file_operations led_chrdev_fops = {
.owner = THIS_MODULE,
.open = led_chrdev_open,
.release = led_chrdev_release,
.write = led_chrdev_write,
};
static struct led_chrdev led_cdev[DEV_CNT] = {
{.led_pin = 7}, // 偏移,GPIO0_C7偏移7位
};
static __init int led_chrdev_init(void)
{
int i = 0;
dev_t cur_dev;
unsigned int val = 0;
printk("led_chrdev init (lubancat2 GPIO0_C7)\n");
led_cdev[0].va_dr = ioremap(GPIO0_DR_H, 4); // 映射数据寄存器物理地址到虚拟地址,GPIO0_C7需要设置GPIO0_DR_H
led_cdev[0].va_ddr = ioremap(GPIO0_DDR_H, 4); // 映射数据方向寄存器物理地址到虚拟地址,GPIO0_C7需要设置GPIO0_DDR_H
alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
led_chrdev_class = class_create(THIS_MODULE, "led_chrdev");
for (; i < DEV_CNT; i++) {
cdev_init(&led_cdev[i].dev, &led_chrdev_fops);
led_cdev[i].dev.owner = THIS_MODULE;
cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);
cdev_add(&led_cdev[i].dev, cur_dev, 1);
device_create(led_chrdev_class, NULL, cur_dev, NULL,
DEV_NAME "%d", i);
}
return 0;
}
module_init(led_chrdev_init);
static __exit void led_chrdev_exit(void)
{
int i;
dev_t cur_dev;
printk("led chrdev exit (lubancat2 GPIO0_C7)\n");
for (i = 0; i < DEV_CNT; i++) {
iounmap(led_cdev[i].va_dr); // 释放数据寄存器虚拟地址
iounmap(led_cdev[i].va_ddr); // 释放数据方向寄存器虚拟地址
}
for (i = 0; i < DEV_CNT; i++) {
cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);
device_destroy(led_chrdev_class, cur_dev);
cdev_del(&led_cdev[i].dev);
}
unregister_chrdev_region(devno, DEV_CNT);
class_destroy(led_chrdev_class);
}
module_exit(led_chrdev_exit);
MODULE_AUTHOR("Embedfire");
MODULE_LICENSE("GPL");
上述函数有不清楚的可以翻看我之前的博客
第一个要点#define DEV_CNT (1)这里要加(),防止后续改动宏定义涉及运算的时候出错。
第二个要点就是使用了container_of函数,用于获取我们用户自定义的led驱动结构体struct led_chrdev,因为内核只认得struct cdev,所以不能直接返回我们自定义的结构体,所以使用这个函数来获得并存储在sturct file的private_data中以便后续使用不需要在使用一次containerof函数。关于这个函数我在之前的博客也有讲可以翻看一下。
第三个要点,在自定义的wirte函数中有get_user(ret, buf);其实这个换成copy_from_user函数更灵活,毕竟可以自选长度,而getuser就只能单字符了。
简单测试:
先在你放上述.c文件目录下创建一个makefile内容如下:
KERNEL_DIR=../../kernel/ #(需要依据实际内核源码路径更改)
ARCH=arm64 #声明需要编译的目标的架构
CROSS_COMPILE=aarch64-linux-gnu- #使用交叉编译器,这里只写前缀,到时候make的时候让内核去适配所有合适的编译器。
export ARCH CROSS_COMPILE #把上述变量变成全局变量且export的变量会优先覆盖系统原有同名变量。
obj-m := led_cdev.o #编译为可加载内核模块,最终产物是led_cdev.ko,obj-m是固定的,根据需要改名,这里就相当于xxx.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean copy
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
在当前makefile目录下执行make
Insmod xxx.ko
#绿灯亮
sudo sh -c 'echo 0 >/dev/led_chrdev0'
#绿灯灭
sudo sh -c 'echo 1 >/dev/led_chrdev0'
- echo 0:输出字符'0'。
- >/dev/led_chrdev0:将echo 0的输出重定向写入设备节点。这一步会触发 Linux 文件操作的open→write→close系统调用流程,最终执行驱动的对应接口。Linux的open之后会调用驱动自定义open,write会调用自定义write,close会调用自定义的release。
感谢阅读到最后,稍后整理rk3588驱动led 的代码。
1328

被折叠的 条评论
为什么被折叠?



