诺亚舟 NP1380 是一款基于 Linux 的电子词典(学习机)。它出厂自带一套完整的图形界面栈,却从来没有公开过官方 SDK。好消息是:固件里什么都有——Qt 库、编译器、控件库,我们只要摸清规则,就能写出和原厂程序一模一样风格的软件。
本系列共三篇:
- 环境搭建与第一个程序(本篇)
- 打造原厂风格的界面
- ABI 兼容与滚动布局实战
认识这台设备
先搞清楚我们面对的是什么:
| 项目 | 详情 |
|---|---|
| CPU | Ingenic JZ4740(MIPSel,小端 MIPS) |
| 系统 | Linux 2.6.24.3 + BusyBox |
| 屏幕 | 320x240 触摸屏 |
| GUI 栈 | Qt/Embedded 2.3.10 + QPE + 诺亚舟私有 NM 控件库 |
| 编译器 | 设备自带 gcc 3.4.4(2006 年的工具链) |
三个名词需要先解释一下,后面会反复出现:
- Qt/Embedded:Qt 2 时代的嵌入式版本,自己直接往 framebuffer 上绘图,不需要 X11。库文件是
libqte.so。 - QPE(Qt Palmtop Environment):基于 Qt/Embedded 的“桌面环境“,提供系统标题栏、主题样式、输入法等。库文件是
libqpe.so。原厂程序的蓝灰色标题栏就是 QPE 画的。 - NM 控件库:诺亚舟私有的控件集(
libnm.so、libNMCommonWidget.so等),提供原厂风格的复选框、按钮、滑块、蓝色滚动条等。
关键结论:固件自带全部运行库。开发流程就是“写 C++ 代码 → 在设备上编译 → 链接固件里的库“,完全不需要自己移植 Qt。
没有真机?用模拟器
整个开发可以在 QEMU 模拟器里完成。OpenNoah 项目维护了一个定制版 QEMU,专门模拟 NP1380(机器类型 noah_np1380),相关资源:
- 定制 QEMU 源码:https://github.com/OpenNoah/qemu
- 模拟器构建说明:https://github.com/OpenNoah/OpenNoah.github.io/wiki/QEMU-Emulation
模拟器通过 SPICE 协议输出画面(用 VirtViewer 连接,端口 41380),并模拟了一块 DM9601 USB 网卡。设备系统里运行着 Dropbear SSH 服务,宿主机通过 root@127.0.0.1:2222 就能登入设备传文件、跑命令。
真机的玩法完全一样:SSH 上去,剩下的步骤没有任何区别。
设备上的编译环境
NP1380 固件自带 gcc 3.4.4,可以直接在设备上编译,省去配置交叉编译工具链的麻烦。对这样一台小设备来说,就地编译一个单文件 GUI 程序只要几秒钟。
编译需要两类头文件:
- Qt 2.3.10 的头文件:固件没有安装完整开发头,需要自备一份
include_resolved/目录(来自 Qt/Embedded 2.3.10 SDK,请自行合法取得)。 - NM 控件的兼容声明:诺亚舟从未公开这些头文件,社区根据固件导出符号恢复了最小声明,放在项目的
include/np1380/目录。第二、三篇会详细用到。
编译命令(设备上执行):
export QTDIR=/opt/qt-2.3.10
export QPEDIR=/opt/QtPalmtop
g++ -DQWS -DQT_NO_DRAGANDDROP -fno-rtti -fno-exceptions \
-I./include -I./include_resolved \
-o qt_hello main.cpp \
-L/opt/QtPalmtop/lib -L/opt/qt-2.3.10/lib \
-lNMCommonWidget -lNMGui -lnm -lqpe -lqte \
-lstdc++ -lm -ldl -lpthread
逐项说明:
-DQWS:告诉 Qt 头文件我们在 Qt/Embedded(QWS,Qt Window System)环境下编译。-fno-rtti -fno-exceptions:Qt 2 不使用 RTTI 和异常,与固件库保持一致的编译配置。-lNMCommonWidget -lNMGui -lnm -lqpe -lqte:链接顺序经过实机验证,从前向后依赖,不要随意调换。- 不需要 qmake,一行 g++ 命令就够了。
第一个程序
下面是一个完整可运行的最小程序:一个占满屏幕的窗口,带原厂系统标题栏,中间显示一行文字。
#include "np1380/abi_storage_compat.h"
#include "np1380/nm_titlebar_compat.h"
#include "np1380/qpe_application_compat.h"
#include <qapplication.h>
#include <qlabel.h>
#include <new>
#include <unistd.h>
/*
* 先照做:每个诺亚舟/QPE 对象都要在这种 4 KiB 槽位里用 placement new
* 构造,不能 delete,程序退出用 _exit()。原因在第三篇详细解释。
*/
static unsigned long g_storage[4][NP1380_ABI_STORAGE_WORDS];
static unsigned int g_used = 0;
static void *nextStorage()
{
return g_storage[g_used++];
}
int main(int argc, char **argv)
{
/* QPEApplication 取代普通的 QApplication,接入已经运行的 QPE 服务。 */
QPEApplication *app = new(nextStorage())
QPEApplication(argc, argv, QApplication::GuiClient);
app->applyStyle();
Global::setDefaultBlueStormStyle();
/*
* 客户区定为 320x214:QPE 会在窗口上方补一条 26 像素的原厂标题栏,
* 加起来正好填满 320x240 的屏幕。
*/
NMQWidget *window = new(nextStorage())
NMQWidget(0, "hello", Qt::WType_TopLevel | Qt::WStyle_StaysOnTop, FALSE);
window->setGeometry(0, 0, 320, 214);
window->setFixedSize(320, 214);
window->setCaption("Hello NP1380");
/* 显式打开系统标题栏。 */
NMTitleButtonsInterface *title = np1380TitleButtons(window);
title->SetCustomButtons(0);
title->SetTitleVisible(TRUE);
QLabel *label = new QLabel("Hello, NP1380!", window, "label");
label->setAlignment(Qt::AlignCenter);
label->setGeometry(0, 80, 320, 40);
label->show();
/* TRUE 表示不让 QPE 再最大化客户区,保留我们算好的 320x214。 */
app->showMainWidget(window, TRUE);
int rc = app->exec();
_exit(rc);
return rc;
}
骨架和任何 Qt 程序一样:应用对象 → 主窗口 → 控件 → 事件循环。不一样的地方只有三处,都是这台设备的规矩:
- 应用对象是
QPEApplication而不是QApplication,它让程序接入已经在运行的 QPE 图形服务。 - 窗口用诺亚舟的
NMQWidget,并通过np1380TitleButtons()拿到标题栏控制接口。 - 所有 QPE/诺亚舟对象都用 placement new 构造在预留槽位里——这是硬性要求,不是风格偏好,第三篇会讲清楚原因。
运行的讲究
编译出的二进制不能直接双击跑,QPE 程序需要一组环境变量,而且必须从 /opt/QtPalmtop/bin 目录启动——QPE 按当前工作目录查找原厂图片、样式和输入法资源,换目录启动会出现图片加载失败、输入法不可用。
运行脚本 run_on_device.sh 的核心内容:
export HOME=/
export QTDIR=/opt/qt-2.3.10
export QPEDIR=/opt/QtPalmtop
export LD_LIBRARY_PATH=/opt/qt-2.3.10/lib:/opt/QtPalmtop/lib:/opt/QtPalmtop/3rdParty:/lib:/usr/lib
export QWS_DISPLAY=LinuxFb:0
export QWS_MOUSE_PROTO=TPanel:/dev/jz_ts
export QWS_KEYBOARD=TTY:/dev/input/event0
cd /opt/QtPalmtop/bin
exec /mnt/UsrDisk/qtdev/qt_hello
三个 QWS_* 变量分别告诉 Qt:画面画到 framebuffer、触摸来自 /dev/jz_ts 触摸面板、按键来自 /dev/input/event0。
文件怎么进设备
这台设备的固件很精简,没有 sftp-server 也没有 scp,SSH 只能跑命令。解决办法朴素但有效:宿主机把文件字节转成十六进制文本,通过 SSH 标准输入喂过去,设备端用 xxd -r -p 还原成文件。
# 宿主机 PowerShell:读字节 -> 转 hex -> 经 SSH 发送 -> 设备端 xxd 还原
$Bytes = [System.IO.File]::ReadAllBytes($LocalPath)
$Lines = for ($Offset = 0; $Offset -lt $Bytes.Length; $Offset += 32) {
$Count = [Math]::Min(32, $Bytes.Length - $Offset)
$Chunk = New-Object byte[] $Count
[Array]::Copy($Bytes, $Offset, $Chunk, 0, $Count)
([BitConverter]::ToString($Chunk) -replace "-", "")
}
$Lines | & ssh -p 2222 -i $KeyPath root@127.0.0.1 "xxd -r -p > '$RemotePath'"
纯文本通道传输,不依赖设备上任何额外工具(xxd 是 BusyBox 自带的),二进制文件也能完好送达。
把上传、编译、启动串起来就是一个一键脚本 deploy_and_build.ps1:
$env:NP1380_SSH_KEY = ".\np1380_id_ed25519" # SSH 私钥
$env:NP1380_KNOWN_HOSTS = ".\np1380_known_hosts" # 独立 known_hosts
$env:NP1380_QT_SDK_ARCHIVE = ".\qt_sdk_include_resolved.tar.gz" # Qt 头文件包(首次)
.\deploy_and_build.ps1 -Run
脚本会把 main.cpp、构建脚本和 include/np1380/ 兼容头传到设备的 /mnt/UsrDisk/qtdev,就地编译,-Run 参数让它顺手结束旧进程、启动新版本并打印日志。改一行代码到看到新界面,一次命令完成。
小结与预告
到这里,环境链路已经打通:QEMU 模拟器(或真机)→ SSH 传文件 → 设备端 g++ 编译 → QPE 环境下运行,并且我们有了第一个带原厂标题栏的窗口。
不过现在这个窗口还很朴素。下一篇《打造原厂风格的界面》会用诺亚舟私有控件把界面武装起来:原厂蓝色滚动条、复选框、分段按钮、滑块、输入法,让程序和系统自带软件看不出差别。