在联想 IdeaPad Duet 上运行 Ubuntu
Running Ubuntu on the Lenovo IdeaPad Duet

原始链接: https://vhaudiquet.fr/blog/duet-ubuntu/

以下是该项目摘要的中文译文: 该项目旨在联想 IdeaPad Duet(一款基于联发科 MT8183 的 Chromebook)上运行完整的 Linux 发行版。作者开启了一段将 ChromeOS 更换为 Ubuntu 的技术旅程。在尝试使用 postmarketOS 因系统不稳定而受阻后,作者决定为 Ubuntu 主线内核进行自定义移植(bring-up)。 该项目需要深入研究 Chromebook 复杂的引导流程,其中包括引导加载程序 *Depthcharge*。由于 Depthcharge 需要特定的内核格式,作者必须通过链式加载 U-Boot 来提供一个能够引导 Ubuntu 内核的标准 UEFI 环境。这一过程经历了大量的反复尝试,并在 AI 代理的协助下编写了显示面板及其他硬件组件的自定义驱动程序。 通过重构 U-Boot 并修正内存映射问题,作者成功在设备上引导了 Ubuntu。尽管蓝牙支持和系统挂起等挑战需要大量的调试工作,但该项目最终生成了一个可用的 Ubuntu 镜像。作者强调,虽然 AI 代理是加速开发和发现漏洞的强大工具,但要成功完成硬件移植,仍需要深厚的工程专业知识来审核、维护并向主线提交代码。

抱歉。
相关文章

原文

I own a Lenovo IdeaPad Duet Chromebook, a ChromeOS 2-in-1 ARM64 tablet. I had been running ChromeOS, then postmarketOS, which suddenly broke. Time for some firmware hacking to try and get Ubuntu to run there!

The Lenovo IdeaPad Duet: a 10.1" tablet with a plug-and-play detachable keyboard, a 1920x1200 screen, an ARM64 MediaTek MT8183 SoC and 4 GiB of RAM (Image credits: Lenovo)

I bought the device a while ago, during my first year at engineering school. It was cheap (~100€), used but in really nice condition. I love 10" laptops for their portability, and this one could even work with a stylus for note-taking, a workflow I wanted to try (turns out it wasn't really for me).

I had also never used ChromeOS, and was really curious. At the time I was developing Android apps, and being able to code natively and run the app on-device sounded exciting. It turned out to be disappointing because the performance in ChromeOS was not really there, and I had to run everything Linux through a container, which felt awkward. I quietly stopped using the device.

A while later, I realized that postmarketOS supported it. That meant a real Linux distribution, based on Alpine Linux, with any DE (GNOME, KDE, etc.) and proper Linux tooling and performance. I had to try.

Installing postmarketOS was really easy, thanks to the effort they put into the wiki and the installation guide. Basically: put the device in 'developer mode', download an image, write it to a USB stick and boot from it using the 'developer mode' menu. You then land in a live postmarketOS image you can use to install to the eMMC.

The ChromeOS firmware 'developer mode' menu, which allows you to select boot media, USB or internal storage. Mine is in French. 'Start legacy BIOS' does nothing here; I suspect this only works on x86 Chromebooks, and they just reused the same firmware.

So far, so good: they even have images with preinstalled DEs like GNOME. I'm a heavy GNOME user on my main computer, so I went with that one. Unfortunately, it turned out to be a mistake. GNOME can be quite resource-hungry, which didn't suit the device's weak CPU and 4 GiB of RAM. It was a bit slow, and there was too much friction for it to be really usable. I also find GNOME ill-suited to a "minimalist" workflow - it tends to go all out. Thinking about it now, a tiling window manager with a terminal-centric workflow would have surely been better for the device.

I kept using it from time to time anyway. Eventually I updated to postmarketOS 26.06, and then the device refused to reboot.

I managed to boot the old 25.12 version, but even writing 26.06 to a USB stick and booting it does not seem to work. Something must have changed between the two versions that silently breaks boot on the Duet. While investigating, I realized that all the necessary drivers were now in the mainline kernel. I also now work at Canonical on Ubuntu development, doing bring-up of RISC-V devices. Given the state of the Duet, an Ubuntu bring-up seemed genuinely feasible. So... I have a weekend. Let's do it!

At this point, I have almost no idea how postmarketOS boots on the device, what the boot flow looks like, and whether anything will work with Ubuntu. One of the easiest ways to get 'something that looks like Ubuntu' is to replace the postmarketOS 26.06 rootfs with an Ubuntu rootfs. So that's what I did.

I booted into my postmarketOS 25.12 USB stick. First, I inspected the state of the internal eMMC storage:

Running fdisk on /dev/mmcblk0, printing the partition table

Ok. So there's a mysterious "ChromeOS Kernel" partition, a /boot EFI partition, and then the rootfs partition. An easy way to then install the Ubuntu rootfs is to just replace that one. Which is exactly what I did, in the dumbest possible way:

With that, I was ready to chroot into /mnt to enjoy my new Ubuntu system.

Chrooted from the postmarketOS 25.12 USB stick into /dev/mmcblk0p3 with the new Ubuntu rootfs. I even installed some packages, like fastfetch! Notice the 6.12.87-mt81 kernel: that's the postmarketOS kernel from the USB stick.

Ok, this is all very nice, but living inside a chroot forever and having to boot from USB just to mount the internal storage doesn't feel great at all. Time to see if we can boot into this new rootfs! I expected this to fail, since I assumed something was wrong with the new postmarketOS 26.06 kernel, but to my surprise it worked flawlessly. Which means the 26.06 issue must have been in the rootfs startup programs, silently crashing something.

Booted from internal storage into the Ubuntu rootfs on /dev/mmcblk0p3. Notice the new 6.18.44-mt81 kernel: that's the postmarketOS kernel from the 26.06 installation, on the /dev/mmcblk0p1 or p2 boot partitions.

As you can see, the device now calls itself localhost, so I'd have to do some setup to finish configuration; but I had already created my user account in the chroot before. Another issue: the new rootfs lacks the firmware for the postmarketOS kernel, so Wi-Fi no longer works. A simple fix is to take the 26.06 image, extract the /lib/firmware/6.18.../* tree and copy it into the rootfs, which would restore Wi-Fi.

But that means staying with the postmarketOS kernel, unable to update. I did not want that, I want full Ubuntu running, including the kernel.

So I did the simplest thing: I installed the linux-generic package from the archive. It copied vmlinuz and initrd into /dev/mmcblk0p2, the /boot partition. I also copied the upstream DTB for the device into the /boot partition, so I could hardcode it on the kernel command line if needed. I expected everything to crash, nothing to work, since I didn't know the bootloader and assumed the kernel wouldn't get the right DTB. I rebooted.

To my surprise, the machine booted normally. Better yet: I ran fastfetch again and everything was identical, including the kernel version.

Wait, what? I had just overwritten that kernel in the /boot partition. Is that partition not used at all?

Understanding the boot flow

Clearly, I needed to take a step back and actually understand how the machine boots, as trying random stuff was not going to work anymore. So I read some resources and talked with Claude, giving him (it?) the URLs and asking for a boot flow explanation.

Turns out the boot flow is a bit complicated on Chromebooks. Here are the schematics that Claude generated when explaining the chain to me. I find them quite clear.

Firmware boot chain for Chromebooks. The boot ROM contains the FSBL, which loads Coreboot, which has upstream support for the Duet. Then, Coreboot loads the ARM Trusted Firmware (not relevant to this analysis), which then loads Depthcharge, the bootloader for ChromeOS devices.

So Depthcharge, the ChromeOS bootloader, is also what shows the 'Developer Mode' menu that lets me pick between USB and internal storage at boot. This is really interesting. Now, what is the next step? What does Depthcharge actually load?

Depthcharge booting internal storage. It only cares about the first, mysterious "ChromeOS kernel" partition. That actually contains a FIT image, which here is basically a bundle of kernel, initrd and device tree.

That clears up the mystery! The /boot partition was never actually used at boot, only by postmarketOS to prepare the Depthcharge FIT image (using the mkdepthcharge tool). So installing my new kernel there was not enough. I would also have to re-generate the Depthcharge image.

EFI environment needed

Although this would be very easy, it would unfortunately not work. See, in this boot flow, there was never EFI-capable firmware involved. Neither EDK2 nor U-Boot is used: Depthcharge loads and boots the kernel directly from the FIT image. That works fine for the postmarketOS kernel, but not for the Ubuntu kernel, which is an EFI executable and expects an EFI environment. Ubuntu only boots via UEFI.

So, we need a UEFI-capable firmware. The easiest to get working would be U-Boot: EDK2 can be harder to work with, and U-Boot already has support for the mt8183 SoC. That support is only present as a config for the 'Pumpkin' reference development board of the SoC, so we will need to do some work into U-Boot to be able to boot it. Our goal is to chainload U-Boot via Depthcharge, in order to get a UEFI environment, that will be able to boot Grub which will then boot our Ubuntu kernel:

Target boot flow after Depthcharge.

Let's get started hacking U-Boot! Wait, but do we even know how to print something to the screen? How are we going to debug this? We can't expect U-Boot with no drivers at all for the board to work right away printing debug logs... and bringing up a screen is not easy. Let's think this through.

Our target

Our goal is to have U-Boot chainloaded as a Depthcharge payload, for it to then be able to access the eMMC and load Grub that is there. Grub will then use the UEFI storage drivers, i.e. U-Boot drivers exposed over the EFI API, to load the kernel and initrd.

U-Boot already has support for the mt8183, so we won't need to write the drivers, which makes this work possible. Unfortunately, we will still need board-specific adaptations for the Lenovo IdeaPad Duet. I'll also refer to it by its codename kukui-krane (kukui being the codename for the mt8183-based chromebooks, krane for the specific Lenovo IdeaPad Duet).

Realistically, we will need some form of logging to debug our U-Boot build. Two options:

  • Using a serial output on the board. This is actually possible, with what is called a SuzyQable which enables 'CCD', Closed-Case Debug, and gives you a UART interface on Chromebooks. Unfortunately, I don't have such a cable (I've ordered one).
  • Using the screen directly. Harder, because we would need to write a driver for that specific panel, or the specific framebuffer as initialized by Coreboot. It is also something that we ultimately want: proper U-Boot support for the device. So let's go!

The Coreboot source tree has a krane_panel.c file with some of the panel initialization code. I was not so sure what to do with that, but I figured that either we could use Coreboot/Depthcharge code to rewrite a panel driver, or directly use the framebuffer that Coreboot had already set up. So I gave Claude that suggestion.

Immediately, it started with a wonderful idea: instead of trying to make that work inside U-Boot directly, just write an assembly stub that shows colors on the screen. It also figured out how to read Coreboot's "LBIO" table to get the framebuffer details. I asked it for a detailed prompt for an agent to implement that, and it wrote one.

krane-fb-stub

I rebooted the Duet, installed OhMyPi and connected it to GLM-5.3-Flash. I then gave it the prompt Claude had prepared, and it started implementing the stub and investigating all available kernel/firmware logs on-device.

Produce the smallest possible standalone ARM64 binary that depthcharge (the ChromeOS firmware bootloader) can load as if it were a Linux kernel, which does nothing but locate the already-initialized boot-splash framebuffer and fill it with solid colors at distinct checkpoints. This validates the whole depthcharge → custom-payload pipeline before any real U-Boot bring-up work begins. No U-Boot involved yet — this is a from-scratch freestanding binary, as small and dependency-free as possible.

Extract of the prompt fed to my local OhMyPi + GLM-5.3-Flash agent

The agent happily started writing ARM assembly as the entry point, then C code for the actual "color display" thing, i.e. parsing Coreboot tables and device tree, finding the framebuffer, and utility code for showing colors.

The flow went like this: the agent would make a build, then bundle it into a Depthcharge image and flash it to internal storage. Then I would reboot into it, see what happened, and report back to the agent. It took a total of 10 tries for us to get a fully working stub.

OhMyPi testing the framebuffer stub in QEMU, mid-implementation (not part of the prompt, by the way)

There were small issues, like wrong register layouts, which required going back and comparing the Coreboot and Depthcharge trees. Debugging was hard at first, because "black screen, no backlight" could mean anything, from no payload at all, to any part of the display driver being wrong.

Eventually, it figured out the real issue. Depthcharge was actually blanking the screen, killing the backlight, and disabling the display. That is a real problem, that the agent chose to circumvent by adding a revival step: exactly undoing what Depthcharge did, right after handoff.

Afterwards, debugging got easier: I started seeing backlight patterns (blinks) that I could describe to the agent as feedback. Soon enough, there were colors on the screen.

Finally, the stub was complete, and showed the color sequence imagined by Claude: red (early init), yellow (DTB parsed), green (LBIO parsed), blue (all good). This was actually completely faked by the agent, as early init, DTB and LBIO are all required to find the framebuffer, so it just parsed all and showed all colors in a sequence. In any case, drawing to the framebuffer now works!

联系我们 contact @ memedata.com