4G USB共享网卡

Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.


本方案基于UniRTOS构建,实现4G共享上网与本地Portal管理。

USBNET Demo基于UniRTOS运行,结合网络附着、PDP配置、Datacall建立、USBNET共享、子网配置与NAT转发机制,在移远通信模块上完成USB虚拟网卡共享上网、本地页面管理与运行状态展示。

系统支持在模块侧建立蜂窝数据链路,并在数据链路建立完成后启动USBNET共享,使Windows、Linux、Android终端能将设备识别为虚拟网卡并具备上网能力;同时支持在本地网段开放Portal服务,通过浏览器访问管理页面,查看设备信息、网络状态、上下行速率以及今日/本周/本月流量统计,并可执行连接、断开、重启设备、切换协议等控制操作。

../../_images/image_RUgobtsVdoKvuLxYxDbc0SmInmf.webp

开发资源

资源名称

数量

简要描述

获取方法

pico开发板

1

项目运行的硬件平台

点此购买

USB数据线

1

用于连接PC和开发板,非“仅充电”类型USB-A转USB-C。

点此购买

PC

1

系统为Windows 10/Windows 11。

自行准备

SIM卡

1

可正常上网的USIM卡。

自行准备

快速上手

硬件连接

../../_images/image_YMAibOrLnoVHm1xKN2QcWFL1n6d.webp
  1. 将SIM卡插入到开发板SIM卡槽内。

  2. 使用数据线连接开发板和电脑。

软件部署

开发环境搭建

参见 快速上手,搭建开发环境并完成基本开发流程。

代码拉取

打开一个新的PowerShell窗口,执行以下命令:

# 拉取示例仓库
unirtos-cli new -r unirtos-maker-examples
# 进入该项目
cd unirtos-maker-examples/4g_donlg

修改配置参数

在打开的PowerShell窗口继续执行环境配置命令:

unirtos-cli env-setup

在PowerShell窗口执行固件编译命令(若模块型号非EG800ZCN_LA,请替换为实际型号):

unirtos-cli build -m EG800ZCN_LA -v EG800ZCN_LA_BETA_20260904

等待编译结束后,编译输出末尾会提示固件编译结果:

SUCCESS: Unirtos project built successfully!

编译成功后,使用QFlash将固件烧录到开发板。

板上验证

  1. 长按开发板电源键(PWKY)开机,打开EPAT工具,根据 日志调试 进行配置,配置完成后重启开发板,从模块上电开始抓取模块日志,确保日志输出完整。

../../_images/image_W51jbVCRpomm31xHAMxci2wMnff.webp
  1. 在网络适配器中确认是否出现Remote NDIS based Internet Sharing Device设备。

../../_images/image_On7fbTJ1Hom9srxFTPRcs5qqnlg.webp
  1. 打开浏览器访问192.168.6.1,能打开内置页面即表示联网成功。

../../_images/image_ISXzbmOTdocCkhxwZpbck2RWnjg.webp

软件设计详解

方案框架

../../_images/image_HpISbx6z7ogZKoxk2dbcrl7hnNc.webp

业务流程

image

启动流程

image

启动流程说明

  1. 系统启动后,应用入口函数 unir_usbnet_demo_init() 通过 UNIRTOS_APP_EXPORT() 自动注册并执行。

  2. unir_usbnet_demo_init() 负责检查主任务是否已存在;若不存在,则创建 qosa_usbnet_demo_task() 主任务。

  3. qosa_usbnet_demo_task() 启动后,先初始化运行参数,读取当前USBNET类型,并在类型不符合预期时执行 qosa_usbnet_set_type() 和设备复位。

  4. 当USBNET类型满足要求后,主任务继续等待网络附着,配置PDP Context,设置USB子网,启用NAT,建立Datacall,并启动USBNET共享链路。

  5. USBNET启动成功后,程序初始化Portal状态缓存,并依次创建状态采集任务 qosa_usbnet_status_task() 和Portal服务任务 qosa_usbnet_portal_task();若已启用应用层DNS方案,仍会继续创建DNS任务。

  6. 主任务完成初始化后进入监督循环,持续处理待执行动作、执行策略检查,并维持服务运行;由此,Portal任务既能响应页面控制请求,也不必直接承担底层链路的控制压力。

代码讲解

项目结构

usbnet/
├── CMakeLists.txt                         # 工程构建入口
├── env_config.json                        # 构建环境配置
├── usb_net.c                              # USBNET Demo核心业务实现
├── menuconfig/                            # 配置目录
├── qos_build/                             # 构建输出目录
├── release/                               # 打包产物目录
└── tools/                                 # 辅助工具目录

入口初始化

unir_usbnet_demo_init():

  1. 打印启动日志。

  2. 检查USBNET Demo主任务是否已存在。

  3. 调用 qosa_task_create() 创建 qosa_usbnet_demo_task()。

void unir_usbnet_demo_init(void)
{
    int err = 0;

### 主任务与建链逻辑
    if (g_usbnet_demo_task_ptr == QOSA_NULL)
    {
        QLOGI("create usbnet demo task");
        err = qosa_task_create(&g_usbnet_demo_task_ptr, 4 * 1024, QOSA_PRIORITY_NORMAL, "usbnet_demo", qosa_usbnet_demo_task, QOSA_NULL);
        if (err != QOSA_OK)
        {
            QLOGI("USBNET demo init task create error");
            return;
        }
        QLOGI("create usbnet demo task ok");
    }

    return;
}
UNIRTOS_APP_EXPORT(200, "usb_net", unir_usbnet_demo_init);

上述代码通过UniRTOS的应用注册宏 UNIRTOS_APP_EXPORT(),将USBNET Demo的入口函数注册到系统应用初始化列表中,由框架统一调度。

主控制与建链逻辑

USBNET Demo的主控制逻辑位于 usb_net.c 的 qosa_usbnet_demo_task(),链路建立的入口位于 usb_net.c 的 _usbnet_demo_connect_data()。

static qosa_bool_t _usbnet_demo_connect_data(void)
{
    qosa_int32_t ret = 0;
    qosa_datacall_conn_t conn = {0};
    qosa_usbnet_config_t *config = QOSA_NULL;

    conn = qosa_datacall_conn_new(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx, QOSA_DATACALL_CONN_UNDEFINED);
    if (!qosa_datacall_get_status(conn))
    {
        _usbnet_demo_configure_pdp_context(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);
        ret = qosa_datacall_start(conn, USBNET_DEMO_WAIT_ATTACH_MAX_WAIT_TIME);
        if (ret != QOSA_DATACALL_OK)
        {
            QLOGI("portal connect datacall fail, ret=%d", ret);
            return QOSA_FALSE;
        }
    }

    if (!_usbnet_demo_apply_usbnet_config(g_usbnet_demo_method))
    {
        QLOGI("portal apply usbnet config fail");
        return QOSA_FALSE;
    }

    _usbnet_demo_configure_usbnet_subnet(g_usbnet_demo_portal_simid);
    _usbnet_demo_log_usbnet_subnet(g_usbnet_demo_portal_simid);
    _usbnet_demo_enable_nat(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);
    _usbnet_demo_cache_upstream_dns(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);

    config = (qosa_usbnet_config_t *)qosa_malloc(sizeof(qosa_usbnet_config_t));
    if (config == QOSA_NULL)
    {
        return QOSA_FALSE;
    }

    config->simid = g_usbnet_demo_portal_simid;
    config->pdpid = g_usbnet_demo_portal_profile_idx;
    config->method = g_usbnet_demo_method;

    ret = qosa_usbnet_start(g_usbnet_demo_portal_simid, _usbnet_demo_func_cb, config);
    if (ret != QOSA_USBNET_ERR_OK)
    {
        QLOGI("portal usbnet start fail, ret=%d", ret);
        qosa_free(config);
        return QOSA_FALSE;
    }

    return QOSA_TRUE;
}

上述代码体现了USBNET Demo的主链路策略:先建立蜂窝数据出口,再配置USB本地子网并启用NAT,最后启动USBNET共享。Portal页面与状态服务依赖底层链路的可用性。

状态模型与缓存

页面展示依赖统一状态对象 usbnet_demo_portal_status_t。该结构定义位于 usb_net.c,主要聚合以下几类信息:

  • 设备信息:型号、固件版本。

  • 网络信息:网络状态、运营商、网络制式、USBNET状态。

  • 流量信息:上下行速率、今日/本周/本月统计。

  • 控制信息:自动重连、策略状态、活动设备、限制额度。

  • 原始统计字段:总流量、上下行字节、时间有效标记。

Portal页面所需的动态数据,统一由 usb_net.c 的 _usbnet_demo_collect_portal_status() 和 usb_net.c 的 _usbnet_demo_collect_portal_status_light() 采集,再通过缓存接口集中保存。

static void _usbnet_demo_collect_portal_status(usbnet_demo_portal_status_t *status)
{
    qosa_int32_t len = 0;
    qosa_uint8_t ps_status = QOSA_NW_REG_UNKNOWN;
    qosa_datacall_conn_t conn = {0};
    qosa_datacall_traffic_statistics_t traffic_statistics = {0};
    qosa_nw_oper_name_t short_name = {0};
    qosa_usbnet_status_e usbnet_status = QOSA_USBNET_STATUS_IDLE;

    if (status == QOSA_NULL)
    {
        return;
    }

    _usbnet_demo_init_portal_status_defaults(status);
    /* 后续依次采集型号、版本、运营商、注册状态、网络制式、USBNET状态、Datacall状态和流量统计 */
}

usb_net.c 中的状态采集任务 qosa_usbnet_status_task() 采用“重采集+轻采集”双路径模型:低频周期执行全量状态更新,高频周期只刷新时间、流量这类热点字段。由此,既能保持页面近实时刷新,又能降低对底层接口的查询压力。

对应的缓存读写函数位于 usb_net.c。这种“先采集、再缓存、后输出”的组织方式,使页面渲染和状态采样解耦,避免多个HTTP请求同时直接访问底层接口。

Portal页面与状态接口

Portal服务主入口位于 usb_net.c 的 qosa_usbnet_portal_task(),请求处理主逻辑位于 usb_net.c 的 _usbnet_demo_handle_portal_client()。

static void qosa_usbnet_portal_task(void *arg)
{
    qosa_int32_t portal_fd = -1;
    qosa_int32_t captive_fd = -1;
    qosa_int32_t client_fd = -1;

    QOSA_UNUSED(arg);
    QLOGI("usbnet portal task start");
    while (g_usbnet_demo_service_running)
    {
        portal_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_PORTAL_PORT);
        captive_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_CAPTIVE_PORT);
        /* 通过select/accept接收客户端请求,并调用_usbnet_demo_handle_portal_client()处理 */
    }
}

Portal服务承担两类输出:

  • 页面输出:返回Portal管理页HTML。

  • 数据输出:返回 /status JSON。

其中 /status JSON的生成由 usb_net.c 的 _usbnet_demo_build_status_json() 完成,页面中的网络状态、上下行速率、今日/本周/本月流量、策略状态和通知信息均来自该函数。

页面并非独立前端工程,而是由C代码在运行时拼接HTML、CSS和JavaScript后直接返回浏览器;这种方式部署简单、资源集中,但模板较长、维护成本偏高。为此,代码按功能将模板拆分为多段拼接,以降低超长格式串带来的风险。

控制动作与监督循环

Portal页面支持连接、断开、重启、协议切换等控制动作,但这些动作不会直接在HTTP请求处理中执行,而是先入队,再由主任务统一处理。

static void _usbnet_demo_process_pending_action(void)
{
    usbnet_demo_portal_action_e action = g_usbnet_demo_pending_action;
    if (action == USBNET_DEMO_PORTAL_ACTION_NONE)
    {
        return;
    }

    g_usbnet_demo_pending_action = USBNET_DEMO_PORTAL_ACTION_NONE;
    g_usbnet_demo_action_busy = QOSA_TRUE;

    switch (action)
    {
        case USBNET_DEMO_PORTAL_ACTION_CONNECT:
            _usbnet_demo_set_notice(_usbnet_demo_connect_data() ? "Connect request executed." : "Connect request failed.");
            break;
        case USBNET_DEMO_PORTAL_ACTION_DISCONNECT:
            _usbnet_demo_set_notice("Disconnect request executing.");
            _usbnet_demo_disconnect_data();
            break;
        case USBNET_DEMO_PORTAL_ACTION_REBOOT:
            _usbnet_demo_set_notice("Reboot request executing.");
            qosa_power_reset(QOSA_RESET_NORMAL);
            break;
        default:
            break;
    }

    g_usbnet_demo_action_busy = QOSA_FALSE;
}

这种设计体现了典型的嵌入式控制原则:Portal任务只负责接收请求,耗时动作由主任务统一串行执行,从而降低HTTP请求与底层链路控制并发执行的风险。

网络限制与策略设计

网络限制能力由 usbnet_demo_access_policy_t 和 _usbnet_demo_apply_policy_request() 组成,主要支持以下配置:

  • 是否允许网络访问。

  • 是否启用时间计划。

  • 起止时间窗口。

  • 总流量限制。

  • Windows / Linux设备单独限制。

static void _usbnet_demo_apply_policy_request(const char *request, char *message, qosa_uint32_t message_len)
{
    usbnet_demo_access_policy_t next_policy = g_usbnet_demo_policy;
    char value[24] = {0};

    if (_usbnet_demo_extract_query_value(request, "network_enabled", value, sizeof(value)))
    {
        next_policy.network_enabled = _usbnet_demo_parse_bool_value(value, next_policy.network_enabled);
    }
    /* 后续继续解析schedule、limit、device policy等字段 */

    g_usbnet_demo_policy = next_policy;
    _usbnet_demo_set_text(message, message_len, "Network policy saved.");
}

当前实现中,策略保存、页面展示和状态回显均已实现,但 usb_net.c 的 _usbnet_demo_enforce_policy() 仍采用保守实现,没有真正执行硬阻断。这意味着当前版本定位为“管理配置+状态展示”的Demo,而不是完整的接入控制系统。

异步回调与资源回收

USBNET异步回调位于 usb_net.c 的 _usbnet_demo_func_cb()。

static void _usbnet_demo_func_cb(void *ctx, void *argv)
{
    qosa_usbnet_config_t      *usbnet_config = (qosa_usbnet_config_t *)ctx;
    qosa_usbnet_general_cnf_t *cnf = argv;
    QLOGI("usbnet callback enter");

    if ((cnf != QOSA_NULL) && (QOSA_OK == cnf->err_code))
    {
        QLOGI("usbnet callback success");
    }
    else
    {
        QLOGI("usbnet callback failed");
    }

    qosa_free(usbnet_config);
}

该回调不负责业务决策,主要职责是接收SDK异步结果、打印日志并回收 qosa_usbnet_config_t。从职责划分上看,可视为USBNET控制链路的完成通知与资源释放点。

应用演示

为了更直观地体现USBNET Demo的跨平台适配能力,演示场景分别针对Windows PC、Linux(Ubuntu)和Android三类终端展开。三类终端均通过USB接入模块侧USBNET共享网络,在终端侧获取本地网段地址后访问Portal页面,展示“可识别、可上网、可管理、可控制”的完整闭环。

Windows PC平台演示效果

  1. PC识别USB虚拟网卡,在设备管理器-网络适配器中能够看到NDIS,即表示识别成功。

../../_images/image_CA4hbOGupo9aDPxpVDAcypLynQh.webp
  1. 打开 cmd 并执行 ipconfig,确认已获取基本网络信息。此时关闭其余网络接口,仅保留USB网卡的网络接口,电脑仍可正常上网。

../../_images/image_GJYFbUV1Yo0k1Gx4Dv1cRpG6nLc.webp
  1. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息,即表示可正常访问网络。

../../_images/image_X5F1bBaVWoJRhUxE2DgcTO7HnuI.webp

Linux(Ubuntu)平台演示效果

在Linux(Ubuntu)环境下接入模块后,需要按下按键切换ECM协议模式。

  1. 将设备的USB接口接入Linux系统。

  2. 网卡加载后,通过 ifconfig 命令查看网卡状态,确认新增usb1网卡。如下图所示:

../../_images/image_PAIMbrhEroMGOFxD8wfcTS1bnef.webp
  1. 确认网络连接。

通过 ping www.baidu.com确认网络连通性。如下图所示:

../../_images/image_P0u5bMAofo1Cv2xqnUzc7KIznoh.webp
  1. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。

../../_images/image_BE6vb0iBwolLcuxtOJyc8O01nWg.webp

Android平台演示效果

在Android终端环境下,可通过支持OTG的手机连接USB网卡。

  1. Android终端接入设备后,需要打开OTG模式。

  2. 模块连接手机并上电后,手机状态栏会显示<···>图标,表示已识别到USB网卡,此时可通过浏览器正常访问网络。

../../_images/image_LKj9bdMbdoQSABxKyBcc17tOnqb.webp
  1. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。

../../_images/image_MghhbPW9zoA19cxztCYcs6T2nec.webp