关键词:rk3399 嵌入式驱动 Linux platform
前言
前面的嵌入式Linux驱动都是描述从特定的SOC与特定设备之间的直接两两通信。而Linux不是为单一某一SOC结构而设计的操作系统,它可以运行在X86、ARM等多种架构多种SOC平台上,如果驱动程序按照SOC与设备之间直接通信的方式实现的话,其效果如下所示:
从Linux源码的角度来看,源码会变得极其臃肿并且充斥的大量的重复代码,比如A SOC平台和C SOC平台都需要各自与一个B设备通信,即A SOC连接了一个B设备,C SOC也连接了一个B设备,Linux系统实现直接两两通信,Linux源码中就会出现重复的对B设备的驱动代码
举个例子,多个人到超市去采购商品,每个人采购多个品类的商品,每个品类的商品都有一个售货员,如果每个购买者按照自己的采购清单直接跟售货员沟通,那么就会出现每个购买者跟多个售货员沟通,每个售货员跟多个购买者沟通的现象,场面就比较混乱,对应到Linux系统源码而言就显得特别无序,售货员多次往返取货会浪费时间,对应到Linux系统源码而言就是代码的重复
为了避免上述提到的问题,提高Linux设备驱动的跨平台能力,Linux系统采取了驱动分离与分层的策略,如下为Linxu驱动分离后的驱动框架示意:
其中:
主机驱动:linux系统内核能够利用SOC的接口驱动,例如,Linux系统内核对RK3399的I2C接口的驱动,就是实现让RK3399的引脚按照I2C功能工作
设备驱动:连接到SOC接口的设备的驱动,例如,RK3399的I2C接口上连接了一个MPU6050,设备驱动就是MPU6050自己的驱动
统一接口的作用如同在上面的例中,在超市门口建立一个采购平台,购买者将清单给平台,平台给售货员发送提货单,这样超市就看起来井井有条,某个售货员也可以一次将多个购买者的同类商品取出,不必往返多次,避免重复
这就是Linxu系统中的总线(Bus)、驱动(driver)和设备(device)模型。如下:
总线负责传递设备信息等。当向系统注册一个驱动时,总线会在设备中查找,如果有匹配的设备,便将两者联系起来;同样,向系统注册一个设备的时候,总线会在驱动中查找,如果有匹配的驱动,便将两者联系起来
以上是为什么采用驱动分离的策略
platform
Linux驱动分离使用了总线(Bus)、驱动(driver)和设备(device)模型,对于SOC的某些外设,是没有总线一说的,因此Linux提出了platform这种虚拟总线,相应的就有驱动platform_driver和设备platform_device
platform总线
Linux系统内使用bus_type结构体表示总线,其定义在Linxu源代码的/include/linux/device.h
中,platform总线是bus_type的一个实例,定义在Linux源代码的/drivers/base/platform.c
中,如下
struct bus_tpye platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
其中 platform_match
就是匹配函数,总线就是使用匹配函数类根据注册的设备来查找驱动的,或根据注册的驱动来查找设备,paltform_match()函数定义在文件/drivers/base/platform.c
中,如下:
static int platform_match(struct device *dev,
struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/*When driver_override is set,only bind to the matching driver*/
if (pdev -> driver_override)
return !strcmp(pdev -> driver_override,drv -> name);
/* Attemp an OF style match first */
if(of_driver_match_device(dev,drv))
return 1;
/*Then try ACPI style match */
if(acpi_driver_match_device(dev,drv))
return 1;
/* Then try to match against the id table */
if(pdrv -> id_table)
return platform_match_id(pdrv -> id_table,pdev) != NULL;
/* fall-back to driver name match */
return(strcmp(pdev -> name,drv -> name) == 0);
}
可以看出,函数内提供了4中匹配方法:
- OF函数匹配,即设备树匹配方式,函数of_deiver_match_device()定义在Linux源代码文件
/include/linux/of_device.h
中,device_driver结构体中有个名为of_match_table
的成员变量,此成员变量保存着驱动的compatible匹配表,设备树中的每个设备节点的compatibal属性会和of_match_table表中的所有成员比较,如果有则表示设备会和此驱动匹配 - ACPI匹配
- id_table匹配,每个platform_driver结构体有一个id_table成员变量,其中保存了很多id信息,这些id信息包含paltform_driver所支持的驱动类型
- name字段匹配
对于支持设备树的Linux版本,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式,即OF函数匹配都会存在,id_table匹配和name字段匹配只存在一种就可以了,比较nama字段的匹配比较简单,用的多
paltform总线,即platform_bus_type的主要作用就是匹配驱动和设备
platform驱动platform_driver
platform驱动,由platform_driver结构体表示,此结构体定义在Linux源代码的/include/linux/platform_device.h
中,内容如下:
struct platform_driver {
int (*probe)(struct platform_device *);
/*
* Traditionally the remove callback returned an int which however is
* ignored by the driver core. This led to wrong expectations by driver
* authors who thought returning an error code was a valid error
* handling strategy. To convert to a callback returning void, new
* drivers should implement .remove_new() until the conversion it done
* that eventually makes .remove() return void.
*/
int (*remove)(struct platform_device *);
void (*remove_new)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
/*
* For most device drivers, no need to care about this flag as long as
* all DMAs are handled through the kernel DMA API. For some special
* ones, for example VFIO drivers, they know how to manage the DMA
* themselves and set this flag so that the IOMMU layer will allow them
* to setup and manage their own I/O address space.
*/
bool driver_managed_dma;
};
其中
- probe函数当设备与驱动匹配成功后会执行
- id_table就是platform总线中匹配驱动和设备的一个匹配方式,id_table是一个数组,每个元素的类型为platform_device_id,其结构体如下
struct platform_device_id{
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
- driver 是device_driver结构体变量,该结构体定义在Linux源代码的
/include/linux/device.h
中,如下:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
其中of_match_table是采用设备树式paltform驱动使用的匹配表,是of_device_id类型,of_device_id类型结构体数据定义在Linux源代码的/include/linux/mod_devicetable.h
中,如下:
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
其中compatible就是设备树中节点的compatible属性,of_match_table中的compatible属性和设备树节点的compatible相同则代表驱动和设备匹配成功
在编写platform驱动的时候,首先定义一个paltform_device结构体变量,然后实现其成员变量,重点是实现匹配方法及probe()函数,具体的驱动程序在probe()函数中编写
定义并初始化好platform_driver后,需要在驱动入口函数(参考[[01嵌入式Linux驱动开发之点亮LED(非设备树)]]的内核模块部分)中调用platform_driver_register()函数,向Linux内核注册一个platform驱动,相应的,注销platform驱动可以用platform_driver_unregister()
见下面测试代码查看platform_driver的应用
platform设备platform_device
准备好platform驱动后,还需要准备pltform设备,由platform_device结构体表示platform设备,结构体定义在Linux源码的/include/linux/platform_device.h
中,如下
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u64 platform_dma_mask;
struct device_dma_parameters dma_parms;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
/*
* Driver name to force a match. Do not set directly, because core
* frees it. Use driver_set_override() to set or clear it.
*/
const char *driver_override;
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
如果Linux内核版本支持设备树,就不需要再用platform_device来描述设备了,改用设备树去描述本文这里不再梳理platform_device,见下面测试代码了解设备树描述platform设备
设备树下platform驱动实验
platform驱动分为总线、驱动和设备,总线是由linux内核提供的,在支持设备树的Linux内核版本下,可以在设备树中描述设备,因此驱动的实现,除在设备树中添加设备外,只需要实现platform_driver即可
设备树描述设备
与前面相同,控制Firefly-RK3399开发板上电LED,引脚GPIO0B_5输出高电平点亮LED,低电平熄灭LED。在设备树中添加设备节点的操作与嵌入式Linux驱动开发之从设备树到点亮LED中相同,这里不再重复。
platform_driver实现
新建.c文件,输入以下内容(本文这里命名为platform_leddriver.c):
#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 <linux/platform_device.h>
/*************************************
*FileName:platform_leddriver.c
*Fuc: RK3399,GPIO0B_5 driver
*
*Author: PineLiu
************************************/
#define CHAR_CNT 1 //设备数量
#define LED_NAME "gumpplatled" //设备名称
#define LEDON 1 //点亮
#define LEDOFF 0 //熄灭
//重映射后的寄存器虚拟地址
static void __iomem *GPIO0_DR; //数据寄存器
static void __iomem *GPIO0_DDR; //方向寄存器
static void __iomem *GPIO0_IOMUX; //复用寄存器
static void __iomem *GPIO0_CRU; //时钟寄存器
//声明设备结构体(该设备包含的属性)
struct platform_leddev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //设备节点类 主动创建设备节点文件用
struct device *device; //设备
int major; //主设备号
int minor; //次设备号
struct device_node *nd; //设备节点
};
//声明一个外部设备,本文这里是字符设备
struct platform_leddev platformled;
//LED打开/关闭
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON){
val = readl(GPIO0_DR);
val |= (1<<13);
writel(val,GPIO0_DR);
}else if(sta == LEDOFF){
val = readl(GPIO0_DR);
val &= ~(1<<13);
writel(val, GPIO0_DR);
}
}
//===========================================================================
//以下实现设备的具体操作函数:open函数、read函数、write函数和release函数
//===========================================================================
//打开设备
static int led_open(struct inode *inode, struct file *filp)
{
filp -> private_data = &platformled; //设置私有数据
return 0;
}
//从设备读取数据
static ssize_t led_read(struct file *filp,char __user *buf,
size_t cnt,loff_t *offt)
{
return 0;
}
//向设备写数据
//filp:设备文件,表示打开的文件描述
//buf :保存着要向设备写入的数据
//cnt :要写入的数据长度
//offt:相对于文件首地址的偏移
//
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);
}else if(ledstat == LEDOFF){
led_switch(LEDOFF);
}
return 0;
}
//关闭,释放设备
//filp: 要关闭的设备文件描述
//
static int led_release(struct inode *inode,struct file *filp)
{
return 0;
}
//==========================================================================
//以下实现的设备具体函数与内核的对应函数的映射
//==========================================================================
//映射设备操作函数
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
//==========================================================================
//内核模块相关
//==========================================================================
//probe函数
static int led_probe(struct platform_device *dev)
{
u32 val = 0;
int ret;
u32 regdata[14];
const char *str;
struct property *proper;
printk("led driver and device was matched!\r\n");
//==============获取节点属性
//获取节点
platformled.nd = of_find_node_by_path("/gumpleddts");
if(platformled.nd == NULL){
printk("gumpleddts node can not found!\r\n");
return -EINVAL;
}else{
printk("gumpleddts node has been found!\r\n");
}
//获取compatible属性
proper = of_find_property(platformled.nd,"compatible",NULL);
if(proper == NULL){
printk("compatible property can not found!\r\n");
}else{
printk("compatibel = %s\r\n",(char *)proper -> value);
}
//获取status属性
ret = of_property_read_string(platformled.nd,"status",&str);
if(ret < 0){
printk("status read failed!\r\n");
}else{
printk("status = %s\r\n",str);
}
//获取reg属性
ret = of_property_read_u32_array(platformled.nd,"reg",regdata,8);
if(ret < 0){
printk("reg property read failed!\r\n");
}else{
u8 i = 0;
printk("reg data:\r\n");
for(i = 0;i<8;i++)
printk("%#X",regdata[i]);
printk("\r\n");
}
//===============初始化连接LED的引脚
//寄存器地址映射
#if 1
GPIO0_CRU = ioremap(regdata[0],regdata[1]);
GPIO0_IOMUX = ioremap(regdata[2],regdata[3]);
GPIO0_DDR = ioremap(regdata[4],regdata[5]);
GPIO0_DR = ioremap(regdata[6],regdata[7]);
#else
GPIO0_CRU = of_iomap(platformled.nd,0);
GPIO0_IOMUX = of_iomap(platformled.nd,1);
GPIO0_DDR = of_iomap(platformled.nd,2);
GPIO0_DR = of_iomap(platformled.nd,3);
#endif
//使能时钟
// GPIO0_CRU |= (1 << (3 + 16));
// GPIO0_CRU &= ~(1 << 3);
val = readl(GPIO0_CRU);
val |= (1 << (3+16));
val &= ~(1 << 3);
writel(val,GPIO0_CRU);
//设置GPIO0B_5引脚复用功能为GPIO
// GPIO0_IOMUX |= (3 << (10+16));
// GPIO0_IOMUX &= ~(3 << 10);
val = readl(GPIO0_IOMUX);
val |= (3 << (10+16));
val &= ~(3 << 10);
writel(val,GPIO0_IOMUX);
//配置GPIO0B_5引脚为输出模式
// GPIO0_DDR |= (1 << 13);
val = readl(GPIO0_DDR);
val |= (1 << 13);
writel(val,GPIO0_DDR);
//配置GPIO0B_5引脚默认关闭LED
// GPIO0_DR &= ~(1 << 13);
val = readl(GPIO0_DR);
val &= ~(1 << 13);
writel(val,GPIO0_DR);
//===========注册设备
//创建设备号
if(platformled.major){ //定义了设备号
platformled.devid = MKDEV(platformled.major,0);
register_chrdev_region(platformled.devid,CHAR_CNT,LED_NAME);
}else{ //没有定义设备号,要向系统申请设备号
alloc_chrdev_region(&(platformled.devid),0,CHAR_CNT,LED_NAME);
platformled.major = MAJOR(platformled.devid);
platformled.minor = MINOR(platformled.devid);
}
printk("platformled major = %d,minor = %d\r\n",platformled.major,platformled.minor);
//初始化cdev
platformled.cdev.owner = THIS_MODULE;
cdev_init(&platformled.cdev,&led_fops);
//向系统注册设备
cdev_add(&platformled.cdev,platformled.devid,CHAR_CNT);
//创建类
platformled.class = class_create(THIS_MODULE,LED_NAME);
if(IS_ERR(platformled.class)){
return PTR_ERR(platformled.class);
}
//创建设备节点
platformled.device = device_create(platformled.class,NULL,platformled.devid,NULL,LED_NAME);
if(IS_ERR(platformled.device)){
return PTR_ERR(platformled.device);
}
return 0;
}
//remove函数
static int led_remove(struct platform_device *dev)
{
//取消映射
iounmap(GPIO0_CRU);
iounmap(GPIO0_IOMUX);
iounmap(GPIO0_DDR);
iounmap(GPIO0_DR);
//注销设备
cdev_del(&platformled.cdev);
unregister_chrdev_region(platformled.devid,CHAR_CNT);
device_destroy(platformled.class,platformled.devid);
class_destroy(platformled.class);
return 0;
}
//匹配表
static const struct of_device_id led_of_match[] = {
{.compatible = "gump-led"}, //与设备树内该设备节点的compatibel相同
{/*Sentinel*/}
};
//platform_driver
static struct platform_driver led_driver = {
.driver = {
.name = "gumpleddts", //设备树中节点名称
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
//驱动模块加载函数
static int __init led_init(void)
{
return platform_driver_register(&led_driver);
}
//驱动模块卸载函数
static void __exit led_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gump");
代码内实现了probe函数,在probe函数内实现了设备号分配,寄存器配置等,使用platform_driver_register
实现驱动的注册,platform_driver_unregister
实现驱动的注销
注:代码内所说的设备,是SOC内相对于cpu而言的片上外设,并不是platform_device,platform_device是指连接到这个片上外设的设备
将上面代码放到ubuntu18.04内编译为ko文件,Makefile文件内容为:
KERNELDIR:=/home/gump/rk3399/kernel-develop-4.4
CURRENT_PATH:=$(shell pwd)
obj-m:=platform_leddriver.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译命令:(可根据自己的交叉编译工具更改)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-`
得到ko文件放到嵌入式Linux系统内,并安装内核模块:
sudo insmod -f platform_leddriver.ko
安装后在/dev
目录下会出现该设备文件夹,如图
测试
测试程序platformTest.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/*************************************************
*FileName:platformTest.c
*Func: GPIO driver module test
*
*Author: pineliu
* **********************************************/
#define LEDOFF 0
#define LEDON 1
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];
fd = open(filename,O_RDWR);
if (fd < 0){
printf("file %s open failed!\r\n",argv[1]);
return -1;
}
databuf[0] = atol(argv[2]);
retvalue = write(fd,databuf,sizeof(databuf));
if(retvalue < 0){
printf("led Control Error!\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;
}
将测试程序进行编译:
aarch64-linux-gnu-gcc platformTest.c -o platformTest
得到可执行文件platformTest
放到嵌入式Linux系统内,并赋予可执行权限
chmod +x platformTest
测试,点亮led:
sudo ./platformTest /dev/gumpplatled 1
实验证明可行
总结
为了提高Linux系统跨平台能力的同时,简化源代码,实现驱动代码复用,因此采用了驱动分离的策略,使用驱动、总线、设备模型。
platform驱动分为总线、驱动和设备,总线是由linux内核提供的,在支持设备树的Linux内核版本下,可以在设备树中描述设备,因此驱动的实现,除在设备树中添加设备外,只需要实现platform_driver即可
相比于基础的设备树驱动,在驱动代码方面,platform设备驱动代码更加的结构化