Apache Mynewt 1.15.0 应用开发与模拟器实战详解

以 blinky 应用为例剖析 Mynewt 应用包结构、syscfg 配置系统,并实战 sim 模拟器目标的构建、控制台捕获与运行验证。

Apache MynewtMynewtApacheRTOS嵌入式2026/10/15

Apache Mynewt 1.15.0 应用开发与模拟器实战详解

目标:以 Mynewt 自带的 blinky 应用为样本,讲清应用包结构、syscfg 配置系统与目标(target)体系,并完整走通 sim 模拟器目标的构建、控制台捕获与运行验证,全部操作已在 Ubuntu 22.04 + Newt 1.15.0 实测通过。

一、环境信息

项目内容
操作系统Ubuntu 22.04.5 LTS
系统架构x86_64
NewtApache Newt 1.15.0
核心仓库apache-mynewt-core 1.15.0(commit ea2b7ebf)
工程目录/opt/mynewt/myproj
示例应用apps/blinky(工程模板自带)
示例目标targets/my_blinky_sim(BSP:@apache-mynewt-core/hw/bsp/native)
构建产物bin/targets/my_blinky_sim/app/apps/blinky/blinky.elf

说明:本文运行的“模拟器”是 Mynewt 的官方特性——native BSP 会把整个 RTOS(内核、调度器、驱动抽象层)编译为宿主 Linux 上的普通可执行文件,硬件外设以软件仿真(如 GPIO 打印、UART 映射伪终端),用于无板卡环境下的应用逻辑开发与调试。

二、Mynewt 开发体系概览

Mynewt 工程由三类元素构成,理解它们即可读懂任何 Mynewt 工程:

元素位置作用
包(package)apps/、仓库内 hw/ sys/ 等最小复用单元,pkg.yml 声明依赖与配置
目标(target)targets/构建配置:选哪个 app + 哪个 BSP + 哪些覆盖
syscfg各包 syscfg.yml分层配置系统:定义(defs)与赋值(vals),按层次合并

三者关系:newt build 从 target 出发,沿 pkg.deps 闭包收集包,把所有包的 syscfg.defs 与 syscfg.vals 按固定优先级合并,生成 MYNEWT_VAL(...) 宏,再编译链接。

💡 技术拓展:RTOS 调度策略怎么选?抢占式固定优先级调度(Zephyr、FreeRTOS 常用)响应实时事件最快,但需谨慎设计优先级防饿死;协作式调度实现简单但长任务会拖住全系统;时间片轮转(同优先级)保证公平。Mynewt 采用固定优先级抢占式调度 + 同优先级时间片轮转(0 为最高优先级,共 32 级),兼具实时性与公平性。本文 sim 目标运行的 blinky 任务即普通优先级任务,通过 os_time_delay 每秒主动让出 CPU,休眠期间调度器切走,这正是 RTOS 应用与裸机 while 循环的本质区别。

三、应用包结构剖析

3.1 目录结构

find /opt/mynewt/myproj/apps/blinky -type f

回显:

/opt/mynewt/myproj/apps/blinky/pkg.yml
/opt/mynewt/myproj/apps/blinky/syscfg.yml
/opt/mynewt/myproj/apps/blinky/src/main.c

3.2 pkg.yml —— 包清单

pkg.name: apps/blinky
pkg.type: app
pkg.description: Basic Blinky application.
pkg.author: "Apache Mynewt <dev@mynewt.apache.org>"
pkg.homepage: "http://mynewt.apache.org/"
pkg.keywords:

pkg.deps:
    - "@apache-mynewt-core/kernel/os"
    - "@apache-mynewt-core/hw/hal"
    - "@apache-mynewt-core/sys/console"
    - "@apache-mynewt-core/sys/log"

逐项解说:pkg.name 是全工程唯一包名;pkg.type 取 app/bsp/lib/sdk/test;pkg.deps 是构建闭包的根——@ 前缀指仓库内包,kernel/os 提供调度器与任务 API,hw/hal 提供硬件抽象(GPIO/UART 等),sys/console 提供控制台与 printf 重定向,sys/log 提供日志框架。闭包内的间接依赖(定时器、统计、主任务栈管理等)由 newt 自动解析,无需手写。

3.3 syscfg.yml —— 配置声明

syscfg.defs:
    LED_BLINK_PIN:
        description: Pin which blinks led.
        value: LED_BLINK_PIN
    BLINKY_PRINT_LED_STATE:
        description: Print led status.
        value: 0

syscfg.vals:
    LOG_IMPLEMENTATION: stub
    STATS_IMPLEMENTATION: stub

syscfg.vals.BLINKY_PRINT_LED_STATE:
    CONSOLE_IMPLEMENTATION: full

逐项解说:syscfg.defs 声明本包定义的配置项及默认值——LED_BLINK_PIN 的默认值写的是另一个配置名(引用 BSP 定义的默认 LED 引脚,native BSP 上为 1);BLINKY_PRINT_LED_STATE 默认 0(不打印)。syscfg.vals 设定本包对其他配置的取值(日志/统计实现用 stub 精简版)。条件配置 syscfg.vals.BLINKY_PRINT_LED_STATE 表示“当该开关打开时”,把 CONSOLE_IMPLEMENTATION 升级为 full——这是个精妙的联动:要打印 LED 状态就需要完整控制台。

3.4 src/main.c —— 应用入口

#include <assert.h>
#include <string.h>

#include "sysinit/sysinit.h"
#include "os/os.h"
#include "bsp/bsp.h"
#include "hal/hal_gpio.h"

static volatile int g_task1_loops;

/* For LED toggling */
int g_led_pin;

int
mynewt_main(int argc, char **argv)
{
    int rc;

    sysinit();

    g_led_pin = MYNEWT_VAL(LED_BLINK_PIN);
    hal_gpio_init_out(g_led_pin, 1);

    while (1) {
        ++g_task1_loops;

        /* Wait one second */
        os_time_delay(OS_TICKS_PER_SEC);

        /* Toggle the LED */
        hal_gpio_toggle(g_led_pin);

        if (MYNEWT_VAL(BLINKY_PRINT_LED_STATE)) {
            printf("LED was toggled %d times.\n", g_task1_loops);
        }
    }
    assert(0);

    return rc;
}

逐段解说:sysinit() 完成所有包的初始化(按 syscfg 依赖排序),是每个 Mynewt 应用的固定起点;MYNEWT_VAL(配置名) 宏在编译期展开为配置值(引用 targets/my_blinky_sim/generated/ 下生成的头文件);os_time_delay(OS_TICKS_PER_SEC) 让任务休眠 1 秒(OS_TICKS_PER_SEC 默认 128 tick/s),期间 CPU 归调度器;hal_gpio_toggle 翻转引脚。MYNEWT_VAL(BLINKY_PRINT_LED_STATE) 为 0 时 printf 分支在编译期被优化掉,零开销。

四、syscfg 配置系统实战

4.1 配置覆盖层次

syscfg 赋值的优先级从低到高为:

仓库包默认值(syscfg.defs) < 应用包 syscfg.vals < 目标目录 syscfg.yml < 目标 target set syscfg 变量

4.2 实测:目标级覆盖引脚号

在目标目录创建 syscfg.yml 覆盖 LED 引脚:

cat > /opt/mynewt/myproj/targets/my_blinky_sim/syscfg.yml <<'EOF'
syscfg.vals:
    LED_BLINK_PIN: 7
    BLINKY_PRINT_LED_STATE: 1
EOF
newt build my_blinky_sim

回显(输出节选):

Compiling repos/apache-mynewt-core/hw/mcu/native/src/hal_gpio.c ...
Linking /opt/mynewt/myproj/bin/targets/my_blinky_sim/app/apps/blinky/blinky.elf
Target successfully built: targets/my_blinky_sim

运行验证(捕获方式见第五章):

hal_gpio set pin  7 to 1
LED was toggled 1 times.
hal_gpio set pin  7 to 0
LED was toggled 2 times.
hal_gpio set pin  7 to 1

解说:两条覆盖同时生效——引脚从默认 1 变为 7(hal_gpio 打印的 pin 7),且每秒输出 LED was toggled N times(CONSOLE_IMPLEMENTATION: full 联动被自动激活,printf 正常输出)。验证配置最终值的方法:newt target config my_blinky_sim 刷新后 grep targets/my_blinky_sim/generated/ 下的头文件。

4.3 常用配置项速查(sim 开发相关)

配置项定义处默认作用
LED_BLINK_PINapps/blinky引用 BSP 值(native 为 1)闪烁引脚号
BLINKY_PRINT_LED_STATEapps/blinky0每秒打印翻转次数
CONSOLE_IMPLEMENTATIONsys/console/fullstubfull 启用完整控制台
LOG_IMPLEMENTATIONsys/log/full 等—stub 精简日志
OS_TICKS_PER_SECkernel/os128系统 tick 频率
CONSOLE_UART_DEVhw/mcu/native—sim 控制台设备

五、sim 模拟器构建与控制台捕获实战

5.1 产物与运行方式

ls -l bin/targets/my_blinky_sim/app/apps/blinky/
timeout 5 bin/targets/my_blinky_sim/app/apps/blinky/blinky.elf

回显:

-rwxr-xr-x 1 root root 3792400 Oct 14 17:20 blinky.elf
uart0 at /dev/pts/0

解说:blinky.elf 是普通 x86-64 ELF,可直接执行;启动打印 uart0 at /dev/pts/0 表示控制台 UART 被映射到一个新分配的伪终端。用 gdb 走正式流程则执行 newt run my_blinky_sim(实测进入 gdb 提示符,输入 run 开始,Ctrl-C 退出)。

5.2 控制台捕获:--uart0 参数

native BSP 的 MCU 支持把 UART 指到外部字符设备(hal_system.c 解析 --uart0 长参数,无参时自动创建 pty)。注意不能用 FIFO:实测把 mkfifo 创建的命名管道传给 --uart0,BSP 对设备做 tcsetattr 时报 Error 25 on line 1340 in file console.c 直接断言退出——必须是真实终端设备。

标准做法是用 Python 造一对 pty 并读走输出,实测脚本(存为 simcap.py):

import os, pty, subprocess, sys, time, select
master, slave = pty.openpty()
sname = os.ttyname(slave)
elf = sys.argv[1]
dur = int(sys.argv[2]) if len(sys.argv) > 2 else 10
extra = sys.argv[3:]
p = subprocess.Popen([elf, "--uart0", sname] + extra)
end = time.time() + dur
out = b""
while time.time() < end:
    r, _, _ = select.select([master], [], [], 0.5)
    if r:
        try:
            data = os.read(master, 4096)
        except OSError:
            break
        if not data:
            break
        out += data
p.kill()
sys.stdout.buffer.write(out)

运行:

python3 simcap.py bin/targets/my_blinky_sim/app/apps/blinky/blinky.elf 15 > console.log
tr '\r' '\n' < console.log | head -8

回显:

uart0 at /dev/pts/0
hal_gpio set pin  1 to 1
hal_gpio set pin  1 to 0
hal_gpio set pin  1 to 1
hal_gpio set pin  1 to 0
hal_gpio set pin  1 to 1
hal_gpio set pin  1 to 0
hal_gpio set pin  1 to 1

解说:native BSP 的 hal_gpio 实现把每次电平变化打印到控制台(真实硬件上就是真引脚动作),15 秒恰好 15 次翻转(每秒 1 次),任务时序与真实目标一致。脚本还可附加参数透传给 elf(如覆盖配置的命令行)。

5.3 模拟器能仿什么、不能仿什么

能力sim 下行为
内核调度/任务/信号量/时间完整真实
GPIO软件仿真,打印电平变化
UART 控制台映射 pty,可交互
文件系统(NFFS/FAT)可用(内核逻辑真实)
BLE(NimBLE)基于宿主 HCI 可双 sim 实例互联
具体芯片外设(SPI 时序、ADC 精度)无法仿真,需真机

六、从 sim 到真机

sim 验证通过后切真机只需换 target,应用与配置原样保留:

newt target create blink_stm
newt target set blink_stm app=apps/blinky bsp=@apache-mynewt-core/hw/bsp/nucleo-f401re build_profile=optimized
newt build blink_stm
newt create-image blink_stm 1.0.0
newt load blink_stm

解说:BSP 决定交叉编译工具链与链接脚本,newt 自动处理;create-image 打 mcuboot 兼容镜像头(OTA 场景);load 依赖板卡连接(stlink/openocd 等)。本文环境无板卡,load 步骤未执行,其余流程实测一致(BSP 换成 native 即为 sim)。

七、推荐工作流模板

新应用开发的标准动作(以新增 apps/demo 为例):

newt pkg new apps/demo --type app
newt target create sim_demo
newt target set sim_demo app=apps/demo bsp=@apache-mynewt-core/hw/bsp/native build_profile=debug

回显:

Download package template for package type "app".
Package pkgName is apps/demo
Target targets/sim_demo created.
Target targets/sim_demo successfully set to: app=apps/demo
Target targets/sim_demo successfully set to: bsp=@apache-mynewt-core/hw/bsp/native
Target targets/sim_demo successfully set to: build_profile=debug

随后:在 apps/demo/pkg.yml 的 pkg.deps 中加入所需包(至少 @apache-mynewt-core/kernel/os 与 @apache-mynewt-core/hw/hal),把模板 src/ 替换为业务代码(入口函数 mynewt_main,第一步 sysinit()),targets/sim_demo/syscfg.yml 写覆盖配置,newt build sim_demo + simcap.py 捕获验证,最后复制 target 换 BSP 出真机版。

八、常见问题(FAQ)

Q1:模拟器跑起来只打印 uart0 at /dev/pts/0,看不到应用输出? A:控制台输出写在 pty 上而不是 stdout。用 5.2 节的 pty 捕获脚本,或开着 newt run 的 gdb 会话看输出;直接在终端 grep /dev/pts/* 是读不到已写入数据的。

Q2:--uart0 传 FIFO(mkfifo)为什么直接断言退出? A:BSP 控制台初始化要对设备做终端属性设置(tcsetattr),FIFO 不是终端设备,实测报 Error 25 on line 1340 in file console.c 后 assert 退出。必须用 pty(script/python pty)或真实串口设备。

Q3:改了 targets/目标名/syscfg.yml 后行为没变? A:确认文件里是 syscfg.vals: 段(赋值)而非 defs:;确认目标名拼对(newt target show 核对);必要时 newt clean <目标> 后重新 build——syscfg 变更通常能触发重编,但手工改过 generated/ 目录时必须 clean。

Q4:MYNEWT_VAL 在哪定义的?想看最终值怎么办? A:由 newt 在 targets/<名>/generated/include/ 下生成头文件定义。查值用 grep -rn "LED_BLINK_PIN" targets/<名>/generated/,或 newt target config <名> 刷新后查看。

Q5:blinky 每秒翻转的时序准吗? A:sim 的 os_time_delay 基于宿主系统时钟换算(默认 OS_TICKS_PER_SEC=128),实测 15 秒恰好 15 次翻转(每秒一条 hal_gpio 打印),时序与真实目标一致;但 sim 下无硬实时保证,调度延迟受宿主负载影响。

Q6:怎么同时跑两个 sim 实例做 BLE 互通? A:NimBLE 支持 host 侧接宿主虚拟 HCI,官方提供了 sim 双实例互通的构建变体;本文未展开,可参考 Mynewt 官方 Newt 手册的 NimBLE sim 章节配置第二个 target。

Q7:newt run 进了 gdb 提示符,输入 run 后报错找不到文件? A:先确认 build 过(产物在 bin/ 下);native BSP 的 gdb 脚本 sim.gdb 以工程相对路径定位 elf,务必在工程根目录执行 newt run。

Q8:应用打印 printf 没输出? A:检查 CONSOLE_IMPLEMENTATION 是否为 full(stub 下 printf 被吞)。两种修法:应用 syscfg.yml 直接写 syscfg.vals: CONSOLE_IMPLEMENTATION: full,或像 blinky 一样声明条件联动(BLINKY_PRINT_LED_STATE 打开时自动升级为 full,本文 4.2 节实测生效)。

Q9:sim 目标能 newt size 吗? A:不能,实测报 not supported for sim targets。体积分析请切真机 target 构建后执行,或直接用宿主 size 命令看 sim ELF 的段大小作粗略参考。

Q10:真机 BSP 列表去哪找? A:newt vals bsp 列出全部可用 BSP;或浏览 repos/apache-mynewt-core/hw/bsp/ 目录。选型时注意 BSP 的 CPU 型号决定工具链(arm-none-eabi 等),newt 会在构建时校验。

九、参考链接