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_region
、cdev_init
、cdev_add
,也没有像博文https://blog.csdn.net/wenhao_ir/article/details/144881830那样去使用函数alloc_chrdev_region
、cdev_init
、cdev_add
,而是使用了快速注册函数register_chrdev
完成这几个函数的功能,下面是其详细介绍:
register_chrdev
是 Linux 驱动程序开发中用于注册字符设备的函数。它的主要作用是向内核注册一个字符设备,从而允许用户空间程序通过设备文件与驱动程序交互。
register_chrdev函数原型
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
参数详解
-
major
指定设备的主设备号。如果为0
,内核会自动分配一个主设备号,并返回分配的值。主设备号用于区分设备的类别。 -
name
设备的名称,用于标识设备。可以通过/proc/devices
文件查看注册的字符设备名称和对应的主设备号。 -
fops
指向struct file_operations
的指针,用于定义该字符设备支持的操作集。例如,open
、read
、write
等函数。
返回值
- 如果成功,返回分配的主设备号(
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);
解释
-
register_chrdev(0, "imx6ull_led", &led_drv)
0
: 让内核自动分配一个主设备号。"imx6ull_led"
: 驱动程序名称(主设备号用来找到驱动程序),便于识别。&led_drv
: 定义字符设备操作的函数指针集合。
-
struct file_operations led_drv
包含设备的操作方法,例如open
、release
和write
。这些方法在应用程序访问设备时被调用。 -
返回的主设备号
如果注册成功,major
变量会保存分配的主设备号,可以用于创建设备节点。例如:mknod /dev/imx6ull_led c <major> 0
-
错误检查
如果注册失败,打印错误信息并返回错误码。
卸载驱动时对应的函数
在卸载驱动时,需要调用 unregister_chrdev
注销设备:
unregister_chrdev(major, "imx6ull_led");
小结
register_chrdev
是字符设备驱动程序中快速注册设备的方法。尽管较为简单,但在复杂场景中可能不够灵活,因此通常在学习或简单驱动开发中使用,推荐更复杂的场景使用 cdev
和 alloc_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 位的作用
-
名称:
- SION: Software Input On
-
位置:
- 寄存器的第 3 位(从 0 开始计数)。
-
功能描述:
- 0:正常模式(默认值)。
- 引脚的输入路径根据复用功能(MUX_MODE)的设置和引脚状态进行控制。
- 1:强制输入模式。
- 无论复用功能(MUX_MODE)的设置如何,始终保持引脚的输入路径打开。
- 0:正常模式(默认值)。
-
影响:
- 在复用为 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位的详细过程如下
-
位操作分析:
0xf
的二进制表示是0000 1111
,表示低 4 位全为 1。~(0xf)
进行按位取反后,结果是1111 0000
,即低 4 位全为 0,其他位全为 1。
-
与运算(
&
):val &= ~(0xf);
等价于val = val & ~(0xf);
- 这会将
val
的低 4 位强制清零,同时保留其他位的原始值。
-
效果:
假设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
)
寄存器功能说明
-
GDIR(GPIO Direction Register):
- 每个 GPIO 引脚都有一个对应的方向位。
- 位值:
- 0:输入(Input)。
- 1:输出(Output)。
-
GPIO5_IO03 对应的位:
- GPIO5 的第 3 个引脚对应
GDIR
寄存器的 第 3 位(bit[3])。
- GPIO5 的第 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 寄存器
- 寄存器名称:
GPIO5_DR
- 寄存器地址:
0x020AC000
(GPIO5 基地址0x020AC000
)。 - 功能描述:
- 用于设置 GPIO 引脚的输出值。
- 每个 GPIO 引脚对应寄存器中的一位:
- 0:引脚输出低电平。
- 1:引脚输出高电平。
- 对于 GPIO5_IO03,对应
GPIO5_DR
的第 3 位(bit[3])。
设置 GPIO 输出值的方法
-
将引脚输出低电平:
- 清除
GPIO5_DR
的第 3 位(bit[3])。*GPIO5_DR &= ~(1 << 3);
- 清除
-
将引脚输出高电平:
- 设置
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);
}
注意事项
-
引脚功能:
- 确保 GPIO5_IO03 已经通过 IOMUXC 配置为 GPIO 功能。
- 确保 GPIO5_IO03 的方向已设置为输出(通过
GPIO5_GDIR
配置)。
-
输出控制的实际效果:
- 输出低电平:物理引脚拉低,输出电压为
0V
。 - 输出高电平:物理引脚拉高,输出电压为供电电压(通常为
3.3V
或1.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.c
和leddrv.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.o
和board_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