程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

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 struct device *led_dev;

static int led_init(void)
{
    int ret = 0;

    printk("%s enter.\n", __func__);

    /* 动态分配字符设备: (major,0) */
    ret = alloc_chrdev_region(&devid, 0, 1,"led");  // ls /proc/devices看到的名字

    /* 返回值为负数,表示操作失败 */
    if(ret < 0){
        printk("alloc char dev region error\n");
        goto fail_devid;
    }

     /* 初始化字符设备,添加字符设备 */
     cdev_init(&led_cdev, &led_fops);
     ret = cdev_add(&led_cdev, devid, 1);
     /* 返回值为负数,表示操作失败 */
     if (ret < 0) {
        printk("char device add failed\n");
        goto fail_cdev;
     }else{
        printk("char device add success\n");
     }

    /* 创建类,它会在sys目录下创建/sys/class/led这个类  */
     led_cls = class_create(THIS_MODULE, "led");
     if(IS_ERR(led_cls)){
         printk("create class failed\n");
         ret = PTR_ERR(led_cls);
         goto fail_class;
     }else{
          printk("create class success\n");
      }

    /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */
     led_dev = device_create(led_cls, NULL, devid, NULL, "led0");
     if(IS_ERR(led_dev)){
         printk("create device failed\n");
         ret = PTR_ERR(led_dev);
         goto fail_device;
     }else{
         printk("create device success\n");
    }

     gpbcon = (volatile unsigned long *)ioremap(0x56000010, 16);
     gpbdata = (volatile unsigned long *)ioremap(0x56000014, 16);
     return 0;

fail_device:
    class_destroy(led_cls);
fail_class:
    cdev_del(&led_cdev);
fail_cdev:
    unregister_chrdev_region(devid, 1);
fail_devid:
    return ret;
}

几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据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>

/* 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 struct device *led_dev;

static int led_init(void)
{
    int ret = 0;

    printk("%s enter.\n", __func__);

    /* 动态分配字符设备: (major,0) */
    ret = alloc_chrdev_region(&devid, 0, 1,"led");  // ls /proc/devices看到的名字

    /* 返回值为负数,表示操作失败 */
    if(ret < 0){
        printk("alloc char dev region error\n");
        goto fail_devid;
    }

     /* 初始化字符设备,添加字符设备 */
     cdev_init(&led_cdev, &led_fops);
     ret = cdev_add(&led_cdev, devid, 1);
     /* 返回值为负数,表示操作失败 */
     if (ret < 0) {
        printk("char device add failed\n");
        goto fail_cdev;
     }else{
        printk("char device add success\n");
     }

    /* 创建类,它会在sys目录下创建/sys/class/led这个类  */
     led_cls = class_create(THIS_MODULE, "led");
     if(IS_ERR(led_cls)){
         printk("create class failed\n");
         ret = PTR_ERR(led_cls);
         goto fail_class;
     }else{
          printk("create class success\n");
      }

    /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */
     led_dev = device_create(led_cls, NULL, devid, NULL, "led0");
     if(IS_ERR(led_dev)){
         printk("create device failed\n");
         ret = PTR_ERR(led_dev);
         goto fail_device;
     }else{
         printk("create device success\n");
    }

     gpbcon = (volatile unsigned long *)ioremap(0x56000010, 16);
     gpbdata = (volatile unsigned long *)ioremap(0x56000014, 16);
     return 0;

fail_device:
    class_destroy(led_cls);
fail_class:
    cdev_del(&led_cdev);
fail_cdev:
    unregister_chrdev_region(devid, 1);
fail_devid:
    return ret;
}

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

[4]The Linux Kernel API Char devices

posted @ 2022-02-10 22:44  大奥特曼打小怪兽  阅读(313)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步