韦东山嵌入式Liunx入门驱动开发一(Hello 驱动编程、字符设备、GPIO基础知识、LED驱动、总线设备驱动模型)


本人学习完韦老师的视频,因此来复习巩固,写以笔记记之。
韦老师的课比较难,第一遍不知道在说什么,但是坚持看完一遍,再来复习,基本上就水到渠成了。
看完视频复习的同学观看最佳!
基于 IMX6ULL-PRO
参考视频 Linux快速入门到精通视频
参考资料:01_嵌入式Linux应用开发完全手册V5.1_IMX6ULL_Pro开发板.pdf

2024.4.18更新 利用图解方式探索内核函数调用
代码上传到git网站驱动代码

一、Hello 驱动编程

1-1 APP 打开的文件在内核中如何表示

APP打开文件时,得到一个整数fd,被称为文件句柄。对于APP的每一个文件句柄,在内核里面都有一个struct file结构体(include/linux/fs.h)与之对应。
请添加图片描述
open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file 结构体里f_flagsf _mode
去读写文件时,文件的当前偏移地址也会保存在f _pos 成员里。

int open(const char *pathname, int flags, mode_t mode);
1-2 打开字符设备节点时,内核中也有对应的 struct file

struct file_operations *f_op,由驱动程序提供。struct file_operations结构体是Linux内核中用于描述文件操作函数集合的结构体。它包含了许多函数指针成员,每个成员对应于不同的文件操作。
在这里插入图片描述

1-3 Hello 驱动程序

如何编写驱动程序步骤
1、确定主设备号,也可让内核分配
2、定义自己file_operations结构体
3、实现drv_open()、drv_read()、drv_write()等函数,写入file_operations结体
4、file_operations结构体告诉内核:register_chrdev()
5、谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
6、有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
7、其他完善:提供设备信息,自动创建设备节点:class_create, device_create

分析Hello 驱动程序代码

/*头文件省略,记得找到源码添加即可*/
#define MIN(a,b) ((a < b) ? a: b)

/*驱动程序*/
/*1、确定主设备号,也可以让内核分配*/
static int major = 0;   //设备号为0 系统自动分配
static char kernel_buf[1024];
static struct class *hello_class;

/*3、实现对应的 drv_open/drv_read/drv_write等函数,填入 file_operations结构体*/
static ssize_t hello_drv_read(struct file *file, char __user * buf, size_t size, loff_t * offset)
{
	int err;
	/*驱动调试 屏幕打印文件名,函数名,行号*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));  //内核存储的数据读到用户buf中
	return MIN(1024, size);
}

static ssize_t hello_drv_write(struct file *file, const char __user * buf, size_t size, loff_t * offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf,  buf, MIN(1024, size));  //用户buf的数据写到内核中
	return MIN(1024, size);
}

static int hello_drv_open(struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int hello_drv_close(struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/*2、定义自己的 file_operations结构体*/
static struct file_operations hell_drv = {
	 .owner = THIS_MODULE,
	 .open = hello_drv_open,
	 .read = hello_drv_read,
	 .write = hello_drv_write,
	 .release = hello_drv_close,
};

/*4、把 file_operations结构体告诉内核: register_chrdev 谁来注册驱动程序啊?*/
/*5、得有一个入口函数:安装驱动程序时,就会去调用这个入口函数*/
static int __init hello_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	/*注册驱动程序*/
	major = register_chrdev(0, "hello", &hell_drv);  
	/*创建设备节点*/
	hello_class = class_create(THIS_MODULE, "hello_class");
	
	err = PTR_ERR(hello_class);  /*错误处理函数*/
	if (IS_ERR(hello_class)){
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(0, "hello");
		return -1;
	}
	//device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");  /* /dev/hello */

	return 0;
}

/*6、有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0)); //MKDEV(major, 0)主设备号 次设备号
	class_destroy(hello_class);
	
	unregister_chrdev(0, "hello");
}

/*7、其他完善:提供设备信息,自动创建设备节点: class_create, device_create*/
module_init(hello_init);   	//修饰为入口函数
module_exit(hello_exit);
MODULE_LICENSE("GPL");	//遵守GPL协议

register_chrdev()是Linux内核中用于注册字符设备驱动程序的函数。在Linux系统中,字符设备是一种提供面向字符的I/O操作的设备,例如终端、打印机、鼠标、键盘、LED、I2C、SPI等。

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major = register_chrdev(0, "hello", &hell_drv);  

其中参数含义如下:
major: 主设备号,用于唯一标识该字符设备驱动程序。
name: 设备名称,用于在/proc/devices中显示。
fops: 指向struct file_operations结构体的指针,包含了该字符设备支持的操作函数。
当调用register_chrdev()函数时,内核会为指定的字符设备注册一个主设备号,并将其与对应的操作函数关联起来。这样,在用户空间通过设备文件和系统调用就能够访问和操作这个字符设备了。

class_create()是Linux内核中用于创建一个新的设备类的函数。在Linux系统中,设备类是一种将相关设备实例进行组织和分类的机制。

struct class *class_create(struct module *owner, const char *name);
hello_class = class_create(THIS_MODULE, "hello_class");

其中参数含义如下:
owner: 指向struct module类型的指针,表示拥有该设备类的模块。
name: 设备类的名称,用于在/sys/class/目录下创建相应的子目录。
成功调用class_create()函数后,会在/sys/class/目录下创建一个与指定名称对应的子目录,用于存放属于该设备类的设备实例。

device_create()是Linux内核中用于创建一个设备实例的函数。在Linux系统中,设备实例是设备类中的具体设备对象。通过device_create()函数可以创建一个新的设备实例,并将其与指定的设备类进行关联。

struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *format, ...);
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");  /* /dev/hello */

其中参数含义如下:
class: 指向struct class类型的指针,表示要创建设备实例所属的设备类。
parent: 指向struct device类型的指针,表示要创建设备实例的父设备。通常为NULL,表示没有父设备。
devt: 设备号,用于标识设备实例的唯一性。
drvdata: 指向设备驱动程序特定的数据结构的指针,可以传递给设备实例。
format: 设备名称的格式化字符串,用于在/sys/class/<class_name>/目录下创建相应的设备实例目录。
成功调用device_create()函数后,会在/sys/class/<class_name>/目录下创建一个与指定格式化字符串对应的设备实例目录,通过在/dev目录下创建相应的设备节点。

1-4 测试

(1) 修改makefile文件,内核路径对应于自己系统上的

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

(2) 打印内核信息,在串口处设置快捷方式
在这里插入图片描述

echo "7 4 1 7" > /proc/sys/kernel/printk

控制内核打印信息级别,>7的才输出。即不输出内核打印信息

/include/linux/kern_levels.h 
#define KERN_SOH    "\001"      /* ASCII Start Of Header */
 
#define KERN_EMERG  KERN_SOH "0"    /* system is unusable 系统崩溃前的信息*/
#define KERN_ALERT  KERN_SOH "1"    /* action must be taken immediately 需要立即处理的消息 */
#define KERN_CRIT   KERN_SOH "2"    /* critical conditions 严重情况*/
#define KERN_ERR    KERN_SOH "3"    /* error conditions 错误*/
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions 警告*/
#define KERN_NOTICE KERN_SOH "5"    /* normal but significant condition 注意 */
#define KERN_INFO   KERN_SOH "6"    /* informational 普通信息*/
#define KERN_DEBUG  KERN_SOH "7"    /* debug-level messages 调试*/

(3) 装载驱动insmod
(4)显示的当前内核已经加载的模块和驱动lsmod
在这里插入图片描述
(5)卸载驱动rmmod
(6)显示主设备号 register_chrdev()

cat /proc/devices

在这里插入图片描述
(7)创建设备节点class_create()和device_create()
在这里插入图片描述

1-5 补充知识
(1) module_init/module_exit 的实现

一个驱动程序有入口函数、出口函数。驱动程序可以被编进内核里,也可以被编译为ko文件后手工加载。

module_init(hello_init)
module_exit(hello_exit);

当编译为ko文件时,使用insmod 命令加载驱动时,内核调用 init_module 函数,实际上就是调用hello_init函数;使用rmmod命令卸载驱动时,内核都是调用cleanup_module 函数,实际上就是调用hello_ exit 函数。

(2) register_chrdev 的内部实现

chrdevs[i]数组项是一个链表头,链表里每一个元素都是一个char_device_struct 结构体,每个元素表示一个驱动程序。

struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int basem inor;
	int minorct;
	char name[64];
	struct cdev *cdev; /* will die */
}*chrdevs[CHRDEV_MAJOR_HASH_SIZE];

它指定了主设备号major 、次设备号 baseminor 、个数 minorct ,在 cdev中含有 file_operations 结构体。因此,内核通过主、次设备号,找到对应的file_operations 结构体”。
APP 打开某个字符设备节点时,进入内核。在内核里根据字符设备节点的主、次设备号,通过cdev_add()函数调用在cdev_map链表中快速得到cdev,再从cdev中得到file_operations结构体。
cdev结构体表示一个字符设备对象。
在这里插入图片描述

(3) class_destroy/device_create 浅析

在/sys目录下创建一些目录、文件,这样 Linux 系统中的 APP就可以根据这些目录或文件来创建设备节点。
在这里插入图片描述

1-6 拓展知识(重要)
(1) register_chrdev_region()

在后续的不断学习中得知早期的驱动是以register_chrdev()函数的内核注册的形式 ,现在都是采用register_chrdev_region()以及alloc_chrdev_region()进行注册设备。
register_chrdev_region()为静态注册,指定设备号。alloc_chrdev_region()为动态注册,自动分配设备号。
在这里插入图片描述
使用方法 指定主设备号和此设备号,然后调用MKDEV合并生成设备ID,调用register_chrdev_region()函数注册一个设备名为dengzj_led的设备,最后编译为.ko文件insmod后,可查看。

#define LED_MA   300  //主设备号 用于区分不同种类的设备  
#define LED_MI   0  //次设备号 用于区分同一类型的多个设备

int  devno = MKDEV(LED_MA,LED_MI); //合并主次设备号,生成设备ID

//1.注册设备号
ret = register_chrdev_region(devno,1,"dengzj_led");
if(ret<0){
	printk("register_chrdev_region fail \n");
	return ret;
}
insmod led.ko
cat /proc/devices

请添加图片描述

(2) 字符设备驱动程序
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>		/*for MKDEV register_chrdev_region*/
#include <linux/cdev.h>  	/*字符设备头文件*/

#define LED_MA   300  //主设备号 用于区分不同种类的设备  
#define LED_MI   0  //次设备号 用于区分同一类型的多个设备

struct cdev cdev; //定义字符设备

int led_open (struct inode * inode, struct file * file){
    printk(" led open go\n");
    return 0;
}

int led_release(struct inode *inode, struct file * file){
    printk(" led release go\n");
    return 0;
}

 struct file_operations led_fops={	//实现需要的文件操作
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
 };	
	
static int led_init(void)
{
	int  devno = MKDEV(LED_MA,LED_MI); //合并主次设备号,生成设备ID
	int  ret; 

	//1.注册设备号
	ret = register_chrdev_region(devno, 1, "dengzj_led");
	if(ret < 0){
		printk("register_chrdev_region fail \n");
		return ret;
	}

	//2.初始化字符设备
	cdev_init(&cdev, &led_fops);  //字符设备的初始化

	ret = cdev_add(&cdev, devno, 1); 
	if(ret < 0){
		printk("cdev_add dengzj \n");
		return -1;
	}

	printk("led init deng_zj \n");
	return 0;
}

static void led_exit(void)
{
	//配对 注销设备
	int  devno = MKDEV(LED_MA,LED_MI); 
	cdev_del(&cdev);
	unregister_chrdev_region(devno,1);
	
	printk("led exit dengzj\n");
}

module_init(led_init); //模块加载入口声明
module_exit(led_exit); //模块卸载入口声明
MODULE_LICENSE("GPL"); //模块免费开源声明
MODULE_AUTHOR("guangzhou dengzj"); //模块作者声明(可选)
(3) 字符设备框架

重要结构体struct cdev
请添加图片描述

二、GPIO基础知识

GPIO: General purpose input/output ,通用的输入输出口

2-1 GPIO结构

(1) 有多组GPIO,每组有多个GPIO。例如:GPIO2_IO15。
(2) 使能:电源/时钟
(3) 模式(Mode):引脚可用于GPIO或其他功能
(4) 方向:引脚Mode设置为GPIO时,可以继续设置它是输出引脚,还是输入引脚
(5) 数值:
◼ 对于输出引脚,可以设置寄存器让它输出高、低电平
◼ 对于输入引脚,可以读取寄存器得到引脚的当前电平

将某一位置1,采用操作

val = val |(1<<n)
val |= (1<<n)

将某一位清0,用与且取反操作

val = val & ~(1<<n)
val &= ~(1<<n)

GPIO控制涉及CCM、IOMUXC、GPIO模块本身等三个部分。
CCM:Clock Controller Module (时钟控制模块),用于设置是否向GPIO模块提供时钟。参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM) 》

IOMUXC:IOMUX Controller,IO复用控制器。引脚的模式和功能,参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC) 》。
在这里插入图片描述
在这里插入图片描述

2-2 GPIO模块内部

在这里插入图片描述
① GPIOx_GDIR:设置引脚方向,每位对应一个引脚 1-output,0-input
② GPIOx_DR:设置输出引脚的电平,每位对应一个引脚 ,1-高电平, 0-低电平
③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚 1-高电平, 0-低电平
GPIO设置步骤
1、使能时钟/电源
2、选择GPIO模式
3、 设置IO方向
4、设置高/低电平

三、LED驱动

3-1 最简单的LED驱动程序

在这里插入图片描述
SVC用于生成系统函数调用.例如,用户程序不允许直接访问硬件,操作系统可以通过SVC提供对硬件的访问。 因此,当用户程序想要使用某些硬件时,可以使用SVC指令,然后执行操作系统中的软件异常处理程序,并提供用户应用程序请求的服务。
(1) 驱动怎么操作硬件?
通过ioremap映射寄存器的物理地址得到虚拟地址,读写虚拟地址。
(2) 驱动怎么和APP传输数据?
通过 copy to_user()、copy_from_user() 这2个函数。

volatile关键字,防止程序被优化,如某一变量的点灯、灭灯(写操作:*p=0;*p=1读操作亦是如此)

ioremap()可以将物理地址映射为虚拟地址

 virt_addr = ioremap(phys_addr, size);

把物理地址phys_addr 开始的一段空间 ,映射为虚拟地址返回值是该段虚拟地址的首地址。实际上,它是按页4096字节进行映射的,即整页整页地映射的。

/*头文件参考源码即可*/
static int major;
static struct class *led_class;

/*register*/
//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3地址:0x02290000 + 0x14
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;

//GPIO5_GDIR地址:0x020AC004
static volatile unsigned int *GPIO5_GDIR;

//GPIO5_DR地址:0x020AC000
static volatile unsigned int *GPIO5_DR;

static ssize_t led_write(struct file *file, const char __user *buf,
			 size_t count, loff_t *ppos)
{
	char val;
	int ret;
	/*copy_from_user : get data from app 用户空间的数据拷贝到内核空间
	*status中的数据在buf中
	*/
	ret = copy_from_user(&val, buf, 1);/*buf里的值是用户空间的state buf=&state*/

	/*to set GPIO register:out 1 or 0*/
	if(val){
		/*set gpio to let led on*/
		*GPIO5_DR &= ~(1<<3);	//输出0
	}
	else{
		/*set gpio to let led off 熄灭*/
		*GPIO5_DR |=(1<<3);	//输出1
	}

	return 1;
}

static int led_open(struct inode *inode, struct file *filp)
{
	/*enable gpio
	*configure gpio5_io3 as gpio
	*configure gpio5_io3 as output
	*/
	/*配置为GPIO引脚功能*/
	*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf; //清零
	*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x05;

	/*GPIO引脚配置为输出方式*/
	*GPIO5_GDIR |= (1<<3);
	return 0;
}

static struct file_operations led_fops = {
	.owner	= THIS_MODULE,
	.write	= led_write,
	.open	= led_open,
};

/*入口函数*/
static int __init led_init(void)
{

	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/*注册字符设备*/
	major = register_chrdev(0, "100ask_led", &led_fops);
	
	/*ioremap 将物理地址映射为虚拟地址*/
	//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3地址:0x02290000 + 0x14
	IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
	
	//GPIO5_GDIR地址:0x020AC004
	GPIO5_GDIR = ioremap(0x020AC004, 4);
	
	//GPIO5_DR地址:0x020AC000
	GPIO5_DR = ioremap(0x020AC000, 4);
	
	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled"); /*系统会创建 /dev/myled的设备节点*/

	return 0;
}

static void __exit led_exit(void)
{
	iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
	iounmap(GPIO5_GDIR);
	iounmap(GPIO5_DR);
	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	
	unregister_chrdev(major, "100ask_led");
};
3-2 LED驱动程序框架

字符设备框架
在这里插入图片描述
LED驱动框架
在这里插入图片描述
应用程序访问驱动程序,会打开一个设备节点,根据设备节点的主、次设备号,在内核里找到file_operations结构体。
LED 驱动能支持多个板子的基础: 分层 思想
这里的目的其实就是把硬件部分代码剥离出来,这种构思的方法以此来引出后面的设备树
在这里插入图片描述

程序解析
(1) led_opr.h 面向对象编程,将LED抽象为一个led_operations 结构体

#ifndef _LED_OPR_H
#define _LED_OPR_H

struct led_operations{
	int(*init)(int which); /*初始化LED, which-哪个LED*/   /*函数指针*/
	int (*ctl)(int which, char status); /*控制LED,which-哪个LED,status:1 亮,0 灭*/
};
struct led_operations *get_board_led_opr(void); /*指针函数 返回值是结构体类型的指针*/

#endif
/*宏定义作用:防止头文件重复定义*/

#ifndef 宏名(_LED_OPR)
#define 宏名(_LED_OPR)

#endif
作用:防止头文件重复定义出错,即当a.h和b.h引用了该头文件,但是d.c又引用a.h和b.h文件,会导致引用两次。

(2) board_demo.c

#include <linux/gfp.h>
#include "led_opr.h"

static int board_demo_led_init(int which) /*初始化LED,which-哪个LED*/
{
	printk("%s %s line %d, led %d\n", __FILE__,__FUNCTION__, __LINE__, which);
	return 0;
}

static int board_demo_led_ctl(int which, char status) /*控制LED,which-哪个LED,status:1 亮 0灭*/
{
	printk("%s %s line %d, led %d\n", __FILE__,__FUNCTION__, __LINE__, which);
	return 0;
}

static struct led_operations board_demo_led_opr = {
	.init = board_demo_led_init,
	.ctl = board_demo_led_ctl,
};

struct led_operations *get_board_led_opr(void)
{
	return &board_demo_led_opr;
}

对led_operations结构体的实例化board_demo_led_opr以及函数体的实现

(3) lederv.c

p_led_opr = get_board_led_opr();

最终p_led_opr指向board_demo_led_opr结构体,通过调用其结构体成员(2个函数指针来实现相应内容)

3-3 课后作业1

实现读LED状态的功能:涉及APP和驱动。
思路单独编写read部分代码
在这里插入图片描述

3-4 具体单板的LED驱动程序

位于02_led_drv_for_boards文件夹中
其实就是在01文件夹中抽象的结构体对应的函数中的基础上添加了iMX6ULL具体硬件LED的信息,部分下面代码所示

		//效率太低 进行了两次读-修改-写操作
		//*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~(0xf); //清零
		//*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= (5);

		//2、设置GPIO5_IO03用于GPIO
		//只进行一次读操作 一次写操作
		val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
		val &= ~(0xf);
		val |= (5);
		*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;

课后作业1:在出口函数处添加iounmap()函数

static void __exit led_exit(void)
{
	iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
	iounmap(GPIO5_GDIR);
	iounmap(GPIO5_DR);
	int i;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	for(i = 0; i < p_led_opr->num; i++){
		device_destroy(led_class, MKDEV(major, i)); //MKDEV(major, i)主设备号 次设备号
	}
	
	class_destroy(led_class);
	unregister_chrdev(0, "100ask_led"); /*注销驱动*/
}

课后作业2

/*部分代码*/
static int board_demo_led_init(int which) /*初始化LED,which-哪个LED*/
{
	unsigned int val;
	int i = 0;
	//printk("%s %s line %d, led %d\n", __FILE__,__FUNCTION__, __LINE__, which);
	if(which == 0){   /*which==次设备号*/
		if(!i){
		CCM_CCGR1 = ioremap(0x20C406C, 4);
		IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290014, 4);
		GPIO5_GDIR = ioremap(0x020AC004, 4);
		GPIO5_DR = ioremap(0x020AC000, 4);
		i++;
		}

		//1、使能GPIO5
		*CCM_CCGR1 |= (3<<30);

		//效率太低 进行了两次读-修改-写操作
		//*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~(0xf); //清零
		//*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= (5);
		
		//2、设置GPIO5_IO03用于GPIO
		//只进行一次读操作 一次写操作
		val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
		val &= ~(0xf);
		val |= (5);
		*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
		
		//3、设置GPIO5_IO03作为output引脚
		*GPIO5_GDIR |= (1<<3);
	}
	else{
		/*由于IM6ULL只有一个灯可以点,故难以找到对应LED的GIIO口,所以提供思路即可。若有LED,在此处配置第二个LED即可*/
	}
	return 0;
}

static int board_demo_led_ctl(int which, char status) /*控制LED,which-哪个LED,status:1 亮 0灭*/
{
	//printk("%s %s line %d, led %d\n", __FILE__,__FUNCTION__, __LINE__, which);
	if(which == 0){
		if(status){		/*on: output 0 点灯*/
			*GPIO5_DR &= ~(1<<3);
		}
		else{			/*off: output 1 熄灭*/
			*GPIO5_DR |= (1<<3);
		}	
	}
	else{
		if(status){		/*on: output 0 点灯*/
			//同理,若有LED灯,在此处配置即可 *GPIO5_DR &= ~(1<<3);
		}
		else{			/*off: output 1 熄灭*/
			//*GPIO5_DR |= (1<<3);
		}		
	}
	return 0;
}
static struct led_operations board_demo_led_opr = {
	.num = 2,
	.init = board_demo_led_init,
	.ctl = board_demo_led_ctl,

};

四、驱动设计的思想

在面向对象时,字符设备驱动程序抽象出一个file_operations结构体;LED驱动程序针对硬件部分抽象出led_operations结构体。
分层思想:
在这里插入图片描述
分离思想:
在这里插入图片描述
对于引脚的操作是相同的,那可以针对该芯片写出比较通用的硬件操作代码;
比如board_A.c 使用芯片Y ,那就可以写出 chipY_gpio.c,它实现芯片Y的GPIO 操作,适用于芯片Y的所有GPIO引脚。使用时,我们只需要在board_A_led.c 中指定使用哪一个引脚即可。

五、总线设备驱动模型

5-1 简介

对于硬件资源,用platform_device结构体来表示;对于硬件的操作,用platform_driver结构体表示。
内核源码: include\linux\platform_device.h
在这里插入图片描述
在内核中有一个虚拟的总线platform_bus_type,它有2个链表结构,左边是
设备Dev链表,右边是驱动Drv链表。左边的设备链表和右边的驱动链表会进行一一比较(通过platform_match函数),若匹配成功,就会调用platform_driver中的probe函数。
如何进行匹配?

(1) platform_device结构体
在这里插入图片描述
(2) platform_driver结构体
在这里插入图片描述
(3) platform_match
在这里插入图片描述
在这里插入图片描述
总线设备驱动编写程序步骤
(1) 分配、设置、注册platform_device结构体,在里面定义所用资源,指定设备名字。
(2) 分配、设置、注册platform_driver结构体,在其中的probe函数里,分配、设置、注册file_operations结构体,并从platform_device 中确实所用硬件资源,指定platform_driver的名字。

5-2 LED总线设备驱动模型

之前的LED驱动框架
在这里插入图片描述
现目标框架如下
在这里插入图片描述
解释:
1、leddrv.c注册驱动程序,创建设备节点类,platform_device(相当于后面的设备树)指定硬件信息,platform_driver指定对硬件信息进行操作(初始化、控制)。
2、注册平台设备驱动两个结构体,并且调用register_led_operations(&board_demo_led_opr)()提前为leddrv.c打开接口。平台总线设备驱动匹配后,调用chip_demo_gpio_probe()来获取硬件资源信息,并且再调用led_class_create_device(),创建设备节点。/dev/100ask_led0,1,…
3、此时应用层open函数可以打开设备,在调用驱动程序的led_drv_open(),在函数中初始化硬件p_led_opr->init(minor),最终调用到底层board_demo_led_init()函数。

1、返回该dev中某类型type资源中的第几个num

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)

2、返回该dev所用的第几个num中断

int platform_get_irq(struct platform_device *dev, unsigned int num)

3、通过名字name)返回该 dev的某类型type资源

struct resource *platform_get_resource_byname(struct platform_device *dev, unsigned int type, const char *name)

4、通过名字name)返回该dev的中断号

int platform_get_irq_byname(struct platform_device *dev const char *name)

程序解析
board_A_led.c中构造platform_device,提供LED硬件相关资源

static void led_dev_release(struct device *dev)
{
}

static struct resource resources[] = {
        {
                .start = GROUP_PIN(3,1),
                .flags = IORESOURCE_IRQ, /*表示是哪一类资源 这里是假设IRQ表示引脚*/
                .name = "100ask_led_pin",
        },
        {
                .start = GROUP_PIN(5,8),
                .flags = IORESOURCE_IRQ,
                .name = "100ask_led_pin",
        },
};

static struct platform_device board_A_led_dev = {
        .name = "100ask_led",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release, /*防止在调用
platform_device_unregister 时会出现警告*/
         },
};

static int __init led_dev_init(void)
{
    int err;
    err = platform_device_register(&board_A_led_dev);   /*注册platform_device*/
    return 0;
}

static void __exit led_dev_exit(void)
{
    platform_device_unregister(&board_A_led_dev);
}

module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

chip_demo_gpio.c中构造platform_device结构体与之匹配

static int g_ledpins[100];
static int g_ledcnt = 0;

/*
*省略部分代码
*/

static struct led_operations board_demo_led_opr = {
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
};

struct led_operations *get_board_led_opr(void)
{
    return &board_demo_led_opr;
}

static int chip_demo_gpio_probe(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1){
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i++);
        if (!res)
            break;
        
        g_ledpins[g_ledcnt] = res->start; 	/*记录引脚 */
        led_class_create_device(g_ledcnt);  /*创建device_create 有多少引脚,创建多少个*/
        g_ledcnt++;
    }
    return 0;
    
}

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1){
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
        if (!res)
            break;
        
        led_class_destroy_device(i);
        i++;
        g_ledcnt--;
    }
    return 0;
}

static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
    },
};

static int __init chip_demo_gpio_drv_init(void)
{
    int err;
    err = platform_driver_register(&chip_demo_gpio_driver); 
    register_led_operations(&board_demo_led_opr);
    
    return 0;
}

static void __exit lchip_demo_gpio_drv_exit(void)
{
    platform_driver_unregister(&chip_demo_gpio_driver);
}

module_init(chip_demo_gpio_drv_init);
module_exit(lchip_demo_gpio_drv_exit);

MODULE_LICENSE("GPL");

a.c编译为a.ko ,里面定义了func_a;如果它想让b.ko使用该函数,那
么a .c里需要导出此函数。并且,使用时要先加载a.ko 。如果先加载b.ko

EXPORT_SYMBOL(led_device_create);
5-3 平台总线驱动函数关系调用图解

请添加图片描述
在这里插入图片描述

  • 43
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,那我来给你讲一下如何编写一个简单的Linux点灯驱动并测试。 1. 编写驱动程序 首先,你需要打开一个文本编辑器,创建一个名为led_drv.c的文件,然后编写一个简单的驱动程序,如下所示: ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/gpio.h> #define LED_PIN 4 static int __init led_drv_init(void) { printk("LED driver initialized.\n"); gpio_request(LED_PIN, "LED"); gpio_direction_output(LED_PIN, 1); return 0; } static void __exit led_drv_exit(void) { gpio_set_value(LED_PIN, 0); gpio_free(LED_PIN); printk("LED driver exited.\n"); } module_init(led_drv_init); module_exit(led_drv_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple LED driver."); ``` 这个驱动程序使用GPIO4控制一个LED灯,初始化时将LED灯亮起来,退出时将LED灯熄灭。注意,这个驱动程序还需要在Makefile中进行编译。 2. 编译驱动程序 在终端中进入驱动程序所在的目录,然后使用以下命令进行编译: ```bash make -C /lib/modules/$(uname -r)/build M=$(pwd) modules ``` 其中,/lib/modules/$(uname -r)/build是Linux内核源代码所在的目录,$(pwd)表示驱动程序所在的目录,modules表示编译为模块,编译成功后会生成一个名为led_drv.ko的文件。 3. 加载驱动程序 使用以下命令将驱动程序加载到内核中: ```bash sudo insmod led_drv.ko ``` 如果没有出现错误,说明驱动程序已经成功加载到内核中了。 4. 测试驱动程序 现在,你可以控制LED灯了!使用以下命令将LED灯熄灭: ```bash sudo echo 0 > /sys/class/gpio/gpio4/value ``` 使用以下命令将LED灯点亮: ```bash sudo echo 1 > /sys/class/gpio/gpio4/value ``` 如果一切正常,你应该可以看到LED灯的亮灭状态发生了变化。 5. 卸载驱动程序 使用以下命令将驱动程序从内核中卸载: ```bash sudo rmmod led_drv ``` 至此,你已经成功地编写了一个简单的Linux点灯驱动并进行了测试。需要注意的是,在测试过程中,一定要小心操作,以免对硬件造成损坏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值