Zephyr入门第四课——项目迁移新建与缓存及按键输入
1. 前言
上节课的博客内容实在是有点多,这之后的每节课一般都只讲2~4个点,追求短小而精悍🐒
在上一课,我们了解并实现了Zephyr的项目结构,最终完成了一个能跑的点灯项目。这节课我们有三个任务,分别是项目迁移新建与缓存,按键输入,时钟配置。为了照顾一些基础薄弱的同学,在部分任务当中会稍微补充一些基础性的知识。
2. 项目迁移与新建
在上一节课,我们尝试为自己的板子完成了板级配置文件,Zephyr的配置很有意思,一次配置之后,如果需要同样的板子去完成不同的任务,做项目配置迁移是非常快的。
回忆一下上节课的内容,关于板级配置文件,核心就是那个boards目录,配置迁移只需要迁移boards目录。而项目本身的配置则需要迁移CMakePresets.json文件,只需要简单的复制粘贴即可快速完成新项目的基础配置。
我们这节课后面要学习的是按键输入与时钟配置,刚好,我们可以从上节课的blinky项目当中迁移板级配置到新的项目当中,这里我给新项目命名为input_rcc,然后我将boards目录和CMakePresets.json文件迁移到了新项目的下面。
❯ mkdir input_rcc
❯ cp -r blinky/boards input_rcc
❯ cp blinky/CMakePresets.json input_rcc
❯ eza --tree input_rcc
input_rcc
├── boards
│ └── arm
│ └── st_h743_board
│ ├── board.cmake
│ ├── board.yml
│ ├── Kconfig.defconfig
│ ├── Kconfig.st_h743_board
│ ├── st_h743_board-pinctrl.dtsi
│ ├── st_h743_board.dts
│ └── st_h743_board_defconfig
└── CMakePresets.json
░▒▓ ~/Mi/zephyrproject/new
我们在上节课对CMakeLists.txt文件做了一些修改,我们加了这样一行代码。
# 指定 board 搜索路径为当前应用目录下的 boards/
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
这次,我们删掉这行代码,我们将BOARD_ROOT变量值,放到CMakePresets.json文件当中,我们在"cacheVariables"里面增加这样一行"BOARD_ROOT": "${sourceDir}",。这样最终的CMakePresets.json文件就变成了。
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"cacheVariables": {
"BOARD": "st_h743_board",
"BOARD_ROOT": "${sourceDir}",
"Python3_EXECUTABLE": "/home/misaki/MisakiCodes/Env/zephyrproject/.venv/bin/python3",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"NO_BUILD_TYPE_WARNING": "ON",
"CMAKE_C_COMPILER": "/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc",
"CMAKE_CXX_COMPILER": "/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-g++"
},
"environment": {
"ZEPHYR_BASE": "/home/misaki/MisakiCodes/Env/zephyrproject/zephyr"
}
},
{
"name": "zephyr-debug",
"inherits": "base",
"binaryDir": "${sourceDir}/cmake-build-debug-zephyr",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "zephyr-release",
"inherits": "base",
"binaryDir": "${sourceDir}/cmake-build-release-zephyr",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "zephyr-debug-build",
"configurePreset": "zephyr-debug"
},
{
"name": "zephyr-release-build",
"configurePreset": "zephyr-release"
}
]
}
最终的CMakeLists.txt文件的内容变成了。
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(input_rcc)
target_sources(app PRIVATE src/main.c)
这么做会让CMakeLists.txt文件更加干净,职责区分也更加明确。
然后,我们只需要在新的input_rcc下面新建一个src目录,然后创建一个main.c文件,接着在input_rcc下面创建prj.conf应用级的项目配置文件,之后就可以用IDE打开然后写代码了。
最终的目录结构如下。
❯ ls
boards CMakeLists.txt CMakePresets.json src
❯ mkdir src/main.c
❯ rm -rf src/main.c
❯ rm -rf src/main.c
❯ touch src/main.c
❯ touch prj.conf
❯ eza --tree .
.
├── boards
│ └── arm
│ └── st_h743_board
│ ├── board.cmake
│ ├── board.yml
│ ├── Kconfig.defconfig
│ ├── Kconfig.st_h743_board
│ ├── st_h743_board-pinctrl.dtsi
│ ├── st_h743_board.dts
│ └── st_h743_board_defconfig
├── CMakeLists.txt
├── CMakePresets.json
├── prj.conf
└── src
└── main.c
░▒▓ ~/Mi/z/new/input_rcc
到此就完成了项目的迁移,我们当前的st_h743_board_defconfig内容是这样的。
# Board
CONFIG_BOARD_ST_H743_BOARD=y
# GPIO
CONFIG_GPIO=y
# Clock
CONFIG_CLOCK_CONTROL=y
CONFIG_CLOCK_CONTROL_STM32_CUBE=y
# 允许调试器在 Sleep/Stop 下保持连接(H7 WFI 会关调试域,不开这个 SWD 必失联)
CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP=y
后续我们不会再动这个板子默认配置文件,后续关于板子的Kconfig我们都写到应用级Kconfig文件prj.conf当中。
[!IMPORTANT]
这里补充一个关于CMake缓存的问题,如果你哪天发现写完设备树之后,到了源代码里面想引用设备,但是缺找不到,并且设备树写的没问题的话,那么说明你的CMake缓存还是旧的,最好的方式是直接删掉构建目录,然后重新加载一下CMake配置。并且这大概率是个你会经常遇见的问题。
3. 按键输入
上节课我们最终完成了一个LED闪烁的例程,也就是blinky。我们这节课来学习Zephyr的Input系统,最终完成一个使用按键控制LED亮灭的任务。同时我们会从零开始完成main.c的内容,以初步了解Zephyr当中常用的API。
-
Zephyr的Input子系统先稍微讲一点理论知识,在初学单片机按键输入的时候,我们肯定写过这样的代码。
#define KEY_GPIO_PORT GPIOA #define KEY_GPIO_PIN GPIO_PIN_0 if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN) == GPIO_PIN_RESET) // 检测到按下 { HAL_Delay(20); // 延时20ms消抖 if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN) == GPIO_PIN_RESET) { // do somethings // 等待按键释放 while (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN) == GPIO_PIN_RESET); } }我们会通过读取某个按键连接的
GPIO的电平信号来确认按键按下与否,这是惯用做法。在上面的写法当中,除了读取按键以外,还有一个对按键做消抖的操作,在检测到按键按下之后,过20ms后再次检测,以避开按键电平抖动。在裸机当中这么写,每次循环到按键读取,CPU都要苦等20ms。后来,我们学到了中断,我们可以让某个按键的
GPIO电平在变化的时候,向CPU发出EXTI中断,这样我们似乎可以避开读取按键占用CPU时间的问题。但实际使用的时候,发现没有消抖了,按键电平抖动影响到了具体的业务功能。再后来,又学到了定时器,我们可以使用定时器中断每隔
20ms去查询按键状态,并且将按键状态记录到某个变量当中。我们终于同时拥有了中断和消抖。但是板子上的定时器没那么多,光是业务本身就要吃掉大部分定时器。于是你想看看能不能在硬件上下功夫。既然按键被按下时,会因为机械特性而产生电平抖动,那是不是可以去吸收这些电平抖动。你想到了一个元器件——电容。这些电平抖动,酷似交流信号,同时电容两边的电压不能突变,只能连续变化,于是你给按键电路并联了电容到地,以吸收电平抖动。到了实际运行的时候发现,只有中断的情况下,电平抖动也消去好多。不过电容容值的选型又成了个问题。以及突逢电容涨价,这个电容砍掉的话量产的硬件能省去很多成本。你不得不去掉电容。
于是你的老板要你要节省成本,你选择拿烙铁烫老板的屁股。于是被老板炒掉,进入
badend。另一个平行世界的你在遇到这种要求下,想到了这样的方法,中断可以保留,但是不再把任务放到中断中执行,中断当中不应执行耗时任务,这件事情你很早就知道了,你会设置标志位来获取按键状态,把耗时任务放到主循环执行。但是这样无法避免抖动。那能不能想办法延迟让标志位改变呢?或者说按下之后延迟
20ms再执行任务呢?这当然可以,你选择在按下按键的时候记录当前系统的ms级别Systrick,当新的Systrick和记录的旧值的差值为20的时候,此时再次查看按键是否被按下。这样就可以同时拥有中断+消抖了。你觉得现在使用的读取按键的方式足够应付老板的项目了。或许这就足够了?
你选择继续学习,你发现按键消抖的方式还有很多,不管是软件和硬件的。但大部分人都会为了成本而使用软件消抖。
后面你又接触到了
FreeRTOS,你发现可以创建一个任务来持续读取按键状态,这样都不用专门分配一个定时器给按键。真是太美妙了。但是按键一多,全局变量满天飞,看着头大。终于,你又接触到了一个不一样的
RTOS——Zephyr。你发现它里面将所有的输入抽象成了一个Input系统,同时支持各种输入设备的输入。什么按键,鼠标,键盘,都不在话下。你觉得这很神奇,你试着了解了一下它的按键消抖思路,和平行世界的你想的差不多,只不过把一些在主循环里面干的活搬到了其他线程当中。Zephyr对于按键输入的处理,默认使用中断,在按键被按下后,产生中断,满足条件会调用你写的回调函数,你可以设置延迟20ms再调用的条件。并且你发现回调函数是运行在系统工作队列线程当中的,并非中断上下文。甚至Zephyr还为按键引入了键码,解耦了配置与代码之间的耦合。这让你觉得的很惊讶,Zephyr真是太棒了,真希望全世界的嵌入式MCU工程师都用Zephyr啊。 -
使用
Input系统那么要怎么在
Zephyr当中享受这套Input系统呢?需要分成两个部分去完成,一个是配置部分,一个是实际
C代码层面。a. 配置部分
细分为
Kconfig和dts两个部分。Zephyr的Input系统的Kconfig部分有很多配置,本节课我们使用下面的两个配置。配置 作用 CONFIG_INPUT=y打开 input 子系统核心。 事件结构、分发机制、 INPUT_CALLBACK_DEFINE注册机制。本身不包含任何硬件驱动CONFIG_INPUT_GPIO_KEYS=y打开 gpio-keys 驱动。 解析 dts里的相关节点,把 GPIO 中断+消抖包装成 input 事件其他的可以直接自行
grep有关INPUT关键词的配置。在
prj.conf当中增加配置项即可。而
dts部分也很简单。像下面这么写即可。/dts-v1/; #include <st/h7/stm32h743Xi.dtsi> #include <zephyr/dt-bindings/input/input-event-codes.h> // 必须导入这个头文件,这里面定义了键码的宏定义 / { gpio_keys: gpio_keys { compatible = "gpio-keys"; debounce-interval-ms = <20>; user_bt1: user1 { gpios = <&gpioh 11 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_A>; }; user_bt2: user2 { gpios = <&gpioh 10 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_B>; }; extend_bt1: e_bt1 { gpios = <&gpiof 7 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_1>; }; extend_bt2: e_bt2 { gpios = <&gpiof 5 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_2>; }; extend_bt3: e_bt3 { gpios = <&gpiof 4 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_3>; }; extend_bt4: e_bt4 { gpios = <&gpiof 3 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_4>; }; extend_bt5: e_bt5 { gpios = <&gpiof 2 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_5>; }; extend_bt6: e_bt6 { gpios = <&gpiof 1 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_6>; }; extend_bt7: e_bt7 { gpios = <&gpiof 0 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_7>; }; extend_bt8: e_bt8 { gpios = <&gpioi 11 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_8>; }; extend_bt9: e_bt9 { gpios = <&gpioi 10 GPIO_ACTIVE_LOW>; zephyr,code = <INPUT_KEY_9>; }; }; };这里我给板子额外还接入了外置
GPIO九键键盘。也就是从extend_bt1到extend_bt9这九个IO口。来说一下上面的
dts代码,我们声明了一个容器节点gpio_keys,内部设置了驱动字符串为compatible = "gpio-keys";,也就是有个驱动的名字叫gpio-keys会接管这个容器节点内部的子硬件节点。同时我们为gpio_keys设置了debounce-interval-ms = <20>;,这代表所有的按键都要做20ms的消抖。而对于子硬件节点,我们设置了每个子硬件节点的GPIO接口和默认有效电平,然后还为其分配了键码zephyr,code = <INPUT_KEY_1>;,我们到了后面的C代码当中,我们不需要访问任何GPIO,我们直接通过键码就能找到GPIO。我们并没有为每一个
GPIO设置输入的模式,是否中断。gpio_keys会为我们自动配置,将引脚配置为输入模式,根据有效电平去自动决定触发方式是上升沿还是下降沿触发。至此我们完成了配置部分。
a. C代码部分
我们来到
main.c当中。来从零简单学习一下Zephyr的常用API。先来看几个头文件
#include <zephyr/kernel.h> #include <zephyr/drivers/gpio.h> #include <zephyr/input/input.h>zephyr/kernel.h最为核心的一个头文件,里面几乎包含所有的内核API有一些常用的函数,分别是
k_sleep(K_MSEC(10)),k_usleep(100),k_msleep(10)。这些函数光看名字我们就可以明白他们的用途,其他的还有创建线程,同步原语,定时器,队列我们会放在专门的一课去讲。zephyr/drivers/gpio.h当中存放了GPIO的全部API。相关的接口可以见下面的调用。其中的spec是struct gpio_dt_spec的实例,Zephyr当中把设备树当中声明的GPIO描述自动转换成C结构体。它就是某个GPIO的配置结构体。#include <zephyr/drivers/gpio.h> // 从设备树节点获取 GPIO 描述符 static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_NODELABEL(led0), gpios); gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); // 配置输出 gpio_pin_configure_dt(&led, GPIO_INPUT); // 配置输入 gpio_pin_set_dt(&led, 1); // 设置输出高 gpio_pin_set_dt(&led, 0); // 设置输出低 gpio_pin_toggle_dt(&led); // 翻转 gpio_pin_get_dt(&led); // 读取输入电平 // 中断 gpio_pin_interrupt_configure_dt(&led, GPIO_INT_EDGE_FALLING); gpio_init_callback(&cb, handler, BIT(led.pin)); gpio_add_callback(led.port, &cb);关于
GPIO_DT_SPEC_GET相关的宏,这些宏来自设备树和zephyr/drivers/gpio.h当中。最常用的是这样写的。#include <zephyr/kernel.h> #include <zephyr/drivers/gpio.h> #define LED0_NODE DT_ALIAS(led0) // 在设备树当中预先将led0节点添加到aliases当中 static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); // 之后就可以拿led去配置相关的gpio了 // 例如下面的 gpio_pin_set_dt(&led, 1);不过此处你可能注意到了,
GPIO_DT_SPEC_GET(LED0_NODE, gpios);这里面的gpios是啥?还记得我们之前在设备树下为某个GPIO硬件节点配置的属性名字吗?leds { // leds compatible = "gpio-leds"; led0: led_0 { gpios = <&gpioh 9 GPIO_ACTIVE_LOW>; }; led1: led_1 { gpios = <&gpioh 12 GPIO_ACTIVE_LOW>; }; led2: led_2 { gpios = <&gpioe 9 GPIO_ACTIVE_LOW>; }; };这个
gpios和设备树GPIO硬件节点的gpios属性对应。驱动代码通过这个名字去取数据,所以你要是试着改成自定义的属性名字,然后也给它一样的值的话,那么驱动代码那边也需要同步修改,不过我们最好就是用gpios这种约定俗成的名字。并且我们暂时也没讲到如何自定义节点属性名字。至此和
GPIO相关的常用API也做了简单的说明。zephyr/input/input.h这个头文件是和输入有关的各种API。Zephyr将很多种输入都抽象到了一起,例如按键,鼠标,键盘,矩阵键盘等等。你以前在其他库当中学到的对于按键输入获取的各种方式,在Zephyr当中都能实现,这里我们不去讲轮询方式,直接中断方式,我们讲Zephyr当中最为推荐的输入方式。talk is cheap, show your my code.#include <zephyr/kernel.h> #include <zephyr/input/input.h> #define LED0_NODE DT_ALIAS(led0) static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); static void input_call_back(struct input_event *event, void *user_data) { // 只处理按键事件 if (event->type != INPUT_EV_KEY) { return; } // 判断是否是对应的key被按下触发 if ((event->code == INPUT_KEY_1) && (event->value == 1)) { gpio_pin_toggle_dt(&led); // 切换led的状态 } } INPUT_CALLBACK_DEFINE(NULL, input_call_back, NULL); int mian(void){ while(true){ k_msleep(1000); } }其中
INPUT_CALLBACK_DEFINE(NULL, input_call_back, NULL);展开之后是下面这样的。static const struct input_callback _input_callback__input_call_back __attribute__((section("." "_input_callback" "." "static" "." "_input_callback__input_call_back_"))) __attribute__((__used__)) = { .dev = ((void *) 0), .callback = input_call_back, .user_data = ((void *) 0), };先说明一下我们做了些什么。我们首先有一个
led节点,接着我们写了一个函数input_call_back,光是看名字就知道这是一个回调函数,函数的参数分别是struct input_event*和void *。在按键输入的学习中,我们暂时不需要考虑void *user_data这个参数的值。然后我们在函数当中,判断回调传入的
event指针指向的变量内部type的内容,以及code和value的值,最终完成了一个按下按键切换LED状态的回调函数。最后通过
INPUT_CALLBACK_DEFINE宏,将我们的回调函数注入到Zephyr的Input系统当中。当按键被按下的时候,Zephyr会接收到那个按键对应的GPIO的中断,然后根据GPIO和键码的对应给code对应的键码,并且把此时的电平信息存到value当中。经过一系列的事情之后,调用用户的回调函数。现在来说一下几个要点知识,首先是
INPUT_KEY_1,这个是键码,在之前写设备树的时候我们就遇到了,在那里设置的键码可以在实际代码中使用,这样就不用写具体的gpio引脚信息了。接着是struct input_event这个结构体。/** * @brief Input event structure. * * This structure represents a single input event, for example a key or button * press for a single button, or an absolute or relative coordinate for a * single axis. */ struct input_event { /** Device generating the event or NULL. */ const struct device *dev; /** Sync flag. */ uint8_t sync; /** Event type (see @ref INPUT_EV_CODES). */ uint8_t type; /** * Event code (see @ref INPUT_KEY_CODES, @ref INPUT_BTN_CODES, * @ref INPUT_ABS_CODES, @ref INPUT_REL_CODES, @ref INPUT_MSC_CODES). */ uint16_t code; /** Event value. */ int32_t value; };这是源码的内容定义,我们平时用输入系统只需要关心
type,code,value三个结构体变量的值就可以,他们分别代表输入事件类型,键码,值。关于输入事件类型和键码,都可以在<zephyr/dt-bindings/input/input-event-codes.h>当中找到定义。value的内容一般对于不同的输入都是会有所不同的。在我们这个按键输入的案例当中,回调会触发两次,当我们没有为gpio的中断方式设置时,Zephyr默认的选择是设置双边沿触发,也就是按下和松开各触发一次回调。我们在代码里面额外判断了(event->value == 1),因此为0的触发情况什么都不会做。那么能否设置单边沿触发呢?当然可以。直接走
gpio配置成单边沿就行gpio_pin_interrupt_configure_dt(&btn, GPIO_INT_EDGE_FALLING);。但是,完全不建议你这么做!因为双边沿检测的话,就能让Input知道按键什么时候按下,什么时候释放了。所以根本没必要设置单边沿。最后就是
INPUT_CALLBACK_DEFINE这个宏了。它是Zephyr Input子系统用来注册全局事件监听器的宏。其本质是把我们写的回调函数挂到一个链表上面,当任何input设备上报事件的时候,内核会遍历链表逐个调用。我们可以来稍微深入学习一下Zephyr对于这部分的具体实现。在上面我们放出了这个宏展开之后的代码。我们看这两个部分,分别是
__attribute__((section("...")))和__attribute__((__used__))这两个,这是C Language的编译器拓展内容,属于GCC特有的,GCC Yes!,如果你用的是LLVM/Clang,Zephyr也兼容的,但是MSVC肯定是不行。这边简单展开有关__attribute__的一些常见属性,在一些特别的场景下很有用。属性 作用 一般什么时候用 section("name")把变量或函数放进指定 ELF段需要把变量或函数放进指定 ELF段的时候used告诉编译器,即使没人引用某个变量,也不许优化掉 某些些宏,防止被 LTO删掉packed取消结构体填充,紧凑排列 网络协议头、Flash 存储结构 aligned(n)按 n 字节边界对齐 DMA 缓冲区(必须 4/32 字节对齐) weak弱符号,允许被强定义覆盖 默认处理函数、可替换的板级代码,这个你可能在写 STM32 HAL的时候就接触到了noreturn函数不会返回 一些死循环函数,不需要返回值 关于段的概念,如果你学过汇编语言,亦或者是学过计算机组成原理+操作系统,那么你肯定是听过这个名词的。我想它应该会出现在
内存空间的分配与回收这个小节,具体到标题应该是叫基本分段存储管理方式。在这里面你会初次接触到段的概念。我们编写的程序当中,不同的变量,函数代码等等,都会被放进不同的段当中。不过我们这里提到的段,和你在专业课中学到的段有所区别,这么一扯会拉出很多知识,我们这边简单的描述。Zephyr或者说编译器当中的段的英文是Section,而专业课当中学到的段的英文是Segment,它们一般都会被翻译成段。Section是链接器和编译器打交道时使用的概念,而Segment则是操作系统和MMU打交道时用的概念。Section被用于Linux/Unix当中的ELF格式。到这里我们可以打住了,如果这里面有你没听过的名词,去寻求搜索引擎亦或是AI的帮助吧。这里再放一个问题
.elf和.bin与.hex三者有什么区别呢?烧录的时候用哪个呢?关于这部分的内容,我打算放在一个番外当中,让大家感受一下一个程序是怎么从
C代码最终到能够上板运行的机器码的。这里大家只需要知道,
Zephyr把变量或者函数放进某个自定义的段中,在启动的时候会去自定义的段当中搜集放进去的东西,最终实现这个回调功能。在这里就是搜集到我们自定义的回调函数,进而实现运行时回调的功能。那么为什么
Zephyr要这么做呢?对于注册回调函数这件事,我们有很多种方法,从最简单的通过函数去注册回调函数,再到__weak这种声明弱定义函数的编译器拓展方式,我们都可以实现回调函数的注册。这里我放一张表格,让同学们感受一下。函数手动注册 __weakLinker Section收集代码耦合 高(要改 main.c) 中(要改同名函数) 零耦合(模块自治) 支持数量 任意 只能一个 任意 运行时开销 malloc / 链表插入 函数调用 零开销(编译期排好) 遗漏风险 高(忘了注册就丢) 中(忘了覆盖用默认) 零(宏自动收集) 内存占用 链表节点 + 指针 无 只读 Flash,不耗 RAM 初始化顺序 手动控制,易错 编译期 由 SYS_INIT 优先级控制 Linker Section就是我们会在番外课当中提到的一些概念,我们会在番外课当中尝试自己定义一个段,向其中放置一些变量。如此一来,大家应该又了解到了一种新的注册回调函数的方式。如此方便的使用方式,往往伴随的是设计时复杂的抽象。
我们再回到
INPUT_CALLBACK_DEFINE这个宏,它的源码定义是这样的。/** * @brief Register a callback structure for input events. * * The @p _dev field can be used to only invoke callback for events generated * by a specific device. Setting dev to NULL causes callback to be invoked for * every event. * * @param _dev @ref device pointer or NULL. * @param _callback The callback function. * @param _user_data Pointer to user specified data. */ #define INPUT_CALLBACK_DEFINE(_dev, _callback, _user_data) \ INPUT_CALLBACK_DEFINE_NAMED(_dev, _callback, _user_data, _callback)三个参数的用途分别是。
参数 类型 含义 _devconst struct device *监听哪个设备, NULL代表监听所有 input 设备_callback函数指针 自定义的回调函数 _user_datavoid *用户自定义数据指针,回调时会原样传回 对应到输入回调结构体里面的三个参数。
/** * @brief Input callback structure. */ struct input_callback { /** @ref device pointer or NULL. */ const struct device *dev; /** The callback function. */ void (*callback)(struct input_event *evt, void *user_data); /** User data pointer. */ void *user_data; };_callback参数我们已经知道它是干什么的了,那么_user_data呢?我们在这里简单的说明一下它的作用。一句话描述就是,它被用于解耦回调函数与具体的数据来源,以及实现同一个回调服务多个不同上下文。举两个很简单的例子,a. 回调不硬编码变量名
static int press_count; // 全局 void cb(struct input_event *evt) { press_count++; // 硬编码依赖全局变量名 }没有
user_data时,回调里只能直接写死全局变量。而有了
user_data,回调通过参数接收指针,不关心数据存在哪里。void cb(struct input_event *evt, void *user_data) { int *count = user_data; (*count)++; // 不关心count是全局还是局部 }b. 同一个回调服务多个不同上下文
当你想记录按键被按下次数时,通过
user_data你可以这么做。static int count_a = 0; static int count_b = 0; void my_callback(struct input_event *evt, void *user_data) { int *cnt = user_data; if (evt->value == 1) { (*cnt)++; } } // 同一套回调逻辑,不同的数据 INPUT_CALLBACK_DEFINE(dev_a, my_callback, &count_a); INPUT_CALLBACK_DEFINE(dev_b, my_callback, &count_b);可以更好的实现代码复用。
最后是
_dev,它的类型是const struct device *,struct device是Zephyr的统一设备抽象层,不管是GPIO、UART、I2C、SPI、ADC,还是自己写的外设驱动,最终都会封装成一个struct device *,可以通过统一的API去操作它。具体的内容我们会在之后展开来讲。默认给
INPUT_CALLBACK_DEFINE传NULL的时候,会监听所有的设备,你也可以只对某一个设备监听。例如你可以这样写,对
gpio-keys设备监听。需要注意的是,INPUT_CALLBACK_DEFINE的三个参数都必须是编译时常量,函数名,字符串,NULL,DEVICE_DT_GET()在编译/链接阶段地址就确定了,属于编译时常量。不可以放变量进去。INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_NODELABEL(gpio_keys)), input_call_back, NULL);这样就可以让
input_call_back只为gpio_keys这个输入设备服务。同理你可以写一些其他的回调函数来为不同的输入设备服务。我们在此不展开讲Zephyr当中更多其他的输入设备,大约在本教程的高级外设驱动部分,会讲到一些其他的输入设备,同时也会带同学们深入理解Zephyr的Input系统。在这次我们只是浅尝辄止的学一下。至此,我们就完成了本节课按键输入内容的学习。
下节课内容预告:配置Zephyr的断点调试与时钟配置