分类 默认分类 下的文章

前言

家里有台吃灰的移动魔盒(CMCC HG680-KY,创维代工),芯片是海思 Hi3798MV300H。原系统是运营商定制的安卓,开机广告、各种预装,看着就烦。

这篇记录我怎么把它从"砖头边缘"折腾成一台能跑 Debian 12 + WiFi + HDMI 终端 + 个人博客 的服务器,包括中间踩过的所有坑——尤其是那几次失败的尝试,比成功的部分更值得记下来。


一、硬件底细

项目参数
型号CMCC HG680-KY(创维 M301H 同方案)
SoC海思 Hi3798MV300H(32 位 ARMv7)
内存1 GiB DDR3
存储8 GB eMMC(8GTF4R)
网口百兆
WiFiMediaTek MT7668(SDIO 接口)
视频输出HDMI 2.0
串口TTL 115200 8N1(板上有排针)

关键认知:这颗芯片是 32 位的,很多网上的"海思刷机包"是给 64 位 mv300/mv310 准备的,型号差一个字母就是天壤之别。


二、第一次失败:海纳思 Ubuntu 包用不了

一开始想走捷径——刷现成的海纳思(HiNas)Ubuntu 20.04,网上教程一大把。

结果:

Failed to send the head frame
Boot head format invalid
###Timeout error

HiTool 无论怎么重试都失败。查了半天才明白:

海纳思官方包是 hi3798mv300(64 位) 的引导,而我这台是 MV300H/310(32 位),aux code 里的 DDR 初始化表对不上,传输阶段就被拒绝。

海纳思官方也写着:MV300H「尚在开发中,暂不适配」。

教训 1:先确认芯片具体型号和位宽,再找刷机包。别看名字像就往里刷。


三、第二次失败:想用 USB 盘刷机

接着尝试从 U 盘的 FAT 分区刷写(很多教程推荐,说不必拆机):

usb start
fatload usb 0:1 0x1000000 bootargs.img

结果 u-boot 直接报:

** Reading file would overwrite reserved memory **

换遍了 0x08000000、0x10000000、0x20000000、0x3C000000 等地址,全部不行——海思这版 u-boot 的内存布局把可用区锁死了,fatload 没有空档可用。

教训 2:U 盘刷机不是万能的,海思平台的 u-boot 有内存保留区限制。TFTP 才是可靠通道。


四、第三次失败:恢复安卓 9 时崩溃

中途想验证"能不能刷回去",于是用 TFTP 恢复安卓 9 的 21 个分区。前 16 个分区都成功(blocks written: OK),传到 system.img(1.2GB)时刷到 1013MB 处开始疯狂报错:

*** irq: data abort ***

盒子直接挂在开机动画,最后落到了 console:/ # 的 recovery shell。

教训 3:单次 TFTP 传 1.2GB 大文件容易把内存/网络压垮,大文件必须分块传。


五、成功路径:32 位 Debian + 原厂内核

想明白之后,方案就清晰了:

原厂 fastboot → 原厂 U-Boot → 原厂 32 位内核 (boot 分区不动)
                                    ↓ bootargs 里 root 改成 p22
                     Debian 12 armhf rootfs(写进 userdata 分区)

只改两个地方:

  1. bootargs:把 root=/dev/block/mmcblk0p17 改成 root=/dev/mmcblk0p22
  2. userdata(p22):写入 Debian 的 ext4 镜像

其余 fastboot、recovery、system、vendor 全部保持原样——这就是随时能刷回安卓的底气。

5.1 骗到 U-Boot 命令行

原厂 U-Boot 的 bootdelay 是 0,进不了命令行。改法(只改 5 个字节):

# 安卓的 dd 禁用了 conv=notrunc,必须用 busybox 的
printf '\xfd\x40\xad\x35' | busybox dd of=/dev/block/mmcblk0p2 bs=1 seek=0 conv=notrunc
printf '9'                | busybox dd of=/dev/block/mmcblk0p2 bs=1 seek=14 conv=notrunc
第一个是 CRC(覆盖 data[4:0x10000]),第二个把 bootdelay=0 改成 9。改完上电就有 9 秒窗口按 Ctrl+C,进 fastboot#。

5.2 TFTP 分块刷写

tftp 0x1000000 rootfs3.part00
mmc write 0 0x1000000 0x65E000 0x40000   # p22 起始扇区 0x65E000

9 个 128MB 分块依次写入,每块验证 blocks written: OK。

关键细节:TFTP 服务器在文件大小是 512 整数倍时,必须补一个 0 长度的结束块,否则 u-boot 会报 TFTP server died。

5.3 改造 /init(最大的坑)

第一次启动直接冻在这里:

Failed to mount devtmpfs at /dev: No such device

原厂内核没编 devtmpfs,systemd 天然起不来。解决办法是自己写一个 /init:

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t tmpfs tmpfs /dev
mkdir -p /dev/pts /dev/shm /dev/run /dev/lock /dev/tmp /dev/var/run
mount -t devpts devpts /dev/pts
mount -t tmpfs tmpfs /run
mount -t tmpfs tmpfs /tmp
mknod /dev/null    c 1 3
mknod /dev/zero    c 1 5
mknod /dev/random  c 1 8
mknod /dev/urandom c 1 9
mknod /dev/console c 5 1
mknod /dev/tty     c 5 0
mknod /dev/ttyAMA0 c 204 64
mkdir -p /run/sshd
...
exec /sbin/init

还有串口登录问题——同样因为没有 devtmpfs,systemd 建不出 ttyAMA0.device:

Timed out waiting for device ttyAMA0.device
Dependency failed for serial-getty@ttyAMA0

所以在 /init 里直接拉 agetty:

setsid /sbin/agetty -L 115200 ttyAMA0 vt102 < /dev/ttyAMA0 > /dev/ttyAMA0 2>&1 &

教训 4:嵌入式内核可能缺很多东西,/init 是第一道关,把它当"最小可用环境"来写。


六、WiFi:差点被型号骗了

盒子能被识别为 sdio:c00v037Ad7608——vendor 0x037A,device 0x7608。

一开始按网上教程下了 MT7663 的驱动,死活绑不上。后来才反应过来:

0x7608 = MT7668,不是 MT7663!(MT7663 是 0x7603)

换成 wlan_mt7668_sdio.ko 后,问题变成:mmc2 报 eMMC/MMC/SD Device NOT detected。

翻遍设备树、时钟、复位配置都正常,最后发现真正的钥匙是一个海思私有模块:

insmod hi_sdio_detect.ko

加载它的瞬间:

==== Card detection to detect SDIO card! ====
card connected!
mmc2: new ultra high speed SDR104 SDIO card at address 0001

它会负责给 SDIO 卡上电并触发检测——没有它,SDIO 控制器永远认为没插卡。

接着还缺固件:

wlan: Direct firmware load for wifi.cfg failed with error -2
wlan: Falling back to user helper

去安卓的 vendor 分区里翻,发现 /vendor/firmware/ 下有 wifi.cfg、TxPwrLimit_*.dat 等一堆文件,全部拷到 /lib/firmware/ 后,wlan0 立刻出现。

insmod hi_sdio_detect.ko
sleep 2
insmod wlan_mt7668_sdio.ko
iw dev wlan0 scan     # 立刻扫到一堆 WiFi

后来配了 wpa_supplicant + dhclient,连上 5GHz 的 202,780Mbps,-61dBm。

教训 5:驱动能不能用,看 vermagic 和 SDIO ID,比看名字靠谱一万倍。厂商私有模块(hi_sdio_detect)往往是隐藏的前置条件。


七、USB 键盘:又缺一个驱动

插上 2.4G 无线键盘没反应,/sys/bus/usb/devices/ 是空的——USB 主机控制器根本没起来。

去 vendor 模块里翻到:

ehci-platform.ko
ohci-platform.ko

加载后:

usb 2-1: New USB device found, idVendor=1ea7, idProduct=0002
hid-generic: input,hidraw0: USB HID v1.10 Keyboard [2.4G RF Keyboard & Mouse]

但 /dev/input/event3 还是不存在(同样因为没 devtmpfs/udev),得自己按 sysfs 里的主次设备号 mknod:

for name in os.listdir("/sys/class/input"):
    if name.startswith("event"):
        dev = open(f"/sys/class/input/{name}/dev").read().strip()
        maj, mi = dev.split(":")
        os.mknod(f"/dev/input/{name}", 0o600 | stat.S_IFCHR, os.makedev(int(maj), int(mi)))

然后自己写键位映射(Shift/Ctrl/方向键/退格),把 evdev 事件喂给 PTY。

教训 6:没有 udev 的世界里,所有 /dev 节点都要手搓,/sys/class/*/dev 是你的地图。


八、HDMI:最玄学的一关

这是整个项目里最难的部分。之前一直以为"没有 DRM 驱动 = 没有 HDMI",直到发现:

cat /proc/fb
# 0 hifb
# 1 hifb

海思的 hifb 帧缓冲驱动是在内核里的!而且 /proc/msp/ 下还有一整套显示调试接口:

cat /proc/msp/hdmi0
# HDMIEnable: YES   HotPlug: YES   PhyOutputEnable: YES
cat /proc/msp/hdmi0_sink
# EDID: HKC 9385 C240W    NativeFormat: 1080P_60
cat /proc/msp/disp1
# State: Open   Format: 1080p60   Interface: HDMI0

HDMI 一直在输出,只是没人往帧缓冲里画东西。

踩的坑:

  1. /dev/fb0 节点不存在 → 手工 mknod /dev/fb0 c 29 0
  2. state 是 0,图层是关的 → echo 1 > /sys/class/graphics/fb0/state
  3. 必须持续持有并刷新 framebuffer,进程一退出画面就黑
  4. 内核没编 fbcon(/sys/class/vtconsole 只有 dummy),所以没有现成的文字控制台
  5. 最大的坑:PIL 生成图像时用了 RGB(3 字节/像素),而硬件是 32bpp(4 字节/像素),画面错位重复了 4 次。改成 convert("RGBA") 后立刻正常

最后写了个 fbterm.py:把 PTY 里跑着的 bash 输出渲染到 framebuffer,键盘输入从 USB 键盘来,也给串口镜像一份。屏幕上还带了个状态栏:

hg680ky | eth0 192.168.3.14 | wlan0 192.168.3.38 (202 -61) | SSH up | KBD ok | 14:52:07

教训 7:厂商定制内核里"驱动不存在"往往是错觉——先翻 /proc 下的私有接口,那里面藏着整个 SDK。


九、磁盘:1.1G 变 4.1G 只花了一条命令

装完系统发现只剩 300MB 空间,一直以为要重新分区。后来 cat /proc/partitions 才发现:

259       14    4296704 mmcblk0p22    # 分区 4.1GB

而文件系统只有 1.15GB(因为写入的镜像就是这么大)。剩下的 3GB 一直空着没人用。

一条命令搞定:

resize2fs /dev/mmcblk0p22
# The filesystem on /dev/mmcblk0p22 is now 1074176 (4k) blocks long.
/dev/root  4.1G  699M  3.2G  18% /

教训 8:空间不够先看分区表,别急着重新分区。


十、把博客跑起来

硬件都通了,就想着把它变成一台真正有点用的服务器。

10.1 环境

apt-get install -y nginx php-fpm php-sqlite3 php-curl php-gd php-mbstring

10.2 Typecho

Typecho 1.3 的安装是三步表单(dbNext 字段必须是 none),可以完全用 curl 自动化完成,不用开浏览器。

10.3 nginx

server {
    listen 80;
    server_name 2040819.xyz _;
    root /var/www;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }
}

10.4 内网穿透(重点)

家里的宽带是 CGNAT:

光猫 WAN: 100.64.115.86     ← 运营商级 NAT

这意味着任何端口转发都不可能生效——没有真正的公网 IP。

试过:

  • 路由器 DMZ ❌(双重 NAT + CGNAT)
  • 光猫 DMZ ❌(同样)
  • IPv6 ❌(光猫给的是 /64,路由器不会做中继)
  • 打 10010 申请公网 IP ❌(联通不给)

最后走 Cloudflare Tunnel:

# 1. 授权(浏览器点一下)
cloudflared tunnel login

# 2. 创建隧道
cloudflared tunnel create blog

# 3. 配置入口
cat > /etc/cloudflared/config.yml <<EOF
tunnel: <隧道ID>
credentials-file: /root/.cloudflared/<隧道ID>.json
ingress:
  - hostname: 2040819.xyz
    service: http://localhost:80
  - service: http_status:404
EOF

# 4. 路由 DNS
cloudflared tunnel route dns blog 2040819.xyz

# 5. 开机自启
cloudflared service install

盒子主动往外连 Cloudflare 的边缘节点(1xlax01~1xlax13),访问者连的是 Cloudflare,再由 CF 把流量送进隧道——完全不需要公网 IP。

而且白嫖了 Cloudflare 的 HTTPS 证书和全球 CDN。

教训 9:CGNAT 环境下,反向隧道是唯一出路;别在端口转发上浪费时间。

10.5 域名踩坑

中途踩了个大坑:为了用 Cloudflare,得把域名 NS 改到 CF。但我最开始用的 wuxingcheng.abrdns.com 是 ClouDNS 免费二级域名——免费版不允许改 NS,试了各种"伪接入"技巧(在 zone 里伪造 NS 记录)也被 ClouDNS 拒绝(根域 NS 记录不让加)。

最后花 ¥10 买了个真域名 2040819.xyz,在 Spaceship 注册,NS 改成 Cloudflare 的,5 分钟解决。

教训 10:免费域名和真域名的差别,就在于你能不能控制 NS。这点钱不该省。


十一、最终成果

┌─────────────────────────────────────────────┐
│  硬件: HG680-KY / Hi3798MV300H / 1G RAM     │
├─────────────────────────────────────────────┤
│  系统: Debian 12 armhf (原厂 4.9.118 内核)  │
│  存储: 4.1GB (剩余 3.2GB)                   │
│  网络: eth0 192.168.3.14 静态                │
│        wlan0 MT7668 5GHz 780Mbps            │
│  显示: HDMI 1080p60 实时终端 + 状态栏        │
│  输入: 串口 ttyAMA0 / USB 键盘              │
│  博客: Typecho 1.3 + nginx + PHP8.2         │
│  外网: Cloudflare Tunnel (HTTPS)            │
│  自启: 6 个 systemd 服务全自动               │
└─────────────────────────────────────────────┘

开机自动完成:

服务作用
usb-load加载 ehci/ohci 驱动(键盘)
wifi-load加载 MT7668 驱动(关键:hi_sdio_detect 先行)
wlan-connectwpa_supplicant 连 WiFi + DHCP
time-syncSNTP 校时(无 RTC 芯片)
fbtermHDMI 终端渲染
cloudflared内网穿透隧道
nginx + php8.2-fpm博客服务

十二、踩坑清单(速查)

#坑解法
1海纳思包刷不进芯片是 32 位 MV300H,用 armhf Debian
2U 盘 fatload 失败海思 u-boot 内存保留区限制,改 TFTP
3TFTP 传大文件崩溃分块 + 补 0 长度结束块
4启动冻结 devtmpfs自己写 /init,tmpfs + mknod
5串口没登录提示/init 里直接起 agetty
6SDIO 检测不到卡必须 insmod hi_sdio_detect.ko
7WiFi 驱动绑不上芯片是 MT7668(0x7608),不是 MT7663
8固件加载失败从 vendor 分区拷 wifi.cfg 等全部文件
9键盘没反应加载 ehci/ohci-platform.ko + 手工建 /dev/input 节点
10HDMI 黑屏echo 1 > /sys/class/graphics/fb0/state + 持续持有 fb
11画面重复 4 次PIL 必须 convert("RGBA")(32bpp)
12磁盘只剩 300MBresize2fs 扩容,不用重分区
13端口转发无效CGNAT,改 Cloudflare Tunnel
14免费域名改不了 NS买个 ¥10 的真域名

结语

从一开始连 HiTool 都刷不进去,到最后这台盒子安安静静躺在电视柜下面,开着 WiFi、接着电视显示终端、跑着这个博客——中间大概重启、断电、改配置了几十次。

回头看,真正难的从来不是"哪条命令能跑通",而是搞清楚失败到底是什么原因:

  • 是芯片型号对不上?
  • 是 u-boot 限制?
  • 是内核缺功能?
  • 还是运营商网络根本不给公网 IP?

只要把"为什么失败"这一层剥开,剩下的都只是查文档和试错。

这台盒子现在的功耗大概 3W,一年电费不到 20 块。它能干的事,比它原厂的安卓系统多得多。

—— 吴星呈,2026 年 11 月