引言和具体的问题描述
上一篇博文:https://blog.csdn.net/wenhao_ir/article/details/145225508
中写的读取按键值的程序,如果按键按得很快,会出现前面的按键值被后面的按键值被覆盖的情况,即前面的按键值还没被来得及被读取,后面的按键值又来了的情况,这种情况在很多应用中是不被允许的,具体情况如下:
如果我以很快的速度连续按了两次KEY1键,而我的应用程序的功能是统计KEY1按了多少次,那很有可能统计的结果是只按了一次,而不是两次。
具体分析如下:
/* 实现文件操作结构体中的read函数 */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
int err;
wait_event_interruptible(gpio_key_wait, g_key);
err = copy_to_user(buf, &g_key, 4);
g_key = 0;
// 返回值为4表明读到了4字节的数据
return 4;
}
......
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
// 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
val = gpiod_get_value(gpio_key->gpiod);
// 打印中断号、GPIO引脚编号和电平值
// printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);
// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
g_key = (gpio_key->gpio << 8) | val;
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED; // 表示中断已处理
}
第一次按下KEY1键后,中断程序在g_key中记录下按键值,然后代码wake_up_interruptible(&gpio_key_wait);
唤醒处于睡眠状态的队列,读取按钮值的线程被唤醒后,检查到g_key变量的值为非零值,所以准备继续执行后面的代码,比如就准备执行代码err = copy_to_user(buf, &g_key, 4);
,但这句代码还没来得执行,又来了一次按键中断,即中断处理函数被再次调用,此时全局变量g_key
的值被更新,虽然都是KEY1的值,具体的值没变,但这里也可理解为更新了g_key
的值,最关键的是中断处理程序执行到wake_up_interruptible(&gpio_key_wait);
时,咱们读取数据的线程并没有处于睡眠状态,所以相当于代码wake_up_interruptible(&gpio_key_wait);
对咱们读取数据的线程并无任何影响,读取数据线的线程会按照自己的节奏继续执行代码err = copy_to_user(buf, &g_key, 4);
,并在执行完毕时把g_key
的值置为零,由于用户空间中一直在循环调用read()函数,所以很快这个线程在运行到wait_event_interruptible(gpio_key_wait, g_key);
时会因为g_key
的值为零而进入休眠状态。
我们可以看到,虽然我们两了KEY1键两次,但是代码err = copy_to_user(buf, &g_key, 4);
只运行了一次,最终如果我们用户空间中程序如果是统计按了KEY1键几次,则只会被统计出1次,而不是2次,这种情况在很多应用中是不被允许的,所以我们要改进程序,改进的方法就是利用环形缓冲区。
环形缓冲区之前之前已经在同时读取多个输入设备的数据时用到过了,详情见 https://blog.csdn.net/wenhao_ir/article/details/144691229
本篇博文中我们继续利用环形缓冲区来改进我们的按键读取程序。
完整源代码
驱动程序gpio_key_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 <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;
static struct gpio_key *gpio_keys_100ask;
/* 主设备号 */
static int major = 0;
static struct class *gpio_key_class;
static int g_key = 0;
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void)
{
return (r == w);
}
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
static void put_key(int key_value)
{
if (!is_key_buf_full())
{
g_keys[w] = key_value;
w = NEXT_POS(w);
}
}
static int get_key(void)
{
int key_value = 0;
if (!is_key_buf_empty())
{
key_value = g_keys[r];
r = NEXT_POS(r);
}
return key_value;
}
/* 实现文件操作结构体中的read函数 */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
int err;
int key_value;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
//从缓形缓冲区中取出数据
key_value = get_key();
err = copy_to_user(buf, &key_value, 4);
// 返回值为4表明读到了4字节的数据
return 4;
}
/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
// 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
val = gpiod_get_value(gpio_key->gpiod);
// 打印中断号、GPIO引脚编号和电平值
// printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);
// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
g_key = (gpio_key->gpio << 8) | val;
//装按键值放入环形缓冲区
put_key(g_key);
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED; // 表示中断已处理
}
/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
// 获取设备树节点指针
struct device_node *node = pdev->dev.of_node;
// count用于存储设备树中描述的GPIO口的数量
int count;
int i;
enum of_gpio_flags flag;
unsigned flags = GPIOF_IN;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
count = of_gpio_count(node);
if (!count)
{
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
if (!gpio_keys_100ask) {
printk("Memory allocation failed for gpio_keys_100ask\n");
return -ENOMEM;
}
for (i = 0; i < count; i++)
{
// 获取GIPO的全局编号及其标志位信息的代码
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0)
{
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
// 获取GPIO口的GPIO描述符的代码
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
if (!gpio_keys_100ask[i].gpiod) {
printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
return -EINVAL;
}
// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。
// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
// 每次循环都重新初始化flags
flags = GPIOF_IN;
// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息
if (flag & OF_GPIO_ACTIVE_LOW)
flags |= GPIOF_ACTIVE_LOW;
// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定
// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定
// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。
err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
// 获取GPIO口的中断请求号
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}
for (i = 0; i < count; i++)
{
char irq_name[32]; // 用于存储动态生成的中断名称
//使用snprintf()函数将动态生成的中断名称写入irq_name数组
snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i); // 根据i生成名称
//调用函数request_irq()来请求并设置一个中断
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
}
/* 注册file_operations */
major = register_chrdev(0, "swh_read_keys_major", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "swh_read_keys_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "swh_read_keys_major");
return PTR_ERR(gpio_key_class);
}
// 由于这里是把多个按键看成是一个设备,你可以想像一个键盘上对应多个按键,但键盘本身是一个设备,所以只有一个设备文件
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "read_keys0"); /* /dev/read_keys0 */
return 0;
}
static int gpio_key_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "swh_read_keys_major");
count = of_gpio_count(node);
for (i = 0; i < count; i++)
{
// 只有在irq有效时才释放中断资源
if (gpio_keys_100ask[i].irq >= 0) {
// 释放GPIO中断资源,下面这句代码做了下面两件事:
// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。
// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
// 释放GPIO描述符
if (gpio_keys_100ask[i].gpiod) {
gpiod_put(gpio_keys_100ask[i].gpiod);
}
}
// 释放内存
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id irq_matach_table[] = {
{ .compatible = "swh-gpio_irq_key" },
{ },
};
/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "swh_irq_platform_dirver",
.of_match_table = irq_matach_table,
},
};
/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&gpio_keys_driver);
return err;
}
/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
* 卸载platform_driver
*/
static void __exit gpio_key_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_keys_driver);
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
测试程序button_test.c
中的代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <time.h>
/*
* ./button_test /dev/100ask_button0
*
*/
// 打印线程的执行函数
void* print_while_waiting(void* arg)
{
while (1)
{
printf("I am another thread, and while the main thread is waiting for a button to be pressed, I can still run normally.\n");
sleep(10); // 每隔10秒打印一次
}
return NULL;
}
int main(int argc, char **argv)
{
int fd;
int val;
pthread_t print_thread;
int keystroke = 0; //记录按键次数
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("Can not open file %s\n", argv[1]);
return -1;
}
// 创建一个线程,每隔一段时间打印输出一条信息表示在等待按键期间,另外的线程在继续正常执行。
if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0)
{
printf("Failed to create print thread\n");
close(fd);
return -1;
}
while (1)
{
/* 3. 读文件 */
read(fd, &val, 4);
/* 提取 GPIO 编号和逻辑值 */
int gpio_number = (val >> 8) & 0xFF; // 高8位为 GPIO 编号
int gpio_value = val & 0xFF; // 低8位为逻辑值
keystroke++;
/* 打印读到的信息 */
printf("GPIO Number: %d, Logical Value: %d\n", gpio_number, gpio_value);
printf("keystrokes is %d\n", keystroke);
}
//pthread_join的作用是使主线程等待线程print_threa结束后再继续执行剩下的代码。
//如果主线程在结束时未等待子线程完成,可能会导致未完成的资源清理或意外的程序终止。
//这里由于主线程中有个条件永远为真的while循环,实际上这句代码没有实际作用。
pthread_join(print_thread, NULL);
close(fd);
return 0;
}
加入的环形缓冲区的代码分析
/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void)
{
return (r == w);
}
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
static void put_key(int key_value)
{
if (!is_key_buf_full())
{
g_keys[w] = key_value;
w = NEXT_POS(w);
}
}
static int get_key(void)
{
int key_value = 0;
if (!is_key_buf_empty())
{
key_value = g_keys[r];
r = NEXT_POS(r);
}
return key_value;
}
.....
环形缓冲区(Ring Buffer)是一个循环队列,用于存储和管理键值数据。在该代码中,环形缓冲区的实现用于存储按键值,并通过生产者-消费者模型协调按键中断处理程序和用户空间的读取操作。以下是环形缓冲区相关代码的详细解释:
1. 缓冲区定义
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
BUF_LEN
:缓冲区的长度(128)。g_keys
:一个整数数组,用作环形缓冲区。r
:读取指针,指向下一个可以读取的位置。w
:写入指针,指向下一个可以写入的位置。
2. 辅助宏和函数
2.1 计算下一个位置
#define NEXT_POS(x) ((x+1) % BUF_LEN)
NEXT_POS(x)
:计算当前索引的下一个位置,通过对缓冲区长度取模实现环形结构。- 示例:如果当前索引为127,
NEXT_POS(127)
将返回0,实现循环。
2.2 判断缓冲区状态
static int is_key_buf_empty(void)
{
return (r == w);
}
- 功能:检查缓冲区是否为空。
- 逻辑:当读指针
r
与写指针w
相等时,缓冲区为空。
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
- 功能:检查缓冲区是否已满。
- 逻辑:当写指针的下一个位置等于读指针时,缓冲区为满。
2.3 写入键值
static void put_key(int key_value)
{
if (!is_key_buf_full())
{
g_keys[w] = key_value;
w = NEXT_POS(w);
}
}
- 功能:将按键值写入缓冲区。
- 逻辑:
- 检查缓冲区是否已满。如果满了,则丢弃新值。
- 如果未满,将按键值写入当前写指针指向的位置。
- 更新写指针到下一个位置。
2.4 读取键值
static int get_key(void)
{
int key_value = 0;
if (!is_key_buf_empty())
{
key_value = g_keys[r];
r = NEXT_POS(r);
}
return key_value;
}
- 功能:从缓冲区中读取按键值。
- 逻辑:
- 检查缓冲区是否为空。如果为空,则返回默认值(0)。
- 如果不为空,读取读指针位置的值。
- 更新读指针到下一个位置。
3. 应用场景
3.1 中断处理程序:gpio_key_isr
- 当按键中断触发时,调用
put_key
将按键值存入缓冲区。 - 如果缓冲区未满,按键值会被存储;否则,按键值会被丢弃。
3.2 用户态读取:gpio_key_drv_read
- 用户态调用
read
系统调用时,驱动会调用get_key
从缓冲区读取一个按键值。 - 如果缓冲区为空,则使用
wait_event_interruptible
让当前进程进入睡眠状态,直到有新按键值写入缓冲区。
4. 环形缓冲区的优点
- 高效利用缓冲区:通过循环结构实现固定大小缓冲区的最大利用率。
- 避免数据溢出:通过
is_key_buf_full
和is_key_buf_empty
控制写入和读取。
总结来说,该环形缓冲区用于在中断上下文中临时存储按键值,避免中断丢失按键事件,同时支持用户空间从缓冲区中异步读取按键值。
设备树文件的修改和更新
和上篇博文 https://blog.csdn.net/wenhao_ir/article/details/145225508 和上上篇博文 https://blog.csdn.net/wenhao_ir/article/details/145176361 没有任何区别,所以这里略。
Makfile文件内容
# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
# 因为测试程序中有线程的创建,所以下面的语句需要添加 -lpthread 链接选项
$(CROSS_COMPILE)gcc -o button_test_02 button_test.c -lpthread
clean:
make -C $(KERN_DIR) M=`pwd` clean
rm -rf modules.order
rm -f button_test_02
obj-m += gpio_key_drv.o
交叉编译出驱动模块和测试程序
源码复制到Ubuntu中。
make
将交叉编译出的gpio_key_drv.ko
和button_test_01
复制到NFS文件目录中,备用。
加载模块
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/read_key_irq_circle_buffer/gpio_key_drv.ko
检查设备文件生成没有
ls /dev/
有了:
运行测试程序
cd /mnt/read_key_irq_circle_buffer
./button_test_02 /dev/read_keys0
很快的速度连续按下按键KEY110次【自己心里默数出多次】,运行结果如下:
可见打印输出的信息也是10次,所以大概应该是没有问题,因为这里极有可能我的手速比不过程序运行的速度,所以其实是测试不出来的,所以我说大概应该是没有问题的。
卸载驱动程序模块
由于程序与卸载有关的代码与上篇博文中的代码相比没有任何改变,所以这里略。
附完整工程文件
https://pan.baidu.com/s/1JB3kTozDJsiPAPMrQdQhpQ?pwd=i3mg