云端部署

Kodi + CoreELEC:将 Amlogic 电视盒子改造成播放器

本文介绍如何为 Amlogic 安卓电视盒子制作 CoreELEC 启动卡,并按 SoC、内存和网口规格筛选设备树;从 SD 卡或 U 盘启动,保留原有 Android 系统。

BINBHO · 图文教程

把吃灰的 Amlogic 安卓电视盒子变成 Kodi 盒子:用 CoreELEC 从 SD 卡或 U 盘启动,不改动盒子里原来的安卓系统。整个过程最容易出错的一步是选设备树(dtb)。这个工具读取卡上的设备树文件夹,按 SoC、内存、网口速度排出候选列表,确认后复制成 dtb.img。

先看清成品

  • 镜像: 下载了 CoreELEC 21.3-Omega 的 Amlogic-ng.arm-…-Generic.img.gz,SHA256 和 GitHub Release 上给出的 digest 一致。
  • 镜像内容: 解开后有 512 MiB 的 FAT 分区,里面有 device_trees/(72 个 .dtb)、aml_autoscript、cfgload、config.ini。
  • 挑选设备树: pytest 3 项通过。直接在镜像文件上实测:
    • --soc s905x3 --ram 4 --gbit 给出 sm1_s905x3_4g_1gbit.dtb、sm1_s905x3_4g.dtb。
    • --apply 之后,dtb.img 的 md5 和源文件一致。
    • s905w2 提示“没有匹配”。Wiki 上说明这款 SoC 在 Amlogic-ne 版本里。
    • 文件夹模式(“Device Trees”)也测过。
  • 没有测过: 没有在真实的盒子上启动。

注意:Kodi 官方版本本身不带任何内容。只播放你有权使用的媒体(自己拍的、自己购买的、公共领域或开放授权的),不要安装盗版插件或第三方“全能”仓库。Team Kodi 明确表示不认可盗版内容,官方论坛也禁止讨论盗版插件。

⚠ 变砖和保修风险:刷系统可能导致设备无法启动,并且可能让厂商保修失效;运营商定制的盒子通常属于运营商资产,刷机前先确认你有权这样处理。本文只演示从 SD 卡或 U 盘启动,不改动盒子内置的 eMMC。写卡之前反复确认目标设备名,写错了会清空你电脑上的硬盘。

另外:按 CoreELEC Wiki 的说明,用 SD 卡启动会改动盒子的启动环境。想恢复成只启动安卓,需要按 Wiki 的 “Restore Amlogic device” 用恢复脚本处理(链接见文末)。

做完之后你会得到什么?

成果 说明
pick_dtb.py 列出候选设备树;--apply 复制成 dtb.img;可以操作已挂载的卡,也可以直接操作镜像文件
一张 CoreELEC 启动卡 盒子原来的安卓系统保持不动

需要准备哪些环境?

项目 要求 说明
盒子 Amlogic SoC,安卓 7.1 或更高 CoreELEC Wiki 的硬性要求
SoC 型号 拆盒看芯片,或者在安卓里用硬件信息 App 查看 决定用 ng、ne 还是 no 版本
SD 卡或 U 盘 Class 10 写卡可以用 A 篇的 flash_sd.sh,记得把 sha256 文件换成 CoreELEC 的
Python 3.8 以上 操作镜像文件时需要 mtools

选哪个版本(CoreELEC Wiki “Generic Devices”)

版本 内核 21.3 支持的 SoC(节选)
Amlogic-ng 厂商内核 4.9 S905X/S905W(GXL)、S905X2、S922X/A311D、S905X3、S905X4
Amlogic-ne 厂商内核 5.4 S905X4、A311D2、S905Y4/S905W2、S928X
Amlogic-no 厂商内核 5.15 22.0 ALPHA 版本,测试用

Wiki 写明 1 GB 内存也能用,播放 4K 内容建议 2 GB 以上。

文件结构

37-kodi-B-src/
├── pick_dtb.py
└── test_pick.py

第 1 步:下载、校验、写卡

wget https://github.com/CoreELEC/CoreELEC/releases/download/21.3-Omega/CoreELEC-Amlogic-ng.arm-21.3-Omega-Generic.img.gz
echo "9edf06e752ed285e11a565584a2369a39b734df56bd0852ce37eee3d9ea9d16b  CoreELEC-Amlogic-ng.arm-21.3-Omega-Generic.img.gz" | sha256sum -c
# 实测:OK(digest 来自 GitHub Release 页面上这个文件的 sha256)

第 2 步:挑选设备树

pick_dtb.py:

"""为 CoreELEC 启动卡挑选设备树(.dtb),并复制成根目录下的 dtb.img。
依据 CoreELEC Wiki(Generic Devices):写好镜像后,从设备树文件夹里复制与你的盒子相符的 .dtb 到根目录,重命名为 dtb.img。
设备树选错时,通常的现象是无法启动、没有网络或者没有 Wi-Fi,回到电脑上换一个文件即可。脚本只改 SD 卡或 U 盘,不碰盒子内置的 eMMC。
用法:
  python pick_dtb.py --card /media/you/COREELEC --soc s905x3 --ram 4 [--gbit] [--model gt_king] [--apply 文件名]
  python pick_dtb.py --image ce.img --soc ...   # 直接操作镜像文件(需要 mtools),测试用"""
import argparse, re, shutil, struct, subprocess
from pathlib import Path

def parse(name):
    m = re.match(r"(?P<fam>[a-z0-9]+)_(?P<rest>.+)\.dtb$", name)
    rest = m["rest"] if m else ""
    ram = re.search(r"(?:^|_)(\d)g(?:_|$)", rest)
    return {"name": name, "fam": m["fam"] if m else "", "soc": (re.match(r"(s9\d\d[a-z0-9]*|a311d|p2\d\d)", rest) or [None])[0],
            "ram": int(ram[1]) if ram else None, "gbit": "1gbit" in rest, "generic": bool(re.fullmatch(r"s9\w+_\dg(_1gbit)?", rest))}

def rank(names, soc, ram, gbit, model=None):
    out = []
    for n in names:
        d = parse(n)
        if d["soc"] != soc.lower(): continue
        if model and model.lower() in n: score = 100
        elif d["generic"] and d["ram"] == ram and d["gbit"] == gbit: score = 50
        elif d["generic"] and d["ram"] == ram: score = 20
        else: continue
        out.append((score, n))
    return [n for _, n in sorted(out, key=lambda x: (-x[0], x[1]))]

def fat_offset(img):
    b = open(img, "rb").read(512)
    return struct.unpack("<I", b[454:458])[0] * 512           # 第 1 个分区(FAT)的起始扇区 × 512

class Card:
    def __init__(self, card=None, image=None):
        self.dir, self.img = (Path(card) if card else None), image
        if image: self.at = f"{image}@@{fat_offset(image)}"
    def dtbs(self):
        if self.dir:
            folder = next((p for p in self.dir.iterdir() if p.is_dir() and p.name.lower().replace(" ", "_") == "device_trees"), None)
            if not folder: raise SystemExit("没找到设备树文件夹:确认这是刚写好的 CoreELEC 卡")
            return sorted(p.name for p in folder.glob("*.dtb"))
        out = subprocess.run(["mdir", "-i", self.at, "-b", "::/device_trees"], capture_output=True, text=True, check=True).stdout
        return sorted(l.rsplit("/", 1)[-1] for l in out.split() if l.endswith(".dtb"))
    def apply(self, name):
        if self.dir:
            folder = next(p for p in self.dir.iterdir() if p.name.lower().replace(" ", "_") == "device_trees")
            shutil.copyfile(folder / name, self.dir / "dtb.img")
        else:
            subprocess.run(["mcopy", "-o", "-i", self.at, f"::/device_trees/{name}", "::/dtb.img"], check=True)

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    g = ap.add_mutually_exclusive_group(required=True); g.add_argument("--card"); g.add_argument("--image")
    ap.add_argument("--soc", required=True); ap.add_argument("--ram", type=int, required=True)
    ap.add_argument("--gbit", action="store_true"); ap.add_argument("--model"); ap.add_argument("--apply")
    a = ap.parse_args()
    card = Card(a.card, a.image); names = card.dtbs()
    if a.apply:
        if a.apply not in names: raise SystemExit(f"卡上没有 {a.apply}")
        card.apply(a.apply); print(f"✓ 已复制 {a.apply} → dtb.img")
    else:
        c = rank(names, a.soc, a.ram, a.gbit, a.model)
        print("候选(从上到下依次尝试):" if c else f"没有匹配 {a.soc} 的设备树,请确认 SoC,以及这个版本(ng/ne/no)是否支持")
        for n in c: print("  ", n)

test_pick.py:

from pick_dtb import rank, parse
NAMES = ["sm1_s905x3_4g_1gbit.dtb", "sm1_s905x3_4g.dtb", "sm1_s905x3_2g.dtb", "sm1_s905x3_beelink_gt1mini2.dtb",
         "g12b_s922x_beelink_gt_king.dtb", "gxl_p212_2g.dtb"]
def test_generic_order():
    assert rank(NAMES, "s905x3", 4, True) == ["sm1_s905x3_4g_1gbit.dtb", "sm1_s905x3_4g.dtb"]
    assert rank(NAMES, "s905x3", 4, False)[0] == "sm1_s905x3_4g.dtb"
def test_model_first():
    assert rank(NAMES, "s905x3", 4, True, "gt1mini2")[0] == "sm1_s905x3_beelink_gt1mini2.dtb"
def test_parse():
    d = parse("sm1_s905x3_2g_1gbit.dtb"); assert (d["soc"], d["ram"], d["gbit"], d["generic"]) == ("s905x3", 2, True, True)
$ python pick_dtb.py --card /media/me/COREELEC --soc s905x3 --ram 4 --gbit
候选(从上到下依次尝试):
   sm1_s905x3_4g_1gbit.dtb
   sm1_s905x3_4g.dtb
$ python pick_dtb.py --card /media/me/COREELEC --soc s905x3 --ram 4 --apply sm1_s905x3_4g_1gbit.dtb
✓ 已复制 sm1_s905x3_4g_1gbit.dtb → dtb.img
$ python pick_dtb.py --card /media/me/COREELEC --soc s905w2 --ram 2
没有匹配 s905w2 的设备树,请确认 SoC,以及这个版本(ng/ne/no)是否支持

如果你的盒子型号正好在列表里(比如 beelink_gt_king),用 --model 参数,它会排在最前面。

第 3 步:第一次启动(Wiki 给出的几种方法)

方法 做法
牙签法 断电,插卡,按住复位键再接通电源,看到 CoreELEC 标志后松开
终端法 在安卓里装一个终端模拟器,插卡后输入 reboot update
恢复模式 在安卓里选择“重启到恢复模式”

检查点:能进入 CoreELEC 的设置向导,并且网口或 Wi-Fi 能联网。如果没网,通常是设备树不对:回到电脑上用 --apply 换下一个候选再试。拔掉卡以后盒子应该还能进入原来的安卓。以上都没有在真机上测过。

调试示例:Wiki 写的是 “Device Trees”,镜像里却是 device_trees

一开始我照着 Wiki 的写法找 Device Trees 文件夹。实际查看 21.3 的镜像,用 mdir 列出来是:

::/aml_autoscript
::/cfgload
::/device_trees/
::/config.ini

所以脚本按“忽略大小写,并把空格当成下划线”来匹配文件夹名,两种写法都能找到。文件夹模式和镜像模式都实测过。

常见问题

现象 原因 处理
插卡后还是进安卓 启动方式没有生效 换一种启动方法;有些盒子只支持其中一种
有画面但没网 设备树不匹配,比如 1gbit 选成了百兆 换候选列表里的下一个
想彻底恢复成安卓 启动环境已经被改了 按 Wiki 的 “Restore Amlogic device” 处理
运营商定制盒子 启动加了锁,而且盒子可能属于运营商 不要刷,联系运营商

模板规则

  • 只从 SD 卡或 U 盘启动,不往 eMMC 里安装。
  • 先确认 SoC,再选版本和设备树;一次只换一个变量。
  • 只用合法内容,不装盗版插件。

项目资源

用途 地址
CoreELEC Generic Devices https://wiki.coreelec.org/coreelec:devgeneric
CoreELEC 安装指南 https://discourse.coreelec.org/t/how-to-install-coreelec/
恢复 Amlogic 设备 https://wiki.coreelec.org/coreelec:restore
CoreELEC 版本发布 https://github.com/CoreELEC/CoreELEC/releases
Is Kodi legal?(官方) https://kodi.tv/article/is-kodi-legal/
Kodi 论坛规则与盗版政策 https://kodi.wiki/view/Official:Forum_rules

旧盒子能不能用,取决于 SoC 和设备树这两件事;不碰 eMMC,失败了拔卡就能回到原来的系统。这几件事做对了,刷机风险就小很多。