led驱动程序实例

前言:
我们使用嵌入式设备,使用任意一款芯片来完成、实现某些功能,本质就是操作GPIO引脚,而操作引脚本质就是操作芯片内部的寄存器,当然了,要结合对应的电路原理图来操作。因此,使用嵌入式芯片本质就是去操作芯片的寄存器。我们在学习的时候有自底向上和自顶向下两种方法,并没有好坏之分,只有合适与否。如果有一定基础,可以考虑自顶向下,追根溯源的方法。还有些场合也应该是自顶向下的,比如看芯片手册,我们就是了解某些引脚有什么功能,怎么用的,那么就从引脚开始,看它是从芯片哪里引出来的,找到了一个节点,再往内部看这个节点又来源哪里,对应哪个寄存器,这样一步步往内探究,操纵它的方法自然也就出来,具体参考韦东山led驱动程序的视频开头部分。对于完全没有基础,没有任何了解,那可能是自底向上更合适。总之,学会反馈调节,多数时候并不是一成不变的,而是自底向上和自顶向下相结合的。

要实现某一个功能,需要知道以下几个方面内容:(1)硬件原理图,设备使用了哪些引脚;(2)查看芯片手册确定引脚GPIO的使用方法;(3)编写程序。

1、硬件原理图
我使用的板子是正点原子的imx6ull开发板,led的硬件原理图如下,使用的引脚是GPIO1_IO03

补充知识:如何看电路原理图上引脚编号及其对应参考手册中的GPIO(韦东山视频《嵌入式Linux驱动开发基础知识》P5的12分50秒处有简单提到)

在使用芯片时,如何根据原理图去定位引脚:
根据引脚编号pin_num -> 去芯片数据手册确定引脚名字pin_name -> 最后去参考手册找到这个引脚对应的控制模块control module和这个模块对应的寄存器register
以上述LED0的引脚为例。引脚编号是J2

一般在电路原理图中,每个引脚都会有一个编号,并且对应写上一个名字,通常这个名字表示这个引脚的功能。有时候功能比较多,可能就会只写一个功能,或者用/隔开表示还有其它功能。如果芯片引脚很大,单纯的顺序编号就不够了,从而引入行列编号

2、查看芯片手册确定GPIO的使用方法
在正式进入驱动开发之前,要学会如何查看芯片手册确定GPIO的使用方法,所采用的方法是自顶向下、追根溯源的方法,也即根据GPIO引脚一步步往内查看其来源并确定路径上涉及的寄存器,下面是参考韦东山老师的驱动入门课程,结合本次实验的阅读imx6ull芯片手册的过程

查看参考手册的GPIO章节(28章)可以看到一个图,右边的PAD1,PAD2就是GPIO引脚,芯片手册的图只列出了两个。从右边往左边看,GPIO引脚通过IOMUX控制器输出,再往左边看,可以看到IOMUX有Block,GPIO,IOMUXC共同控制,其中Block表示GPIO的源头可能来自其它模块,也可能来自GPIO模块,具体来自哪个模块由IOMUXC这个复用控制器来控制,同时GPIO模块又受到来自CCM时钟模块的控制,因此我们要把某一个IO复用为GPIO,从右到左的路径来看,涉及GPIO、IOMUXC、和CCM模块的多组寄存器

注意:在参考手册原本的图中,并没有CCM这个模块,韦东山老师视频里加上了这个模块。为什么需要这个模块,可以这样理解,对于ARM芯片,每个控制器/模块都是需要时钟进行同步的,这个时钟通常来自最高频率的分频,不一定就是和CPU的时钟频率一样,但是总要有一个时钟来进行同步,指导控制外设(外设分为soc内部外设和外部外设)何时更新状态。可以理解为计算机就是在时钟的指导下通过同步的方式有序执行指令进行工作的,所有的外设都需要时钟。回顾数电里面的内容,数电里面从来就没有离开过时钟,芯片内部很大器件就是由触发器组成,而触发器只在时钟的同步下更新状态。一般情况下,外设的时钟频率低于主频,以GPIO输出为例,CPU在主频同步下执行一条指令将输出值0/1写入输出寄存器,但是对应的GPIO引脚并不会马上更新状态,因为它的时钟比较慢,可能再过几个主频时钟周期,GPIO外设的时钟周期才会完成一次,此时GPIO控制器才会从输出寄存器中拿数据更新引脚的状态。在以前学习51单片机的时候,我们只需要一个晶振就可以,好像并不需要配置外设的时钟,都是拿来就用,那是因为所有外设都是使用了和cpu一样的时钟。对于高级的芯片比如ARM,其所提供的内部外设(也就是各种接口、控制器,比如GPIO控制器,LCD控制器)资源通常是过剩的,如果所有外设的时钟都开启,那么能耗会很大,因为考虑到嵌入式系统的低功耗需求,各个外设的时钟是按需开启,并且可以设置频率。

找到了配置GPIO的路径之后,我们自底向上(从左到右)再查看芯片手册确定如何确定具体使用哪些寄存器以及如何配置寄存器
在原理图中,LED0接到了引脚GPIO1_IO03,
(1)先看CCM时钟控制模块,找到芯片参考手册的CCM章节,在第18章

然后电机system Clocks小节找到下面这个表

下拉找到GPIOn模块

从表中找到GPIOn模块中GPIO1_CLK_ENABLE对应寄存器CCGR1[CG13],因此GPIO1的时钟门控使能对应时钟控制模块中寄存器CCGR1的CG13位域

紧接着找到CCM Memory Map小节中的CCGR1寄存器

可以看到描述中CCRG寄存器的27-26位用于使能gpio1的时钟,并且在这个寄存器描述的开头可以找到寄存器的地址

(2)分析IOMUXC模块寄存器,IOMUXC寄存器用于确定GPIO复用为什么功能,也配置GPIO的模式。查看芯片参考手册中GPIO那个图,看到IOMUCX控制器涉及两类寄存器,分别是SW_MUX_CTL_PAD_和SW_PAD_CTL_PAD_,我们要复用为GPIO功能,因此去IOMUXC章节的IOMUXC Memory Map小节可以找到寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03和IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 ,可以查看到如何配置GPIO1_IO03如何复用为GPIO功能以及如何配置模式。

IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03用于配置上拉/下拉,速率等模式,具体看实现需求

(3)继续查看芯片参考手册中GPIO那个图,可以看到GPIO模块涉及DR,GDIR,PSR,ICR1,ICR2,EDGE_SEL,IMR,ISR寄存器,去GPIO章节的GPIO Memory Map章节可以查看到对应的寄存器如何使用

3、编写程序
本章使用最传统的方式编写led驱动,后续会出新的博客一步步深入,以led驱动程序为例深入学习现代驱动程序是如何编写的
嵌入式软件编写是离不开硬件的,因此首先总结涉及到的寄存器,再编写代码
(1)首先要使能CCM_CCGR1寄存器中的CG13位域,以使能GPIO模块的时钟。再第2部分,已经列出了CCM_CCGR1的寄存器地址为20C_406Ch,并且默认是使能的,因此可以不用设置CCM寄存器了
(2)配置IOMUXC模块的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址为20E_0068h)中的MUX_MODE位域也即IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03[0, 3]为0101,将GPIO1_IO03复用为gpio1模式。再配置IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03(地址为20E_02F4h)寄存器配置gpio口的上下拉等模式,这里可以先不考虑模式配置。
(3)配置GPIO模块的寄存器GPIO1_DR,GPIO1_GDIR,GPIO_PSR,GPIO_ICR1,GPIO_ICR2,GPIO_IMR,GPIO_ISR,GPIO_EDGE_SEL
查阅参考手册得到GPIO1模块寄存器组的地址

DR寄存器用于设置GPIO的输出值或者用于读取GPIO作为输入时的值,对于GPIO1_IO03,就是操作DR[3]

GDIR寄存器用于设置GPIO的方向,对于本次实验,GPIO1_IO03用于控制LED0亮灭,因此设置为输出,GDIR[3] = 1

PSR寄存器是read_only,当GPIO被设置为输入时,可以读取对应位获取对应引脚的输入状态,具有锁存功能。比DR寄存器读取状态准确

ICR、IMR、ISR寄存器是当引脚复用为中断功能时用于配置中断的,这里暂时不涉及

总结一下,我们需要操作的寄存器也就IOMUXC模块的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址为20E_0068h),GPIO模块的寄存器GPIO1_DR,GPIO1_GDIR。下面用最传统的方式编写LED0驱动,而编写这个驱动,本质就是GPIO1_IO03的引脚驱动。

根据上面的总结,结合前一篇关于hello驱动程序的代码编写逻辑,一个简易的led驱动的代码编写逻辑可以如下组织:
(1)实现open, write,close函数并使用这三个函数构造operation函数
(2)在init函数进行寄存器地址映射,在exit函数撤销映射
(3)在open函数配置GPIO1_IO03为输出
(4)在write函数根据用户空间传入的参数设施GPIO1_IO03的输出
(5)完善其它内容,比如创建设备节点,将init函数、exit函数分别指定为入口函数、出口函数,打印内核信息等。

led驱动代码如下:

点击查看代码
//led_driver.c

#include "asm/memory.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/printk.h>
#include <linux/init.h>
#include <asm/io.h>

// (1)确定主设备号,也可以让内核分配
static int major = 0;
static int state;
static struct class *led_class;

/*
需要操作的寄存器也就IOMUXC模块的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址为20E_0068h),
GPIO模块的寄存器GPIO1_DR(209_C000h),
GPIO1_GDIR(209_C004h)
*/
static volatile unsigned int *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static volatile unsigned int *GPIO1_DR;
static volatile unsigned int *GPIO1_GDIR;

#define MIN(a, b)   (a < b ? a : b)

// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t led_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
    char val;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
   /*
   读取从用户空间传入的数据,给DR寄存器写0/1控制GPIO1_IO03输出高/低电平以控制LED亮灭
   */
    copy_from_user(&val, buf, 1);
    if(val)
    {
        *GPIO1_DR &= ~(1 << 3);

        printk("led on\n");
    }
    else {
        *GPIO1_DR |= (1 << 3);
        printk("led off\n");
    }
    return 1;   //返回1,表示写入1个数据

}


static int led_drv_open (struct inode * node, struct file * file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /*
    打开设备,配置gpio为输出
    因为加载驱动后不一定使用驱动,因此寄存器地址映射在open处进行而不是在init函数中进行更合理
    毕竟打开设备就意味着要使用设备了
    */
    int val;
    val = *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
    val &= ~0xf;
    val |= 0x5;
    *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = val;

    *GPIO1_GDIR |= (1 << 3);    //设置为输出

    return 0;
}
static int led_drv_close (struct inode * node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /*
    关闭设备,撤销寄存器地址映射

    */
    return 0;
}

// (2)定义自己的 file_operations
static struct file_operations led_drv = {
    .owner  = THIS_MODULE,
    .open   = led_drv_open,
    .write  = led_drv_write,
    .release    = led_drv_close,
};



// (4)把 file_operations 结构体告诉内核:register_chrdev 注册驱动程序,因此先跳到步骤5


// (5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init led_init(void)
{
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    major = register_chrdev(0, "led_driver", &led_drv); //注册驱动
    /*
    寄存器地址映射
    */
    /*
    需要操作的寄存器也就IOMUXC模块的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址为20E_0068h),
    GPIO模块的寄存器GPIO1_DR(209_C000h),
    GPIO1_GDIR(209_C004h)
    */
    IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(0x020E0068, 4);
    GPIO1_DR = ioremap(0x0209C000, 4);
    GPIO1_GDIR = ioremap(0x0209C004, 4);


    led_class = class_create(THIS_MODULE, "led_driver");
    err = PTR_ERR(led_class);
    if (IS_ERR(led_class))
    {
        unregister_chrdev(major, "led_driver");
        return -1;
    }

    device_create(led_class, NULL, MKDEV(major, 0), NULL, "led_driver");

    return 0;

}

// (6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit led_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    iounmap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    device_destroy(led_class, MKDEV(major, 0));
    class_destroy(led_class);


    unregister_chrdev(major, "led_driver");
    
}

// (7)其他完善:提供设备信息,自动创建设备节点:class_create,device_create
module_init(led_init);    //凭什么说上面的Init函数就是入口函数?这里这个宏的作用就是告诉内核这是入口函数
module_exit(led_exit);
MODULE_LICENSE("GPL");

用户空间中led应用程序如下:

点击查看代码
//led_test.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>


int main(int argc, char **argv)
{
    int fd;
    char status;
    if(argc != 3)
    {
        printf("Usage: %s <dev> [on/off]\n", argv[0]);
    }
    fd = open(argv[1], O_RDWR);
    if(fd < 0)
    {
        printf("can not open\n");
        return -1;
    }
    if(strcmp(argv[2], "on") == 0)
    {
        status = 1;
        write(fd, &status, 1);
    }
    else {
        status = 0;
        write(fd, &status, 1);
    }
    return 0;

}


posted @ 2024-04-09 01:01  tstars  阅读(291)  评论(0)    收藏  举报