本文章参考韦东山嵌入式Linux应用开发完全手册👨💻百问网资料下载中心 — 百问网资料下载中心 3.0 文档 (100ask.net)
简介
Linux驱动=驱动框架+硬件操作=驱动框架+单片机;
在上篇文章中已经介绍了实现一个字符设备驱动程序的详细过程,本文章以一个简单的例子LED驱动程序来介绍Linux中不同的驱动编写方法;主要介绍驱动编写的三种方法
1. 传统写法
1. 最简单的驱动程序
直接在驱动程序中实现资源,要使用的引脚、以及怎么操作引脚都在驱动代码中,这种方式最简单,但是扩展性差,想改变引脚配置需要直接修改驱动中的代码,不符合驱动设计的思想;
驱动源码如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <asm/io.h>
static unsigned int major;
static struct class *led_class;
/* registers */
// 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;//volatile关键字告诉编译器,该变量的值可能会在未被代码显式修改的情况下被外部因素改变,因此在优化编译时不应该对其访问进行优化。
static int led_open(struct inode *inode, struct file *file)
{
/* enable gpio GPIO5默认使能,无需设置*/
/*configure pin as gpio*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5;
/*configure gpio as output */
*GPIO5_GDIR |= (1<<3);
return 0;
}
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int ret;
char kernel_buf;
/* copy_from_user : get data from app */
ret=copy_from_user(&kernel_buf, buf, 1);
/* to set gpio register: out 1/0 */
if (kernel_buf)
{
/* set gpio to let led on */
*GPIO5_DR &= ~(1<<3);
}
else
{
/* set gpio to let led off */
*GPIO5_DR |= (1<<3);
}
return 1;
}
static struct file_operations led_drv=//定义一个静态的file_operations结构体,并且给它的成员赋值
{
.owner = THIS_MODULE,
.open =led_open,
.write =led_write,
};
static int __init led_drv_init(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
major=register_chrdev( 0, "led_drv",&led_drv);//第一个参数输入0,让系统给设置主设备号,返回主设备号
/* ioremap
ioremap是一个在Linux内核编程中常用的函数,用于映射物理地址到内核虚拟地址空间中。
在Linux内核中,访问外设的寄存器、设备内存或其他物理地址空间时,
需要使用ioremap函数将这些物理地址映射到内核虚拟地址空间中,
以便内核可以通过指针来访问这些地址。
*/
// IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);//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 MKDEV(major, 0):主设备号和次设备号*/
return 0;
}
static void __exit led_drv_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, "led_drv");
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
这是最简单的驱动程序只需以下步骤:
1. 确定主设备号
2. 定义实现自己的file_operations结构体(核心)
3. 实现入口函数,在入口函数中注册驱动程序
4. 实现出口函数,在出口函数中卸载驱动程序
5. 其他,如创建设备节点
在file_operations结构体中配置、操作LED的GPIO引脚,本例只需要实现file_operations结构体中的.open、.write函数,在open中实现LED引脚的配置,在write中操作引脚实现点灯操作。
需要根据自己开发板手册查看LED灯的GPIO引脚信息然后进行配置、操作;
配置GPIO其实就是设置对应寄存器信息,GPIO操作相关名词解释如下;
配置过程:
- 使能:设置CCM_CCGRx寄存器中某位使能对应的GPIO模块 // 默认是使能的;
- 模式:设置IOMUX来选择引脚用于GPIO;
- 方向:设置GPIOx_GDIR中某位为1/0,把该引脚设置为输出功能/输入模式;
- 数值:写/读GPIOx_DR某位的值,
注意:
1. 驱动怎么操作硬件?
通过 ioremap 映射寄存器的物理地址得到虚拟地址,读写虚拟地址。这是因为硬件寄存器通常位于物理内存地址空间中,而内核和用户空间程序通常不能直接访问这些物理地址。为了安全和方便管理,Linux内核使用虚拟内存系统来隔离物理内存和虚拟内存。因此,驱动程序需要一种机制来将硬件寄存器的物理地址映射到内核的虚拟地址空间中,这样内核代码就可以像访问普通内存一样来读写这些寄存器了。在入口函数中映射寄存器,便于file_operations结构体中的函数配置、操作LED灯的GPIO;(映射寄存器通常加上关键字volatile 避免编译器优化,确保寄存器的值准确)
2. 驱动怎么和APP传输数据?
同上原因,通过copy_to_user、copy_from_user这2个函数,在用户空间和内核空间之间安全地复制数据。
仅仅展示驱动程序,想测试需要实现应用程序和管理驱动程序;
2. 分层/分离思想
2.1 分层思想
在上述的编写过程中,所有硬件资源全部在驱动程序中,仅仅局限于实现单一功能,那么如何实现一个驱动程序支持多个开发板呢?可以使用分层思想;
把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c):
以面向对象的思想,改进代码,抽象出一个结构体:
每个单板相关的board_X.c实现自己的led_operations结构体,供上层 的leddrv.c调用:
驱动程序文件如下 :
简明说,就是把上述驱动程序中的硬件操作拎出来单独实现,达到驱动和硬件解耦,想使用不同开发板时,直接修改底层硬件资源代码,驱动代码不用修改;
直接贴源码:
在led_operations.h文件中抽象出开发板结构体,因为本文用简单的LED驱动程序演示,所以只需要实现两个函数,一个函数中配置LED引脚资源,另一个函数中用于控制LED亮灭。
#ifndef __led_operations_H
#define __led_operations_H
struct led_operations
{
int num;
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
在board_demo.c中填充led_operations中的函数指针,并返回结构体指针;
主要实现LED灯GPIO引脚地址的映射,然后设置对应GPIO引脚寄存器,
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_operations.h"
#include <asm/io.h>
static volatile unsigned int *CCM_CCGR1 ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR ;
static volatile unsigned int *GPIO5_DR ;
static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
{
printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
if(!CCM_CCGR1)
{
CCM_CCGR1=ioremap(0x020C4000 + 0x6C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3=ioremap(0x02290000 + 0x14, 4);
GPIO5_GDIR=ioremap(0x020AC004, 4);
GPIO5_DR=ioremap(0x020AC000, 4);
}
*CCM_CCGR1|=0xc000000;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &=0xf0;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |=0x05;
*GPIO5_GDIR |=(1<<3);
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, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
if (which == 0)
{
if (status) /* on: output 0*/
{
/* d. 设置GPIO5_DR输出低电平
* set GPIO5_DR to configure GPIO5_IO03 output 0
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b0
*/
*GPIO5_DR &= ~(1<<3);
}
else /* off: output 1*/
{
/* e. 设置GPIO5_IO3输出高电平
* set GPIO5_DR to configure GPIO5_IO03 output 1
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b1
*/
*GPIO5_DR |= (1<<3);
}
}
return 0;
}
static struct led_operations board_demo_led_opr = {
.num=1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_demo_led_opr;
}
在驱动程序中只需调用底层封装的函数指针,实现驱动硬件解耦,想使用其他开发板只需提供对应的开发板硬件资源。
本驱动程序中,在入口函数中注册file_operations结构体,然后调用函数获得底层封装的开发板硬件结构体指针,创建类,和设备号,在file_operations结构体的函数指针中调用底层接口函数。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <asm/io.h>
#include "led_operations.h"
static unsigned int major;
static struct class *led_class;
struct led_operations *p_led_operations;//在入口函数中获得这个指针
static int led_drv_open(struct inode *node, struct file *file)
{
int minor = iminor(node);
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/*根据次设备号初始化ed */
p_led_operations->init(minor);//从文件中获得次设备号
return 0;
}
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int ret;
char status;
struct inode *inode = file_inode(file);
int minor = iminor(inode);
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* copy_from_user : get data from app */
ret=copy_from_user(&status, buf, 1);
/*根据次设备号和status控制led */
p_led_operations->ctl(minor, status);。
return 1;
}
static struct file_operations led_drv=//定义一个静态的file_operations结构体,并且给它的成员赋值
{
.owner = THIS_MODULE,
.open =led_drv_open,
.write =led_drv_write,
};
static int __init led_drv_init(void)
{
int err;
int i;
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
major=register_chrdev( 0, "myled",&led_drv);//第一个参数输入0,让系统给设置主设备号,返回主设备号
led_class = class_create(THIS_MODULE, "myled_class");//led_class" 是设备类的名称,用于标识该类。
err = PTR_ERR(led_class);//PTR_ERR 是一个宏,用于将指针转换为错误码。
if (IS_ERR(led_class)) //宏 IS_ERR,它用于检查一个指针是否为错误指针
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "myled");
return -1;
}
p_led_operations=get_board_led_opr();
for(i=0;i<p_led_operations->num;i++)
{
device_create(led_class, NULL, MKDEV(major, i), NULL, "myled%d",i); /* /dev/myled 0/1 MKDEV(major, 0):主设备号和次设备号*/
}
return 0;
}
static void __exit led_drv_exit(void)
{
int i;
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
for (i=0;i<p_led_operations->num;i++)
device_destroy(led_class, MKDEV(major, i));
class_destroy(led_class);
unregister_chrdev(major, "myled");
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
2.2 分离
在上述中介绍了分层的思想,将整个驱动层序分成上下两层,上层中实现和硬件无关的操作,比如注册字符设备驱动,在下层实现硬件相关操作,比如实现某个开发板的LED操作;
在上述分层思想的驱动框架下,如果硬件上更换了一个引脚来控制LED灯,就需要修改led_operations结构体中的init、ctl函数。可以在此基础上引用分离思想优化驱动程序;
struct led_operations
{
int num;
int (*init) (int which); /* 初始化LED, which-哪个LED */
int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};
实际情况是,每一款芯片它的 GPIO 操作都是类似的。比如:GPIO1_3、 GPIO5_4 这2个引脚接到LED:
GPIO1_3属于第1组,即GPIO1。
a) 有方向寄存器DIR、数据寄存器DR等,基础地址是 addr_base_addr_gpio1。
b) 设置为output引脚:修改GPIO1的DIR寄存器的bit3。
c) 设置输出电平:修改GPIO1的DR寄存器的bit3。
GPIO5_4属于第5组,即GPIO5。
a) 有方向寄存器DIR、数据寄存器DR等,基础地址是 addr_base_addr_gpio5。
b) 设置为output引脚:修改GPIO5的DIR寄存器的bit4。
c) 设置输出电平:修改GPIO5的DR寄存器的bit4。
既然引脚操作那么有规律,并且这是跟主芯片相关的,那可以针对该芯片写 出比较通用的硬件操作代码。 比如board_A.c使用芯片chipY,那就可以写出:chipY_gpio.c,它实现 芯片Y的GPIO操作,适用于芯片Y的所有GPIO引脚。 使用时,我们只需要在board_A_led.c中指定使用哪一个引脚即可。程序结构如下:
以面向对象的思想,在board_A_led.c中实现led_resouce结构体,它定 义“资源”──要用哪一个引脚。
在chipY_gpio.c中仍是实现led_operations结构体,它要写得更完善, 支持所有GPIO。
分层/分离思想驱动框架源码文件:
在led_resource.h文件中,使用一个简单的结构体描述一个GPIO引脚,并定义一个声明一个函数用于返回该结构体;在头文件中宏定义用于引脚操作;
#ifndef __led_resource_H
#define __led_resource_H
#define GROUP(x) (x>>16)//将值x的高十六位移到底16位存储
#define PIN(x) (x&0xFFFF)//将值x的高16位清零,保留低16位
#define GROUP_PIN(g,p) ((g<<16) | (p))//将值g低16位移到高16位并或上p
/* bit[31:16] = group */
/* bit[15:0] = which pin */
/* 用一个整数低16位表示pin,高16位表示group*/
struct led_resource
{
int pin;
};
struct led_resource *get_led_resouce(void);//定义函数返回结构体指针
#endif
在board_A_led.c文件中指定要使用哪个引脚,
#include "led_resource.h"//在当前文件目录中查询该头文件
static struct led_resource board_A_led =
{
.pin = GROUP_PIN(3,1),//确定使用gpio引脚,第几组第几位,用整数pin表示
};
struct led_resource *get_led_resouce(void)//定义声明了一个函数 get_led_resource,它返回一个指向 struct led_resource 类型的指针。
{
return &board_A_led;
}
在led_operations.h文件中抽象一个结构体用于描述引脚的配置、操作,并声明一个函数用于返回该结构体指针;其实就是抽象出结构体通过函数指针实现引脚的配置;
#ifndef __led_operations_H
#define __led_operations_H
struct led_operations
{
int num;
int (*init) (int which); /* 初始化LED, which-哪个LED */
int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};
struct led_operations *led_operations(void);//定义函数返回结构体指针
#endif
在chip_demo_gpio.c文件中实现上述定义的引脚操作结构体,通过board_A_led.c文件中的led_rsc = get_led_resouce()获得要使用的引脚;填充具体引脚操作函数,为了便于使用应该在本文件中实现一个开发板所有GPIO引脚;这里仅仅展示一个实例,实际使用只用实现每个引脚的配置就行
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_operations.h"
#include "led_resource.h"
static struct led_resource *led_rsc;
static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
{
//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
if (!led_rsc)
{
led_rsc = get_led_resouce();
}
printk("init gpio: group %d, pin %d\n", GROUP(led_rsc->pin), PIN(led_rsc->pin));
switch(GROUP(led_rsc->pin))
{
case 0:
{
printk("init pin of group 0 ...\n");
break;
}
case 1:
{
printk("init pin of group 1 ...\n");
break;
}
case 2:
{
printk("init pin of group 2 ...\n");
break;
}
case 3:
{
printk("init pin of group 3 ...\n");
break;
}
}
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, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(led_rsc->pin), PIN(led_rsc->pin));
switch(GROUP(led_rsc->pin))
{
case 0:
{
printk("set pin of group 0 ...\n");
break;
}
case 1:
{
printk("set pin of group 1 ...\n");
break;
}
case 2:
{
printk("set pin of group 2 ...\n");
break;
}
case 3:
{
printk("set pin of group 3 ...\n");
break;
}
}
return 0;
}
static struct led_operations board_demo_led_opr = {
.num =2,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *led_operations(void)
{
return &board_demo_led_opr;
}
在led_drv.c文件中如上节程序就可,我们只进行了硬件资源的优化,将硬件操作分离,在本例中,chip_demo_gpio.c中实现了一个开发板的所有引脚操作,只需在board_A_led.c文件中指定要使用的引脚就可;达到驱动和硬件的解耦,想使用不用的开发板、引脚,只需修改底层硬件代码就行,上层驱动不用修改;
2. 总线设备驱动模型
总线设备驱动模型是Linux内核中用于管理和驱动硬件设备的一种架构,它采用分层分离的方式,将设备从驱动中剥离出来,通过总线进行连接和管理。这个模型主要由总线(bus)、设备(device)和驱动(driver)三个部分组成。
平台总线模型引入platform_device/platform_driver,将“资源”与“驱动”分离开来,所以冗余代码太多,修改引脚时设备端的代码需要重新编译。更换引脚时,上图中的led_drv.c基本不用改,但是需要修改led_dev.c;
什么是platform_device/platform_driver?
是平台总线模型中是两个核心概念; platform_device
是Linux内核中一种特殊类型的设备,表示平台总线下的设备。它是对特定硬件平台相关设备的抽象表示。
每个platform_device
都包含以下关键信息:
- 设备名称(name):用于标识设备的唯一名称,通常与驱动程序中的名称相匹配。
- 设备ID(id):用于区分相同名称下的不同设备实例。
- 设备资源(resources):包括设备的物理地址、中断号等资源信息,这些信息通过
struct resource
结构体数组表示,供驱动程序访问。 - 设备特定的数据(platform_data/of_node):指向设备特定数据的指针,这些数据可能是平台数据(platform_data)或设备树节点(of_node),用于传递设备相关的额外信息给驱动程序。
在系统启动时,Linux内核会通过解析平台数据(Platform Data)来初始化platform_device
,并将其注册到内核中。这样,操作系统就能够正确识别并驱动这些设备。
platform_driver
是表示驱动程序的实体,用于与platform_device
进行匹配并控制设备。每个platform_driver
都包含以下关键信息:
- 驱动程序名称(name):用于与
platform_device
进行匹配的名称。 - 探测函数(probe):当设备与驱动程序匹配成功时调用的函数,用于初始化设备。该函数通常会执行设备资源的映射、中断的注册等任务。
- 移除函数(remove):当设备被卸载时调用的函数,用于清理设备资源,如释放映射的内存、注销中断等。
- 可选的挂起和恢复函数(suspend/resume):用于在系统进入休眠和唤醒时保存和恢复设备状态。
platform_driver
的注册过程通常涉及填写一个struct platform_driver
结构体,并通过调用platform_driver_register()
函数将其注册到内核中。内核会维护一个platform_driver
的列表,并在系统启动时或设备被热插拔时,尝试将已注册的platform_driver
与platform_device
进行匹配。如果匹配成功,将调用驱动程序的probe
函数来初始化设备。
在总线驱动模型中,我们可以把platform_device/platform_driver理解成两个结构体,分别描述硬件资源和驱动资源;通过总线进行通信,这里的“总线”并不是传统意义上的物理总线(如PCI、USB等),而是指Linux内核中用于连接设备和驱动程序的抽象层;
一个最基础的平台总线模型,主要完成两步就行;
1. 分配/设置/注册platform_device结构体
在里面定义所用资源,指定设备名字。
2. 分配/设置/注册platform_driver结构体
在其中的probe函数里,分配/设置/注册file_operations结构体,
并从platform_device 中确实所用硬件资源。
指定platform_driver 的名字。
源码文件如下:
核心源码:
在board_A_led.c文件中实现platform_device结构体,board_A_led.c作为一个可加载模块,里面也有入口函数、出口函数。在入口函 数中注册platform_device结构体,在platform_device结构体中指定使用 哪个GPIO引脚。
在看入口函数,它调用platform_device_register函数,向内核注册 board_A_led_dev结构体,在board_A_led_dev结构体中指定设备名、指定资源即要使用的引脚;
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/platform_device.h>
#include "led_resource.h"//在当前文件目录中查询该头文件
static void led_dev_release(struct device *dev)
{
}
/*结构体数组*/
static struct resource resources_led[] = {
{
.start =GROUP_PIN(5,3),
.flags = IORESOURCE_IRQ,
.name = "myled",
} ,
{
.start =GROUP_PIN(5,8),
.flags = IORESOURCE_IRQ,
.name = "myled",
},
};
static struct platform_device board_A_led_dev=
{
.name ="myled",
.num_resources = ARRAY_SIZE(resources_led),
.resource = resources_led,
.dev = {
.release = led_dev_release,
},
};
static int led_dev_init (void)
{
int ret;
ret=platform_device_register(&board_A_led_dev);
return 0;
}
static void 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_drvier结构体,即注册platform_driver结构体,它使用 Bus/Dev/Drv模型,当有匹配的platform_device时,它的probe函数就会被 调用。
在入口函数中注册一个platform_drvier结构体chip_demo_gpio_drv,然后获得引脚操作结构体指针board_demo_led_opr;
platform_drvier结构体chip_demo_gpio_drv的核心是探针函数chip_demo_gpio_probe,从匹配的platform_device中获取资源,确定GPIO引脚,把引脚记录下来,在操作硬件时使用;如果发现新的GPIO引脚就调用上层声明的函数led_class_create_device(g_ledcnt),创建设备节点;
硬件具体操作就是board_demo_led_opr结构体,组g_ledpins,里面 的值来自platform_device,在probe函数中根据platform_device的资源 确定了引脚:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/platform_device.h>
#include <asm/io.h>
#include "led_operations.h"
#include "led_drv.h"
#include "led_resource.h"
static int gled_pins [100];
static int gled_cnt=0;
static volatile unsigned int *CCM_CCGR1 ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR ;
static volatile unsigned int *GPIO5_DR ;
static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
{
printk("init gpio: group %d, pin %d\n", GROUP(gled_pins[which]), PIN(gled_pins[which]));
switch(GROUP(gled_pins[which]))
{
case 0:
{
printk("init pin of group 0 ...\n");
break;
}
case 1:
{
printk("init pin of group 1 ...\n");
break;
}
case 2:
{
printk("init pin of group 2 ...\n");
break;
}
case 3:
{
printk("init pin of group 3 ...\n");
break;
}
case 4:
{
printk("init pin of group 2 ...\n");
break;
}
case 5:
{
printk("init pin of group 5 pin 3 ...\n");
if(!CCM_CCGR1)
{
CCM_CCGR1=ioremap(0x020C4000 + 0x6C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3=ioremap(0x02290000 + 0x14, 4);
GPIO5_GDIR=ioremap(0x020AC004, 4);
GPIO5_DR=ioremap(0x020AC000, 4);
}
*CCM_CCGR1|=0xc000000;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &=0xf0;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |=0x05;
*GPIO5_GDIR |=(1<<3);
break;
}
}
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, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(gled_pins[which]), PIN(gled_pins[which]));
switch(GROUP(gled_pins[which]))
{
case 0:
{
printk("set pin of group 0 ...\n");
break;
}
case 1:
{
printk("set pin of group 1 ...\n");
break;
}
case 2:
{
printk("set pin of group 2 ...\n");
break;
}
case 3:
{
printk("set pin of group 3 ...\n");
break;
}
case 4:
{
printk("init pin of group 2 ...\n");
break;
}
case 5:
{
printk("init pin of group 5 pin 3 ...\n");
if (status) /* on: output 0*/
{
/* d. 设置GPIO5_DR输出低电平
* set GPIO5_DR to configure GPIO5_IO03 output 0
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b0
*/
*GPIO5_DR &= ~(1<<3);
}
else /* off: output 1*/
{
/* e. 设置GPIO5_IO3输出高电平
* set GPIO5_DR to configure GPIO5_IO03 output 1
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b1
*/
*GPIO5_DR |= (1<<3);
}
break;
}
}
return 0;
}
static struct led_operations board_demo_led_opr = {
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *led_operations(void)
{
return &board_demo_led_opr;
}
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
int i=0;
struct resource *res;
/*记录引脚*/
while(1)
{
res=platform_get_resource(pdev, IORESOURCE_IRQ, i++) ;//获得资源
if (!res)
{
break;
}
gled_pins[gled_cnt]=res->start;//记录资源
/*device_create*/
led_device_create(gled_cnt);
gled_cnt++;
}
return 0;
}
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
int i=0;
struct resource *res;
/*device_destroy*/
while(1)
{
res=platform_get_resource(pdev, IORESOURCE_IRQ, i++) ;//获得资源
if (!res)
break;
led_device_destroy(i);
i++;
gled_cnt--;
}
return 0;
}
struct platform_driver chip_demo_gpio_drv={
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "myled",
},
};
static int chip_demo_gpio_drv_init (void)
{
int ret;
ret=platform_driver_register(&chip_demo_gpio_drv);
register_led_operations(&board_demo_led_opr);
return 0;
}
static void chip_demo_gpio_drv_exit (void)
{
platform_driver_unregister(&chip_demo_gpio_drv);
}
module_init(chip_demo_gpio_drv_init);
module_exit(chip_demo_gpio_drv_exit);
MODULE_LICENSE("GPL");
总线驱动模型的核心就是实现 platform_device/platform_driver两个结构体,通过这两个结构体将硬件资源和驱动分离开;
总线设备模型和分层分离驱动模型有什么关系?
- 总线设备模型和分层分离驱动模型在Linux内核中并不是相互排斥的,而是可以相互结合使用的。例如,在总线设备模型中,驱动程序可能也采用分层分离的设计思想来组织代码。
- 分层分离驱动模型可以看作是对总线设备模型的一种补充和优化,它提供了更细粒度的代码组织和复用方式。
这里有一个问题就是我们通过两个结构体分别实现硬件和驱动,platform_driver匹配到platform_device时才会调用probe函数,那么platform_device/platform_driver之间怎么匹配呢?
匹配规则:
1. 最先比较
platform_device.driver_override 和 platform_driver.driver.name,可以设置platform_device 的 driver_override,强制选择某个 platform_driver。
2. 然后比较
platform_device. name和platform_driver.id_table[i].name;platform_driver.id_table 是“platform_device_id”指针,表示该 drv 支持若干 个device,它里面列出了各个device的{.name, .driver_data},其中的“name”表示该 drv 支持的设备的名字,driver_data是些提供给该device的私有数据。
3. 最后比较
platform_device.name 和platform_driver.driver.name;platform_driver.id_table 可能为空, 这时可以根据platform_driver.driver.name来寻找同名的platform_device。
3 . 设备树
以LED驱动为例,在上述中实现的驱动框架,不管哪一种方法都需要大量的代码来实现硬件的操作,在上述中我们实现了驱动、硬件分开,但是局限性太大;如果你要更换LED所用的GPIO引脚,你依然需要修改驱动程序源码、重新编译驱动、重新加载驱动。 在内核中,使用同一个芯片的板子,它们所用的外设资源不一样,比如A板 用GPIO A,B板用GPIO B。而 GPIO的驱动程序既支持GPIO A也支持GPIO B, 你需要指定使用哪一个引脚,怎么指定?在c代码中指定。 随着ARM芯片的流行,内核中针对这些ARM板保存有大量的、没有技术含量的文件。所以引入设备树代替内核中这些大量没有技术的文件;
设备树(Device Tree)是一种用于描述硬件设备信息的数据结构,它采用树状结构来表示系统中的设备及其之间的连接关系。设备树的使用极大地简化了硬件设备的配置和管理,特别是在嵌入式系统和复杂的计算机系统中。
1. 设备数的组成和文件类型
DTS文件是设备树的源码文件,采用ASCII文本格式编写,文件扩展名为.dts。DTS文件通过树形结构描述板级设备信息,如CPU数量、内存基地址、外设接口(如IIC、SPI)上连接的设备等。每个设备在DTS文件中表示为一个节点,节点通过属性来描述设备信息,属性是键值对的形式。DTS文件可以引用DTSI文件(类似于C语言中的头文件),以减少代码冗余,便于维护和共享。
DTB文件是DTS文件编译后的二进制文件,由Linux内核解析。DTB文件包含了设备树的完整信息,用于在系统启动阶段配置和初始化硬件设备。DTS文件通过设备树编译器(DTC)编译成DTB文件,DTC工具的源码位于Linux内核的scripts/dtc目录下。
DTC是设备树编译器,用于将DTS文件编译成DTB文件。DTC工具依赖于多个源文件(如dtc.c、flattree.c、fstree.c等),最终编译并链接出DTC可执行文件。在Linux内核源码的scripts/dtc目录下可以找到DTC工具的源码。
设备树的使用流程:
- 编写DTS文件:根据硬件平台的实际情况,编写描述板级设备信息的DTS文件。
- 编译DTS文件:使用DTC工具将DTS文件编译成DTB文件。
- 系统启动:在系统启动阶段,bootloader加载启动镜像并提取DTB文件,然后将其传递给内核。
- 内核解析DTB文件:内核根据DTB文件中的设备树信息来配置和初始化硬件设备。
设备树的优势
- 分离硬件描述与内核代码:避免了硬编码方式带来的问题,如内核臃肿、修改硬件信息需重新编译内核等。
- 简化硬件适配:对于同一SOC的不同主板,只需更换设备树文件即可实现无差异支持,无需更换内核文件。
- 提高系统可移植性:使得Linux系统更容易移植到不同的硬件平台上。
设备树只是用来给内核里的驱动程序,指定硬件的信息。比如LED驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
简单说就是把前面我们实现的与硬件有关的操作全部使用设备树代替;
设备树抽象如下:
我们要做的就是编写设备树文件(dts: device tree source),它需要编译为 dtb(device tree blob)文件,内核使用的是dtb文件。
示例设备树dts文件:
1. 根节点(Root Node)
- 表示:由“/”表示,是Device Tree的入口点(Entry Point)。
- 属性:根节点通常包含一些必要的属性,如
#address-cells
、#size-cells
、model
和compatible
等。其中,#address-cells
和#size-cells
定义了子节点reg
属性中地址和长度所占用的32位字数量;model
和compatible
则用于描述设备的型号和兼容性信息。
2. 子节点(Child Nodes)
- 结构:每个子节点都以“node-name@unit-address { ... }”的形式表示,其中
node-name
是设备名,unit-address
是设备的地址(可选),{...}
内是该节点的属性。 - 属性:子节点可以包含多个属性,这些属性以“key = value”的形式表示,用于描述设备的具体配置信息。属性值的类型可以是字符串、整数、布尔值、二进制数据等。
3. 属性(Properties)
- 类型:属性是dts文件中描述设备的核心部分,它们可以是预定义的(如
compatible
、reg
等),也可以是厂商自定义的。属性值可以是简单的字符串、整数列表(cells)、二进制数据等。 - 用途:属性用于描述设备的各种配置信息,如设备类型、地址空间、中断号、引脚配置等。这些信息对于内核识别和配置硬件设备至关重要。
4. 包含文件(Include Files)
- 作用:dts文件可以通过
#include
指令包含其他dts或dtsi文件(Device Tree Source Include file)。dtsi文件通常包含多个机器公用的设备描述信息,而dts文件则专注于描述某个具体产品的特性。 - 示例:
#include "skeleton.dtsi"
,这样可以将skeleton.dtsi文件中的内容包含到当前dts文件中。
5. 特殊节点
- aliases:用于定义节点的别名,方便在其他地方引用。
- chosen:不是一个真实的设备节点,而是用于uboot向Linux内核传递启动参数(如bootargs)的特殊节点。
示例结构
/{ // Device Tree的根节点
#address-cells = <1>; // 指定子节点中reg属性地址字段占用的32位字数量
#size-cells = <1>; // 指定子节点中reg属性大小字段占用的32位字数量
model = "My Board"; // 设备的型号描述
compatible = "vendor,my-board"; // 设备的兼容性字符串,用于匹配设备驱动
serial@101f0000 { // 定义一个名为serial的设备节点,其单元地址为0x101f0000
compatible = "arm,pl011"; // 表明该设备兼容的驱动是ARM的PL011 UART驱动
reg = <0x101f0000 0x1000>; // 设备寄存器的物理地址和长度,这里地址是0x101f0000,长度是0x1000
interrupts = <10>; // 设备使用的中断号,这里中断号为10
};
// 其他子节点... // 这里可以添加更多的设备节点描述
};
#address-cells
和#size-cells
属性定义了在reg
属性中,地址和大小信息分别由多少个32位单元(cells)组成。这些属性是在Device Tree的上下文中“继承”的,但实际上是在每个节点解析其reg
属性时独立应用的。
2. 设备树的语法
DTS文件布局(layout):
/dts-v1/; // 表示版本
[memory reservations] // 格式为: /memreserve/ <address> <length>;
/ {
[property definitions]
[child nodes]
};
node的格式:设备树中的基本单元,被称为“node”,其格式为:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
label是标号,可以省略。label的作用是为了方便地引用node,比如:
/dts-v1/;
/ {
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};
可以使用下面2种方法来修改uart@fe001000这个node:
// 在根节点之外使用label引用node:
&uart0 {
status = “disabled”;
};
或在根节点之外使用全路径:
&{/uart@fe001000} {
status = “disabled”;
};
properties的格式
简单地说,properties就是“name=value”,value有多种取值方式。
Property格式1:
[label:] property-name = value;
Property格式2(没有值):
[label:] property-name;
Property取值只有3种:
arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示),
string(字符串),
bytestring(1个或多个字节)
示例:
a) Arrays of cells : cell就是一个32位的数据,用尖括号包围起来;
interrupts = <17 0xc>;
数据使用2个cell来表示,用尖括号包围起来:
clock-frequency = <0x00000001 0x00000000>;
b) A null-terminated string (有结束符的字符串),用双引号包围起来:
compatible = "simple-bus";
c) A bytestring(字节序列) ,用中括号包围起来:
local-mac-address = [00 00 12 34 56 78]; // 每个byte使用2个16进制数来表示
local-mac-address = [000012345678]; // 每个byte使用2个16进制数来表示
可以是各种值的组合, 用逗号隔开:
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";
设备树文件不需要我们从零写出来,内核支持了某款芯片比如imx6ull,在 内核的 arch/arm/boot/dts 目录下就有了能用的设备树模板,一般命名为 xxxx.dtsi。“ i”表示“include”,被别的文件引用的。 我们使用某款芯片制作出了自己的单板,所用资源跟 xxxx.dtsi 是大部分 相同,小部分不同,所以需要引脚xxxx.dtsi并修改。 dtsi 文件跟dts文件的语法是完全一样的。 dts 中可以包含.h头文件,也可以包含dtsi文件,在.h头文件中可以定义 一些宏。
示例:
/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
/ {
……
};
3. 内核对设备数的处理
从源代码文件dts文件开始,设备树的处理过程为:
- dts在PC机上被编译为dtb文件;
- u-boot把dtb文件传给内核;
- 内核解析dtb文件,把每一个节点都转换为device_node结构体;
- 对于某些device_node结构体,会被转换为platform_device结构体。
4. 使用设备树实现LED驱动程序
无论是使用设备树、总线还是传统写法,他们的核心要素都是一样的,只是指定“硬件资源”的方式不一眼,platform_device/platform_driver只是编程的技 巧,不涉及驱动的核心;
使用设备树实现驱动程序中的要点就是设备树节点要与platform_driver匹配;
在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写 设备树。但是,匹配过程所要求的东西是固定的:
- 设备树要有compatible属性,它的值是一个字符串
- platform_driver 中要有of_match_table,其中一项的.compatible 成员设置 为一个字符串
- 上述2个字符串要一致。
示例如下:
如果在设备树节点里使用reg属性,那么内核生成对应的platform_device 时会用reg属性来设置IORESOURCE_MEM类型的资源。 如果在设备树节点里使用 interrupts 属性,那么内核生成对应的 platform_device 时会用reg属性来设置IORESOURCE_IRQ类型的资源。对于 interrupts 属性,内核会检查它的有效性,所以不建议在设备树里使用该属性 来表示其他资源。 在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写 设备树。驱动程序中根据pin属性来确定引脚,那么我们就在设备树节点中添加 pin 属性。 设备树节点中:
#define GROUP_PIN(g,p) ((g<<16) | (p))
100ask_led0 {
compatible = “100ask,led”;
pin = <GROUP_PIN(5, 3)>;
};
驱动程序中,可以从 platform_device 中得到 device_node,再用of_property_read_u32得到属性的值:
struct device_node* np = pdev->dev. of_node;
int led_pin;
int err = of_property_read_u32(np, “pin”, &led_pin);
在本文实例中需要添加的设备节点代码是一样的,你需要找到你的单板所用 的设备树文件,在它的根节点下添加如下内容:
#define GROUP_PIN(g,p) ((g<<16) | (p))
100ask_led@0 {
compatible = "100ask,leddrv";
pin = <GROUP_PIN(3, 1)>;
};
100ask_led@1 {
compatible = "100as,leddrv";
pin = <GROUP_PIN(5, 8)>;
};
完成了设备树的修改接下来就是,修改我们的platform_driver源码;关键代码在chip_demo_gpio.c,主要看里面的platform_driver,代码如下:
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
int err=0;
int led_pin;
struct device_node *np;
/*记录引脚*/
np =pdev->dev.of_node;//判断这个platfrom_driver支持的platfrom_device是否来自设备树
if(!np)
return -1;
err= of_property_read_u32(np,"pin", &led_pin);//从np中读取pin属性保存在len_pin变量中
gled_pins[gled_cnt]=led_pin;//记录资源
/*device_create*/
led_device_create(gled_cnt);
gled_cnt++;
return 0;
}
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
int i=0;
int err=0;
int led_pin;
struct device_node *np;
np =pdev->dev.of_node;//判断这个platfrom_driver支持的platfrom_device是否来自设备树
if(!np)
return -1;
err= of_property_read_u32(np,"pin", &led_pin);//从np中读取pin属性保存在len_pin变量中
/*device_destroy*/
for(i=0;i<gled_cnt;i++)
{
if(gled_pins[i]==led_pin)
{
led_device_destroy(i);
gled_pins[i] = -1;
break;
}
}
for(i=0;i<gled_cnt;i++)
{
if(gled_pins[i]!=-1)
break;
}
if(i==gled_cnt)//这个i的值取决于for(i=0;i<gled_cnt;i++)中i的值
gled_cnt=0;//全局变量gled_cnt 值清零
return 0;
}
static const struct of_device_id my_led[] = {
{ .compatible = "dcq,myled" },
{ },
};
struct platform_driver chip_demo_gpio_drv={
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "myled",
.of_match_table = my_led,
},
};
static int chip_demo_gpio_drv_init (void)
{
int ret;
ret=platform_driver_register(&chip_demo_gpio_drv);
register_led_operations(&board_demo_led_opr);
return 0;
}
static void chip_demo_gpio_drv_exit (void)
{
platform_driver_unregister(&chip_demo_gpio_drv);
}
module_init(chip_demo_gpio_drv_init);
module_exit(chip_demo_gpio_drv_exit);
MODULE_LICENSE("GPL");
指定.of_match_table,它用来给设备数节点匹配,如果设备树节点中有compatile属性,并且值等于指定的“dcq,myled”就是导致probe函数被调用;
static const struct of_device_id my_led[] = {
{ .compatible = "dcq,myled" },
{ },
};
struct platform_driver chip_demo_gpio_drv={
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "myled",
.of_match_table = my_led,
},
};
上机实验:
- 使用新的设备树dtb文件启动单板,查看/sys/firmware/devicetree/base下 有无节点
- 查看/sys/devices/platform目录下有无对应的platform_device
- 加载驱动:
[root@100ask:~]# insmod leddrv.ko
[root@100ask:~]# insmod chip_demo_gpio.ko
测试驱动:
[root@100ask:~]# ./ledtest /dev/100ask_led0 on
[root@100ask:~]# ./ledtest /dev/100ask_led0 off
4. 总结
本文主要介绍了字符设备驱动程序的常规实现方法,无论是哪一种实现方法,他们只是实现的框架不一样,核心要是都是相同。核心在于file_operations结构体的实现,驱动的注册;
传统写法:最简单的字符设备驱动程序,直接将驱动和硬件操作杂糅在一起,程序整体较差;
可以通过分层/分离思想实现驱动程序,将驱动和硬件资源分层,将硬件资源分离,这样实现代码量大,但是易于扩展,移植;
总线模型:引入platform_device和platform_driver,实现驱动和资源分开;代码量大;
设备树:通过配置文件指定硬件资源,避免了内核中臃肿的驱动代码;