Misakiotoha
Misakiotoha
发布于 2026-08-13 / 128 阅读
0
0

Zephyr入门第四课——项目迁移新建与缓存及按键输入

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

  1. 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啊。

  2. 使用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这种声明弱定义函数的编译器拓展方式,我们都可以实现回调函数的注册。这里我放一张表格,让同学们感受一下。

    函数手动注册 __weak Linker 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)
    

    三个参数的用途分别是。

    参数 类型 含义
    _dev const struct device * 监听哪个设备,NULL代表监听所有 input 设备
    _callback 函数指针 自定义的回调函数
    _user_data void * 用户自定义数据指针,回调时会原样传回

    对应到输入回调结构体里面的三个参数。

    /**
     * @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的断点调试与时钟配置


评论