本系列文章所编写的驱动源码仓库,欢迎Star~
https://github.com/Mculover666/linux_driver_study。
一、按键原理图
正点原子alpha开发板板载了两个按键,一个复位按键,一个用户按键,用户按键原理图如下:
按键KEY0连接到UART1_CTS引脚,并有上拉电阻。
二、在设备树中添加节点
1. 设置引脚功能及电气属性
找到 iomuxc 节点,添加按键引脚复用:
pinctrl_key0: key0grp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
>;
};
2. 添加key0节点
在根节点下添加key0节点:
//08-key-irq实验, 用于自己编写的KEY驱动
key0 {
compatible = "atk, gpio-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key0>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
};
3. 检查PIN是否被使用
设备树中搜索 UART1_CTS_B
, 未找到。
4. 编译设备树
make dtbs
使用新的设备树重新启动:
三、编写按键驱动
1. 编写模块
static int __init key_module_init(void)
{
return 0;
}
static void __exit key_module_exit(void)
{
}
module_init(key_module_init);
module_exit(key_module_exit);
MODULE_AUTHOR("Mculover666");
MODULE_LICENSE("GPL");
编写Makefile:
KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m = key.o
build: kernel_module
kernel_module:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
编译成功,继续下一步。
2. 编写平台设备驱动框架
引入头文件:
编写平台设备驱动框架:
static int gpio_key_probe(struct platform_device *pdev)
{
return 0;
}
static int gpio_key_remove(struct platform_device *pdev)
{
return 0;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "atk, gpio-key" },
{ },
};
static struct platform_driver gpio_key_device_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.owner = THIS_MODULE,
.name = "gpio-key",
.of_match_table = of_match_ptr(gpio_key_of_match),
}
};
static int __init key_module_init(void)
{
return platform_driver_register(&gpio_key_device_driver);
}
static void __exit key_module_exit(void)
{
platform_driver_unregister(&gpio_key_device_driver);
}
3. 编写字符设备驱动框架
引入头文件:
封装全局变量:
struct key_dev {
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
};
static struct key_dev key;
编写字符设备驱动框架:
static int key_open(struct inode *inode, struct file *fp)
{
return 0;
}
static int key_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int key_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int key_release(struct inode *inode, struct file *fp)
{
return 0;
}
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
};
static int gpio_key_probe(struct platform_device *pdev)
{
int ret;
//分配cdev设备号
ret = alloc_chrdev_region(&key.dev, 0, 1, "key");
if (ret != 0) {
printk("alloc_chrdev_region fail!");
return -1;
}
//初始化cdev
key.cdev = cdev_alloc();
if (!key.cdev) {
printk("cdev_alloc fail!");
return -1;
}
//设置fop操作函数
key.cdev->owner = THIS_MODULE;
key.cdev->ops = &key_fops;
//注册cdev
cdev_add(key.cdev, key.dev, 1);
// 创建设备类
key.class = class_create(THIS_MODULE, "key_class");
if (!key.class) {
printk("class_create fail!");
return -1;
}
//创建设备节点
key.device0 = device_create(key.class, NULL, key.dev, NULL, "key0");
if (IS_ERR(key.device0)) {
printk("device_create device0 fail!");
return -1;
}
return 0;
}
static int gpio_key_remove(struct platform_device *pdev)
{
// 将设备从内核删除
cdev_del(key.cdev);
// 释放设备号
unregister_chrdev_region(key.dev, 1);
// 删除设备节点
device_destroy(key.class, key.dev);
// 删除设备类
class_destroy(key.class);
}
4. 按键初始化与去初始化
引入头文件:
在全局变量中添加中断号:
struct key_dev {
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
int irq; /*!< 中断号 */
};
编写按键初始化函数,完成以下事情:
- 从设备树中解析到gpio
- 设置gpio引脚为输入
- 请求中断
static int key_init(void)
{
int ret;
// 获取设备树节点
key.node = of_find_node_by_name(NULL, "key0");
if (!key.node) {
printk("key0 node find fail!\n");
return -1;
}
// 提取gpio
key.gpio = of_get_named_gpio(key.node, "key-gpio", 0);
if (key.gpio < 0) {
printk("find key-gpio propname fail!\n");
return -1;
}
// 初始化gpio
ret = gpio_request(key.gpio, "key-gpio");
if (ret < 0) {
printk("gpio request fail!\n");
return -1;
}
gpio_direction_input(key.gpio);
// 获取中断号
key.irq = gpio_to_irq(key.gpio);
if (key.irq < 0) {
printk("gpio_to_irq fail!\n");
gpio_free(key.gpio);
return -1;
}
// 申请中断
ret = request_irq(key.irq, key0_handler, IRQF_TRIGGER_FALLING, "key_irq", &key);
if (ret < 0) {
printk("irq request fail, ret is %d!\n", ret);
gpio_free(key.gpio);
return -1;
}
return 0;
}
中断函数如下:
static irqreturn_t key0_handler(int irq, void *dev_id)
{
int val;
struct key_dev *dev = (struct key_dev *)dev_id;
val = gpio_get_value(dev->gpio);
printk("key press on gpio %d, val is %d!\n", dev->gpio, val);
return IRQ_RETVAL(IRQ_HANDLED);
}
编写按键去初始化函数:
static void key_deinit(void)
{
// 释放中断
free_irq(key.irq, &key);
// 释放gpio
gpio_free(key.gpio);
}
在 probe 函数中调用按键初始化函数:
ret = key_init();
if (ret < 0) {
printk("key init fail!\n");
return -1;
}
在 remove 函数中调用按键去初始化函数:
// 按键去初始化
key_deinit();
5. 测试
编译驱动模块,加载:
按下开发板按键,可以看到驱动模块打印的数据:
按键中断是完成了,但是其中还有部分抖动情况,要添加去抖功能。
四、GPIO去抖
1. GPIO子系统自带的去抖功能
GPIO子系统自带去抖功能,API如下:
/**
* gpiod_set_debounce - sets @debounce time for a @gpio
* @gpio: the gpio to set debounce time
* @debounce: debounce time is microseconds
*
* returns -ENOTSUPP if the controller does not support setting
* debounce.
*/
int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce)
第二个值 debounce 是去抖时长,单位us。
2. 设备树节点中添加去抖时长值
在设备树中添加 debounce-interval 属性,设置去抖时长,单位ms:
//08-key-irq实验, 用于自己编写的KEY驱动
key0 {
compatible = "atk, gpio-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key0>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
debounce-interval = 16;
status = "okay";
};
3. 驱动添加对去抖的支持
优化按键初始化函数,解析设备树给出的去抖时长,并通过gpio子系统设置该值:
// 解析设备树,获取去抖时长
if (of_property_read_u32(key.node, "debounce-interval", &key.debounce_interval)) {
key.debounce_interval = 10; // default
}
// 设置去抖时长
if (key.debounce_interval) {
ret = gpio_set_debounce(key.gpio, key.debounce_interval * 1000);
if (ret < 0) {
printk("gpio_set_debounce fail, ret is %d!\n", ret);
}
}
4. 测试结果
果然,返回结果表示不支持设置去抖时长,只能用软件定时器消抖。
五、给应用传递键值——原子变量
1. 原子变量
在头文件include/linux/types.h
中定义:
typedef struct {
int counter;
} atomic_t;
原子变量的操作,针对每个架构都不同,ARM架构的在 arch/arm/include/asm/atomic.h
中。
(1)初始化原子变量
(2)读取或设置原子变量的值
/*
* On ARM, ordinary assignment (str instruction) doesn't clear the local
* strex/ldrex monitor on some implementations. The reason we can use it for
* atomic_set() is the clrex or dummy strex done on every exception return.
*/
2. 设置键值
在全局变量中添加键值原子变量,再添加一个标志位原子变量,用于表示按键按下:
struct key_dev {
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
int debounce_interval; /*!< 去抖时长 */
atomic_t keyval; /*!< 键值 */
atomic_t press; /*!< 标志按键是否按下 */
int irq; /*!< 中断号 */
};
在按键初始化函数中初始化键值:
// 初始化键值与标志位
atomic_set(&key.keyval, 0xFF);
atomic_set(&key.press, 0);
定义KEY0键值:
在按键中断函数中设置键值:
static irqreturn_t key0_handler(int irq, void *dev_id)
{
int val;
struct key_dev *dev = (struct key_dev *)dev_id;
val = gpio_get_value(dev->gpio);
if (val == 0) {
atomic_set(&dev->keyval, KEY0_VALUE);
atomic_set(&key.press, 1);
} else {
atomic_set(&dev->keyval, 0xFF); // 无效键值
}
return IRQ_RETVAL(IRQ_HANDLED);
}
3. 传递键值
引入头文件:
编写驱动:
static int key_open(struct inode *inode, struct file *fp)
{
fp->private_data = &key;
return 0;
}
static int key_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
int ret;
int keyval, press;
struct key_dev *dev = (struct key_dev *)fp->private_data;
press = atomic_read(&dev->press);
keyval = atomic_read(&dev->keyval);
if (press == 1 && keyval != 0xFF) {
atomic_set(&key.press, 0);
ret = copy_to_user(buf, &keyval, sizeof(keyval));
return 0;
}
return -1;
}
static int key_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int key_release(struct inode *inode, struct file *fp)
{
fp->private_data = NULL;
return 0;
}
4. 编写应用程序
应用程序使用死循环读取键值:
int main(int argc, char *argv[])
{
int fd;
int ret;
int keyval;
// 检查参数
if (argc != 2) {
printf("usage: ./test_key [device]\n");
return -1;
}
fd = open(argv[1], O_RDWR);
while (1) {
ret = read(fd, &keyval, sizeof(keyval));
if (ret < 0) {
// 键值无效
} else {
if (keyval == 0x01) {
printf("key 0 press!\n");
}
}
}
}
编译:
arm-linux-gnueabihf-gcc test_key.c -o test_key
5. 测试
找到设备节点:
运行测试程序:
./test_key /dev/key0
六、使用软件定时器进行消抖
参考:i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例。
1. 优化驱动
引入头文件:
添加全局变量:
struct key_dev {
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
int debounce_interval; /*!< 去抖时长 */
atomic_t keyval; /*!< 键值 */
atomic_t press; /*!< 标志按键是否按下 */
struct timer_list timer; /*!< 用于软件消抖 */
int irq; /*!< 中断号 */
};
修改按键中断处理函数,如果使用软件定时器消抖,则在中断中重启定时器:
static irqreturn_t key0_handler(int irq, void *dev_id)
{
int val;
struct key_dev *dev = (struct key_dev *)dev_id;
if (dev->debounce_interval) {
// 启动软件定时器, 消抖时间后再去读取
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->debounce_interval));
dev->timer.data = (volatile long)dev_id;
} else {
// 立即读取
val = gpio_get_value(dev->gpio);
if (val == 0) {
atomic_set(&dev->keyval, KEY0_VALUE);
atomic_set(&key.press, 1);
} else {
atomic_set(&dev->keyval, 0xFF);
}
}
return IRQ_RETVAL(IRQ_HANDLED);
}
编写定时器中断处理函数:
static void timer_handler(unsigned long arg)
{
int val;
struct key_dev *dev = (struct key_dev *)arg;
val = gpio_get_value(dev->gpio);
if (val == 0) {
atomic_set(&dev->keyval, KEY0_VALUE);
atomic_set(&key.press, 1);
} else {
atomic_set(&dev->keyval, 0xFF);
}
}
在按键初始化函数中,首先判断设备树有没有描述消抖时长,如果有则优先通过gpio子系统设置,gpio子系统不支持再使用软件定时器消抖:
// 设置去抖时长
if (debounce_interval) {
ret = gpio_set_debounce(key.gpio, key.debounce_interval * 1000);
if (ret < 0) {
printk("gpio_set_debounce not support, use soft timer!\n");
// 设置软件定时器用于消抖
init_timer(&key.timer);
key.timer.function = timer_handler;
key.debounce_interval = debounce_interval;
}
}
在按键去初始化函数中,将软件定时器从内核删除:
// 删除定时器
del_timer(&key.timer);
优化完成,重新编译。
2. 测试
加载驱动模块:
运行: