嵌入式Linux中platform平台设备模型的框架(实现LED驱动)
在前面讨论的所有LED驱动程序中,把全部设备信息和驱动代码都写在了一个文件中,从本质上看,这种开发方式与单片机的开发并没有太大的区别,一旦硬件信息发生变化,就必须要修改驱动程序的源码。然而,Linux作为一个发展成熟、功能齐全、结构复杂的操作系统,它对于代码的可维护性、复用性非常看重。为了解决驱动代码和设备信息高度耦合的问题,Linux提出了设备驱动模型的概念,在该模型中引入了总线的概念,它可以对驱动代码和设备信息进行有效地分离。
在Linux内核中,定义了一种虚拟总线,即平台总线(platform bus)。它用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来表示(继承了设备驱动模型中的device结构体)。而平台驱动使用platform_driver结构体来进行表示(继承了设备驱动模型中的device_driver结构体)。其中,device和device_driver结构体是bus总线模型中的两个重要成员(platform继承自bus),本例不进行讨论。
下面就使用platform平台设备模型来实现对LED的驱动。由于代码实现了分离,所以驱动文件有两个,一个负责描述平台设备,另一个负责平台驱动。下面先给出平台设备的代码,文件名为led_pdev.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> //以下定义总线及寄存器的物理地址 #define AHB4_PERIPH_BASE (0x50000000) #define RCC_BASE (AHB4_PERIPH_BASE + 0x0000) #define RCC_MP_GPIOENA (RCC_BASE + 0xA28) #define GPIOA_BASE (AHB4_PERIPH_BASE + 0x2000) #define GPIOA_MODER (GPIOA_BASE + 0x0000) #define GPIOA_OTYPER (GPIOA_BASE + 0x0004) #define GPIOA_OSPEEDR (GPIOA_BASE + 0x0008) #define GPIOA_PUPDR (GPIOA_BASE + 0x000C) #define GPIOA_ODR (GPIOA_BASE + 0x0014) #define GPIOA_BSRR (GPIOA_BASE + 0x0018) #define GPIOG_BASE (AHB4_PERIPH_BASE + 0x8000) #define GPIOG_MODER (GPIOG_BASE + 0x0000) #define GPIOG_OTYPER (GPIOG_BASE + 0x0004) #define GPIOG_OSPEEDR (GPIOG_BASE + 0x0008) #define GPIOG_PUPDR (GPIOG_BASE + 0x000C) #define GPIOG_ODR (GPIOG_BASE + 0x0014) #define GPIOG_BSRR (GPIOG_BASE + 0x0018) #define GPIOB_BASE (AHB4_PERIPH_BASE + 0x3000) #define GPIOB_MODER (GPIOB_BASE + 0x0000) #define GPIOB_OTYPER (GPIOB_BASE + 0x0004) #define GPIOB_OSPEEDR (GPIOB_BASE + 0x0008) #define GPIOB_PUPDR (GPIOB_BASE + 0x000C) #define GPIOB_ODR (GPIOB_BASE + 0x0014) #define GPIOB_BSRR (GPIOB_BASE + 0x0018) //以下填充一个LED设备资源的结构体(包含三个端口的配置寄存器和一个端口时钟寄存器) static struct resource led_resource[] = { [0] = DEFINE_RES_MEM(GPIOA_MODER, 4), [1] = DEFINE_RES_MEM(GPIOA_OTYPER, 4), [2] = DEFINE_RES_MEM(GPIOA_OSPEEDR, 4), [3] = DEFINE_RES_MEM(GPIOA_PUPDR, 4), [4] = DEFINE_RES_MEM(GPIOA_BSRR, 4), [5] = DEFINE_RES_MEM(GPIOG_MODER, 4), [6] = DEFINE_RES_MEM(GPIOG_OTYPER, 4), [7] = DEFINE_RES_MEM(GPIOG_OSPEEDR, 4), [8] = DEFINE_RES_MEM(GPIOG_PUPDR, 4), [9] = DEFINE_RES_MEM(GPIOG_BSRR, 4), [10] = DEFINE_RES_MEM(GPIOB_MODER, 4), [11] = DEFINE_RES_MEM(GPIOB_OTYPER, 4), [12] = DEFINE_RES_MEM(GPIOB_OSPEEDR, 4), [13] = DEFINE_RES_MEM(GPIOB_PUPDR, 4), [14] = DEFINE_RES_MEM(GPIOB_BSRR, 4), [15] = DEFINE_RES_MEM(RCC_MP_GPIOENA, 4), }; //以下定义一个release空函数,无此函数在移除模块时会出错 static void led_release(struct device *dev) { ; } //以下定义一个包含三个端口引脚的数组,其内容排列为{第几组端口,第几个引脚} //端口从0组开始,A端口为第0组;引脚也从0开始 unsigned int led_port_pin[6] = {0, 13, 6, 2, 1, 5}; //以下填充一个platform_device设备结构体 static struct platform_device led_pdev = { .name = "led_pdev", //设备名称,匹配时使用 .id = 0, //设备id号(次设备号),若只有一个设备可不用(本例可不用) .num_resources = ARRAY_SIZE(led_resource), //资源的长度 .resource = led_resource, //上面定义的LED资源结构体 .dev = { //定义设备 .release = led_release, //release函数,必须要,否则移除时报错 .platform_data = led_port_pin,//指明端口引脚数组 }, }; //以下定义模块的入口函数 static __init int led_pdev_init(void) { platform_device_register(&led_pdev);//注册一个platform设备 printk("led_dev initted!\n"); return 0; } //以下定义模块的出口函数 static __exit void led_pdev_exit(void) { platform_device_unregister(&led_pdev);//释放一个platform设备 printk("led_dev exited!\n"); } module_init(led_pdev_init); module_exit(led_pdev_exit); MODULE_LICENSE("GPL");
接下来给出平台驱动的代码,文件名为led_pdrv.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/mod_devicetable.h> #include <linux/io.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> dev_t devid; //设备号 static struct class *led_class; //类结构体 static void __iomem *clkaddr;//端口时钟变量 //定义一个led_data结构体来管理LED设备硬件信息 struct led_data { volatile void __iomem *MODER_A; volatile void __iomem *OTYPER_A; volatile void __iomem *OSPEEDR_A; volatile void __iomem *PUPDR_A; volatile void __iomem *BSRR_A; volatile void __iomem *MODER_G; volatile void __iomem *OTYPER_G; volatile void __iomem *OSPEEDR_G; volatile void __iomem *PUPDR_G; volatile void __iomem *BSRR_G; volatile void __iomem *MODER_B; volatile void __iomem *OTYPER_B; volatile void __iomem *OSPEEDR_B; volatile void __iomem *PUPDR_B; volatile void __iomem *BSRR_B; unsigned int led_r_pin, led_g_pin, led_b_pin;//定义红、绿、蓝引脚 unsigned int port_reg_a, port_reg_g, port_reg_b;//定义端口A、G、B struct cdev led_cdev; //定义字符型结构体 }; //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { unsigned int tmp; //以下通过led_cdev结构体找到led_data结构体的首地址并赋值给指针led struct led_data *led = container_of(inode->i_cdev, struct led_data, led_cdev); filp->private_data = led;//把上面获取的led结构体存入file的私有变量private_data中,后面的函数会用到 //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp |= (0x1 << led->port_reg_a) | (0x1 << led->port_reg_g) | (0x1 << led->port_reg_b); iowrite32(tmp, clkaddr); return 0; } //实现release函数,为file_oprations结构体函数 static int led_release(struct inode *inode, struct file *filp) { unsigned int tmp; //以下把私有变量private_data中的值赋值给指针led(该值在上面open函数中存入) struct led_data *led = (struct led_data *)filp->private_data; //以下禁能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp &= ~((0x1 << led->port_reg_a) | (0x1 << led->port_reg_g) |(0x1 << led->port_reg_b)); iowrite32(tmp, clkaddr); return 0; } //实现write函数,为file_oprations结构体成员函数 static ssize_t led_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos) { unsigned char value; unsigned long n; //以下把私有变量private_data中的值赋值给指针led(该值在上面open函数中存入) struct led_data *led = (struct led_data *)filp->private_data; n = copy_from_user(&value, buf, count); //从应用空间获取值 switch(value) //根据应用空间的值判断具体操作 { case 0: //全部点亮三个LED iowrite32((0x01 << (led->led_r_pin + 16)), led->BSRR_A); iowrite32((0x01 << (led->led_b_pin + 16)), led->BSRR_B); iowrite32((0x01 << (led->led_g_pin + 16)), led->BSRR_G); break; case 1: //点亮红色LED iowrite32((0x01 << (led->led_r_pin + 16)), led->BSRR_A); break; case 2: //点亮绿色LED iowrite32((0x01 << (led->led_g_pin + 16)), led->BSRR_G); break; case 3: //点亮蓝色LED iowrite32((0x01 << (led->led_b_pin + 16)), led->BSRR_B); break; case 4: //熄灭红色LED iowrite32((0x01 << led->led_r_pin), led->BSRR_A); break; case 5: //熄灭绿色LED iowrite32((0x01 << led->led_g_pin), led->BSRR_G); break; case 6: //熄灭蓝色LED iowrite32((0x01 << led->led_b_pin), led->BSRR_B); break; case 7: //全部熄灭三个LED iowrite32((0x01 << led->led_r_pin), led->BSRR_A); iowrite32((0x01 << led->led_b_pin), led->BSRR_B); iowrite32((0x01 << led->led_g_pin), led->BSRR_G); break; default: //全部熄灭 iowrite32((0x01 << led->led_r_pin), led->BSRR_A); iowrite32((0x01 << led->led_b_pin), led->BSRR_B); iowrite32((0x01 << led->led_g_pin), led->BSRR_G); } return count; } //填充一个file_oprations类型的结构体,名为led_dev_fops,包含上述声明的成员函数 static struct file_operations led_dev_fops = { .owner = THIS_MODULE,
.open = led_open, //指定open函数成员 .release = led_release, //指定release函数成员 .write = led_write, //指定write函数成员 }; //probe函数中,驱动提取platform设备中的资源,并完成字符设备的注册 static int led_pdrv_probe(struct platform_device *pdev) { unsigned int tmp; struct led_data *led; //定义一个led_data结构体的指针变量led unsigned int *led_port_pin; //定义一个无符号整型指针变量led_port_pin //以下定义用于获取设备资源的各个端口寄存器结构体和端口时钟寄存器结构体 struct resource *mem_MODER_A; struct resource *mem_OTYPER_A; struct resource *mem_OSPEEDR_A; struct resource *mem_PUPDR_A; struct resource *mem_BSRR_A; struct resource *mem_MODER_G; struct resource *mem_OTYPER_G; struct resource *mem_OSPEEDR_G; struct resource *mem_PUPDR_G; struct resource *mem_BSRR_G; struct resource *mem_MODER_B; struct resource *mem_OTYPER_B; struct resource *mem_OSPEEDR_B; struct resource *mem_PUPDR_B; struct resource *mem_BSRR_B; struct resource *mem_CLK; //以下动态申请led结构体大小的内存 led = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL); if(!led) return -ENOMEM; //以下动态申请led_port_pin变量大小的内存(有6个数值) led_port_pin = devm_kzalloc(&pdev->dev, sizeof(unsigned int)*6, GFP_KERNEL); if(!led_port_pin) return -ENOMEM; //以下获取platform设备中的私有数据,得到端口组号及引脚号,并赋值给相应的变量 led_port_pin = dev_get_platdata(&pdev->dev); led->port_reg_a = led_port_pin[0]; led->led_r_pin = led_port_pin[1]; led->port_reg_g = led_port_pin[2]; led->led_g_pin = led_port_pin[3]; led->port_reg_b = led_port_pin[4]; led->led_b_pin = led_port_pin[5]; //以下获取platform设备中的各个寄存器的地址 mem_MODER_A = platform_get_resource(pdev, IORESOURCE_MEM, 0); mem_OTYPER_A = platform_get_resource(pdev, IORESOURCE_MEM, 1); mem_OSPEEDR_A = platform_get_resource(pdev, IORESOURCE_MEM, 2); mem_PUPDR_A = platform_get_resource(pdev, IORESOURCE_MEM, 3); mem_BSRR_A = platform_get_resource(pdev, IORESOURCE_MEM, 4); mem_MODER_G = platform_get_resource(pdev, IORESOURCE_MEM, 5); mem_OTYPER_G = platform_get_resource(pdev, IORESOURCE_MEM, 6); mem_OSPEEDR_G = platform_get_resource(pdev, IORESOURCE_MEM, 7); mem_PUPDR_G = platform_get_resource(pdev, IORESOURCE_MEM, 8); mem_BSRR_G = platform_get_resource(pdev, IORESOURCE_MEM, 9); mem_MODER_B = platform_get_resource(pdev, IORESOURCE_MEM, 10); mem_OTYPER_B = platform_get_resource(pdev, IORESOURCE_MEM, 11); mem_OSPEEDR_B = platform_get_resource(pdev, IORESOURCE_MEM, 12); mem_PUPDR_B = platform_get_resource(pdev, IORESOURCE_MEM, 13); mem_BSRR_B = platform_get_resource(pdev, IORESOURCE_MEM, 14); mem_CLK = platform_get_resource(pdev, IORESOURCE_MEM, 15); //以下把获取到的寄存器地址转映射为虚拟地址 led->MODER_A = devm_ioremap(&pdev->dev, mem_MODER_A->start, resource_size(mem_MODER_A)); led->OTYPER_A = devm_ioremap(&pdev->dev, mem_OTYPER_A->start, resource_size(mem_OTYPER_A)); led->OSPEEDR_A = devm_ioremap(&pdev->dev, mem_OSPEEDR_A->start, resource_size(mem_OSPEEDR_A)); led->BSRR_A = devm_ioremap(&pdev->dev, mem_BSRR_A->start, resource_size(mem_BSRR_A)); led->PUPDR_A = devm_ioremap(&pdev->dev, mem_PUPDR_A->start, resource_size(mem_PUPDR_A)); led->MODER_G = devm_ioremap(&pdev->dev, mem_MODER_G->start, resource_size(mem_MODER_G)); led->OTYPER_G = devm_ioremap(&pdev->dev, mem_OTYPER_G->start, resource_size(mem_OTYPER_G)); led->OSPEEDR_G = devm_ioremap(&pdev->dev, mem_OSPEEDR_G->start, resource_size(mem_OSPEEDR_G)); led->BSRR_G = devm_ioremap(&pdev->dev, mem_BSRR_G->start, resource_size(mem_BSRR_G)); led->PUPDR_G = devm_ioremap(&pdev->dev, mem_PUPDR_G->start, resource_size(mem_PUPDR_G)); led->MODER_B = devm_ioremap(&pdev->dev, mem_MODER_B->start, resource_size(mem_MODER_B)); led->OTYPER_B = devm_ioremap(&pdev->dev, mem_OTYPER_B->start, resource_size(mem_OTYPER_B)); led->OSPEEDR_B = devm_ioremap(&pdev->dev, mem_OSPEEDR_B->start, resource_size(mem_OSPEEDR_B)); led->BSRR_B = devm_ioremap(&pdev->dev, mem_BSRR_B->start, resource_size(mem_BSRR_B)); led->PUPDR_B = devm_ioremap(&pdev->dev, mem_PUPDR_B->start, resource_size(mem_PUPDR_B)); clkaddr = devm_ioremap(&pdev->dev, mem_CLK->start, resource_size(mem_CLK)); //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp |= (0x1 << led->port_reg_a) | (0x1 << led->port_reg_g) | (0x1 << led->port_reg_b); iowrite32(tmp, clkaddr); //以下把GPIOA、GPIOB、GPIOG端口配置为输出、上位模式 tmp = ioread32(led->MODER_A); tmp &= ~(0x3 << (led->led_r_pin * 2)); tmp |= (0x1 << (led->led_r_pin * 2)); iowrite32(tmp, led->MODER_A); tmp = ioread32(led->MODER_B); tmp &= ~(0x3 << (led->led_b_pin * 2)); tmp |= (0x1 << (led->led_b_pin * 2)); iowrite32(tmp, led->MODER_B); tmp = ioread32(led->MODER_G); tmp &= ~(0x3 << (led->led_g_pin * 2)); tmp |= (0x1 << (led->led_g_pin * 2)); iowrite32(tmp, led->MODER_G); tmp = ioread32(led->PUPDR_A); tmp &= ~(0x3 << (led->led_r_pin * 2)); tmp |= (0x1 << (led->led_r_pin * 2)); iowrite32(tmp, led->PUPDR_A); tmp = ioread32(led->PUPDR_B); tmp &= ~(0x3 << (led->led_b_pin * 2)); tmp |= (0x1 << (led->led_b_pin * 2)); iowrite32(tmp, led->PUPDR_B); tmp = ioread32(led->PUPDR_G); tmp &= ~(0x3 << (led->led_g_pin * 2)); tmp |= (0x1 << (led->led_g_pin * 2)); iowrite32(tmp, led->PUPDR_G); //以下设定GPIOA、GPIOB、GPIOG端口初始值 iowrite32((0x01 << led->led_r_pin), led->BSRR_A); iowrite32((0x01 << led->led_b_pin), led->BSRR_B); iowrite32((0x01 << led->led_g_pin), led->BSRR_G); //申请主设备号 if(alloc_chrdev_region(&devid, 0, 1, "led") < 0) { printk("Couldn't alloc_chrdev_region!\r\n"); return -EFAULT; } led->led_cdev.owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&led->led_cdev, &led_dev_fops); //填充上面申请到的主设备号到字符设备 if(cdev_add(&led->led_cdev, devid, 1) < 0) { printk("Couldn't add chrdev!\r\n"); return -EFAULT; } //创建一个设备节点 device_create(led_class, NULL, devid, NULL, "led" ); //以下把LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中,后面移除时会用到 platform_set_drvdata(pdev, led); printk("platform driver probed!\n"); return 0; } //remove函数中,删除设备并释放设备号 static int led_pdrv_remove(struct platform_device *pdev) { //platform_get_drvdata,获取当前LED灯对应的结构体 struct led_data *cur_data = platform_get_drvdata(pdev); //删除字符设备 cdev_del(&cur_data->led_cdev); //销毁设备节点 device_destroy(led_class, devid); //释放主设备号 unregister_chrdev_region(devid, 1); printk("platform driver removed!\n"); return 0; } //以下填充一个platform_driver结构体 static struct platform_driver led_pdrv = { .probe = led_pdrv_probe, //指定probe函数成员 .remove = led_pdrv_remove, //指定remove函数成员 .driver.name = "led_pdev", //指定设备名称 }; //以下定义模块的入口函数 static __init int led_pdrv_init(void) { led_class = class_create(THIS_MODULE, "my_leds"); //创建一个类 platform_driver_register(&led_pdrv); //注册一个platform驱动 printk("led platform driver initted!\n"); return 0; } //以下定义模块的出口函数 static __exit void led_pdrv_exit(void) { platform_driver_unregister(&led_pdrv); //释放一个platform驱动 class_destroy(led_class); //销毁类 printk("led platform driver exited!\n"); } module_init(led_pdrv_init); module_exit(led_pdrv_exit); MODULE_LICENSE("GPL");
下面是编译以上两个文件配套的Makefile文件,内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/build_image/build/ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := led_pdev.o led_pdrv.o all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
以下是应用程序代码,文件名为app.c。
#include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; unsigned char val = 0; fd = open("/dev/led", O_RDWR); //打开设备节点 if( fd < 0 ) printf("can`t open\n"); if( argc != 3 ) //命令参数不对时提示 { printf("Usage :\n"); printf("%s <all|red|green|blue> <on|off>\n", argv[0]); return 0; } if(strcmp(argv[1], "all") == 0) { if(strcmp(argv[2], "on") == 0) val = 0; //值为0时全部点亮 else val = 7; //值为7时全部熄灭 } else if(strcmp(argv[1], "red") == 0) { if(strcmp(argv[2], "on") == 0) val = 1; //值为1时红色点亮 else val = 4; //值为4时红色熄灭 } else if(strcmp(argv[1], "green") == 0) { if(strcmp(argv[2], "on") == 0) val = 2; //值为2时绿色点亮 else val = 5; //值为5时绿色熄灭 } else if(strcmp(argv[1], "blue") == 0) { if(strcmp(argv[2], "on") == 0) val = 3; //值为3时蓝色点亮 else val = 6; //值为6时蓝色熄灭 } write(fd, &val, 1); //把值写入设备节点 close(fd); //关闭设备节点 return 0; }
完成后,先执行make命令编译驱动程序,编译成功后会生成名一个为led_pdev.ko的设备模块文件和一个名为led_pdrv.ko的驱动模块文件。之后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。完成后,把编译生成的两驱动模块文件和应用程序文件一起拷贝到NFS共享目录下 。然后先在开发板上执行“insmod led_pdev.ko”,把设备模块插入到内核中,然后再执行“insmod led_pdrv.ko”,把驱动模块插入到内核中(插入顺序无所谓)。可执行lsmod命令查看一下两个模块是否加载成功,并查看一下/dev目录是否已经生成了设备节点文件led。此外,还可以执行“cat /proc/devices”查看一下led设备的主设备号。以上都正常后,就可以执行应用程序来进行测试了,测试过程和结果与“嵌入式Linux中的LED驱动控制”一文中的一样。
下图是展示了一些过程,包括了:插入led_pdev.ko和led_pdrv.ko,查看当前内核模块,查看设备节点,查看新建的类,查看虚拟设备,查看platform设备等。
下图继续展示:查看platform驱动,移除led_pdev模块、移除led_pdrv模块等。
接下来对Linux中的platform平台设备进行一下讨论。
在Linux 的设备驱动模型中,总线是负责匹配设备和驱动,它维护着两个链表,里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时,总线便会调用platform_match函数对新增的设备或驱动进行匹配。在Linux内核中,通过platform_bus_type来描述平台总线,该总线在内核启动的时候自动进行注册。
平台设备的工作是为驱动程序提供设备信息,包括了硬件信息和软件信息两部分,具体说明如下。
• 硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源、IO口等等。对于硬件信息,使用结构体struct resource来保存设备所提供的资源。针对ARM芯片,通常使用IORESOURCE_MEM资源类型。在资源的起始地址和结束地址中,对于IORESOURCE_MEM(或是IORESOURCE_IO),他们表示要使用到的内存的起始位置和结束位置,若只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。
• 软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等。对于软件信息,以私有数据的形式进行封装保存,Linux设备模型使用device结构体来抽象物理设备,该结构体的成员platform_data可用于保存设备的私有数据。
平台设备的主要功能是将硬件部分的代码与驱动部分的代码分开,并一起注册到平台设备总线中。平台设备总线相当于为设备和驱动之间搭建了一座“桥梁”,他们拥有统一的数据结构和函数接口,设备与驱动的数据通过这座桥梁进行交换。
内核使用platform_device结构体来描述平台设备。当初始化了platform_device结构体后,需要把它注册(挂载)到平台设备总线上。注册平台设备需要使用platform_device_register函数。当需要注销(移除)某个平台设备时,使用platform_device_unregister函数,它通知平台设备总线移除该设备。platform_device结构体的一般形式如下。
struct platform_device { const char *name; //设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致 int id; //指定设备的编号,Linux支持同名设备,同名设备之间通过该编号进行区分 struct device dev; //Linux设备模型中的device结构体,通过继承该结构体可复用它的相关代码,方便内核管理平台设备 u32 num_resources; //记录资源的个数,内核提供了宏ARRAY_SIZE来计算数组的个数 struct resource *resource;//平台设备提供给驱动的资源 const struct platform_device_id *id_entry;//平台总线提供的另一种匹配方式,依然是通过比较字符串,id_entry用于保存匹配的结果 ...... };
内核使用platform_driver结构体来描述平台驱动。当初始化了platform_driver结构体之后,也需要把它注册(挂载)到平台设备总线上。注册平台驱动需要使用platform_driver_register函数。当需要注销(移除)某个平台驱动时,使用platform_driver_unregister函数,它通知平台设备总线移除该驱动。对于最基本的平台驱动框架,只需要实现probe函数和remove函数即可。platform_driver结构体的一般形式如下。
struct platform_driver { int (*probe)(struct platform_device *); //函数指针,当总线为设备和驱动匹配后,会回调执行该函数,对设备进行初始化 int (*remove)(struct platform_device *);//函数指针,当移除某个平台设备时,会回调执行该函数指针,通常实现probe函数操作的逆过程 struct device_driver driver; //Linux设备模型中的device_driver结构体,通过继承该结构体就获取设备模型驱动对象的特性 const struct platform_device_id *id_table;//表示该驱动能够兼容的设备类型 };
通过上面注册平台设备和平台驱动,就搭建好了整个platform设备驱动平台,那如何在驱动中获取到平台设备中的结构体resource提供的资源呢?通常会在驱动的probe函数中执行platform_get_resource函数,用来获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针(若资源类型为IORESOURCE_IRQ,平台设备驱动提供platform_get_irq函数接口)。对于存放在device结构体中的成员platform_data的软件信息,则使用dev_get_platdata函数来获取内容。当平台总线成功匹配驱动和设备时,内核会调用驱动中的probe函数,在该函数中使用上述的函数接口来获取资源并初始化设备。然后填充platform_driver结构体,最后在模块入口中调用platform_driver_register函数进行注册。
以上是对platform平台的基本介绍,下面来讨论一下代码。
先看平台设备部分的代码。在led_pdev.c文件中,填充了一个struct resource结构体,它是platform_device中的一个重要结构体(见上面struct platform_device的成员),用来描述硬件资源的具体情况,其一般形式如下。
struct resource { resource_size_t start; //指定资源的起始地址 resource_size_t end; //指定资源的结束地址 const char *name; //指定资源的名字(可以设置为NULL) unsigned long flags; //指定该资源的类型 ...... };
在资源的起始地址和结束地址中,对于IORESOURCE_MEM或者是IORESOURCE_IO,他们表示要使用的内存起始位置和结束位置。若只用一个中断引脚或者是一个通道,则它们的start和end成员值必须要相等。在资源类型中,指定的资源包括有I/O、Memory、Register、IRQ、DMA、Bus等多种类型,但在以ARM芯片为主的嵌入式Linux系统中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM类型。
在上面的设备代码中,对struct resource结构体的内容又做了进一步的封装,使用了宏DEFINE_RES_MEM来定义寄存器,并按顺序进行编号排列(在驱动中要按位置来获取相对应的硬件信息)。DEFINE_RES_MEM用于定义IORESOURCE_MEM类型的资源,只需要传入两个参数,一个是寄存器地址,另一个是大小。由于芯片寄存器都是32位的,因此,这里选择4个字节大小的空间。所有的MEM资源进行编号,0对应了GPIOA_MODER,1对应了GPIOA_OTYPER等等,驱动要根据这些编号来获得对应的寄存器地址。
在代码中还使用一个数组rled_port_pin来记录端口的寄存器及其具体引脚,在填充平台私有数据时,只需要把数组的首地址赋给platform_data即可。此外,Linux内核还提供了宏定义ARRAY_SIZE来计算struct resource的长度。全部工作完成后,在模块入口函数中调用了platform_device_register函数进行注册,这样,当系统加载该模块时,新定义的平台设备就会被注册到Linux内核中去了,即平台总线挂载了LED的平台设备。同理,在模块出口函数中调用了platform_device_unregister函数进行注销,当系统卸载该模块时,平台设备就会被从Linux内核中移除,即平台总线卸载了LED的平台设备。
接下来看平台驱动部分的代码。在led_pdrv.c文件中,自定义了一个struct led_data结构体,它内部封装了__iomem类型的寄存器名称、引脚、端口和一个字符型设备。在open接口函数中,有一句“struct led_data *led = container_of(inode->i_cdev, struct led_data, led_cdev)”,它的意思是,通过struc led_data的led_cdev成员地址找到struc led_data自身的首地址,并把它赋值给结构体指针变量led。后一句“filp->private_data = led” ,它的意思是把刚才获取到的结构体变量存入file结构体的私有变量private_data中,以便于后续的函数使用(只要有file结构体参数的接口函数都可以获取到)。比如,在其后的write及release接口函数中,都有一句“struct led_data *led = (struct led_data *)filp->private_data”,就是用来获取保存值的。
在代码中,对于寄存器的逻辑操作都是通过函数ioread32及iowrite32来进行的,要注意的是在操作BSRR寄存器时,高16位是清零,低16位是置位,所以在置位和清零操作之间相差了16,其余部分可参看前面的“嵌入式Linux中的LED驱动控制(续)”一文。
在probe函数中,定义了资源结构体resource的指针变量,用于接收(获取)platform_device中的硬件资源信息,接着通过platform_get_resource函数按编号(顺序)分别来获取各个寄存器的物理地址,通过dev_get_platdata函数获取platform设备中的私有数据(包含有端口和引脚信息)。函数devm_kzalloc是内核内存分配函数,它跟设备有关,当设备被拆卸或者驱动卸载时,内存会被自动释放(如果内存不使用时,也可以使用函数devm_kfree强制释放)。其原型为:void * devm_kzalloc (struct device * dev, size_t size, gfp_t gfp),第一个参数dev为申请内存的目标设备。第二个参数size为申请的内存大小。第三个参数gfp为申请内存的类型标志。GFP_KERNEL标志表示此次内存分配由内核进程调用,凡是内核内存的正常分配,该分配方式最常用。代码中使用该函数申请了1个led结构体的空间和6个led_port_pin的变量空间。接下来就是一些常规操作(初始化寄存器、申请设备号、注册设备、创建设备节点等),具体可参见“嵌入式Linux中字符型驱动程序的基本框架”一文。最后,执行了一句“platform_set_drvdata(pdev, led)”,它把当前配置好的LED数据信息存入平台驱动结构体pdev->dev->driver_data中,后面在移除时会用到这部分信息。
在remove函数中,进行删除设备并释放设备号等操作,它在卸载设备(或驱动)时触发执行。其中的一句“struct led_data *cur_data = platform_get_drvdata(pdev)”,就是取出刚才在probe函数的最后存入的LED数据信息,它包含有led字符型设备,在进行cdev_del时要用它来作为参数。
最后,把前面实现的probe和remove函数填充到一个platform_driver结构体结构体中去,并在模块入口函数led_pdrv_init中把它注册到内核中,并在模块出口函数led_pdrv_exit中去卸载它。这里需要注意一下,本例中的新建类和删除类都放在了入口和出口函数中进行,而创建设备节点和删除设备节点以及设备号申请和释放都放在了probe和remove函数中进行。
上述设备和驱动代码依据名称来进行匹配(在虚拟平台总线platform bus上匹配),即platform_device结构体中的成员“.name = "led_pdev"”和platform_driver结构体中的成员“.driver.name = "led_pdev"”进行匹配,成功后会触发执行驱动模块中的probe函数。如果名称不一致,则说明未匹配成功,此时只会加载模块(触发了入口函数执行),不会触发probe函数执行。其实platform平台的匹配还有很多种方式,由于本例只是一个基础示例,所以就不赘述了。