Linux嵌入式驱动开发——platform机制的使用(led驱动示例实现)


linux内核分离思想,platform机制

分析:

  • 一个完整的硬件设备驱动必然包含两个部分:

  • 纯硬件相关内容:寄存器的起始地址,GPIO编号等;

  • 纯软件相关内容:if…else,混杂设备,接口等;

  • 纯软件用来操作纯硬件 。

  • 如果仅仅是硬件GPIO编号进行了改动,驱动的改动仅仅也只是修改硬件相关(寄存器的起始地址和GPIO编号)的修改,软件无需改动。

  • 但是实际修改过程中发现代码的改动量相当之大,如果让驱动代码的改动量小,移植性好,首先想到利用宏替换驱动代码中的硬件相关的内容

  • 但是寄存器的起始地址,GPIO编号他们同样是“事物”同样具有额外的其他属性,利用宏不能完整描述特性,势必要考虑到结构体进行描述

结论:

  • linux内核如何做到既可以让驱动的可移植性变得好并且对硬件信息的描述更加的丰富呢?

  • 利用linux内核的分离思想,platform机制分离思想:就是将驱动的纯硬件信息和纯软件信息进行彻底分开,一旦将来软件写好,就无需改动,将来驱动开发者的重心放在硬件信息即可,本质目的就是让驱动代码的可移植性变得非常好

  • 问:如何实现内核的分离思想呢?

  • 答:利用platform机制

  • 问:内核的platform机制如何实现的呢?

  • 答:ftp://DRV/ESD_DRV_09.ppt

  • 总结:驱动如果采用platform机制实现,只需关注以下两个数据结构:

struct platform_device
struct platform_driver

使用platform的方式方法

  • platform_device装载硬件信息的方法有三种:
  • 1.单独使用struct resource进行描述
  • 2.单独使用自定义的描述硬件信息的数据结构来描述
  • 3.可以同时使用struct resource和自定义的描述硬件信息的数据结构来描述硬件信息
  • 两个配套函数:
  • platform_device_register(&硬件节点对象)向内核dev链表添加注册硬件节点对象, 一旦添加完毕,什么遍历drv链表,什么匹配,什么调用probe函数,什么给probe函数传递硬件节点的首地址这些都是由内核完成!如果一旦匹配成功,硬件节点对象的首地址会传递给匹配成功的软件节点的probe函数

  • platform_device_unregister(&硬件节点对象) , 从内核的dev链表删除硬件节点对象,内核会调用软件节点的remove函数


struct platform_device如何使用

  struct platform_device {
   	const char	* name;
			int		id;
			struct resource	* resource;
			u32		num_resources;
			struct device	dev;
					.platform_data
			...
   };  
  • 功能:用于描述一个驱动中纯硬件信息
  • 成员说明:
  • name:硬件节点的名称,将来用于匹配,必须初始化
  • id:硬件节点的编号,如果dev链表上,仅有一个名称为name的硬件节点,id=-1即可,如果dev链表上,有多个名称为name(同名),通过id进行编号(0,1,2,…)
  • resource:装载纯硬件信息相关内容

struct resource 结构体描述

struct resource {
	unsigned long start;
	unsigned long end;
	unsigned long flags;
	...
};   
  • 功能:用于描述硬件信息
  • 成员:
  • start:起始硬件信息,例如:寄存器的起始地址

  • end:结束硬件信息,例如:寄存器的结束地址

  • flags:硬件信息的标志

    • IORESOURCE_MEM:用于指示此硬件信息为地址信息 0xC001C000这个物理地址可以用此标志修饰
    • IORESOURCE_IRQ:用于指示此硬件信息为GPIO信息,PAD_GPIO_C+12这个GPIO可以用此标志进行修饰
  • num_resources:用来指示用struct resource描述的硬件信息的个数

  • dev:此结构体变量中重点关注其中的void *platform_data字段

  • platform_data字段可以用来装载驱动开发者自定义的硬件信息

  • 例如:自定义的硬件信息

 struct led_resource{
 			.gpio
 			.name
}
 struct btn_resource{
 			.gpio
 			.name
 			.code
}

struct platform_driver如何使用

struct platform_driver {
	int (*probe)(struct platform_device *pdev);
	int (*remove)(struct platform_device *pdev);
	struct device_driver driver;
	...
};
  • 功能:描述一个驱动中纯软件相关内容
  • 成员说明:
  • probe: 一旦纯硬件节点和纯软件节点匹配成功,内核就会调用此函数,也就是说此函数是否被调用,至关重要, probe函数被调用,说明硬件软件匹配成功,一个完整的,硬件设备驱动诞生,这样纯软件才可以踏踏实实访问, 纯硬件形参pdev指针指向匹配成功的硬件节点对象 。

  • remove:从dev链表删除硬件节点或者从drv链表删除
    软件节点,不管删除哪一个,都代表这个驱动不再完整
    此时内核就会调用remove函数
    remove函数和probe函数是一对死对头!
    形参pdev指针指向匹配成功的硬件节点对象

  • driver:重点关注其中的name字段,此字段将来用于匹配

配套函数:

  • platform_driver_register(&软件节点对象) 向内核drv链表添加软件节点对象,内核会帮你遍历dev链表,帮你匹配,如果匹配成功,帮你调用probe函数,帮你把匹配成功的硬件节点传递给probe函数。

  • platform_driver_unregister(&软件节点对象) 从内核drv链表删除软件节点, 内核会调用软件节点的remove函数 。


代码示例及操作步骤

  • led_dev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>

//声明描述LED硬件相关的数据结构
struct led_resource {
    unsigned long phys_address; //寄存器的起始物理地址
    int size; //寄存器存储空间的大小
    int pin; //引脚编号
};

//定义初始化LED1的硬件信息对象
static struct led_resource led1 = {
    .phys_address = 0xC001C000,
    .size = 0x24,
    .pin = 17
};

//仅仅去除警告
static void led_release(struct device *dev) {}

//定义初始化描述LED的硬件信息节点对象
static struct platform_device led_dev = {
    .name = "tarena", //用于匹配
    .id = -1,//硬件节点编号
    .dev = {
        .platform_data = &led1,//装载自定义的硬件信息 
        .release = led_release //仅仅是为了去除警告而已
    }
};

static int led_dev_init(void)
{
    //1.向内核dev链表添加注册硬件节点对象即可
    //内核会帮你遍历drv链表,匹配,调用,传参
    platform_device_register(&led_dev);
    return 0;
}

static void led_dev_exit(void)
{
    //从内核的dev链表删除硬件节点对象
    platform_device_unregister(&led_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

  • led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/io.h>
#include <linux/uaccess.h>

//声明描述LED硬件信息的数据结构
struct led_resource {
    unsigned long phys_address;
    int size;
    int pin;
};

static void *gpiobase;
static unsigned long *gpioout, *gpiooutenb, *gpioaltfn0;
static int pin1;

#define LED_ON  0x100001
#define LED_OFF 0x100002
static long led_ioctl(struct file *file,
                        unsigned int cmd,
                        unsigned long arg)
{
    //1.分配内核缓冲区
    int kindex;

    //2.拷贝用户数据到内核
    copy_from_user(&kindex, (int *)arg,  sizeof(kindex));

    //3.解析用户命令
    switch(cmd) {
        case LED_ON:
                if(kindex == 1)
                    *gpioout &= ~(1 << pin1);
                /*else ... if*/
            break;
        case LED_OFF:
                if(kindex == 1)
                    *gpioout |= (1 << pin1);
                /*else ... if*/
            break;
        default:
            return -1;
    }
    return 0;
}

//定义初始化硬件操作接口对象
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = led_ioctl
};

//定义初始化混杂设备对象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};

//匹配成功,内核调用
//pdev:指向匹配成功的硬件节点
//pdev=led_dev.c中的&led_dev
static int led_probe(struct platform_device *pdev)
{
    //1.通过pdev指针获取自定义的硬件信息
    //pdata=&led1(led_dev.c中)
    struct led_resource *pdata = pdev->dev.platform_data;

    //2.处理硬件信息
    //2.1.将硬件寄存器的物理地址映射到内核虚拟地址上
    gpiobase = ioremap(pdata->phys_address, pdata->size);
    gpioout = (unsigned long *)(gpiobase + 0x00);
    gpiooutenb = (unsigned long *)(gpiobase + 0x04);
    gpioaltfn0 = (unsigned long *)(gpiobase + 0x20);
    
    //获取GPIO编号
    pin1 = pdata->pin;

    //2.2配置为GPIO功能,配置为输出,输出1
    *gpioaltfn0 &= ~(0x3 << (2*pin1));
    *gpioaltfn0 |= (1 << (2*pin1));
    *gpiooutenb |= (1 << pin1);
    *gpioout |= (1 << pin1);

    //3.注册混杂设备对象
    misc_register(&led_misc);
    return 0; //执行成功返回0,执行失败返回负值
}

//删除软件或者硬件节点,内核调用
//pdev:指向匹配成功的硬件节点
//pdev=led_dev.c中的&led_dev
static int led_remove(struct platform_device *pdev)
{
    //1.卸载混杂设备
    misc_deregister(&led_misc);
    //2.输出1,解除地址映射
    *gpioout |= (1 << pin1);
    iounmap(gpiobase);
    return 0; //执行成功返回0,执行失败返回负值
}

//定义初始化软件节点对象
static struct platform_driver led_drv = {
    .driver = {
        .name = "tarena" //用于匹配
    },
    .probe = led_probe, //匹配成功,内核调用
    .remove = led_remove, //卸载软件或者硬件,内核调用
};

static int led_drv_init(void)
{
    //1.向内核drv链表注册软件节点对象
    //什么遍历,什么匹配,什么调probe,什么传参都是内核完成
    platform_driver_register(&led_drv);
    return 0;
}

static void led_drv_exit(void)
{
    //1.从内核的drv链表删除软件节点对象
    //内核调用remove函数
    platform_driver_unregister(&led_drv);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");

  • Makefile
obj-m += led_drv.o led_dev.o
all:
	make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
	make -C /opt/kernel SUBDIRS=$(PWD) clean

  • led_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define LED_ON  0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令

int main(int argc, char *argv[])
{
    int fd;
    int index; //分配用户缓冲区,保存操作灯的编号

    if(argc != 3) {
        printf("用法:%s <on|off> <1|2|3|4>\n", argv[0]);
        return -1;
    }

    //打开设备
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0) {
        printf("打开设备失败!\n");
        return -1;
    }
    
    //"1"->1
    index = strtoul(argv[2], NULL, 0);

    //应用ioctl->软中断->内核sys_ioctl->驱动led_ioctl
    if(!strcmp(argv[1], "on"))
        ioctl(fd, LED_ON, &index);
    else if(!strcmp(argv[1], "off"))
        ioctl(fd, LED_OFF, &index);

    //关闭设备
    close(fd);
    return 0;
}

实施步骤:

  • 上位机执行:
   mkdir /opt/drivers/day10/1.0 -p
   cd /opt/drivers/day10/1.0
   touch led_dev.c led_drv.c 
   vim led_drv.c
   vim Makefile
   make
   cp *.ko /opt/rootfs/home/drivers/
  • 下位机执行:
   cd /home/drivers
   insmod led_dev.ko 
   insmod led_drv.ko //查看probe函数是否被调用
   rmmod led_drv //查看remove函数是否被调用
   rmmod led_dev      			
   
   insmod led_drv.ko
   insmod led_dev.ko //查看probe函数是否被调用
   rmmod led_dev //查看remove函数是否被调用
   rmmod led_drv
	
	#使用test demo
/home# insmod led_drv.ko
/home# insmod led_dev.ko //查看probe函数是否被调用
/home# cat /proc/devices //查看当前设备号,使用的是混杂设备 所以主设备号固定为10
/home# cat /sys/class/misc/myled/dev 
10:41
/home# mknode /dev/myled c 10 41 //创建设备文件在/dev下
/home# ./led_test on 1 //点亮LED灯。
/home# ./led_test off 1 //关闭LED灯。

相关问题

  • 问:probe函数做哪些工作呢?

  • 答:明确:probe函数被调用预示着一个完整的驱动诞生预示着纯软件可以操作访问纯硬件

  • probe函数一般做如下工作:

    • 1.通过形参pdev指针获取匹配成功的纯硬件信息(寄存器物理地址,编号等)
    • 2.要处理获取到的纯硬件信息(各种该): 该地址映射的地址映射; 该申请资源的申请资源; 该注册的注册; 该初始化的初始化
    • 3.注册混杂设备或者字符设备给用户提供访问操作硬件的接口
  • 总结:2,3两个步骤之前都是在驱动的入口函数中完成, 而现在统一放在probe函数中完成 remove和probe对着干!

  • 问:probe函数通过形参pdev指针如何获取到纯硬件信息呢(resource描述的硬件信息)

    • 答:用以下函数即可获取
 struct resource *platform_get_resource(
	 									struct platform_device *pdev,
	 									unsigned long flags,
	 									int index
	 									)
  • 函数功能:probe函数或者remove函数通过形参pdev 指针来获取到匹配成功的resource描述的硬件信息
  • 参数:
    • pdev:传递匹配成功的硬件节点的首地址
    • flags:要获取的硬件信息的类型,要不是IORESOURCE_MEM要不是IORESOURCE_IRQ
    • index:同类型资源的偏移量
    • 返回值:返回获取的resource描述的硬件信息对象首地址

单设备文件多LED控制(代码示例)

在led_dev.c文件中的申请多个LED的信息,在led_drv.c中获取并使用,具体如下:

  • led_dev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <mach/platform.h>
#include <linux/gpio.h>

//声明描述LED硬件相关的数据结构


//定义初始化LED1的硬件信息对象
static struct resource led_info[] = {
    {
        .start = 0xC001C000,
        .end = 0xC001C000 + 0x24,
        .flags = IORESOURCE_MEM //地址类信息
    },
    {
        .start = 12,//GPIO编号
        .end = 12,//无效,为了好看
        .flags = IORESOURCE_IRQ
    },
    {
        .start = 7,
        .end = 7,
        .flags = IORESOURCE_IRQ
    },
    {
        .start = 11,
        .end = 11,
        .flags = IORESOURCE_IRQ
    }
};

//仅仅去除警告
static void led_release(struct device *dev) {}

//定义初始化描述LED的硬件信息节点对象
static struct platform_device led_dev = {
    .name = "tarena", //用于匹配
    .id = -1,//硬件节点编号
    .resource = led_info,//装载resource描述的硬件信息
    .num_resources = ARRAY_SIZE(led_info),//硬件信息个数
    .dev = {
        .release = led_release //仅仅是为了去除警告而已
    }
};

static int led_dev_init(void)
{
    //1.向内核dev链表添加注册硬件节点对象即可
    //内核会帮你遍历drv链表,匹配,调用,传参
    platform_device_register(&led_dev);
    printk("register device led_dev.\n");
    return 0;
}

static void led_dev_exit(void)
{
    //从内核的dev链表删除硬件节点对象
    platform_device_unregister(&led_dev);
    printk("unregister device led_dev.\n");
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

  • led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/mm.h>
#include <linux/slab.h>


static void *gpiobase;
static unsigned long *gpiocout, *gpiocoutenb, *gpioaltfn0;
static int pin1;
static int pin2;
static int pin3;

#define LED_ON  0x100001
#define LED_OFF 0x100002
static long led_ioctl(struct file *file,
                        unsigned int cmd,
                        unsigned long arg)
{
    //1.分配内核缓冲区
    int kindex;

    //2.拷贝用户数据到内核
    copy_from_user(&kindex, (int *)arg,  sizeof(kindex));
    //3.解析用户命令
    switch(cmd) {
        case LED_ON:
                if(kindex == 1)
                    *gpiocout &= ~(1 << pin1);
                if(kindex == 2)
                    *gpiocout &= ~(1 << pin2);
                if(kindex == 3)
                    *gpiocout &= ~(1 << pin3);
                printk("open led on. kindex = %d\n", kindex);
            break;
        case LED_OFF:
                if(kindex == 1)
                    *gpiocout |= (1 << pin1);
                if(kindex == 2)
                    *gpiocout |= (1 << pin2);
                if(kindex == 3)
                    *gpiocout |= (1 << pin3);
                printk("open led off. kindex = %d\n", kindex);
            break;
        default:
            return -1;
    }
    return 0;
}

//定义初始化硬件操作接口对象
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = led_ioctl
};

//定义初始化混杂设备对象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};


//匹配成功,内核调用
//pdev:指向匹配成功的硬件节点
//pdev=led_dev.c中的&led_dev
static int led_probe(struct platform_device *pdev)
{

    //通过指针得到resource描述的硬件信息
    //preg_res = &led_info[0]
    //ppin_res = &led_info[1]
    struct resource *preg_res;
    struct resource *ppin_res_1;
    struct resource *ppin_res_2;
    struct resource *ppin_res_3;

    preg_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    ppin_res_1 = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    ppin_res_2 = platform_get_resource(pdev, IORESOURCE_IRQ, 1);
    ppin_res_3 = platform_get_resource(pdev, IORESOURCE_IRQ, 2);

    //处理硬件信息
    gpiobase = ioremap(preg_res->start, preg_res->end - preg_res->start);
    gpiocout = (unsigned long *)(gpiobase + 0x00);
    gpiocoutenb = (unsigned long *)(gpiobase + 0x04);
    gpioaltfn0 = (unsigned long *)(gpiobase + 0x20);

    //获取GPIO编号
    pin1 = ppin_res_1->start;
    pin2 = ppin_res_2->start;
    pin3 = ppin_res_3->start;

    //配置GPIO功能为输出并输出1
    //led1
    *gpioaltfn0 &= ~(0x3 << (2 *pin1));
    *gpioaltfn0 |= (1 << (2*pin1));
    *gpiocoutenb |= (1 <<pin1);
    *gpiocout |= (1 << pin1);

    //led2
    *gpioaltfn0 &= ~(0x3 << (2 *pin2));
    *gpioaltfn0 |= (1 << (2*pin2));
    *gpiocoutenb |= (1 <<pin2);
    *gpiocout |= (1 << pin2);
    
    //led3
    *gpioaltfn0 &= ~(0x3 << (2 *pin3));
    *gpioaltfn0 |= (1 << (2*pin3));
    *gpiocoutenb |= (1 <<pin3);
    *gpiocout |= (1 << pin3);

    printk("register led_misc myled.\n");
    misc_register(&led_misc);

    return 0; //执行成功返回0,执行失败返回负值
}

//删除软件或者硬件节点,内核调用
//pdev:指向匹配成功的硬件节点
//pdev=led_dev.c中的&led_dev
static int led_remove(struct platform_device *pdev)
{
    //1.卸载混杂设备
    misc_deregister(&led_misc);
    printk("unregister led_misc myled.\n");
    
    *gpiocout |= (1<<pin1);
    *gpiocout |= (1<<pin2);
    *gpiocout |= (1<<pin3);

    //解除地址映射
    iounmap(gpiobase);
    
    return 0; //执行成功返回0,执行失败返回负值
}

//定义初始化软件节点对象
static struct platform_driver led_drv = {
    .driver = {
        .name = "tarena" //用于匹配
    },
    .probe = led_probe, //匹配成功,内核调用
    .remove = led_remove, //卸载软件或者硬件,内核调用
};

static int led_drv_init(void)
{
    //1.向内核drv链表注册软件节点对象
    //什么遍历,什么匹配,什么调probe,什么传参都是内核完成
    platform_driver_register(&led_drv);
    return 0;
}

static void led_drv_exit(void)
{
    //1.从内核的drv链表删除软件节点对象
    //内核调用remove函数
    platform_driver_unregister(&led_drv);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");

  • led_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define LED_ON  0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令

int main(int argc, char *argv[])
{
    int fd;
    int index; //分配用户缓冲区,保存操作灯的编号

    if(argc != 3) {
        printf("用法:%s <on|off> <1|2|3|4>\n", argv[0]);
        return -1;
    }

    //打开设备
    fd = open("/dev/myled", O_RDWR);
    printf("open file /sys/class/misc/myled/dev\n");
    //fd = open("/sys/class/misc/myled/dev", O_RDWR);
    if (fd < 0) {
        printf("open device fail!\n");
        return -1;
    }
    
    //"1"->1
    index = strtoul(argv[2], NULL, 0);

    //应用ioctl->软中断->内核sys_ioctl->驱动led_ioctl
    if(!strcmp(argv[1], "on"))
        ioctl(fd, LED_ON, &index);
    else if(!strcmp(argv[1], "off"))
        ioctl(fd, LED_OFF, &index);

    //关闭设备
    close(fd);
    return 0;
}
  • Makefile
obj-m += led_drv.o led_dev.o
all:
	make -C /home/ww/ARM/kernel SUBDIRS=$(PWD) modules

test: led_test.c
	arm-cortex_a9-linux-gnueabi-gcc $< -o led_test
clean:
	make -C /home/ww/ARM/kernel SUBDIRS=$(PWD) clean
	rm led_test

  • 执行结果:
#先注册led_dev.ko和先注册led_drv.ko是一样的,没区别,但是使用的时候需要两个模块都存在并且匹配成功,
insmod led_dev.ko
insmod led_drv.ko
ls /sys/class/misc/

在这里插入图片描述

发布了78 篇原创文章 · 获赞 25 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_37596943/article/details/103552752