网站经营性备案,上海闵行网站建设公司,网络培训的网站建设,滕州哪里有做网站的字符设备点亮led灯实验rk3568笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正#xff01;#xff01;#xff01;裸机驱动开发与带有操作系统的驱动开发最大的区别是是否要单一的操作寄存器还是…字符设备点亮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是通过页表查找到物理地址的但是假如有多层页表的时候如果没有tlbMMU 每次进行虚拟地址到物理地址的转换都必须直接访问内存中的页表会带来显著的性能下降和总线带宽浪费影响如下地址转换延迟暴增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_Part1GPIO_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第716位也要为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、保留2b11硬件未实现该功能禁止使用。以下是完整的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_pin16)); 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_pin16)); 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_pin16)); val ~((unsigned int)0x01 (led_cdev-led_pin)); /*设置GPIO引脚输出低电平*/ } else{ val | ((unsigned int)0x01 (led_cdev-led_pin16)); 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/ #需要依据实际内核源码路径更改 ARCHarm64 #声明需要编译的目标的架构 CROSS_COMPILEaarch64-linux-gnu- #使用交叉编译器这里只写前缀到时候make的时候让内核去适配所有合适的编译器。 export ARCH CROSS_COMPILE #把上述变量变成全局变量且export的变量会优先覆盖系统原有同名变量。 obj-m : led_cdev.o #编译为可加载内核模块最终产物是led_cdev.koobj-m是固定的根据需要改名这里就相当于xxx.o all: $(MAKE) -C $(KERNEL_DIR) M$(CURDIR) modules .PHONE:clean copy clean: $(MAKE) -C $(KERNEL_DIR) M$(CURDIR) clean在当前makefile目录下执行makeInsmod xxx.ko#绿灯亮 sudo sh -c echo 0 /dev/led_chrdev0 #绿灯灭 sudo sh -c echo 1 /dev/led_chrdev0echo 0输出字符0。/dev/led_chrdev0将echo 0的输出重定向写入设备节点。这一步会触发 Linux 文件操作的open→write→close系统调用流程最终执行驱动的对应接口。Linux的open之后会调用驱动自定义openwrite会调用自定义writeclose会调用自定义的release。感谢阅读到最后稍后整理rk3588驱动led 的代码。