树莓派3B+驱动开发(2)- LED驱动(传统模式)

github主页:https://github.com/snqx-lqh
本项目github地址:https://github.com/snqx-lqh/RaspberryPiDriver
本项目硬件地址:https://oshwhub.com/from_zero/shu-mei-pai-kuo-zhan-ban
欢迎交流

笔记说明

如我在驱动开发总览中说的那样,一般的驱动开发模式就是有3种。

传统设备驱动模式:将所有的资源分配放进一个文件中。

PlatformDevice/PlatformDriver模式:资源放进PlatformDevice,实现驱动放进PlatformDriver文件。

设备树/PlatformDriver模式:资源放进设备树,实现驱动放进PlatformDriver文件。设备树一种是提前编译内核的时候就编译好,还有一种方式是添加设备树插件。

这一节主要说明传统的模式,就是将资源全部放进一个文件中处理。

主要参考的文章:

正点原子《I.MX6U 嵌入式 Linux 驱动开发指南 V1.81》

韦东山《01_嵌入式Linux应用开发完全手册V5.1_IMX6ULL_Pro开发板》

本节源码路径02_Firmware/02_led_drv_cdev

开发流程

1、初始化一个设备操作函数结构体 struct file_operations,并注册内部函数。该结构体就是这个模块文件读写的相关操作

2、编写入口函数,在这个里面注册设备,并将前面定义的file_operations结构体传入注册函数。自动分配节点需要创建类后创建设备节点。

3、编写出口函数,将设备注销。删除类和设备节点。

4、在入口函数完善IO引脚初始化,出口函数完善引脚相关处理。

5、在设备操作函数中完善各个操作函数的实现逻辑。

需要的头文件

先说明这部分代码需要包含的头文件。

#include <linux/init.h>     //用于定义模块初始化和清理相关的宏以及函数原型等内容
#include <linux/module.h>   //包含了众多用于构建内核模块的基本定义、宏和函数原型
#include <asm/io.h>         //用于处理输入 / 输出操作,尤其是和硬件底层的内存映射 I/O 相关的操作
#include <linux/string.h>   //提供了一系列字符串处理相关的函数
#include <linux/fs.h>       //定义了文件系统操作的各种结构体、函数原型等内容
#include <linux/uaccess.h>  //用于处理用户空间(User Space)和内核空间(Kernel Space)之间的数据拷贝操作
#include <linux/cdev.h>     //定义了字符设备相关的结构体

设备操作函数结构体

我们需要定义一个struct file_operations结构体,以供我们后面操作这个模块,并且需要注册这个结构体中的操作函数。

// 通过文件读取,得到当前LED的状态
ssize_t led_drv_read(struct file* filp, char __user* buf, size_t len, loff_t* off)
{return  0;
}// 通过向文件写入LED状态,控制LED灯
ssize_t led_drv_write(struct file* filp, const char __user* buf, size_t len, loff_t* off)
{return 0;
}const struct file_operations led_fops = {.owner   = THIS_MODULE,.read    =  led_drv_read,.write   =  led_drv_write,
};

编写入口函数

入口函数就是我们加载这个模块的时候会调用的函数,这里我们将注册一个字符型设备,并且动态的为其分配设备号,其实还有静态方法,指定设备号分配,可以看正点原子教程,这里不做说明,因为感觉没有动态好用。

static dev_t  led_dev_num = 0;          // 设备编号
static struct cdev   led_cdev;          // 字符设备结构体
static struct class  *class  = NULL;    //类
static struct device *device = NULL;    //设备static int __init led_drv_cdev_init(void)
{// 将该模块注册为一个字符设备,并动态分配设备号if (alloc_chrdev_region(&led_dev_num, 0, 1, "led_drv")) {printk(KERN_ERR"failed to register kernel module!\n");return -1;}printk(KERN_INFO"led_drv device major & minor is [%d:%d]\n", MAJOR(led_dev_num), MINOR(led_dev_num));//初始化并添加一个cdevcdev_init(&led_cdev, &led_fops);cdev_add(&led_cdev, led_dev_num, 1);//创建类class = class_create("led_drv");if (IS_ERR(class)) {return PTR_ERR(class);}//创建设备device = device_create(class, NULL, led_dev_num, NULL, "led_drv");if (IS_ERR(device)) {return PTR_ERR(device);}return 0;
}module_init(led_drv_cdev_init);

为什么要使用创建类和设备,因为不使用的话就需要自己加载模块后创建,使用以下指令创建设备节点文件:

mknod /dev/led_drv c 200 0

“c”表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。主设备号不一定是200,我是动态生成的,需要在内核加载信息中去查看,因为我在代码中动态申请设备号后写了个打印,使用如下指令:

demsg | tail

但是这样没有创建类和设备方便,所以就使用创建类和设备了。

编写出口函数

出口函数就是,我们在注销设备的时候会执行的函数,一般我们需要注销我们前面定义的字符设备。

static void __exit led_drv_cdev_exit(void)
{// 释放字符设备cdev_del(&led_cdev);unregister_chrdev_region(led_dev_num, 1);//删除类和设备device_destroy(class, led_dev_num);class_destroy(class);
}module_exit(led_drv_cdev_exit);

出口&入口函数完善引脚初始化

我们把框架搭好后,就可以开始初始化引脚相关的寄存器了,其实操作寄存器的方式和单片机感觉很像,但是内核不能直接操作寄存器物理地址,我们需要先把寄存器相关地址,映射到虚拟地址,然后进行操作。

首先就是虚拟地址的映射,具体操作相关如下,映射后我们才能使用相关的寄存器。

// 寄存器地址
#define BCM2837_GPIO_FSEL0_BASE     0x3F200000   // GPIO功能选择寄存器0  
#define BCM2837_GPIO_FSEL1_BASE     0x3F200004   // GPIO功能选择寄存器1  
#define BCM2837_GPIO_FSEL2_BASE     0x3F200008   // GPIO功能选择寄存器2
#define BCM2837_GPIO_SET0_BASE      0x3F20001C   // GPIO置位寄存器0      
#define BCM2837_GPIO_CLR0_BASE      0x3F200028   // GPIO清零寄存器0      
#define BCM2837_GPIO_LEV0_BASE      0x3F200034   // GPIO清零寄存器0  // 寄存器对应的虚拟ioremap后的地址,会在后面初始化
static void __iomem *BCM2837_GPIO_FSEL0 = NULL;
static void __iomem *BCM2837_GPIO_FSEL1 = NULL;
static void __iomem *BCM2837_GPIO_FSEL2 = NULL;
static void __iomem *BCM2837_GPIO_SET0  = NULL;
static void __iomem *BCM2837_GPIO_CLR0  = NULL;
static void __iomem *BCM2837_GPIO_LEV0  = NULL;static int __init led_drv_cdev_init(void)
{// 将树莓派引脚控制相关的寄存器进行虚拟地址映射,使得可以控制对应寄存器BCM2837_GPIO_FSEL0 = ioremap(BCM2837_GPIO_FSEL0_BASE, 0x04);BCM2837_GPIO_FSEL1 = ioremap(BCM2837_GPIO_FSEL1_BASE, 0x04);BCM2837_GPIO_FSEL2 = ioremap(BCM2837_GPIO_FSEL2_BASE, 0x04);BCM2837_GPIO_SET0  = ioremap(BCM2837_GPIO_SET0_BASE , 0x04);BCM2837_GPIO_CLR0  = ioremap(BCM2837_GPIO_CLR0_BASE , 0x04);BCM2837_GPIO_LEV0  = ioremap(BCM2837_GPIO_LEV0_BASE , 0x04);// 注册字符设备、创建类和设备// ************** //return 0;
}

我们可以使用映射后的寄存器写一些对寄存器的读写操作来控制引脚电平的读写。

// 想要控制的LED灯,BCM引脚定义,这是设计的扩展板上的定义
#define LED2 17
#define LED3 27
#define LED4 22
#define LED5 23#define LED_OUTPUT 1
#define LED_INPUT  0/*** @brief 设置对应引脚的高低电平* @param pin    需要设置的引脚* @param level  1是高电平 0是低电平*/
void gpio_set_level(int pin, int level)
{// 通过想要的电平判断现在想要控制的寄存器void* reg = (level ? BCM2837_GPIO_SET0 : BCM2837_GPIO_CLR0);iowrite32(1 << pin, reg);
}/*** @brief 获得引脚的电平* @param pin 需要获得的引脚* @return 1是高电平 0是低电平*/
static int gpio_get_level(int pin)
{int ret = 0;int pin_level = 0;// 读取引脚电平寄存器pin_level = ioread32( BCM2837_GPIO_LEV0 );// 将想要读取的引脚的状态,提取出来ret = (1 << pin );ret = ret & pin_level;if(ret){return 1;}else{return 0;}
}/*** @brief 设置GPIO的寄存器,配置GPIO是输入还是输出* @param pin   需要设置的引脚* @param mode  1是输出模式 0是输入模式*/
static void gpio_set_mode(int pin, int mode)
{void *reg = NULL;int   val = 0;int   pin_ctl = 0;// 通过pin号来确定要控制的寄存器if(pin < 10){reg = BCM2837_GPIO_FSEL0;}else if(pin < 20){reg = BCM2837_GPIO_FSEL1;}else if(pin < 30){reg = BCM2837_GPIO_FSEL2;}// 比如我要控制11号脚,就是要控制BCM2837_GPIO_FSEL1_BASE的1号位置的3个位。pin_ctl = pin % 10;// 将对应的gpio的功能选择位全部写0val =  ~(7   << (pin_ctl * 3));val &= ioread32(reg);// 控制设置对应的gpio的功能选择位写 000 还是 001val |= (mode << (pin_ctl * 3));iowrite32(val, reg);
}

然后在初始化的时候我们就可以初始化我们的引脚

static int __init led_drv_cdev_init(void)
{// 寄存器进行虚拟地址映射// ************** //// 控制引脚输入输出状态gpio_set_mode(LED2, LED_OUTPUT);gpio_set_mode(LED3, LED_OUTPUT);gpio_set_mode(LED4, LED_OUTPUT);gpio_set_mode(LED5, LED_OUTPUT);// 设置引脚电平gpio_set_level(LED2, 0);gpio_set_level(LED3, 0);gpio_set_level(LED4, 0);gpio_set_level(LED5, 0);// 注册字符设备、创建类和设备// ************** //return 0;
}

在出口函数完善相关引脚处理,主要是释放内存映射。

static void __exit led_drv_cdev_exit(void)
{// 设置电平为高gpio_set_level(LED2, 1);gpio_set_level(LED3, 1);gpio_set_level(LED4, 1);gpio_set_level(LED5, 1);// 取消gpio物理内存映射iounmap(BCM2837_GPIO_FSEL0);iounmap(BCM2837_GPIO_FSEL1);iounmap(BCM2837_GPIO_FSEL2);iounmap(BCM2837_GPIO_SET0);iounmap(BCM2837_GPIO_CLR0);iounmap(BCM2837_GPIO_LEV0);// 释放字符设备、删除类和设备// ************** //
}

设备操作函数完善逻辑

我们之前不是写了write和read的操作函数吗,现在就需要完善这些操作函数,使得用户文件在调用这个文件的时候可以操作gpio进行控制。

先解释读函数,这里比较重要的操作就是copy_to_user,他会把第二个参数里面的值复制到第一个参数中去,这里的buf就是我们用户层想读的值,len是用户写的长度。

#define MIN(a, b) (a < b ? a : b)// 通过文件读取,得到当前LED的状态
ssize_t led_drv_read(struct file* filp, char __user* buf, size_t len, loff_t* off)
{int ret = 0;int char_len = 0;char led_state[4];printk("%s %s line %d\r\n", __FILE__, __FUNCTION__, __LINE__);led_state[0] = gpio_get_level(LED2);led_state[1] = gpio_get_level(LED3);led_state[2] = gpio_get_level(LED4);led_state[3] = gpio_get_level(LED5);char_len = sizeof(led_state);int real_len = MIN(len,char_len);ret = copy_to_user(buf, led_state, real_len);return ret < 0 ? ret : real_len;
}

然后是写函数,其实就和读差不多了,主要是copy_from_user,能够把用户传进的值进行复制处理。

// 通过向文件写入LED状态,控制LED灯
ssize_t led_drv_write(struct file* filp, const char __user* buf, size_t len, loff_t* off)
{int ret = 0;char led_state[4];int char_len = 0;printk("%s %s line %d\r\n", __FILE__, __FUNCTION__, __LINE__);char_len = sizeof(led_state);int real_len = MIN(len,char_len);ret = copy_from_user(led_state, buf, real_len);switch (led_state[0]){case 0:gpio_set_level(LED2, led_state[1]);break;  case 1:gpio_set_level(LED3, led_state[1]);break;case 2:gpio_set_level(LED4, led_state[1]);break;case 3:gpio_set_level(LED5, led_state[1]);break;default:break;}return 0;
}

到这里,这个驱动文件就编写完成了,后面就是编译加载了。

编写应用层函数

驱动编写完成后,我们需要有一个应用层函数来调用这个驱动,写出以下示例。主要功能就是打开驱动,写和读驱动的内容。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>/** sudo ./led_drv_cdev_app -w 0 0* sudo ./led_drv_cdev_app -r*/int main(int argc, char **argv)
{int fd;char writeBuff[3];char readBuff[5];int len;if (argc < 2){printf("Usage: %s -w <string>\n", argv[0]);printf(" %s -r\n", argv[0]);return -1;}fd = open("/dev/led_drv", O_RDWR);if (fd == -1){printf("can not open file /dev/led_drv\n");return -1;}if ((0 == strcmp(argv[1], "-w")) && (argc == 4)){writeBuff[0] = (char)atoi(argv[2]);writeBuff[1] = (char)atoi(argv[3]);writeBuff[2] = '\0';printf("write : %d, %d\n", writeBuff[0], writeBuff[1]);write(fd, writeBuff, 3);}else if((0 == strcmp(argv[1], "-r")) && (argc == 2)){len = read(fd, readBuff, 5);readBuff[4] = '\0';printf("pin read : 0x%x, 0x%x, 0x%x, 0x%x\n", readBuff[0], readBuff[1], readBuff[2], readBuff[3]);}else{printf("APP Failed\n");}close(fd);return 0;
}

编译加载

makefile主要参考正点原子和韦东山的写法。这个Makefile会把驱动模块和应用程序一起编译。

# 模块驱动,必须以obj-m=xxx形式编写
obj-m = led_drv_cdev.oKDIR = /home/linux-rpi-6.6.y/
CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
CROSS_COMPILE = arm-linux-gnueabihf-all:$(MAKE) -C $(KDIR) M=$(PWD) $(CROSS)  modules$(CROSS_COMPILE)gcc -o led_drv_cdev_app led_drv_cdev_app.c.PHONY: clean
clean:$(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean

makefile编译完成后,直接make就行

make

将make生成的文件,如我上文写了脚本,通过scp传到树莓派上。具体的内容参数,自己修改。

#!/bin/shsendfile="led_drv_cdev.ko led_drv_cdev_app"
pi_user=pi
pi_ip=192.168.2.149
pi_dir=/home/pi/RpiDriver/02_led_drv_cdevscp ${sendfile} ${pi_user}@${pi_ip}:${pi_dir}

在树莓派上,我们需要加载并使用这个模块。

sudo insmod led_drv_cdev.kosudo ./led_drv_cdev_app -w 0 1
sudo ./led_drv_cdev_app -rsudo rmmod led_drv_cdev

可以使用dmesg查看运行过程中的变化

dmesg | tail

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/36444.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

ArrayList常见操作源码逐句剖析

目录 前言 正文 1.需要了解的一些字段属性 1.存储 ArrayList 元素的数组缓冲区。 2.集合的大小 3.默认集合容量大小 2.ArrayList对象创建 1.无参构造 2.有参构造1 3.有参构造2 3.添加元素add(E e)以及扩容机制 ​编辑 后言 前言 源码的剖析有助于理解设计模式&…

现代密码学|Rabin密码体制及其数学基础 | 椭圆曲线密码体制及其运算 | DH密钥交换及中间人攻击

文章目录 参考Rabin密码体制及其数学基础中国剩余定理二次剩余Rabin密码体制实例 椭圆曲线密码体制及其运算原理运算规则加密解密实例 DH密钥交换及中间人攻击中间人攻击 参考 现代密码学&#xff5c;Rabin密码体制及其数学基础 现代密码学&#xff5c;椭圆曲线密码体制及其运…

硬件选型规则

光源选型: 先用型号中带H的&#xff0c;没有的选标准的. 光源和光源控制器的搭配需要确保接口一致。 根据型号表中的最佳工作距离和相机的尺寸。 光源控制器选型&#xff1a; 首先选择海康风格系列光源控制器考虑与光源的接口匹配。功率应该满足接近光源功率。检查是否退市…

sharedPreference包的使用总结

文章目录 1 概念介绍2 实现方法3 示例代码我们在上一章回中介绍了"如何自定义评分条"相关的内容,本章回中将介绍如何实现本地存储.闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 Flutter是一套跨平台的UI框架,它不像原生SDK一样提供本地存储功能,因此,我们在…

TCP连接的时候遇到的异常(目标端口没开放)

import asyncioasync def check_port(ip, port, timeout1):"""检查目标 IP 和端口是否开放:param ip: 目标 IP 地址:param port: 目标端口:param timeout: 超时时间&#xff08;秒&#xff09;"""try:reader, writer await asyncio.open_connec…

C总结(C语言知识点,深化重难点)

C语言 1.使用C语言的7个步骤2.ASCII码3.提高程序可读性的机巧4.如何使用多种整形5.打印多种整形6.课移植类型&#xff1a;stdint.h和inttypes.h7.浮点数常量8.浮点值的上溢和下溢9.使用数据类型11.常量和C预处理器12.转换说明的意义12.1转换不匹配13.副作用和序列点14.数组简介…

burpsuite(6)暴力破解与验证码识别绕过

声明!!! 学习视频来自B站UP主泷羽sec&#xff0c;如涉及侵权马上删除文章 视频链接&#xff1a;泷羽sec-bilibili 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 项目地址&#xff1a;https://github.com/f0ng/cap…

抗DDOS设备

0x00 定义: 抗DDOS设备顾名思义&#xff0c;就是防御DDoS攻击的设备&#xff0c;通常包含三个部分&#xff1a;检测中心、清洗中心和管理中心 检测中心主要负责对流量进行检测&#xff0c;发现流量异常后上报管理中心&#xff0c;由管理中心下发引流策略至清洗中心&#xff0…

systemV信号量与消息队列

目录 引言 ipc简介 ipc在kernel的管理机制&#xff08;简介&#xff09; 信号量 理解信号量 原子 结论 mmap 消息队列 接口 引言 在复杂的软件系统中&#xff0c;进程间的协调和通信是确保系统高效、稳定运行的关键。System V是一套历史悠久且功能强大的进程间通信&a…

【CKS最新模拟真题】Falco 的运行时安全性

系列文章目录 【CKS最新模拟真题】获取多个集群的上下文名称并保存到指定文件中 文章目录 系列文章目录参考地址一、TASK二、解题过程1、问题一解题2、问题二解题 参考地址 CKS考试允许打开falco的地址 https://falco.org/docs/reference/rules/supported-fields/ 一、TASK …

Altium Designer学习笔记 32 DRC检查_丝印调整

基于Altium Designer 23学习版&#xff0c;四层板智能小车PCB 更多AD学习笔记&#xff1a;Altium Designer学习笔记 1-5 工程创建_元件库创建Altium Designer学习笔记 6-10 异性元件库创建_原理图绘制Altium Designer学习笔记 11-15 原理图的封装 编译 检查 _PCB封装库的创建Al…

【原生js案例】webApp实现鼠标移入移出相册放大缩小动画

图片相册这种动画效果也很常见&#xff0c;在我们的网站上。鼠标滑入放大图片&#xff0c;滑出就恢复原来的大小。现在我们使用运动定时器来实现这种滑动效果。 感兴趣的可以关注下我的系列课程【webApp之h5端实战】&#xff0c;里面有大量的css3动画效果制作原生知识分析&…

MetaGPT 安装

1. 创建环境 conda create -n metagpt python3.10 && conda activate metagpt2. 可编辑方式安装 git clone --depth 1 https://github.com/geekan/MetaGPT.git cd MetaGPT pip install -e .3. 配置 metagpt --init-config运行命令&#xff0c;在C盘位置C:\Users\325…

流量地球(Java Python JS C++ C )

题目描述 流浪地球计划在赤道上均匀部署了N个转向发动机,按位置顺序编号为0~N-1。 初始状态下所有的发动机都是未启动状态;发动机启动的方式分为”手动启动"和”关联启动"两种方式;如果在时刻1一个发动机被启动,下一个时刻2与之相邻的两个发动机就会被”关联启动”…

Milvus向量数据库04-Pipelines搭建RAG应用

Milvus向量数据库04-Pipelines搭建RAG应用 Zilliz Cloud Pipelines 可以将文档、文本片段和图像等非结构化数据转换成可搜索的向量并存储在 Collection 中。本文将介绍 Zilliz Cloud Pipelines 的三种主要类型并提供示例代码&#xff0c;展示如何使用 Pipelines 搭建 RAG 应用。…

离线写博客(失败) - 用Markdown来离线写博客

因为想控制一下用网&#xff0c;但是又有写博客的需求&#xff0c;所以想研究一下离线写博客。 我看CSDN上面好像有很多介绍&#xff0c;Windows Live Writer 啦&#xff0c;Markdown啦&#xff0c;还有一些其他的&#xff0c;我看了一下&#xff0c;好像 Markdown还有点儿靠谱…

【洛谷】B3844 [GESP样题 二级] 画正方形(详细注释)

#include <iostream> using namespace std; int main() {//声明一个整型变量n&#xff0c;用于接收输入的数值&#xff0c;该数值将决定后续输出图案的行数和列数int n; cin >> n;//声明两个整型变量i和j&#xff0c;分别用作外层循环和内层循环的计数器int i, j;/…

Linux-音频应用编程

ALPHA I.MX6U 开发板支持音频&#xff0c;板上搭载了音频编解码芯片 WM8960&#xff0c;支持播放以及录音功能&#xff01;本章我们来学习 Linux 下的音频应用编程&#xff0c;音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升&#xff0c;但是笔者仅向大家介绍 Li…

番茄社区双端视频APP源码_内附安装教程

新版视频源码|类似番茄APP视频付费软件APP源码教程 番茄社区双端视频APP源码&#xff0c;带安装教程&#xff0c;源码非组件&#xff0c;短视频、图片、交流、讨论、电影、电视剧。 没有什么问题宝塔就可以搭建&#xff0c;采集方面是火车头可以自己写规则&#xff01;

Onchain 正在蚕食 Offchain

目录 未来在链上 价值创造 技术加速 机构采用 小队&#xff1a;加速链上经济 我们正在进入一个代码就是货币、信任被编程、全球接入打破传统经济界限的时代。这种新兴模式就是我们所说的链上经济。 在此领域&#xff0c;Solana 因其在基础设施和应用程序方面的持续创新而脱颖而…