当前位置: 首页 > news >正文

Linux驱动学习之第二个驱动程序(LED点亮关闭驱动程序-分层设计思想,使其能适应不同的板子-驱动程序模块为多个源文件怎么写Makefile)

目录

  • 看这篇博文前请先掌握下面这些博文中的知识
  • 需要的PDF资料
  • 完整源代码
    • board_fire_imx6ull-pro.c中的代码
    • leddrv.c中的代码
    • ledtest.c中的代码
  • 程序设计思想和文件结构
  • 实现分层思想的具体方法
    • 概述
    • 具体实现分析
      • 定义结构体`led_operations`用来集合各个单板硬件层面操作LED的函数
      • 定义具体的某个单板的操作LED的各个具体函数,并作为结构体`led_operations`的一个实例的成员函数
      • 定义函数`get_board_led_opr`返回结构体`led_operations`的一个实例的指针
      • 上层文件(leddrv.c)的模块初始化函数中调用函数`get_board_led_opr`获得单板的底层操作实例
      • 在驱动程序中去调用底层函数对硬件的操作
  • 本篇博文略去对驱动程序代码的分析,重点分析底层对硬件操作的代码
  • 驱动程序代码虽然略去分析,但是模块初始化函数中注册驱动程序的方法和之前的有所不同【函数register_chrdev()的介绍】
    • register_chrdev函数原型
    • 参数详解
    • 返回值
    • 示例代码
    • 卸载驱动时对应的函数
    • 小结
  • 在硬件层面操作的代码的分析(`board_fire_imx6ull-pro.c`中的代码)
    • 定义用于存储寄存器映射地址值的指针
      • 关键字`volatile`的含义
    • LED控制初始化操作函数`board_demo_led_init`分析
      • 源代码
      • `ioremap`函数分析
      • GPIO操作的第01步:使能对应GPIO口的时钟(CCGR寄存器)
      • GPIO操作的第02步:设置对应的GPIO口引脚为GPIO功能【】,而不是别的复用功能
      • GPIO操作的第03步:设置对应的GPIO口为输出引脚(IO口中的Output)【GDIR寄存器】
      • GPIO操作的第04步:找到对应的GPIO口的数据寄存器并映射到虚拟地址空间【(GPIOx_DR寄存器】
    • LED控制函数`board_demo_led_ctl`分析
    • LED控制的关闭函数(释放内存映射)
  • Makfile文件的编写
  • 交叉编译
  • 上板测试
  • 附工程压缩文件和PDF文档

看这篇博文前请先掌握下面这些博文中的知识

https://blog.csdn.net/wenhao_ir/article/details/144888989
https://blog.csdn.net/wenhao_ir/article/details/144901797
https://blog.csdn.net/wenhao_ir/article/details/144881830

需要的PDF资料

开发板的原理图文件:
https://pan.baidu.com/s/1CEwPbcNUIicA1HVGwG6e5A?pwd=m9wb
IMX6ULL-CPU芯片的操作手册:
https://pan.baidu.com/s/1hZWmgmvYAA9mvzAHsPiw4g?pwd=eiuh

完整源代码

board_fire_imx6ull-pro.c中的代码

#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 <asm/io.h>#include "led_opr.h"static volatile unsigned int *CCM_CCGR1                              ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR                             ;
static volatile unsigned int *GPIO5_DR                               ;static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5的时钟信号* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);  //5的二进制表示为0101*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}static struct led_operations board_demo_led_opr = {.num  = 1,.init = board_demo_led_init,.ctl  = board_demo_led_ctl,.close = board_demo_led_close,
};struct led_operations *get_board_led_opr(void)
{return &board_demo_led_opr;
}

leddrv.c中的代码

#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 "led_opr.h"/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;#define MIN(a, b) (a < b ? a : b)/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;struct inode *inode = file_inode(file);int minor = iminor(inode);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据次设备号和status控制LED */p_led_opr->ctl(minor, status);return 1;
}static int led_drv_open (struct inode *node, struct file *file)
{int minor = iminor(node);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据次设备号初始化LED */p_led_opr->init(minor);return 0;
}static int led_drv_close (struct inode *node, struct file *file)
{int minor = iminor(node);int err;/* 根据次设备号解除相关LED的寄存器映射关系 */err = p_led_opr->close(minor);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);printk(KERN_INFO "LED driver: resources released.\n");return 0;
}/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {.owner	 = THIS_MODULE,.open    = led_drv_open,.read    = led_drv_read,.write   = led_drv_write,.release = led_drv_close,
};/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{int err;int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "imx6ull_led", &led_drv);  /* /dev/led */led_class = class_create(THIS_MODULE, "imx6ull_led_class");err = PTR_ERR(led_class);if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");return -1;}p_led_opr = get_board_led_opr();for (i = 0; i < p_led_opr->num; i++)device_create(led_class, NULL, MKDEV(major, i), NULL, "imx6ull_led%d", i); /* /dev/imx6ull_led0,1,... */return 0;
}/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);for (i = 0; i < p_led_opr->num; i++)device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */class_destroy(led_class);unregister_chrdev(major, "imx6ull_led");
}/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");

ledtest.c中的代码


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>/** ./ledtest /dev/100ask_led0 on* ./ledtest /dev/100ask_led0 off*/
int main(int argc, char **argv)
{int fd;char status;/* 1. 判断参数 */if (argc != 3) {printf("Usage: %s <dev> <on | off>\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open(argv[1], O_RDWR);if (fd == -1){printf("can not open file %s\n", argv[1]);return -1;}/* 3. 写文件 */if (0 == strcmp(argv[2], "on")){status = 1;write(fd, &status, 1);}else{status = 0;write(fd, &status, 1);}close(fd);return 0;
}

程序设计思想和文件结构

希望写出的程序能给分层,即把真正对各单板的LED操作的实现最为最底层,然后有个中间层,是驱动程充中各单板实现LED操作都需要的代码,这样将来有个新板子只需要修改下最底层的代码就能用了。比如下面的工程文件结构:
在这里插入图片描述
以board为前缀的文件都是每个单板具体的对LED操作的底层代码,然后中间层代码放在文件leddrv.c中。
IMX6ULL开发板的底层代码在文件board_fire_imx6ull-pro.c中。
测试程序代码放在ledtest.c中。

实现分层思想的具体方法

概述

每个单板都有各自的LED控制的在硬件层面上的初始化函数、控制函数、释放(关闭)函数,我们只需要把这些函数集合到我们自定义的名为led_operations结构体中,然后在驱动程序的文件操作结构体(file_operations 结构体)中的open、read、write、release函数中去调用led_operations结构体实例中对应的函数即可,用led_operations结构体的一个实例就相当于是一个单板。

具体实现分析

定义结构体led_operations用来集合各个单板硬件层面操作LED的函数

首先在led_opr.h中定义一个名为led_operations结构体,用于存储各个单板具体的LED的硬件层面的操作函数,代码如下:

struct led_operations {int num; /* 表示有几个LED,实际上也是LED的 */int (*init) (int which); /* 初始化LED, which-哪个LED */int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */int (*close) (int which); /* 主要是对映射的寄存器作解除映射操作 */
};

定义具体的某个单板的操作LED的各个具体函数,并作为结构体led_operations的一个实例的成员函数

然后在文件board_fire_imx6ull-pro.c中定义具体的IMX6ULL开发板操作LED的硬件层面的函数,然后把这些函数作为led_operations结构体的一个实例的成员。相关代码如下:

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}static struct led_operations board_demo_led_opr = {.num  = 1,.init = board_demo_led_init,.ctl  = board_demo_led_ctl,.close = board_demo_led_close,
};

定义函数get_board_led_opr返回结构体led_operations的一个实例的指针

最后,提供一个实例化结构体board_demo_led_opr的函数供上层代码调用,从而获得一个单板LED操作实例:

struct led_operations *get_board_led_opr(void)
{return &board_demo_led_opr;
}

注意:这个函数需要在头文件中声明,因为它是在文件leddrv.c中被调用的。
在这里插入图片描述

上层文件(leddrv.c)的模块初始化函数中调用函数get_board_led_opr获得单板的底层操作实例

在上层文件(leddrv.c)的模块初始化函数中调用函数get_board_led_opr获得单板的底层操作的led_operations结构体的实例的指针,比如下面这句代码:

p_led_opr = get_board_led_opr();

在这里插入图片描述

在驱动程序中去调用底层函数对硬件的操作

在驱动程序的文件操作结构体(struct file_operations led_drv)的成员函数中去调用底层的对硬件的操作,说句题外话对底层硬件的操作其实就是对寄存器的操作,比如下面的代码:

static int led_drv_open (struct inode *node, struct file *file)
{int minor = iminor(node);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据次设备号初始化LED */p_led_opr->init(minor);return 0;
}

在这里插入图片描述

static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;struct inode *inode = file_inode(file);int minor = iminor(inode);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据次设备号和status控制LED */p_led_opr->ctl(minor, status);return 1;
}

在这里插入图片描述

static int led_drv_close (struct inode *node, struct file *file)
{int minor = iminor(node);int err;/* 根据次设备号解除相关LED的寄存器映射关系 */err = p_led_opr->close(minor);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);printk(KERN_INFO "LED driver: resources released.\n");return 0;
}

在这里插入图片描述

本篇博文略去对驱动程序代码的分析,重点分析底层对硬件操作的代码

本篇博文略去对驱动程序代码的分析,因为在前面的三篇博文中我已经把Linux驱动程序的框架和结构搞得比较清楚了,具体是下面三篇博文。
https://blog.csdn.net/wenhao_ir/article/details/144888989
https://blog.csdn.net/wenhao_ir/article/details/144901797
https://blog.csdn.net/wenhao_ir/article/details/144881830
所以,本文重点去关注硬件层面的实现,也就是文件board_fire_imx6ull-pro.c中的代码。

驱动程序代码虽然略去分析,但是模块初始化函数中注册驱动程序的方法和之前的有所不同【函数register_chrdev()的介绍】

led_init函数的代码如下:

static int __init led_init(void)
{int err;int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "imx6ull_led", &led_drv);  /* /dev/led */led_class = class_create(THIS_MODULE, "imx6ull_led_class");err = PTR_ERR(led_class);if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");return -1;}p_led_opr = get_board_led_opr();for (i = 0; i < p_led_opr->num; i++)device_create(led_class, NULL, MKDEV(major, i), NULL, "imx6ull_led%d", i); /* /dev/imx6ull_led0,1,... */return 0;
}

从中我们可以看出,在整个设备的注册过程中,并没有用到博文 https://blog.csdn.net/wenhao_ir/article/details/144888989中提到的函数alloc_chrdev_regioncdev_initcdev_add,也没有像博文https://blog.csdn.net/wenhao_ir/article/details/144881830那样去使用函数alloc_chrdev_regioncdev_initcdev_add,而是使用了快速注册函数register_chrdev完成这几个函数的功能,下面是其详细介绍:

register_chrdev 是 Linux 驱动程序开发中用于注册字符设备的函数。它的主要作用是向内核注册一个字符设备,从而允许用户空间程序通过设备文件与驱动程序交互。

register_chrdev函数原型

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

参数详解

  1. major
    指定设备的主设备号。如果为 0,内核会自动分配一个主设备号,并返回分配的值。主设备号用于区分设备的类别。

  2. name
    设备的名称,用于标识设备。可以通过 /proc/devices 文件查看注册的字符设备名称和对应的主设备号。

  3. fops
    指向 struct file_operations 的指针,用于定义该字符设备支持的操作集。例如,openreadwrite 等函数。

返回值

  • 如果成功,返回分配的主设备号(major 参数)。
  • 如果失败,返回一个负数错误码。

示例代码

int major;
struct file_operations led_drv = {.owner = THIS_MODULE,.open = led_open,.release = led_release,.write = led_write,
};major = register_chrdev(0, "imx6ull_led", &led_drv);
if (major < 0) {printk(KERN_ERR "Failed to register device: %d\n", major);return major;
}
printk(KERN_INFO "Registered device with major number %d\n", major);

解释

  1. register_chrdev(0, "imx6ull_led", &led_drv)

    • 0: 让内核自动分配一个主设备号。
    • "imx6ull_led": 驱动程序名称(主设备号用来找到驱动程序),便于识别。
    • &led_drv: 定义字符设备操作的函数指针集合。
  2. struct file_operations led_drv
    包含设备的操作方法,例如 openreleasewrite。这些方法在应用程序访问设备时被调用。

  3. 返回的主设备号
    如果注册成功,major 变量会保存分配的主设备号,可以用于创建设备节点。例如:

    mknod /dev/imx6ull_led c <major> 0
    
  4. 错误检查
    如果注册失败,打印错误信息并返回错误码。


卸载驱动时对应的函数

在卸载驱动时,需要调用 unregister_chrdev 注销设备:

unregister_chrdev(major, "imx6ull_led");

小结

register_chrdev 是字符设备驱动程序中快速注册设备的方法。尽管较为简单,但在复杂场景中可能不够灵活,因此通常在学习或简单驱动开发中使用,推荐更复杂的场景使用 cdevalloc_chrdev_region

在硬件层面操作的代码的分析(board_fire_imx6ull-pro.c中的代码)

定义用于存储寄存器映射地址值的指针

static volatile unsigned int *CCM_CCGR1                              ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR                             ;
static volatile unsigned int *GPIO5_DR                               ;

关键字volatile的含义

这些定义里面首先要理解关键字volatile的含义,详细解释请参见我的另一篇博文:
https://blog.csdn.net/wenhao_ir/article/details/144983848

LED控制初始化操作函数board_demo_led_init分析

源代码

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}

ioremap函数分析

相关代码如下:

CCM_CCGR1                               = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);
GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);

这些代码显然就是把寄存器的物理地址映射到Linux的虚拟地址中,在前面的博文
IMX6ULL开发板基础实验:Framebuffer驱动程序的简单应用实例代码详细分析中,用到了函数mmap,也是把地址映射到虚拟地址,不同的是mmap映射的是设备的内存,而这里咱们需要映射的是CPU的物理地址。
对于函数mmap的讲解详见我的另一篇博文
嵌入式Linux开发中的内存映射函数mmap详解?什么叫内存映射?什么叫对设备的显式和隐式读写操作?什么叫程序的虚拟内存?

对这里遇到的函数 ioremap()的详细分析,见博文 https://blog.csdn.net/wenhao_ir/article/details/144985338

GPIO操作的第01步:使能对应GPIO口的时钟(CCGR寄存器)

GPIO操作的第01步例是使能对应GPIO口的时钟。

相关的控制模块是CCM(Clock Controller Module-时钟控制模块) 。

首先我们要知道我们要操作哪个GPIO口。

从下面这幅开发板的原理图截图我们可以知道:
在这里插入图片描述
我们需要操作的对象是第5组GPIO口的第3个GPIO口,即GPIO5_IO03,别人的经验告诉我们GPIO的时钟信号由CCM(Clock Controller Module)寄存器控制,所以去IMX6ULL的的操作手册中去查找相关的CCM(Clock Controller Module)寄存器,不妨直接在手册中搜索“gpio5”,发现有下面的内容:
在这里插入图片描述
点击一看,发现正是CCM模块中的内容:
在这里插入图片描述
上面红框中的内容显示GPIO5的时钟控制为CCGR1[CG15],即CCGR1寄存器的CG15位,然后我们就去查CCGR1寄存器,先搜索PDF左边的书签:
在这里插入图片描述
通过PDF的书签搜索到了,点击查看:
在这里插入图片描述
从这幅截图中我们可以看出,CG15位是指CCGR1寄存器的第30位和31位,其初始值为11,但在Description描述中却为Reserved,Reserved的意思是保留未用的意思,那这样看来,这个操作手册有点前后矛盾了,可能是文档的疏漏吧,别人的经验告诉我们,其实这两位就是控制GPIO5的时钟的,具体意义如下:

  • 00: 时钟关闭。
  • 01: 时钟仅在硬件需要时使能。
  • 10: 时钟保留,通常不用。
  • 11: 时钟始终使能。

这里它的Reset值为11,说明它默认就是使能的,所以其实这里不需要设置,不过为了程序的健壮性和兼容性,有可能这个系列别的CPU需要设置嘛,所以不忍这里也设置一下其值。

从上面这幅截图我们还可以看出,CCGR1的物理地址为20C_406Ch,即20C406C,这样我们就获得了其物理地址,所以便可以写出下面的代码了:

CCM_CCGR1 = ioremap(0x20C406C, 4);/* GPIO5_IO03 */
/* a. 使能GPIO5的时钟信号* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/
*CCM_CCGR1 |= (3<<30);

注意:*CCM_CCGR1 |= (3<<30);只设置了31位和32位为1,寄存器别的位的值没有变 ,这个或运算表达式应该是比较好懂的。

GPIO操作的第02步:设置对应的GPIO口引脚为GPIO功能【】,而不是别的复用功能

在这里,我们需要设置GPIO5_IO03,即第5组GPIO口的第3个引脚为输出功能。

别人的经验告诉我们,这涉及到 IOMUXC(I/O Multiplex Controller),所以去书签中搜索IOMUXC
在这里插入图片描述
可见是第32章的内容,所以就浏览这一章的内容,然后浏览起来才一个麻烦,不妨借助搜索引擎或chatgpt,找到最有可能的寄存器,然后去操作手册中去确认。

问chatgpt:
我想去控制IMX6ULL的GPIO5的第3个引脚为GPIO功能,该设置IOMUXC中的哪个寄存器?

回答:
IMX6ULL 中,控制 GPIO5 的第 3 个引脚(GPIO5_IO03) 为 GPIO 功能时,需要设置 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 寄存器。


寄存器名称与地址

  • 寄存器名称IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
  • 寄存器地址0x2290014

寄存器功能说明
该寄存器用于控制引脚 SNVS_TAMPER3(复用为 GPIO5_IO03)的复用功能。寄存器的低 4 位(bit[3:0])决定该引脚的功能。


复用模式设置
为了将引脚配置为 GPIO 功能,需要将低 4 位设置为 0101(ALT5 模式)

  • 低 4 位值说明
    • 0000(ALT0):复用为主功能(例如某外设)。
    • 0101(ALT5):复用为 GPIO 功能。

chatgpt说是寄存器IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3的低 4 位(bit 3:0)决定该引脚的功能,并且说这个寄存器的物理地址为0x2290014

我们可以搜索TAMPER3,也可以按操作手册的格式搜索地址229_0014h,去找到操作手册中对应的内容(从下面的截图来看,还是搜索地址是最精确的):
在这里插入图片描述
对应的内容为:
在这里插入图片描述
注意,这个寄存器的第3位SION的意义如下:
在 IMX6ULL 的 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 寄存器中,第3 位(SION)用于控制引脚的输入路径是否始终有效。
以下是详细说明:


SION 位的作用

  1. 名称

    • SION: Software Input On
  2. 位置

    • 寄存器的第 3 位(从 0 开始计数)。
  3. 功能描述

    • 0:正常模式(默认值)。
      • 引脚的输入路径根据复用功能(MUX_MODE)的设置和引脚状态进行控制。
    • 1:强制输入模式。
      • 无论复用功能(MUX_MODE)的设置如何,始终保持引脚的输入路径打开。
  4. 影响

    • 在复用为 GPIO 功能时,如果引脚仅用于输出,可以关闭输入路径(即 SION = 0),以节省功耗。
    • 如果引脚需要作为输入或具有输入功能(如读取状态、实现中断等),需要打开输入路径(即 SION = 1)。

SION 位的使用场景

  • GPIO 作为输入引脚
    如果引脚配置为 GPIO 输入,或者需要读取引脚的状态,必须将 SION 设置为 1,确保输入路径打开。

  • GPIO 作为输出引脚
    如果引脚仅用作 GPIO 输出,可以将 SION 设置为 0,避免不必要的输入路径开销。

  • 复用其他功能时

    • 某些复用功能(如 UART、I2C 等)可能需要强制打开输入路径,即设置 SION = 1。
    • 如果复用功能本身不需要输入,通常可以关闭 SION(设置为 0)。

从上面的截图中可以看出,chatgpt是对的,所以可以写出下面的代码:

IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/
val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
val &= ~(0xf);
val |= (5); //5的二进制表示为0101
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;

其中val &= ~(0xf);的作用是清零低 4 位,防止之前的值对当前配置产生干扰。
其清零低4位的详细过程如下

  1. 位操作分析

    • 0xf 的二进制表示是 0000 1111,表示低 4 位全为 1。
    • ~(0xf) 进行按位取反后,结果是 1111 0000,即低 4 位全为 0,其他位全为 1。
  2. 与运算(&

    • val &= ~(0xf); 等价于 val = val & ~(0xf);
    • 这会将 val 的低 4 位强制清零,同时保留其他位的原始值。
  3. 效果
    假设 val 的原始值为 0b1011_1111,执行 val &= ~(0xf); 后:

    原始值: 1011 1111
    ~(0xf): 1111 0000
    按位与: 1011 0000
    

    结果是 0b1011_0000,即低 4 位清零,其他位保持不变。

GPIO操作的第03步:设置对应的GPIO口为输出引脚(IO口中的Output)【GDIR寄存器】

还是按第02步中的方法,直接先问chatgpt,然后再去手册中查阅。

问:已经设置好了IMX6ULL的GPIO5的第3个引脚为GPIO功能,现在想把它设为output,该设置哪个寄存器,怎么设置?

IMX6ULL 中,将 GPIO5 的第 3 个引脚(GPIO5_IO03) 配置为 输出,需要设置 GPIO5_GDIR(GPIO Direction Register) 寄存器。


寄存器名称与地址

  • 寄存器名称GPIO5_GDIR
  • 寄存器地址0x020AC004(GPIO5 基地址 0x020AC000 + 偏移 0x4

寄存器功能说明

  1. GDIR(GPIO Direction Register)

    • 每个 GPIO 引脚都有一个对应的方向位。
    • 位值:
      • 0:输入(Input)。
      • 1:输出(Output)。
  2. GPIO5_IO03 对应的位

    • GPIO5 的第 3 个引脚对应 GDIR 寄存器的 第 3 位(bit[3])。

所以我们去操作手册中搜索20A_C004h,结果没有搜索到结果…

那就去搜索GPIO5_GDIR,有一个结果:
在这里插入图片描述
在这里插入图片描述
从上需截图可以看出,其地址为20AC004,所以以后我们第一次搜索地址的时候如果找不到结果,不妨去掉最后的h再试
最后面的蓝色的字表明了详细介绍是在多少页,我们可以看到是在1359页。
在这里插入图片描述
这个介绍是个通用的介绍,从中我们可以看出你要操作某个组的GPIO的第几个GPIO口,就设置哪个位,比如我要操作GPIO5的第3位,我就去设置GPIO5_GDIR的第3位就行了,当值为0时是输入口,当值为1时为输出口。
据此,可以写出代码如下:

GPIO5_GDIR = ioremap(0x020AC000 + 0x4, 4);/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/
*GPIO5_GDIR |= (1<<3);

GPIO操作的第04步:找到对应的GPIO口的数据寄存器并映射到虚拟地址空间【(GPIOx_DR寄存器】

先要找到目标寄存器,还是先问chatgpt,然后去应证。

问:现在我把GPIO5_IO03已经设为输出引脚了,我到哪个寄存器中去设置它的输出值?

在 IMX6ULL 中,GPIO5_IO03 配置为输出引脚后,设置其输出值需要操作 GPIO5 的数据寄存器 GPIO5_DR(GPIO Data Register)


GPIO5_DR 寄存器

  1. 寄存器名称GPIO5_DR
  2. 寄存器地址0x020AC000(GPIO5 基地址 0x020AC000)。
  3. 功能描述
    • 用于设置 GPIO 引脚的输出值。
    • 每个 GPIO 引脚对应寄存器中的一位:
      • 0:引脚输出低电平。
      • 1:引脚输出高电平。
    • 对于 GPIO5_IO03,对应 GPIO5_DR 的第 3 位(bit[3])。

设置 GPIO 输出值的方法

  1. 将引脚输出低电平

    • 清除 GPIO5_DR 的第 3 位(bit[3])。
      *GPIO5_DR &= ~(1 << 3);
      
  2. 将引脚输出高电平

    • 设置 GPIO5_DR 的第 3 位(bit[3])。
      *GPIO5_DR |= (1 << 3);
      

完整代码示例
以下是将 GPIO5_IO03 输出高电平或低电平的代码:

#define GPIO5_BASE  (0x020AC000)   // GPIO5 基地址
#define GPIO5_DR    (GPIO5_BASE + 0x0) // GPIO5_DR 寄存器地址void set_gpio5_io03_output_value(int value)
{volatile unsigned int *dr;unsigned int val;// 映射 GPIO5_DR 寄存器dr = ioremap(GPIO5_DR, 4);if (!dr) {printk("Failed to ioremap GPIO5_DR register\n");return;}// 读取当前 GPIO5_DR 的值val = readl(dr);if (value) {// 设置 GPIO5_IO03 输出高电平 (bit[3] = 1)val |= (1 << 3);} else {// 设置 GPIO5_IO03 输出低电平 (bit[3] = 0)val &= ~(1 << 3);}// 写回 GPIO5_DR 寄存器writel(val, dr);// 解除映射iounmap(dr);
}

注意事项

  1. 引脚功能

    • 确保 GPIO5_IO03 已经通过 IOMUXC 配置为 GPIO 功能。
    • 确保 GPIO5_IO03 的方向已设置为输出(通过 GPIO5_GDIR 配置)。
  2. 输出控制的实际效果

    • 输出低电平:物理引脚拉低,输出电压为 0V
    • 输出高电平:物理引脚拉高,输出电压为供电电压(通常为 3.3V1.8V,具体取决于硬件设计)。

接下来,我们去手册中应证一下,看下chatgpt说的对不对,搜索020A_C000
只找到下面这个结果:
在这里插入图片描述
看不出什么,只知道这个地址与GPIO5有关…
再继续搜索书签:GPIO5_DR 也没有结果
全文搜索 GPIO5_DR,有了:
在这里插入图片描述
可见,其物理地址为 20A_C000 详细介绍在1358页的28.5.1节:
在这里插入图片描述
可见你要使GPIO5_IO03输出0和1,只需要写GPIO5_DR的第3位就行了。
所以可以写出下面的代码:

 GPIO5_DR = ioremap(0x020AC000 + 0, 4);/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/
*GPIO5_GDIR |= (1<<3);

LED控制函数board_demo_led_ctl分析

源代码如下:

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}

这个代码就没啥好说了,只要清楚了LED控制的初始化函数board_demo_led_init,这里理解起来就很简单了。

LED控制的关闭函数(释放内存映射)

static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}

这段代码也没啥好说的,只是在释放前先判断下对应的指针是否为NULL,如果不是NULL,再去执行释放操作。这段代码是我自己写的,不是教程中提供的。

Makfile文件的编写

这个Makfile文件和之前的驱动程序的Makefile文件相比(https://blog.csdn.net/wenhao_ir/article/details/144881830),不同的是这里的驱动程序为两个C文件,具体是board_fire_imx6ull-pro.cleddrv.c,所以难度相当于增加了,不过大部分Makefile语句还是和(https://blog.csdn.net/wenhao_ir/article/details/144881830)中的代码相同,所以要了解这里的这个Makefile文件,可以先看下(https://blog.csdn.net/wenhao_ir/article/details/144881830)中对Makefile文件的解读。具体来说,你需要打开链接 https://blog.csdn.net/wenhao_ir/article/details/144958830 查看。

具体的Makefile文件的内容如下:

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88all:make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o led_driver_test ledtest.c clean:make -C $(KERN_DIR) M=`pwd` cleanrm -rf modules.orderrm -f ledtest# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o# leddrv.c board_demo.c 编译成 led_driver.ko
led_driver-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= led_driver.o

其实关键的就是最后两行语句:

# leddrv.c board_demo.c 编译成 led_driver.ko
led_driver-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= led_driver.o

这其实没啥好说的,照这个格式去填写就行了,这里由Linux源码中的Makefile定义的多文件模块构建规则。再比如,如果我要用 leddrv.oboard_fire_imx6ull-pro.o生成模块文件suwenhao.ko,那我就像下面这样写:

suwenhao-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= suwenhao.o

交叉编译

代码复制到Ubuntu中…
在这里插入图片描述

make 

在这里插入图片描述
在这里插入图片描述
复制上面两个红框中的文件到NFS文件系统中以备用…

在这里插入图片描述

上板测试

cd /mnt/led_driver

加载驱动:

insmod led_driver.ko

在这里插入图片描述
运行测试程序:

首先打开LED

./led_driver_test /dev/imx6ull_led0 on

在这里插入图片描述
上面打印输出的语句:

[  294.737362] LED driver: resources released.

是下面这句代码产生的:
在这里插入图片描述
当主函数执行到close(fd);时例会调用函数led_drv_close

发现板子上编号为2的LED打开了:

在这里插入图片描述
然后关闭LED:

./led_driver_test /dev/imx6ull_led0 off

在这里插入图片描述
发现灯关了~

卸载驱动程序模块:

卸载前先看下系统中当前存在的驱动程序模块:

cat /proc/devices

在这里插入图片描述
然后再执行卸载驱动模块的命令:

rmmod led_driver

再查看系统中当前存在的驱动程序模块:

cat /proc/devices

在这里插入图片描述
发现正常卸载了…

测试结束…一切OK~

附工程压缩文件和PDF文档

工程压缩文件:
https://pan.baidu.com/s/1LzdbQ1LDcAxbv2T7akvnxQ?pwd=ixqv

开发板的原理图文件:
https://pan.baidu.com/s/1CEwPbcNUIicA1HVGwG6e5A?pwd=m9wb
IMX6ULL-CPU芯片的操作手册:
https://pan.baidu.com/s/1hZWmgmvYAA9mvzAHsPiw4g?pwd=eiuh


http://www.mrgr.cn/news/83098.html

相关文章:

  • 创建型模式5.单例模式
  • 单片机-外部中断
  • ESP32 IDF VScode出现头文件“无法打开 源 文件 ”,并有红色下划线警告
  • JVM之Java内存模型
  • 在不到 5 分钟的时间内将威胁情报 PDF 添加为 AI 助手的自定义知识
  • 基于ROS先验地图的机器人自主定位与导航SLAM
  • 【深度学习】布匹寻边:抓边误差小于5px【附完整链接】
  • 【vue3封装element-plus的反馈组件el-drawer、el-dialog】
  • docker搭建atlassian-confluence:7.2.0
  • XS5037C一款应用于专业安防摄像机的图像信号处理芯片,支持MIPI和 DVP 接口,内置高性能ISP处理器,支持3D降噪和数字宽动态
  • Onedrive精神分裂怎么办(有变更却不同步)
  • 【Redis源码】 RedisObject结构体
  • 单片机-定时器中断
  • formik 的使用
  • 202305 青少年软件编程等级考试C/C++ 一级真题答案及解析(电子学会)
  • ESP32编译和双OTA分区问题
  • Ubuntu更改内核
  • 使用LinkedList手撕图的邻接表
  • eNSP之家----ACL实验入门实例详解(Access Control List访问控制列表)(重要重要重要的事说三遍)
  • (五)WebGL中vertexAttribPointer方法的使用详解
  • Linux系统中解决端口占用问题
  • STM32内置Flash
  • Vue3组件通讯——自定义事件(子->父)
  • C++和Python中负数取余结果的区别
  • python中的列表推导式详解
  • Django学习笔记之数据库(一)