ARM Linux 驱动开发篇--- 设备树下的 LED 驱动实验-- Ubuntu20.04

ARM Linux 驱动开发篇--- 设备树下的 LED 驱动实验-- Ubuntu20.04
🎬 渡水无言个人主页渡水无言

专栏传送门: 《linux专栏》   《嵌入式linux驱动开发》
⭐️流水不争先,争的是滔滔不绝

 📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、设备树 LED 驱动原理

二、硬件原理图分析(看过之前的博客可以忽略了)

三、实验程序编写

四、LED 灯驱动程序编写

五、编写测试 APP

六、运行测试

6.1、编译驱动程序和测试 APP

总结


前言

前几期博客我们详细的讲解了设备树语法以及在驱动开发中常用的 OF 函数,本期博客就正式开始

第一个基于设备树的 Linux 驱动实验。


一、设备树 LED 驱动原理

本期博客使用设备树来向 Linux 内核传递相关的寄存器物理地址,Linux 驱动文件使用上一章讲解的OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO。本章实验还是比较简单的,本章实验重点内容如下:

①、在 imx6ull-alientek-emmc.dts 文件中创建相应的设备节点。

②、编写驱动程序(新版led驱动实验程序编写),获取设备树中的相关属性值。

③、使用获取到的有关属性值来初始化LED所使用的GPIO。

二、硬件原理图分析(看过之前的博客可以忽略了)

从图中可以看出,LED0 接到了 GPIO_3 上,GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03 输出低电平 (0) 的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平 (1) 的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03 的输出电平,输出 0 就亮,输出 1 就灭。
 

三、实验程序编写

在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件, 在根节点“/”最后面输入如下所示内容:

alphaled { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-led"; status = "okay"; reg = < 0x020C406C 0x04 /* CCM_CCGR1_BASE */ 0x020E0068 0x04 /* SW_MUX_GPIO1_IO03_BASE */ 0x020E02F4 0x04 /* SW_PAD_GPIO1_IO03_BASE */ 0x0209C000 0x04 /* GPIO1_DR_BASE */ 0x0209C004 0x04 >; /* GPIO1_GDIR_BASE */ };

属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长。
(cell),地址长度也占用一个字长(cell)。

属性 compatbile 设置 alphaled 节点兼容性为“atkalpha-led”。
属性 status 设置状态为“okay”。
reg 属性设置了驱动里面所要使用的寄存器物理地址。

比如0X020C406C 0X04表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。
设备树修改完成以后输入如下命令重新编译一下 imx6ull-alientek-emmc.dts。

make dtbs

编译完成以后得到 imx6ull-alientek-emmc.dtb,使用新的 imx6ull-alientek-emmc.dtb 启动Linux 内核。Linux 启动成功以后进入到/proc/device-tree/目录中查看是否有“alphaled”这个节点。

四、LED 灯驱动程序编写

#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 版本 : V1.0 描述 : LED驱动文件。 其他 : 无 ***************************************************************/ #define DTSLED_CNT 1 /* 设备号个数 */ #define DTSLED_NAME "dtsled" /* 名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 映射后的寄存器虚拟地址指针 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO03; static void __iomem *SW_PAD_GPIO1_IO03; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; /* dtsled设备结构体 */ struct dtsled_dev{ dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ struct device_node *nd; /* 设备节点 */ }; struct dtsled_dev dtsled; /* led设备 */ /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) { val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); }else if(sta == LEDOFF) { val = readl(GPIO1_DR); val|= (1 << 3); writel(val, GPIO1_DR); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { filp->private_data = &dtsled; /* 设置私有数据 */ return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, 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]; /* 获取状态值 */ if(ledstat == LEDON) { led_switch(LEDON); /* 打开LED灯 */ } else if(ledstat == LEDOFF) { led_switch(LEDOFF); /* 关闭LED灯 */ } return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 设备操作函数 */ static struct file_operations dtsled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { u32 val = 0; int ret; u32 regdata[14]; const char *str; struct property *proper; /* 获取设备树中的属性数据 */ /* 1、获取设备节点:alphaled */ dtsled.nd = of_find_node_by_path("/alphaled"); if(dtsled.nd == NULL) { printk("alphaled node nost find!\r\n"); return -EINVAL; } else { printk("alphaled node find!\r\n"); } /* 2、获取compatible属性内容 */ proper = of_find_property(dtsled.nd, "compatible", NULL); if(proper == NULL) { printk("compatible property find failed\r\n"); } else { printk("compatible = %s\r\n", (char*)proper->value); } /* 3、获取status属性内容 */ ret = of_property_read_string(dtsled.nd, "status", &str); if(ret < 0){ printk("status read failed!\r\n"); } else { printk("status = %s\r\n",str); } /* 4、获取reg属性内容 */ ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10); if(ret < 0) { printk("reg property read failed!\r\n"); } else { u8 i = 0; printk("reg data:\r\n"); for(i = 0; i < 10; i++) printk("%#X ", regdata[i]); printk("\r\n"); } /* 初始化LED */ #if 0 /* 1、寄存器地址映射 */ IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]); SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]); SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]); GPIO1_DR = ioremap(regdata[6], regdata[7]); GPIO1_GDIR = ioremap(regdata[8], regdata[9]); #else IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1); SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2); GPIO1_DR = of_iomap(dtsled.nd, 3); GPIO1_GDIR = of_iomap(dtsled.nd, 4); #endif /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); /* 清楚以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, IMX6U_CCM_CCGR1); /* 3、设置GPIO1_IO03的复用功能,将其复用为 * GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03); /*寄存器SW_PAD_GPIO1_IO03设置IO属性 *bit 16:0 HYS关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper功能 *bit [12]: 1 pull/keeper使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度100Mhz *bit [5:3]: 110 R0/6驱动能力 *bit [0]: 0 低转换率 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 4、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, GPIO1_GDIR); /* 5、默认关闭LED */ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); /* 注册字符设备驱动 */ /* 1、创建设备号 */ if (dtsled.major) { /* 定义了设备号 */ dtsled.devid = MKDEV(dtsled.major, 0); register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME); } else { /* 没有定义设备号 */ alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */ dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */ dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */ } printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor); /* 2、初始化cdev */ dtsled.cdev.owner = THIS_MODULE; cdev_init(&dtsled.cdev, &dtsled_fops); /* 3、添加一个cdev */ cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT); /* 4、创建类 */ dtsled.class = class_create(THIS_MODULE, DTSLED_NAME); if (IS_ERR(dtsled.class)) { return PTR_ERR(dtsled.class); } /* 5、创建设备 */ dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME); if (IS_ERR(dtsled.device)) { return PTR_ERR(dtsled.device); } return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); /* 注销字符设备驱动 */ cdev_del(&dtsled.cdev);/* 删除cdev */ unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */ device_destroy(dtsled.class, dtsled.devid); class_destroy(dtsled.class); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("duan"); 

dtsled.c 文件中的内容其实和之前的led实验的内容基本一样,只是 dtsled.c 中包含了处理设备树的代码,我们重点来看一下这部分代码。

在设备结构体 dtsled_dev 中添加了成员变量 nd,nd 是 device_node 结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。

通过 of_find_node_by_path 函数得到 alphaled 节点,后续其他的 OF 函数要 使用 device_node。

通过 of_find_property 函数获取 alphaled 节点的 compatible 属性,返回值为 property 结构体类型指针变量,property 的成员变量 value 表示属性值。

通过 of_property_read_string 函数获取 alphaled 节点的 status 属性值。

通过 of_property_read_u32_array 函数获取 alphaled 节点的 reg 属性所有值, 并且将获取到的值都存放到 regdata 数组中。

使用“古老”的 ioremap 函数完成内存映射,将获取到的 regdata 数组中的寄存器物理地址转换为虚拟地址。

使用 of_iomap 函数一次性完成读取 reg 属性以及内存映射,of_iomap 函数是设备树推荐使用的 OF 函数。

五、编写测试 APP

本次博客直接使用的测试 APP和之前的led实验一样,将之前实验的 ledApp.c 文件复制到本章实验工程下即可。

#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : ledApp.c 版本 : V1.0 描述 :led驱测试APP。 其他 : 无 使用方法 :./ledtest /dev/led 0 关闭LED ./ledtest /dev/led 1 打开LED ***************************************************************/ #define LEDOFF 0 #define LEDON 1 /* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功;其他 失败 */ int main(int argc, char *argv[]) { int fd, retvalue; char *filename; unsigned char databuf[1]; if(argc != 3){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开led驱动 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("file %s open failed!\r\n", argv[1]); return -1; } databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ /* 向/dev/led文件写入数据 */ retvalue = write(fd, databuf, sizeof(databuf)); if(retvalue < 0){ printf("LED Control Failed!\r\n"); close(fd); return -1; } retvalue = close(fd); /* 关闭文件 */ if(retvalue < 0){ printf("file %s close failed!\r\n", argv[1]); return -1; } return 0; }

六、运行测试

6.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为 dtsled.o,Makefile 内容如下所示:

KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2 CURRENT_PATH := $(shell pwd) obj-m := dtsled.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译测试 APP输入如下命令编译测试 ledApp.c 这个测试程序:

arm-linux-gnueabihf-gcc ledApp.c -o ledApp

6.2、运行测试

将上一小节编译出来的 dtsled.ko 和 ledApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

sudo cp dtsled.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
sudo cp ledApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载 dtsled.ko 驱动模块:

depmod //第一次加载驱动的时候需要运行此命令 modprobe dtsled.ko //加载驱动

驱动加载成功以后会在终端中输出一些信息,如下图所示:

从上图中可以看出,alpahled 这个节点找到了,并且 compatible 属性值为“atkalpha-led”,status 属性值为“okay”。reg 属性的值为“0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4”,这些都和我们设置的设备树一致。驱动加载成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:

./ledApp /dev/dtsled 1 //打开 LED 灯

输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭 LED 灯:

./ledApp /dev/dtsled 0//关闭 LED 灯

输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否熄灭。如果要卸载驱动的话输入如下命令即可:

rmmod dtsled.ko


总结

本期博客就正式完成了第一个基于设备树的 Linux 驱动实验。

Read more

【STL源码剖析】从源码看 list:从迭代器到算法

【STL源码剖析】从源码看 list:从迭代器到算法

半桔:个人主页  🔥 个人专栏: 《Linux手册》《手撕面试算法》《C++从入门到入土》 🔖源码之前,了不秘密。 文章目录 * 前言 * 一. list 概述 * 二. list 的节点 * 三. list 迭代器 * 3.1 定义 * 3.2 构造 * 3.3 重载 * 四. list 数据结构 * 五. list 的构造和内存管理 * 六. list 的接口 本文并不适合STL初学者。对于那些熟练掌握 C++ 模板和 STL 的日常使用,理解内存分配与对象生命周期,并且有扎实的数据结构基础,希望深刻了解STL实现细节,从而得以提升对STL的扩充能力,或是希望藉由观察STL源代码,学习世界一流程序员身手,

By Ne0inhk
【C++】STL之list模拟实现:关于链表容器的双向迭代器你知道多少?

【C++】STL之list模拟实现:关于链表容器的双向迭代器你知道多少?

前言: 前面的博客中我已经介绍了STL核心容器之一的list相关接口的使用,今天我们就从底层出发,来模拟实现一下list的那些核心接口函数。同时,也来感受一下list的双向迭代器到底与string和vector的随机迭代器有哪些区别? list容器功能接口介绍:https://blog.ZEEKLOG.net/Miun123/article/details/151685386?spm=1001.2014.3001.5502 废话不多说,我们直接进入今天的正题👇️👇️👇️ list容器深度剖析及模拟实现 我们想要模拟实现list容器,那就要理解list容器的底层结构。前面的博客已经提到,其本质就是一个双向链表,所以,成员变量就应该包含一个记录头节点的指针,以及记录有效节点个数的变量。同时,为了list容器可以满足不同类型的数据,我们将所有的类实现为类模板。 1、定义节点结构 struct创建的类默认所有的成员但是公开的,而节点结构就需要公开被list访问。 template<class T> struct list_node { // 成员变量 T _da

By Ne0inhk
排序--数据结构初阶(4)(C/C++)

排序--数据结构初阶(4)(C/C++)

文章目录 * 前言 * 理论部分: * 1.直接插入排序 * 2.希尔排序 * 3.直接选择排序 * 4.堆排序 * 5.冒泡排序 * 6.快速排序 * 归并排序 * 非比较排序 * 计数排序 * 作业部分 前言 这是数据结构初阶的最后一期,虽然来说在C++的库函数里面有sort函数可以代替这里所有的方法,并且时间复杂度也是优于他们的,但是sort函数是由他们写出来的,因此,还是是有必要学习一下的 理论部分: 这里的代码实现都是按升序来的 排序的话建议先写单趟再写整体 这些排序在两数相等的时候一般是不进行操作的(一般这么写) 1.直接插入排序 就是目前在最后的那个数跟前面每个数比,看看要插哪 时间复杂度:O(n2) 最好的情况下是O(n) 在小段小段有序时有极大的优势(相对于选择排序跟冒泡排序) 代码实现: void InsertSort(int* a, int n)

By Ne0inhk
大模型应用:最优路径规划实践:A*算法找最优解,大模型做自然语言解释.91

大模型应用:最优路径规划实践:A*算法找最优解,大模型做自然语言解释.91

一、引言         算法是个很有意义的课题,尽管大模型让我们不需要像以前学习机器学习那样,需要很深的数学基础,但结合算法来应用大模型确实是个很有趣的事情,传统算法经过数十年发展,已在路径规划、优化计算等领域达到极高的精确度;另一方面,大语言模型的崛起让人机交互变得前所未有的自然流畅。然而,一个不容忽视的现实是:再精确的算法,如果用户看不懂、不会用,就只是实验室里的玩具;再流畅的表达,如果缺乏技术可靠性,就只是华丽的空谈。         想象这样一个场景:导航系统为你计算出了一条理论上最优的路线,却只能用一串坐标告诉你怎么走;或者,一个能言善道的助手热情地为你指路,却把你带进了死胡同。这两种情况,本质上都是技术能力与用户体验之间的断裂。这种断裂并非偶然,而是单一技术路线的固有局限。算法擅长计算却不善表达,模型善于沟通却难以精确,各有所长,也各有所短。因此,真正的突破不在于让某一方变得更强大,而在于让两者形成互补协作的关系,用各自的长处弥补对方的短板。         这是我们今天想探讨得A*算法与大模型融合的核心价值所在。A*算法如同精密的"空间计算大脑",保证路径的数学最

By Ne0inhk