• linux驱动移植LED字符设备驱动


    我们在linux驱动基础概念这一节中粗略介绍了linux驱动的概念,以及应用程序是如何调用驱动程序的。

    这一节我们将一点亮LED为例来介绍字符设备驱动的编写。

    一、LED硬件资源

    1.1 硬件接线

    查看Mini2440原理图、S3C2440数据手册,了解如何点亮LED。Mini2440裸机开发之点亮LED中我们已经介绍了Mini2440 LED1~LED4的接线方式,以及寄存器的设置,这里简单说一下,就不具体介绍了:

    • LED1~LED4对应引脚GPB5~GPB8,以点亮LED1为例;
    • 配置控制寄存器GPBCON(0x56000010)的bit[11:10]=01,使GPB5引脚为输出模式;
    • 配置数据寄存器GPBDAT(0x56000014)的bit5=0,使GPB5引脚输出低电平;

    二、LED驱动程序

    在/work/sambashare/drivers下创建2.led_dev文件夹。用来保存LED驱动程序以及测试应用程序。

    2.1 编写led_open、led_write函数

    /* GPB寄存器 */
    static volatile unsigned long *gpbcon = NULL;
    static volatile unsigned long *gpbdata = NULL;
    
    /* GPB5~GPB8配置为输出 */
    static int led_open(struct inode *inode, struct file *file)
    {
        *gpbcon &= ~((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16));
        *gpbcon |= ((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16));
        return 0;
    }
    
    /* 点亮/熄灭 LED01~LED4 */
    static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
    {
        int val;
        
        copy_from_user(&val, buf, count);   // 用户空间到内核空间传递数据
    
        printk("value %d",val);
    
        if(val == 1){
            /* 点亮 */
            *gpbdata &= ~((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8));
        }
        else{
            /* 熄灭 */
            *gpbdata |= ((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8));
        }
        return 0;
    }

    2.2 注册LED驱动程序

    static struct file_operations led_fops = {
        .owner  =   THIS_MODULE,
        .open   =   led_open,
        .write  =   led_write,
    };
    
    static dev_t devid;                   // 起始设备编号
    static struct cdev led_cdev;          // 保存操作结构体的字符设备 
    static struct class *led_cls;
    
    static int led_init(void)
    {
        
        /* 动态分配一组字符设备编号: (major,0) */
        if(OK == alloc_chrdev_region(&devid, 0, 1,"led")){   // ls /proc/devices看到的名字
            printk("register_chrdev_region ok\n");
        }else {
            printk("register_chrdev_region error\n");
            return ERROR;
        }
        
         cdev_init(&led_cdev, &led_fops);
         cdev_add(&led_cdev, devid, 1);
    
    
        /* 创建类,它会在sys目录下创建/sys/class/led这个类  */
         led_cls = class_create(THIS_MODULE, "led");
         if(IS_ERR(led_cls)){
             printk("can't create class\n");
             return ERROR;
         }
        /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */
         device_create(led_cls, NULL, devid, NULL, "led0"); 
    
         gpbcon = (volatile unsigned long *)ioremap(0x56000010, 4);
         gpbdata = (volatile unsigned long *)ioremap(0x56000014, 4);
         return 0;
    }

    几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:

    •  I/O 映射方式(I/O-mapped):典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
    • 内存映射方式(Memory-mapped):RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

    但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。

    一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。

    Linux在io.h头文件中声明了函数ioremap,用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中(这里是内核空间):

    #define ioremap(cookie,size)           __ioremap(cookie,size,0)
    void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

    其中:

    • phys_addr:要映射的起始的IO地址;
    • size:要映射的空间的大小;
    • flags:要映射的IO空间和权限有关的标志;

    该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。在本例中就是通过读写ioremap之后的虚拟地址进行控制io引脚的。

    2.3 卸载LED驱动程序

    static void __exit led_exit(void)
    {
        /* 注销虚拟地址 */
        iounmap(gpbcon);
        iounmap(gpbdata);
    
        printk("led driver exit\n");
        /* 注销类、以及类设备 /sys/class/led会被移除*/
        device_destroy(led_cls, devid);
        class_destroy(led_cls);
    
        cdev_del(&led_cdev);
        unregister_chrdev_region(devid, 1);
        return;
    }

    使用iounmap取消ioremap所做的映射。

    2.4 led_dev.c完整代码   

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/io.h>
    #include <linux/uaccess.h>
    
    /*
     全局静态变量:希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用,也就是说限制其作用域只在
     定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用
     */
    #define OK   (0)
    #define ERROR  (-1)
    
    /* GPB寄存器 */
    static volatile unsigned long *gpbcon = NULL;
    static volatile unsigned long *gpbdata = NULL;
    
    /* GPB5~GPB8配置为输出 */
    static int led_open(struct inode *inode, struct file *file)
    {
        *gpbcon &= ~((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16));
        *gpbcon |= ((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16));
        return 0;
    }
    
    /* 点亮/熄灭 LED01~LED4 */
    static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
    {
        int val;
        
        copy_from_user(&val, buf, count);   // 用户空间到内核空间传递数据
    
        printk("value %d",val);
    
        if(val == 1){
            /* 点亮 */
            *gpbdata &= ~((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8));
        }
        else{
            /* 熄灭 */
            *gpbdata |= ((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8));
        }
        return 0;
    }
    
    static struct file_operations led_fops = {
        .owner  =   THIS_MODULE,
        .open   =   led_open,
        .write  =   led_write,
    };
    
    static dev_t devid;                   // 起始设备编号
    static struct cdev led_cdev;          // 保存操作结构体的字符设备 
    static struct class *led_cls;
    
    static int led_init(void)
    {
        
        /* 动态分配字符设备: (major,0) */
        if(OK == alloc_chrdev_region(&devid, 0, 1,"led")){   // ls /proc/devices看到的名字
            printk("register_chrdev_region ok\n");
        }else {
            printk("register_chrdev_region error\n");
            return ERROR;
        }
        
         cdev_init(&led_cdev, &led_fops);
         cdev_add(&led_cdev, devid, 1);
    
    
        /* 创建类,它会在sys目录下创建/sys/class/led这个类  */
         led_cls = class_create(THIS_MODULE, "led");
         if(IS_ERR(led_cls)){
             printk("can't create class\n");
             return ERROR;
         }
        /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */
         device_create(led_cls, NULL, devid, NULL, "led0"); 
    
         gpbcon = (volatile unsigned long *)ioremap(0x56000010, 4);
         gpbdata = (volatile unsigned long *)ioremap(0x56000014, 4);
         return 0;
    }
    
    static void __exit led_exit(void)
    {
        /* 注销虚拟地址 */
        iounmap(gpbcon);
        iounmap(gpbdata);
    
        printk("led driver exit\n");
        /* 注销类、以及类设备 /sys/class/led会被移除*/
        device_destroy(led_cls, devid);
        class_destroy(led_cls);
    
        cdev_del(&led_cdev);
        unregister_chrdev_region(devid, 1);
        return;
    }
    
    module_init(led_init);
    module_exit(led_exit);
    MODULE_LICENSE("GPL");
    View Code

    2.5 Makefile

    KERN_DIR :=/work/sambashare/linux-5.2.8
    all:
        make -C $(KERN_DIR) M=`pwd` modules 
    clean:
        make -C $(KERN_DIR) M=`pwd` modules clean
        rm -rf modules.order
    
    obj-m += led_dev.o

    三、LED驱动测试应用程序

    在2.led_dev下创建test文件夹,保存测试应用程序。

    3.1 main.c

    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    void print_usage(char *file)
    {
        printf("Usage:\n");
        printf("%s <dev> <on|off>\n",file);
        printf("eg. \n");
        printf("%s /dev/led0 on\n", file);
        printf("%s /dev/led0 off\n", file);
    }
    
    int main(int argc,char **argv)
    {
        int fd;
        int val;
        char *filename;
    
        if (argc != 3){
            print_usage(argv[0]);
            return 0;
        }
    
        filename = argv[1];
        fd = open(filename,O_RDWR);
        if(fd == -1){
            printf("can't open %s!\n",filename);
            return 0;
        }
    
       if (!strcmp("on", argv[2])){
            // 亮灯
            val = 1;
            printf("%s on!\n",filename);
            write(fd, &val, 4);
        }else if (!strcmp("off", argv[2])){
            // 灭灯
            val = 0;
            printf("%s off!\n",filename);
            write(fd, &val, 4);
        }else{
            print_usage(argv[0]);
        }
    
        return 0;
    }

    3.2 Makefile

    all:
        arm-linux-gcc -march=armv4t -o main main.c
    clean:
        rm -rf *.o main

    四、烧录开发板测试

    LED驱动目录结构如下:

    4.1 编译驱动

    执行make命令编译驱动,并将驱动程序拷贝到nfs文件系统:

    cd /work/sambashare/drivers/2.led_dev
    make
    cp /work/sambashare/drivers/2.led_dev/led_dev.ko /work/nfs_root/rootfs

    安装驱动:

    [root@zy:/]# insmod led_dev.ko
    led_dev: loading out-of-tree module taints kernel.
    register_chrdev_region ok

    查看设备节点文件:

    [root@zy:/]# ls /dev/led0 -l
    crw-rw----    1 0        0         249,   0 Jan  1 00:00 /dev/led0

    4.2 编译测试应用程序

    执行make命令编译测试应用程序,并将测试应用程序拷贝到nfs文件系统:

    cd test
    make
    cp ./main /work/nfs_root/rootfs

    运行应用程序:

    ./main /dev/led0 on
    ./main /dev/led0 off

    可以看到LED1~LED4同时点亮和同时熄灭。

    如果你想单独控制每一个LED,那需要为每个LED编写对应的驱动程序,这里就不演示了。

    4.3 卸载LED驱动

    通过用lsmod可以查看当前安装了哪些驱动:

    [root@zy:/]# lsmod
    led_dev 1956 0 - Live 0xbf004000 (O)

    卸载时直接运行:

    rmmod led_dev

    五、代码下载

    Young / s3c2440_project[drivers]

    参考文章

    [1]二、Linux驱动之简单编写字符设备

    [2]07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动 

    [3]3.修改第一个程序来点亮LED

  • 相关阅读:
    Hadoop命令解释
    sql的嵌套
    设计模式1 订阅者模式
    我的桌面515
    夜黑我也黑
    测试测试
    竖表转横表(支持多列)
    昨天晚上做了一个梦
    viewpage插件修改版增加 复制媒体文件地址
    PhireeNote 只有自动保存功能的简易记事本
  • 原文地址:https://www.cnblogs.com/zyly/p/15881284.html
Copyright © 2020-2023  润新知