开发环境:
【资料图】
主机:Ubuntu12.04
开发板:RT5350
Openwrt:Openwrt15.05
1 硬件原理图1
由于发光二级管单向导电特性,即只有在正向电压(二极管的正极接正,负极接负)下才能导通发光。如图所示,如果 GPIO 输出高电平,LED就会被点亮,如果 GPIO 输出低电平,LED 就会熄灭。对于我们的驱动开发,无论是单片机、还是 ARM、或者是我们的 MIPS,核心思想,都是读写某个地址,即操作某个寄存器。
2 寄存器介绍RT5350 一共有 28 个 GPIO 管脚,这 28 个 GPIO,除了 GPIO0,其他全部是与其他功能引脚复用的。
表1
GPIO1、GPIO2 与 I2C复用。
表2
GPIO3~6 与 SPI 复用。
表3
GPIO7~14,与 UARTF 即串口 2 复用。
这些复用关系,都可以通过查阅 RT5350 的芯片手册得到。
而这些复用功能, 我们可以通过 GPIOMODE 寄存器来进行选择, 通过查手册得知,GPIOMODE寄存器的地址为 0x10000060。
表4
GPIOMODE 寄存器 bit0 位用于选择 GPIO1、GPIO2 对应的引脚是用于 IIC总线,还是用于GPIO。
GPIOMODE 寄存器 bit1 位用于选择 GPIO3~6 对应的引脚用于 SPI 总线,还是用于 GPIO。
GPIOMODE 寄存器 bit2~4 位用于选择 UARTF 对应的引脚工作于哪个模块,具体定义如下。
表5
从该表格可以看出,UARTF 对应的引脚,可以工作于 UARTF、PCM、I2S、GPIO 四种模式,将 GPIOMODE 寄存器 bit24 位设置为相应的值,就能让这些引脚工作于相应的模式,比如将GPIOMODE 寄存器 bit24 位的值设置为 7,则让相应的引脚工于 GPIO 模式。
当将相应的引脚设置为 GPIO 以后,我们接下来就需要操作该 GPIO 了,操作 GPIO 不外乎就是设置 GPIO 是输入还是输出、让其输出高电平还是低电平、读取其电平状态。不管是哪种操作,都有对应的寄存器。
表6
GPIO21_00_DIR 寄存器,用于设置 GPIO0~21 的方向,当相应的位被设置为 1,则表示相应的 GPIO 管脚被设置为了输出,如果被设置为了 0,则相应的 GPIO 引脚就被设置为了输入。
表7
当相应 GPIO 引脚被设置为输出时,设置 GPIO21_00_DATA 寄存器的相应位为 1,则让该GPIO 引脚输出了高电平,如果设置 GPIO21_00_DATA 寄存器的相应位为 0,则让该 GPIO 引脚输出了低电平。
当相应 GPIO 引脚被设置为输入时,则通过读取 GPIO21_00_DATA 寄存器时,就能读取相应的 GPIO 引脚的状态。
关于更多的 GPIO 操作的寄存器介绍,请自行查阅手册。
3 编写驱动程序我们通过前面章节的学习,掌握了驱动程序的框架,接下来我们就来写一个驱动程序,实现操作 GPIO25、GPIO26 两个 GPIO 引脚。具体驱动实现如下。
#include < linux/mm.h >#include < linux/miscdevice.h >#include < linux/slab.h >#include < linux/vmalloc.h >#include < linux/mman.h >#include < linux/random.h >#include < linux/init.h >#include < linux/raw.h >#include < linux/tty.h >#include < linux/capability.h >#include < linux/ptrace.h >#include < linux/device.h >#include < linux/highmem.h >#include < linux/crash_dump.h >#include < linux/backing-dev.h >#include < linux/bootmem.h >#include < linux/splice.h >#include < linux/pfn.h >#include < linux/export.h >#include < linux/io.h >#include < linux/aio.h >#include < linux/kernel.h >#include < linux/module.h >#include < asm/uaccess.h >#define MYLEDS_LED1_ON 0#define MYLEDS_LED1_OFF 1#define MYLEDS_LED2_ON 2#define MYLEDS_LED2_OFF 3volatile unsigned long *GPIOMODE;volatile unsigned long *GPIO27_22_DIR;volatile unsigned long *GPIO27_22_DATA;static struct class *myleds_class;static int myleds_open(struct inode *inode, struct file *file){/* 让 GPIO#25、GPIO#26 输出高电平,同时熄灭 LED1、LED2 */*GPIO27_22_DATA &= ~((1< <3)|(1< <4));return 0;}static long myleds_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg){switch(cmd){case MYLEDS_LED1_ON:// 点亮 LED1*GPIO27_22_DATA |= (1< <3);break;case MYLEDS_LED1_OFF: // 熄灭 LED1*GPIO27_22_DATA &= ~(1< <3);break;case MYLEDS_LED2_ON:// 点亮 LED2*GPIO27_22_DATA |= (1< <4);break;case MYLEDS_LED2_OFF: // 熄灭 LED2*GPIO27_22_DATA &= ~(1< <4);break;default:break;}return 0;}/* 1.分配、设置一个 file_operations 结构体 */static struct file_operations myleds_fops = {.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module 变量 */.open = myleds_open,.unlocked_ioctl = myleds_unlocked_ioctl,};int major;static int __init myleds_init(void){/* 2.注册 */major = register_chrdev(0, "myleds", &myleds_fops);/* 3.自动创建设备节点 *//* 创建类 */myleds_class = class_create(THIS_MODULE, "myleds");/* 类下面创建设备节点 */device_create(myleds_class, NULL, MKDEV(major, 0), NULL, "myleds"); ///dev/myleds/* 4.硬件相关的操作 *//* 映射寄存器的地址 */GPIOMODE = (volatile unsigned long *)ioremap(0x10000060, 4);GPIO27_22_DIR = (volatile unsigned long *)ioremap(0x10000674, 4);GPIO27_22_DATA = (volatile unsigned long *)ioremap(0x10000670, 4);/* 设置相应管脚用于 GPIO *//*** LED1 ---- GPIO#25** LED2 ---- GPIO#26*/*GPIOMODE |= (0x1< <14);/* 将 GPIO#25、GPIO#26 设置为输出 */*GPIO27_22_DIR = (1< <3)|(1< <4);return 0;}static void __exit myleds_exit(void){unregister_chrdev(major, "myleds");device_destroy(myleds_class, MKDEV(major, 0));class_destroy(myleds_class);iounmap(GPIOMODE);iounmap(GPIO27_22_DIR);iounmap(GPIO27_22_DATA);}module_init(myleds_init);module_exit(myleds_exit);MODULE_LICENSE("GPL");
因为我们的开发板上跑的是 Linux系统,因此操作某个寄存器的时候,需要将它的物理地址映射成虚拟地址,通过 ioremap()函数来进行映射,该函数的参数 1,就是对应的寄存器的物理地址,参数 2 是需要映射多大,可以理解为寄存器有多大,返回值就是该寄存器对应的虚拟地址了。
当寄存器地址映射为虚拟地址以后,然后基于前面写的驱动程序框架,就能很容易的编写出自己的 GPIO 驱动了,和操作单片机没有什么差别了。
4 编写 Makefile驱动写好以后,自然是需要想办法来编译该驱动了,通过前面的章节的学习,我们需要给该驱动编写一个 Makefile 文件。
首先是新建一个文件夹,取名为 myleds,然后在 myleds 目录下再新建一个文件夹,取名为 src,然后将上面的驱动文件复制到 src 目录下,并且在 src 目录下新建一个 Makefile,
内容如下。
obj-m += myleds.o
然后回到 myleds 目录,再创建一个 Makefile 文件,内容如下。
# #Copyright (C) 2008-2019 OpenWrt.org##This is free software, licensed under the GNU General Public License v2.# See /LICENSE for more information.#include $(TOPDIR)/rules.mkinclude $(INCLUDE_DIR)/kernel.mkPKG_NAME:=myledsPKG_RELEASE:=1include $(INCLUDE_DIR)/package.mkdefine KernelPackage/myledsSUBMENU:=Other modules# DEPENDS:=@!LINUX_3_3TITLE:=Motor driverFILES:=$(PKG_BUILD_DIR)/myleds.ko# AUTOLOAD:=$(call AutoLoad,30,myleds,1)KCONFIG:=endefdefine KernelPackage/myleds/descriptionThis is a myleds driversendefMAKE_OPTS:= \\ARCH="$(LINUX_KARCH)" \\CROSS_COMPILE="$(TARGET_CROSS)" \\SUBDIRS="$(PKG_BUILD_DIR)"define Build/Preparemkdir -p $(PKG_BUILD_DIR)$(CP) ./src/* $(PKG_BUILD_DIR)/endefdefine Build/Compile$(MAKE) -C "$(LINUX_DIR)" \\$(MAKE_OPTS) \\modulesendef$(eval $(call KernelPackage,myleds))
如果不清楚为什么这样编写 Makefile 文件,请查看前面章节的相关介绍。
5 编译驱动程序接下来,我们就来配置编译驱动程序。首先将 myleds 文件夹传到 OpenWrt 源码的package/kernel 目录下。然后进入 OpenWrt 源码的顶层目录,执行 make menuconfig。
$ make menuconfig
在弹出的菜单界面里,首先进入 Kernel modules 选项。
Kernel modules --- >Other modules --- >< * > kmod-myleds
图2
然后我们就能看到我们的驱动程序的选项了 kmod-myleds,将它配置成M,也可以便已进入内核。最后,退出保存。然后再执行编译命令。
make V=99编译完成以后,就能在源码目录下的bin/ramips/packages/base目录下看到我们的内核模块驱动程序的软件包 kmod-myleds_3.18.109-1_ramips_24kec.ipk。
19.6 动态的加载和卸载内核驱动模块软件包通过前面的努力,我们终于得到了我们自己的内核驱动模块软件包了。接下来就来使用它。首先将 kmod-myleds_3.18.109-1_ramips_24kec.ipk 软件包传到开发板上面。然后使用 opkg install 命令来安装软件包。
opkg install kmod-myleds_3.18.109-1_ramips_24kec.ipk使用 opkg list 命令来查看已经安装了哪些软件包。这里是否执行该命令都无所谓。接下来进入我们的/lib/modules/3.18.109目录,就能看到我们的驱动程序模块了。
cd lib/modules/3.18.109/接下来就通过 insmod 命令来装载驱动模块。
insmod myleds.ko驱动安装成功以后,我们就能看到 GPIO 驱动对应的设备节点了。
审核编辑:汤梓红