[{"data":1,"prerenderedAt":3380},["ShallowReactive",2],{"blog-post-zh-nanomq-on-zephyr-rtos":3},{"post":4,"translationPath":3379},{"id":5,"title":6,"author":7,"body":8,"cover":3364,"coverHeight":3365,"coverWidth":3366,"date":3367,"description":3368,"draft":3369,"extension":3370,"meta":3371,"navigation":3372,"path":3373,"pinned":3372,"seo":3374,"sitemap":3375,"stem":3376,"tags":3377,"__hash__":3378},"blog_zh\u002Fzh\u002Fblog\u002Fnanomq-on-zephyr-rtos.md","在 Zephyr RTOS 上运行 NanoMQ：让 MQTT Broker 直接跑在 MCU 上",null,{"type":9,"value":10,"toc":3319},"minimark",[11,15,23,26,44,55,58,62,65,68,103,110,114,119,123,126,158,161,164,178,181,184,187,190,248,251,325,335,341,345,362,511,514,520,535,541,547,574,581,585,598,605,623,634,641,647,653,664,670,673,679,750,791,794,801,874,880,897,901,905,920,926,933,939,943,949,955,958,964,967,997,1001,1004,1018,1025,1031,1034,1040,1055,1061,1067,1071,1084,1090,1093,1099,1116,1126,1130,1136,1142,1145,1151,1154,1160,1166,1168,1211,1214,1250,1263,1267,1271,1277,1283,1286,1292,1295,1316,1320,1323,1329,1345,1348,1354,1364,1370,1376,1382,1388,1407,1413,1417,1420,1426,1429,1443,1449,1453,1470,1479,1482,1485,1491,1508,1514,1520,1527,1533,1536,1542,1549,1555,1568,1574,1584,1587,1714,1721,1724,1738,1742,1749,1859,1907,1911,1929,1934,2282,2287,2488,2517,2549,2595,2599,2688,2692,2695,2700,2703,2718,2724,2738,2746,2749,2757,2764,2778,2784,2803,2810,2816,2819,2822,2825,2831,2852,2855,2875,2884,2888,2895,2907,2928,2945,3103,3117,3133,3152,3161,3177,3201,3205,3304,3307,3313,3316],[12,13,14],"h2",{"id":14},"简介",[16,17,18,19],"p",{},"MQTT 和 Zephyr 最常见的组合方式在客户端一侧：基于 Zephyr 的设备作为 MQTT 客户端，连接远端的 Broker。",[20,21,22],"strong",{},"那么，Broker 本身能不能直接跑在 MCU 上？",[16,24,25],{},"在一些边缘场景中，专门部署一台 Linux 网关会带来不必要的复杂度，这时这种做法就能派上用场。例如，几十台现场设备通过 Modbus、串口或私有协议通信，需要统一汇聚到 MQTT；又如网络不稳定，需要先在本地缓存数据，等连接恢复后再上传。常规做法是在设备旁边部署一台 Linux 网关，但额外的功耗、维护和物理防护成本都不低。把 Broker 直接运行在 MCU 上，就可以省掉这一层。",[16,27,28,33,34,38,39,43],{},[29,30,32],"a",{"href":31},"\u002Fzh","NanoMQ"," 是 EMQ 开源的边缘 MQTT Broker，最初为 Linux 和其他 POSIX 环境设计。本文探讨在 Zephyr RTOS 上运行 NanoMQ Broker 核心需要做哪些工作。应用层使用同一套 ",[35,36,37],"code",{},"nanomq\u002F"," 源码和 NanoNNG MQTT 协议栈，没有裁剪功能。受 Zephyr 平台的约束，部分外围功能已关闭或尚未验证；完整的差异和限制列表见",[29,40,42],{"href":41},"#%E9%99%84%E5%BD%95-a%E4%B8%8E%E9%80%9A%E7%94%A8%E7%89%88-nanomq-%E7%9A%84%E5%B7%AE%E5%BC%82","附录 A","。",[16,45,46,47,50,51,43],{},"仓库中包含两个示例：一个运行在真实的 ESP32-S3 硬件上，另一个运行在 ",[35,48,49],{},"qemu_x86"," 仿真环境中。两个示例的对比和选择建议见",[29,52,54],{"href":53},"#%E9%99%84%E5%BD%95-b%E4%B8%A4%E4%B8%AA%E7%A4%BA%E4%BE%8B%E5%AF%B9%E6%AF%94","附录 B",[16,56,57],{},"本文接下来依次介绍环境准备、编译与部署、功能测试，以及移植过程本身，包括移植中遇到的框架层面的问题。",[12,59,61],{"id":60},"zephyr-简介","Zephyr 简介",[16,63,64],{},"Zephyr 是由 Linux 基金会托管的开源实时操作系统（RTOS），面向资源受限的嵌入式设备。",[16,66,67],{},"Zephyr 有四个特性与本次移植关系密切：",[69,70,71,78,87,93],"ul",{},[72,73,74,77],"li",{},[20,75,76],{},"可配置、可伸缩。"," 系统功能通过 Kconfig 选项开启，最小配置可以压缩到几十 KB。因此同一套代码既能用在简单的传感器节点上，也能用在边缘网关上。",[72,79,80,83,84,86],{},[20,81,82],{},"用 devicetree 描述硬件。"," 开发板之间的差异通过 devicetree 和 board overlay 定义，应用代码与底层硬件无关。正因如此，本次移植可以用同一份应用源码同时编译 ",[35,85,49],{}," 和 ESP32-S3 两个目标。",[72,88,89,92],{},[20,90,91],{},"内置网络协议栈。"," Zephyr 提供 TCP\u002FIP、兼容 BSD 的 socket 层以及 TLS。NanoNNG 的 Zephyr 平台适配层就构建在这套 socket API 之上。",[72,94,95,98,99,102],{},[20,96,97],{},"POSIX API 子集。"," Zephyr 通过 ",[35,100,101],{},"CONFIG_POSIX_API"," 支持 pthreads、文件描述符等接口，原本为 POSIX 环境设计的中间件因此也能移植过来。",[16,104,105,106,109],{},"本文基于 Zephyr 4.4（",[35,107,108],{},"11a87708d41","）。",[12,111,113],{"id":112},"nanomq-简介","NanoMQ 简介",[16,115,116,118],{},[29,117,32],{"href":31}," 是 EMQ 开源的 MQTT Broker，也是 LF Edge 旗下的项目，定位是面向物联网边缘环境的超轻量、高性能 MQTT Broker。EMQX 面向云端部署，NanoMQ 则面向嵌入式系统和工业现场。",[120,121,122],"h3",{"id":122},"架构",[16,124,125],{},"NanoMQ 采用分层架构：",[69,127,128,134,140,146,152],{},[72,129,130,133],{},[20,131,132],{},"平台适配层："," 识别底层硬件和操作系统，向上层提供可移植的 API，避免依赖特定平台。本文通过新增一个平台适配层来支持 Zephyr，依靠的正是这一层设计。",[72,135,136,139],{},[20,137,138],{},"任务层："," 采用 Actor 模型和线程级并行，支持在 SMP 系统上扩展。",[72,141,142,145],{},[20,143,144],{},"传输层："," 通过 pipe 和 client 管理 TCP 和 UDP 流，使用零拷贝机制减少内存占用。",[72,147,148,151],{},[20,149,150],{},"协议层："," 把字节流解析为 MQTT 报文，生成事件，并管理飞行窗口。",[72,153,154,157],{},[20,155,156],{},"应用层："," 集成规则引擎和全局主题树，通过无锁状态机对外提供 MQTT 消息和事件。",[120,159,160],{"id":160},"实现",[16,162,163],{},"NanoMQ 完全用 C 语言实现，没有外部运行时依赖，适合交叉编译到各种目标开发板。",[16,165,166,167,173,174,177],{},"它的异步 I\u002FO 和 Actor 架构构建在 NanoMQ 团队维护的 NNG 派生版本（",[29,168,172],{"href":169,"rel":170},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoNNG",[171],"nofollow","NanoNNG","）之上，这个版本扩展了 MQTT 协议支持，并加入了 ",[35,175,176],{},"nanolib"," 工具库。",[16,179,180],{},"在协议层面，NanoMQ 完整支持 MQTT 3.1.1、MQTT 3.1 和 MQTT 5.0，包括 QoS 0\u002F1\u002F2、保留消息、遗嘱消息、共享订阅、主题别名等 MQTT 特性。",[12,182,183],{"id":183},"环境准备",[120,185,186],{"id":186},"前置条件",[16,188,189],{},"需要准备以下内容：",[69,191,192,228,235,245],{},[72,193,194,195,202,203,206,207,210,211,213,214,217,218],{},"Zephyr 4.4+ 的 west 工作区，以及配套的 Python 虚拟环境。",[20,196,197,198,201],{},"这个 venv 里同时装有 ",[35,199,200],{},"west"," 本身和 Zephyr 的 Python 依赖。"," venv 放在哪里由你决定：Zephyr 官方文档使用工作区根目录下的 ",[35,204,205],{},".venv\u002F","，本文验证所用的环境则放在 ",[35,208,209],{},"~\u002Fzephyr-venv","。激活后，",[35,212,200],{}," 就在 ",[35,215,216],{},"PATH"," 中：",[219,220,226],"pre",{"className":221,"code":223,"language":224,"meta":225},[222],"language-bash","source \u003Cvenv>\u002Fbin\u002Factivate      # ~\u002Fzephyr-venv in this article\nexport ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1\n","bash","",[35,227,223],{"__ignoreMap":225},[72,229,230,231,234],{},"Zephyr SDK 1.0.x。需要单独下载并解压 SDK，然后把 ",[35,232,233],{},"ZEPHYR_SDK_INSTALL_DIR"," 设为它的安装目录。",[72,236,237,238,241,242,244],{},"如果要运行 ESP32-S3 硬件示例，还需要 ",[20,239,240],{},"ESP-IDF"," 环境（见下一节）。只编译 ",[35,243,49],{}," 则不需要。",[72,246,247],{},"功能测试需要额外的 Python 依赖和 Mosquitto 客户端，详见“功能测试”一节。",[16,249,250],{},"验证所用的版本固定如下。e1000 补丁针对的是对应的 Zephyr 源码版本，升级 Zephyr 后需要重新验证。",[252,253,254,267],"table",{},[255,256,257],"thead",{},[258,259,260,264],"tr",{},[261,262,263],"th",{},"组件",[261,265,266],{},"版本 \u002F Commit",[268,269,270,282,290,301],"tbody",{},[258,271,272,276],{},[273,274,275],"td",{},"Zephyr",[273,277,278,279,281],{},"4.4，",[35,280,108],{},"（e1000 补丁适用于该版本）",[258,283,284,287],{},[273,285,286],{},"Zephyr SDK",[273,288,289],{},"1.0.1",[258,291,292,294],{},[273,293,32],{},[273,295,296,297,300],{},"0.25.6（基于上游 ",[35,298,299],{},"master","）",[258,302,303,310],{},[273,304,305,306,309],{},"NanoNNG（",[35,307,308],{},"nng\u002F"," 子模块）",[273,311,312,313,316,317,320,321,324],{},"基于上游 ",[35,314,315],{},"main","，加上 Zephyr 平台适配层。",[35,318,319],{},"zephyr-rtos"," 分支提交了子模块的 gitlink，因此 ",[35,322,323],{},"git clone --recursive"," 会检出完整的平台适配层",[16,326,327,328,330,331,334],{},"如果还没有创建 Zephyr 工作区，可以执行下面的命令（这些命令把 venv 放在 ",[35,329,209],{},"，也就是本文验证环境使用的位置；放在其他位置，或按官方文档的建议放在 ",[35,332,333],{},"\u003Cworkspace>\u002F.venv","，同样可行）：",[219,336,339],{"className":337,"code":338,"language":224,"meta":225},[222],"python3 -m venv ~\u002Fzephyr-venv\nsource ~\u002Fzephyr-venv\u002Fbin\u002Factivate\npip install west\n\nwest init ~\u002Fzephyrproject\ncd ~\u002Fzephyrproject\nwest update\npip install -r zephyr\u002Fscripts\u002Frequirements.txt\n",[35,340,338],{"__ignoreMap":225},[120,342,344],{"id":343},"esp32-s3-开发环境","ESP32-S3 开发环境",[16,346,347,353,354,357,358,361],{},[20,348,349,350,352],{},"本节只适用于 ESP32-S3 硬件示例。如果只编译 ",[35,351,49],{},"，可以跳过本节，使用上一节的标准 Zephyr 环境。"," 硬件示例需要 Espressif 的 HAL blob 以及烧录和串口工具，但",[20,355,356],{},"交叉编译器仍然来自 Zephyr SDK","：1.0.1 已经包含该目标所需的 ",[35,359,360],{},"xtensa-espressif_esp32s3_zephyr-elf"," 工具链，ESP-IDF 不参与编译。",[363,364,365,388,476],"ol",{},[72,366,367,370,371,373,374,377,378,381,382],{},[20,368,369],{},"获取 ESP-IDF","（它提供 ",[35,372,200],{},"、",[35,375,376],{},"esptool"," 和 ",[35,379,380],{},"idf-monitor","）。以下两种安装方式任选其一：",[219,383,386],{"className":384,"code":385,"language":224,"meta":225},[222],"# a) Clone from git\ngit clone --recursive https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesp-idf.git ~\u002Fesp\u002Fesp-idf\ncd ~\u002Fesp\u002Fesp-idf && .\u002Finstall.sh esp32s3\n#    the activation script is ~\u002Fesp\u002Fesp-idf\u002Fexport.sh\n\n# b) Install with the ESP-IDF Installation Manager (eim)\n#    the activation script is ~\u002F.espressif\u002Ftools\u002Factivate_idf_v\u003Cversion>.sh\n",[35,387,385],{"__ignoreMap":225},[72,389,390,393,394,396,397,400,401,404,405,411,413,414,417,418,424,426,429,430,432,433,436,437,439,440,442,443,373,445,447,448,451,452,454,455,457,458,377,460,463,464,467,468,471,472,475],{},[20,391,392],{},"每次编译前激活环境","，并把 ",[35,395,233],{}," 指向 Zephyr SDK。",[398,399],"br",{},"本文的验证环境在 ",[35,402,403],{},"~\u002F.bashrc"," 中把这一步写成了 alias：",[219,406,409],{"className":407,"code":408,"language":224,"meta":225},[222],"alias esp-zephyr='source ~\u002F.espressif\u002Ftools\u002Factivate_idf_v6.1.sh \\\n    && export ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1'\n",[35,410,408],{"__ignoreMap":225},[398,412],{},"每次编译前先运行 ",[35,415,416],{},"esp-zephyr","。展开后相当于：",[219,419,422],{"className":420,"code":421,"language":224,"meta":225},[222],"source ~\u002F.espressif\u002Ftools\u002Factivate_idf_v6.1.sh   # ~\u002Fesp\u002Fesp-idf\u002Fexport.sh if installed from git\nexport ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1\nexport ZEPHYR_TOOLCHAIN_VARIANT=zephyr\n",[35,423,421],{"__ignoreMap":225},[398,425],{},[20,427,428],{},"激活这个环境后，就用它替代上一节的 Zephyr venv","：ESP-IDF 的 Python 环境自带 ",[35,431,200],{},"（以及 ",[35,434,435],{},"python3","），两者激活一个即可，以最后激活的为准。编译 ESP32 目标时用这个环境；如果只编译 ",[35,438,49],{},"，用上一节的 Zephyr venv 即可，不需要安装 ESP-IDF。",[398,441],{},"激活 ESP-IDF 与使用哪个编译器无关，它只是把 ",[35,444,200],{},[35,446,376],{},"（",[35,449,450],{},"west flash"," 的 ESP32 runner 会调用它）和 ",[35,453,380],{}," 加入 ",[35,456,216],{},"。编译器由 ",[35,459,233],{},[35,461,462],{},"ZEPHYR_TOOLCHAIN_VARIANT=zephyr"," 决定，后者指向 Zephyr SDK 中对应的 Xtensa 工具链。",[20,465,466],{},"不要","把它设为 ",[35,469,470],{},"espressif","：当前的 Zephyr 版本已经移除了这个 variant，设置后 CMake 会去查找源码树中并不存在的文件。也可以完全不设置 ",[35,473,474],{},"ZEPHYR_TOOLCHAIN_VARIANT","，Zephyr 会自己找到 Zephyr SDK，但显式设置更不容易出错。",[72,477,478,481,482,488,490,493,494,497,498,501,502,504,505,510],{},[20,479,480],{},"获取 Espressif HAL blob","，在工作区中执行一次即可：",[219,483,486],{"className":484,"code":485,"language":224,"meta":225},[222],"cd ~\u002Fzephyrproject\nwest blobs fetch hal_espressif\n",[35,487,485],{"__ignoreMap":225},[398,489],{},[20,491,492],{},"不要跳过这一步。"," 缺少 blob 时，",[35,495,496],{},"CONFIG_WIFI_ESP32"," 会在没有任何提示的情况下变为不可用。编译照样成功，固件也能正常启动，但 Wi-Fi 无法使用，构建系统也不会给出任何警告。串口控制台上永远不会出现预期的 ",[35,499,500],{},"wifi: connected"," 信息。",[398,503],{},"各发行版的具体配置说明（包括系统依赖、SDK 安装、权限和常见错误）见 ",[29,506,509],{"href":507,"rel":508},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoMQ-Zephyr-Example\u002Fblob\u002Fmain\u002Fexamples\u002Fnanomq_zephyr_esp32s3\u002Fsetup-fedora-zh.md",[171],"setup-fedora-zh.md","。该指南以 Fedora 为例，使用其他发行版时请相应替换包管理器命令。",[120,512,513],{"id":513},"获取源码",[219,515,518],{"className":516,"code":517,"language":224,"meta":225},[222],"cd ~\u002Fzephyrproject\ngit clone --recursive -b zephyr-rtos https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq.git\ncd nanomq\n",[35,519,517],{"__ignoreMap":225},[16,521,522,525,526,528,529,531,532,534],{},[35,523,524],{},"--recursive"," 选项会同时拉取 ",[35,527,308],{}," 子模块，其中是带有 Zephyr 平台适配层的 NanoNNG 分支。",[35,530,319],{}," 分支在仓库中提交了子模块的 gitlink，因此检出的版本与上表列出的 commit 一致。",[35,533,323],{}," 总是检出仓库记录的 gitlink；要切换到其他 commit，需要显式执行 checkout。",[16,536,537,538,540],{},"如果克隆时没有加 ",[35,539,524],{},"，可以用以下命令初始化子模块：",[219,542,545],{"className":543,"code":544,"language":224,"meta":225},[222],"git submodule update --init --recursive\n",[35,546,544],{"__ignoreMap":225},[16,548,549,550,553,554,557,558,557,561,557,564,567,568,570,571,573],{},"克隆完成后，用 ",[35,551,552],{},"git submodule status nng"," 检查子模块：如果输出的开头",[20,555,556],{},"不是"," ",[35,559,560],{},"-",[20,562,563],{},"也不是",[35,565,566],{},"+","，说明子模块已经检出到仓库记录的 gitlink（",[35,569,560],{}," 表示尚未初始化，",[35,572,566],{}," 表示检出的 commit 与记录的不一致）。",[16,575,576,577,580],{},"本文后续所有命令都在仓库根目录（",[35,578,579],{},"~\u002Fzephyrproject\u002Fnanomq","）下执行。",[120,582,584],{"id":583},"qemu_x86zephyr-e1000-驱动补丁","qemu_x86：Zephyr e1000 驱动补丁",[16,586,587,589,590,593,594,597],{},[35,588,49],{}," 示例需要给 Zephyr 的 e1000 驱动打一个两行的补丁。",[20,591,592],{},"这个补丁是必需的。"," 不打补丁时，Broker 能正常启动并打印 ",[35,595,596],{},"NanoMQ Broker is started successfully!","，但任何客户端都无法建立连接。",[16,599,600,601,604],{},"问题的原因是 QEMU 的 e1000 设备模型在复位时会清零 RCTL 寄存器，其中包括 ",[20,602,603],{},"RCTL_BAM","（Broadcast Accept Mode，bit 15）。真实硬件默认会置位这一位，设备模型却不会。",[16,606,607,608,611,612,615,616,619,620,622],{},"Zephyr 的 ",[35,609,610],{},"eth_e1000"," 驱动只写入 ",[35,613,614],{},"RCTL_EN | RCTL_MPE","，从不设置 BAM。结果是",[20,617,618],{},"设备模型会静默丢弃所有广播帧","，ARP 解析因此失败，SLIRP 也就无法转发最初的 TCP 连接。截至 4.4（",[35,621,108],{},"），上游 Zephyr 中仍然存在这个问题。",[16,624,625,626,629,630,633],{},"补丁要打在 ",[20,627,628],{},"Zephyr 源码树本身","（工作区中的 ",[35,631,632],{},"zephyr\u002F"," 目录，而不是本仓库）。",[16,635,636,637,640],{},"把下面的 diff 保存为 ",[35,638,639],{},"zephyr-e1000-bam.patch","，然后用以下命令应用：",[219,642,645],{"className":643,"code":644,"language":224,"meta":225},[222],"cd ~\u002Fzephyrproject\u002Fzephyr\ngit apply \u002Fpath\u002Fto\u002Fzephyr-e1000-bam.patch\n",[35,646,644],{"__ignoreMap":225},[219,648,651],{"className":649,"code":650,"language":224,"meta":225},[222],"diff --git a\u002Fdrivers\u002Fethernet\u002Feth_e1000.c b\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n--- a\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n+++ b\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n@@ -329,7 +329,7 @@ static const struct ethernet_api e1000_api = {\n                                    \\\n        irq_enable(DT_INST_IRQN(inst));             \\\n        iow32(dev, CTRL, CTRL_SLU); \u002F* Set link up *\u002F       \\\n-       iow32(dev, RCTL, RCTL_EN | RCTL_MPE | DT_INST_PROP(inst, rdmts) \u003C\u003C RDMTS_OFFSET); \\\n+       iow32(dev, RCTL, RCTL_EN | RCTL_MPE | RCTL_BAM | DT_INST_PROP(inst, rdmts) \u003C\u003C RDMTS_OFFSET); \\\n        iow32(dev, ITR, DT_INST_PROP(inst, itr) & (uint32_t)GENMASK(15, 0)); \\\n    }                               \\\n                                    \\\ndiff --git a\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h b\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n--- a\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n+++ b\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n@@ -27,6 +27,7 @@ extern \"C\" {\n #define IMS_RXT0   (1 \u003C\u003C 7) \u002F* Receiver Timer *\u002F\n\n #define RCTL_MPE   (1 \u003C\u003C 4) \u002F* Multicast Promiscuous Enabled *\u002F\n+#define RCTL_BAM   (1 \u003C\u003C 15) \u002F* Broadcast Accept Mode *\u002F\n\n #define TDESC_EOP       (1) \u002F* End Of Packet *\u002F\n #define TDESC_RS   (1 \u003C\u003C 3) \u002F* Report Status *\u002F\n",[35,652,650],{"__ignoreMap":225},[16,654,655,656,659,660,663],{},"注意，",[35,657,658],{},"RCTL_MPE"," 的含义是 ",[20,661,662],{},"Multicast"," Promiscuous Enabled（bit 4），只影响组播接收，并不会开启广播接收，所以驱动现有的设置不能代替 BAM。也正是因为这一区别，这个问题长期没有被发现。",[16,665,666,667],{},"这个补丁只影响 QEMU 模拟的网卡。",[20,668,669],{},"ESP32-S3 硬件示例不需要这个补丁。",[120,671,672],{"id":672},"端口与服务",[16,674,675,676,678],{},"在本文验证过的配置下，两个示例开放相同的 MQTT、REST 和 WebSocket 端口。对于 ",[35,677,49],{},"，端口实际能否访问取决于下一节介绍的端口转发配置。",[252,680,681,699],{},[255,682,683],{},[258,684,685,688,691,694,696],{},[261,686,687],{},"服务",[261,689,690],{},"端口",[261,692,693],{},"ESP32-S3",[261,695,49],{},[261,697,698],{},"认证 \u002F 加密",[268,700,701,717,732],{},[258,702,703,706,709,712,714],{},[273,704,705],{},"MQTT over TCP",[273,707,708],{},"1883",[273,710,711],{},"支持",[273,713,711],{},[273,715,716],{},"无认证，无 TLS",[258,718,719,722,725,727,729],{},[273,720,721],{},"REST API",[273,723,724],{},"8081",[273,726,711],{},[273,728,711],{},[273,730,731],{},"Basic 认证；凭据需要自行配置（没有可用的默认值），无 TLS",[258,733,734,737,740,742,744],{},[273,735,736],{},"MQTT over WebSocket",[273,738,739],{},"8083",[273,741,711],{},[273,743,711],{},[273,745,746,747],{},"无认证，无 TLS；路径为 ",[35,748,749],{},"\u002Fmqtt",[16,751,752,753,756,757,760,761,377,764,767,768,771,772,775,776,779,780,783,784,779,787,790],{},"REST 监听器通过 Kconfig 选项 ",[35,754,755],{},"CONFIG_BROKER_REST_API"," 开启：",[35,758,759],{},"main.c"," 根据它设置 ",[35,762,763],{},"http_server.enable",[35,765,766],{},"auth_type","，而 ",[35,769,770],{},"conf_init"," 中的默认值是关闭。",[20,773,774],{},"但只打开这个开关还不够","：",[35,777,778],{},"CONFIG_BROKER_REST_USER"," \u002F ",[35,781,782],{},"CONFIG_BROKER_REST_PASS"," 默认都是空字符串，也就是说没有可用的默认凭据；上游公开的凭据 ",[35,785,786],{},"admin",[35,788,789],{},"public"," 同样会被拒绝。只有把两者都设为你自己的凭据，8081 端口才会监听；否则 Broker 会在启动时打印一行说明，并保持该端口关闭。如果你自己从配置中去掉这个开关，8081 端口同样不会有服务监听，客户端会收到 RST。这是预期行为，并不是故障。",[120,792,793],{"id":793},"安全注意事项",[16,795,796,797,800],{},"两个示例的默认配置都假定运行在",[20,798,799],{},"受控的实验环境","中。不加修改直接部署会带来安全风险。",[252,802,803,813],{},[255,804,805],{},[258,806,807,810],{},[261,808,809],{},"项目",[261,811,812],{},"当前配置",[268,814,815,835,858,866],{},[258,816,817,820],{},[273,818,819],{},"监听地址",[273,821,822,823,826,827,830,831,834],{},"MQTT ",[35,824,825],{},"nmq-tcp:\u002F\u002F0.0.0.0:1883","、WebSocket ",[35,828,829],{},"nmq-ws:\u002F\u002F0.0.0.0:8083\u002Fmqtt"," 和 REST ",[35,832,833],{},"0.0.0.0:8081"," 都监听所有网络接口",[258,836,837,840],{},[273,838,839],{},"REST 认证",[273,841,842,843,779,845,848,849,852,853,779,855,857],{},"Basic 认证；凭据来自 Kconfig 选项 ",[35,844,778],{},[35,846,847],{},"_PASS","，两者",[20,850,851],{},"默认都是空字符串（没有可用的默认凭据）","；未设置这两项，或仍设为上游公开的 ",[35,854,786],{},[35,856,789],{}," 时，8081 端口不会监听",[258,859,860,863],{},[273,861,862],{},"MQTT 认证",[273,864,865],{},"未启用，任何客户端都可以连接、发布和订阅",[258,867,868,871],{},[273,869,870],{},"传输加密",[273,872,873],{},"无。NanoNNG 的 Zephyr 移植没有启用 TLS，因此 MQTT、WebSocket 和 REST 流量都以明文传输",[16,875,876,879],{},[20,877,878],{},"在明文 HTTP 上，Basic 认证只是 Base64 编码，并不是加密。"," 因此，同一网络路径上的观察者可以还原出凭据。它能防止误访问，但挡不住有意的攻击。真正的访问控制仍然要靠网络边界。",[16,881,882,883,779,885,887,888,891,892,109,894],{},"在实验网络以外使用示例之前，至少要做到：把监听地址限制在实际需要的网络接口上；设置你自己的 REST 凭据（公开的 ",[35,884,786],{},[35,886,789],{}," 组合现在会被拒绝）；把管理接口放在可信网络之后，或放在额外的 TLS 代理之后。至于 MQTT 认证：如果这个移植版本的认证路径已经验证过，可以直接启用；否则应在链路层或 TLS 代理处实施访问控制。",[20,889,890],{},"不要以为通用版 NanoMQ 的认证设置可以直接用在这些示例的嵌入式配置上","（两者的配置来源不同，见",[29,893,42],{"href":41},[20,895,896],{},"不要把示例的默认配置暴露在公网或不可信网络中。",[12,898,900],{"id":899},"编译与运行esp32-s3-硬件示例","编译与运行：ESP32-S3 硬件示例",[120,902,904],{"id":903},"_1-配置本地凭据wi-fi-与-rest","1. 配置本地凭据（Wi-Fi 与 REST）",[16,906,907,908,911,912,915,916,919],{},"将 ",[35,909,910],{},"local.conf.example"," 复制为 ",[35,913,914],{},"local.conf","。该文件已列在 ",[35,917,918],{},".gitignore"," 中，你的凭据不会提交到仓库。",[219,921,924],{"className":922,"code":923,"language":224,"meta":225},[222],"cp demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf.example \\\n   demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf\n",[35,925,923],{"__ignoreMap":225},[16,927,928,929,932],{},"编辑 ",[35,930,931],{},"demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf","，填入 SSID 和 PSK：",[219,934,937],{"className":935,"code":936,"language":224,"meta":225},[222],"CONFIG_BROKER_WIFI_SSID=\"your-ssid\"\nCONFIG_BROKER_WIFI_PSK=\"your-passphrase\"\n\n# prj.conf already enables CONFIG_BROKER_REST_API, but 8081 only starts\n# listening once these two are set (and are not the public admin\u002Fpublic);\n# leaving them unset means the port stays closed.\nCONFIG_BROKER_REST_USER=\"your-rest-user\"\nCONFIG_BROKER_REST_PASS=\"your-rest-passphrase\"\n",[35,938,936],{"__ignoreMap":225},[120,940,942],{"id":941},"_2-编译","2. 编译",[16,944,945,948],{},[35,946,947],{},"EXTRA_CONF_FILE"," 的路径相对于应用目录解析：",[219,950,953],{"className":951,"code":952,"language":224,"meta":225},[222],"west build -b esp32s3_devkitc\u002Fesp32s3\u002Fprocpu -d build\u002Fesp32s3_nanomq \\\n    demo\u002Fnanomq_zephyr_esp32s3 -- -DEXTRA_CONF_FILE=local.conf\n",[35,954,952],{"__ignoreMap":225},[16,956,957],{},"本文验证的配置启用了 MQTT、REST 和 WebSocket，编译成功后会输出如下内存报告：",[219,959,962],{"className":960,"code":961,"language":224,"meta":225},[222],"Memory region         Used Size  Region Size  %age Used\n           FLASH:      961204 B   16776960 B      5.73%\n     iram0_0_seg:       54028 B     415492 B     13.00%\n     dram0_0_seg:      379640 B     399108 B     95.12%\n     irom0_0_seg:      686320 B        32 MB      2.05%\n     drom0_0_seg:      830132 B        32 MB      2.47%\n    ext_dram_seg:     5152752 B        32 MB     15.36%\n...\nSuccessfully created ESP32-S3 image.\n",[35,963,961],{"__ignoreMap":225},[16,965,966],{},"其中有两个数字尤其重要：",[69,968,969,979],{},[72,970,971,974,975,978],{},[35,972,973],{},"dram0_0_seg"," 对应内部 SRAM，使用率已达 ",[20,976,977],{},"95.12%","。这是最紧张的内存区域，也是要把 Broker 堆和静态内存池移到 PSRAM 的原因；",[72,980,981,984,985,988,989,992,993,43],{},[35,982,983],{},"ext_dram_seg"," 显示 ",[20,986,987],{},"5,152,752 B（≈5.2 MB）","，这是",[20,990,991],{},"实际放入外部 PSRAM 的数据量","，占该区域的 15.36%。这个数字与该区域 32 MB 的大小、模组 16 MB 的容量之间是什么关系，见下文",[29,994,996],{"href":995},"#%E5%85%B3%E4%BA%8E%E5%BC%80%E5%8F%91%E6%9D%BF","关于开发板",[120,998,1000],{"id":999},"_3-烧录固件","3. 烧录固件",[16,1002,1003],{},"通过 USB 将开发板连接到主机。开发板可能使用板载 USB-UART 桥接芯片，也可能使用 ESP32-S3 原生的 USB-JTAG\u002FSerial 接口，因此设备会显示为以下两种之一：",[69,1005,1006,1012],{},[72,1007,1008,1011],{},[35,1009,1010],{},"\u002Fdev\u002FttyUSB*","：独立的 USB-UART 桥接芯片，例如 CP2102 或 CH34x",[72,1013,1014,1017],{},[35,1015,1016],{},"\u002Fdev\u002FttyACM*","：原生 USB CDC，包括 ESP32-S3 的 USB-JTAG\u002FSerial 接口",[16,1019,1020,1021,1024],{},"本文以 ",[35,1022,1023],{},"\u002Fdev\u002FttyUSB0"," 为例。请在你的系统上查看实际的设备名：",[219,1026,1029],{"className":1027,"code":1028,"language":224,"meta":225},[222],"ls \u002Fdev\u002FttyUSB* \u002Fdev\u002FttyACM*\n# Or check which device node appears when you connect the board\ndmesg | tail -5\n",[35,1030,1028],{"__ignoreMap":225},[16,1032,1033],{},"如果烧录时因权限不足而失败，把当前用户加入设备所属的用户组，然后重新登录使更改生效：",[219,1035,1038],{"className":1036,"code":1037,"language":224,"meta":225},[222],"ls -l \u002Fdev\u002FttyUSB0                 # The fourth column shows the group\nsudo usermod -aG \u003Cgroup> \"$USER\"\n",[35,1039,1037],{"__ignoreMap":225},[16,1041,1042,1043,1046,1047,1050,1051,1054],{},"这个用户组在 Debian\u002FUbuntu 和 Fedora 上通常是 ",[35,1044,1045],{},"dialout","，在 Arch 上是 ",[35,1048,1049],{},"uucp","。请始终以 ",[35,1052,1053],{},"ls -l"," 显示的用户组为准，不要按发行版的默认值去推测。",[16,1056,1057,1058,1060],{},"然后烧录固件，把 ",[35,1059,1023],{}," 替换为实际的设备名：",[219,1062,1065],{"className":1063,"code":1064,"language":224,"meta":225},[222],"west flash -d build\u002Fesp32s3_nanomq --runner esp32 --esp-device \u002Fdev\u002FttyUSB0\n",[35,1066,1064],{"__ignoreMap":225},[120,1068,1070],{"id":1069},"_4-查看串口控制台","4. 查看串口控制台",[16,1072,1073,1074,1076,1077,1080,1081,1083],{},"波特率为 115200。可以使用 ",[35,1075,380],{}," 或 ",[35,1078,1079],{},"miniterm","。给 ",[35,1082,380],{}," 传入 ELF 文件后，它可以解码二进制日志：",[219,1085,1088],{"className":1086,"code":1087,"language":224,"meta":225},[222],"idf-monitor --port \u002Fdev\u002FttyUSB0 build\u002Fesp32s3_nanomq\u002Fzephyr\u002Fzephyr.elf\n",[35,1089,1087],{"__ignoreMap":225},[16,1091,1092],{},"下面是一次实际运行的启动日志，其中省略了路径前缀，SSID 用占位符代替：",[219,1094,1097],{"className":1095,"code":1096,"language":224,"meta":225},[222],"I (octal_psram): vendor id    : 0x0d (AP)\nI (octal_psram): density      : 0x05 (128 Mbit)\nI (esp_psram): Found 16MB PSRAM device\nI (esp_psram): Speed: 80MHz\nI (esp_psram): SPI SRAM memory test OK\n*** Booting Zephyr OS build v4.4.0-4779-g11a87708d415 ***\nwifi: connecting to \"\u003Cyour-ssid>\" (attempt 1)\nwifi: connected to \"\u003Cyour-ssid>\"\nwifi: connected — starting DHCPv4 client\nwifi: IPv4 address assigned (DHCPv4)\nsntp: ntp.aliyun.com: epoch=1789468030, realtime seeded\nnet: iface 0x3fc96fd8 dev=wifi up=1\nnet: ipv4 \u003Cboard-ip>\n2026-09-15 10:27:10 [0] INFO  web_server.c:548 start_rest_server: http:\u002F\u002F0.0.0.0:8081\u002Fapi\u002Fv4\n2026-09-15 10:27:10 [0] WARN  broker.c:1400 broker: NanoMQ (ver 0.25.6) Serving HTTP Server on http:\u002F\u002F(null):8081\nNanoMQ Broker is started successfully!\n",[35,1098,1096],{"__ignoreMap":225},[16,1100,1101,1102,1104,1105,377,1108,1111,1112,1115],{},"只有在编译时配置了 REST 凭据（即第 1 步中的 ",[35,1103,914],{},"），才会出现 ",[35,1106,1107],{},"start_rest_server",[35,1109,1110],{},"Serving HTTP Server"," 这两行。未配置时，这个位置只会显示一行 ",[35,1113,1114],{},"rest: REST API stays off - ...","，8081 端口上也没有任何服务监听。",[16,1117,1118,1119,1122,1123,1125],{},"启动顺序为：",[20,1120,1121],{},"PSRAM 检测与内存测试 → Zephyr 启动 → Wi-Fi 关联 → DHCP 分配地址 → SNTP 校时 → Broker 初始化","。最后一行出现 ",[35,1124,596],{}," 时，Broker 即已就绪。",[120,1127,1129],{"id":1128},"_5-验证连接","5. 验证连接",[16,1131,1132,1133,1135],{},"Broker 就绪后，在同一网络中的主机上执行以下检查。先测试 REST API（凭据就是你在 ",[35,1134,914],{}," 中设置的那一组；如果没有设置，8081 端口上没有服务监听，curl 会直接连接失败，这是正常现象）：",[219,1137,1140],{"className":1138,"code":1139,"language":224,"meta":225},[222],"curl -u \u003Crest-user>:\u003Crest-pass> http:\u002F\u002F\u003Cboard-ip>:8081\u002Fapi\u002Fv4\u002Fbrokers\u002F\n",[35,1141,1139],{"__ignoreMap":225},[16,1143,1144],{},"测试 MQTT 消息收发时，在任意终端中用 Mosquitto 客户端订阅：",[219,1146,1149],{"className":1147,"code":1148,"language":224,"meta":225},[222],"mosquitto_sub -h \u003Cboard-ip> -t 'test\u002F#' -v\n",[35,1150,1148],{"__ignoreMap":225},[16,1152,1153],{},"在另一个终端中发布一条消息：",[219,1155,1158],{"className":1156,"code":1157,"language":224,"meta":225},[222],"mosquitto_pub -h \u003Cboard-ip> -t 'test\u002Fhello' -m 'hello from mosquitto' -q 1\n",[35,1159,1157],{"__ignoreMap":225},[16,1161,1162,1163],{},"订阅端应收到：",[35,1164,1165],{},"test\u002Fhello hello from mosquitto",[120,1167,996],{"id":996},[16,1169,1170,1171,1176,1177,1180,1181,1184,1185,1188,1189,1192,1193,1196,1197,1200,1201,1204,1205,1207,1208,43],{},"本示例在 ",[29,1172,1175],{"href":1173,"rel":1174},"https:\u002F\u002Fdocs.espressif.com\u002Fprojects\u002Fesp-dev-kits\u002Fzh_CN\u002Flatest\u002Fesp32s3\u002Fesp32-s3-lcd-ev-board\u002Fuser_guide.html#esp32-s3-lcd-ev-board-v1-5",[171],"ESP32-S3-LCD-EV-Board"," 上验证，使用 ESP32-S3-WROOM-1-",[20,1178,1179],{},"N16R16V"," 模组（16 MB Flash 和 16 MB ",[20,1182,1183],{},"八线"," PSRAM）。板级配置沿用 ",[35,1186,1187],{},"esp32s3_devkitc\u002Fesp32s3\u002Fprocpu"," 的 SoC 定义。模组配置上的差异体现在两处：",[35,1190,1191],{},"boards\u002Fesp32s3_devkitc_procpu.overlay","（Flash 与 PSRAM 容量）和 ",[35,1194,1195],{},"prj.conf"," 中的 ",[35,1198,1199],{},"CONFIG_SPIRAM_MODE_OCT","（八线模式）。",[20,1202,1203],{},"如果你的模组不是 N16R16V，这两处都必须修改","。八线（Octal）和四线（Quad）PSRAM 的配置不能只改一个参数就互换：使用四线 PSRAM 的模组还需要把 ",[35,1206,1199],{}," 换成 ",[35,1209,1210],{},"CONFIG_SPIRAM_MODE_QUAD",[16,1212,1213],{},"链接报告显示：FLASH 961,204 B，内部 SRAM 379,640 B \u002F 399,108 B（95.12%），放入外部 PSRAM 的数据为 5,152,752 B。",[16,1215,1216,1217,447,1220,1223,1224,1226,1227,1230,1231,1234,1235,1237,1238,1241,1242,1245,1246,1249],{},"最后这个数字（约 5.2 MB）很容易被误读为 PSRAM 的容量或其地址窗口的大小，所以有必要说清这三个值之间的关系。模组的 PSRAM 物理容量是 ",[20,1218,1219],{},"16 MB",[35,1221,1222],{},"CONFIG_ESP_SPIRAM_SIZE","）。链接脚本把 ",[35,1225,983],{}," 声明为一个 ",[20,1228,1229],{},"32 MB 的区域","，这是它与 ",[35,1232,1233],{},"drom0_0_seg"," 共用的 DCACHE0 地址窗口，只是地址空间的上限，并不代表可用容量。链接报告中的 ",[20,1236,987],{}," 是该区域的 ",[20,1239,1240],{},"Used Size","，即实际放入 PSRAM 的数据量。其中包括 ",[35,1243,1244],{},"CONFIG_ESP_SPIRAM_HEAP_SIZE"," 指定的 4 MB Broker 堆，以及从内部 SRAM 移出的静态内存池，例如 Wi-Fi 驱动的 ",[35,1247,1248],{},".noinit"," 段、net_buf 内存池和 Zephyr POSIX 对象池。也就是说，5.2 MB 既不是 PSRAM 的容量，也不是它的映射窗口大小。",[16,1251,1252,1255,1256,1258,1259,1262],{},[20,1253,1254],{},"时钟","。开发板上没有 RTC。DHCP 完成后，",[35,1257,759],{}," 会执行一次 SNTP 同步，为 ",[35,1260,1261],{},"CLOCK_REALTIME"," 设置初始时间：最多把两台公共 SNTP 服务器轮流查询两遍，任意一台响应即返回。因此日志中的时间戳是真实的 UTC 时间。这一步只是尽力而为：如果没有服务器响应，Broker 仍会正常启动，只是时间戳停留在 Unix 纪元（每次查询 2 秒，两遍之间间隔 1 秒，所以最坏情况下启动时间会增加约 9 秒）。",[12,1264,1266],{"id":1265},"编译与运行qemu_x86","编译与运行：qemu_x86",[120,1268,1270],{"id":1269},"_1-编译","1. 编译",[16,1272,1273,1274,1276],{},"打上 e1000 补丁后执行以下命令（先激活“环境准备 → 前置条件”中的 Zephyr 工作区 venv，确保 ",[35,1275,200],{}," 可用；不需要 ESP-IDF）：",[219,1278,1281],{"className":1279,"code":1280,"language":224,"meta":225},[222],"west build -b qemu_x86 -d build\u002Fnanomq_zephyr_qemu_x86 demo\u002Fnanomq_zephyr_qemu_x86\n",[35,1282,1280],{"__ignoreMap":225},[16,1284,1285],{},"编译成功时，输出的结尾类似这样：",[219,1287,1290],{"className":1288,"code":1289,"language":224,"meta":225},[222],"[289\u002F290] Building C object zephyr\u002FCMakeFiles\u002Fzephyr_final.dir\u002Fmisc\u002Fempty_file.c.obj\n[290\u002F290] Linking C executable zephyr\u002Fzephyr.elf\nMemory region         Used Size  Region Size  %age Used\n             RAM:     2608020 B        31 MB      8.02%\n        IDT_LIST:           0 B         2 KB      0.00%\nGenerating files from build\u002Fnanomq_zephyr_qemu_x86\u002Fzephyr\u002Fzephyr.elf for board: qemu_x86\u002Fatom\n",[35,1291,1289],{"__ignoreMap":225},[16,1293,1294],{},"（这个数值对代码布局很敏感：代码改动几十个字节，就可能让某个段跨过页边界，使报告的数值整整变化一个 4 KB 的页。这部分是对齐填充，并不是实际增长。）",[16,1296,1297,1298,1301,1302,1305,1306,1308,1309,1311,1312,1315],{},"结尾显示的 ",[35,1299,1300],{},"qemu_x86\u002Fatom"," 是完整的板卡名称。",[35,1303,1304],{},"atom"," 是 ",[35,1307,49],{}," 的 SoC 限定符，所用的板卡\u002FSoC 限定机制与 ESP32-S3 一侧的 ",[35,1310,1187],{}," 相同。这并不表示 ",[35,1313,1314],{},"-b qemu_x86"," 选错了板卡。",[120,1317,1319],{"id":1318},"_2-运行","2. 运行",[16,1321,1322],{},"推荐的做法是在后台启动 QEMU，并把串口控制台的输出写入文件，这样终端可以留给后面的验证步骤使用：",[219,1324,1327],{"className":1325,"code":1326,"language":224,"meta":225},[222],"qemu-system-i386 -m 32 -cpu qemu32,+nx,+pae,sse,sse2,pni -machine q35 \\\n    -device isa-debug-exit,iobase=0xf4,iosize=0x04 -no-reboot -machine acpi=off \\\n    -serial file:\u002Ftmp\u002Fqemu.log -display none \\\n    -netdev user,id=n1,hostfwd=tcp:127.0.0.1:1883-:1883,hostfwd=tcp:127.0.0.1:8081-:8081,hostfwd=tcp:127.0.0.1:8083-:8083 \\\n    -device e1000,netdev=n1 \\\n    -kernel build\u002Fnanomq_zephyr_qemu_x86\u002Fzephyr\u002Fzephyr.elf &\n",[35,1328,1326],{"__ignoreMap":225},[16,1330,1331,1332,1196,1334,1337,1338,1341,1342],{},"端口转发通过 ",[35,1333,1195],{},[35,1335,1336],{},"CONFIG_NET_QEMU_USER_EXTRA_ARGS"," 配置，它把 1883\u002F8081\u002F8083 绑定到 ",[35,1339,1340],{},"127.0.0.1","，与上面的手动命令一致。两者遵循同一条规则：",[20,1343,1344],{},"谁运行 QEMU，谁就决定它的端口绑定到哪些网络接口。",[16,1346,1347],{},"也可以让 west 直接启动 QEMU：",[219,1349,1352],{"className":1350,"code":1351,"language":224,"meta":225},[222],"west build -d build\u002Fnanomq_zephyr_qemu_x86 -t run\n",[35,1353,1351],{"__ignoreMap":225},[16,1355,1356,1357,1359,1360,1363],{},"这种方式会自动使用 ",[35,1358,1195],{}," 中的三个端口转发，",[20,1361,1362],{},"但会把串口控制台接到 stdio 上，并占用当前终端","，所以只适合用来观察启动过程。",[16,1365,1366,1369],{},[20,1367,1368],{},"重启前先清理旧实例","。残留的 QEMU 会一直占用这三个端口，新实例会立即启动失败：",[219,1371,1374],{"className":1372,"code":1373,"language":224,"meta":225},[222],"qemu-system-i386: ... Could not set up host forwarding rule 'tcp:127.0.0.1:8083-:8083'\n",[35,1375,1373],{"__ignoreMap":225},[16,1377,1378,1381],{},[35,1379,1380],{},"-t run"," 尤其容易留下这种进程：停止它时，它启动的 QEMU 实例不一定会一起退出。清理时，按端口查出对应的 PID：",[219,1383,1386],{"className":1384,"code":1385,"language":224,"meta":225},[222],"ss -ltnp | grep -E ':(1883|8081|8083)' | grep -oP 'pid=\\K[0-9]+' | sort -u | xargs -r kill\n",[35,1387,1385],{"__ignoreMap":225},[16,1389,1390,1393,1394,1396,1397,1400,1401,1404,1405,43],{},[35,1391,1392],{},"qemu-system-i386"," 也可以由 west 自行找到（可以是 Zephyr SDK 的主机工具，也可以是 ",[35,1395,216],{}," 中由发行版软件包提供的版本，例如 Fedora 的 ",[35,1398,1399],{},"qemu-system-x86","）。如果手动运行时报 ",[35,1402,1403],{},"command not found","，请把 QEMU 所在的目录加入 ",[35,1406,216],{},[16,1408,1409,1412],{},[35,1410,1411],{},"-m 32"," 选项为 QEMU 分配 32 MB 内存。Zephyr 的链接脚本会预留其中一部分，因此链接报告中显示的可用 RAM 约为 31 MB（本文使用的镜像占用约 2.5 MB，其中约 1 MB 是 malloc arena）。",[120,1414,1416],{"id":1415},"_3-启动日志","3. 启动日志",[16,1418,1419],{},"以下是一次实际运行的启动日志（省略了部分 DEBUG 行和路径前缀）：",[219,1421,1424],{"className":1422,"code":1423,"language":224,"meta":225},[222],"*** Booting Zephyr OS build v4.4.0-4779-g11a87708d415 ***\nrtc: CMOS clock 2026-09-15 10:27:23 UTC, realtime seeded\nnet: iface 0x19e94c dev=eth0 up=1\nnet: ipv4 10.0.2.15\n2026-09-15 10:27:23 [0] DEBUG broker.c:1102 broker: db init finished\n2026-09-15 10:27:23 [0] DEBUG broker.c:1111 broker: listener init finished\n2026-09-15 10:27:23 [0] DEBUG broker.c:1128 broker: HTTP init finished\n2026-09-15 10:27:23 [0] INFO  web_server.c:548 start_rest_server: http:\u002F\u002F0.0.0.0:8081\u002Fapi\u002Fv4\n2026-09-15 10:27:23 [0] WARN  broker.c:1400 broker: NanoMQ (ver 0.25.6) Serving HTTP Server on http:\u002F\u002F(null):8081\nNanoMQ Broker is started successfully!\n",[35,1425,1423],{"__ignoreMap":225},[16,1427,1428],{},"只有在编译时配置了凭据，才会出现那两行 REST 日志。",[16,1430,1431,1432,1434,1435,1437,1438,779,1440,1442],{},"最后一行出现 ",[35,1433,596],{}," 时，Broker 即已就绪。之后可以验证 REST API（前提是编译时已通过 ",[35,1436,914],{}," 设置了 ",[35,1439,778],{},[35,1441,847],{},"；否则 8081 端口上没有服务监听）：",[219,1444,1447],{"className":1445,"code":1446,"language":224,"meta":225},[222],"curl -u \u003Crest-user>:\u003Crest-pass> http:\u002F\u002F127.0.0.1:8081\u002Fapi\u002Fv4\u002Fbrokers\u002F\n",[35,1448,1446],{"__ignoreMap":225},[120,1450,1452],{"id":1451},"关于-malloc-arena","关于 malloc arena",[16,1454,1455,1457,1458,1461,1462,1465,1466,1469],{},[35,1456,49],{}," 启用了 MMU，而 Zephyr 的 libc 默认提供的 ",[35,1459,1460],{},"malloc"," arena 只有 16 KB。NNG 的 Zephyr 平台适配层用 ",[35,1463,1464],{},"malloc()"," 作为分配器，单个 pipe 的接收队列会占用 ",[35,1467,1468],{},"msq_len * 8"," 字节，可能超出这个上限。",[16,1471,1472,1473,1475,1476,1478],{},"因此 ",[35,1474,1195],{}," 把 arena 增大到 1 MB。这样一来，镜像的内存占用中约有 1 MB 属于 ",[35,1477,1460],{}," arena，不属于 Broker 本身。",[12,1480,1481],{"id":1481},"功能测试",[16,1483,1484],{},"仓库附带一套功能测试，对两个示例运行同一组测试。运行测试前，先安装所需的 Python 依赖（运行器会检查这些依赖，缺失时报错退出）：",[219,1486,1489],{"className":1487,"code":1488,"language":224,"meta":225},[222],"python3 -m pip install paho-mqtt requests\n",[35,1490,1488],{"__ignoreMap":225},[16,1492,1493,1494,377,1497,1500,1501,1504,1505,43],{},"系统中还需要安装 Mosquitto 命令行客户端（",[35,1495,1496],{},"mosquitto_pub",[35,1498,1499],{},"mosquitto_sub","）。在 Debian\u002FUbuntu 上安装 ",[35,1502,1503],{},"mosquitto-clients"," 软件包；在 Fedora 上安装 ",[35,1506,1507],{},"mosquitto",[16,1509,1510,1513],{},[35,1511,1512],{},"--list"," 会列出所有测试组（本文涉及其中 8 个，见下表）：",[219,1515,1518],{"className":1516,"code":1517,"language":224,"meta":225},[222],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --list\n",[35,1519,1517],{"__ignoreMap":225},[16,1521,1522,1523,1526],{},"对于正在运行的硬件示例，用 ",[35,1524,1525],{},"--no-manage"," 告诉运行器 Broker 已在运行，不需要由运行器管理：",[219,1528,1531],{"className":1529,"code":1530,"language":224,"meta":225},[222],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --no-manage --addr \u003Cboard-ip>\n",[35,1532,1530],{"__ignoreMap":225},[16,1534,1535],{},"也可以只运行指定的测试组：",[219,1537,1540],{"className":1538,"code":1539,"language":224,"meta":225},[222],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --no-manage --addr \u003Cboard-ip> \\\n    --group mqtt_v311 --group rest_get\n",[35,1541,1539],{"__ignoreMap":225},[16,1543,1544,1545,1548],{},"以下结果在 ESP32-S3 硬件示例上得到，用 ",[35,1546,1547],{},"--group"," 逐个运行下表中的 8 个测试组。为便于阅读，省略了路径前缀。",[219,1550,1553],{"className":1551,"code":1552,"language":224,"meta":225},[222],"========================================================================\nZephyr broker functional test suite — broker \u003Cboard-ip>:1883\ngroups: mqtt_v311, mqtt_v5, rest_get, ws_v311, ws_v5, capacity, ws_abort, survival\n========================================================================\n--no-manage: assuming a broker is already running; leftover mosquitto clients are NOT cleaned up\nbroker connect RTT 16.1 ms -> time-scale 4.0 (auto)\n[1\u002F8] mqtt_v311      PASS  (120.1s)\n[2\u002F8] mqtt_v5        PASS  (211.3s)\n[3\u002F8] rest_get       PASS  (2.9s)\n[4\u002F8] ws_v311        PASS  (279.0s)\n[5\u002F8] ws_v5          PASS  (15.9s)\n[6\u002F8] capacity       PASS  (13.1s)\n[7\u002F8] ws_abort       PASS  (31.9s)\n[8\u002F8] survival       PASS  (90.3s)\n------------------------------------------------------------------------\nRESULT: pass=8 fail=0\n",[35,1554,1552],{"__ignoreMap":225},[16,1556,1557,1560,1561,1563,1564,1567],{},[35,1558,1559],{},"rest_get"," 测试组还要求被测 Broker 在编译时配置了 REST 凭据（见各示例的 ",[35,1562,910],{},"）。未配置时，该测试组会报告为 ",[20,1565,1566],{},"SKIP","，不算失败：",[219,1569,1572],{"className":1570,"code":1571,"language":224,"meta":225},[222],"| [worker rest_get] SKIP: no REST API on \u003Caddr>:8081 — the demos leave it off\n| until CONFIG_BROKER_REST_USER\u002FPASS are set (local.conf); pass\n| --rest-user\u002F--rest-pass for a broker that has them\n",[35,1573,1571],{"__ignoreMap":225},[16,1575,1576,1577,779,1580,1583],{},"也就是说，测试套件会区分“REST 未启用”和“REST 有故障”：前者会跳过并给出说明，只有后者才判为失败。Broker 配置好凭据后，用 ",[35,1578,1579],{},"--rest-user",[35,1581,1582],{},"--rest-pass"," 把同一组凭据传给测试套件，该测试组即可正常运行。",[16,1585,1586],{},"硬件示例上各测试组的覆盖范围和运行时间如下：",[252,1588,1589,1602],{},[255,1590,1591],{},[258,1592,1593,1596,1599],{},[261,1594,1595],{},"测试组",[261,1597,1598],{},"覆盖范围",[261,1600,1601],{},"硬件运行时间",[268,1603,1604,1617,1634,1646,1659,1672,1685,1698],{},[258,1605,1606,1611,1614],{},[273,1607,1608],{},[35,1609,1610],{},"mqtt_v311",[273,1612,1613],{},"会话、保留消息，以及 v4\u002Fv5 互通",[273,1615,1616],{},"120.1s",[258,1618,1619,1624,1631],{},[273,1620,1621],{},[35,1622,1623],{},"mqtt_v5",[273,1625,1626,1627,1630],{},"会话过期、用户属性、",[35,1628,1629],{},"$share"," 和主题别名",[273,1632,1633],{},"211.3s",[258,1635,1636,1640,1643],{},[273,1637,1638],{},[35,1639,1559],{},[273,1641,1642],{},"所有 REST GET 路由",[273,1644,1645],{},"2.9s",[258,1647,1648,1653,1656],{},[273,1649,1650],{},[35,1651,1652],{},"ws_v311",[273,1654,1655],{},"基于 WebSocket 的 MQTT 3.1.1",[273,1657,1658],{},"279.0s",[258,1660,1661,1666,1669],{},[273,1662,1663],{},[35,1664,1665],{},"ws_v5",[273,1667,1668],{},"基于 WebSocket 的 MQTT 5",[273,1670,1671],{},"15.9s",[258,1673,1674,1679,1682],{},[273,1675,1676],{},[35,1677,1678],{},"capacity",[273,1680,1681],{},"12 个并发 CONNECT + QoS 1 回环",[273,1683,1684],{},"13.1s",[258,1686,1687,1692,1695],{},[273,1688,1689],{},[35,1690,1691],{},"ws_abort",[273,1693,1694],{},"WebSocket 连接异常中断时连接池的稳定性",[273,1696,1697],{},"31.9s",[258,1699,1700,1705,1711],{},[273,1701,1702],{},[35,1703,1704],{},"survival",[273,1706,312,1707,1710],{},[35,1708,1709],{},"attack.py"," 缩小规模的负载与会话反复建连测试",[273,1712,1713],{},"90.3s",[16,1715,1716,1717,1720],{},"运行器会自动调整参数：它先测量到 Broker 的 TCP 往返时间（本地回环低于 1 ms，真实硬件上经 Wi-Fi 为几十到几百毫秒；本次运行为 16.1 ms），判断 Broker 不在本地后，默认切换到 ",[35,1718,1719],{},"--time-scale 4","，把 CI 脚本按本地网络延迟设定的 sleep 间隔拉长。",[12,1722,1723],{"id":1723},"移植过程",[16,1725,1726,1727,1730,1731,1734,1735,1737],{},"NanoMQ 的分层本身就包含平台适配层（nng 的 ",[35,1728,1729],{},"nni_plat_*","），所以大部分移植工作是",[20,1732,1733],{},"为 Zephyr 补全这一层","。真正的硬性障碍反而在应用层：",[35,1736,37],{}," 中有几处直接调用 POSIX，没有办法绕开。下面逐层说明。",[120,1739,1741],{"id":1740},"_1-应用层裁剪-posix-依赖","1. 应用层：裁剪 POSIX 依赖",[16,1743,1744,1745,1748],{},"应用层的 POSIX 依赖集中在四处，全部用 ",[35,1746,1747],{},"__ZEPHYR__"," 条件编译处理，没有另加一层抽象：",[252,1750,1751,1764],{},[255,1752,1753],{},[258,1754,1755,1758,1761],{},[261,1756,1757],{},"文件",[261,1759,1760],{},"原有的 POSIX 依赖",[261,1762,1763],{},"处理方式",[268,1765,1766,1790,1809,1829],{},[258,1767,1768,1773,1783],{},[273,1769,1770],{},[35,1771,1772],{},"apps\u002Fbroker.c",[273,1774,1775,1776,779,1779,1782],{},"用 ",[35,1777,1778],{},"signal()",[35,1780,1781],{},"sigaction"," 安装信号处理函数",[273,1784,1785,1786,1789],{},"整段跳过。Zephyr 没有 POSIX 信号语义；嵌入式 Broker 的主循环按自身的条件退出，不靠 ",[35,1787,1788],{},"^C"," 终止",[258,1791,1792,1797,1802],{},[273,1793,1794],{},[35,1795,1796],{},"nanomq.c",[273,1798,1799],{},[35,1800,1801],{},"#include \u003Csys\u002Fptrace.h>",[273,1803,1804,1805,1808],{},"条件编译。",[35,1806,1807],{},"check_trace()"," 只在 CLI 路径上调用，嵌入式 Broker 不会走这条路径",[258,1810,1811,1816,1822],{},[273,1812,1813],{},[35,1814,1815],{},"mqtt_api.c",[273,1817,1818,1821],{},[35,1819,1820],{},"nng_access(dir, W_OK)","（检查文件日志目录是否可写）",[273,1823,1824,1825,1828],{},"条件编译。picolibc 没有 ",[35,1826,1827],{},"W_OK","；嵌入式环境下文件日志后端始终关闭，跳过这项检查没有副作用",[258,1830,1831,1837,1852],{},[273,1832,1833,1836],{},[35,1834,1835],{},"process.c","（整个编译单元）",[273,1838,1839,779,1842,779,1845,1848,1849],{},[35,1840,1841],{},"fork",[35,1843,1844],{},"kill",[35,1846,1847],{},"chdir",", ",[35,1850,1851],{},"\u003Cpaths.h>",[273,1853,1854,1855,1858],{},"不参与编译；示例提供 ",[35,1856,1857],{},"process_stub.c","，补上它的公开符号（全部返回 -1）",[16,1860,1861,1863,1864,1867,1868,1871,1872,1875,1876,1878,1879,779,1882,779,1885,1888,1889,1892,1893,377,1896,1899,1900,1903,1904,43],{},[35,1862,1857],{}," 之所以能这样“假装”实现，是因为这五个符号要么根本没有调用点，要么只会在永远不会执行的路径上被调用：",[35,1865,1866],{},"process_daemonize()"," 的两处调用都受 ",[35,1869,1870],{},"daemon == true"," 判断保护，",[35,1873,1874],{},"process_send_signal()"," 只被 CLI 路径上的 ",[35,1877,1807],{}," 调用，而 ",[35,1880,1881],{},"process_is_alive()",[35,1883,1884],{},"pidgrp_send_signal()",[35,1886,1887],{},"process_create_child()"," 在",[20,1890,1891],{},"整个应用中都没有调用点","。嵌入式 Broker 调用 ",[35,1894,1895],{},"broker()",[35,1897,1898],{},"conf_init()"," 时，",[35,1901,1902],{},"daemon"," 默认为 false，所以这些路径一条都不会走到——",[20,1905,1906],{},"桩实现的实际作用只是让链接成功",[120,1908,1910],{"id":1909},"_2-平台适配层接口清单与-posix-差异","2. 平台适配层：接口清单与 POSIX 差异",[16,1912,1913,1914,1917,1918,1921,1922,377,1925,1928],{},"这一层是 nng 的 Zephyr 实现（",[35,1915,1916],{},"src\u002Fplatform\u002Fzephyr\u002F","，共 22 个文件，其中 18 个是 ",[35,1919,1920],{},".c","：16 个实现下面列出的各个接口族，另有 2 个桩实现 ",[35,1923,1924],{},"zephyr_peerid.c",[35,1926,1927],{},"zephyr_socketpair.c","，对应表中两项不支持的功能）。代码按 nng 的接口族组织，每个接口族映射到 Zephyr 中的相应机制：",[16,1930,1931],{},[20,1932,1933],{},"接口清单",[252,1935,1936,1952],{},[255,1937,1938],{},[258,1939,1940,1943,1946,1949],{},[261,1941,1942],{},"接口族",[261,1944,1945],{},"实现文件",[261,1947,1948],{},"对应的 POSIX 机制",[261,1950,1951],{},"Zephyr 实现",[268,1953,1954,1981,2008,2029,2048,2078,2105,2131,2153,2176,2197,2215,2236,2261],{},[258,1955,1956,1959,1964,1971],{},[273,1957,1958],{},"内存",[273,1960,1961],{},[35,1962,1963],{},"zephyr_alloc.c",[273,1965,1966,779,1968],{},[35,1967,1460],{},[35,1969,1970],{},"free",[273,1972,1973,1974,1977,1978,1980],{},"PSRAM 上的 ",[35,1975,1976],{},"k_heap","，或 libc 的 ",[35,1979,1464],{},"（视目标而定）",[258,1982,1983,1986,1991,1999],{},[273,1984,1985],{},"时钟与休眠",[273,1987,1988],{},[35,1989,1990],{},"zephyr_clock.c",[273,1992,1993,779,1996],{},[35,1994,1995],{},"clock_gettime",[35,1997,1998],{},"nanosleep",[273,2000,2001,2004,2005],{},[35,2002,2003],{},"k_uptime_get()","，加上 Zephyr 的 POSIX ",[35,2006,2007],{},"nanosleep()",[258,2009,2010,2013,2018,2024],{},[273,2011,2012],{},"线程与同步",[273,2014,2015],{},[35,2016,2017],{},"zephyr_thread.c",[273,2019,2020,2023],{},[35,2021,2022],{},"pthread_*","（互斥锁 \u002F 读写锁 \u002F 条件变量 \u002F 线程）",[273,2025,2026,2027,300],{},"Zephyr 的 pthread 实现（",[35,2028,101],{},[258,2030,2031,2034,2039,2045],{},[273,2032,2033],{},"原子操作",[273,2035,2036],{},[35,2037,2038],{},"zephyr_atomic.c",[273,2040,2041,2042],{},"C11 ",[35,2043,2044],{},"\u003Cstdatomic.h>",[273,2046,2047],{},"原生 64 位原子操作，或基于内嵌互斥锁的回退实现",[258,2049,2050,2053,2058,2072],{},[273,2051,2052],{},"调试与错误码",[273,2054,2055],{},[35,2056,2057],{},"zephyr_debug.c",[273,2059,2060,779,2063,779,2066,779,2069],{},[35,2061,2062],{},"abort",[35,2064,2065],{},"printf",[35,2067,2068],{},"strerror",[35,2070,2071],{},"errno",[273,2073,2074,2077],{},[35,2075,2076],{},"printk","、libc，以及 errno ↔ nng 错误码的映射",[258,2079,2080,2083,2091,2102],{},[273,2081,2082],{},"TCP（客户端）",[273,2084,2085,1848,2088],{},[35,2086,2087],{},"zephyr_tcpdial.c",[35,2089,2090],{},"zephyr_tcpconn.c",[273,2092,2093,779,2096,779,2099],{},[35,2094,2095],{},"socket",[35,2097,2098],{},"connect",[35,2100,2101],{},"setsockopt",[273,2103,2104],{},"Zephyr 网络栈提供的 BSD 兼容 socket",[258,2106,2107,2110,2115,2128],{},[273,2108,2109],{},"TCP（服务端）",[273,2111,2112],{},[35,2113,2114],{},"zephyr_tcplisten.c",[273,2116,2117,779,2119,779,2122,779,2125],{},[35,2118,2095],{},[35,2120,2121],{},"bind",[35,2123,2124],{},"listen",[35,2126,2127],{},"accept",[273,2129,2130],{},"同上",[258,2132,2133,2136,2141,2151],{},[273,2134,2135],{},"UDP",[273,2137,2138],{},[35,2139,2140],{},"zephyr_udp.c",[273,2142,2143,779,2145,779,2148],{},[35,2144,2095],{},[35,2146,2147],{},"sendmsg",[35,2149,2150],{},"recvmsg",[273,2152,2130],{},[258,2154,2155,2158,2166,2172],{},[273,2156,2157],{},"描述符与轮询队列",[273,2159,2160,1848,2163],{},[35,2161,2162],{},"zephyr_pollq_poll.c",[35,2164,2165],{},"zephyr_sockfd.c",[273,2167,2168,2171],{},[35,2169,2170],{},"poll()","（POSIX 上还会在此之上叠加 epoll \u002F kqueue \u002F eventfd）",[273,2173,607,2174],{},[35,2175,2170],{},[258,2177,2178,2181,2186,2192],{},[273,2179,2180],{},"Socket 地址转换",[273,2182,2183],{},[35,2184,2185],{},"zephyr_sockaddr.c",[273,2187,2188,2191],{},[35,2189,2190],{},"sockaddr"," 与 nng 结构体之间的转换",[273,2193,607,2194,2196],{},[35,2195,2190],{},"（去掉了 AF_UNIX）",[258,2198,2199,2202,2207,2212],{},[273,2200,2201],{},"DNS 解析",[273,2203,2204],{},[35,2205,2206],{},"zephyr_resolv_gai.c",[273,2208,2209],{},[35,2210,2211],{},"getaddrinfo()",[273,2213,2214],{},"Zephyr 有这个接口，但语义不同（见下表）",[258,2216,2217,2220,2225,2233],{},[273,2218,2219],{},"随机数",[273,2221,2222],{},[35,2223,2224],{},"zephyr_rand_urandom.c",[273,2226,2227,1848,2230],{},[35,2228,2229],{},"\u002Fdev\u002Furandom",[35,2231,2232],{},"getrandom()",[273,2234,2235],{},"按是否有硬件熵源分别处理（见下表）",[258,2237,2238,2240,2245,2254],{},[273,2239,1757],{},[273,2241,2242],{},[35,2243,2244],{},"zephyr_file.c",[273,2246,2247,373,2250,2253],{},[35,2248,2249],{},"stdio",[35,2251,2252],{},"stat","、文件锁",[273,2255,2256,377,2258,2260],{},[35,2257,2249],{},[35,2259,2252],{}," 是真实实现；临时目录和文件锁是回退实现（见下表）",[258,2262,2263,2266,2271,2279],{},[273,2264,2265],{},"唤醒管道",[273,2267,2268],{},[35,2269,2270],{},"zephyr_pipe.c",[273,2272,2273,779,2276],{},[35,2274,2275],{},"pipe()",[35,2277,2278],{},"eventfd()",[273,2280,2281],{},"Zephyr 没有对应的机制（见下表）",[16,2283,2284],{},[20,2285,2286],{},"POSIX 假设不成立的地方",[252,2288,2289,2301],{},[255,2290,2291],{},[258,2292,2293,2296,2299],{},[261,2294,2295],{},"POSIX 假设",[261,2297,2298],{},"Zephyr 现状",[261,2300,1763],{},[268,2302,2303,2328,2342,2362,2384,2401,2417,2433,2444,2460,2472],{},[258,2304,2305,2313,2319],{},[273,2306,2307,779,2310],{},[35,2308,2309],{},"readv()",[35,2311,2312],{},"writev()",[273,2314,2315,2316],{},"不可用，也没有 ",[35,2317,2318],{},"\u003Csys\u002Fuio.h>",[273,2320,1775,2321,779,2324,2327],{},[35,2322,2323],{},"read()",[35,2325,2326],{},"write()"," 循环模拟，并明确保留短传输语义（细节 1）",[258,2329,2330,2336,2339],{},[273,2331,2332,779,2334],{},[35,2333,2275],{},[35,2335,2278],{},[273,2337,2338],{},"不可用",[273,2340,2341],{},"轮询队列不注册唤醒描述符，改为靠自身的超时向前推进",[258,2343,2344,2352,2355],{},[273,2345,2346,779,2349],{},[35,2347,2348],{},"flock()",[35,2350,2351],{},"lockf()",[273,2353,2354],{},"没有文件锁",[273,2356,2357,2358,2361],{},"沿用 ",[35,2359,2360],{},"posix_file.c"," 自带的嵌入式回退做法：返回成功，但不实际加锁",[258,2363,2364,2367,2370],{},[273,2365,2366],{},"临时目录 \u002F 工作目录",[273,2368,2369],{},"两个概念都不存在",[273,2371,2372,2375,2376,2379,2380,2383],{},[35,2373,2374],{},"nni_plat_temp_dir()"," 返回 ",[35,2377,2378],{},"\"\u002Ftmp\"","（那里是否真的挂载了文件系统，由调用方自己的文件操作反映出来）；",[35,2381,2382],{},"nni_plat_getcwd()"," 返回 NULL",[258,2385,2386,2392,2394],{},[273,2387,2388,2391],{},[35,2389,2390],{},"SO_PEERCRED"," 等对端凭据",[273,2393,2338],{},[273,2395,2396,2397,2400],{},"返回 ",[35,2398,2399],{},"NNG_ENOTSUP","。对端凭据只用于 AF_UNIX IPC 传输，而 Zephyr 上没有这种传输",[258,2402,2403,2409,2412],{},[273,2404,2405,2408],{},[35,2406,2407],{},"socketpair()","（AF_UNIX 域 socket 对）",[273,2410,2411],{},"没有 AF_UNIX",[273,2413,2396,2414,2416],{},[35,2415,2399],{},"；依赖它的 IPC 传输因此在 Zephyr 上不可用（见附录 A）",[258,2418,2419,2423,2426],{},[273,2420,2421],{},[35,2422,2229],{},[273,2424,2425],{},"没有这个设备",[273,2427,2428,2429,2432],{},"有硬件熵源的目标（ESP32-S3 TRNG）使用 ",[35,2430,2431],{},"sys_csrand_get()","；没有硬件熵源的目标回退到软件生成器，代码中注明它不适合用于机密数据",[258,2434,2435,2438,2441],{},[273,2436,2437],{},"64 位原子操作",[273,2439,2440],{},"32 位非 x86 目标没有原生支持",[273,2442,2443],{},"回退到“基于内嵌互斥锁的原子操作”（细节 2）",[258,2445,2446,2451,2454],{},[273,2447,2448],{},[35,2449,2450],{},"SO_BINDTODEVICE",[273,2452,2453],{},"不支持绑定网络接口",[273,2455,2456,2457,2459],{},"在设置选项时直接返回 ",[35,2458,2399],{},"，而不是接受该选项后悄悄使用默认接口",[258,2461,2462,2466,2469],{},[273,2463,2464],{},[35,2465,2211],{},[273,2467,2468],{},"有，但是同步调用",[273,2470,2471],{},"接受同步语义：解析在调用方线程上完成，不引入工作线程",[258,2473,2474,2479,2485],{},[273,2475,2476],{},[35,2477,2478],{},"pthread_condattr_setclock()",[273,2480,2481,2482],{},"依赖 ",[35,2483,2484],{},"CONFIG_POSIX_CLOCK_SELECTION",[273,2486,2487],{},"启动时校验；单调时钟不可用时进程直接终止——否则所有带超时的等待都会悄无声息地立即超时",[16,2489,2490,2500,2501,779,2503,1888,2505,2508,2509,2512,2513,2516],{},[20,2491,2492,2493,779,2496,2499],{},"细节 1：",[35,2494,2495],{},"readv",[35,2497,2498],{},"writev"," 的短传输语义","。简单地“循环到所有 iovec 都写完为止”是错误的。POSIX 的 ",[35,2502,2495],{},[35,2504,2498],{},[20,2506,2507],{},"第一次出现短传输时","就返回，报告已传输的字节数，剩下的部分由调用方重新提交。模拟实现如果越过一个没传完的 iovec 继续往下传，字节流里就会出现空洞；如果在已经成功传输一部分、随后遇到 ",[35,2510,2511],{},"EAGAIN"," 时返回 -1，调用方会以为什么都没发出去，于是把这些数据再发一遍。正确的做法是在第一次短传输处停下，返回累计的字节数——",[35,2514,2515],{},"posix_sockfd.c"," 中的注释（\"we didn't send all the data, the caller will resubmit\"）就是调用方所依赖的约定。",[16,2518,2519,2522,2523,2526,2527,557,2530,447,2533,2536,2537,2540,2541,2544,2545,2548],{},[20,2520,2521],{},"细节 2：原子操作为什么不用 pthread 互斥锁","。直观的做法是在每个原子变量里内嵌一个 ",[35,2524,2525],{},"pthread_mutex_t","，但 Zephyr 的 ",[35,2528,2529],{},"pthread_mutex_init()",[20,2531,2532],{},"从固定大小的池中分配",[35,2534,2535],{},"posix_mutex_pool","，按位图分配，池耗尽后返回 ",[35,2538,2539],{},"ENOMEM","）。nng 中初始化原子变量的地方共有 26 处，只有 5 处有配对的 ",[35,2542,2543],{},"nni_atomic_fini*()"," 调用来归还槽位（pipe 上的三处、消息引用计数，以及 inproc pair），其余的都没有对应的释放调用。改用内核自己的 ",[35,2546,2547],{},"struct k_mutex"," 就没有这个问题：它直接内嵌在变量中，不占用池，也不需要释放。",[16,2550,2551,2558,2559,2562,2563,2566,2567,2570,2571,2574,2575,2578,2579,2582,2583,2586,2587,2590,2591,2594],{},[20,2552,2553,2554,2557],{},"细节 3：",[35,2555,2556],{},"ENABLE_LOG"," 必须同时传给两侧","。nanolib 的 ",[35,2560,2561],{},"conf.c"," 是否初始化日志后端、",[35,2564,2565],{},"log_*()"," 是否编译成实际代码，都由 ",[35,2568,2569],{},"-DENABLE_LOG"," 决定，而且这个宏必须",[20,2572,2573],{},"同时","传给应用和 libnng。原因在构建系统：nng 的 CMake 只识别 ",[35,2576,2577],{},"NNG_*"," 形式的缓存变量，普通宏只能通过 ",[35,2580,2581],{},"CMAKE_C_FLAGS"," 传进去。只传给一侧时不会有任何提示——链接成功，Broker 也能运行，但一行日志都不会输出（",[35,2584,2585],{},"conf->log.type"," 没有被初始化）。",[35,2588,2589],{},"ACL_SUPP"," 也是如此，而且条件不满足时后果更危险：两侧定义不一致会导致 ",[35,2592,2593],{},"struct conf"," 的内存布局错位。",[120,2596,2598],{"id":2597},"_3-编译期宏约定","3. 编译期宏约定",[252,2600,2601,2614],{},[255,2602,2603],{},[258,2604,2605,2608,2611],{},[261,2606,2607],{},"宏",[261,2609,2610],{},"作用",[261,2612,2613],{},"不满足时",[268,2615,2616,2631,2646,2669],{},[258,2617,2618,2622,2628],{},[273,2619,2620],{},[35,2621,2556],{},[273,2623,2624,2625,2627],{},"让 nanolib 初始化日志后端，并把 ",[35,2626,2565],{}," 编译成实际代码",[273,2629,2630],{},"日志功能静默失效",[258,2632,2633,2637,2643],{},[273,2634,2635],{},[35,2636,2589],{},[273,2638,2639,2640,2642],{},"让 ",[35,2641,2593],{}," 带上 ACL 字段（默认开启）",[273,2644,2645],{},"两侧定义不一致 → 结构体布局错位",[258,2647,2648,2653,2666],{},[273,2649,2650],{},[35,2651,2652],{},"SUPP_NANO_LIB",[273,2654,2655,2656,2658,2659,2662,2663],{},"隐藏 ",[35,2657,1796],{}," 的 ",[35,2660,2661],{},"main()","，同时保留 ",[35,2664,2665],{},"get_cache_argc\u002Fargv",[273,2667,2668],{},"与示例自己的入口函数冲突",[258,2670,2671,2676,2681],{},[273,2672,2673],{},[35,2674,2675],{},"SUPP_SYSLOG",[273,2677,2678],{},[20,2679,2680],{},"故意不设置",[273,2682,2683,2684,2687],{},"会去找 ",[35,2685,2686],{},"syslog()","，而 Zephyr 没有这个函数",[120,2689,2691],{"id":2690},"_4-两个框架层面的陷阱","4. 两个框架层面的陷阱",[16,2693,2694],{},"下面两个问题属于同一类：Zephyr API 的实际行为与预期不同，出错时又没有任何错误信息，排查成本很高。",[2696,2697,2699],"h4",{"id":2698},"_1-首个客户端连入时-psram-堆损坏","1. 首个客户端连入时 PSRAM 堆损坏",[16,2701,2702],{},"ESP32-S3 有约 512 KB 内部 SRAM，其中约 416 KB 可用。Broker 的数据面需要几 MB 内存，因此必须使用 PSRAM。",[16,2704,2705,2706,2709,2710,2713,2714,2717],{},"Zephyr 提供了 ",[35,2707,2708],{},"shared_multi_heap"," 来管理多个堆区域，乍看很合适。但它",[20,2711,2712],{},"不是线程安全的","：底层的 ",[35,2715,2716],{},"sys_heap"," 在访问时没有加锁。",[16,2719,2720,2721,43],{},"NanoNNG 会在多个线程中分配内存，包括 poller、taskq 工作线程，以及每个连接关联的 aio。结果就是",[20,2722,2723],{},"第一个客户端连接建立时，堆就被破坏了",[16,2725,2726,2727,2730,2731,2734,2735,2737],{},"解决方法是不用 ",[35,2728,2729],{},"shared_multi_heap_alloc()","，改为在 PSRAM 窗口上使用带锁的普通 ",[35,2732,2733],{},"struct k_heap","。不需要驻留在 SRAM 中的静态池也一并移到 PSRAM，包括 Wi-Fi 驱动的 ",[35,2736,1248],{}," 段、net_buf 池和 Zephyr POSIX 对象池（约 60 KB）。这样内部 SRAM 就留给了真正需要它的部分。",[2696,2739,2741,2742,2745],{"id":2740},"_2-net_mgmt-回调掩码不是按位匹配的","2. ",[35,2743,2744],{},"net_mgmt"," 回调掩码不是按位匹配的",[16,2747,2748],{},"Wi-Fi 连接过程需要监听多个事件。直觉上会把它们合并成一个掩码，只注册一个回调：",[219,2750,2755],{"className":2751,"code":2753,"language":2754,"meta":225},[2752],"language-c","\u002F* Incorrect: the two events use different layer codes *\u002F\nnet_mgmt_init_event_callback(&cb, handler,\n    NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_IPV4_DHCP_BOUND);\nnet_mgmt_add_event_callback(&cb);\n","c",[35,2756,2753],{"__ignoreMap":225},[16,2758,2759,2760,2763],{},"这样配置后，回调",[20,2761,2762],{},"永远不会被触发","，也不会产生任何日志或错误码。",[16,2765,2766,2767,447,2770,2773,2774,2777],{},"原因在 ",[35,2768,2769],{},"mgmt_run_slist_callbacks()",[35,2771,2772],{},"subsys\u002Fnet\u002Fip\u002Fnet_mgmt.c","）中。它在判断回调是否匹配某个事件时，要求",[20,2775,2776],{},"层代码（layer code）完全相等","，而不是做按位与：",[219,2779,2782],{"className":2780,"code":2781,"language":2754,"meta":225},[2752],"if (!(NET_MGMT_GET_LAYER(mgmt_event->event) ==\n      NET_MGMT_GET_LAYER(cb->event_mask)) ||\n    !(NET_MGMT_GET_LAYER_CODE(mgmt_event->event) ==\n      NET_MGMT_GET_LAYER_CODE(cb->event_mask)) || ...\n",[35,2783,2781],{"__ignoreMap":225},[16,2785,2786,2787,2790,2791,2794,2795,2798,2799,2802],{},"把层代码不同的事件合并到一个掩码里，",[35,2788,2789],{},"cb->event_mask"," 中的层代码就变成了两者的按位或，结果和哪个事件都匹配不上。以这两个事件为例，Wi-Fi 事件的层代码是 ",[35,2792,2793],{},"0x0D","，IPv4 是 ",[35,2796,2797],{},"0x03","，按位或得到 ",[35,2800,2801],{},"0x0F","，与两者都不相等。",[16,2804,2805,2806,2809],{},"正确的做法是",[20,2807,2808],{},"每个事件单独注册一个回调","，处理函数可以共用：",[219,2811,2814],{"className":2812,"code":2813,"language":2754,"meta":225},[2752],"\u002F* Correct: one callback structure per event *\u002F\nnet_mgmt_init_event_callback(&wifi_cb, handler, NET_EVENT_WIFI_CONNECT_RESULT);\nnet_mgmt_add_event_callback(&wifi_cb);\n\nnet_mgmt_init_event_callback(&dhcp_cb, handler, NET_EVENT_IPV4_DHCP_BOUND);\nnet_mgmt_add_event_callback(&dhcp_cb);\n",[35,2815,2813],{"__ignoreMap":225},[12,2817,2818],{"id":2818},"总结",[16,2820,2821],{},"在 MCU 上运行 Broker 的价值在于省掉一台边缘网关：现场已经部署的开发板可以顺带运行一个 Broker，把异构设备统一接入 MQTT，并在网络中断时先在本地缓存消息。",[120,2823,2824],{"id":2824},"适用场景",[16,2826,2827,2828,2830],{},"结合",[29,2829,42],{"href":41},"中的列表来看，当前实现适合在受控网络内做小规模的边缘汇聚和协议转换。选型前需要先确认以下几点：",[69,2832,2833,2840,2843,2846],{},[72,2834,2835,2836,2839],{},"目前不包含 TLS、磁盘持久化和 MQTT 侧认证（三者在附录 A 的“后续计划”一列中都标为计划支持），因此",[20,2837,2838],{},"现阶段不适合用作公网或不可信网络中的 Broker","；本文的示例默认监听所有网络接口，REST 虽然提供 Basic 认证，但没有加密，凭据也需要自行配置（没有可用的默认凭据）。详见“安全注意事项”。",[72,2841,2842],{},"没有文件系统，缓存的消息和持久会话在断电后会丢失。",[72,2844,2845],{},"并发能力受 Zephyr 线程配额限制。本文没有给出正式的吞吐量或并发基准测试数据；附录 A 引用的官方性能数据来自多核 POSIX 环境，不代表本示例的性能。",[72,2847,2848,2849,2851],{},"除 ",[35,2850,49],{}," 和 ESP32-S3 外，尚未在其他开发板上验证。",[120,2853,2854],{"id":2854},"代码与示例",[16,2856,2857,2858,2658,2863,2865,2866,2658,2871,2874],{},"移植代码位于 ",[29,2859,2862],{"href":2860,"rel":2861},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq\u002Ftree\u002Fzephyr-rtos",[171],"nanomq\u002Fnanomq",[35,2864,319],{}," 分支。示例已移到 ",[29,2867,2870],{"href":2868,"rel":2869},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoMQ-Zephyr-Example\u002Ftree\u002Fmain\u002Fexamples",[171],"nanomq\u002FNanoMQ-Zephyr-Example",[35,2872,2873],{},"examples\u002F"," 目录下，各自的 README 写明了从零开始搭建并运行的完整流程。",[16,2876,2877,2878,2883],{},"如果你在其他开发板上验证了这个移植版本，或者遇到了新问题，欢迎到 ",[29,2879,2882],{"href":2880,"rel":2881},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq\u002Fdiscussions",[171],"NanoMQ 社区","参与讨论。",[12,2885,2887],{"id":2886},"附录-a与通用版-nanomq-的差异","附录 A：与通用版 NanoMQ 的差异",[16,2889,2890,2891,2894],{},"这个 Zephyr 移植版本",[20,2892,2893],{},"不能等同替代通用版 NanoMQ","。它在多个方面与通用版不同，针对具体场景评估这个移植版本时，需要考虑这些差异。",[16,2896,2897,2900,2901,2903,2904,43],{},[20,2898,2899],{},"基线版本。"," 移植基于上游 ",[35,2902,299],{}," 分支，即 NanoMQ ",[20,2905,2906],{},"0.25.6",[16,2908,2909,2912,2913,2916,2917,2920,2921,2923,2924,2927],{},[20,2910,2911],{},"配置来源。"," 通用版 NanoMQ 从 ",[35,2914,2915],{},"nanomq.conf"," 读取配置。Zephyr 版本改用",[20,2918,2919],{},"内嵌的最小配置","：以 ",[35,2922,1898],{}," 提供的内置默认值为基础，再由启动代码直接设置监听地址等关键字段，不经过配置文件解析器。",[35,2925,2926],{},"conf"," 结构体及其语义保持不变，只是配置来源从文件变成了在内存中构造。",[16,2929,2930,2933,2934,2937,2938,2941,2942,775],{},[20,2931,2932],{},"功能可用性。"," 外围功能（REST、规则引擎、桥接等）",[20,2935,2936],{},"全部参与编译，在运行时控制开关","，没有在编译期裁掉，这样同一份源码在各平台上的行为保持一致。下表列出此版本中不可用或未经验证的能力，并区分两类情况：",[20,2939,2940],{},"尚未完成的工作（计划支持）","，以及",[20,2943,2944],{},"受平台或依赖限制（暂无计划）",[252,2946,2947,2960],{},[255,2948,2949],{},[258,2950,2951,2954,2957],{},[261,2952,2953],{},"能力",[261,2955,2956],{},"现状",[261,2958,2959],{},"后续计划",[268,2961,2962,2979,2995,3012,3032,3045,3058,3086],{},[258,2963,2964,2967,2974],{},[273,2965,2966],{},"文件系统相关",[273,2968,2969,2970,2973],{},"配置、日志和持久会话不写入磁盘；",[35,2971,2972],{},"$SYS"," 只反映运行时状态",[273,2975,2976,2978],{},[20,2977,711],{},"：只需挂载 Zephyr 的 fs 子系统（fat \u002F ext2 \u002F fcb 等）",[258,2980,2981,2984,2990],{},[273,2982,2983],{},"SQLite 持久化",[273,2985,2986,2987,300],{},"未包含（",[35,2988,2989],{},"NNG_ENABLE_SQLITE=OFF",[273,2991,2992,2994],{},[20,2993,711],{},"：随文件系统一起启用",[258,2996,2997,3000,3007],{},[273,2998,2999],{},"Parquet",[273,3001,3002,3003,3006],{},"未包含。由 ",[35,3004,3005],{},"SUPP_PARQUET"," 宏控制（不是 CMake 选项），未定义时不会编译进来",[273,3008,3009,3011],{},[20,3010,711],{},"：同上，依赖文件系统",[258,3013,3014,3017,3027],{},[273,3015,3016],{},"TLS",[273,3018,2986,3019,3022,3023,3026],{},[35,3020,3021],{},"NNG_ENABLE_TLS=OFF","）。Zephyr 提供 mbedTLS（本次编译已启用）和 ",[35,3024,3025],{},"tls_credentials"," 子系统",[273,3028,3029,3031],{},[20,3030,711],{},"：启用后即可使用 MQTTS \u002F WSS \u002F HTTPS",[258,3033,3034,3037,3040],{},[273,3035,3036],{},"MQTT 侧认证（密码 \u002F ACL）",[273,3038,3039],{},"既未接入，也未验证；目前任何客户端都能连接",[273,3041,3042,3044],{},[20,3043,711],{},"：只需接入配置并完成验证",[258,3046,3047,3050,3053],{},[273,3048,3049],{},"规则引擎",[273,3051,3052],{},"内嵌配置中没有对应的开关路径；未验证",[273,3054,3055,3057],{},[20,3056,711],{},"：纯软件模块，只差配置接入",[258,3059,3060,3063,3077],{},[273,3061,3062],{},"IPC 传输",[273,3064,3065,3066,3068,3069,3072,3073,3076],{},"示例中已关闭（",[35,3067,759],{}," 中设置 ",[35,3070,3071],{},"ipc_internal = false","，这是运行时赋值，不是编译选项），因此 ",[35,3074,3075],{},"nanomq ctl"," 管理通道不可用",[273,3078,3079,3082,3083],{},[20,3080,3081],{},"待平台支持","：需要具名 AF_UNIX，而 Zephyr 目前只提供匿名 ",[35,3084,3085],{},"socketpair",[258,3087,3088,3091,3097],{},[273,3089,3090],{},"QUIC",[273,3092,2986,3093,3096],{},[35,3094,3095],{},"NNG_ENABLE_QUIC=OFF","）。nng 的 QUIC 实现依赖 msquic",[273,3098,3099,3102],{},[20,3100,3101],{},"暂无计划","：这些依赖的体积不适合嵌入式目标",[16,3104,3105,3108,3109,3112,3113,3116],{},[20,3106,3107],{},"线程模型。"," Linux 版本依赖完整的 POSIX 动态线程池。在 Zephyr 上，NanoNNG 的线程数是固定的（taskq = 2 \u002F poller = 1 \u002F expire = 1），再加上 Zephyr POSIX 线程池的上限 ",[35,3110,3111],{},"CONFIG_POSIX_THREAD_THREADS_MAX=16","，且每个 pthread 的栈由 ",[35,3114,3115],{},"CONFIG_DYNAMIC_THREAD_STACK_SIZE"," 固定为 16 KB，Broker 的并发能力会相应受限。",[16,3118,3119,557,3122,3124,3125,3128,3129,3132],{},[20,3120,3121],{},"性能数据。",[29,3123,32],{"href":31}," 官方公布的数据包括：最小功能集下启动占用不到 200 KB；吞吐量最高可达百万级 TPS；在多核 CPU 上性能最高可达 Mosquitto 的 10 倍。需要强调这些数据的适用范围：它们来自",[20,3126,3127],{},"多核 POSIX 环境","下的基准测试，官方页面没有给出相应测试的硬件和配置细节，而且这些数据",[20,3130,3131],{},"不代表本文示例的性能","——Zephyr 端的线程配额远小于多核 Linux 系统（见上一条），实际的并发特性也不同。本文没有对示例做正式的性能测试。",[16,3134,3135,3138,3139,3141,3142,3144,3145,3148,3149,3151],{},[20,3136,3137],{},"内存分配。"," 不同目标使用的分配器不同：",[35,3140,49],{}," 使用 libc 的 ",[35,3143,1464],{},"，其 malloc arena 就是 Broker 的堆（见 qemu 一节）；ESP32-S3 的内部 SRAM 放不下数据面，因此定义了 ",[35,3146,3147],{},"NNG_ZEPHYR_ALLOC_SMH","，把 nng 的内存分配切换到 PSRAM 上的 ",[35,3150,1976],{},"（见“移植过程 → 4. 两个框架层面的陷阱”）。",[16,3153,3154,3157,3158,3160],{},[20,3155,3156],{},"时间来源。"," 两个示例都使用真实的 UTC 时间，但获取方式不同：",[35,3159,49],{}," 从 QEMU 的 CMOS RTC 获取初始时间，ESP32-S3 没有 RTC，通过 SNTP 获取初始时间。Zephyr 不包含时区数据库，因此显示的时间始终是 UTC。",[16,3162,3163,3166,3167,3169,3170,779,3172,779,3174,3176],{},[20,3164,3165],{},"移除的模块。"," Broker 的 ",[35,3168,1835],{}," 依赖 ",[35,3171,1841],{},[35,3173,1844],{},[35,3175,1847],{},"，无法在 Zephyr 上编译；示例中用一个提供相同符号的桩实现替代它。这些符号要么只在守护进程和 CLI 路径中调用，要么根本没有被调用，因此嵌入式 Broker 不会执行到它们（见“移植过程 → 1. 应用层”中表格的对应条目）。",[16,3178,3179,557,3182,3184,3185,3188,3189,3192,3193,3196,3197,3200],{},[20,3180,3181],{},"已验证平台。",[35,3183,49],{},"（32 位）和 ESP32-S3。构建系统会为",[20,3186,3187],{},"所有 32 位非 x86 目标","（ARM、RISC-V、Xtensa 等）启用原子操作的回退实现（",[35,3190,3191],{},"NNG_ZEPHYR_NO_STDATOMIC","，见“移植过程 → 2. 平台适配层”中的细节 2）——",[20,3194,3195],{},"ESP32-S3 本身就在使用它","，所以这条路径已经包含在硬件验证中。尚未验证的是 ARM \u002F RISC-V ",[20,3198,3199],{},"开发板","本身（后续计划：在 ARM \u002F RISC-V 等开发板上验证，届时网络驱动、中断和内存预算都需要重新验证）。",[12,3202,3204],{"id":3203},"附录-b两个示例对比","附录 B：两个示例对比",[252,3206,3207,3223],{},[255,3208,3209],{},[258,3210,3211,3213,3218],{},[261,3212],{},[261,3214,3215],{},[35,3216,3217],{},"demo\u002Fnanomq_zephyr_esp32s3",[261,3219,3220],{},[35,3221,3222],{},"demo\u002Fnanomq_zephyr_qemu_x86",[268,3224,3225,3239,3252,3265,3278,3291],{},[258,3226,3227,3232,3235],{},[273,3228,3229],{},[20,3230,3231],{},"目标平台",[273,3233,3234],{},"ESP32-S3 硬件",[273,3236,3237],{},[35,3238,49],{},[258,3240,3241,3246,3249],{},[273,3242,3243],{},[20,3244,3245],{},"硬件要求",[273,3247,3248],{},"ESP32-S3 开发板",[273,3250,3251],{},"无",[258,3253,3254,3259,3262],{},[273,3255,3256],{},[20,3257,3258],{},"网络",[273,3260,3261],{},"Wi-Fi STA + DHCP",[273,3263,3264],{},"QEMU SLIRP 用户态网络",[258,3266,3267,3272,3275],{},[273,3268,3269],{},[20,3270,3271],{},"内存 \u002F 存储",[273,3273,3274],{},"16 MB Flash + 八线 PSRAM（数据面位于 PSRAM）",[273,3276,3277],{},"31 MB 模拟 RAM，无持久存储",[258,3279,3280,3285,3288],{},[273,3281,3282],{},[20,3283,3284],{},"额外前提",[273,3286,3287],{},"Wi-Fi 凭据",[273,3289,3290],{},"Zephyr e1000 驱动补丁",[258,3292,3293,3298,3301],{},[273,3294,3295],{},[20,3296,3297],{},"典型用途",[273,3299,3300],{},"实际场景评估和硬件测试",[273,3302,3303],{},"开发阶段的快速验证",[16,3305,3306],{},"两个示例共用同一份应用源码和同一套 NanoNNG ExternalProject 编译流程，区别只在于板级支持和网络层。",[16,3308,3309,3310,3312],{},"开发阶段推荐使用 ",[35,3311,49],{},"。完整地编译并运行一次只需几秒，比在硬件上烧录再复位快得多。",[16,3314,3315],{},"实际场景评估和硬件测试请使用 ESP32-S3 示例。",[16,3317,3318],{},"两个示例完整的编译和运行步骤见本文对应章节。",{"title":225,"searchDepth":3320,"depth":3320,"links":3321},2,[3322,3323,3324,3329,3337,3345,3351,3352,3358,3362,3363],{"id":14,"depth":3320,"text":14},{"id":60,"depth":3320,"text":61},{"id":112,"depth":3320,"text":113,"children":3325},[3326,3328],{"id":122,"depth":3327,"text":122},3,{"id":160,"depth":3327,"text":160},{"id":183,"depth":3320,"text":183,"children":3330},[3331,3332,3333,3334,3335,3336],{"id":186,"depth":3327,"text":186},{"id":343,"depth":3327,"text":344},{"id":513,"depth":3327,"text":513},{"id":583,"depth":3327,"text":584},{"id":672,"depth":3327,"text":672},{"id":793,"depth":3327,"text":793},{"id":899,"depth":3320,"text":900,"children":3338},[3339,3340,3341,3342,3343,3344],{"id":903,"depth":3327,"text":904},{"id":941,"depth":3327,"text":942},{"id":999,"depth":3327,"text":1000},{"id":1069,"depth":3327,"text":1070},{"id":1128,"depth":3327,"text":1129},{"id":996,"depth":3327,"text":996},{"id":1265,"depth":3320,"text":1266,"children":3346},[3347,3348,3349,3350],{"id":1269,"depth":3327,"text":1270},{"id":1318,"depth":3327,"text":1319},{"id":1415,"depth":3327,"text":1416},{"id":1451,"depth":3327,"text":1452},{"id":1481,"depth":3320,"text":1481},{"id":1723,"depth":3320,"text":1723,"children":3353},[3354,3355,3356,3357],{"id":1740,"depth":3327,"text":1741},{"id":1909,"depth":3327,"text":1910},{"id":2597,"depth":3327,"text":2598},{"id":2690,"depth":3327,"text":2691},{"id":2818,"depth":3320,"text":2818,"children":3359},[3360,3361],{"id":2824,"depth":3327,"text":2824},{"id":2854,"depth":3327,"text":2854},{"id":2886,"depth":3320,"text":2887},{"id":3203,"depth":3320,"text":3204},"\u002Fimages\u002Fblog\u002Fzh\u002Fnanomq-on-zephyr-rtos\u002Fcover.webp",684,1520,"2026-09-29","借助 Zephyr RTOS 在 MCU 上直接运行 NanoMQ MQTT Broker，不再需要单独的 Linux 网关。本文介绍 ESP32-S3 和 qemu_x86 两个示例、NanoNNG 的 Zephyr 平台适配层，以及目前的限制。",false,"md",{},true,"\u002Fzh\u002Fblog\u002Fnanomq-on-zephyr-rtos",{"title":6,"description":3368},{"loc":3373,"lastmod":3367},"zh\u002Fblog\u002Fnanomq-on-zephyr-rtos",[],"ZXu_g3WbYsJLLU0rJWCTXVkNjShn7Kg7nuLyytfCHls","\u002Fblog\u002Fnanomq-on-zephyr-rtos",1794542896955]