- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
导读
embassy-net-nrf91是 Embassy 生态中面向 Nordic nRF91 系列蜂窝调制解调器(如 nRF9160)的embassy-net驱动 crate,它通过 nRF91 应用处理器(Application Core)与蜂窝调制解调器之间的 IPC(Inter-Processor Communication)共享内存通道,把蜂窝网络能力抽象成标准的embassy-net网络设备,让开发者直接使用TcpSocket、UDP、DNS 等高层 API。本文围绕该驱动,从快速上手示例、核心 API、IPC 底层原理到蜂窝上下文管理(APN 配置、认证、重附着)逐层展开,读完后你将能够基于当前仓库在 nRF9160 上搭建一个可用的蜂窝 TCP 客户端,并理解驱动内部"控制块 + 消息列表"的共享内存通信机制。
一、驱动概览:蜂窝调制解调器如何接入 embassy-net
embassy-net-nrf91定位在 embassy-net 的网络设备层。它实现了embassy-net-driver-channel定义的设备接口,对外暴露一个ch::Device类型的网络驱动(NetDriver),可以直接传给embassy_net::Stack创建网络栈,进而获得 TCP/UDP 等 socket 能力。
该驱动在 embassy-net-nrf91/README.md 中明确说明了两点核心事实:
- 它是Nordic nRF91 系列蜂窝调制解调器的
embassy-net驱动; - 可运行在任何 executor 上(Interoperability: "This crate can run on any executor"),这意味着它不依赖特定的调度器实现,与 Embassy 之外的其他 async 运行时也能协作。
关于使用示例,README 指向仓库内的 examples/nrf9160 目录(当前仓库中实际存在examples/nrf9160/Cargo.toml、examples/nrf9160/memory.x以及examples/nrf9160/src/bin/modem_tcp_client.rs示例程序)。
二、快速上手:在 nRF9160 上跑通蜂窝 TCP 客户端
仓库中的 modem_tcp_client.rs 是官方提供的完整可运行示例,它演示了从驱动创建、后台任务派发、APN 配置到 TCP 连接的完整流程。
2.1 中断处理:IPC 中断入口
驱动依赖 nRF91 的 IPC 外设中断来接收调制解调器的通知,示例通过#[interrupt]宏声明中断处理函数,并调用驱动的on_ipc_irq():
#[interrupt] fn IPC() { embassy_net_nrf91::on_ipc_irq(); }on_ipc_irq()在 lib.rs 中的实现会清除 IPC_NS 的中断使能并唤醒驱动内部注册的 waker,驱动后台任务随后轮询 IPC 事件。
2.2 创建驱动:共享内存与三个句柄
驱动通过new_with_trace()(或new())创建,返回四元组(NetDriver, Control, Runner, TraceReader):
let (device, control, runner, tracer) = embassy_net_nrf91::new_with_trace(STATE.init(State::new()), ipc_mem, TRACE.init(TraceBuffer::new())).await;参数含义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
state | &mut State | 驱动共享状态,通过State::new()创建,用StaticCell静态存放 |
shmem | &mut [MaybeUninit<u8>] | 应用核与调制解调器共享的 IPC 内存区(见下文内存要求) |
trace_buffer | &mut TraceBuffer | 调制解调器 trace 数据缓冲,通过TraceBuffer::new()创建 |
shmem在示例中取自链接脚本定义的 IPC 内存区域:
let ipc_mem = unsafe { let ipc_start = &__start_ipc as *const u8 as *mut MaybeUninit<u8>; let ipc_end = &__end_ipc as *const u8 as *mut MaybeUninit<u8>; let ipc_len = ipc_end.offset_from(ipc_start) as usize; slice::from_raw_parts_mut(ipc_start, ipc_len) };对应的 memory.x 为 nRF9160 定义了三个内存区域:
MEMORY { FLASH : ORIGIN = 0x00000000, LENGTH = 1024K RAM : ORIGIN = 0x20010000, LENGTH = 192K IPC : ORIGIN = 0x20000000, LENGTH = 64K } PROVIDE(__start_ipc = ORIGIN(IPC)); PROVIDE(__end_ipc = ORIGIN(IPC) + LENGTH(IPC));可见 IPC 共享内存独占 RAM 低端 64KB(0x2000_0000 起始)。
2.3 后台任务:modem_task 与 net_task
驱动需要两个后台任务持续运行:Runner::run()负责驱动后台处理,embassy_net::Runner::run()负责网络栈:
#[embassy_executor::task] async fn modem_task(runner: Runner<'static>) -> ! { runner.run().await } #[embassy_executor::task] async fn net_task(mut runner: embassy_net::Runner<'static>) -> ! { runner.run().await }Runner::run()的实现在 lib.rs:它注册 waker 后调用state.poll()处理 IPC 事件,并轮询ch.poll_tx()获取待发送的 IP 包,通过消息0x7006_0004(IP send)发送给调制解调器。
2.4 建立网络栈并配置 APN
示例随后初始化网络栈、把驱动挂到接口上,并使用context::Control配置蜂窝上下文:
let control = CONTROL.init(context::Control::new(control, 0).await); spawner.spawn(unwrap!(control_task( control, context::Config { apn: b"iot.nat.es", auth_prot: context::AuthProt::Pap, auth: Some((b"orange", b"orange")), pin: None, }, iface )));最后iface.wait_config_up().await等待接口配置就绪,即可创建TcpSocket发起连接。
三、驱动核心 API:new / new_with_trace 与运行时对象
驱动在 lib.rs 中提供了两个构造入口和若干运行时对象:
new(state, shmem):创建(NetDriver, Control, Runner),不带 trace;new_with_trace(state, shmem, trace_buffer):额外返回TraceReader,用于读取调制解调器内部 trace 通道数据(调试蜂窝协议栈问题时非常有用);NetDriver<'a>:类型别名ch::Device<'a>,传给embassy_net::Stack;Control<'a>:运行时控制句柄,可执行 AT 命令、打开/关闭裸 socket、设置链路状态(lib.rs);Runner<'a>:后台运行器,必须与所有网络操作并发运行(lib.rs);State/TraceBuffer:均为const fn new()创建的共享状态,用StaticCell静态分配。
Control::wait_init()会等待调制解调器完成 IPC 初始化(调制解调器通过 IPC 事件通道 2 上报modem_info后,驱动把init置位并唤醒等待者,见 lib.rs)。
四、底层原理:IPC 共享内存与"控制块 + 消息列表"协议
这是本驱动最核心的机制,全部实现在 lib.rs 中,理解它能帮助你排查驱动集成问题。
4.1 共享内存的布局与分配
new_internal()(lib.rs)在传入的共享内存上通过Allocator顺序分配以下结构:
ControlBlock(控制块,crate 内部#[repr(C)]结构,lib.rs);- RX 缓冲(
RX_SIZE = 8 * 1024,lib.rs); - Trace 缓冲(
TRACE_SIZE = 16 * 1024)。
ControlBlock中包含版本号、RX 基址、两个列表(control list 与 data list)、调制解调器信息指针、trace 指针、消息槽(msgs: [[Message; LIST_LEN]; 2],LIST_LEN = 32)以及 4 个 1500 字节的发送缓冲tx_bufs(lib.rs)。
4.2 内存与安全属性要求
new_internal()对共享内存有严格断言(lib.rs):
- 长度不能为 0;
- 长度必须是 8KB(
SPU_REGION_SIZE)的整数倍; - 指针必须 8KB 对齐;
- 共享内存必须位于 RAM 低端 128KB 内(
shmem_ptr + shmem_len < 0x2002_0000)。
驱动还会通过SPU_S(System Protection Unit,安全侧)把对应 RAM 区域配置为非安全(secattr(false))、可读可写可执行,并把外设 ID 42(IPC 相关)设为非安全,从而允许非安全侧的调制解调器访问共享内存(lib.rs)。
4.3 消息列表协议
应用核与调制解调器之间通过两条"列表"(control list 与 data list)交换Message。Message是#[repr(C)]结构(lib.rs),包含:
id(高 16 位区分命令类型、低 16 位区分子类型,如 AT 命令0x0001_0003、打开 socket0x7001_0004、IP 发送0x7006_0004、关闭 socket0x7009_0004);channel(1 = control,2 = data);data/data_len(载荷指针与长度);param(44 字节参数区)。
每个ListItem(lib.rs)由state(高 16 位序号,低 8 位标记 sent/held/freed)与message指针组成。发送方把Message写入消息槽、设置列表项状态并触发对应 IPC 通道的tasks_send;接收方轮询 IPC 事件、检查列表项状态与序号匹配后处理消息(process(),lib.rs)。
4.4 IPC 事件通道与中断唤醒
驱动使用IPC_NS的事件通道 0/2/4/6/7(lib.rs):
| 通道 | 用途 |
|---|---|
| 0 | 一般事件(清除) |
| 2 | 调制解调器初始化完成,上报modem_info与收发列表 |
| 4 | 调制解调器投递 control/data 消息(数据接收通知、AT 响应等) |
| 6 | 一般事件(清除) |
| 7 | trace 数据就绪 |
所有事件统一由on_ipc_irq()触发 waker,Runner::run()中被唤醒后轮询处理。RX 路径上,调制解调器通过 IP receive 通知(id >> 28 == 9、子类型 1)把数据包送入驱动,驱动复制到PacketBuf并交给ch::Runner::try_rx()(lib.rs);TX 路径则由Runner从ch.poll_tx()取出待发包,经send_message()复制到共享内存的tx_bufs后发送(lib.rs)。
4.5 安全校验:PointerChecker
由于调制解调器返回的是共享内存中的裸指针,驱动使用PointerChecker(lib.rs)对每个传入指针做边界与对齐校验,越界即 panic,防止不可信指针导致内存破坏。
五、蜂窝上下文管理:context::Control 高层 API
底层Control之上,context.rs 提供了面向"某个 PDN 上下文(PDP context)"的高层封装,把 APN 配置、认证、附着、地址获取等流程封装成类型安全的 API。
5.1 配置结构:Config 与 AuthProt
pub struct Config<'a> { pub apn: &'a [u8], // 目标 APN 地址 pub auth_prot: AuthProt, // 认证协议 pub auth: Option<(&'a [u8], &'a [u8])>, // (用户名, 密码) pub pin: Option<&'a [u8]>, // SIM PIN } pub enum AuthProt { None = 0, // 无认证 Pap = 1, // PAP 认证 Chap = 2, // CHAP 认证 }对应 context.rs。Control::new(control, cid)接收底层控制句柄与上下文 IDcid,并会先等待调制解调器初始化完成(context.rs)。
5.2 configure():AT 命令序列
configure(&config)(context.rs)通过at-commandscrate 的CommandBuilder依次下发:
+CFUN=0:先关闭射频功能(避免配置冲突);+CGDCONT=<cid>,"IP",<apn>:配置 PDP 上下文的数据类型与 APN;+CGAUTH=<cid>,<auth_prot>[,<username>,<password>]:配置认证协议与凭据(无凭据时省略用户名/密码参数);+CPIN=<pin>:若有 SIM PIN 则下发,命令失败(返回 ERROR)会被忽略——文档注释明确说明"忽略表示无需 PIN 的 ERROR"。
文档注释特别提醒:configure()会断开当前 APN 连接,仅在配置发生变化时才应调用;配置完成后需要调用enable()激活。
5.3 附着与状态查询
attach():+CGATT=1附着 PDN;detach():+CGATT=0分离 PDN;status():依次执行+CGATT查询附着状态、+CGPADDR查询 IP 地址、+CGCONTRDP查询网关与 DNS,返回Status结构(context.rs):
pub struct Status { pub attached: bool, // 是否已附着 APN pub ip: Option<IpAddr>, // 分配的 IP pub gateway: Option<IpAddr>, // 网关 pub dns: Vec<IpAddr, 2>, // DNS 服务器(最多 2 个) }+CGCONTRDP响应中可解析出网关、主备 DNS 与 MTU 等参数(context.rs),但因 MTU 字段未使用,驱动仍按固定MTU = 1500工作(lib.rs)。
5.4 enable() / disable() 与 PDN 保持
disable():+CFUN=0关闭调制解调器;enable():+CFUN=1开启,并追加%XPDNCFG=1,让调制解调器在 PDN 分离后保持存活(context.rs)。
5.5 run():自动重附着控制循环
run<F: Fn(&Status)>(reattach)(context.rs)是示例中实际使用的核心方法,流程为:
enable()激活调制解调器;wait_attached()每秒轮询一次直到附着成功(context.rs);open_raw_socket()打开 IP 裸 socket 并取得文件描述符(底层消息0x7001_0004,lib.rs);- 调用
set_link_state(LinkState::Up)通知网络栈链路可用,并调用用户回调reattach(&status); - 循环中每 10 秒检查附着状态,一旦掉线则
set_link_state(LinkState::Down)、关闭 socket、等待重新附着后重开 socket 并恢复链路。
用户回调reattach在示例中用于把调制解调器分配的 IP 与默认路由写入 embassy-net 接口(apply_status,modem_tcp_client.rs):
fn apply_status(iface: Iface<'static>, status: &Status) { let Some(IpAddr::V4(addr)) = status.ip else { panic!("Unexpected IP address"); }; unwrap!(iface.set_ip_addrs([IpCidr::V4(Ipv4Cidr::new(addr, 32))])); let stack = iface.stack(); if let Some(IpAddr::V4(gateway)) = status.gateway { unwrap!(stack.routes().add_default_ipv4_route(gateway, iface.handle())); } else { stack.routes().remove_default_ipv4_route(); } }六、Trace 通道:调试蜂窝协议栈的利器
new_with_trace()返回的TraceReader读取调制解调器通过共享内存上报的内部 trace。底层通过 IPC 事件通道 7 通知(lib.rs),调制解调器先上报TraceContext(包含最多 3 个TraceChannel,lib.rs),随后驱动轮询各通道的读写指针,把新增数据按0xEF 0xBE <长度> <通道ID>的头部封装写入TraceWriter(handle_trace,lib.rs)。
示例中trace_task把 trace 数据经 UART(BufferedUarteTx,1M 波特率)转发到调试串口(modem_tcp_client.rs),在调试蜂窝连接问题时可以直观观察调制解调器协议栈行为。
七、特性开关与依赖
crate 的 Cargo.toml 提供两个可选特性:
defmt:启用 defmt 日志格式化(同时打开heapless、embassy-time的 defmt 支持);log:启用标准log宏日志。
依赖方面,驱动基于nrf-pac(crate 内部通过nrf_pac as pac访问寄存器)、cortex-m、embassy-net-driver-channel(设备通道抽象)、embassy-sync(pipe、waker)、embassy-time(定时器)、heapless(定长容器)与at-commands(AT 命令构建/解析)。示例工程 examples/nrf9160/Cargo.toml 中以features = ["defmt", "nrf9160-s", "time-driver-rtc1", "gpiote", "unstable-pac", "time"]使用embassy-nrf,以["defmt", "tcp", "ipv4", "medium-ip", "icmp-ping-reply"]使用embassy-net,构建目标为thumbv8m.main-none-eabihf。
八、互操作性与适用前提
按照 README 的说明,该驱动可运行在任何 executor上——它不直接绑定 embassy-executor 的特定平台实现,仅依赖poll_fn、waker 注册等标准 async 原语。这为在 nRF91 上搭配其他运行时(或自行封装的调度器)使用提供了灵活性。
需要特别说明的适用前提:
- 该驱动面向nRF91 系列(示例与文档均以 nRF9160 为准),依赖其特有的 IPC 外设、SPU 安全属性和 LTE 调制解调器固件;
- 共享内存必须满足 8KB 对齐、位于 RAM 低端 128KB 的约束,链接脚本需按 memory.x 预留 IPC 区域;
- 应用核需运行在 TrustZone 安全侧(示例使用
nrf9160-s特性),因为驱动需要访问SPU_S、POWER_S等安全侧外设来配置调制解调器与共享内存属性。
九、小结
embassy-net-nrf91通过"IPC 共享内存 + 控制块/消息列表"协议,把 nRF91 蜂窝调制解调器封装为标准的embassy-net网络设备:底层new()/new_with_trace()完成共享内存分配、SPU 安全配置与调制解调器启动;Runner在后台轮询 IPC 事件与消息列表;context::Control则把 APN 配置、PAP/CHAP 认证、附着与自动重附着封装成高层 API。从 modem_tcp_client.rs 出发,配合 memory.x 的内存布局与 Cargo.toml 的特性配置,即可快速搭建可复用的蜂窝网络应用。
- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
相关推荐
embassy-net-driver-channel 使用指南:为 embassy-net 编写通道式网络驱动的完整实践
embassy net driver channel 使用指南:为 embassy net 编写通道式网络驱动的完整实践 embassy net driver
嵌入式物联网异步编程embassy-net-driver-channel 演进全解:用通道化接口驱动 embassy-net 异步网络栈
embassy net driver channel 演进全解:用通道化接口驱动 embassy net 异步网络栈 导读 embassy net driver
嵌入式物联网异步编程embassy-net-esp-hosted 集成指南:在 Embassy 异步框架中通过 SPI/SDIO 驱动 ESP-Hosted 网络协处理器
embassy net esp hosted 集成指南:在 Embassy 异步框架中通过 SPI/SDIO 驱动 ESP Hosted 网络协处理器 导读 e
嵌入式物联网异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考