news 2026/1/13 13:41:39

字符设备点亮led灯实验rk3568

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
字符设备点亮led灯实验rk3568

字符设备点亮led灯实验rk3568

笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正!!!

裸机驱动开发与带有操作系统的驱动开发最大的区别是是否要单一的操作寄存器还是说要符合操作系统的胃口进行设计。

带有操作系统的驱动开发势必要求设备驱动附加更多的代码和功能,把单一的驱动变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API。

一、内存管理单元mmu

流程:

当没有启用MMU的时候,CPU在读取指令或者访问内存时便会将地址直接输出到芯片的引脚上,此地址直接被内存接收,这段地址称为物理地址, 如下图所示。

简单地说,物理地址就是内存单元的绝对地址,好比你电脑上插着一张8G的内存条,则第一个存储单元便是物理地址0x0000, 内存条的第6个存储单元便是0x0005,无论处理器怎样处理,物理地址都是它最终的访问的目标。

当CPU开启了MMU时,CPU发出的地址将被送入到MMU,被送入到MMU的这段地址称为虚拟地址, 之后MMU会根据去访问页表地址寄存器(这个寄存器是在cpu内部的,为 MMU 提供页表在物理内存中的起始地址)然后去内存中找到页表(假设只有一级页表)的条目,从而翻译出实际的物理地址, 如下图所示。

作用:

  1. 保护内存
  2. 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换
  3. 突破物理内存限制,如一个程序运行要10gb,但是真实内存只有4gb依旧跑的起来,因为mmu对不使用的部分进行了换出换入。操作系统会把暂时用不到的物理页保存到硬盘的交换空间(虚拟内存文件)中,释放物理内存给当前需要的程序使用;当程序需要访问这些页时,操作系统再把它们从硬盘加载回物理内存(这个过程叫缺页异常处理)。
  4. Mmu在现代计算机中是集成在cpu内部。

二、快表tlb

上述说到mmu是通过页表查找到物理地址的,但是假如有多层页表的时候,如果没有tlb,,MMU 每次进行虚拟地址到物理地址的转换,都必须直接访问内存中的页表,会带来显著的性能下降和总线带宽浪费,影响如下:

  1. 地址转换延迟暴增,CPU 效率大幅降低。因为内存的访问延迟远高于 CPU 内部硬件(比如寄存器、MMU 电路)的操作延迟,两者相差数百倍甚至上千倍。
  2. 内存总线带宽被大量占用。因为内存总线是 CPU 和内存之间传输数据的 “通道”,带宽是有限的。(本人操作系统有点摆以前,建议大家去豆包一下就知道了,把计算过程看一下立马懂)
  3. 多级页表的优势几乎完全丧失。操作系统引入多级页表的核心目的,是节省页表本身占用的内存空间(按需分配页表,而非一次性分配完整的一级页表)。但多级页表的代价是增加了内存访问次数,这个代价原本是靠 TLB 来抵消的 ——TLB 命中时可以跳过所有页表访问步骤。
  4. 权限检查效率同步下降。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'
  1. echo 0:输出字符'0'。
  2. >/dev/led_chrdev0:将echo 0的输出重定向写入设备节点。这一步会触发 Linux 文件操作的open→write→close系统调用流程,最终执行驱动的对应接口。Linux的open之后会调用驱动自定义open,write会调用自定义write,close会调用自定义的release。

感谢阅读到最后,稍后整理rk3588驱动led 的代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/11 9:37:36

一文搞懂AI大语言模型工作原理,初中生都能看懂

01 神经网络1&#xff0c;神经元&#xff1a;神经网络的最小单元神经网络的灵感来源于人类大脑的神经元&#xff0c;每个神经元就像一棵 “小树”&#xff0c;树突接收其它神经元的信号&#xff0c;细胞体处理信号&#xff0c;轴突把处理后的信号传给下一个神经元。生物神经元示…

作者头像 李华
网站建设 2026/1/13 9:27:55

3.2IT审计

1、IT审计范围的确定&#xff1a;总体范围、组织范围、物理范围、逻辑范围、其他相关内容 2、IT审计风险主要包括&#xff1a;固有风险、控制风险、检查风险和总体审计风险。 3、常用审计方法包括&#xff1a;访谈法、调查法、检查法、观察法、测试法、程序代码检查法 4、常用的…

作者头像 李华
网站建设 2025/12/25 6:30:23

深入理解类加载器

目录 一、回忆类加载过程 二、类加载器 1、定义与本质 2、JVM内置类加载体系 3、自定义类加载器 ClassLoader类中的两个关键方法: 核心原则&#xff1a; 4、类加载器加载的顺序 &#xff08;1&#xff09;核心&#xff1a;双亲委派模型下的委托与加载顺序 1&#xff…

作者头像 李华
网站建设 2025/12/24 14:50:20

腾讯云国际站代理商的定制化技术支持服务的成功案例有哪些?

腾讯云国际站代理商的定制化技术支持服务案例&#xff0c;广泛覆盖电商、金融、游戏、文娱等多个出海核心领域&#xff0c;既解决了企业跨境合规难题&#xff0c;又实现了成本优化与业务效率提升&#xff0c;以下是具体案例详情&#xff1a;电商领域东南亚电商平台合规改造&…

作者头像 李华
网站建设 2026/1/2 11:23:55

Dify可视化编排功能对比传统代码开发的优势

Dify可视化编排如何重塑AI应用开发 在企业争相布局大模型的今天&#xff0c;一个现实问题摆在面前&#xff1a;为什么很多团队投入大量人力开发的AI系统最终却难以上线&#xff1f;答案往往出人意料——不是模型不够强&#xff0c;而是构建方式太原始。大多数项目仍依赖传统编码…

作者头像 李华
网站建设 2025/12/24 14:19:39

以品质之道,养铸铁试验平台之生生不息

铸铁试验平台的国家标准的制定和执行对于整个行业的发展和进步具有重要的推动作用。通过严格执行国家标准&#xff0c;可以有效地提高铸铁试验平台的质量和安全性能&#xff0c;保障相关行业的生产和使用安全。同时&#xff0c;国家标准的制定也可以促进相关行业的技。 铸铁试验…

作者头像 李华