안녕하세요 여러분, 매일 간단한 드라이버입니다. 시간이 지남에 따라 Linux 드라이버에 점점 더 익숙해지고 드라이버 프로그램 작성을 배우는 것이 점점 더 쉬워질 것입니다. 오늘은 간단한 버튼드라이브를 해보겠습니다.
1. Linux에서의 버튼 구동 원리
버튼 드라이버와 LED 드라이버의 원리는 기본적으로 동일하며, 둘 다 GPIO를 구동하지만, 하나는 GPIO의 High와 Low 레벨을 읽고, 다른 하나는 GPIO에서 High와 Low 레벨을 출력하는 것입니다. 이번에는 키 입력을 구현하기 위해 드라이버에서 정수 변수를 사용하여 키 값을 표현하는데, 애플리케이션은 읽기 기능을 통해 키 값을 읽어 키가 눌렸는지 확인합니다.
여기서 키 값을 저장하는 변수는 공유 리소스이므로 드라이버는 여기에 키 값을 써야 하고, 애플리케이션은 키 값을 읽어야 합니다. 따라서 이를 보호해야 합니다. 정수 변수의 경우 첫 번째 선택은 원자 연산입니다. 원자 연산을 사용하여 변수를 할당하고 읽습니다. Linux에서 키 드라이버의 원리는 매우 간단합니다.다음으로 드라이버 작성을 시작합니다.
이 장의 루틴은 Linux에서 GPIO 입력 드라이버 작성을 보여주기 위한 것입니다. 실제 버튼 드라이버는 이 장에서 설명하는 방법을 사용하지 않습니다. Linux의 입력 하위 시스템은 입력 장치에 특별히 사용됩니다.
2. 하드웨어 도식 분석
위 그림에서 볼 수 있듯이 KEY0 버튼은 I.MX6U의 UART1_CTS IO에 연결되어 있고 KEY0은 10K 풀업 저항에 연결되어 있으므로 KEY0을 누르지 않았을 때 UART1_CTS는 하이 레벨이 되어야 한다. 누르면 UART1_CTS가 로우 레벨이 됩니다.
3. 개발 환경
-
CPU : IMX6ULL
-
커널 버전: Linux-5.19
4. 장치 트리 파일 수정
1. pinctrl 노드 추가
I.MX6U-ALPHA 개발 보드의 KEY는 PIN UART1_CTS_B를 사용합니다. imx6ul-14x14-evk.dtsi를 열고 iomuxc 노드의 imx6ul-evk 하위 노드 아래에 "pinctrl_key"라는 하위 노드를 만듭니다. 노드 내용 다음과 같습니다.
pinctrl_key: keygrp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 /* key0 */
>;
};
3행에서는 PIN GPIO_IO18을 GPIO1_IO18로 재사용합니다.
2. KEY 장치 노드 추가
루트 노드 "/" 아래에 KEY 노드를 생성합니다. 노드 이름은 "key"이고 노드 내용은 다음과 같습니다.
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "imx6ull-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
};
-
6행에서 pinctrl-0 속성은 KEY에서 사용하는 PIN에 해당하는 pinctrl 노드를 설정합니다.
-
7행에서 key-gpio 속성은 KEY에서 사용하는 GPIO를 지정합니다.
3. 해당 PIN을 다른 주변기기에서 사용하고 있는지 확인하세요.
본 실험에서 버튼이 사용하는 PIN은 UART1_CTS_B이므로 먼저 PIN UART1_CTS_B가 다른 pinctrl 노드에서 사용되는지 확인하고, 사용한다면 차단해야 하며, GPIO GPIO1_IO18이 다른 주변기기에서 사용되는지 확인한다. 있는 경우에도 차단되어야 합니다.
장치 트리가 작성된 후 "make dtbs" 명령을 사용하여 장치 트리를 다시 컴파일한 다음 새로 컴파일된 imx6ull-toto.dtb 파일을 사용하여 Linux 시스템을 시작합니다. 시동 성공 후 "/proc/device-tree" 디렉토리에 들어가 "key" 노드가 존재하는지 확인합니다. 존재한다면 디바이스 트리가 기본적으로 성공적으로 수정되었음을 의미합니다. (구체적으로 드라이버 검증이 필요합니다.) .결과는 다음과 같습니다.
/ # ls /proc/device-tree/
#address-cells clock-osc pmu
#size-cells compatible regulator-can-3v3
aliases cpus regulator-peri-3v3
backlight-display dts_led regulator-sd1-vmmc
beep key soc
chosen memory@80000000 sound-wm8960
clock-cli model spi4
clock-di0 name timer
clock-di1 panel
5. 버튼 드라이버 작성
장치 트리가 준비되면 드라이버를 작성할 수 있습니다. key.c에 다음 내용을 입력하십시오.
/************************************************************
* Copyright © toto Co., Ltd. 1998-2029. All rights reserved.
* Description:
* Version: 1.0
* Autor: toto
* Date: Do not edit
* LastEditors: Seven
* LastEditTime: Do not edit
************************************************************/
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#define KEY_CNT 1 /* 设备号数量 */
#define KEY_NAME "key" /* 设备名字 */
/*定义按键值*/
#define KEY0_VALUE 0xF0 /* 按键值 */
#define INVAKEY 0x00 /* 无效按键值 */
/* key 设备结构体 */
struct key_dev
{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int key_gpio; /* key 所使用的GPIO编号 */
atomic_t keyvalue; /* 按键值 */
};
struct key_dev keydev; /* key 设备 */
/*
* @Brief 初始化按键使用的GPIO管脚
* @Call Internal or External
* @Param
* @Note NOne
* @RetVal 无
*/
int keyio_init(void)
{
keydev.nd = of_find_node_by_path("/key");
if(keydev.nd == NULL)
{
return -EINVAL;
}
keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
if(keydev.key_gpio < 0)
{
printk("can't get key-gpio\n");
return -EINVAL;
}
printk("key_gpio:%d\n", keydev.key_gpio);
/* c初始化 key 使用的IO */
gpio_request(keydev.key_gpio, "key0"); /* 请求IO */
gpio_direction_input(keydev.key_gpio); /* 设置为输入 */
return 0;
}
/*
* @Brief 打开设备
* @Call Internal or External
* @Param inode:
* @Param filp:设备文件
* @Note NOne
* @RetVal 0:成功 其他值:失败
*/
static int beep_open(struct inode *inode, struct file *filp)
{
int ret = 0;
/* 设置私有数据 */
filp->private_data = &keydev;
/* 初始化按键IO */
ret = keyio_init();
if(ret < 0)
{
return ret;
}
return 0;
}
/*
* @Brief 从设备读数据
* @Call Internal or External
* @Param filp:要打开的设备文件描述符
* @Param buf:返回给用户空间的数据地址
* @Param cnt:要读取的数据长度
* @Param offt:相对于文件首地址的偏移
* @Note NOne
* @RetVal 读取的字节数,若为负值,表示读失败
*/
static ssize_t beep_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char value;
struct key_dev *dev = filp->private_data;
/* key0 按下 */
if(gpio_get_value(dev->key_gpio) == 0)
{
/* 等待按键释放 */
while(!gpio_get_value(dev->key_gpio));
atomic_set(&dev->keyvalue, KEY0_VALUE);
}
else
{
/* 无效的按键值 */
atomic_set(&dev->keyvalue, INVAKEY);
}
/* 保存按键值 */
value = atomic_read(&dev->keyvalue);
ret = copy_to_user(buf, &value, sizeof(value));
return ret;
}
/*
* @Brief 写数据到设备
* @Call Internal or External
* @Param filp:要打开的设备文件描述符
* @Param buf:要写入设备的数据地址
* @Param cnt:要写入的数据长度
* @Param offt:相对于文件首地址的偏移
* @Note NOne
* @RetVal 写入的字节数,若为负值,表示写失败
*/
static ssize_t beep_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @Brief 关闭/释放设备
* @Call Internal or External
* @Param inode:
* @Param filp:要关闭的设备文件描述符
* @Note NOne
* @RetVal NOne
*/
static int beep_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = beep_open,
.read = beep_read,
.write = beep_write,
.release = beep_release,
};
/*
* @Brief 驱动入口函数
* @Call Internal or External
* @Param None
* @Note NOne
* @RetVal NOne
*/
static int __init mykey_init(void)
{
/* 初始化原子变量 */
atomic_set(&keydev.keyvalue, INVAKEY);
/* 注册字符设备驱动 */
/* 创建设备号 */
if(keydev.major) /* 定义了设备号 */
{
keydev.devid = MKDEV(keydev.major, 0);
register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
}
else
{
/* 申请设备号 */
alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);
/* 获取主设备号 */
keydev.major = MAJOR(keydev.devid);
/* 获取次设备号 */
keydev.minor = MINOR(keydev.devid);
}
printk("%s new_chrdev major:%d minor:%d\n", __func__,
keydev.major, keydev.minor);
/* 初始化cdev */
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);
/* 添加一个cdev */
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
/* 创建类 */
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if(IS_ERR(keydev.class))
{
return PTR_ERR(keydev.class);
}
/* 创建设备 */
keydev.device = device_create(keydev.class, NULL,
keydev.devid, NULL, KEY_NAME);
if(IS_ERR(keydev.device))
{
return PTR_ERR(keydev.device);
}
return 0;
}
/*
* @Brief 驱动出口函数
* @Call Internal or External
* @Param None
* @Note NOne
* @RetVal NOne
*/
static void __exit mykey_exit(void)
{
/* 注销字符设备驱动 */
gpio_free(keydev.key_gpio);
/* 注销字符设备 */
/* 删除cdev */
cdev_del(&keydev.cdev);
/* 释放分配的设备号 */
unregister_chrdev_region(keydev.devid, KEY_CNT);
device_destroy(keydev.class, keydev.devid);
class_destroy(keydev.class);
}
module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("toto");
6. 테스트 앱 작성
key_app.c 테스트 프로그램의 구체적인 코드는 다음과 같습니다.
/********************************************
*Description:
*Version: 1.0
*Autor: toto
*Date: Do not edit
*LastEditors: Seven
*LastEditTime: Do not edit
********************************************/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define KEY0VALUE 0xF0
#define INVAKEY 0x00
/*
* @Brief main 主程序
* @Call Internal or External
* @Param argc:
* @Param argv:
* @Note NOne
* @RetVal 0-成功;其他-失败
*/
int main(int argc, char *argv[])
{
int fd, retval;
char *filename;
unsigned char keyvalue;
if(argc != 2)
{
printf("argc != 2\n");
return -1;
}
filename = argv[1];
/*打开驱动文件*/
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("open filename:%d failed\n", filename);
return -1;
}
/* 要执行的操作:打开或关闭 */
while(1)
{
read(fd, &keyvalue, sizeof(keyvalue));
if(keyvalue == KEY0VALUE)
{
/* 按下 */
printf("KEY0 down, value:0x%x\n", keyvalue);
}
}
/*关闭文件*/
close(fd);
return 0;
}
7. 작동 확인
개발 보드의 전원을 켜고 key.ko 및 key_app 두 파일을 /lib/modules/5.19.0-g794a2f7be62d-dirty/ 디렉터리에 복사한 후 다음 명령을 입력하여 key.ko 드라이버 모듈을 로드합니다.
/ # insmod /lib/modules/5.19.0-g794a2f7be62d-dirty/key.ko
[ 108.608375] key_driver: loading out-of-tree module taints kernel.
[ 108.619185] mykey_init new_chrdev major:242 minor:0
드라이버가 성공적으로 로드된 후 다음 명령을 사용하여 테스트할 수 있습니다.
./key_app /dev/key
개발 보드에서 KEY0 버튼을 누르면 key_app이 아래와 같이 키 정보를 획득하고 출력합니다.
/home/app # ./key_app /dev/key
[ 155.043576] key_gpio:18
KEY0 down, value:0xf0
KEY0 down, value:0xf0
KEY0 down, value:0xf0
KEY0 down, value:0xf0
KEY0 down, value:0xf0
KEY0 down, value:0xf0
위에서 볼 수 있듯이 KEY0을 누르면 "KEY0 down, value = 0XF0"이 인쇄되어 키가 눌렸음을 나타냅니다. 가끔 KEY0을 한 번 누르면 "KEY0 down, value = 0XF0"이라는 여러 줄이 출력되는 경우가 있는데, 이는 우리 코드가 키 디바운스 처리를 수행하지 않기 때문입니다. 드라이버를 제거하려면 다음 명령을 입력하십시오.
rmmod key.ko