前言:
平台总线设备驱动模型,其实是总线设备驱动模型的一个子集。它的总线是platform_bus,由linux内核提供,我们只需要往里面注册设备和驱动就行了。现在有了设备树,不注册设备也行。
我对这个模型的理解,就如其名,最核心的就是“平台”二字,平台是什么呢,其实就是SOC厂商,我们知道所谓SOC就是CPU+各种常用的外设控制器,像三星、NXP等就是做这些事情的公司,同一公司推出的SOC芯片,可称为同一平台,例如三星平台的SOC有exnoys,2410,2440,等,这一系列的SOC,其外设控制器,比如串口哇,SPI哇等等,可能不同SOC之间改动并不大,而支持三星SOC的这些驱动,就可以叫做“三星平台设备驱动”,也就是说,针对三星芯片写的驱动,很多时候可以只写一套驱动,而给很多基于三星的芯片的开发板使用。这个就是平台总线设备驱动的最大好处。这其实也是对总线设备驱动思想的一种贯彻。
代码分析:
代码思想:这个模型最重要的是设备(硬件信息)和驱动分离,即有一个匹配机制,使配对起来的驱动能够从另一方得到硬件信息(设备树/注册设备),至于其他的例如生成设备节点,file_operations等,还是和字符设备驱动一样。实际上编写代码的时候,从probe之后,就完全和字符设备驱动一样了(口诀:入号注节硬操)。下面我会着重介绍设备和驱动匹配部分的代码。
在平台设备文件中:
12th_platform_dev.c
(这个属于传统写法,有设备树其实不用注册设备,再追根溯源呢,就是设备树其实会在内核启动之后被内核展开成设备,所以本质上这两者是一样的)
当然要做的是注册设备啦,是这个函数:
ret = platform_device_register(&pdev);
if(ret!=0){
printk(KERN_EMERG "platform_device_register error\n");
return ret;
}
其参数是一个描述设备的结构体,返回值等于0表示注册成功
这个描述设备的结构体,包含的信息是什么呢?首先,我们知道,设备和驱动是要匹配的,所以一定有一个名字或者什么标识,去和驱动匹配(驱动也要有这个名字才能匹配成功)。
然后我们要注册设备的目的是什么?当然是给到硬件外设的信息给到驱动,例如led驱动,就是要在这个led设备结构体里描述它gpio的寄存器是什么地址,再如是key驱动,我们除了要在这个key设备结构体里描述它的gpio寄存器地址,还有中断号也要描述,这样当这个设备和平台总线中对应的驱动匹配之后,驱动才能获取到你的板子上led或者key的寄存器/中断,从而才能写代码去控制led/key。
下面就是设备结构体的样子:
struct platform_device pdev={
.name = "pmykey",
.id = -1,
.num_resources=ARRAY_SIZE(pdev_res),//这个是硬件信息的数量
.resource = pdev_res,//这个就是存着硬件信息的结构体数组
.dev = {
.release = pdev_release,
}
};
存着硬件信息的结构体数组,里面存的当然是gpio或者中断啦,这样写:
struct resource pdev_res[]={
[0]={
.start = 0x11000C20,//KEY的寄存器基地址
.end = (0x11000C20 + 4*4 -1),
.flags = IORESOURCE_MEM, //内存类型的,GPIO属于此类
},
[1]={
.start = 999,//KEY的中断,这是我乱写的,只是为了演示,直接写中断号即可
.end = 999,
.flags = IORESOURCE_IRQ, //中断类型的
},
};
这样,当平台设备这个驱动模块被insmod时,就会把这个设备注册到平台总线中了。可以通过/sys/bus/platform_bus/devices看有没有成功注册。
12th_platform_drv.c
以下介绍分传统方法和设备树方法,写驱动中怎么和设备匹配,怎么获取硬件信息。
首先不急着获取,先注册驱动先,不然驱动没有注册到平台总线中,就没法和设备匹配,没有匹配成功自然就没法拿到硬件信息了。以下是注册平台驱动的函数,里面的参数是一个描述驱动的结构体。
ret = platform_driver_register(&pdrv);
if(ret!=0){
printk(KERN_EMERG "platform_driver_register error\n");
return ret;
}
这个描述驱动的结构体里面需要有什么内容呢?首先!当然是name或者其他什么能和设备匹配上的标识啦,不然怎么匹配。然后既然是描述一个驱动,那配对成功之后,肯定是要对外设进行操作了,如果是led驱动,要去点亮/熄灭等,如果是key驱动,要去读取按键值,设置中断之类的。所以驱动的结构体里还有一个在匹配成功后要执行的函数,那就是大名鼎鼎的probe函数。
驱动结构体在代码中的样子如下:
//还是用设备树的方式获取硬件资源
struct platform_driver pdrv = {
.probe= pdrv_prob,
.remove = pdrv_remove,
.driver={
.name = "ppmykey",//优先级3 用作匹配
.of_match_table = fdt_match_table, //优先级1 用作匹配
},
//.id_table = pdrv_id_table,//优先级2 用作匹配
};
我们可以看到,里面不仅有name,还有of_match_table,还有id_table,name好理解,那of_match_table和id_table是干嘛的呢?其实它们也是用于匹配的,而且它们不止记录了一个可以配对的名字,而是可以记录很多个。意思也即是,可以有很多个设备,使用这个驱动!
获取硬件信息传统方法:
其中name和id_table都是用于传统的匹配方式,即你需要写一个platform_dev注册到平台总线,然后这个设备的name和驱动的name或者id_table一致(只要里面的一个匹配就成),它们就会匹配成功。
name我们知道,同名就成,那如果我们的name不想取和设备的name一样,我们可以在id_table做文章,id_table写法如下:
//非设备树方式匹配获取硬件资源
const struct platform_device_id pdrv_id_table[] = {
{"pmykey",0x4444},//前面是与pdev的name匹配的,后面是辅助信息
{ },
};
“pmykey”和设备的name一样就行。同时你可以定义多个,当其他设备有和id_table列表中其中某一个一样名字的,也可以使用这个驱动。例如这个,pmykey和我们刚刚注册的设备的name一样,那我们这个驱动就能拿到刚刚注册的设备的硬件信息了。
刚才说,匹配成功之后会进入probe函数,在这里面我们就可以用字符设备驱动的口诀来进行正在的驱动代码开发了(入号注节硬操),获取硬件资源是“硬”字诀,在那一部分编写获取硬件资源的代码。以下就是驱动中获取硬件资源的代码:
//4.获取硬件资源
// 传统方式:这个其实也可以获取设备树的信息
pdev_desc->res = platform_get_resource(dev,IORESOURCE_MEM,0);//得到gpio基地址的数组
if(pdev_desc->res==NULL){
printk(KERN_EMERG "platform_get_resource error\n");
ret = -ENOMEM;
goto err_4;
}else{
printk(KERN_EMERG "platform_get_resource ok\n");
}
//+0是CON +1是DAT +2是PUD +3是DRV
pdev_desc->virreg_base = ioremap(pdev_desc->res->start,resource_size(pdev_desc->res));//映射4个寄存器
if(pdev_desc->virreg_base==NULL){
printk(KERN_EMERG "ioremap error\n");
ret = -ENOMEM;
goto err_4;
}else{
printk(KERN_EMERG "ioremap ok\n");
}
//配置GPX1-1为输入
*(pdev_desc->virreg_base) &= (~(0xF<<4)); //清除4-7位
*(pdev_desc->virreg_base) |=((0x0)<<4); //将4-7位置为0x1
//初始化GPX1-1为高电平,即和原理图一样
*(pdev_desc->virreg_base+1) &= (~(0x1<<1)); //清除第1位
*(pdev_desc->virreg_base+1) |=((0x1)<<1); //将第1位置为0x1,即高电平
//初始化GPX1-1为上拉,即和原理图一样
*(pdev_desc->virreg_base+2) &= (~(0x3<<2)); //清除【3:2】位
*(pdev_desc->virreg_base+2) |=((0x3)<<2); //将其置为0x3,即上拉
pdev_desc->irqno = platform_get_irq(dev,0);//得到中断号
ret = request_irq(pdev_desc->irqno,keyirq_handler,IRQ_TYPE_EDGE_BOTH,"keyirq",NULL);
if(ret!=0){ //返回0说明正确
printk(KERN_EMERG "pdev_desc->irqno error\n");
goto err_4;
}else{
printk(KERN_EMERG "request_irq ok\n");
}
printk(KERN_EMERG "pdev_desc->irqno is %d\n",pdev_desc->irqno);
来看看运行效果:
因为这个版本的内核不支持从平台文件获取中断号,所以报错了,但是代码在低版本的内核是有用的,可以借鉴。
获取硬件信息设备树方法:
首先看驱动中记录和设备树匹配的table是of_device_id,当里面的compatible属性和设备树某个节点的compatible匹配,就会成功probe了,然后我们就可以从这个设备树节点拿到硬件信息,如中断或者gpio等,这个table在代码里是这样的:
//设备树方式匹配获取硬件资源
const struct of_device_id fdt_match_table[]={
{.compatible = "gpio-key"},//compatible
{ },
};
再贴出设备树对应的节点:
mykey {
compatible = "gpio-key";
gpios = <&gpx1 1 GPIO_PULL_UP>;
interrupt-parent = <&gpx1>;
interrupts = <1 IRQ_TYPE_EDGE_BOTH>;
};
匹配成功之后,同上,在“硬”字诀的部分,写上获取硬件信息的函数:
// 设备树方式:
//得到节点先
pdev_desc->mynode = of_find_compatible_node(NULL,NULL,"gpio-key");
if(pdev_desc->mynode==NULL){
printk(KERN_EMERG "of_find_compatible_node error\n");
ret = -ENOMEM;
goto err_4;
}
//得到gpio
pdev_desc->mygpio = of_get_named_gpio_flags(pdev_desc->mynode,"gpios",0,NULL);
if (!gpio_is_valid(pdev_desc->mygpio))
printk("gpio isn't valid\n");
else printk("gpio num=%d",pdev_desc->mygpio);
ret= gpio_request(pdev_desc->mygpio, "gpios");
if(ret!=0){
printk(KERN_EMERG "gpio_request error\n");
ret = -ENOMEM;
goto err_5;
}
gpio_direction_input(pdev_desc->mygpio);//初始化为输入模式
//获取中断号--申请中断
pdev_desc->irqno = of_irq_get(pdev_desc->mynode,0);
ret = request_irq(pdev_desc->irqno,keyirq_handler,IRQ_TYPE_EDGE_BOTH,"keyirq",NULL);
if(ret!=0){ //返回0说明正确
printk(KERN_EMERG "pdev_desc->irqno error\n");
goto err_5;
}
printk(KERN_EMERG "pdev_desc->irqno is %d\n",pdev_desc->irqno);
这样就能获取设备树里的硬件信息了,下面且看运行效果:
我们不要使用传统方式可以注释掉id_table或者是不加载平台设备的驱动模块,这里我是不加载平台设备的驱动模块。
奇怪的现象:
到这里其实就结束了,但是我发现了一个奇怪的现象,没明白怎么回事哈哈,不过是无伤大雅的现象。
当同时使用传统方式和设备树方式时,代码里只有一个print,但是会两次地分别以两种方式获取硬件信息并打印:
这其实是我忘记注释掉传统代码,然后无意中发现的,从这里可以看出来,它确实是从设备树获取硬件信息的优先级毕竟高。
以上就是全部内容了,如有帮助点点关注~