• 嵌入式Linux学习笔记(三) 字符型设备驱动--LED的驱动开发


    目录

    (1).参考资料

    (2).LED硬件配置实现

    (3).嵌入式内核模块实现

    (4).设备创建和释放

    (5).测试代码实现

    (6).Makefile实现

    (7).文件上传和执行

    (8).总结

    (9).代码地址

     在成功构建了一个能够运行在开发板平台的系统后,下一步就要正式开始项目的实现(这里前提是有一定的C语言基础,对ARM体系的软/硬件有一定了解),根据需求分解任务,可以发现包含的外设有LED,BEEP,RS232,六轴传感(SPI接口),光环境传感器(I2C),音频输出, RTC等,如果按照这个顺序去实现驱动,一定程度其实又回归最初的模块学习的策略,如果从应用的角度,先实现基本框架,来验证能否满足预期,这比测试模块驱动的更重要,也更容易有产出感。 按照这个需求,就可以先把实际工作分解为如下几个步骤:

      1.完成LED驱动,能够正常控制LED的点亮和关闭(本节完成)

      2.完成RS232的驱动,能够实现串口的通讯

      3.定义一套上位机、下位机之间的通讯协议(也可以使用主流工业协议如Modbus), 并在上位机和下位机编码实现通讯协议的组包和解包

      4.实现一套界面化的上位机工具,带有调试功能和控制功能

      既然初步的工作已经清晰,就可以开始第一步的工作,完成LED的驱动。

     

    参考资料

      1. 开发板原理图 《IMX6UL_ALPHA_V2.0(底板原理图)》 《IMX6ULL_CORE_V1.4(核心板原理图)》 

      2. 正点原子《Linux驱动开发指南说明V1.0》 第四十章 字符驱动设备开发

      3. 宋宝华 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》 第六章 字符驱动设备

      4. 恩智浦官方手册 《IMX6ULL参考手册》Chapter 18:Clock Controller Module(CCM)/Chapter 28:General Purpose Input/Output (GPIO)

     

    LED硬件配置实现

      首先当然要确定原理图,下图来自底板和核心板原理图。

      通过追踪就可以查看当前使用LED的引脚为GPIO1_IO3。

      确定硬件后,第一步就是配置GPIO需要使用的寄存器了,对于使用过单片机的用户来说,对于GPIO这类外设,一般包含以下步骤:

      1. 使能模块时钟

      2. 配置模块或者相关模块的寄存器,使模块复用到需要的功能

      3. 提供对外访问的接口

      对于嵌入式Linux来说,这部分也没有区别,硬件初始化接口(具体寄存器可使用《IMX6ULL参考手册》查询)

     1 /**
     2  * LED硬件初始化,引脚GPIO1_IO03
     3  * 
     4  * @param NULL
     5  *
     6  * @return NULL
     7  */
     8 static void led_gpio_init(void)
     9 {
    10     u32  value;
    11 
    12     /*1. 寄存器地址映射*/
    13     IMX6U_CCM_CCGR1 = ioremap(0X020C406C, 4);     //时钟使能 
    14     SW_MUX_GPIO1_IO03 = ioremap(0X020E0068, 4);   //复用功能设置
    15     SW_PAD_GPIO1_IO03 = ioremap(0X020E02F4, 4);   //设置PAD的输出状态
    16     GPIO1_DR = ioremap(0X0209C000, 4);            //设置LED输出
    17     GPIO1_GDIR = ioremap(0X0209C004, 4);          //设置GPIO的状态
    18 
    19     /*2.时钟使能*/
    20     value = readl(IMX6U_CCM_CCGR1);
    21     value &= ~(3 << 26);    
    22     value |= (3 << 26);
    23     writel(value, IMX6U_CCM_CCGR1);
    24     printk("led write 0");
    25 
    26     /*3.复用功能设置*/
    27     writel(5, SW_MUX_GPIO1_IO03);
    28 
    29     /*4.引脚IO功能设置*/
    30     writel(0x10B0, SW_PAD_GPIO1_IO03);
    31 
    32     /*5.引脚输出功能配置*/
    33     value = readl(GPIO1_GDIR);
    34     value |= (1 << 3);    /* 设置新值 */
    35     writel(value, GPIO1_GDIR); 
    36 
    37     /*5.关闭LED显示,高电平关闭*/
    38     value = readl(GPIO1_DR);
    39     value |= (1 << 3);    
    40     writel(value, GPIO1_DR);
    41 
    42     printk(KERN_INFO"led hardware init ok
    ");
    43 }
    View Code

      硬件资源释放.

     1 /**
     2  * 释放硬件资源
     3  * 
     4  * @param NULL
     5  *
     6  * @return NULL
     7  */
     8 static void led_gpio_release(void)
     9 {
    10     iounmap(IMX6U_CCM_CCGR1);
    11     iounmap(SW_MUX_GPIO1_IO03);
    12     iounmap(SW_PAD_GPIO1_IO03);
    13     iounmap(GPIO1_DR);
    14     iounmap(GPIO1_GDIR);
    15 }
    View Code

      硬件设备管理

     1 /**
     2  *LED灯开关切换
     3  * 
     4  * @param status  LED开关状态,1开启,0关闭
     5  *
     6  * @return NULL
     7  */
     8 static void led_switch(u8 status)
     9 {
    10     u32 value;
    11     value = readl(GPIO1_DR);
    12 
    13     switch(status)
    14     {
    15         case LED_OFF:
    16             printk(KERN_INFO"led off
    ");
    17             value |= (1 << 3);    
    18             writel(value, GPIO1_DR);
    19             break;
    20         case LED_ON:
    21             printk(KERN_INFO"led on
    ");
    22             value &= ~(1 << 3);    
    23             writel(value, GPIO1_DR);
    24             break;
    25         default:
    26             printk(KERN_INFO"Invalid LED Set");
    27             break;
    28     }
    29 }
    View Code

    至此,我们就实现了和硬件执行的接口

    led_gpio_init()/led_gpio_release()/led_switch(n)

     

    嵌入式内核模块实现

      嵌入式内核模块的参考本系列的第一篇文件,主要提供加载到Linux内核,用于insmod和rmmod访问的接口,这部分因为已经讲过,如果希望理解就去看第一节内容,或者参考上面提供的资料。

      Linux加载的接口:

     1 /**
     2  * 驱动入口函数
     3  * 
     4  * @param NULL
     5  *
     6  * @return the error code, 0 on initialization successfully.
     7  */
     8 static int __init led_module_init(void)
     9 {
    10     //此处添加设备注册的实现
    11     //......
    12 }
    13 module_init(led_module_init);
    View Code

      Linux释放的接口:

     1 /**
     2  * 驱动释放函数
     3  * 
     4  * @param NULL
     5  *
     6  * @return the error code, 0 on release successfully.
     7  */
     8 static void __exit led_module_exit(void)
     9 {
    10    //此处添加设备注销的实现
    11    //......    
    12 }
    13 module_exit(led_module_exit);
    View Code

      此外,在添加驱动说明,如作者,许可证和驱动说明等

    1 MODULE_AUTHOR("zc");                          //模块作者
    2 MODULE_LICENSE("GPL v2");                     //模块许可协议
    3 MODULE_DESCRIPTION("led driver");             //模块许描述
    4 MODULE_ALIAS("led_driver");                   //模块别名
    View Code

      至此本节的准备工作全部完成,下面就开始完成总线上设备的创建,这也是本章最核心的特征。

     

    设备创建和释放

      设备创建如果按照固定的结构,使用起来虽然有些困难,如果按照官方流程来实现,是有迹可循的。但是如何从应用层的访问接口open,read,write,close到底层驱动的xxx_open, xxx_read, xxx_write, xxxx_close的调用,这部分的理解在整个驱动机制的重要部分,这部分的难度当然不是一次可以讲清楚的,这里先抛砖引玉,在后面驱动的实践中会步步深入去理解。

      作为熟悉C语言知识的开发者来说,可以很清楚open这一类接口是用来访问文件的,而在Linux中,字符型设备和块设备就体现了"一切都是文件"的思想,参考《Linux设备驱动开发详解:基于最新的Linux 4.0内核》第5章的说明,

    通关VFS(virtual Filesytem), 将上层接口操作/dev/*下的设备文件,最后访问到驱动内部注册的实际操作硬件的接口。

     

    想理解这部分知识,就需要理解应用层接口做了什么工作,参考这篇文章,https://www.jianshu.com/p/f3f5a33f2c59以open为例。

    open函数,这里可以简述步骤(下面所有实现在linux/fs/namei.c文件中)

    1.获取一个可用的id,用于外部的记录,如fd

    2.根据name名称如"/dev/led"获取file指针信息,包含设备的实际信息

    3.将fd与file关联起来,后续就可以通关fd直接访问file指针的内容(设备端信息指针file),至此我们就获取设备端的信息

    4.创建inode类型的数据nd,这部分就是VFS中链接到真正驱动的位置信息,其中包含的cdev *i_cdev即是和设备相关的指针,至于这部分如何链接到实际设备,等后续深入了解后在详细了解。

    5.file和nd的链接则依靠file->f_path.mnt和nd->path.mnt配置相等实现

    到达这一步,当然还远远不够,但目前只是初步入门,先不过度深入,下面开始驱动编写。其中在module_init中主要完成注册流程,module_exit中完成释放流程,此外还要实现访问LED的接口,具体如下:

      1.访问LED的硬件接口链接

      1 /**
      2  * 获取LED资源
      3  * 
      4  * @param inode    
      5  * @param filp
      6  *
      7  * @return the error code, 0 on initialization successfully.
      8  */
      9 int led_open(struct inode *inode, struct file *filp)
     10 {
     11     filp->private_data = &led_driver_info;
     12     return 0;
     13 }
     14 
     15 /**
     16  * 释放LED设备资源
     17  * 
     18  * @param inode
     19  * @param filp
     20  * 
     21  * @return the error code, 0 on initialization successfully.
     22  */
     23 int led_release(struct inode *inode, struct file *filp)
     24 {
     25     return 0;
     26 }
     27 
     28 /**
     29  * 从LED设备读取数据
     30  * 
     31  * @param filp
     32  * @param buf
     33  * @param count
     34  * @param f_ops
     35  *
     36  * @return the error code, 0 on initialization successfully.
     37  */
     38 ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
     39 {
     40     return 0;
     41 }
     42 
     43 /**
     44  * 向LED设备写入数据
     45  * 
     46  * @param filp
     47  * @param buf
     48  * @param count
     49  * @param f_ops
     50  *
     51  * @return the error code, 0 on initialization successfully.
     52  */
     53 ssize_t led_write(struct file *filp, const char __user *buf, size_t count,  loff_t *f_pos)
     54 {
     55     int result;
     56     u8 databuf[2];
     57 
     58     result = copy_from_user(databuf, buf, count);
     59     if(result < 0) {
     60         printk(KERN_INFO"kernel write failed!
    ");
     61         return -EFAULT;
     62     }
     63     
     64     /*利用数据操作LED*/
     65     led_switch(databuf[0]);
     66     return 0;
     67 }
     68 
     69 /**
     70  * light从设备读取状态
     71  * 
     72  * @param filp
     73  * @param cmd
     74  * @param arg
     75  *  
     76  * @return the error code, 0 on initialization successfully.
     77  */
     78 long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
     79 {
     80     switch(cmd){
     81         case 0:
     82             led_switch(0);
     83             break;
     84         case 1:
     85             led_switch(1);
     86             break;
     87         default:
     88             printk(KERN_INFO"Invalid Cmd!
    ");
     89             return -ENOTTY;
     90     }
     91 
     92     return 0;
     93 }
     94 
     95 /* 设备操作函数 */
     96 static struct file_operations led_fops = {
     97     .owner = THIS_MODULE,
     98     .open = led_open,
     99     .read = led_read,
    100     .write = led_write,
    101     .unlocked_ioctl = led_ioctl,
    102     .release = led_release,
    103 };
    View Code

      2.创建设备,添加到设备总线上,这里要提到知识点,

      对于一个设备的基本id,由主设备号和子设备号组成,其中主设备就是挂载在/proc/devices下的设备总线上,如果设备已经存在,则可以用register_chdev_region直接生成设备信息,则需要使用alloc_chrdev_region申请新的设备信息。

      在获取设备信息结构后,可通过cdev_init将cdev,设备号以及上面的硬件操作接口函数链接起来。

      最后通过cdev_add将设备信息挂载到设备总线上,这时通过cat /proc/devices就可以查看设备是否添加成功。

     1 int result;
     2 
     3     led_driver_info.major = DEFAULT_MAJOR;
     4     led_driver_info.minor = DEFAULT_MINOR;
     5 
     6     /*在总线上创建设备*/    
     7     /*1.申请字符设备号*/
     8     if(led_driver_info.major){
     9         led_driver_info.dev_id = MKDEV(led_driver_info.major, led_driver_info.minor);
    10         result = register_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT, DEVICE_LED_NAME);
    11     }
    12     else{
    13         result = alloc_chrdev_region(&led_driver_info.dev_id, 0, DEVICE_LED_CNT, DEVICE_LED_NAME);
    14         led_driver_info.major = MAJOR(led_driver_info.dev_id);
    15         led_driver_info.minor = MINOR(led_driver_info.dev_id);
    16     }
    17     if(result < 0){
    18         printk(KERN_INFO"dev alloc or set failed
    ");    
    19         return result;
    20     }
    21     else{
    22         printk(KERN_INFO"dev alloc or set ok, major:%d, minor:%d
    ", led_driver_info.major,  led_driver_info.minor);    
    23     }
    24     
    25     /*2.添加设备到相应总线上*/
    26     cdev_init(&led_driver_info.cdev, &led_fops);
    27     led_driver_info.cdev.owner = THIS_MODULE;
    28     result = cdev_add(&led_driver_info.cdev, led_driver_info.dev_id, DEVICE_LED_CNT);
    29     if(result != 0){
    30         unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
    31         printk(KERN_INFO"cdev add failed
    ");
    32         return result;
    33     }else{
    34         printk(KERN_INFO"device add Success!
    ");    
    35     }
    View Code

      3.在/dev/下根据设备号创建设备节点,用于应用上层接口的访问,这部分和mknod /dev/led c 主设备号 从设备号功能一致,理论使用指令也可,具体如下。

     1 /* 4、创建类 */
     2     led_driver_info.class = class_create(THIS_MODULE, DEVICE_LED_NAME);
     3     if (IS_ERR(led_driver_info.class)) {
     4         printk(KERN_INFO"class create failed!
    ");
     5         unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
     6         cdev_del(&led_driver_info.cdev);    
     7         return PTR_ERR(led_driver_info.class);
     8     }
     9     else{
    10         printk(KERN_INFO"class create successed!
    ");
    11     }
    12 
    13     /* 5、创建设备 */
    14     led_driver_info.device = device_create(led_driver_info.class, NULL, led_driver_info.dev_id, NULL, DEVICE_LED_NAME);
    15     if (IS_ERR(led_driver_info.device)) {
    16         printk(KERN_INFO"device create failed!
    ");
    17                 unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);       
    18                 cdev_del(&led_driver_info.cdev);
    19         
    20         class_destroy(led_driver_info.class);
    21         return PTR_ERR(led_driver_info.device);
    22     }
    23     else{
    24         printk(KERN_INFO"device create successed!
    ");
    25     }
    26 
    27     /*硬件初始化*/
    28     led_gpio_init();
    View Code

      至此,创建设备并添加到设备总线的流程实现完毕,这就是module_init中需要的所有实现。

    2.释放模块

      在上面我们创建设备,占用了系统资源,在卸载模块的时候,这些都要全部释放,不然就会造成内存的泄露,具体如下。

     1 /**
     2  * 驱动释放函数
     3  * 
     4  * @param NULL
     5  *
     6  * @return the error code, 0 on release successfully.
     7  */
     8 static void __exit led_module_exit(void)
     9 {
    10     /* 注销字符设备驱动 */
    11     device_destroy(led_driver_info.class, led_driver_info.dev_id);
    12     class_destroy(led_driver_info.class);
    13 
    14     cdev_del(&led_driver_info.cdev);
    15     unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
    16 
    17     /*硬件资源释放*/
    18     led_gpio_release();
    19 }
    20 module_exit(led_module_exit);
    View Code
     

    测试代码实现  

      在上面驱动代码就已经实现,但对于应用来说,实现驱动并不是结束,我们还要完成测试单元,但驱动的有效性进行测试,这部分因为并不是严格的工业化项目,所以只做简单的测试,代码如下

     1 #include<unistd.h>
     2 #include<sys/types.h>
     3 #include<sys/stat.h>
     4 #include<fcntl.h>
     5 #include<stdio.h>
     6 
     7 /**
     8  * 测试LED工作
     9  * 
    10  * @param NULL
    11  *
    12  * @return NULL
    13  */
    14 int main(int argc, const char *argv[])
    15 {
    16     unsigned char val = 1;
    17     int fd;
    18 
    19     fd = open("/dev/led", O_RDWR | O_NDELAY);
    20     if(fd == -1)
    21     {
    22         printf("/dev/led open error");
    23         return -1;
    24     }
    25 
    26     if(argc > 1){   
    27         val = atoi(argv[1]);
    28     }
    29 
    30     write(fd, &val, 1);
    31 
    32     close(fd);   
    33 }
    View Code
     

    Makefile实现

      Makefile的语法也是嵌入式Linux开发中重要知识,如果没有对bash语法有深刻的认识,且理解编译原理的那部分知识,这部分其实也十分困难,这也不是三两句可以说清楚的,等积累一段时间后专门用笔记讲解这部分内容,初步能大致看懂,修改会编译就够了。

     1 KERNELDIR := /usr/code/linux
     2 CURRENT_PATH := $(shell pwd)
     3 obj-m := led.o
     4 
     5 build: kernel_modules
     6 
     7 kernel_modules:
     8     $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
     9 clean:
    10     $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    View Code

      保存为Makefile后,使用make指令,就可以编译生成需要的led.ko文件,此外通过

      arm-linux-gnueabihf-gcc -o led_test led_test.c也可以生成我们需要的测试文件。

     

    文件上传和执行

      可通过sd卡,ssh或者nfs系统,将上述文件添加到上章编译完成的系统中,

      执行insmod /usr/driver/led.ko将驱动加载

      执行lsmod查询当前加载的驱动

      通过./usr/app/led_test 1或者./usr/app/led_test 0控制LED的点亮和关闭,现象如下:

      

     

    总结

      至此,关于LED的驱动开发基本讲解完成,虽然开发参考了部分例程用了不到2个小时,但完成这篇文档用了4个小时,为了能够将知识可以解决出来,去查询书籍,以及去查看内核代码,但是这是值得的,我感觉对驱动有了更深刻的认知,但我认为这是值得的,下节将开始Uart驱动的编写实现,整个流程算走上了正轨,不过我本身还要工作,这是因为五一才有这种效率更新,不过我已经制定了计划,希望能够顺利的去学习吧。

     

    代码地址

      相关代码在https://github.com/Imx6ull-app/remote_manage中kernal_mod/led下查看。

  • 相关阅读:
    CodeForces1214B
    CodeForces1214A
    LuoGuP4551最长异或路径
    GXOI2018 滚粗记
    [BZOJ 4818/LuoguP3702][SDOI2017] 序列计数 (矩阵加速DP)
    [LuoguP3808] 【模板】AC自动机(简单版)数组版
    [NOIP 2016D2T2/Luogu P1600] 天天爱跑步 (LCA+差分)
    [CF160D]Edges in MST (最小生成树+LCA+差分)
    [Luogu P2891/POJ 3281/USACO07OPEN ]吃饭Dining
    [BZOJ 2287/POJ openjudge1009/Luogu P4141] 消失之物
  • 原文地址:https://www.cnblogs.com/zc110747/p/12803856.html
Copyright © 2020-2023  润新知