Zephyr 基础篇总结(保姆级):手把手用一个完整工程打通设备树 + Kconfig + 代码(按键中断控制 LED + 串口日志)

本文是「Zephyr 内核从入门到精通」系列第 06 篇,也是基础篇的收官之作。前五篇分别讲了架构、开发环境、设备树、Kconfig,本篇不讲新概念,而是带你从零新建一个工程,把这些知识点串成一条能真正跑起来的线:按键 → 中断 → 翻转 LED → 串口打印日志。

全文按「建目录 → 写四个文件 → 编译 → 烧录 → 按键实测 → 排错」的顺序走,每一步都告诉你做什么、为什么、应该看到什么。新手照着抄就能跑通。建议先点赞收藏,打开终端跟着敲一遍。

目录

  • 一、基础篇知识地图
  • 二、核心:Zephyr 开发心智模型(三件套)
  • 三、动手前:准备工作与目录规划
  • 四、第一步:新建工程目录
  • 五、第二步:写 CMakeLists.txt(构建脚本)
  • 六、第三步:写 prj.conf(功能开关)
  • 七、第四步:写 app.overlay(硬件描述,按需)
  • 八、第五步:写 src/main.c(业务逻辑)
  • 九、第六步:编译(west build)
  • 十、第七步:烧录(west flash)
  • 十一、第八步:按键实测与预期串口输出
  • 十二、工程目录结构全景
  • 十三、加强版排错表 + 两个「真相之源」
  • 十四、基础篇能力自检清单
  • 十五、总结与下一步

一、基础篇知识地图

先把前几篇的关系理顺,这是组装能力的前提。如果下面这张表你每一行都能讲清楚,本篇会非常轻松;如果有空白,建议回去补一篇。

在这里插入图片描述

篇目解决的问题一句话
02 架构Zephyr 怎么组织四层解耦,换板不改码
03 开发环境怎么跑起来Host 工具 + SDK,west 六步
04 设备树硬件长什么样硬件的「登记表」
05 Kconfig启用哪些功能功能的「清单」
06 打通(本篇)怎么组合用三件套协作,建一个完整工程

二、核心:Zephyr 开发心智模型(三件套)

这是基础篇最该内化的一张图。后面所有的代码、配置、报错,都能用它解释。

在这里插入图片描述

任何一个 Zephyr 功能 = 设备树(硬件)+ Kconfig(功能)+ 代码(逻辑)三者协作。

  • 设备树(硬件):告诉系统用到哪些引脚、哪些外设节点。比如「LED 接在 GPIO0 的第 13 脚」。
  • Kconfig(功能):告诉系统启用哪些子系统 / 驱动。比如「我要用 GPIO,要用日志」。
  • 代码(逻辑):你的业务逻辑本身。比如「按键按下就翻转 LED」。

固定开发路径,永远是这三步顺序

硬件(app.overlay)→ 功能(prj.conf)→ 逻辑(main.c)

为什么是这个顺序?因为代码(逻辑)要引用设备树里的节点、要调用被 Kconfig 打开的 API。底座没铺好,上层逻辑无从谈起。记住这条路径,你写任何新功能都不会乱。

本篇就严格按这个顺序,外加构建脚本 CMakeLists.txt,一共四个文件,把一个完整功能落地。


三、动手前:准备工作与目录规划

开始前确认两件事(这是 03 篇的内容,这里只做检查):

  1. 开发环境已就绪:能成功跑通官方 blinky 示例。也就是说 west、Zephyr SDK、Python 虚拟环境都装好了。
  2. 虚拟环境已激活。每次新开终端都要先激活,否则会报 west: command not found
# Linux / macOS
source ~/zephyrproject/.venv/bin/activate

# Windows PowerShell
~\zephyrproject\.venv\Scripts\Activate.ps1

本篇用到的板子:以官方常见的 nrf52840dk 开发板为例(带板载 LED 和按键,开箱即用)。如果你用别的板子,只要这块板的 dts 里定义了 led0sw0 两个别名(绝大多数官方板都有),代码一行都不用改——这就是「换板不改码」。后文凡是出现 <board> 的地方,都替换成你自己的板名即可。

板名的「v2 斜杠写法」:现在 Zephyr 用 <板名>/<SoC> 的格式,比如 nrf52840dk/nrf52840。老教程里 nrf52840dk_nrf52840 的下划线写法已经过时,新版本可能报 board not found。不确定板名就执行 west boards 列出所有可用板子。


四、第一步:新建工程目录

我们要建一个独立的应用工程(不是改 Zephyr 源码,也不是改 samples)。找一个你喜欢的位置,比如家目录下:

# 进入你的工作区(任意位置都行,别放在 zephyr 源码里)
cd ~

# 新建工程主目录和源码子目录
mkdir -p my_button_led/src
mkdir -p my_button_led/boards

cd my_button_led

执行后你会得到这样一个空骨架:

my_button_led/
├── src/        # 放业务代码
└── boards/     # 放板级专属覆盖文件(可选,本篇可空着)

为什么要建 src/ Zephyr 约定业务代码放在 src/ 下,CMakeLists.txt 里也是这么引用的。boards/ 用来放某块板专属的 overlay/conf,本篇我们用工程根目录下的通用 app.overlayboards/ 留着先空着即可。

接下来,我们按「硬件 → 功能 → 逻辑」的顺序,加上构建脚本,依次创建四个文件。


五、第二步:写 CMakeLists.txt(构建脚本)

放哪:工程根目录 my_button_led/CMakeLists.txt(和 src/ 同级)。

作用:告诉 Zephyr 构建系统「我是一个 Zephyr 应用、我的工程叫什么、源码有哪些」。这是每个工程的入口,少了它 west build 根本不知道从哪开始。

完整内容(这是标准最小写法,几乎所有工程开头都长这样):

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_button_led)

target_sources(app PRIVATE src/main.c)

逐行解释(新手必看):

含义
cmake_minimum_required(VERSION 3.20.0)要求 CMake 版本不低于 3.20,这是现行 Zephyr 的硬性要求
find_package(Zephyr ...)找到并加载 Zephyr 构建框架,$ENV{ZEPHYR_BASE} 指向 Zephyr 源码根目录
project(my_button_led)声明工程名(随便起,建议和目录同名)
target_sources(app PRIVATE src/main.c)src/main.c 加进编译。多个源文件就接着往后加

注意find_package 必须在 project() 之前调用,顺序写反会报错。直接照抄上面的模板最稳。


六、第三步:写 prj.conf(功能开关)

放哪:工程根目录 my_button_led/prj.conf

作用:这就是 Kconfig 三件套里的「功能清单」。我们这个例子要用到 GPIO(控制 LED、读按键)和日志系统(串口打印),所以要把这两个子系统打开。没打开就用,代码会编译失败或运行无效。

完整内容

# 打开 GPIO 驱动子系统(控制 LED、读取按键、配置中断都需要它)
CONFIG_GPIO=y

# 打开日志系统(用于串口打印 LOG_INF 等)
CONFIG_LOG=y

# 日志默认级别:3 = INFO(数值越大越啰嗦:0关闭 1错误 2警告 3信息 4调试)
CONFIG_LOG_DEFAULT_LEVEL=3

逐项说明:

  • CONFIG_GPIO=y:开启 GPIO 子系统。gpio_pin_configure_dtgpio_pin_toggle_dtgpio_pin_interrupt_configure_dt 这些 API 全靠它。不开这一行,链接阶段会找不到这些函数。
  • CONFIG_LOG=y:开启日志框架。LOG_MODULE_REGISTERLOG_INF 靠它。
  • CONFIG_LOG_DEFAULT_LEVEL=3:设成 INFO 级,确保我们的 LOG_INF 能打印出来。如果设成 1(只显示错误),LOG_INF 的内容就不会出现,这是新手最容易踩的「日志怎么不打印」的坑。

小贴士y 表示启用,=3 是数值型配置。等号两边不要加空格,# 开头是注释。

七、第四步:写 app.overlay(硬件描述,按需)

放哪:工程根目录 my_button_led/app.overlay

作用:这是设备树「覆盖文件」,用来在不改板级 dts 的前提下,微调硬件描述。

好消息:绝大多数官方开发板的板级 dts 里已经定义好了 led0sw0 别名(分别指向板载 LED 和按键)。如果你用的是这类板子,这一步可以完全跳过,连 app.overlay 文件都不用建,代码里用 DT_ALIAS(led0) 就能直接取到。

什么时候才需要写 overlay?当你想换引脚、或你的板子没定义这两个别名时。下面给一个示例(仅在需要改 LED 引脚时才用,否则别建这个文件):

/ {
    aliases {
        led0 = &my_led;
        sw0  = &my_button;
    };
};

&led0 {
    /* 把 LED 改到 GPIO0 的第 13 脚,低电平点亮 */
    gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
};

怎么判断要不要建这个文件? 先什么都不建,直接进入下一步编译。如果编译报 DT_ALIAS(led0) 之类的设备树错误,再回来建 overlay 补别名。对照 build/zephyr/zephyr.dts 这个「设备树真相之源」(后面会讲),搜索 aliases 节点就知道你的板子有没有 led0 / sw0

本篇为了通用,假设你的板子已有 led0/sw0 别名,因此不创建 app.overlay


八、第五步:写 src/main.c(业务逻辑)

放哪my_button_led/src/main.c

作用:真正的业务逻辑——配置 LED 和按键、给按键挂中断、按下时翻转 LED 并打日志。

注意看:整段代码里没有任何裸引脚号(像 13GPIO0 这种),全部通过 DT_ALIAS 从设备树取。这就是「换板不改码」能成立的根本原因。

完整内容(复制即用):

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>

/* 注册一个名为 demo 的日志模块,级别 INFO */
LOG_MODULE_REGISTER(demo, LOG_LEVEL_INF);

/* 从设备树取 LED 和按键的描述(端口 + 引脚 + 有效电平),代码中无任何裸引脚号 */
static const struct gpio_dt_spec led =
        GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec btn =
        GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);

/* 中断回调需要的数据结构 */
static struct gpio_callback btn_cb_data;

/* 按键中断回调:在这里翻转 LED 并打印日志 */
static void on_button_press(const struct device *dev,
                            struct gpio_callback *cb, uint32_t pins)
{
    gpio_pin_toggle_dt(&led);
    LOG_INF("button pressed, led toggled");
}

int main(void)
{
    int ret;

    /* 1) 确认设备就绪(驱动是否初始化成功) */
    if (!gpio_is_ready_dt(&led)) {
        LOG_ERR("LED device not ready");
        return -1;
    }
    if (!gpio_is_ready_dt(&btn)) {
        LOG_ERR("button device not ready");
        return -1;
    }

    /* 2) 配置 LED 为输出,初始熄灭 */
    ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
    if (ret != 0) {
        LOG_ERR("failed to configure led, ret=%d", ret);
        return -1;
    }

    /* 3) 配置按键为输入 */
    ret = gpio_pin_configure_dt(&btn, GPIO_INPUT);
    if (ret != 0) {
        LOG_ERR("failed to configure button, ret=%d", ret);
        return -1;
    }

    /* 4) 配置按键中断:上升沿(按下变为有效电平)触发 */
    ret = gpio_pin_interrupt_configure_dt(&btn, GPIO_INT_EDGE_TO_ACTIVE);
    if (ret != 0) {
        LOG_ERR("failed to configure interrupt, ret=%d", ret);
        return -1;
    }

    /* 5) 注册中断回调,并绑定到按键所在端口 */
    gpio_init_callback(&btn_cb_data, on_button_press, BIT(btn.pin));
    gpio_add_callback(btn.port, &btn_cb_data);

    LOG_INF("system ready, press the button");

    /* main 返回后,系统继续运行,中断照常工作 */
    return 0;
}

逐段拆解(这是本篇最核心的代码,务必看懂):

步骤API干了什么 / 为什么
日志注册LOG_MODULE_REGISTER(demo, LOG_LEVEL_INF)声明本文件用日志,模块名 demo。必须有它,LOG_INF 才能用
取硬件GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios)从设备树 led0 别名的 gpios 属性里,把端口/引脚/电平打包成一个结构体
就绪检查gpio_is_ready_dt(&led)确认底层 GPIO 控制器初始化成功,避免对没准备好的硬件操作
配输出gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE)把 LED 引脚设为输出、初始为「无效」(熄灭)
配输入gpio_pin_configure_dt(&btn, GPIO_INPUT)把按键引脚设为输入
配中断gpio_pin_interrupt_configure_dt(&btn, GPIO_INT_EDGE_TO_ACTIVE)设置成「跳到有效电平的那条边沿」触发中断,即按下瞬间触发
挂回调gpio_init_callback + gpio_add_callback把我们的 on_button_press 函数登记为中断处理函数。BIT(btn.pin) 表示只关心这一个引脚
翻转gpio_pin_toggle_dt(&led)在回调里翻转 LED 状态(亮→灭 / 灭→亮)

为什么 main 返回 0 后系统不退出? Zephyr 是 RTOS,main 只是一个线程。它返回后内核继续运行,中断子系统照常工作,所以按键依然能触发回调。这和裸机 main 里写 while(1) 死循环是不同的思路。

三者依赖关系(一图看穿三件套)

代码里的元素依赖谁缺了会怎样
DT_ALIAS(led0) / DT_ALIAS(sw0)设备树里的别名节点编译报错(找不到节点)
gpio_* 系列 APIprj.confCONFIG_GPIO=y链接报错(找不到符号)
LOG_*prj.confCONFIG_LOG=y日志不打印 / 报错

少任意一环,要么编译失败,要么功能静默缺失。这正是三件套心智模型的价值——一眼看出一个功能在三个维度各需要做什么。


九、第六步:编译(west build)

四个文件(实际是 3 个 + 可选的 overlay)都写好了,现在编译。在工程根目录 my_button_led/ 下执行:

# -p always 表示每次都全新构建(pristine),改了配置/设备树务必加它
# -b 后面是板名,用 v2 斜杠写法
# 末尾的 . 表示「当前目录就是工程目录」
west build -p always -b nrf52840dk/nrf52840 .

nrf52840dk/nrf52840 换成你自己的板名。

为什么强烈建议加 -p always Zephyr 的构建缓存很「记仇」:你改了 prj.conf 或设备树,旧缓存可能不刷新,导致「改了没反应」。-p always 强制全新构建,能避开 90% 的玄学问题。代价只是慢一点点。

预期输出(成功时尾部大致长这样):

-- west build: building application
[1/150] Preparing syscall dependency handling
...
[148/150] Linking C executable zephyr/zephyr.elf
[149/150] Generating zephyr/zephyr.hex
Memory region         Used Size  Region Size  %age Used
           FLASH:       28456 B         1 MB        2.71%
             RAM:        6240 B       256 KB        2.38%
        IDT_LIST:          0 B         2 KB        0.00%
[150/150] Building done.

看到最后那张 Memory region 内存占用表 + Building done 就是成功了。生成的固件在 build/zephyr/zephyr.hex(或 .elf)。

如果这一步红字报错,先别慌,跳到第十三节「加强版排错表」对号入座,绝大多数是配置或板名问题。


十、第七步:烧录(west flash)

编译产物有了,烧进开发板。先用 USB 把板子连上电脑,然后在工程根目录执行:

west flash

west flash 会自动调用板子对应的烧录工具(nRF 板用 nrfjprog/J-Link,其它板各有后端),把固件写进 Flash。

预期输出(成功时大致如下):

-- west flash: rebuilding
-- west flash: using runner nrfjprog
-- runners.nrfjprog: Flashing file: build/zephyr/zephyr.hex
Parsing image file.
Erasing page ...
Applying system reset.
Run.
-- runners.nrfjprog: Board with serial number xxxxxxxxx flashed successfully.

看到 flashed successfully 就烧好了,板子会自动复位运行新程序。

【📷 截图位:终端 west flash 输出 flashed successfully + 开发板通过 USB 连接电脑的实物照片】


十一、第八步:按键实测与预期串口输出

固件跑起来了,但怎么看到日志?需要打开串口监视器。

1)打开串口。 nrf52840dk 自带 USB 转串口,连上后会出现一个串口设备。用任意串口工具连接,波特率 115200

# Linux 下用 minicom(设备名按实际,可能是 /dev/ttyACM0)
minicom -D /dev/ttyACM0 -b 115200

# 也可以用 Zephyr 自带的(板子支持时)
# west espressif monitor   # 仅 ESP 系列

Windows 用户可用 PuTTY / MobaXterm / 串口助手,选对 COM 口、波特率 115200。

2)按一下复位键,应当先看到启动日志:

*** Booting Zephyr OS build vX.Y.Z ***
[00:00:00.123,456] <inf> demo: system ready, press the button

这行 system ready, press the button 来自 main 的 LOG_INF,说明初始化全部成功。

3)按下板载按键,每按一次:

  • 板载 LED 状态翻转(亮的灭、灭的亮);
  • 串口打印一行日志
[00:00:05.678,901] <inf> demo: button pressed, led toggled
[00:00:07.234,567] <inf> demo: button pressed, led toggled
[00:00:09.001,234] <inf> demo: button pressed, led toggled

每行格式说明:[时间戳] <inf> 模块名: 内容<inf> 就是 INFO 级,对应我们的 LOG_INF

到这里,三件套就被完整打通了:设备树描述了 LED/按键 → Kconfig 打开了 GPIO/日志 → 代码把它们组装成了「按键中断翻转 LED + 打印日志」的完整功能。恭喜,基础篇的核心技能你已经亲手验证过一遍了。


十二、工程目录结构全景

最终,一个标准 Zephyr 应用工程的最小结构如下(本篇我们建了其中加粗的部分):

my_button_led/
├── CMakeLists.txt        # ★ 构建脚本(必需)
├── prj.conf              # ★ Kconfig 功能配置(必需)
├── app.overlay           # ☆ 设备树覆盖(可选,板子有 led0/sw0 时可不建)
├── boards/               # ☆ 板级专属 overlay / conf(可选)
│   └── nrf52840dk_nrf52840.overlay
├── src/
│   └── main.c            # ★ 业务代码(必需)
└── build/                # 编译后自动生成,别手动改
    └── zephyr/
        ├── .config       # 真相之源 1:最终生效的 Kconfig
        ├── zephyr.dts    # 真相之源 2:合并后的完整设备树
        ├── zephyr.elf    # 固件(调试用)
        └── zephyr.hex    # 固件(烧录用)

记住: 三个文件是任何工程都少不了的最小集合; 是按需;build/ 目录是自动产物,里面藏着排错的两把钥匙,下一节细说。


十三、加强版排错表 + 两个「真相之源」

基础阶段的问题高度集中,掌握定位套路远比死记报错重要。先记住排错决策树,再查下面分五层的速查表。

在这里插入图片描述

两个「真相之源」(必背)

无论遇到什么诡异问题,先问自己:这是配置问题还是硬件描述问题?然后去看对应的文件:

  • build/zephyr/.config —— Kconfig 的最终生效值。你在 prj.conf 写的、板子默认带的、依赖自动开的,全部合并后的真实结果都在这里。怀疑「某功能没开」就 grep 它,例如:

    grep CONFIG_GPIO build/zephyr/.config
    # 期望看到:CONFIG_GPIO=y
    
  • build/zephyr/zephyr.dts —— 合并后的完整设备树。板级 dts + 你的 app.overlay 合并后的最终结果。怀疑「节点/别名/引脚不对」就搜它,例如搜 aliases、搜 led0、看节点的 status 是不是 okay

💡 习惯养成一句话:Kconfig 问题看 .config,设备树问题看 zephyr.dts 这两个文件能解决基础阶段绝大多数疑问,遇事不要瞎猜,先看真相之源。

加强版报错速查表(按五层定位)

#层级现象 / 报错原因解决办法
1环境west: command not found没激活虚拟环境source .venv/bin/activate(Win: Activate.ps1
2环境ZEPHYR_BASE 相关报错 / 找不到 Zephyr环境变量没设或路径错在 zephyrproject 下重新激活环境,确认 west 能跑
3配置board not found / unknown board板名写错或用了旧下划线写法改用 v2 斜杠写法 xxx/yyy,先 west boards 查正确板名
4编译undefined reference to gpio_pin_configure_dt(链接错)prj.conf 没开 CONFIG_GPIO=y加上 CONFIG_GPIO=y,并 -p always 重编
5编译LOG_INF / LOG_MODULE_REGISTER 未定义没开日志或没注册模块prj.confCONFIG_LOG=y;源文件加 LOG_MODULE_REGISTER(...)
6编译改了 prj.conf/overlay「改了没反应」用了旧构建缓存west build -p always -b <board> . 全新构建
7设备树编译报 DT_ALIAS(led0) 相关错 / 节点不存在板子没定义 led0/sw0 别名写 app.overlay 在 aliases{} 里补别名;对照 zephyr.dts 确认
8设备树编译过但 LED/按键无反应,节点存在设备树里该节点 status 不是 okayzephyr.dts,在 overlay 里把对应节点 status = "okay";
9设备树overlay 改了引脚但不生效overlay 文件名/位置不对,没被加载工程根放 app.overlay,或 boards/<board>.overlay;看 zephyr.dts 是否合并进去
10配置功能/外设确实没生效对应 CONFIG_xxx 没开grep CONFIG_xxx build/zephyr/.config 确认是否为 y
11烧录west flash 报找不到 runner / 设备板子没连、驱动缺失、被占用检查 USB 连接、装好 J-Link/驱动、关掉占用串口的程序
12烧录烧录成功但板子没反应没复位、跑的还是旧程序、供电不足按复位键、换 USB 口/线,确认 flashed successfully
13逻辑/输出串口无任何日志波特率不对 / 串口选错 / 日志级别太低波特率设 115200、选对 COM 口、CONFIG_LOG_DEFAULT_LEVEL=3
14逻辑启动日志有,但按键无 button pressed中断没配好 / 别名指错按键 / 触发边沿反了检查 gpio_pin_interrupt_configure_dt 边沿;核对 sw0 指向的引脚
15逻辑按一次打印好多行(抖动)机械按键抖动学到中断/定时器后加软件消抖;基础阶段可忽略

排查顺序建议:环境 → 配置 → 编译 → 设备树 → 烧录 → 逻辑,从前往后排。前面没通,后面无意义。每一层卡住,都先回到对应的「真相之源」看一眼。


十四、基础篇能力自检清单

逐条对照,全部能打勾,说明基础篇你已经真正过关,可以进内核篇:

  • 能解释四层架构与「换板不改码」的原理
  • 能独立搭好环境、跑通 blinky(真机 / QEMU)
  • 能读懂设备树节点,会用 app.overlay 改硬件
  • 能用 prj.conf / menuconfig 裁剪功能
  • 能从零新建工程,写全 CMakeLists.txt / prj.conf / src/main.c
  • 能用三件套独立实现一个完整功能(如本文的按键中断 + LED + 日志)
  • 会用 west build -p alwayswest flash,能看懂构建/烧录输出
  • 能用 .configzephyr.dts 两个「真相之源」自主排错

全部打勾,基础篇过关。哪怕只差一两条,回到对应章节再练一遍,性价比极高。


十五、总结与下一步

回顾一下本篇你亲手做了什么:

  1. 建立了 Zephyr 最重要的三件套协作心智模型:硬件(设备树)+ 功能(Kconfig)+ 逻辑(代码);
  2. 按固定路径从零建了一个完整工程:新建目录 → CMakeLists.txt → prj.conf →(按需 overlay)→ main.c;
  3. 走通了编译 → 烧录 → 串口实测全流程,亲眼看到「按键翻转 LED + 打印日志」;
  4. 掌握了一套分五层的排错方法,并牢记两个「真相之源」:.configzephyr.dts

至此,「会用」这一关你已经通过。从下一篇起进入内核篇,第一篇《Zephyr 内核架构》:调度器怎么决定谁先跑、线程是怎么切换的、信号量/互斥量这些同步机制底层如何运转。我们将从「会用」走向「懂原理」,这才是吃透一个 RTOS 的分水岭

如果这个系列帮到了你,点赞 + 收藏 + 关注三连支持,内核篇马上开更。实践中遇到的报错,欢迎贴在评论区,我看到都会回。下一篇《Zephyr 内核架构》,内核篇,不见不散。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值