程序框架说明(和之前的LED驱动进行对比)
这个程序的框架与之前学习的第二个驱动程序(控制LED)的框架基本一致,第二个驱动程序的链接如下:
https://blog.csdn.net/wenhao_ir/article/details/144973219
所以如果前两这个LED驱动程序的框架掌握得很清楚了,这里驱动程序的框架就没啥好说的了。
不过二者还是有点不同,具体的不同如下:
①在控制LED的驱动程序中,下层的代码虽然被独立为一个文件,但并没有作为一个单独的模块,即没有初始函数和退出函数,而是和驱动的上层代码文件一起被编译成一个模块。本文中的下层驱动代码和上层驱动代码就是各自形成一个模块,虽然我觉得这样并不方便(相当于加载一个驱动还得加载两个模块,增加了操作的复杂性),但是既然官方提供的代码框架是这样,我就不改了,以后我就根据实际需要去选择是用LED驱动中的那种只有一个模块的结构还是这篇博文中被分别编译成两个模块的结构吧。
②在控制LED的驱动程序中,设备文件的建立是在驱动程序框架的上层文件的模块初始化函数中完成的,关键代码如下:
函数get_board_led_opr
是在底层文件中定义的函数,相关关键代码如下:
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;
}
而本文中的驱动程序的框架中,设备文件的创建和销毁是在驱动程序的上层文件中被封装成了两个函数,代码如下:
void register_button_operations(struct button_operations *opr)
{
int i;
p_button_opr = opr;
for (i = 0; i < opr->count; i++)
{
device_create(button_class, NULL, MKDEV(major, i), NULL, "swh_button%d", i);
}
}
void unregister_button_operations(void)
{
int i;
for (i = 0; i < p_button_opr->count; i++)
{
device_destroy(button_class, MKDEV(major, i));
}
}
底层代码在初始化和退出时调用这两个在上层文件中的函数进行设备文件的创建和销毁,相关代码如下:
int board_imx6ull_button_drv_init(void)
{
register_button_operations(&my_buttons_ops);
return 0;
}
void board_imx6ull_button_drv_exit(void)
{
unregister_button_operations();
}
可见,如果这两个文件被分别编译成两个模块,这里的加载顺序应该是要先加载上层的模块,再加载下层的模块。因为下层的模块需要调用上层模块的函数。
完整源代码
自定义头文件button_drv.h
的内容
#ifndef _BUTTON_DRV_H
#define _BUTTON_DRV_H
struct button_operations {
int count;
void (*init) (int which);
int (*read) (int which);
void (*close) (int which);
};
void register_button_operations(struct button_operations *opr);
void unregister_button_operations(void);
#endif
驱动程序上层源代码button_drv.c
的内容
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/fs.h>
#include <linux/signal.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>
#include <linux/capi.h>
#include <linux/kernelcapi.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
#include "button_drv.h"
static int major = 0;
static struct button_operations *p_button_opr;
static struct class *button_class;
static int button_open (struct inode *inode, struct file *file)
{
int minor = iminor(inode);
p_button_opr->init(minor);
return 0;
}
static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off)
{
unsigned int minor = iminor(file_inode(file));
char level;
int err;
level = p_button_opr->read(minor);
err = copy_to_user(buf, &level, 1);
return 1;
}
static int button_close (struct inode *inode, struct file *file)
{
int minor = iminor(inode);
p_button_opr->close(minor);
return 0;
}
static struct file_operations button_fops = {
.open = button_open,
.read = button_read,
.release = button_close,
};
void register_button_operations(struct button_operations *opr)
{
int i;
p_button_opr = opr;
for (i = 0; i < opr->count; i++)
{
device_create(button_class, NULL, MKDEV(major, i), NULL, "swh_button%d", i);
}
}
void unregister_button_operations(void)
{
int i;
for (i = 0; i < p_button_opr->count; i++)
{
device_destroy(button_class, MKDEV(major, i));
}
}
EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);
int button_init(void)
{
major = register_chrdev(0, "swh_button", &button_fops);
button_class = class_create(THIS_MODULE, "swh_button");
if (IS_ERR(button_class))
return -1;
return 0;
}
void button_exit(void)
{
class_destroy(button_class);
unregister_chrdev(major, "swh_button");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
驱动程序下层源代码board_100ask_imx6ull.c
的内容
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/io.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 "button_drv.h"
struct imx6ull_gpio {
volatile unsigned int dr; // 数据寄存器
volatile unsigned int gdir; // 数据方向寄存器
volatile unsigned int psr; // 状态寄存器
volatile unsigned int icr1; // 中断配置寄存器1
volatile unsigned int icr2; // 中断配置寄存器2
volatile unsigned int imr; // 中断屏蔽寄存器
volatile unsigned int isr; // 中断状态寄存器
volatile unsigned int edge_sel; // 边沿选择寄存器
};
/* enable GPIO4 */
static volatile unsigned int *CCM_CCGR3;
/* enable GPIO5 */
static volatile unsigned int *CCM_CCGR1;
/* set GPIO5_IO03 as GPIO */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;
/* set GPIO4_IO14 as GPIO */
static volatile unsigned int *IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B;
static struct imx6ull_gpio *gpio4;
static struct imx6ull_gpio *gpio5;
static void board_imx6ull_button_init(int which) /* 初始化button, which-哪个button */
{
if (which == 0) {
/* 1. 检查并映射需要的寄存器 */
if (!CCM_CCGR1) {
CCM_CCGR1 = ioremap(0x20C406C, 4);
}
if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
}
if (!gpio5) {
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
}
/* 2. enable GPIO5 Clock
* CG15, b[31:30] = 0b11
*/
*CCM_CCGR1 |= (3 << 30);
/* 3. set GPIO5_IO01 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;
/* 4. set GPIO5_IO01 as input
* GPIO5 GDIR, b[1] = 0b0
*/
gpio5->gdir &= ~(1 << 1);
} else if (which == 1) {
/* 1. 检查并映射需要的寄存器 */
if (!CCM_CCGR3) {
CCM_CCGR3 = ioremap(0x20C4074, 4);
}
if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = ioremap(0x20E01B0, 4);
}
if (!gpio4) {
gpio4 = ioremap(0x020A8000, sizeof(struct imx6ull_gpio));
}
/* 2. enable GPIO4 Clock
* CG6, b[13:12] = 0b11
*/
*CCM_CCGR3 |= (3 << 12);
/* 3. set GPIO4_IO14 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = 5;
/* 4. set GPIO4_IO14 as input
* GPIO4 GDIR, b[14] = 0b0
*/
gpio4->gdir &= ~(1 << 14);
}
}
static int board_imx6ull_button_read (int which) /* 读button, which-哪个 */
{
//printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
if (which == 0)
return (gpio5->psr & (1<<1)) ? 1 : 0;
else
return (gpio4->psr & (1<<14)) ? 1 : 0;
}
static void board_imx6ull_button_close(int which) /* 关闭button, which-哪个button */
{
if (which == 0) {
/* 解除 GPIO5 相关寄存器的映射 */
if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = NULL;
}
if (gpio5) {
iounmap(gpio5);
gpio5 = NULL;
}
if (CCM_CCGR1) {
iounmap(CCM_CCGR1);
CCM_CCGR1 = NULL;
}
} else if (which == 1) {
/* 解除 GPIO4 相关寄存器的映射 */
if (IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
iounmap(IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B);
IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = NULL;
}
if (gpio4) {
iounmap(gpio4);
gpio4 = NULL;
}
if (CCM_CCGR3) {
iounmap(CCM_CCGR3);
CCM_CCGR3 = NULL;
}
}
}
static struct button_operations my_buttons_ops = {
.count = 2,
.init = board_imx6ull_button_init,
.read = board_imx6ull_button_read,
.close = board_imx6ull_button_close,
};
int board_imx6ull_button_drv_init(void)
{
register_button_operations(&my_buttons_ops);
return 0;
}
void board_imx6ull_button_drv_exit(void)
{
unregister_button_operations();
}
module_init(board_imx6ull_button_drv_init);
module_exit(board_imx6ull_button_drv_exit);
MODULE_LICENSE("GPL");
测试程序button_test.c
的内容
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv)
{
int fd;
char val;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\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. 写文件 */
read(fd, &val, 1);
printf("get button : %d\n", val);
close(fd);
return 0;
}
驱动程序框架和逻辑没有一点问题,重点还是对硬件的操作
驱动程序的框架的逻辑通过之前几个驱动程序的学习,已经一点问题没有了,所以本篇博文的重点还是在如何对硬件进行操作。
驱动程序底层文件中的设备打开函数board_imx6ull_button_init分析
注意:这个函数已经被我优化得与官方的有较大不同了
注意:函数board_imx6ull_button_init
已经被我优化得与官方的有较大不同了。比如官方并没有分区是哪个按键,而是统一进行了寄存器的映射,我的优化就进行了区分。再有,官方在映射操作前只判断了CCM_CCGR1
寄存器指针是否为空值,而我是对各个寄存器的指针都检查了。
函数board_imx6ull_button_init的源代码
代码如下:
static void board_imx6ull_button_init(int which) /* 初始化button, which-哪个button */
{
if (which == 0) {
/* 1. 检查并映射需要的寄存器 */
if (!CCM_CCGR1) {
CCM_CCGR1 = ioremap(0x20C406C, 4);
}
if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
}
if (!gpio5) {
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
}
/* 2. enable GPIO5 Clock
* CG15, b[31:30] = 0b11
*/
*CCM_CCGR1 |= (3 << 30);
/* 3. set GPIO5_IO01 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;
/* 4. set GPIO5_IO01 as input
* GPIO5 GDIR, b[1] = 0b0
*/
gpio5->gdir &= ~(1 << 1);
} else if (which == 1) {
/* 1. 检查并映射需要的寄存器 */
if (!CCM_CCGR3) {
CCM_CCGR3 = ioremap(0x20C4074, 4);
}
if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = ioremap(0x20E01B0, 4);
}
if (!gpio4) {
gpio4 = ioremap(0x020A8000, sizeof(struct imx6ull_gpio));
}
/* 2. enable GPIO4 Clock
* CG6, b[13:12] = 0b11
*/
*CCM_CCGR3 |= (3 << 12);
/* 3. set GPIO4_IO14 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = 5;
/* 4. set GPIO4_IO14 as input
* GPIO4 GDIR, b[14] = 0b0
*/
gpio4->gdir &= ~(1 << 14);
}
}
根据原理图确定要对哪个GPIO口进行读操作
在这里,我们需要板上的KEY1和KEY2进行操作,相关原理图如下:
原理图百度网盘链接:https://pan.baidu.com/s/1SWuF_GMdPLAYOUeDDPH9qw?pwd=ame9
从原理图中我们可以看出,我们需要对GPIO5_IO01和GPIO4_IO14进行操作。相关寄存器的初始化的操作步骤与博文 https://blog.csdn.net/wenhao_ir/article/details/144973219中没啥本质区别,代码中注释也写得很清楚,这里就不赘述了。
这里需要分析一下下面这些代码。
语句 if (!CCM_CCGR1)
及类似代码的作用
在代码中,if (!CCM_CCGR1)
的作用是检查指针 CCM_CCGR1
是否为 NULL,即是否未被初始化(映射到物理地址)。如果 CCM_CCGR1
的值为 NULL
,表示 ioremap()
尚未被调用,需要执行映射操作。以下是它的意义与上下文解释:
上下文
CCM_CCGR1
是一个全局变量,指向一个特定的寄存器地址,用于控制 IMX6ULL 的 GPIO 时钟。- 在嵌入式开发中,直接访问硬件寄存器的地址是通过
ioremap
函数完成的。ioremap
将物理地址映射到内核虚拟地址空间中,使得驱动程序可以通过指针间接访问硬件寄存器。 CCM_CCGR1
在程序初始状态下是 NULL,因为它是一个全局变量,默认未被初始化。
if (!CCM_CCGR1)
的作用
- 检查初始化状态:
- 如果
CCM_CCGR1
为NULL
,表示映射还未完成,需要调用ioremap
将对应物理地址映射到内核虚拟地址空间中。 - 避免重复映射:一旦
CCM_CCGR1
已经成功映射,就不需要再次调用ioremap
。确保寄存器地址只映射一次,避免重复的ioremap
操作,提高代码的安全性和效率。
- 如果
其它几行类似的代码,比如下面这些,也是相同的作用,这里就不再赘述。
if (!CCM_CCGR3)
if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1)
if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B)
if (!iomux)
if (!gpio4)
if (!gpio5)
如何利用结构体imx6ull_gpio
实现对GPIO的各个寄存器的访问?
试分析下面有关联的三段代码是如何实现gpio5->gdir是指向我们需要操作的GPIO方向寄存器的?
struct imx6ull_gpio {
volatile unsigned int dr;
volatile unsigned int gdir;
volatile unsigned int psr;
volatile unsigned int icr1;
volatile unsigned int icr2;
volatile unsigned int imr;
volatile unsigned int isr;
volatile unsigned int edge_sel;
};
static struct imx6ull_gpio *gpio5;
gpio5->gdir &= ~(1 << 1);
以下是这三段代码如何实现 gpio5->gdir
指向目标寄存器的详细分析:
1. struct imx6ull_gpio
结构体的定义
struct imx6ull_gpio {
volatile unsigned int dr; // 数据寄存器
volatile unsigned int gdir; // 数据方向寄存器
volatile unsigned int psr; // 状态寄存器
volatile unsigned int icr1; // 中断配置寄存器1
volatile unsigned int icr2; // 中断配置寄存器2
volatile unsigned int imr; // 中断屏蔽寄存器
volatile unsigned int isr; // 中断状态寄存器
volatile unsigned int edge_sel; // 边沿选择寄存器
};
- 这是一个结构体,用于描述 IMX6ULL 芯片中 GPIO 控制器的寄存器布局。
- 每个字段表示某个寄存器,字段的顺序和寄存器在硬件地址中的排列顺序一致。
- 偏移量:
gdir
是第 2 个字段,因为IMX6ULL是32位的处理器,所以其对应的unsigned int
类型占用4个字节,所以其偏移量为 4 字节(因为dr
是第 1 个字段,占 4 字节)。
2. gpio5
的定义和映射
static struct imx6ull_gpio *gpio5;
gpio5
是一个指针,类型为struct imx6ull_gpio *
。- 它是用来指向 GPIO5 控制器寄存器的虚拟地址。
3. 通过 ioremap
实现寄存器映射
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
-
ioremap
功能:- 将物理地址
0x20AC000
对应的寄存器区域映射到内核虚拟地址空间。 - 返回的虚拟地址赋值给
gpio5
,让gpio5
指向寄存器区域的起始地址。
- 将物理地址
-
sizeof(struct imx6ull_gpio)
:- 指定了映射的大小,等于结构体的总大小,即包含所有相关寄存器。
-
物理地址
0x20AC000
:- 根据芯片文档,这是 GPIO5 控制器寄存器组的基地址。
4. 访问 gpio5->gdir
【GPIO5的方向寄存器】
gpio5->gdir &= ~(1 << 1);
-
偏移量计算:
gpio5
的值是虚拟地址,指向寄存器组的起始地址。gpio5->gdir
访问的是偏移量为 4 字节 的地址【因为IMX6ULL是32位的处理器,所以其对应的unsigned int
类型占用4个字节】:gpio5 + 4
指向物理地址0x20AC004
(即 GPIO5 数据方向寄存器的地址)。
-
操作解析:
gpio5->gdir &= ~(1 << 1)
等价于:- 读取
gpio5->gdir
(地址0x20AC004
)的当前值。 - 对其进行按位与操作,清除第 1 位(将其置为 0)。
- 写回处理后的值到
gpio5->gdir
,完成对寄存器的修改。
- 读取
总结:如何实现了指向GPIO5的方向寄存器的?
- 定义
struct imx6ull_gpio
结构体,与硬件寄存器布局一致。 - 使用
ioremap
将 GPIO5 控制器的物理地址0x20AC000
映射到虚拟地址。 - 通过
gpio5->gdir
访问虚拟地址中偏移量为 4 的位置,即目标寄存器。 - 对目标寄存器进行读写操作,从而完成 GPIO 数据方向的配置。
这样的设计屏蔽了直接操作物理地址的复杂性,提供了一个简洁的抽象层,更易于开发和维护。
为什么不分析别的语句了?
整个驱动程序就只有上面这个board_imx6ull_button_init
要难理解点,其它的在之前的驱动程序学习中,都已经了解得比较清楚了,就不再赘述了。
Makefile文件的编写
# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o button_test button_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f ledtest
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += button_drv.o
obj-m += board_100ask_imx6ull.o
这个没啥好说的…
交叉编译
工程目录复制到Ubuntu中…
然后make
把生成的ko文件和ELF可执行文件复制到网络系统文件中以备用…
上板测试
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
进入相应的目录:
cd /mnt/read_button_values_drivers/
加载驱动程序模块(注意有顺序要求)
先加载button_drv.ko
insmod button_drv.ko
再加载board_100ask_imx6ull.ko
insmod board_100ask_imx6ull.ko
这里要注意,为什么加载button_drv.ko
提示“loading out-of-tree module taints kernel”,而加载board_100ask_imx6ull.ko
时没有提示?
原因见:https://blog.csdn.net/wenhao_ir/article/details/145109760
看下相应的设备文件生成没有
ls /dev/
运行测试程序
先测试KEY1
我们先不按下按键运行下面的命令,从原理图来说,此时应该读到的值是1
./button_test /dev/swh_button0
果然如我们所料是1
我们再按下按键运行下面的命令,从原理图来说,此时应该读到的值是0
./button_test /dev/swh_button0
然后测试KEY2
我们先不按下按键运行下面的命令,从原理图来说,此时应该读到的值是1
./button_test /dev/swh_button1
果然如我们所料是1
我们再按下按键运行下面的命令,从原理图来说,此时应该读到的值是0
./button_test /dev/swh_button1
果然如我们所料是0
。
卸载模块
先查看下模块的依赖关系:
lsmod
所以应该先卸载board_100ask_imx6ull
,再卸载button_drv
rmmod board_100ask_imx6ull
这个模块卸载后,根据相关源代码,设备文件应该被删除了,我们看下设备文件还有没有:
ls /dev/
果然如我所料,设备文件/dev/swh_button0
和/dev/swh_button1
都不存在了。
卸载模块button_drv
前,设备类的目录和驱动程序的主设备号应该都还在,我们查看一下:
ls /sys/class/
果然设备类的目录还在。
cat /proc/devices
果然驱动程序还在,而且主设备号是245。
接下来卸载button_drv
模块:
rmmod button_drv
设备类不在了:
ls /sys/class/
驱动程序和主设备号也不在了:
cat /proc/devices
至此,卸载模块部分也测试完毕。
附完整工程文件
https://pan.baidu.com/s/1wuARJoTiR4oM3dBkOeVT0g?pwd=g74j