从零到一:把 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",而是证明了这条路:
- 不依赖厂商闭源固件——AT 指令集从二进制里提取,行为完全透明
- 分层抽象让代码可移植——OSAL/HAL 两层换掉就能搬到任何 Zephyr 支持的板子
- 官方 C 框架是好东西——
at_client引擎零修改直接用,比自己写状态机省一个月 - Zephyr + LVGL 是 MCU 端产品化的现代组合——320KB RAM 的 F413 跑得游刃有余
全部代码(OSAL、hal_uart、摇杆、UI、网络任务)加起来不到 3000 行,
都是可读、可改、可搬走的。希望对大家有帮助,欢迎交流!




