嵌入式Linux中platform平臺裝置模型的框架(實現LED驅動)

fxzq發表於2024-06-20

在前面討論的所有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);
            // iowrite32(0x20000000, 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);
            // iowrite32(0x2000, 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 0;
}
//定義一個file_oprations型別的結構體,名為led_dev_fops,包含上述宣告的成員函式
static struct file_operations led_dev_fops = {
    .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驅動控制”一文中的一樣,這裡就不再給出了。

下面就來對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的變數空間。接下來就是一些常規操作(初始化暫存器、申請裝置號、註冊裝置、建立裝置節點等),具體可參見前面的相關章節。最後,執行了一句“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函式中進行。

相關文章