本次实验基于总线设备驱动模型实现按键驱动程序的编写,给上层应用程序提供检测按键是否按下的操作接口,上层应用根据按键是否按下控制led的亮灭。所以上层应用程序会同时使用led和按键的驱动接口,但是对于下层驱动而言,这二者是分离的,因此只需要专注于编写按键驱动程序就可以了。
在正点原子imx6ull开发板上按键原理图如下:
按键KEY0接到的引脚名字是uart1_cts,查找芯片参考手册第28章(GPIO章节),找到下面这个表
从表中找到GPIO1_IO18这一项,写着UART1_CTS_B,跟原理图上引脚名一致,说明这是按键Key0对应的引脚。我们要读取按键是否按下,因此我们需要把UART1_CTS_B这个引脚也即GPIO1_IO18设置为GPIO功能(GPIO功能代指普通的输入输出功能)。与led驱动程序的步骤类似,我们需要查看GPIO章节中有关引脚的原理图,其涉及到IOMUXC控制器、CMM时钟控制器,以及相关的GPIO控制器。
(1)CCM时钟控制器
在CCM控制器章节找到CCGR1寄存器的CG13位域控制GPIO1模块的时钟使能,接着在CCM章节的寄存器映射小节找到寄存器地址(20C_406Ch)及各位域的配置方式
(2)IOMUC复用控制器
在IOMUXC控制器章节找到复用寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B(20E_008Ch)
(2)GPIO控制器
需要将GPIO设置为输入功能,因此涉及GDIR和PSR寄存器,其中GDIR将GPIO配置为输入,PSR寄存器用于读取引脚输入状态
在GPIO章节找到上表可以查看到GPIO1相关的寄存器
这里插入话题GPIO是什么,为什么叫GPIO:
GPIO全称是General Purpose Input / Output 通用目的输入输出。既然是通用目的(General Purpose),那说明它可以用于不同的功能,比如有些引脚输出能力强,可以作为输出,这时候它就被用作最一般的输出功能,但是并不意味着它只能做普通输入输出引脚,它也可以复用为其它功能(具体查看芯片手册)。又比如本次实验中按键接到的引脚名字叫UART1_CTS,说明它的主要功能是用于UART串口,但是不代表不能用于输入,本实验就用它作为按键输入。芯片就是通过输入/输出数据来提供功能的,所有引脚都是输入/输出数据,也因此才叫IO,但是同一个引脚可能可以被复用为多种功能,比如UART,PWM,SPI,因此它是通用的,也即General Purpose,所以合起来才叫GPIO。更细致地分类解释的话,所有的引脚都具有输入/输出功能,它们都属于GPIO,但是如果某一个引脚只是单纯的用于输入/输出,比如LED的引脚GPIO1_IO03,这个引脚具有较强的输出能力,一般就用于输出,并且这个引脚属于GPIO1组的第3个IO引脚,那么这个引脚就命名为GPIO1_IO03;再比如按键Key0的引脚一般情况下输出能力较弱,会作为UART串口功能,那么这个引脚就命名UART1_CTS,但是它实际上是GPIO1组的第18个IO引脚,因此它是GPIO1_IO18,但是命名为UART1_CTS。
分析完硬件和寄存器部分后,开始写驱动代码,分为三部分:设备树、下层驱动代码,上层驱动框架
1、设备树
在根节点下添加如下节点
点击查看代码
mykey0 {
compatible = "key0_driver";
pin = <GROUP_PIN(1, 18)>;
pin_reg = <
0x20C406C /*CCM_CCGR1*/
0x20E008C /*IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B*/
0x209C004 /*GPIO1_GDIR*/
0x209C008 /*GPIO1_PSR*/
>;
};
2、包含platform_driver结构体的代码
这部分就是下层涉及具体硬件的操作函数,除了填充platform_driver结构体的内容外,还要向上层提供具体设备的操作函数,比如硬件的初始化、控制,同时在probe函数中调用上层接口为每一个匹配的设备注册文件视图。具体的代码如下:
头文件button_rsc.h
点击查看代码
#ifndef __BUTTON__RSC__H
#define __BUTTON__RSC__H
#define GROUP_PIN(g, p) ((g << 16) | (p)) //转换成引脚编号
#define PIN(x) (x & 0xffff) //获取引脚属于哪一个
#define GROUP(x) ((x >> 16) & 0xffff) //获取引脚属于哪一组
struct button_resource{
int group_pin; //[31:16]存放组,[15:0]存放
//定义变量存放涉及到的寄存器的地址
unsigned int CCM_CCGR1;
unsigned int IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
unsigned int GDIR;
unsigned int DR;
unsigned int PSR;
};
struct button_registers_after_map{
unsigned int* CCM_CCGR1;
unsigned int* IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
unsigned int* GDIR;
unsigned int* DR;
unsigned int* PSR;
};
#endif
头文件button_opr.h
点击查看代码
#ifndef __BUTTON_OPR__H
#define __BUTTON_OPR__H
struct button_operations{
int (*init) (unsigned int which); //初始化引脚,which表示引脚编号
int (*read) (unsigned int which); //读取引脚电平状态,which表示引脚编号
};
#endif
头文件button_drv.h
点击查看代码
#ifndef __BUTTON_DRV_H__
#define __BUTTON_DRV_H__
#include "button_opr.h" //如果在board_button.c中button_drv头文件的包含在button_opr前面的话,这里就需要先包含button_opr.h,否则会有警告,因为struct button_operations定义在button_opr.h
void button_device_register(int minor);
void button_device_unregister(int minor);
void register_button_operations(struct button_operations *button_opr);
#endif
board_button.c文件
点击查看代码
/*
下层按键驱动程序,实现具体的硬件操作
(1)实现按键GPIO引脚的操作函数,包括初始化函数、读取按键状态函数,通过结构体指针形式向上层注册
(2)实现platform_driver结构体,从设备树生成的platform_device获取寄存器资源,并提供匹配和卸载时的probe和remove函数,还有其他属性
(3)实现驱动入口/出口函数
*/
#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/delay.h"
#include "linux/ioport.h"
#include "linux/mod_devicetable.h"
#include "linux/of.h"
#include "linux/platform_device.h"
#include "asm/io.h"
#include <linux/delay.h>
#include "button_rsc.h"
#include "button_drv.h"
#include "button_opr.h"
int cur_counts = 0;
struct button_resource g_buttons[10];
struct button_registers_after_map g_buttons_map_addr[10];
/*
构造操作函数结构体,为上层驱动框架提供实体操作函数
*/
int button_init(unsigned int which) //初始化引脚,which表示引脚编号
{
/*根据which初始化GPIO,其中which是次设备号,也是g_buttons中保存资源结构体的下标*/
unsigned int val;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//地址映射
if(!g_buttons_map_addr[which].CCM_CCGR1)
{
g_buttons_map_addr[which].CCM_CCGR1 = ioremap(g_buttons[which].CCM_CCGR1, 4);
g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B = ioremap(g_buttons[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B, 4);
g_buttons_map_addr[which].GDIR = ioremap(g_buttons[which].GDIR, 4);
g_buttons_map_addr[which].PSR = ioremap(g_buttons[which].PSR, 4);
}
//使能时钟
val = *g_buttons_map_addr[which].CCM_CCGR1;
val |= (0x3 << 26);
*(g_buttons_map_addr[which].CCM_CCGR1) = val;
/*
配置GPIO1_IO18为GPIO模式
*/
val = *g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
val &= (~0xf); //低四位先清零
val |= (0x5); //设置为GPIO模式
*(g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B) = val;
/*
设置GPIO1_IO18为输出
*/
*(g_buttons_map_addr[which].GDIR) &= ~(0x1 << 18);
return 0;
}
int button_read(unsigned int which) //读取引脚电平状态,which表示引脚编号
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(!(*(g_buttons_map_addr[which].PSR) & (1 << 18)))
{
//udelay(10000); //延时10ms 参数太大,udelay不允许,改为mdelay
mdelay(10);
if(!(*(g_buttons_map_addr[which].PSR) & (1 << 18)))
return 0;
}
return 1;
}
struct button_operations button_oprs = {
.init = button_init,
.read = button_read,
};
int board_button_probe(struct platform_device *pdev)
{
struct device_node *np;
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//获取device_node结构体
np = pdev->dev.of_node;
if(!np)
{
printk("device_node error\n");
return -1;
}
//获取寄存器信息
err = of_property_read_u32(np, "pin", &g_buttons[cur_counts].group_pin);
err = of_property_read_u32_index(np, "pin_reg", 0, &g_buttons[cur_counts].CCM_CCGR1);
err = of_property_read_u32_index(np, "pin_reg", 1, &g_buttons[cur_counts].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B);
err = of_property_read_u32_index(np, "pin_reg", 2, &g_buttons[cur_counts].GDIR);
err = of_property_read_u32_index(np, "pin_reg", 3, &g_buttons[cur_counts].PSR);
//创建设备节点,使用上层驱动提供的函数
button_device_register(cur_counts);
//节点数加1
cur_counts++;
return 0;
}
int board_button_remove(struct platform_device *pdev)
{
int i;
int err;
struct device_node* np;
int button_pin;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//获取device_node结构体
np = pdev->dev.of_node;
err = of_property_read_u32(np, "pin", &button_pin);
for(i = 0; i < cur_counts; i++)
{
if(g_buttons[i].group_pin == button_pin)
{
button_device_unregister(i);
g_buttons[i].group_pin = -1;
break;
}
}
for(i = 0; i < cur_counts; i++)
{
if(g_buttons[i].group_pin != -1)
{
break;
}
}
if(i == cur_counts)
{
cur_counts = 0;
}
return 0;
}
static struct of_device_id mykeys[] = {
{
.compatible = "key0_driver"
},
{},
};
struct platform_driver button_drv = {
.probe = board_button_probe,
.remove = board_button_remove,
.driver = {
.name = "key",
.of_match_table = mykeys,
},
};
static int __init button_driver_init(void)
{
int err;
err = platform_driver_register(&button_drv); //注册平台驱动
//调用上层接口注册、向上层驱动提供操作函数
register_button_operations(&button_oprs);
return 0;
}
static void __exit button_driver_exit(void)
{
platform_driver_unregister(&button_drv);
}
module_init(button_driver_init);
module_exit(button_driver_exit);
MODULE_LICENSE("GPL");
3、上层驱动框架
点击查看代码
/*
按键驱动程序上层框架,主要实现:
(1)file_operations结构体内容实现
(2)添加驱动入口/出口函数,在入口函数注册驱动,在出口函数撤销驱动
(3)提供下层驱动向上层注册的接口:创建设备节点、删除设备节点、下层向上层注册按键相关的操作函数
*/
#include "asm/memory.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.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 <linux/printk.h>
#include <linux/init.h>
#include <asm/io.h>
#include "button_opr.h"
int major; //主设备号
struct class* button_class; //设备类
struct button_operations* p_button_opr;
void button_device_register(int minor)
{
device_create(button_class, NULL, MKDEV(major, minor), NULL, "key_%d", minor);
}
void button_device_unregister(int minor)
{
device_destroy(button_class, MKDEV(major, minor));
}
void register_button_operations(struct button_operations* button_opr)
{
p_button_opr = button_opr;
}
EXPORT_SYMBOL(button_device_register);
EXPORT_SYMBOL(button_device_unregister);
EXPORT_SYMBOL(register_button_operations);
int button_open(struct inode * node, struct file * file)
{
int which;
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
which = iminor(node);
p_button_opr->init(which);
return 0;
};
ssize_t button_read (struct file *file, char __user *user, size_t count, loff_t *start)
{
struct inode *node;
unsigned int which;
int ret;
int err;
// printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
node = file_inode(file);
which = iminor(node);
ret = p_button_opr->read(which);
err = copy_to_user(user, &ret, 1);
return ret;
};
int button_close (struct inode *node, struct file *file)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
struct file_operations button_opr = {
.owner = THIS_MODULE,
.open = button_open,
.read = button_read,
.release = button_close
};
static int __init button_init(void)
{
/*
驱动入口函数
(1)注册驱动;(2)创建类
*/
int err;
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(major, "button_driver", &button_opr);
button_class = class_create(THIS_MODULE, "button_class");
err = PTR_ERR(button_class);
if (IS_ERR(button_class))
{
unregister_chrdev(major, "button_driver");
return -1;
}
return 0;
}
static void __exit button_exit(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
class_destroy(button_class);
unregister_chrdev(major, "button_driver");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
4、应用程序,按键按下灯亮,再按下灯灭,如此反复
点击查看代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char** argv)
{
int fd_button, fd_led;
char val;
char state;
/*判断参数*/
if(argc != 3)
{
printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);
}
fd_button = open(argv[1], O_RDWR);
fd_led = open(argv[2], O_RDWR);
if(fd_button == -1 || fd_led == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
/*读文件, 控制led灯*/
state = 0;
write(fd_led, &state, 1);
while(1)
{
read(fd_button, &val, 1);
if(val == 0)
{
usleep(10000);
read(fd_button, &val, 1);
if(val == 0)
{
state = ~state;
}
write(fd_led, &state, 1);
// printf("led state: %d", state);
sleep(1);
}
}
close(fd_button);
close(fd_led);
return 0;
}
至此,按键驱动程序完成。本次驱动的编写与led驱动程序类似,可以看作是一次巩固。
使用按键控制led亮灭看似很简单的功能,但是以此为基础探究Linux的知识却可以很深入。比如使用中断方式判断按键是否按下、灯和按键是两个事件,可以引申出进程线程,进而基于此实现进程线程的通信、同步,消息机制、互斥等等。而且任何嵌入式设备功能的开发,本质都是操作IO操作寄存器,和点灯、按键没有本质的区别,只不过框架稍微复杂一些,关键在linux的那些知识。
接下来要深入学习pinctrl和gpio子系统,这部分在嵌入式linux开发中使用率很高,属于提供工作效率的工具,应该要掌握,预计花费1-2个月的时间学习掌握。