GPIO驱动学习实践

简介

linux驱动有三大类:字符设备驱动、块设备驱动、网络设备驱动
本文关注的是字符设备驱动-----以LED驱动为例

程序设计

头文件

下面是一些必要的头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/proc_fs.h>
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/aio.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
#include <linux/ioctl.h> /* needed for the _IOW etc stuff used later */
#include <linux/mm.h>
#include <linux/delay.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h> /*put_user*/
#include <asm/io.h>
#include <asm/system.h> /* cli(), *_flags */
#include <asm/page.h>
#include <asm/semaphore.h>

宏定义

IMMAP内部存储器映射寄存器

这一部分是IO空间对应的唯一的物理空间地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define IMMAP_BASE 0xFF000000 //物理地址
#define IMMAP_LEN 0x100000 //地址空间大小
/* 此处一般有一个自定义设备结构体,用来保存设备相关的各种信息,包括主、次设备号,物理地址映射寄存器等必要结构,如下所示 */
typedef struct io_dev_s
{
u16 major;
u16 minor;
gpio_pa pa;
gpio_pb pb;
gpio_pd pd;
gpio_pc pc;
u32 phy_immr;
u32 phy_base;
u32 phy_len;
struct class *cs;
struct class_device *cd;
spinlock_t lock;
struct cdev cdev;
}io_dev;

用户程序在运行中不能直接访问物理地址,这个地址是唯一的,我们需要将物理地址映射到0~4G大小的虚拟地址空间供用户程序使用;而驱动程序可以分为用户模式驱动和内核模式驱动:

  • 地址空间划分

    linux内核将虚拟地址空间划分为两部分供用户使用
    • 用户空间:0x00000000~0xBFFFFFFF的3G大小的低地址空间;
    • 内核空间:0xC0000000~0xFFFFFFFF的1G大小的高地址空间。
  • 设备驱动模式

    • 用户驱动模式:

      实现函数:

      1
      immr_map (&immr, IMMAP_LEN, IMMAP_BASE); //将物理地址空间映射到低3G地址的用户空间,将映射后的地址保存在immr内部存储器映射寄存器中

      上述函数实现物理地址映射是通过系统调用mmap函数实现的,过程如下:

      1
      2
      fd = open (MEM_FILE, O_RDWR)) //打开内存文件 /dev/mem
      *start = (VUINT32) mmap (NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FILE, fd, base)) //返回映射到用户进程虚拟地址空间的基地址

      注:mmap负责把文件内容映射到进程的虚拟地址空间,通过对这段内存的读取和修改来实现对文件的读取和修改,而不需要再调用read和write;这里的操作是把系统内存看作一个文件,而GPIO相关的寄存器是这个文件中的一部分内容,通过映射,GPIO的实际物理地址映射为用户空间的虚拟地址,这样返回一个虚拟空间GPIO的起始地址,便可以根据地址偏移量计算每个寄存器的虚拟地址,进而达到在用户空间程序直接访问的目的。

    • 内核驱动模式:

      实现函数:

      1
      2
      request_mem_region(io_dev.phy_base, io_dev.phy_len, IO_DRIVER_NAME); //为该驱动向内核申请指定物理地址的使用权,一旦获得使用权其他驱动便不可以使用这段内存;
      (unsigned long) ioremap(io_dev.phy_base, io_dev.phy_len); //得到该段物理地址空间的权限之后还需要将该段物理地址映射到内核地址空间,供内核调用;

      这里内核驱动模式与用户驱动模式的区别在于用户模式下驱动的控制是直接读写IO的映射之后的虚拟地址实现的,而本部分的内核调用是先使用系统调用函数诸如read,write等等进入内核空间,再由内核使用内核调用函数(由用户编写的read,write,ioctl等函数,下面会提到)来实现对IO的控制;本次对于GPIO驱动的控制是使用的内核驱动模式。

主、次设备号

1
2
#define IO_MAJOR 211
#define IO_MINOR 0

在Linux内核看来,主设备号标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备。

驱动实现 io_init & io_exit

  1. 实现物理地址到内核空间的映射并计算各个寄存器的地址偏移量;
  2. 获取dev_t类型的设备编号并以此向内核注册该设备:

    1
    2
    dev = MKDEV(io_dev.major,io_dev.minor);
    register_chrdev_region(dev, 1, IO_DRIVER_NAME);
  3. 编写各个内核调用的函数诸如:io_open, io_close, io_read, io_write,io_ioctl,同时初始化file_operations结构体,实例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /*这里以io_ioctl为例*/
    ssize_t io_ioctl(struct inode * inode, struct file *filp, unsigned int cmd, unsigned long arg)
    {
    io_dev * dev; //自定义设备结构体
    dev = filp->private_data;
    switch(cmd)
    {
    case LED_CTRL_INIT:
    *(dev->pb.pbpar) &= ~(arg);
    *(dev->pb.pbdir) |= arg;
    break;
    case LED_CTRL_ON:
    *(dev->pb.pbdat) &= ~(arg);
    break;
    case LED_CTRL_OFF:
    *(dev->pb.pbdat) |= arg;
    break;
    default:
    return -1;
    break;
    }
    return 0;
    }
    struct file_operations io_fops = {
    .owner = THIS_MODULE,
    .read = io_read,
    .write = io_write,
    .open = io_open,
    .ioctl = io_ioctl,
    .release = io_release,
    };
  4. 初始化cdev结构体,cdev结构体是设备驱动实现的关键,cdev是linux用来管理字符设备的结构体,其在内核中采用数组结构设计,这样系统中有多少个主设备号就约定了数组大小,此设备号采用链表管理,同一主设备号下可以有多个子设备。设备即文件,上层应用要访问设备,必须通过文件,cdev中包含file_operations结构体,该结构体就是驱动的文件操作集合。其分为一下几个步骤:

    1
    2
    3
    4
    5
    cdev_init(&dev->cdev, &io_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &io_fops;
    cdev_add (&dev->cdev, devno, 1);
  5. 创建设备类,使得当调用insmod命令加载驱动时可以自动在/dev目录下创建该设备节点文件

    1
    2
    io_dev.cs = class_create(THIS_MODULE,IO_DRIVER_NAME);
    io_dev.cd = class_device_create(io_dev.cs, NULL, dev, NULL, IO_DRIVER_NAME);
  6. 卸载设备-io_exit
    前面部分实现的是设备加载时调用执行的程序即io_init(),而设备退出时也需要响应的函数即io_exit:

    1
    2
    3
    4
    cdev_del(&io_dev.cdev);
    unregister_chrdev_region(MKDEV (io_dev.major, io_dev.minor), 1);
    iounmap((void __iomem *)io_dev.phy_immr);
    release_mem_region(io_dev.phy_base,io_dev.phy_len);

至此设备驱动基本编写完毕,但仍有一些后续工作:

1
2
3
4
5
6
module_init(io_init); /* load the module */
module_exit(io_exit); /* unload the module */
/* before is some decription of the model,not necessary */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("zhangh");

编译驱动

这里需要将源文件编译为.ko的驱动模块,使用insmod加载到内核,相应的rmmod命令删除驱动模块。
下面是一个标准的Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ifeq ($(KERNELRELEASE),) #开始满足这个条件 KERNELRELEASE为空
KERNELDIR ?= /home/zhanghao/task/kernal/linux-2.6.20 #指定Linux内核目录位置
PWD := $(shell pwd) #打印当前驱动源码的本地位置
modules: #编译为驱动模块
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
#-C 指定内核Makefile的路径,可以使用相对路径。
#-M 指定要编译的文件的路径,可以使用相对路径。
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
else #再次读取这个Makefile文件时 KERNELRELEASE已被赋值
MODULE_NAME := my_gpio #指定驱动模块的名字 ,不要和当前目录下的文件同名(算上扩展名比较)
CORE_OBJS := my_gpio.o #指定驱动模块的核心文件(有init 和 exit)
DEPE_OBJS := *.o #除了核心文件以外的其它依赖文件
$(MODULE_NAME)-objs := $(DEPE_OBJS) $(CORE_OBJS) #指定驱动模块的所有依赖文件
obj-m := my_gpio.o #最终由xxx-objs链接生成my_gpio.o,再生成my_gpio.ko
endif

Makefile参考链接