嵌入式Linux中的LED驅動控制(裝置樹方式)

fxzq發表於2024-07-02

Linux3.1之後的核心版本,引入了裝置樹的概念。同時,裝置樹還需要Bootloader的支援,如果使用Uboot,在1.1.3版本之後就可以支援裝置樹了。

裝置樹概念的提出其實有兩方面的原因。其一當然是程式碼冗餘,導致Linux核心臃腫不堪。在Linux核心原始碼中,只要是透過了基金會的認可,就可以把某個廠商的板級支援程式碼納入到Linux核心原始碼當中。比如國內曾經風靡一時的友善之臂Mini2440開發板,從Linux-2.6.31版本開始就被Linux官方核心所支援了,至今還可以在Linux核心原始碼中看到它的相關檔案(如:arch/arm/mach-s3c24xx/mach-mini2440.c以及arch/arm/configs/mini2440_defconfig等)。然而在Linux核心原始碼中,同樣還包含有很多其他廠商的板級支援檔案。其實這些開發板只要所選用的晶片相同,則很大一部分程式碼是相同的。這就造成了核心的冗餘和臃腫,並有愈演愈烈的趨勢(難怪Linus要發火了)。第二個原因,其實也是順理成章的。從Linux2.6版本之後核心就引入了platform匯流排平臺的概念,把驅動分成了裝置(platform_device)和驅動(platform_driver)兩個單獨的檔案,現在只要把裝置檔案從核心中提出來,單獨形成一個第三方檔案,不就不影響核心了嗎。這樣做還有一個好處,即這個提出來的裝置檔案,只要在驅動中統一相關介面和命名規則,該檔案的大部分工作還可以由廠商來完成(或由廠商提供的工具來完成),大大提高了開發的效率和可靠性。這個被單獨提出來的第三方檔案,後來就演化成了現在的裝置樹配置檔案。

下面就透過裝置樹方式來實現對LED的驅動。先給出裝置樹的配置內容,在核心原始碼(本例在/opt/ebf_linux_kernel_mp157_depth1/目錄下)的arch/arm/boot/dts目錄下找到一個名為“stm32mp157a-basic.dts”的檔案,該檔案就是開發板配套提供的裝置樹原始檔。開啟它,並在根節點的最後加入本例LED裝置的配置內容,如下。

/ {
    model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board";
    compatible = "st,stm32mp157a-dk1", "st,stm32mp157";

    aliases {
        ethernet0 = &ethernet0;
        serial0 = &uart4;
        serial1 = &usart1;
        serial2 = &usart2;
        serial3 = &usart3;        
    };

    chosen {
        stdout-path = "serial0:115200n8";
    };

    memory@c0000000 {
        reg = <0xc0000000 0x40000000>;
    };

    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        retram: retram@0x38000000 {
            compatible = "shared-dma-pool";
            reg = <0x38000000 0x10000>;
            no-map;
        };

        mcuram: mcuram@0x30000000 {
            compatible = "shared-dma-pool";
            reg = <0x30000000 0x40000>;
            no-map;
        };

        mcuram2: mcuram2@0x10000000 {
            compatible = "shared-dma-pool";
            reg = <0x10000000 0x40000>;
            no-map;
        };

        vdev0vring0: vdev0vring0@10040000 {
            compatible = "shared-dma-pool";
            reg = <0x10040000 0x2000>;
            no-map;
        };

        vdev0vring1: vdev0vring1@10042000 {
            compatible = "shared-dma-pool";
            reg = <0x10042000 0x2000>;
            no-map;
        };

        vdev0buffer: vdev0buffer@10044000 {
            compatible = "shared-dma-pool";
            reg = <0x10044000 0x4000>;
            no-map;
        };

        gpu_reserved: gpu@d4000000 {
            reg = <0xd4000000 0x4000000>;
            no-map;
        };
    };

    sram: sram@10050000 {
        compatible = "mmio-sram";
        reg = <0x10050000 0x10000>;
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0 0x10050000 0x10000>;

        dma_pool: dma_pool@0 {
            reg = <0x0 0x10000>;
            pool;
        };
    };

    leds {
        compatible = "gpio-leds";
        status = "okay";
        heartbeat {
            label = "heartbeat";
            gpios = <&gpioa 14 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
            default-state = "off";
        };
    };
    v3v3: regulator-3p3v {
        compatible = "regulator-fixed";
        regulator-name = "v3v3";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
        regulator-boot-on;
    };

    vdd: regulator-vdd {
        compatible = "regulator-fixed";
        regulator-name = "vdd";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
        regulator-boot-on;
    };

    vdd_usb: regulator-vdd-usb {
        compatible = "regulator-fixed";
        regulator-name = "vdd_usb";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
        regulator-boot-on;
    };

    v2v8: v2v8 {
        compatible = "regulator-fixed";
        regulator-name = "v2v8";
        regulator-min-microvolt = <2800000>;
        regulator-max-microvolt = <2800000>;
        regulator-always-on;
        regulator-boot-on;
    };

    vbus_otg: regulator-vbus-otg {
        compatible = "regulator-fixed";
        regulator-name = "vbus_otg";
        regulator-min-microvolt = <5000000>;
        regulator-max-microvolt = <5000000>;
        regulator-always-on;
        regulator-boot-on;
    };

    usb_phy_tuning: usb-phy-tuning {
        st,hs-dc-level = <2>;
        st,fs-rftime-tuning;
        st,hs-rftime-reduction;
        st,hs-current-trim = <15>;
        st,hs-impedance-trim = <1>;
        st,squelch-level = <3>;
        st,hs-rx-offset = <2>;
        st,no-lsfs-sc;
    };
    
    //以下為本次LED的追加內容
    rgb_led{
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "fire,rgb_led";
        ranges;
        //紅色LED節點
        led_red@0x50002000{
            compatible = "fire,led_red";
            reg = < 0x50002000 0x00000004
                    0x50002004 0x00000004
                    0x50002008 0x00000004
                    0x5000200C 0x00000004
                    0x50002018 0x00000004
                    0x50000A28 0x00000004 >;
            status = "okay";
        };
        //綠色LED節點
        led_green@0x50000A28{
            compatible = "fire,led_green";
            reg = < 0x50008000 0x00000004
                    0x50008004 0x00000004
                    0x50008008 0x00000004
                    0x5000800C 0x00000004
                    0x50008018 0x00000004 >;
            status = "okay";
        };
        //藍色LED節點
        led_blue@0x50000A28{
            compatible = "fire,led_blue";
            reg = < 0x50003000 0x00000004
                    0x50003004 0x00000004
                    0x50003008 0x00000004
                    0x5000300C 0x00000004
                    0x50003018 0x00000004 >;
            status = "okay";
        };
    };
};

在以上內容中,最末尾的部分才是本次追加的內容,其他部分內容是原裝置樹就有的,不要改動,完成後儲存並編譯它。編譯要在原始碼根目錄下進行(即/opt/ebf_linux_kernel_mp157_depth1/目錄下),先執行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig進行配置,然後執行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs進行編譯。編譯完成後,會在裝置樹所在目錄下(arch/arm/boot/dts)生成名為stm32mp157a-basic.dtb的裝置樹檔案,把該檔案透過NFS複製到開發板的/boot/dts/目錄下並替換原有裝置樹檔案,然後執行reboot重啟開發板(不能按reset鍵重啟)。

以下是平臺驅動部分的程式碼,檔名為led.c。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/platform_device.h>
static dev_t devid;                     //裝置號
static struct cdev led_cdev;             //定義字元型結構體
struct class *led_class;                //類結構體
struct device_node *rgb_led_device_node; //rgb_led的裝置樹節點結構體
//以下定義led資源結構體,儲存獲取得到的節點資訊以及轉換後的虛擬暫存器地址
struct led_resource
{
    struct device_node *device_node; 
    void __iomem *MODER;
    void __iomem *OTYPER;
    void __iomem *OSPEEDR;
    void __iomem *PUPDR;
    void __iomem *BSRR;
};
static void __iomem *clkaddr;    //埠時鐘變數
//以下定義RGB三個燈的led_resource結構體,儲存獲取得到的節點資訊
struct led_resource led_red;
struct led_resource led_green;
struct led_resource led_blue;
//實現open函式,為file_oprations結構體成員函式
static int led_open(struct inode *inode, struct file *filp)
{
    unsigned int tmp;
    //以下使能GPIOA、GPIOB、GPIOG埠時鐘
    tmp = ioread32(clkaddr);
    tmp |=  0x43;
    iowrite32(tmp, clkaddr);
    return 0;
}
//實現write函式,為file_oprations結構體成員函式
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    unsigned char value;
    unsigned long n;
    n = copy_from_user(&value, buf, cnt);    //從應用空間獲取值
    switch(value)            //根據應用空間的值判斷具體操作
  {
        case 0:                        //全部點亮三個LED
            iowrite32(0x20000000, led_red.BSRR);
            iowrite32(0x200000, led_blue.BSRR);
            iowrite32(0x40000, led_green.BSRR);
            break;
        case 1:                        //點亮紅色LED
            iowrite32(0x20000000, led_red.BSRR);
            break;
        case 2:                        //點亮綠色LED
            iowrite32(0x40000, led_green.BSRR);
            break;
        case 3:                        //點亮藍色LED
            iowrite32(0x200000, led_blue.BSRR);
            break;
        case 4:                        //熄滅紅色LED
            iowrite32(0x2000, led_red.BSRR);
            break;
        case 5:                        //熄滅綠色LED
            iowrite32(0x04, led_green.BSRR);
            break;
        case 6:                        //熄滅藍色LED
            iowrite32(0x20, led_blue.BSRR);
            break;
        case 7:                        //全部熄滅三個LED
            iowrite32(0x2000, led_red.BSRR);
            iowrite32(0x20, led_blue.BSRR);
            iowrite32(0x04, led_green.BSRR);
            break;
        default:                        //全部熄滅
            iowrite32(0x2000, led_red.BSRR);
            iowrite32(0x20, led_blue.BSRR);
            iowrite32(0x04, led_green.BSRR);
            break;
    }
       return cnt;
}
//實現release函式,為file_oprations結構體函式
static int led_release(struct inode *inode, struct file *filp)
{
    unsigned int tmp;
    //以下禁能GPIOA、GPIOB、GPIOG埠時鐘
    tmp = ioread32(clkaddr);
    tmp &= ~0x43;
    iowrite32(tmp, clkaddr);
    return 0;
}
//填充一個file_oprations型別的結構體,名為led_dev_fops,包含上述宣告的成員函式
static struct file_operations led_dev_fops =
    {
        .owner = THIS_MODULE,
        .open = led_open,            //指定open函式成員
        .write = led_write,            //指定write函式成員
        .release = led_release,    //指定release函式成員
};
//probe函式中,驅動提取裝置樹中的資源,並完成字元裝置的註冊
static int led_pdrv_probe(struct platform_device *pdv)
{
    unsigned int tmp;
    //獲取rgb_led的裝置樹節點
    rgb_led_device_node = of_find_node_by_path("/rgb_led");
    if (rgb_led_device_node == NULL)
    {
        printk(KERN_ERR "\t  get rgb_led failed!  \n");
        return -1;
    }
    //獲取rgb_led節點的紅燈子節點
    led_red.device_node = of_find_node_by_name(rgb_led_device_node,"led_red");
    if (led_red.device_node == NULL)
    {
        printk(KERN_ERR "\n get rgb_led_red_device_node failed ! \n");
        return -1;
    }
    //以下獲取裝置節點中紅燈子節點的reg屬性並轉化為虛擬地址
    led_red.MODER = of_iomap(led_red.device_node, 0);
    led_red.OTYPER = of_iomap(led_red.device_node, 1);
    led_red.OSPEEDR = of_iomap(led_red.device_node, 2);
    led_red.PUPDR = of_iomap(led_red.device_node, 3);
    led_red.BSRR = of_iomap(led_red.device_node, 4);
    clkaddr = of_iomap(led_red.device_node, 5);
    //以下使能GPIOA、GPIOB、GPIOG埠時鐘
    tmp = ioread32(clkaddr);
    tmp |=  0x43;
    iowrite32(tmp, clkaddr);
    //以下設定模式暫存器:輸出模式
    tmp = ioread32(led_red.MODER);
    tmp &= ~(0x3 << (13 * 2));
    tmp |= (0x1 << (13 * 2));
    iowrite32(tmp, led_red.MODER);
    //以下設定輸出型別暫存器:推輓模式
    tmp = ioread32(led_red.OTYPER);
    tmp &= ~(0x1 << 13);
    iowrite32(tmp, led_red.OTYPER);
    //以下設定輸出速度暫存器:高速
    tmp = ioread32(led_red.OSPEEDR);
    tmp &= ~(0x3 << (13 * 2));
    tmp |= (0x2 << (13 * 2));
    iowrite32(tmp, led_red.OSPEEDR);
    //以下設定上下拉暫存器:上拉
    tmp = ioread32(led_red.PUPDR);
    tmp &= ~(0x3 << (13 * 2));
    tmp |= (0x1 << (13 * 2));
    iowrite32(tmp,led_red.PUPDR);
    //以下設定置位暫存器:預設輸出高電平
    tmp = ioread32(led_red.BSRR);
    tmp |= (0x1 << 13);
    iowrite32(tmp, led_red.BSRR);
    //獲取rgb_led節點的綠燈子節點
    led_green.device_node = of_find_node_by_name(rgb_led_device_node,"led_green");
    if (led_green.device_node == NULL)
    {
        printk(KERN_ERR "\n get rgb_led_green_device_node failed ! \n");
        return -1;
    }
    //以下獲取裝置節點中綠燈子節點的reg屬性並轉化為虛擬地址
    led_green.MODER = of_iomap(led_green.device_node, 0);
    led_green.OTYPER = of_iomap(led_green.device_node, 1);
    led_green.OSPEEDR = of_iomap(led_green.device_node, 2);
    led_green.PUPDR = of_iomap(led_green.device_node, 3);
    led_green.BSRR = of_iomap(led_green.device_node, 4);
    //以下設定模式暫存器:輸出模式
    tmp = ioread32(led_green.MODER);
    tmp &= ~(0x3 << (2 * 2));
    tmp |= (0x1 << (2 * 2));
    iowrite32(tmp,led_green.MODER);
    //以下設定輸出型別暫存器:推輓模式
    tmp = ioread32(led_green.OTYPER);
    tmp &= ~(0x1 << 2);
    iowrite32(tmp, led_green.OTYPER);
    //以下設定輸出速度暫存器:高速
    tmp = ioread32(led_green.OSPEEDR);
    tmp &= ~(0x3 << (2 * 2));
    tmp |= (0x2 << (2 * 2));
    iowrite32(tmp, led_green.OSPEEDR);
    //以下設定上下拉暫存器:上拉
    tmp = ioread32(led_green.PUPDR);
    tmp &= ~(0x3 << (2 * 2));
    tmp |= (0x1 << (2 * 2));
    iowrite32(tmp,led_green.PUPDR);
    //以下設定置位暫存器:預設輸出高電平
    tmp = ioread32(led_green.BSRR);
    tmp |= (0x1 << 2);
    iowrite32(tmp, led_green.BSRR);
    //獲取rgb_led節點的藍燈子節點
    led_blue.device_node = of_find_node_by_name(rgb_led_device_node,"led_blue");
    if (led_blue.device_node == NULL)
    {
        printk(KERN_ERR "\n get rgb_led_blue_device_node failed ! \n");
        return -1;
    }
    //以下獲取裝置節點中藍燈子節點的reg屬性並轉化為虛擬地址
    led_blue.MODER = of_iomap(led_blue.device_node, 0);
    led_blue.OTYPER = of_iomap(led_blue.device_node, 1);
    led_blue.OSPEEDR = of_iomap(led_blue.device_node, 2);
    led_blue.PUPDR = of_iomap(led_blue.device_node, 3);
    led_blue.BSRR = of_iomap(led_blue.device_node, 4);
    //以下設定模式暫存器:輸出模式
    tmp = ioread32(led_blue.MODER);
    tmp &= ~(0x3 << (5 * 2));
    tmp |= (0x1 << (5 * 2));
    iowrite32(tmp,led_blue.MODER);
    //以下設定輸出型別暫存器:推輓模式
    tmp = ioread32(led_blue.OTYPER);
    tmp &= ~(0x1 << 5);
    iowrite32(tmp, led_blue.OTYPER);
    //以下設定輸出速度暫存器:高速
    tmp = ioread32(led_blue.OSPEEDR);
    tmp &= ~(0x3 << (5 * 2));
    tmp |= (0x2 << (5 * 2));
    iowrite32(tmp, led_blue.OSPEEDR);
    //以下設定上下拉暫存器:上拉
    tmp = ioread32(led_blue.PUPDR);
    tmp &= ~(0x3 << (5 * 2));
    tmp |= (0x1 << (5 * 2));
    iowrite32(tmp,led_blue.PUPDR);
    //以下設定置位暫存器:預設輸出高電平
    tmp = ioread32(led_blue.BSRR);
    tmp |= (0x1 << 5);
    iowrite32(tmp, led_blue.BSRR);
    //申請主裝置號
    if (alloc_chrdev_region(&devid, 0, 1, "led") < 0)
    {
        printk("fail to alloc devid\n");
        return -EFAULT;
    }
    led_cdev.owner = THIS_MODULE;
    //繫結前面宣告的file_oprations型別的結構體到字元裝置
    cdev_init(&led_cdev, &led_dev_fops);
    //填充上面申請到的主裝置號到字元裝置
    if ( cdev_add(&led_cdev, devid, 1) < 0)
    {
        printk("fail to add cdev\n");
        return -EFAULT;
    }
    //建立一個類
    led_class = class_create(THIS_MODULE, "my_leds");
    //建立一個裝置節點
    device_create(led_class, NULL, devid, NULL, "led");
    printk("platform driver probed!\n");
    return 0;
}
//remove函式中,刪除裝置並釋放裝置號
static int led_pdrv_remove(struct platform_device *pdev)
{
    //以下實現各個暫存器的解除對映
    iounmap(clkaddr);
    iounmap(led_green.MODER);
    iounmap(led_green.OTYPER);
    iounmap(led_green.OSPEEDR);
    iounmap(led_green.PUPDR);
    iounmap(led_green.BSRR);
    iounmap(led_red.MODER);
    iounmap(led_red.OTYPER);
    iounmap(led_red.OSPEEDR);
    iounmap(led_red.PUPDR);
    iounmap(led_red.BSRR);
    iounmap(led_blue.MODER);
    iounmap(led_blue.OTYPER);
    iounmap(led_blue.OSPEEDR);
    iounmap(led_blue.PUPDR);
    iounmap(led_blue.BSRR);
    unregister_chrdev_region(devid, 1); //釋放主裝置號
    cdev_del(&led_cdev);                          //刪除字元裝置
    device_destroy(led_class, devid);          //銷燬裝置節點
    class_destroy(led_class);                     //銷燬類
    printk("platform driver removed!\n");
    return 0;
}
//填充of_device_id結構體,名為rgb_led,用於指明匹配表
static const struct of_device_id rgb_led[] = {
    {.compatible = "fire,rgb_led"},        //匹配內容
    {/* sentinel */}
};
//以下填充一個platform_driver結構體
struct platform_driver led_platform_driver = {
    .probe = led_pdrv_probe,        //指定probe函式成員
    .remove = led_pdrv_remove,    //指定remove函式成員
    .driver = {
        .name = "rgb-leds-platform",    //指定裝置名稱
        .owner = THIS_MODULE,
        .of_match_table = rgb_led,        //指定匹配表名稱
    }
};
//以下定義模組的入口函式
static int __init led_pdrv_init(void)
{
    platform_driver_register(&led_platform_driver);//註冊一個platform驅動
    printk("led platform driver initted!\n");
    return 0;
}
//以下定義模組的出口函式
static void __exit led_pdrv_exit(void)
{
    platform_driver_unregister(&led_platform_driver); //釋放一個platform驅動
    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.o
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) 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.ko的驅動模組檔案。然後對應用程式進行交叉編譯,執行“arm-linux-gnueabihf-gcc app.c -o app”即可。實驗結果與“嵌入式Linux中的LED驅動控制”一文中的完全一樣,這裡就不給出了。

--待續--

相關文章