前面说了那么多,终于到了这一激动人心的环节——点灯。我们在一开始做裸机开发的时候就是从点灯开始的,那么如何在Linux下通过驱动程序点亮一个LED呢?其实和裸机开发差不多,Linux下也是通过最终配置寄存器实现IO口输入输出的功能的。我们要点亮的LED是连接在GPIO1_IO03这个引脚上,在引脚输出低电平的时候灯点亮。所以我们就需要通过驱动程序实现对该引脚的操作。

地址映射

我们要通过系统对指定地址的寄存器进行操作,但是一定要清楚:我们通过系统操作的地址并不是寄存器的实际物理地址。这里需要用到一个叫做MMU(内存管理单元Memory Management Unit)的东西。关于MMU的内容太多了,我们一定不要深究。大致原理可以看一下下面的图

CentOS系统点亮硬盘_#define

 

也就是说CPU是通过虚拟地址来操作实际的物理地址的。所以我们只需要知道怎么将物理地址和虚拟地址之间转换就行了。这里就要用到两个函数

static inline void __iomem *ioremap(phys_addr_t offset, size_t size)
{
    return (void __iomem *)(unsigned long)offset;
}
#endif

/********** ionmap的定义没找到!  **********/
#define iounmap                __arm_iounmap

第一个函数ioremap就是将物理地址换算到虚拟地址,要注意点是这个虚拟地址是指针的形式。第二个函数就是在用完虚拟地址后将其释放掉。

MMU程序中的使用

在裸机开发里我们讲过,点亮LED的过程基本上就是初始化CCM和GPIO,然后再向对应的GPIO里写入数据。在使用的时候就要对CCM和GPIO对应的地址在程序中转换为虚拟地址

#include <linux/io.h>

/**
 * @brief 待使用的寄存器物理地址
 * 
 */
#define CCM_CCGR1_BASE          (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE  (0X020E02F4)
#define GPIO1_GDIR_BASE         (0X0209C004)
#define GPIO1_DR_BASE           (0X0209C000)

/**
 * @brief 映射后的虚拟地址
 * 
 */
static void __iomem *IMX6UL_CCM_CCGR1;
static void __iomem *IMX6UL_SW_MUX_GPIO1_IO03;
static void __iomem *IMX6UL_SW_PAD_GPIO1_IO03;
static void __iomem *IMX6UL_GPIO1_DR;
static void __iomem *IMX6UL_GPIO1_GDIR;

int void main(void)
{
    IMX6UL_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
    IMX6UL_SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    IMX6UL_SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    IMX6UL_GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    IMX6UL_GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
}

把虚拟地址和物理地址都定义成宏以便后续的使用,物理地址在手册里能查到。因为每个寄存器占了4个字节,所以在使用ioremap函数的时候第二个参数是4。在程序里使用映射以后的地址就可以了。

内核空间数据操作

在获取到虚拟地址以后,在初始化的过程中要向对应的寄存器中写入相应的数据,这个过程是驱动里的,所以是在内核空间的内容。这里就要用到一个写操作了。常用的读写数据的函数也是在io.h这个库文件里

/**********读函数**********/
#ifndef readb
#define readb readb
static inline u8 readb(const volatile void __iomem *addr)
{
    return __raw_readb(addr);
}
#endif

#ifndef readw
#define readw readw
static inline u16 readw(const volatile void __iomem *addr)
{
    return __le16_to_cpu(__raw_readw(addr));
}
#endif

#ifndef readl
#define readl readl
static inline u32 readl(const volatile void __iomem *addr)
{
    return __le32_to_cpu(__raw_readl(addr));
}
#endif
/**********写函数**********/
#ifndef writeb
#define writeb writeb
static inline void writeb(u8 value, volatile void __iomem *addr)
{
    __raw_writeb(value, addr);
}
#endif

#ifndef writew
#define writew writew
static inline void writew(u16 value, volatile void __iomem *addr)
{
    __raw_writew(cpu_to_le16(value), addr);
}
#endif

#ifndef writel
#define writel writel
static inline void writel(u32 value, volatile void __iomem *addr)
{
    __raw_writel(__cpu_to_le32(value), addr);
}
#endif

可以看出来,读和写的函数就是在write/read后面加上数据的类型(b——1个byte;w——1个word;l——4个byte)。由于我们的寄存器都是32位的对应4个字节,所以读和写都要用到readl和writel。

初始化

在对寄存器初始化的时候和裸机开发时候的思路一样,都是读——改——写。我们要先将寄存器内值读出来,修改为我们需要的值以后再写回去。比如我们需要初始化CCM

int val = 0;
val = readl(IMX6UL_CCM_CCGR1);  //读取CCM_CCGR1的值
val &= ~(3<<26);                //清除bit26、27
val |= (3<<26);                 //bit26、27置1
writel(val, IMX6UL_CCM_CCGR1);

过程跟裸机开发一样,没什么可说的,主要就是用到了readl和writel两个函数,修改的值哪个bit置0置1这里就不讲了。

驱动编写测试

这里先做一个简单的驱动,目的就是挂载驱动模块的时候灯点亮,卸载模块的时候灯熄灭。原理很简单,就是在挂载模块的时候对应的初始化函数里对相应的寄存器设置值,LED对应的GPIO01的DR(数据寄存器)里对应的bit为0(低电平时LED点亮),在卸载模块的时候将该bit置1。

整个过程代码如下

/**
 * @file led.c
 * @author your name (you@domain.com)
 * @brief led点亮程序模块测试
 * @version 0.1
 * @date 2022-04-04
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#define DEV_MAJOR   200             //设备号
#define DEV_NAME    "LED"   //设备名称

// static char writebuf[100];
// static char readbuf[100];

/**
 * @brief 寄存器物理地址
 * 
 */
#define CCM_CCGR1_BASE          (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE  (0X020E02F4)
#define GPIO1_GDIR_BASE         (0X0209C004)
#define GPIO1_DR_BASE           (0X0209C000)

/**
 * @brief   内存映射后虚拟内存地址 
 * 
 */
static void __iomem *IMX6UL_CCM_CCGR1;
static void __iomem *IMX6UL_SW_MUX_GPIO1_IO03;
static void __iomem *IMX6UL_SW_PAD_GPIO1_IO03;
static void __iomem *IMX6UL_GPIO1_DR;
static void __iomem *IMX6UL_GPIO1_GDIR;



// /**
//  * @brief 打开设备文件
//  * 
//  * @return int 
//  */
static int led_open(struct inode *inode, struct file *filp)
{
    printk("dev open!\r\n");
    return 0;
}

/**
 * @brief 关闭设备文件
 * 
 * @return int 
 */
static int led_release(struct inode *inode, struct file *filp)
{
    printk("dev release!\r\n");
    return 0;
}

/**
 * @brief 读设备文件数据
 * 
 * @param filp 
 * @param buf 
 * @param count 
 * @param ppos 
 * @return ssize_t 
 */
static ssize_t led_read(struct file *filp, 
                               __user char *buf,
                               size_t count, 
                               loff_t *ppos)
{
    int ret = 0;
    printk("dev read data!\r\n");

    if (ret == 0){
        return 0;
    }
    else{
        printk("kernel read data error!");
        return -1;
    }
}

/**
 * @brief 设备文件数据写入
 * 
 * @param file 
 * @param buf 
 * @param count 
 * @param ppos 
 * @return ssize_t 
 */
static ssize_t led_write(struct file *file, 
                        const char __user *buf, 
                        size_t count, 
                        loff_t *ppos)
{   
    int ret = 0;
    printk("dev write data!\r\n");
    if (ret == 0){
        return 0;
    }
    else{
        printk("kernelwrite err!\r\n");
        return -1;
    }
}   

/**
 * @brief 字符设备操作操作集
 * 
 */
static struct file_operations led_fops= {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .read = led_read,
    .write = led_write,
};

/**
 * @brief 初始化
 * 
 * @return int 
 */
static int __init led_init(void)
{   
    int ret = 0;
    int val = 0;
    /*led初始化*/
    //获取地址映射
    IMX6UL_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
    IMX6UL_SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    IMX6UL_SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    IMX6UL_GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    IMX6UL_GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
    printk("ioremap finished!\r\n");

    //时钟初始化
    
    val = readl(IMX6UL_CCM_CCGR1);  //读取CCM_CCGR1的值
    val &= ~(3<<26);                //清除bit26、27
    val |= (3<<26);                //bit26、27置1
    writel(val, IMX6UL_CCM_CCGR1);
    printk("CCM init finished!\r\n");

    /*GPIO初始化*/
    writel(0x5, IMX6UL_SW_MUX_GPIO1_IO03);
    writel(0x10B0, IMX6UL_SW_PAD_GPIO1_IO03);
    printk("GPIO SW init finished!\r\n");

    val = readl(IMX6UL_GPIO1_GDIR);
    val |= 1<<3;                        //bit3=1,设置为输出
    writel(val, IMX6UL_GPIO1_GDIR);
    printk("GPIO GDIR init finished!\r\n");

    val = readl(IMX6UL_GPIO1_DR);
    val &= ~(1<<3);
    writel(val,IMX6UL_GPIO1_DR);

    printk("device init!\r\n");
  
    //字符设备注册
    ret = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);
    if(ret < 0 ){
        printk("device init failed\r\n");
        return -EIO;
    }
    return 0;
}

/**
 * @brief 卸载
 * 
 */
static void __exit led_exit(void)
{   
    int val = 0;
    //关闭led
    val = readl(IMX6UL_GPIO1_DR);
    val |= (1<<3);
    writel(val ,IMX6UL_GPIO1_DR);


    //取消地址映射
    iounmap(IMX6UL_CCM_CCGR1);
    iounmap(IMX6UL_SW_MUX_GPIO1_IO03);
    iounmap(IMX6UL_SW_PAD_GPIO1_IO03);
    iounmap(IMX6UL_GPIO1_DR);
    iounmap(IMX6UL_GPIO1_GDIR);

    //字符设备注销
    unregister_chrdev(DEV_MAJOR,DEV_NAME);
    printk("device exit\r\n");

}

module_init(led_init);      //模块加载
module_exit(led_exit);      //模块卸载

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZeqiZ");

在初始化的时候,每个步骤后面都加了个调试信息通过printk打印出来,在卸载的时候,一定要注意先使用虚拟地址将要修改的寄存器修改完成后再释放地址,否则肯定就错了! 

记得修改Makefile里对应的文件名,make以后将ko文件复制到根目录系统下,先通过depmod分析新添加的模块文件,然后可以直接使用modprobe挂载模块,led点亮,再用rmmod命令卸载模块,led熄灭,很简单!