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

【MySQL数据库基础】(一)保姆级 MySQL 环境配置教程!CentOS 7+Ubuntu 双系统全覆盖

【MySQL数据库基础】(一)保姆级 MySQL 环境配置教程!CentOS 7+Ubuntu 双系统全覆盖

前言         作为后端开发、数据库学习的入门必备,MySQL 的环境配置是很多小伙伴的第一道 “小关卡”。尤其是不同 Linux 发行版(CentOS 7、Ubuntu)的安装步骤差异,再加上系统自带 MariaDB 的干扰、密码策略限制、中文编码等坑,很容易让人踩雷卡壳。         这篇博客就带来保姆级 MySQL 环境配置指南,不仅详细拆解 CentOS 7 下的完整安装步骤(从卸载冲突环境到配置优化),还补充了 Ubuntu 系统的安装流程,全程命令可直接复制,新手也能一步到位搞定 MySQL 环境,告别配置报错的烦恼!下面就让我们正式开始吧! 一、前置知识:为什么要先处理 MariaDB?         MySQL 被 Oracle 收购后,很多 Linux 发行版(比如 CentOS 7、

By Ne0inhk
【hacker送书第15期】AI绘画精讲与AIGC时代游戏美术设计:从入门到精通

【hacker送书第15期】AI绘画精讲与AIGC时代游戏美术设计:从入门到精通

文章目录 * 😊前言 * AI绘画精讲:Stable Diffusion从入门到精通💕 * 内容简介 * 获取方式 * AIGC时代:游戏美术设计与AI绘画应用从入门到精通💕 * 内容简介 * 获取方式 * 😊总结 😊前言 随着人工智能技术的飞速发展,AI绘画已经成为了一个备受瞩目的领域。在这个背景下,北京大学出版社推出了一系列关于AI绘画的优秀图书,其中就包括了《AI绘画精讲:Stable Diffusion从入门到精通》和《AIGC时代:游戏美术设计与AI绘画应用从入门到精通》。这两本书都是为了帮助读者全面了解和掌握AI绘画的精髓,推动人工智能技术在艺术领域的应用发展。 AI绘画精讲:Stable Diffusion从入门到精通💕 内容简介 Stable Diffusion是一款非常受欢迎的 AI 绘画与设计软件。AI绘画和传统绘画有什么不同、AI 绘画的基本逻辑是什么、如何让 AI 绘画软件为我们工作、如何生成符合要求的作品,本书将一一进行解析。 本书共 13 章内容。首先循序渐进地介绍了 A

By Ne0inhk

在普通电脑上跑大模型?!llama.cpp 实战指南(真·CPU救星)

文章目录 * 🤯 为什么你需要关注llama.cpp? * 🚀 手把手实战:十分钟跑通模型 * 第一步:准备战场环境 * 第二步:获取模型文件(关键!) * 第三步:启动模型交互! * 🛠️ 高级玩法解锁 * 💡 我的深度体验报告 * 👍 真香时刻 * 🤔 遇到的坑 * 🌟 超实用场景推荐 * 🔮 未来展望:CPU的逆袭? 还在为没显卡跑不动AI模型发愁?这个开源项目让我的旧笔记本起死回生了! 朋友们!今天要分享一个让我拍桌子叫绝的开源神器——llama.cpp。当初看到这个项目时我整个人都惊呆了:纯C++实现!不需要GPU!普通CPU就能跑! 作为一个常年被显卡价格PUA的程序员,这简直是救命稻草啊! 🤯 为什么你需要关注llama.cpp? 先说说我踩过的坑吧。去年想在家折腾开源大模型,结果: * 显卡要求动不动就16G显存(我的1060直接哭晕) * 装依赖环境能折腾一整天(Python版本地狱啊!) * 跑个7B模型风扇像直升机起飞(邻居以为我在挖矿) 直到发现了Georgi Gergan

By Ne0inhk

基于Llama-Factory的企业知识库问答系统构建

基于Llama-Factory的企业知识库问答系统构建 在企业数字化转型的浪潮中,员工对内部信息的即时获取需求正变得前所未有的迫切。想象这样一个场景:一名新入职的员工想了解年假申请流程,他不再需要翻找长达几十页的制度文档,也不必反复追问HR同事,而是直接在企业IM工具里问一句:“我怎么申请年假?”——系统立刻给出清晰、准确的操作指引。 这背后,正是大语言模型与企业私有知识深度融合的结果。然而,通用大模型虽然“博学”,却对企业内部规则“一无所知”。如何让AI真正理解组织的“专属语言”?微调(Fine-tuning)成为关键路径。但传统微调动辄需要多卡A100、数周训练周期和专业算法团队支持,对大多数企业而言门槛过高。 直到像 Llama-Factory 这样的开源框架出现,局面才被彻底改变。它把复杂的模型定制过程封装成可配置、可视化的流水线,使得单张消费级显卡也能完成领域模型的训练。这意味着,即使是非算法背景的工程师,也能在几天内为公司打造一个“懂业务”的AI助手。 Llama-Factory 的核心定位是一个开箱即用的大模型微调集成环境。它不是某个单一技术的实现者,而是一个高度

By Ne0inhk