从零到一:把 UniKnect Gen1-PRO 移植到 Zephyr RTOS

从零到一:把 UniKnect Gen1-PRO 移植到 Zephyr RTOS(纯 C,含 LVGL 图形界面)

一块 STM32 NUCLEO-F413ZH 主板 + 移远 UniKnect Gen1-PRO 扩展板(EC200U 4G Cat.1 模组),
官方只提供 MicroPython 固件。本文记录如何从官方固件里"挖"出 AT 指令集,
再从零搭建一套纯 C 的 Zephyr 驱动库,最终实现:4G 联网、MQTT 上云、传感器采集、
LVGL 多页面 UI、五向摇杆导航——全部自主可控,无任何闭源依赖。


一、硬件平台

组成 说明
主控板 STM32 NUCLEO-F413ZH(Cortex-M4 @ 96MHz,1.5MB Flash / 320KB RAM,板载 ST-LINK)
扩展板 UniKnect Gen1-PRO:EC200U 4G 模组、AHT20 温湿度、LIS2DH12 加速度计、光敏电阻
显示屏 1.8" ST7735 TFT 扩展板(SPI),板载五向摇杆(电阻分压,单 ADC 线)
关键连接 MCU 与 4G 模组之间是 USART6 三线制 UART(AT 命令口),PG14=TX / PG9=RX

二、第一步:从 MicroPython 固件里提取 AT 指令集

官方 MicroPython 固件是编译好的 ELF,没有公开源码。但模组怎么控制,全藏在这个二进制里。

2.1 思路

EC200U 的 AT 指令是纯文本(AT+QIOPEN、AT+QMTCONN……),编译进固件后以字符串形式存在。
而这份固件 not stripped(带调试符号),所以直接用字符串提取就能看到全部指令。

2.2 提取代码

import re

data = open('UniKnect_MicroPython_F413ZH_v1.6.0.elf', 'rb').read()

# 提取所有可打印字符串(>=6 字符)
strings = re.findall(rb'[ -~]{6,}', data)

at_cmds = set()
for s in strings:
    t = s.decode('ascii', 'ignore')
    # AT 指令形如 AT+QIOPEN / +QMTCONN / AT+CREG
    for m in re.findall(r'\bAT\+[A-Z0-9&]+', t):
        at_cmds.add(m)

for c in sorted(at_cmds):
    print(c)

2.3 提取结果(节选)

AT+QIACT   AT+QICSGP   AT+QIDNSGIP  AT+QIOPEN   AT+QISEND   AT+QICLOSE
AT+QMTCFG  AT+QMTOPEN  AT+QMTCONN   AT+QMTPUB   AT+QMTDISC  AT+QMTCLOSE
AT+QGPS    AT+QGPSLOC  AT+CMGF      AT+CMGS     AT+QFOPEN   AT+QGPSEND ...

一共提取出 117 条,按功能分组后就是一张完整的"固件行为地图":

功能域 代表指令 用途
网络 AT+CPIN? AT+CEREG? AT+QIACT SIM 检测、注册、PDP 激活
Socket AT+QIOPEN/QISEND/QICLOSE/QIDNSGIP TCP/UDP、DNS
MQTT AT+QMTCFG/QMTOPEN/QMTCONN/QMTPUB 完整 MQTT 客户端
GNSS AT+QGPS/QGPSLOC/QGPSEND 定位
文件 AT+QFOPEN/QFREAD/QFWRITE 模组内部文件系统
音频 AT+QTTS/QAUDPLAY TTS 语音合成

这一步的价值:不用猜、不用翻上千页手册,固件里"真正在用的"指令一目了然。

2.4 意外收获:官方 C 框架

顺藤摸瓜还发现移远公开了 C 语言版本的框架 uniknect-c(Apache-2.0),
里面的 at_client 引擎 + ql_* 业务模块(网络/socket/MQTT/HTTP/FTP/GNSS/SMS)
就是这套 AT 指令的完整可读写实现——这成为后面 Zephyr 移植的主力代码。


三、第二步:从零搭建 Zephyr 驱动库

整体分层(每一层都是自己搭的):

应用层        main.c / ui.c / joystick.c          ← 自己的应用
业务层        ql_net / ql_mqtt / ql_socket         ← 移远 C 框架(Apache-2.0)
AT 引擎       at_client(RT-Thread 血统)           ← 移远 C 框架
系统抽象      OSAL(qosa_*,自移植)                ← 本文工作
硬件抽象      hal_uart(自实现)                    ← 本文工作
RTOS          Zephyr v4.4                          ← 现成

3.1 OSAL:把 CMSIS-RTOS2 接口映射到 Zephyr

原框架的 OSAL 是 FreeRTOS/CMSIS-RTOS2 写的,接口是干净的句柄风格。
用 Zephyr 内核对象一一对应实现即可:

/* qosa_sem.c —— 信号量,Zephyr 版 */
typedef struct { struct k_sem sem; } OSA_SemHndl;

int qosa_sem_create(osa_sem_t *semRef, u32_t initialCount)
{
    OSA_SemHndl *h = malloc(sizeof(OSA_SemHndl));
    k_sem_init(&h->sem, initialCount, K_SEM_MAX_LIMIT);
    *semRef = h;
    return QOSA_OK;
}

int qosa_sem_wait(osa_sem_t semRef, u32_t timeout)   /* 0=不等 /-1=永久 /ms */
{
    OSA_SemHndl *h = semRef;
    k_timeout_t t = (timeout == 0)        ? K_NO_WAIT :
                    (timeout == (u32_t)-1) ? K_FOREVER : K_MSEC(timeout);
    return k_sem_take(&h->sem, t) == 0 ? QOSA_OK : QOSA_ERROR_TIMEOUT;
}

int qosa_sem_release(osa_sem_t semRef)   /* ISR 里也能调 */
{
    k_sem_give(&((OSA_SemHndl *)semRef)->sem);
    return QOSA_OK;
}

同样的方法映射:qosa_task → k_thread、qosa_mutex → k_mutex、qosa_msgq → k_msgq、
qosa_timer → k_timer + k_work、qosa_event → k_event。一共 9 个文件,全部零依赖可单测。

3.2 hal_uart:AT 命令的物理通道

5 个函数撑起整个 AT 引擎。核心是 RX 中断 + 环形缓冲 + 事件回调:

/* hal_uart.c —— USART6 中断接收 */
static void at_uart_isr(const struct device *dev, void *user_data)
{
    uint8_t buf[64];
    while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
        if (uart_irq_rx_ready(dev)) {
            int len = uart_fifo_read(dev, buf, sizeof(buf));
            if (len > 0) {
                ringbuffer_put(&g_rb, buf, len);   /* 数据入环 */
                if (g_rx_ind_cb) g_rx_ind_cb();     /* ISR 里给信号量 */
            }
        }
    }
}

void qosa_uart_hardware_init(void)
{
    const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(usart6));
    uart_configure(uart, &(struct uart_config){
        .baudrate = 115200, .parity = UART_CFG_PARITY_NONE,
        .stop_bits = UART_CFG_STOP_BITS_1, .data_bits = UART_CFG_DATA_BITS_8,
        .flow_ctrl = UART_CFG_FLOW_CTRL_NONE });
    uart_irq_callback_set(uart, at_uart_isr);
    uart_irq_rx_enable(uart);
}

at_client 引擎(1200 行)拿过来零修改直接跑——这就是分层抽象的价值。

3.3 传感器与显示:设备树 + Zephyr 驱动

设备树 overlay 声明硬件(三行代码挂一个传感器):

&i2c1 {
    aht20: aht20@38 { compatible = "aosong,aht20"; status = "okay"; };
    lis2dh12: lis2dh12@19 { compatible = "st,lis2dh"; status = "okay"; };
};
&adc1 {
    pinctrl-0 = <&adc1_in15_pc5 &adc1_in11_pc1>;   /* 光敏 + 摇杆 */
    status = "okay";
    channel@f { reg = <15>; zephyr,gain = "ADC_GAIN_1"; ... };  /* 注意 @f 是 16 进制 */
    channel@b { reg = <11>; ... };
};

读传感器(Zephyr 标准 sensor API):

const struct device *imu = DEVICE_DT_GET(DT_NODELABEL(lis2dh12));
sensor_sample_fetch(imu);
sensor_channel_get(imu, SENSOR_CHAN_ACCEL_XYZ, acc);   /* 三轴加速度到手 */

3.4 LVGL 图形界面 + 摇杆导航

Zephyr 自带 LVGL 9.5。关键一步是写了个 display shim(120 行),把自研的
裸 SPI LCD 驱动包装成 Zephyr display API,LVGL 直接对接:

/* LVGL keypad 输入设备:摇杆 → LVGL 导航键 */
static void joy_indev_read(lv_indev_t *indev, lv_indev_data_t *data)
{
    joy_key_t key = joystick_poll();              /* 边沿检测后的方向 */
    if (key != JOY_KEY_NONE) {
        data->key = (key == JOY_KEY_CENTER) ? LV_KEY_ENTER :
                    (key == JOY_KEY_LEFT || key == JOY_KEY_UP) ? LV_KEY_PREV
                                                               : LV_KEY_NEXT;
        data->state = LV_INDEV_STATE_PRESSED;
        data->continue_reading = true;            /* 下一次读补一个 RELEASED */
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
        data->continue_reading = false;
    }
}

摇杆映射:四方向 = 焦点跳转,按下(CENTER)= 确认。

3.5 网络任务:独立线程跑 4G/MQTT

联网是秒级阻塞操作(附着、重连),单独一个线程,主循环永远流畅:

/* 主循环(UI/传感器/摇杆,100ms tick) */
while (1) {
    sensor_read_and_display();               /* 采集 + LCD 刷新 */
    joystick_handle();                       /* 摇杆 → LVGL */
    net_publish("uniknect/zephyr-c", json);  /* 非阻塞投递到网络任务 */
    k_msleep(100);
}

/* 网络任务(独立线程):模组开机 → 附着 → MQTT 连接/保活/重连 */
void net_task_entry(void *arg)
{
    ql_module_hardware_init();      /* 模组开机(耐心探测,不动 PWRKEY) */
    ql_at_uart_init(&cfg);          /* AT 引擎初始化 */
    ql_net_attach(net);             /* 4G 附着拿 IP */
    while (1) {
        mqtt_keepalive_and_publish_from_queue();   /* 队列取出 → 发布 */
    }
}

四、编译与下载

环境:Windows + Zephyr SDK(工作区自带),Git Bash 三条命令:

# 1. 环境(一次)
export ZEPHYR_BASE=/d/luglZephyrproject/zephyr
export ZEPHYR_SDK_INSTALL_DIR=/d/luglZephyrproject/zephyr-sdk-1.0.1/zephyr-sdk-1.0.1
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr

# 2. 编译
cd /d/luglZephyrproject
west build -b nucleo_f413zh /d/UniKnect/zephyr-uniknect -p always -d build/zuk

# 3. 烧录(板载 ST-LINK)
west flash -d build/zuk

烧录完成即自动运行。控制台(ST-Link 虚拟串口,115200)能看到完整启动日志:

*** Booting Zephyr OS build v4.4.0-25 ***
[demo] === sensor+LCD+MQTT demo start ===
[demo] lvgl ui init OK
[INFO] [modem] probing AT... (round 0, up to 25s)
[INFO] [modem] online (round 0)
[INFO] SIM card detected successfully
[INFO] Network registration successful
[net] attach OK, IP: 10.137.5.146
[net] mqtt connected
[demo] {"n":0,"light_mv":1303,"temp_c10":305,"humi_rh10":458,"ax_mg":344,...}


五、最终成果

功能全景

模块 实现
4G 联网 EC200U 自动开机/附着/DNS/NTP 校时,冷启动全自动
MQTT 连接自建服务器,每秒 JSON 上云,断线自动重连
传感器 温湿度(AHT20)、三轴加速度(LIS2DH12)、光敏(ADC)——带丢配置自愈
显示 ST7735 LCD + LVGL 9.5 三页面 UI(主页/联网状态/传感器)
摇杆 五向摇杆 = LVGL 键盘输入,四方向导航 + 按下确认
上报开关 传感器页可开关 MQTT 上报


资源占用

Flash RAM
官方 MicroPython 固件 ~7MB(含解释器+全套库) 320KB 几乎吃满
本文纯 C + LVGL ~270KB ~100KB

MQTT 服务器端实拍


六、写在最后

这套移植最有价值的不是"跑通了一个 demo",而是证明了这条路:

  1. 不依赖厂商闭源固件——AT 指令集从二进制里提取,行为完全透明
  2. 分层抽象让代码可移植——OSAL/HAL 两层换掉就能搬到任何 Zephyr 支持的板子
  3. 官方 C 框架是好东西——at_client 引擎零修改直接用,比自己写状态机省一个月
  4. Zephyr + LVGL 是 MCU 端产品化的现代组合——320KB RAM 的 F413 跑得游刃有余

全部代码(OSAL、hal_uart、摇杆、UI、网络任务)加起来不到 3000 行,
都是可读、可改、可搬走的。希望对大家有帮助,欢迎交流!


1 个赞

:+1: :+1: