目录
一、编写驱动程序的三种方法
1.1 传统方法
在传统方法中,资源和驱动都放在一个文件里面,使用哪个引脚,怎么操作引脚都写死在一个文件中,这种方法缺点就是扩展性差,每次修改引脚时都要重新编译。
1.2 总线设备驱动模型
在总线设备驱动模型中,我们把资源和驱动分离开来,如果需要修改引脚,我们只需要单独修改编译资源部分,而不需要编译驱动部分,增加了可扩展性,这种方法的缺点就是会产生一大堆资源文件在内核中,不方便管理。
1.3 设备树
在引入设备树后,我们的资源统一放到设备树中管理,修改资源只需要更改设备树文件,极大的方便资源的管理。
二、IMX6ULL按键控制LED灯亮灭(实现部分)
我所采用的是前面介绍的传统方法来实现,为了方便拓展,我也分了两层来实现,一层是chip_xxx_gpio.c,作为硬件层,主要用来配置引脚资源和操作相应寄存器;另一层是xxx_drv.c,作为应用层和硬件层的中间层,向上(应用层)提供驱动接口(open,read,write等等),向下(硬件层)调用硬件操作。下面是我的代码的结构,源码放在文章末尾。
2.1 按键驱动部分(button_drv_source)
2.1.1 硬件层(chip_xxx_gpio.c)
首先是硬件层,这部分涉及到具体的gpio的操作以及引脚的配置,所以我们要从板子的原理图,以及芯片手册开始看起(这两个文件我会一起放在源码中)。
在板子的原理图中(100ask_imx6ull_v1.1.pdf文件),我们先通过目录找到GPIO部分:
在右边我们可以看到两个按键的原理图,我们使用的是左边的按键:
从原理图上可以看到,我们可以通过第5组GPIO模块的第1个引脚(GPIO5_01)来获得引脚电平,并且可以知道按键未按下时引脚为高电平,按下时引脚为低电平。
在原理图上得到gpio引脚信息后,接下来我们就可以通过芯片手册(IMX6ULLRM.pdf文件)来配置各个模块。我们主要操作的模块有三个:
![](https://i-blog.csdnimg.cn/blog_migrate/7f73ba9f540c20b8a17c442760c47633.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/c12cefbb06ff4de266e5aadecf1ddee1.png)
首先是CCM模块,这个模块主要用来控制时钟,哪组 GPIO用哪个 CCM_CCGR 寄存器来设置,请看上图红框部分(如GPIO5使用CCGR1[CG15]来控制)。
我们可以到芯片手册里面找到CCGR1寄存器的位置(手册Chapter 18: Clock Controller Module (CCM) --> CCM Memory Map/Register Definition --> CCM下 ),可以看到30-31位是gpio5的时钟使能位,并且找到了寄存器的物理地址(0x20C406C):
CCM_CCGR 寄存器中某 2 位的取值含义如下:
① 00:该 GPIO 模块全程被关闭。
② 01:该 GPIO 模块在 CPU run mode 情况下是使能的;在 WAIT 或 STOP 模式下,关闭。
③ 10:保留。
④ 11:该 GPIO 模块全程使能。
因为我们肯定要gpio模块全程使能,所以要初始化CCM_CCGR1模块的30-31位为11,初始化代码如下:
/* 声明指针 */
static volatile unsigned int *CCM_CCGR1;
/* 初始化指针,指针指向CCM_CCGR1寄存器,ioremap的作用是将物理地址映射到虚拟地址上 */
CCM_CCGR1 = ioremap(0x20C406C, 4);
/* 使能GPIO5时钟*/
*CCM_CCGR1 |= (3<<30); //30-31位置1
其次是IOMUXC模块,用于控制引脚的复用,这里我们找到控制gpio5_1引脚的寄存器(手册Chapter 32: IOMUX Controller (IOMUXC) --> IOMUXC SNVS Memory Map/Register Definition --> IOMUXC_SNVS下):
所以我们需要初始化寄存器IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1的低四位MUX_MODE为0101,初始化代码如下:
/* 声明指针 */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;
/* 让指针指向寄存器 */
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
/* 初始化寄存器低四位为0101*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 &= ~(0xf); //低四位清0
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 |= 0x5; //低四位MUX_MODE置0101
最后就是gpio5对应的寄存器了,下面是gpio模块内部框图:
我们暂时只需要用到三个寄存器,GDIR(用于设置方向)、DR(设置输出电平)、PSR(读取输入电平),由于每个模块的寄存器地址都是连续的,并且每个相隔4字节,所以我们可以用一个结构体来存放所有寄存器地址:
初始化代码如下:
/* gpio寄存器,因为每个gpio模块的寄存器地址连续,且各个寄存器之间相隔4B */
struct imx6ull_gpio_registers {
volatile unsigned int DR; //gpio data register
volatile unsigned int GDIR; //gpio direction register
volatile unsigned int PSR; //gpio pad status register
volatile unsigned int ICR1; //gpio interrupt configuration register1
volatile unsigned int ICR2; //gpio interrupt configuration register2
volatile unsigned int IMR; //gpio interrupt mask register
volatile unsigned int ISR; //gpio interrupt status register
volatile unsigned int EDGE_SEL; //gpio edge select register
};
/* 指针指向第一个寄存器的地址 */
gpio5_registers = ioremap(0x20AC000, sizeof(struct imx6ull_gpio_registers));
/* 3. 设置gpio5_1引脚为input,因为我们要读取引脚数据*/
gpio5_registers->GDIR &= ~(1<<1); //将GDIR寄存器的第一位置0
在知道怎么初始化寄存器后,我们就可以开始编写硬件层的代码了,硬件层代码如下:
chip_button_gpio.h
#ifndef _CHIP_BUTTON_GPIO_H
#define _CHIP_BUTTON_GPIO_H
/*gpio寄存器,因为每个gpio模块的寄存器地址连续,且各个寄存器之间相隔4B*/
struct imx6ull_gpio_registers {
volatile unsigned int DR; //gpio data register
volatile unsigned int GDIR; //gpio direction register
volatile unsigned int PSR; //gpio pad status register
volatile unsigned int ICR1; //gpio interrupt configuration register1
volatile unsigned int ICR2; //gpio interrupt configuration register2
volatile unsigned int IMR; //gpio interrupt mask register
volatile unsigned int ISR; //gpio interrupt status register
volatile unsigned int EDGE_SEL; //gpio edge select register
};
//对button的操作
struct button_operations {
int (*init) (int which);
int (*read) (int which);
};
static int button_gpio_init(int which);
static int button_gpio_read(int which);
#endif
chip_button_gpio.c
#include <linux/platform_device.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 <asm/io.h>
#include "chip_button_gpio.h"
#include "button_drv.h"
/* enable GPIO5 clock*/
static volatile unsigned int *CCM_CCGR1;
/* set GPIO5_IO01 as GPIO */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;
/* GPIO registers */
static struct imx6ull_gpio_registers *gpio5_registers;
/*提供给上一层的接口*/
static struct button_operations button_opr = {
.init = button_gpio_init,
.read = button_gpio_read,
};
/*初始化button,which指定哪一个button*/
static int button_gpio_init(int which){
/* 将物理地址映射到虚拟地址上 */
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
gpio5_registers = ioremap(0x20AC000, sizeof(struct imx6ull_gpio_registers));
/* 默认就是第0个按键 */
if (which == 0){
/* 1. enable GPIO5 clock*/
*CCM_CCGR1 |= (3<<30); //30-31位置1
/* 2. set GPIO5_IO01 as GPIO*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 &= ~(0xf); //低四位清0
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 |= 0x5; //低四位MUX_MODE置0101
/* 3. set GPIO5_IO01 as input*/
gpio5_registers->GDIR &= ~(1<<1); //将GDIR寄存器的第一位置0
}
/* 后面如果想使用多个按键的话可以用if else判断 */
return 0;
}
/*读取引脚电平,查看按键是否按下,0表示未按下,1表示按下,-1表示出错*/
static int button_gpio_read(int which){
//将psr寄存器的第一位状态返回,该位即为引脚状态。按下按钮时为0,未按下时为1
return (gpio5_registers->PSR & (1<<1)) ? 1 : 0;
}
/*入口函数,模块一被装载就执行*/
static int chip_button_gpio_drv_init(void){
//将button_opr传给上一层
register_button_operations(&button_opr);
//创建从设备
button_device_create(0);
return 0;
}
/*入口函数,模块一被装载就执行*/
static void chip_button_gpio_drv_exit(void){
//销毁从设备
button_device_destroy(0);
}
module_init(chip_button_gpio_drv_init);
module_exit(chip_button_gpio_drv_exit);
MODULE_LICENSE("GPL");
其中:
button_operations是提供给上一层(即中间层)的接口,它包含了两个函数,button_gpio_init和button_gpio_read。button_gpio_init函数用于初始化各个寄存器,而button_gpio_read则读取引脚电平,并且前面我们知道按键未按下时引脚为高电平,按下时引脚为低电平。
chip_button_gpio_drv_init和chip_button_gpio_drv_exit两个函数是模块的入口和出口函数,模块被装载(insmod)则执行chip_button_gpio_drv_init,模块被卸载(rmmod)则执行chip_button_gpio_drv_exit。入口函数主要把button_operations传给上一层并且创建子设备,出口函数则销毁子设备。硬件层代码如下:
2.1.2 中间层(xxx_drv.c)
在linux中,一切皆文件,设备也是作为文件存在于linux里面的,我们在应用层对设备进行各种操作就相当于对文件进行读写操作,所以为了让应用层能够通过对文件的操作来控制设备,我们要提供接口给应用层(如open,read,write等等),比如通过read读取引脚电平(按键是否按下),通过write来让引脚为高电平或者低电平(控制led是否打开)。这些接口通过file_operations结构体来实现,中间层代码如下:
button_drv.h
#ifndef _BUTTON_DRV_H
#define _BUTTON_DRV_H
#include "chip_button_gpio.h"
void button_device_create(int minor);
void button_device_destroy(int minor);
void register_button_operations(struct button_operations *opr);
#endif
button_drv.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 "button_drv.h"
/*确定主设备号,可以自己指定,也可以设置为0让系统自动分配*/
static int major = 0;
static struct class *button_class;
/*指向button_operations的指针*/
struct button_operations *p_button_opr;
/*创建button设备,minor是从设备号*/
void button_device_create(int minor){
device_create(button_class, NULL, MKDEV(major, minor), NULL, "lzp_button%d", minor);
}
/*销毁对应的button设备*/
void button_device_destroy(int minor){
device_destroy(button_class, MKDEV(major, minor));
}
/*获取chip_button_gpio中的button_operations*/
void register_button_operations(struct button_operations *opr){
p_button_opr = opr;
}
/*使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用*/
EXPORT_SYMBOL(button_device_create);
EXPORT_SYMBOL(button_device_destroy);
EXPORT_SYMBOL(register_button_operations);
/*实现对应的open/read/write等函数,填入file_operations结构体*/
static ssize_t button_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
/*获取次设备号,即read函数想要操作的设备*/
unsigned int minor = iminor(file_inode(file));
/*读取按键电平*/
char level = p_button_opr->read(minor);
/*将传回给用户空间,不能直接访问buf,需要用到copy_to_user函数*/
copy_to_user(buf, &level, 1);
return 0;
}
static int button_drv_open (struct inode *node, struct file *file){
/*获取次设备号*/
int minor = iminor(node);
/*根据次设备号初始化button*/
p_button_opr->init(minor);
return 0;
}
/*定义自己button的file_operations结构体*/
static struct file_operations button_drv = {
.open = button_drv_open,
.read = button_drv_read,
};
/*把file_operations结构体告诉内核:注册驱动程序*/
/*入口函数:安装驱动程序时,就会去调用这个入口函数*/
static int button_init(void){
int err;
/*注册字符设备,把file_operations结构体告诉内核,获取主设备号*/
major = register_chrdev(0, "lzp_button", &button_drv);
button_class = class_create(THIS_MODULE, "lzp_button_class");
err = PTR_ERR(button_class);
if (IS_ERR(button_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "lzp_button");
return -1;
}
return 0;
}
/*出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void button_exit(void)
{
class_destroy(button_class);
unregister_chrdev(major, "lzp_button");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
2.2 led驱动部分(led_drv_source)
由于led驱动源码整体架构和button几乎一模一样,只是一些引脚和寄存器的操作不同,比如在led硬件层中我们要写DR寄存器来操作led的亮灭,写入低电平则led亮起,写入高电平则led熄灭,对应的gpio引脚也不一样。
这里我就不进一步细讲,直接贴代码了,如果有不懂的地方可以评论区交流。
2.2.1 硬件层(chip_xxx_gpio.c)
chip_led_gpio.h
#ifndef _CHIP_LED_GPIO_H
#define _CHIP_LED_GPIO_H
/*gpio寄存器,因为每个gpio模块的寄存器地址连续,且各个寄存器之间相隔4B*/
struct imx6ull_gpio_registers {
volatile unsigned int DR; //gpio data register
volatile unsigned int GDIR; //gpio direction register
volatile unsigned int PSR; //gpio pad status register
volatile unsigned int ICR1; //gpio interrupt configuration register1
volatile unsigned int ICR2; //gpio interrupt configuration register2
volatile unsigned int IMR; //gpio interrupt mask register
volatile unsigned int ISR; //gpio interrupt status register
volatile unsigned int EDGE_SEL; //gpio edge select register
};
//对led的操作
struct led_operations {
int (*init) (int which);
int (*ctl) (int which, int status);
};
static int led_gpio_init(int which);
static int led_gpio_write(int which, int status);
#endif
chip_led_gpio.c
#include <linux/platform_device.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 <asm/io.h>
#include "chip_led_gpio.h"
#include "led_drv.h"
/* enable GPIO5 clock*/
static volatile unsigned int *CCM_CCGR1;
/* set GPIO5_IO03 as GPIO */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
/* GPIO5 registers */
static struct imx6ull_gpio_registers *gpio5_registers;
/*提供给上一层的接口*/
static struct led_operations led_opr = {
.init = led_gpio_init,
.ctl = led_gpio_write,
};
/*初始化led,which指定哪一个led*/
static int led_gpio_init(int which){
/* 将物理地址映射到虚拟地址上 */
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
gpio5_registers = ioremap(0x20AC000, sizeof(struct imx6ull_gpio_registers));
/* 默认就是第0个按键 */
if (which == 0){
/* 1. enable GPIO5 clock*/
*CCM_CCGR1 |= 0x11; //30-31位置11
/* 2. set GPIO5_IO01 as GPIO*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~(0xf); //低四位清0
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5; //低四位MUX_MODE置0101
/* 3. set GPIO5_IO01 as input*/
gpio5_registers->GDIR |= (1<<3); //将GDIR寄存器的第三位置1,表示输入
}
/* 后面如果想使用多个按键的话可以用if else判断 */
return 0;
}
/*写DR寄存器,写0灯亮,写1灯灭*/
static int led_gpio_write(int which, int status){
if(status == 1){
//status为1则让灯亮,DR第三位置0
gpio5_registers->DR &= ~(1<<3);
}else{
//status为0则让灯灭,DR第三位置1
gpio5_registers->DR |= (1<<3);
}
return 0;
}
/*入口函数,模块一被装载就执行*/
static int chip_led_gpio_drv_init(void){
//将led_opr传给上一层
register_led_operations(&led_opr);
//创建从设备
led_device_create(0);
return 0;
}
/*入口函数,模块一被装载就执行*/
static void chip_led_gpio_drv_exit(void){
//销毁从设备
led_device_destroy(0);
}
module_init(chip_led_gpio_drv_init);
module_exit(chip_led_gpio_drv_exit);
MODULE_LICENSE("GPL");
2.2.2 中间层(led_drv.c)
led_drv.h
#ifndef _LED_DRV_H
#define _LED_DRV_H
#include "chip_led_gpio.h"
void led_device_create(int minor);
void led_device_destroy(int minor);
void register_led_operations(struct led_operations *opr);
#endif
led_drv.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_drv.h"
/*确定主设备号,可以自己指定,也可以设置为0让系统自动分配*/
static int major = 0;
static struct class *led_class;
/*指向led_operations的指针*/
struct led_operations *p_led_opr;
/*创建led设备,minor是从设备号*/
void led_device_create(int minor){
device_create(led_class, NULL, MKDEV(major, minor), NULL, "lzp_led%d", minor);
}
/*销毁对应的led设备*/
void led_device_destroy(int minor){
device_destroy(led_class, MKDEV(major, minor));
}
/*获取chip_led_gpio中的led_operations*/
void register_led_operations(struct led_operations *opr){
p_led_opr = opr;
}
/*使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用*/
EXPORT_SYMBOL(led_device_create);
EXPORT_SYMBOL(led_device_destroy);
EXPORT_SYMBOL(register_led_operations);
/*实现对应的open/read/write等函数,填入file_operations结构体*/
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset){
char status;
/*获取次设备号,即read函数想要操作的设备*/
unsigned int minor = iminor(file_inode(file));
/*读取用户传来的status*/
copy_from_user(&status, buf, 1);
/*操作led*/
p_led_opr->ctl(minor, status);
return 0;
}
static int led_drv_open (struct inode *node, struct file *file){
/*获取次设备号*/
int minor = iminor(node);
/*根据次设备号初始化button*/
p_led_opr->init(minor);
return 0;
}
/*定义自己led的file_operations结构体*/
static struct file_operations led_drv = {
.open = led_drv_open,
.write = led_drv_write,
};
/*把file_operations结构体告诉内核:注册驱动程序*/
/*入口函数:安装驱动程序时,就会去调用这个入口函数*/
static int led_init(void){
int err;
/*注册字符设备,把file_operations结构体告诉内核,获取主设备号*/
major = register_chrdev(0, "lzp_led", &led_drv);
led_class = class_create(THIS_MODULE, "lzp_led_class");
err = PTR_ERR(led_class);
if (IS_ERR(led_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "lzp_led");
return -1;
}
return 0;
}
/*出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void led_exit(void)
{
class_destroy(led_class);
unregister_chrdev(major, "lzp_led");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
2.3 编写应用层程序实现按键操作led亮灭
编写好驱动程序后我们就要开始使用驱动程序提供的接口了,可以看到对设备的操作和对文件的操作基本一样(open、read、write),这里我只使用的简单的查询模式来实现按键控制led亮灭(用一个while循环一直读取按键的电平,如果检测到按键按下则使led亮起,如果检测到按键松开则熄灭led),应用层代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./button_led_test /dev/lzp_button0 /dev/lzp_led0
*/
int main(int argc, char **argv)
{
int fd_button;
int fd_led;
char level;
int status;
/* 1. 判断参数 */
if (argc != 3) {
return -1;
}
/* 2. 打开文件 */
if ((fd_button = open(argv[1], O_RDWR)) == -1){//打开button
printf("can not open file %s\n", argv[1]);
return -1;
}
if ((fd_led = open(argv[2], O_RDWR)) == -1){//打开led
printf("can not open file %s\n", argv[2]);
return -1;
}
/* 3. 读文件 */
while(1){
read(fd_button, &level, 1);
if(level == 0){
//如果按键按下
status = 1;
write(fd_led, &status, 1);
}else{
//如果按键松开
status = 0;
write(fd_led, &status, 1);
}
}
close(fd_led);
close(fd_button);
return 0;
}
三、编译源码,测试
编译之前记得把makefile里面的KERN_DIR换成你自己的内核目录路径:
在目录下执行make指令,执行完后会把按键和led的ko文件都复制到当前目录,需要用到的几个文件就是下图红线框出来的几个(button_drv.ko,chip_button_gpio.ko,led_drv.ko,chip_led_gpio.ko,button_led_test):
连上板子,挂载nfs,装载模块,这里要注意装载模块顺序,先装载中间层(xxx_drv.ko),再装载硬件层(chip_xxx_gpio.ko):
查看button驱动和led设备文件是否存在:
最后测试使用button_led_test可执行文件测试:
可以看到程序卡住了,因为我们写了一个while循环,这个时候我们可以去按下按键(板子右上角的按键):
按下(绿色led亮起):
松开(led熄灭):
至此,imux6ull按键控制led亮灭的实现结束,代码写的有点糙,希望大佬轻喷,如果有需要的话我后面还可以把设备树也加到里面去(大家也可以基于我这个结构去扩展)。
最后,附上代码链接:gitee地址