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

从零开始“养龙虾”:OpenClaw 本地极简部署与 QQ 机器人接入全保姆级教程

从零开始“养龙虾”:OpenClaw 本地极简部署与 QQ 机器人接入全保姆级教程

文章目录 * 引言 * 什么是 OpenClaw? * 为什么选择 OpenClaw? * 一、基础环境准备 * 1. 安装 Node.js (v22及以上) * 2.安装 Git * 3. 解决 npm 被拦截(没报错跳过) * 二、一键部署与唤醒“龙虾” * 1.全自动拉取与组装 * 2.醒龙虾与配置“大脑” * 三、接入官方 QQ 机器人(可选) * 1. 领取官方机器人的“身份证” * 2. 本地安装专属通信插件 * 3. 结果展示 * 总结 引言 什么是 OpenClaw? 最近开源界有一只“红皮小龙虾”非常火,它就是 OpenClaw。

By Ne0inhk
Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战 前言 在进行 Flutter for OpenHarmony 的去中心化应用(DApp)或加密货币钱包开发时,支持标准的 WalletConnect 协议是链接用户钱包的关键。wallet_connect 是该协议的 Dart 实现,它能让你的鸿蒙 App 安全地与 MetaMask、Trust Wallet 等钱包建立双向加密连接。本文将探讨如何在鸿蒙系统下构建安全、稳定的 Web3 授权流程。 一、原理解析 / 概念介绍 1.1 基础原理

By Ne0inhk

Qwen3-VL-2B部署案例:博物馆导览机器人系统

Qwen3-VL-2B部署案例:博物馆导览机器人系统 1. 引言:视觉语言模型在智能导览中的应用价值 随着人工智能技术的发展,视觉语言模型(Vision-Language Model, VLM)正逐步从实验室走向实际应用场景。在公共服务领域,尤其是博物馆、美术馆等文化场所,智能化导览系统的需求日益增长。传统的语音讲解或静态图文介绍已难以满足用户对交互性、个性化和沉浸式体验的期待。 Qwen3-VL-2B-Instruct 作为阿里云开源的最新一代视觉语言模型,具备强大的图文理解、空间感知与多模态推理能力,为构建高可用的导览机器人系统提供了理想的技术底座。该模型支持图像识别、OCR解析、语义问答、上下文记忆等多种功能,并内置针对指令任务优化的 Instruct 版本,能够快速适配定制化场景。 本文将围绕 Qwen3-VL-2B-Instruct 模型,结合 Qwen3-VL-WEBUI 部署方案,详细介绍其在博物馆导览机器人系统中的落地实践,涵盖环境搭建、功能实现、关键代码及性能优化建议。 2. 技术选型与系统架构设计 2.1 为什么选择 Qwen3-VL-2B-Inst

By Ne0inhk
【PX4+ROS完全指南】从零实现无人机Offboard控制:模式解析与实战

【PX4+ROS完全指南】从零实现无人机Offboard控制:模式解析与实战

引言 无人机自主飞行是机器人领域的热门方向,而PX4作为功能强大的开源飞控,配合ROS(机器人操作系统)的灵活性与生态,成为实现高级自主飞行的黄金组合。然而,许多初学者对PX4的飞行模式理解不清,更不知道如何通过ROS编写可靠的Offboard控制程序。 本文将带你彻底搞懂PX4 6大核心飞行模式,实现无人机的自动起飞、悬停、轨迹跟踪(圆形/方形/螺旋)与降落。 亮点一览: * ✅ 深度解析PX4飞行模式(稳定/定高/位置/自动/Offboard) * ✅ 明确ROS可控制的模式与指令接口 * ✅ 完整的ROS功能包(C++实现,状态机设计) * ✅ 支持位置控制与速度控制双模式 * ✅ 内置圆形、方形、螺旋轨迹生成器 * ✅ 详细的安全机制与失效保护配置 无论你是准备参加比赛、做科研,还是想入门无人机开发,这篇文章都将是你宝贵的参考资料。 第一部分:PX4飞行模式深度剖析 PX4的飞行模式可以看作一个控制权逐级递增的层级结构。理解这些模式是编写控制程序的前提。 1. 稳定模式(STABILIZED / MANUAL / ACRO) * 核心特点:

By Ne0inhk