38.Linux INPUT 子系统实验
按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux 内核为此专门做了一个叫做 input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了 input 框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input 核心层负责处理这些事件。
38.1 input 子系统
比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息
图 38.1.1.1 中左边就是最底层的具体设备,比如按键、USB 键盘/鼠标等,中间部分属于Linux 内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出 input 子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
事件层:主要和用户空间进行交互。
- input 子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_device 即可。
input_dev 注册过程示例代码如下所示:
1 struct input_dev *inputdev; /* input 结构体变量 */
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6 ......
7 inputdev = input_allocate_device(); /* 申请 input_dev */
8 inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */
9
10 /*********第一种设置事件和事件值的方法***********/
11 __set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
12 __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
13 __set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
14 /************************************************/
15
16 /*********第二种设置事件和事件值的方法***********/
17 keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
BIT_MASK(EV_REP);
18 keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |=
BIT_MASK(KEY_0);
19 /************************************************/
20
21 /*********第三种设置事件和事件值的方法***********/
22 keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
BIT_MASK(EV_REP);
23 input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
24 /************************************************/
25
26 /* 注册 input_dev */
27 input_register_device(inputdev);
28 ......
29 return 0;
30 }
31
32 /* 驱动出口函数 */
33 static void __exit xxx_exit(void)
34 {
35 input_unregister_device(inputdev); /* 注销 input_dev */
36 input_free_device(inputdev); /* 删除 input_dev */
37 }
第 1 行,定义一个 input_dev 结构体指针变量。
第 4~30 行,驱动入口函数,在此函数中完成 input_dev 的申请、设置、注册等工作。第 7行调用 input_allocate_device 函数申请一个 input_dev。第 10~23 行都是设置 input 设备事件和按键值,这里用了三种方法来设置事件和按键值。第 27 行调用 input_register_device 函数向 Linux内核注册 inputdev。
第 33~37 行,驱动出口函数,第 35 行调用 input_unregister_device 函数注销前面注册的input_dev,第 36 行调用 input_free_device 函数删除前面申请的 input_dev。
按键的上报事件的参考代码如下所示:
示例代码 38.1.2.7 事件上报参考代码
1 /* 用于按键消抖的定时器服务函数 */
2 void timer_function(unsigned long arg)
3 {
4 unsigned char value;
5
6 value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
7 if(value == 0){ /* 按下按键 */
8 /* 上报按键值 */
9 input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1,按下 */
10 input_sync(inputdev); /* 同步事件 */
11 } else { /* 按键松开 */
12 input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0,松开 */
13 input_sync(inputdev); /* 同步事件 */
14 }
15 }
第 6 行,获取按键值,判断按键是否按下。
第 9~10 行,如果按键值为 0 那么表示按键被按下了,如果按键按下的话就要使用input_report_key 函数向 Linux 系统上报按键值,比如向 Linux 系统通知 KEY_0 这个按键按下了。
第 12~13 行,如果按键值为 1 的话就表示按键没有按下,是松开的。向 Linux 系统通知KEY_0 这个按键没有按下或松开了。
38.2实验程序编写
- 创建按键所使用的引脚 pinctrl 子节点
首先在 stm32mp15-pinctrl.dtsi 文件中根节点创建按键对应的 pinctrl 子节点:
key_pins_a: key_pins-0 {
pins1 {
pinmux = <STM32_PINMUX('G', 3, GPIO)>, /* KEY0 */
<STM32_PINMUX('H', 7, GPIO)>; /* KEY1 */
bias-pull-up;
slew-rate = <0>;
};
pins2 {
pinmux = <STM32_PINMUX('A', 0, GPIO)>; /* WK_UP */
bias-pull-down;
slew-rate = <0>;
};
};
这里我们将 STM32MP1 开发板上的三个按键:KEY0、KEY1 和 WK_UP 都设置了,虽然本小节我们只使用到 KEY0。由于 KEY0 和 KEY1 这两个按键是低电平有效(按下以后为低电平),WK_UP 是高电平有效(按下以后为高电平)。所以这里分开初始化,其中第 2-7 行初始化KEY0 和 KEY1 使用的 PG3 和 PH7 这两个引脚,第 9~13 行初始化 WK_UP 使用的 PA0 引脚。
- 创建按键所使用的引脚 pinctrl 子节点
接下就是创建按键设备节点,我们在stm32mp157d-atk.dts根节点创建的 key 节点,修改完成以后如下所示:
key {
compatible = "alientek,key";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&key_pins_a>;
key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpiog>;
interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
//前面两行等价于interrupts-extended = <&gpiog 3 IRQ_TYPE_EDGE_BOTH>;
};
- 按键驱动程序keyinput.c
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#define KEYINPUT_NAME "keyinput" /* 名字 */
/* key设备结构体 */
struct key_dev{
struct input_dev *idev; /* 按键对应的input_dev指针 */
struct timer_list timer; /* 消抖定时器 */
int gpio_key; /* 按键对应的GPIO编号 */
int irq_key; /* 按键对应的中断号 */
};
static struct key_dev key; /* 按键设备 */
/*
* @description : 按键中断服务函数
* @param – irq : 触发该中断事件对应的中断号
* @param – arg : arg参数可以在申请中断的时候进行配置
* @return : 中断执行结果
*/
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
if(key.irq_key != irq)
return IRQ_NONE;
/* 按键防抖处理,开启定时器延时15ms */
disable_irq_nosync(irq); /* 禁止按键中断 */
mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
/*
* @description : 按键初始化函数
* @param – nd : device_node设备指针
* @return : 成功返回0,失败返回负数
*/
static int key_gpio_init(struct device_node *nd)
{
int ret;
unsigned long irq_flags;
/* 从设备树中获取GPIO */
key.gpio_key = of_get_named_gpio(nd, "key-gpio", 0);
if(!gpio_is_valid(key.gpio_key)) {
printk("key:Failed to get key-gpio\n");
return -EINVAL;
}
/* 申请使用GPIO */
ret = gpio_request(key.gpio_key, "KEY0");
if (ret) {
printk(KERN_ERR "key: Failed to request key-gpio\n");
return ret;
}
/* 将GPIO设置为输入模式 */
gpio_direction_input(key.gpio_key);
/* 获取GPIO对应的中断号 */
key.irq_key = irq_of_parse_and_map(nd, 0);
if(!key.irq_key){
return -EINVAL;
}
/* 获取设备树中指定的中断触发类型 */
irq_flags = irq_get_trigger_type(key.irq_key);
if (IRQF_TRIGGER_NONE == irq_flags)
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
/* 申请中断 */
ret = request_irq(key.irq_key, key_interrupt, irq_flags, "Key0_IRQ", NULL);
if (ret) {
gpio_free(key.gpio_key);
return ret;
}
return 0;
}
/*
* @description : 定时器服务函数,用于按键消抖,定时时间到了以后
* 再读取按键值,根据按键的状态上报相应的事件
* @param – arg : arg参数就是定时器的结构体
* @return : 无
*/
static void key_timer_function(struct timer_list *arg)
{
int val;
/* 读取按键值并上报按键事件 */
val = gpio_get_value(key.gpio_key);
input_report_key(key.idev, KEY_0, !val);
input_sync(key.idev);
enable_irq(key.irq_key);
}
/*
* @description : platform驱动的probe函数,当驱动与设备
* 匹配成功以后此函数会被执行
* @param – pdev : platform设备指针
* @return : 0,成功;其他负值,失败
*/
static int atk_key_probe(struct platform_device *pdev)
{
int ret;
/* 初始化GPIO */
ret = key_gpio_init(pdev->dev.of_node);
if(ret < 0)
return ret;
/* 初始化定时器 */
timer_setup(&key.timer, key_timer_function, 0);
/* 申请input_dev */
key.idev = input_allocate_device();
key.idev->name = KEYINPUT_NAME;
#if 0
/* 初始化input_dev,设置产生哪些事件 */
__set_bit(EV_KEY, key.idev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, key.idev->evbit); /* 重复事件,比如按下去不放开,就会一直输出信息 */
/* 初始化input_dev,设置产生哪些按键 */
__set_bit(KEY_0, key.idev->keybit);
#endif
#if 0
key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
key.idev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
#endif
key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
input_set_capability(key.idev, EV_KEY, KEY_0);
/* 注册输入设备 */
ret = input_register_device(key.idev);
if (ret) {
printk("register input device failed!\r\n");
goto free_gpio;
}
return 0;
free_gpio:
free_irq(key.irq_key,NULL);
gpio_free(key.gpio_key);
del_timer_sync(&key.timer);
return -EIO;
}
/*
* @description : platform驱动的remove函数,当platform驱动模块
* 卸载时此函数会被执行
* @param – dev : platform设备指针
* @return : 0,成功;其他负值,失败
*/
static int atk_key_remove(struct platform_device *pdev)
{
free_irq(key.irq_key,NULL); /* 释放中断号 */
gpio_free(key.gpio_key); /* 释放GPIO */
del_timer_sync(&key.timer); /* 删除timer */
input_unregister_device(key.idev); /* 释放input_dev */
return 0;
}
static const struct of_device_id key_of_match[] = {
{.compatible = "alientek,key"},
{/* Sentinel */}
};
static struct platform_driver atk_key_driver = {
.driver = {
.name = "stm32mp1-key",
.of_match_table = key_of_match,
},
.probe = atk_key_probe,
.remove = atk_key_remove,
};
module_platform_driver(atk_key_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
第 14~19 行,自定义的按键设备结构体 struct key_dev,用于描述一个按键设备,其中的成员变量包括一个 input_dev 指针变量,定时器 timer、GPIO 以及中断号。
第 29~39 行,按键中断处理函数 key_interrupt,当按键按下或松开的时候都会触发,也就是 上 升 沿 和 下 降 沿 都 会 触 发 此 中 断 。 key_interrupt 函 数 中 的 操 作 也 很 简 单 , 调 用disable_irq_nosync 函数先禁止中断,然后使用 mod_timer 打开定时器,定时时长为 15ms。
第 46~87 行,按键的 GPIO 初始化。
第 95~105 行,定时器服务函数 key_timer_function,使用到定时器的目的主要是为了使用软件的方式进行按键消抖处理,在key_timer_function 函数中,我们使用 gpio_get_value 获取按键 GPIO 的电平状态,使用 input_report_key 函数上报按键事件。在 val 变量前加了一个取反,因为按键按下的情况下,获取到的 val 为 0,按键松开的情况下,获取到的 val 为 1。但是 input子系统框架规定按键按下上报 1,按键松开上报 0,所以这里需要进行取反操作。事件上报完成之后使用 input_sync 函数同步事件,表示此事件已上报完成,input 子系统核心层就会进行相关的处理。第 104 行 enable_irq 函数使能中断,因为在按键中断发生的时候我们会关闭中断,等事件处理完成之后再打开。
第 113-160 行,platform 驱动的 probe 函数 atk_key_probe,其中第 136~161 行,使用input_allocate_device 函数申请 input_dev,然后设置相应的事件以及事件码(也就是 KEY 模拟成那个按键,这里我们设置为 KEY_0)。最后使用 input_register_device 函数向 Linux 内核注册 input_dev。
第 168~176 行,platform 驱动的 remove 函数 mykey_remove,在该函数中先释放 GPIO 在使用 del_timer_sync 删除定时器并且调用 input_unregister_device 卸载按键设备。
- 测试 APP, keyinputApp.c
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : keyinputApp.c
作者 : 正点原子Linux团队
版本 : V1.0
描述 : input子系统测试APP。
其他 : 无
使用方法 :./keyinputApp /dev/input/event1
论坛 : www.openedv.com
日志 : 初版V1.0 2021/02/2 正点原子Linux团队创建
***************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>
/*
* @description : main主程序
* @param – argc : argv数组元素个数
* @param – argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, ret;
struct input_event ev;
if(2 != argc) {
printf("Usage:\n"
"\t./keyinputApp /dev/input/eventX @ Open Key\n"
);
return -1;
}
/* 打开设备 */
fd = open(argv[1], O_RDWR);
if(0 > fd) {
printf("Error: file %s open failed!\r\n", argv[1]);
return -1;
}
/* 读取按键数据 */
for ( ; ; ) {
ret = read(fd, &ev, sizeof(struct input_event));
if (ret) {
switch (ev.type) {
case EV_KEY: // 按键事件
if (KEY_0 == ev.code) { // 判断是不是KEY_0按键
if (ev.value) // 按键按下
printf("Key0 Press\n");
else // 按键松开
printf("Key0 Release\n");
}
break;
/* 其他类型的事件,自行处理 */
case EV_REL:
break;
case EV_ABS:
break;
case EV_MSC:
break;
case EV_SW:
break;
};
}
else {
printf("Error: file %s read failed!\r\n", argv[1]);
goto out;
}
}
out:
/* 关闭设备 */
close(fd);
return 0;
}
第 38.1.3 小节已经说过了,Linux 内核会使用 input_event 结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于 input_event 结构体。第 19 行定义了一个 input_event类型变量 ev。
第 36~65 行,当我们向 Linux 内核成功注册 input_dev 设备以后,会在/dev/input 目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX 就是对应的 input 设备文件。我们读取这个文件就可以获取到输入事件信息,比如按键值什么的。使用 read 函数读取输入设备文件,也就是/dev/input/eventX,读取到的数据按照 input_event 结构体组织起来。获取到输入事件以后(input_event 结构体类型)使用 switch case 语句来判断事件类型,本章实验我们设置的事件类型为 EV_KEY,因此只需要处理 EV_KEY 事件即可。比如获取按键编号(KEY_0 的编号为11)、获取按键状态,按下还是松开的?
38.3运行测试
将上一小节编译出来 keyinput.ko 和 keyinputApp 这两个文件拷贝到 rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录 lib/modules/5.4.31 中。在加载 keyinput.ko 驱动模块之前,在/dev 目录下根本就不存在 input 文件夹,原因在于此时系统中并没有加载过 input 输入类设备,所以这个文件夹不存在。接下来输入如下命令加载 keyinput.ko这个驱动模块。
depmod //第一次加载驱动的时候需要运行此命令
modprobe keyinput.ko //加载驱动模块
当驱动模块加载成功以后再来看一下/dev/input 目录下有哪些文件,结果如图 38.4.2.2 所示:
从图可以看出,在/dev/input 目录下生成了一个 event0 文件,这其实就是我们注册的驱动所对应的设备文件。keyinputApp 就是通过读取/dev/input/event0 这个文件来获取输入事件信息的,输入如下测试命令:
./keyinputApp /dev/input/event0
然后按下开发板上的 KEY 按键,可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在 Linux 内核中 KEY_0 为 11。另外,我们也可以不用 keyinputApp 来测试驱动,可以直接使用 hexdump 命令来查看
/dev/input/event0 文件内容,输入如下命令:
图 38.4.2.4 就是 input_event 类型的原始事件数据值,采用十六进制表示,这些原始数据的
含义如下:
type 为事件类型,查看示例代码 38.1.2.3 可知,EV_KEY 事件值为 1,EV_SYN 事件值为0。因此第 1 行表示 EV_KEY 事件,第 2 行表示 EV_SYN 事件。code 为事件编码,也就是按键号,查看示例代码 38.1.2.4 可以,KEY_0 这个按键编号为 11,对应的十六进制为 0xb,因此第1 行表示 KEY_0 这个按键事件,最后的 value 就是按键值,为 1 表示按下,为 0 的话表示松开。
综上所述,示例代码 38.4.2.1 中的原始事件值含义如下:
第 1 行,按键(KEY_0)按下事件。
第 2 行,EV_SYN 同步事件,因为每次上报按键事件以后都要同步的上报一个 EV_SYN 事件。
第 3 行,按键(KEY_0)松开事件。
第 4 行,EV_SYN 同步事件,和第 2 行一样。
38.4 Linux 自带按键驱动程序的使用
Linux 内核也自带了 KEY 驱动,如果要使用内核自带的 KEY 驱动的话需要配置 Linux 内核,不过 Linux 内核一般默认已经使能了 KEY 驱动,但是我们还是要检查一下。按照如下路径找到相应的配置选项:
→ Device Drivers
→ Input device support
→ Generic input layer (needed for keyboard, mouse, …) (INPUT [=y])
→ Keyboards (INPUT_KEYBOARD [=y])
→GPIO Buttons
选中“GPIO Buttons”选项,将其编译进 Linux 内核中
Linux 内核自带的 KEY 驱动文件为drivers/input/keyboard/gpio_keys.c,gpio_keys.c 采用了 platform 驱动框架,在 KEY 驱动上使用了 input 子系统实现。在 gpio_keys.c 文件中找到如下所示内容:
758 static const struct of_device_id gpio_keys_of_match[] = {
759 { .compatible = "gpio-keys", },
760 { },
761 };
...
static struct platform_driver gpio_keys_device_driver = {
1013 .probe = gpio_keys_probe,
1014 .shutdown = gpio_keys_shutdown,
1015 .driver = {
1016 .name = "gpio-keys",
1017 .pm = &gpio_keys_pm_ops,
1018 .of_match_table = gpio_keys_of_match,
1019 .dev_groups = gpio_keys_groups,
1020 }
1021 };
1022
1023 static int __init gpio_keys_init(void)
1024 {
1025 return platform_driver_register(&gpio_keys_device_driver);
1026 }
1027
1028 static void __exit gpio_keys_exit(void)
1029 {
1030 platform_driver_unregister(&gpio_keys_device_driver);
1031 }
从示例代码 38.5.1.1 可以看出,这就是一个标准的 platform 驱动框架,如果要使用设备树来描述 KEY 设备信息的话,设备节点的 compatible 属性值要设置为“gpio-keys”。当设备和驱动匹配以后 gpio_keys_probe 函数就会执行
38.5 自带按键驱动程序的使用
要使用 Linux 内 核 自 带 的 按 键 驱 动 程 序 很 简 单 , 只 需 要 根 据Documentation/devicetree/bindings/input/gpio-keys.txt 这个文件在设备树中添加指定的设备节点即可,节点要求如下:
①、节点名字为“gpio-keys”。
②、gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。
③、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己:
gpios:KEY 所连接的 GPIO 信息。
interrupts:KEY 所使用 GPIO 中断信息,不是必须的,可以不写。
label:KEY 名字
linux,code:KEY 要模拟的按键,也就是示例代码 38.1.2.4 中的这些按键。
④、如果按键要支持连按的话要加入 autorepeat。这里我们将开发板上的三个按键都用起来,KEY0、KEY1 和 WKUP 分别模拟为键盘上的:L、S 和 Enter(回车)健。打开 stm32mp157d-atk.dts,先添加一个头文件“dt-bindings/input/input.h”此文件就是“linux,code”属性的按键宏定义,
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&key_pins_a>;
autorepeat;
key0 {
label = "GPIO Key L";
linux,code = <KEY_L>;
gpios = <&gpiog 3 GPIO_ACTIVE_LOW>;
};
key1 {
label = "GPIO Key S";
linux,code = <KEY_S>;
gpios = <&gpioh 7 GPIO_ACTIVE_LOW>;
};
wkup {
label = "GPIO Key Enter";
linux,code = <KEY_ENTER>;
gpios = <&gpioa 0 GPIO_ACTIVE_HIGH>;
gpio-key,wakeup;
};
};
第 5 行,autorepeat 表示按键支持连按。
第 7~11 行,STM32MP1 开发板 KEY0 按键信息,名字设置为“GPIO Key L”,这里我们将开发板上的 KEY0 按键设置为“EKY_L”这个按键,也就是‘L’键,效果和键盘上的‘L’键一样。
第 13~17 行,KEY1 按键设置,模拟键盘上的‘S’键。
第 19~24 行,WKUP 按键,模拟键盘上的‘回车’键,注意,WKUP 按键连接的 PA0 引脚,查看原理图可以知道,WKUP 按下去以后是高电平,因此这里设置高电平有效。最后一定要检查一下设备树看看这些引脚有没有被用到其他外设上,如果有的话要删除掉相关代码!
重新编译设备树,然后用新编译出来的 stm32mp157d-atk.dtb 启动 Linux 系统,系统启动以后查看/dev/input 目录,看看都有哪些文件, 可以看出存在 event0 这个文件,这个文件就是 KEY 对应的设备文件,使用hexdump 命令来查看/dev/input/event0 文件,输入如下命令:
hexdump /dev/input/event0然后按下 STM32MP1 开发板上的按键,终端输出图 38.5.2.2 所示内容:
如果按下 KEY 按键以后会在终端上输出图 38.5.2.2 所示的信息那么就表示 Linux 内核的按键驱动工作正常。至于图 38.5.2.2 中内容的含义大家就自行分析,这个已经在 38.4.2 小节详细的分析过了,这里就不再讲解了。
大家如果发现按下 KEY 按键以后没有反应,那么请检查一下三方面:
①、是否使能 Linux 内核 KEY 驱动。
②、设备树中 gpio-keys 节点是否创建成功。
③、在设备树中是否有其他外设也使用了 KEY 按键对应的 GPIO,但是我们并没有删除掉这些外设信息。检查 Linux 启动 log 信息,看看是否有类似下面这条信息:
gpio-keys gpio-keys: failed to get gpio: -16
上述信息表示 GPIO 申请失败,失败的原因就是有其他的外设正在使用此 GPIO。
39 LinuxPWM驱动实验
39.1设备树修改
PA10 可以作为 TIM1 的通道 3 的 PWM 输出引脚,所以我们需要在设备树里面添加 PA10的引脚信息以及 TIM1 通道 3 的 PWM 信息。
- 添加 PA10 引脚信息
打开 stm32mp15-pinctrl.dtsi 文件,在 iomuxc 节点下添加 GPIO1_IO04 的引脚信息,如下所示:
可以看出 ST 官方已经设置好了 TIM1 的 CH1、CH2 和 CH4 这三个通道的引脚配置,但是这里我们只需要 CH3,因此将示例代码 29.2.1.1 改成如下所示:
pwm1_pins_a: pwm1-0 {
pins {
pinmux = <STM32_PINMUX('A', 10, AF1)>; /* TIM1_CH3 */
bias-pull-down;
drive-push-pull;
slew-rate = <0>;
};
};
pwm1_sleep_pins_a: pwm1-sleep-0 {
pins {
pinmux = <STM32_PINMUX('A', 10, ANALOG)>; /* TIM1_CH3 */
};
};
示例代码 39.2.1.2 中仅仅将 PA10 复用为 TIM1 的 CH3,大家一定要根据自己所使用的板子硬件来配置引脚。
- 向 timers1 节点追加信息
前面已经讲过了,stm32mp151.dtsi 文件中已经有了“timers1”节点,但是这个节点默认是
disable 的,而且还不能直接使用。需要在 stm32mp157d-atk.dts 文件中向 timers1 节点追加一些
内容,在 stm32mp157d-atk.dts 文件中加入如下所示内容:
&timers1 {
status = "okay";
// spare all DMA channels since they are not needed for PWM output
/delete-property/dmas;
/delete-property/dma-names;
pwm1: pwm {
pinctrl-0 = <&pwm1_pins_a>;
pinctrl-1 = <&pwm1_sleep_pins_a>;
pinctrl-names = "default", "sleep";
#pwm-cells = <2>;
status = "okay";
};
};
第 4、5 行,关闭 DMA 功能,因为 PWM 输出不需要 DMA。
第 7 行,pinctrl-0 属性指定 TIM1 的 CH3 所使用的输出引脚对应的 pinctrl 节点,这里设置为示例代码 39.2.1.2 中的 pwm1_pins_a。
- 屏蔽掉其他复用的 IO
检查一下设备树中有没有其他外设用到 PA10,如果有的话需要屏蔽掉!注意,不能只屏蔽掉 PA10 的 pinctrl 配置信息,也要搜索一下“gpioa 10”,看看有没有哪里用到,用到的话也要屏蔽掉。
39.2使能pwm驱动
- ST 官方的 Linux 内核已经默认使能了 PWM 驱动,所以不需要我们修改,但是为了学习,我们还是需要知道怎么使能。打开 Linux 内核配置界面,按照如下路径找到配置项:
-> Device Drivers
-> Pulse-Width Modulation (PWM) Support
-> <*> STMicroelectronics STM32 PWM //选中
39.3 PWM 驱动测试
1、确定 TIM1 对应的 pwmchipX 文件
使用新的设备树启动系统,然后将开发板上的 PA10 引脚连接到示波器上,通过示波器来查看 PWM 波形图。我们可以直接在用户层来配置 PWM,进入目录/sys/class/pwm 中有个 pwmchip0,但是我们并不知道这个 pwmchip0 是否为 TIM1 对应的文件。我们可以通过查看 pwmchip0 对应的地址是否和 TIM1 定时器寄存器起始地址是否一致来确定其是否属于 TIM1。输入如下命令进入到 pwmchip0 目录:
cd pwmchip0 //进入
进入到 pwmchip0 目录以后会打印出其路劲,如图 39.3.2 所示:
从图 可以看出 pwmchip0 对应的定时器寄存器起始地址为 0X44000000,根据示例代码 39.1.1.1 中的 timers1 节点,可以知道 TIM1 这个定时器的寄存器起始地址就是 0X44000000。因此,pwmchip0 就是 TIM1 对应的文件。为什么要用这么复杂的方式来确定定时器对应的 pwmchip 文件呢?因为当 STM32MP157开启多个定时器的 PWM 功能以后,其 pwmchip 文件就会变! 这个在下一章讲解 LCD 的背光
调节实验中就会体现出来,LCD 背光使用的 TIM4_CH2,当同时开始 TIM1 和 TIM4 的 PWM功能以后,此时 pwmchip0 就不是 TIM1 了!而是 TIM4。按照我们的惯性思维,肯定会一直操作 pwmchip0 来控制 TIM1_CH3 这路 PWM,但是会发现一直提示设备忙,无法操作,这个笔者在做实验的时候被困了很久!
2、调出 pwmchip0 的 pwm2 子目录
pwmchip0 是整个 TIM1 的总目录,而 TIM1 有 4 路 PWM,每路都可以独立打开或关闭。CH1-CH4 对应的编号为 0~3,因此打开 TIM1 的 CH3 输入如下命令:
echo 2 > /sys/class/pwm/pwmchip0/export
上述命令中 2 就是 TIM1_CH3,如果要打开 TIM1 的CH1,那就是 0。执行完成会在 pwmchip0目录下生成一个名为“pwm2”的子目录,如图 39.3.3 所示:
2、设置 PWM 的频率
注意,这里设置的是周期值,单位为 ns,比如 20KHz 频率的周期就是 50000ns,输入如下命令:
echo 50000 > /sys/class/pwm/pwmchip0/pwm2/period
3、设置 PWM 的占空比
这里不能直接设置占空比,而是设置的一个周期的 ON 时间,也就是高电平时间,比如20KHz 频率下 20%占空比的 ON 时间就是 10000,输入如下命令:
echo 10000 > /sys/class/pwm/pwmchip0/pwm2/duty_cycle
4、使能 TIM1 的通道 3
一定要先设置频率和波特率,最后在开启定时器,否则会提示参数错误!输入如下命令使能 TIM1 的通道 3 这路 PWM:
echo 1 > /sys/class/pwm/pwmchip0/pwm2/enable
设置完成使用示波器查看波形是否正确,此时 PWM 频率为 20KHz,占空比为 20%,与我们设置的一致。如果要修改频率或者占空比的话一定要注意这两者时间值,比如 20KHz 频率的周期值为 50000ns,那么你在调整占空比的时候 ON 时间就不能设置大于 50000,否则就会提示你参数无效。
5、极性反转
前面我们也可以修改 PWM 的极性,上面我们设置的 PWM 占空比为 20%,我们只需要修改极性就可以将占空比变为 80%。向/pwmchip0/pwm2/polarity 文件写入“inversed”即可反转极性,命令如下:
echo “inversed” > /sys/class/pwm/pwmchip0/pwm2/polarity
极性反转以后占空比就变为了 80%,如果要恢复回原来的极性,向/pwmchip0/pwm2/polarity文件写入“normal”即可,命令如下:
echo “normal” > /sys/class/pwm/pwmchip0/pwm2/polarity
40.LCD
40.5 LCD 驱动程序编写
40.5.1 修改设备树
首先就是修改设备树,重点要注意三个地方:
①、LCD 所使用的 IO 配置,由于 STM32MP1 的 IO 支持复用,所以大家实际所使用的 LCD引脚可能不一样。因此首先要根据实际硬件设计,修改 LCD 所有使用的 IO 配置。
②、LDTC 接口节点修改,修改相应的属性值,告诉内核要指定那个输出接口,比如输出到 RGB LCD 屏、MIPI 屏等。本实验用到是正点原子 ATK4380 屏,也就是 RGB LCD 屏。
③、输出接口节点的编写,比如本实验用到的 RGB LCD 屏需要在根节点下添加 RGB LCD节点。
④、LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。接下来我们依次来看一下上面这三个节点如何去修改。
1、LCD 屏幕使用的 IO 配置
首先要检查一下设备树中 LCD 所使用的 IO 配置,这个其实 ST 都已经给我们写好了,需要修改,不过我们还是要看一下。打开 arch/arm/boot/dts/stm32mp15-pinctrl.dtsi 文件,在 pinctrl节点中找到如下内容:
第 1~35 行,子节点 ltdc_pins_b,为 RGB LCD 的 24 根数据线配置项和 4 根控制线配置项。
第 37~67 行,子节点 ltdc_pins_sleep_b,同样也是 RGB LCD 的 24 根数据线配置项和 4 根控制线配置项。可以看出,这里有两个 pinmux 分别为:ltdc_pins_b 和 ltdc_pins_sleep_b,其中 ltdc_pins_b是在默认模式下的 RGB LCD pinmux 配置,ltdc_pins_sleep_b 是在 sleep 模式下 RGB LCD 的pinmux 配置。如果我们想要 LCD 屏进入睡眠模式就切换为 sleep 模式。正点原子 STM32MP1 开发板 RGB LCD 屏幕所使用的引脚和 ST 官方开发板一致,因此示例代码不需要做任何修改。如果你所使用的开发板其 LCD 引脚和正点原子 STM32MP1开发板不一致,一定要根据自己的实际硬件修改示例代码
2、LDTC 接口节点修改
LTDC 节点在 stm32mp151.dtsi 里已经写好一部分了,我们只需要告诉 LTDC 节点输出到RGB LCD 屏里就行。在 stm32mp157d-atk.dts 文件,添加如下内容所示:
<dc {
pinctrl-names = "default", "sleep";
pinctrl-0 = <<dc_pins_b>;
pinctrl-1 = <<dc_pins_sleep_b>;
status = "okay";
port {
#address-cells = <1>;
#size-cells = <0>;
ltdc_ep0_out: endpoint@0 {
reg = <0>;
remote-endpoint = <&rgb_panel_in>;
};
};
};
第 2~4 行,给 LTDC 设置了两个 pinmux 模式,pinctrl-0 为 default 模式,pinctrl-1 为 sleep模式,系统默认使用 default 模式。
第 8~9 行,设置 port 下子节点的 reg 属性的地址信息描述。
第 11~14 行,在 port 下添加了一个子节点为 ltdc_ep0_out。在第 12 行里,reg 属性值为 0。在第 13 行里,remote-endpoint 属性是用来告诉 ltdc 节点输出到那里,我们是用 RGB LCD 屏做实验,所以输出到 rgb_panel_in 接口。
3、输出接口的编写
我们还需要添加一个 LCD 设备树节点,在 stm32mp157d-atk.dts 文件的根节点“/”下添加如下所示内容:
panel_rgb: panel-rgb {
compatible = "alientek,lcd-rgb";
backlight = <&backlight>;
status = "okay";
port {
rgb_panel_in: endpoint {
remote-endpoint = <<dc_ep0_out>;
};
};
};
第 2 行,设置 compatible 属性值为“alientek,lcd-rgb”,所以我们稍后要在 panel-simple.c 文件里的 platform_of_match 数组增加一个 of_device_id 结构体,此结构体的 compatible 成员属性值为“alientek,lcd-rgb”。
第 3 行,设置 backlight 属性值为“&backlight”,此属性值为引用背光节点,稍后给大家讲解如何编写。
第 6~9 行,告诉 LCD 驱动,要从 LTDC 节点里获取显示数据。第 8 行就是引用 ltdc 节点。
40.5.2 在 panel-simple.c 文件里面添加屏幕参数
接着我们就要在panel-simple.c文件里面添加屏幕参数,打开此文件,找到platform_of_match数组,添加如下内容:
示例代码 40.5.2.1 屏的匹配属性
1 .compatible = “alientek,lcd-rgb”,
2 .data = &alientek_desc,
添加完成以后如图 40.5.2.1 所示:
在图 40.5.2.1 里的 alientek_desc 保存参数,我们还需要继续在 panel-simple.c 里面实现alientek_desc,添加如下所示代码:
static const struct drm_display_mode ATK7016_mode = {
.clock = 31000, /* LCD 像素时钟,单位 KHz */
.hdisplay = 800, /* LCD X 轴像素个数 */
.hsync_start = 800 + 88 , /* LCD X 轴+hbp 的像素个数 */
.hsync_end = 800 + 88 +48, /* LCD X 轴+hbp+hspw 的像素个数*/
.htotal = 800 + 88 +48 + 40,/* LCD X 轴+hbp+hspw+hfp */
.vdisplay = 480, /* LCD Y 轴像素个数 */
.vsync_start = 480 + 32, /* LCD Y 轴+vbp 的像素个数 */
.vsync_end = 480 + 32 + 3, /* LCD Y 轴+vbp+vspw 的像素个数 */
.vtotal = 480 + 32 + 3 +13,/* LCD Y 轴+vbp+vspw+vfp */
.vrefresh = 60, /* LCD 的刷新频率为 60HZ */
};
static const struct panel_desc alientek_desc = {
.modes = &ATK7016_mode,
.num_modes = 1,
.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
};
在示例代码 40.5.2.2 就是 ATK7016 屏的参数,并且设置为 RGB888 模式。如果使用的其他屏幕,请按照屏幕手册对应的时序参数设置:
第 2~11 行,ATK7016 屏幕时序参数,根据自己所使用的屏幕修改即可。
40.5.3 LCD 屏幕背光节点信息
1、背光 PWM 节点设置
LCD 背光使用 PWM 来控制,通过 PWM 波形来调节屏幕亮度。关于 PWM 已经在《第三十九章 Linux PWM 驱动实验》进行了详细的讲解。正点原子的 LCD 接口背光控制 IO 连接到了 STM32MP1 的 PD13 引脚上,我们需要将 PD13复用为 TIM4_CH2,然后配置 TIM4 的 CH2 输出 PWM 信号,然后通过此 PWM 信号来控制LCD 屏幕背光的亮度,接着我们来看一下如何在设备树中添加背光节点信息。首先是 PD13 这个 pinmux 的配置,在 stm32mp15-pinctrl.dtsi 中找到如下内容:
pwm4_pins_b: pwm4-1 {
pins {
pinmux = <STM32_PINMUX('D', 13, AF2)>; /* TIM4_CH2 */
bias-pull-down;
drive-push-pull;
slew-rate = <0>;
};
};
pwm4_sleep_pins_b: pwm4-sleep-1 {
pins {
pinmux = <STM32_PINMUX('D', 13, ANALOG)>; /* TIM4_CH2 */
};
};
示例代码 40.5.3.1 默认设置了 PD13 引脚的两种 pinmux 配置,从第 3 行可以看出,设置PD13 复用为 TIM4_CH2,TIM4_CH2 这里的意思为 TIM4 里的第二个 PWM 通道,并且设置电气属性为内部下拉和推挽输出。这是因为 ST 官方开发板就使用了 PD13 作为 LCD 的背光控制
引脚,正点原子 STM32MP1 开发板也使用 PD13 作为 LCD 背光控制引脚,所以不需要修改。如果你使用其他引脚作为 LCD 的背光控制引脚,那么就需要进行修改。继续在 stm32mp157d-atk.dts 文件中向 timers4 追加内容,如下所示:
&timers4 {
status = "okay";
/* spare dmas for other usage */
/delete-property/dmas;
/delete-property/dma-names;
pwm4: pwm {
pinctrl-0 = <&pwm4_pins_b>;
pinctrl-1 = <&pwm4_sleep_pins_b>;
pinctrl-names = "default", "sleep";
#pwm-cells = <2>;
status = "okay";
};
};
第 2 行,把 status 设置为 okay。
第 4~5 行,设置此节点不用 dma。
第 6 行,pwm4 是我们为 pwm 设置的一个别名。
第 7~9 行,设置 PWM 所使用的 IO 配置。
第 10 行,此参数是用来规定 pwms 属性的参数。比如:#pwm-cells =<2>,表示 pwms 属性有 2 个参数,如下所示:pwms= <&pwm4 1 5000000>其中 pwm4 表示使用 PWM4,后面两个是参数,其中 1 表示使用 PWM4 的通道 2(通道从 1开始);5000000 表示为 200Hz。
如果背光用的其他 pwm 通道,比如 pwm2,那么就需要仿照示例代码 40.5.3.2,向 timers2节点追加相应的内容。比如:如果我们要设置 TIM2_CH4,那么 timers2 里的 pwm 节点下的pinmux 配置就是 TIM2_CH4。
2、backlight 节点设置
到这里,PWM 和相关的 IO 已经准备好了,但是 Linux 系统怎么知道 TIM4_CH2 就是控制LCD 背光的呢?因此我们还需要一个节点来将 LCD 背光和 TIM4_CH2 连接起来。这个节点就是 backlight,backlight 节点描述可以参考 Documentation/devicetree/bindings/leds/backlight/pwmbacklight.txt 这个文档,此文档详细讲解了 backlight 节点该如何去创建,这里大概总结一下:
①、节点名称要为“backlight”。
②、节点的 compatible 属性值要为“pwm-backlight”,因此可以通过在 Linux 内核中搜索“ pwm-backlight ”来查找 PWM 背 光 控 制 驱 动 程 序 , 这 个 驱 动 程 序 文 件 为drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。
③、pwms 属性用于描述背光所使用的 PWM 的通道以及 PWM 频率,比如本章我们要使用的 pwm4 的第二个通道,pwm 频率设置为200Hz。
④、brightness-levels 属性描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就是亮度最低,255 表示 100%占空比,也就是亮度最高。至于设置几级亮度,大家可以自行填写此属性。
⑤、default-brightness-level 属性为默认亮度级别。根据上述 5 点设置 backlight 节点,我们在根节点创建一个 backlight 节点,在 stm32mp157datk.dts 文件中新建内容如下:
backlight: backlight {
compatible = "pwm-backlight";
pwms = <&pwm4 1 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
power-supply = <&v3v3>;
default-brightness-level = <7>;
status = "okay";
};
第 3 行,设置背光使用 pwm4 的第二个通道,PWM 频率为 200Hz。
第 4 行,设置 8 级背光(0~7),分别为 0、4、8、16、32、64、128、255,对应占空比为0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少的话可以自行添加一些其他的背光等级值。
第 5 行,设置默认背光等级为 7,也就是 100%的亮度。注意:背光的驱动代码有点 bug,如果设置 0 级屏不会灭屏,打开 pwm_bl.c 文件,找到 123行,如图 40.5.3.1 所示:
40.6 运行测试
40.6.1 LCD 屏幕的 DRM 基本测试
1、编译新的内核和设备树
输入如下命令重新编译 Linux 内核和设备树:
make uImage dtbs LOADADDR=0xC2000040 -j16
编译完成以后一会要使用新的设备树和内核启动 Linux 内核。
2、配置内核
ST 官方的默认配置已经使能了 DRM 驱动,还是要告诉各位如何配置内核,打开 Linux内核图形化配置界面,按下路径找到对应的配置项:
-> Device Drivers
-> Graphics support
[] Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) //选中
[] DRM Support for STMicroelectronics SoC Series //选中
-> Display Panels
[] support for simple panels //选中
-> Backlight & LCD device support
[] Generic PWM based Backlight Driver //选中
用新的 uImage 镜像和 stm32mp157d-atk.dtb 设备树用来启动内核,如果设置正确那么在文件系统/sys/class/drm/路径下,有如图 40.6.1.1 所示内容:
因为 Linux 一切皆文件,所以 DRM 驱动肯定会提供一个接口给用户使用,接口为“/dev/dri/card0”。可以通过此接口来设置 LCD 的显示。
2、文件系统使能 libdrm 库
在前面 40.2 小节里,没有 libdrm 库是不能调用 drm 驱动的,所以我们要在文件系统使能libdrm 库,跳转到 buildroot-2020.02.6 的目录下,打开 buildroot 的图形化配置界面,根据如下配置去使能 libdrm 库。
Location:
-> Target packages
-> Libraries
-> Graphics
->[*]libdrm //选中
->[*]Install test programs //选中
make编译生成rootfs.tar,图中的 Install test programs 配置会生成 modetest 命令,此命令是用来测试 DRM 驱动。保存并重新编译 buildroot 的文件系统,然后直接将新得到的根文件系统解压到开发板正在使用的根文件系统中,具体操作参考第19章节
3、测试
重新启动开发板,我们使用 modetest 命令进行测试,输入如下命令可以查看 modetest 命令的使用方法:modetest --help
先输入如下命令查看一下设备信息:
modetest -M stm
-M:指定模块,这里我们查看“stm”这个设备。
输入命令以后就会打印出 stm 这个设备的详细信息,比较长,图 40.6.1.3 是一些重要信息:
从图 40.6.1.3 可以看出,Connectors 的 id 为 32,CRTC 的 id 为 35,这两个 ID 很重要,一会测试要用到,大家要根据自己的实际情况填写。
输入如下命令测试 DRM 驱动:
modetest -M stm -s 32@35:800x480
命令参数介绍如下:
-M:指定 stm 模块。
-s:32 表示 connectors 的 ID,35 表示 CRTC 的 ID ,800x480 表示显示的模式。即屏幕大小,运行完命令屏幕会显示 彩色方框
40.6.2 LCD 屏幕的 FB 基本测试
在 40.2 小节里我们说了,KMS 包含了 FB 框架。DRM 驱动默认为 CRTC 用来控制,CRTC是可以模仿 FB 框架,实现使用 FB 接口。示例代码 40.2.2.5 中的第 213 行就是负责初始化基于CRTC 的 FB 接口。我们只需在 Linux 内核图形化配置界面里配置以下选项。
1、使能 DRM 驱动的 FB
配置路径如下:
Device Drivers
-> Graphics support
-> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support)
-> []Enable legacy fbdev support for your modesetting driver //选中
2、使能 PL110
-> Device Drivers
-> Graphics support
-> Frame buffer Devices
-> Support for frame buffer devices
-> <>ARM PrimeCell PL110 support //选中,支持/dev/fb0
3、使能 Linux logo 显示
Linux 内核启动的时候可以选择显示小企鹅 logo。打开 Linux 内核图形化配置界面,按下
路径找到对应的配置项:
-> Device Drivers
-> Graphics support
-> [] Bootup logo //选中
-> [] Standard black and white Linux logo (NEW) //选中
-> [] Standard 16-color Linux logo (NEW) //选中
-> [] Standard 224-color Linux logo (NEW) //选中
40.6.3 设置 LCD 作为终端控制台
LCD 作为终端控制台前提条件要实现 FB 接口。我们一直使用 MobaXterm 作为 Linux 开发板终端,开发板通过串口和 MobaXterm 进行通信。现在我们已经驱动起来 LCD 并且提供了 FB接口,所以可以设置 LCD 作为终端,也就是开发板使用自己的显示设备作为自己的终端,接上键盘就可以直接在开发板上敲命令了,将 LCD 设置为终端控制台的方法如下:
1、设置 uboot 中的 bootargs
重启开发板,进入 Linux 命令行,重新设置 bootargs 参数的 console 内容,命令如下所示:
setenv bootargs 'console=tty1 console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.137.4:/home/tao/linux/nfs/rootfs,proto=tcp rw ip=192.168.137.3:192.168.137.4:192.168.137.1:255.255.255.0::eth0:off'
这里我们设置了两遍 console,第一次设置 console=tty1,也就是设置 LCD 屏幕为控制台,第二遍又设置 console=ttymxc0,115200,也就是设置串口也作为控制台。相当于我们打开了两个 console,一个是 LCD,一个是串口,大家重启开发板就会发现 LCD 和串口都会显示 Linux 启动 log 信息。但是此时我们还不能使用 LCD 作为终端进行交互,因为我们的设置还未完成。
2、修改/etc/inittab 文件
打开开发板根文件系统中的/etc/inittab 文件,在里面加入下面这一行:
tty1::askfirst:-/bin/sh
修改完成以后保存/etc/inittab 并退出,然后重启开发板,重启以后开发板 LCD 屏幕最后一行会显示下面一行语句:Please press Enter to activate this console.上述提示语句说的是:按下回车键使能当前终端。至此,我们就拥有了两套终端,一个是基于串口的 MobaXterm,一个就是我们开发板的 LCD 屏幕,但是为了方便调试,我们以后还是以 MobaXterm 为主。我们可以通过下面这一行命令向 LCD 屏幕输出“hello linux!”
echo hello linux! > /dev/tty1
40.6.4 LCD 背光调节
- 背光调节不用像上一章 PWM 实验那样,直接操作 pwmchipX(X=0~N)目录里面的文件,但是我们还是需要看一下。因为本章我们开启了 TIM4_CH2 这路 PWM,上一章开启了 TIM1_CH3这路 PWM,相当于我们开启了两个不同的定时器对应的 PWM 通道。因此在/sys/class/pwm目录下存在两个 pwmchipX 目录,可以看出此时有 pwmchip0 和 pwmchip4,上一章 PWM 实验中 pwmchip0 对
应 TIM1,那么本章是不是呢?进入 pwmchip0 目录,查看一下路径中的寄存器首地址,此时 pwmchip0 对应的定时器寄存器首地址为 0X40002000,这个正是 TIM4的寄存器收地址!所以在本章 pwmchip0 对应的是 TIM4。同样的方法查看一下 pwmchip4 的地
址,从图 40.6.4.3 可以看出,pwmchip4 对应的定时器寄存器首地址为 0X4000000,这个是 TIM1定时器寄存器地址。所以在本章 pwmchip0 对应的是 TIM4,pwmchip4 对应的是 TIM1!大家在开启多路 PWM 以后,一定要使用这个的方法来确定 TIM 对应的 pwmchip 文件!!
第 40.5 小节已经讲过了,背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7,我们可以通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:
/sys/devices/platform/backlight/backlight/backlight
文件夹中的 brightness 表示当前亮度等级,max_bgigntness 表示最大亮度等级。当前这两个文件内容如图 40.6.4.5 所示:
从图可以看出,当前屏幕亮度等级为 7,根据前面的分析可以,这个是 100%亮度。屏幕最大亮度等级为 7。如果我们要修改屏幕亮度,只需要向 brightness 写入需要设置的屏幕亮度等级即可。比如设置屏幕亮度等级为 6,那么可以使用如下命令:
echo 6 > brightness
输入上述命令以后就会发现屏幕亮度变暗了,如果设置 brightness 为 0 的话就会关闭 LCD背光,屏幕就会熄灭。
40.Linux I2C 驱动实验
41.6 实验程序编写
本实验对应的例程路径为:开发板光盘→1、程序源码→2、Linux 驱动例程→21_iic。
41.6.1 修改设备树
1、IO 修改或添加
AP3216C 用到了 I2C5 接口。因为 I2C5 所使用的 IO 分别为 PA11 和 PA12,所以我们要根据数据手册设置 I2C5 的 pinmux 的配置。如果要用到 AP3216C 的中断功能的话还需要初始化AP_INT 对应的 PE4 这个 IO,本章实验我们不使用中断功能。因此只需要设置 PA11 和 PA12 这个两个 IO 复用为 AF4 功能,ST 其实已经将这个两个 IO 设置好了,打开 stm32mp15-pinctrl.dtsi,然后找到如下内容:
示例代码 41.6.1.1 中,定义了 I2C5 接口的两个 pinmux 配置分别为:i2c5_pins_a 和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在 sleep 状态下使用。
2、在 i2c5 节点追加 ap3216c 子节点
接着我们打开 stm32mp157d-atk.dts 文件,通过节点内容追加的方式,向 i2c5 节点中添加“ap3216c@1e”子节点,节点如下所示:
第 2~4 行,给 I2C5 节点设置了 pinmux 配置。
第 7 行,ap3216c 子节点,@后面的“1e”是 ap3216c 的器件地址。
第 8 行,设置 compatible 值为“alientek,ap3216c”。
第 9 行,reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。设备树修改完成以后使用“make dtbs”重新编译一下,然后使用新的设备树启动 Linux 内核。/sys/bus/i2c/devices 目录下存放着所有 I2C 设备,如果设备树修改正确的话,会在/sys/bus/i2c/devices 目录下看到一个名为“0-001e”的子目录,“0-001e”就是 ap3216c 的设备目录,“1e”就是 ap3216c 器件地址。进入0-001e 目录,可以看到“name”文件,name 文件保存着此设备名字,在这里使用cat打印就是“ap3216c”,
41.6.2 AP3216C 驱动编写
新建名为“21_iic”的文件夹,然后在 21_iic 文件夹里面创建 vscode 工程,工作区命名为“iic”。工程创建好以后新建 ap3216c.c 和 ap3216creg.h 这两个文件,ap3216c.c 为 AP3216C 的驱动代码,ap3216creg.h 是 AP3216C 寄存器头文件。先在 ap3216creg.h 中定义好 AP3216C 的寄存器,输入如下内容,
- ap3216creg.h
#ifndef AP3216C_H
#define AP3216C_H
#define AP3216C_ADDR 0X1E /* AP3216C器件地址 */
/* AP3316C寄存器 */
#define AP3216C_SYSTEMCONG 0x00 /* 配置寄存器 */
#define AP3216C_INTSTATUS 0X01 /* 中断状态寄存器 */
#define AP3216C_INTCLEAR 0X02 /* 中断清除寄存器 */
#define AP3216C_IRDATALOW 0x0A /* IR数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS数据高字节 */
#endif
- ap3216c.c
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : ap3216c.c
作者 : 正点原子Linux团队
版本 : V1.0
描述 : AP3216C驱动程序
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2021/03/19 正点原子Linux团队创建
***************************************************************/
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"
#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"
struct ap3216c_dev {
struct i2c_client *client; /* i2c 设备 */
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
unsigned short ir, als, ps; /* 三个光传感器数据 */
};
/*
* @description : 从ap3216c读取多个寄存器数据
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->client;
/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* ap3216c地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = ® /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/
/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ap3216c地址 */
msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
msg[1].buf = val; /* 读取数据缓冲区 */
msg[1].len = len; /* 要读取的数据长度*/
ret = i2c_transfer(client->adapter, msg, 2);
if(ret == 2) {
ret = 0;
} else {
printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
ret = -EREMOTEIO;
}
return ret;
}
/*
* @description : 向ap3216c多个寄存器写入数据
* @param - dev: ap3216c设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->client;
b[0] = reg; /* 寄存器首地址 */
memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */
msg.addr = client->addr; /* ap3216c地址 */
msg.flags = 0; /* 标记为写数据 */
msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */
return i2c_transfer(client->adapter, &msg, 1);
}
/*
* @description : 读取ap3216c指定寄存器值,读取一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器
* @return : 读取到的寄存器值
*/
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
u8 data = 0;
ap3216c_read_regs(dev, reg, &data, 1);
return data;
}
/*
* @description : 向ap3216c指定寄存器写入指定的值,写一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要写的寄存器
* @param - data: 要写入的值
* @return : 无
*/
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
u8 buf = 0;
buf = data;
ap3216c_write_regs(dev, reg, &buf, 1);
}
/*
* @description : 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
* : 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
* @param - ir : ir数据
* @param - ps : ps数据
* @param - ps : als数据
* @return : 无。
*/
void ap3216c_readdata(struct ap3216c_dev *dev)
{
unsigned char i =0;
unsigned char buf[6];
/* 循环读取所有传感器数据 */
for(i = 0; i < 6; i++) {
buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
}
if(buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
dev->ir = 0;
else /* 读取IR传感器的数据 */
dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);
dev->als = ((unsigned short)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */
if(buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
dev->ps = 0;
else /* 读取PS传感器的数据 */
dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ap3216c_open(struct inode *inode, struct file *filp)
{
/* 从file结构体获取cdev的指针,在根据cdev获取ap3216c_dev结构体的首地址 */
struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
struct ap3216c_dev *ap3216cdev = container_of(cdev, struct ap3216c_dev, cdev);
/* 初始化AP3216C */
ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x04); /* 复位AP3216C */
mdelay(50); /* AP3216C复位最少10ms */
ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0X03); /* 开启ALS、PS+IR */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
short data[3];
long err = 0;
struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
struct ap3216c_dev *dev = container_of(cdev, struct ap3216c_dev, cdev);
ap3216c_readdata(dev);
data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ap3216c_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};
/*
* @description : i2c驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - client : i2c设备
* @param - id : i2c设备ID
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
struct ap3216c_dev *ap3216cdev;
/* */
ap3216cdev = devm_kzalloc(&client->dev, sizeof(*ap3216cdev), GFP_KERNEL);
if(!ap3216cdev)
return -ENOMEM;
/* 注册字符设备驱动 */
/* 1、创建设备号 */
ret = alloc_chrdev_region(&ap3216cdev->devid, 0, AP3216C_CNT, AP3216C_NAME);
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", AP3216C_NAME, ret);
return -ENOMEM;
}
/* 2、初始化cdev */
ap3216cdev->cdev.owner = THIS_MODULE;
cdev_init(&ap3216cdev->cdev, &ap3216c_ops);
/* 3、添加一个cdev */
ret = cdev_add(&ap3216cdev->cdev, ap3216cdev->devid, AP3216C_CNT);
if(ret < 0) {
goto del_unregister;
}
/* 4、创建类 */
ap3216cdev->class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev->class)) {
goto del_cdev;
}
/* 5、创建设备 */
ap3216cdev->device = device_create(ap3216cdev->class, NULL, ap3216cdev->devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev->device)) {
goto destroy_class;
}
ap3216cdev->client = client;
/* 保存ap3216cdev结构体 */
i2c_set_clientdata(client,ap3216cdev);
return 0;
destroy_class:
device_destroy(ap3216cdev->class, ap3216cdev->devid);
del_cdev:
cdev_del(&ap3216cdev->cdev);
del_unregister:
unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
return -EIO;
}
/*
* @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
* @param - client : i2c设备
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_remove(struct i2c_client *client)
{
struct ap3216c_dev *ap3216cdev = i2c_get_clientdata(client);
/* 注销字符设备驱动 */
/* 1、删除cdev */
cdev_del(&ap3216cdev->cdev);
/* 2、注销设备号 */
unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
/* 3、注销设备 */
device_destroy(ap3216cdev->class, ap3216cdev->devid);
/* 4、注销类 */
class_destroy(ap3216cdev->class);
return 0;
}
/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
{"alientek,ap3216c", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{ /* Sentinel */ }
};
/* i2c驱动结构体 */
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.id_table = ap3216c_id,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init ap3216c_init(void)
{
int ret = 0;
ret = i2c_add_driver(&ap3216c_driver);
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ap3216c_exit(void)
{
i2c_del_driver(&ap3216c_driver);
}
/* module_i2c_driver(ap3216c_driver) */
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
在示例代码 41.6.2.2 里,没有定义一个全局变量,那是因为 linux 内核不推荐使用全局变量,要使用内存的就用 devm_kzalloc 之类的函数去申请空间。
第 33~41 行,自定义一个 ap3216c_dev 结构体。第 34 行的 client 成员变量用来存储从设备树提供的 i2c_client 结构体。第 40 行的 ir、als 和 ps 分别存储 AP3216C 的 IR、ALS 和 PS 数据。
第 51~77 行,ap3216c_read_regs 函数实现多字节读取,但是 AP3216C 好像不支持连续多字节读取,此函数在测试其他 I2C 设备的时候可以实现多给字节连续读取,但是在 AP3216C上不能连续读取多个字节,不过读取一个字节没有问题的。
第 87~103 行,ap3216c_write_regs 函数实现连续多字节写操作。
第 111~117 行,ap3216c_read_reg 函数用于读取 AP3216C 的指定寄存器数据,用于一个寄存器的数据读取。
第 126~131 行,ap3216c_write_reg 函数用于向 AP3216C 的指定寄存器写入数据,用于一个寄存器的数据写操作。
第 141~162 行,读取 AP3216C 的 PS、ALS 和 IR 等传感器原始数据值。
第 171~225 行,标准的字符设备驱动框架。ap3216c_dev 结构体里有一个 cdev 的变量成员,
第 174 行就是获取 ap3216c_dev 里的 cdev 这个变量的地址,在第 175 行使用 container_of 宏获取 ap3216c_dev 的首地址。
第 234~285 行,ap3216c_probe 函数,当 I2C 设备和驱动匹配成功以后此函数就会执行,和platform 驱动框架一样。此函数前面都是标准的字符设备注册代码,第 275 行,调用i2c_set_clientdata 函数将 ap3216cdev 变量的地址绑定到 client,进行绑定之后,可以通过i2c_get_clientdata 来获取 ap3216cdev 变量指针。
第 292~305 行,ap3216c_remove 函数,当 I2C 驱动模块卸载时会执行此函数。第 294 行通过调用 i2c_get_clientdata 函数来得到 ap3216cdev 变量的地址,后面执行的一系列卸载、注销操作都是前面讲到过的标准字符设备。
第 308~311 行,ap3216c_id 匹配表,i2c_device_id 类型。用于传统的设备和驱动匹配,也就是没有使用设备树的时候。
第 314~317 行,ap3216c_of_match 匹配表,of_device_id 类型,用于设备树设备和驱动匹配。这里只写了一个 compatible 属性,值为“alientek,ap3216c”。
第 320~329 行,ap3216c_driver 结构体变量,i2c_driver 类型。
第 336~342 行,驱动入口函数 ap3216c_init,此函数通过调用 i2c_add_driver 来向 Linux 内核注册 i2c_driver,也就是 ap3216c_driver。
第 349~352 行,驱动出口函数 ap3216c_exit,此函数通过调用 i2c_del_driver 来注销掉前面注册的 ap3216c_driver。
- 测试 APP:ap3216cApp.c
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : ap3216cApp.c
作者 : 正点原子Linux团队
版本 : V1.0
描述 : ap3216c设备测试APP。
其他 : 无
使用方法 :./ap3216cApp /dev/ap3216c
论坛 : www.openedv.com
日志 : 初版V1.0 2021/03/19 正点原子Linux团队创建
***************************************************************/
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd;
char *filename;
unsigned short databuf[3];
unsigned short ir, als, ps;
int ret = 0;
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}
while (1) {
ret = read(fd, databuf, sizeof(databuf));
if(ret == 0) { /* 数据读取成功 */
ir = databuf[0]; /* ir传感器数据 */
als = databuf[1]; /* als传感器数据 */
ps = databuf[2]; /* ps传感器数据 */
printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
}
usleep(200000); /*100ms */
}
close(fd); /* 关闭文件 */
return 0;
}
ap3216cApp.c 文件内容很简单,就是在 while 循环中不断的读取 AP3216C 的设备文件,从而得到 ir、als 和 ps 这三个数据值,然后将其输出到终端上。
41.7运行测试
编写 Makefile 文件,本章实验的 Makefile 文件和第四十章实验基本一样,只是将 obj-m 变量的值改为“ap3216c.o
当驱动模块加载成功以后使用 ap3216cApp 来测试,输入如下命令:
./ap3216cApp /dev/ap3216c
测试 APP 会不断的从 AP3216C 中读取数据,然后输出到终端上