Ubuntu draaien op de Lenovo IdeaPad Duet
Ik bezit een Lenovo IdeaPad Duet Chromebook, een ChromeOS 2-in-1 ARM64-tablet. In eerste instantie draaide ik ChromeOS, daarna postmarketOS, maar dat laatste stopte plotseling met werken. Het werd tijd voor wat firmware-hacking om te proberen Ubuntu erop te krijgen.
De Lenovo IdeaPad Duet
De Lenovo IdeaPad Duet is een tablet van 10,1 inch met een plug-and-play afneembaar toetsenbord, een scherm met een resolutie van 1920x1200, een ARM64 MediaTek MT8183 SoC en 4 GiB RAM.
Ik kocht het apparaat een tijd geleden tijdens mijn eerste jaar op de ingenieursopleiding. Hij was goedkoop (~100€), gebruikt maar in zeer goede staat. Ik hou van 10-inch laptops vanwege hun draagbaarheid, en deze kon zelfs met een stylus voor aantekeningen worden gebruikt, een workflow die ik wilde proberen (hoewel dat uiteindelijk niet echt iets voor mij bleek te zijn).
Daarnaast had ik nog nooit ChromeOS gebruikt en was ik erg nieuwsgierig. Op dat moment ontwikkelde ik Android-apps, en de mogelijkheid om native te coderen en de app direct op het apparaat uit te voeren klonk spannend. Dit bleek echter tegenvallen omdat de prestaties in ChromeOS onvoldoende waren en ik alles voor Linux via een container moest draaien, wat onhandig aanvoelde. Ik stopte stilzwijgend met het gebruik van het apparaat.
Een tijd later realiseerde ik me dat postmarketOS het ondersteunde. Dat betekende een echt Linux-distributie, gebaseerd op Alpine Linux, met elke gewenste Desktop Environment (GNOME, KDE, etc.) en proper Linux-tooling en prestaties. Ik moest het proberen.
Het installeren van postmarketOS was erg eenvoudig dankzij de wiki en de installatiegids. In grote lijnen: zet het apparaat in 'developer mode', download een image, schrijf deze naar een USB-stick en boot vanaf die stick via het 'developer mode'-menu. Vervolgens kom je in een live postmarketOS-image die je kunt gebruiken om te installeren op de eMMC.
In het 'developer mode'-menu van de ChromeOS-firmware kun je kiezen tussen boot-media, USB of interne opslag. De optie 'Start legacy BIOS' doet hier niets; ik vermoed dat dit alleen werkt op x86 Chromebooks en dat ze simpelweg dezelfde firmware hebben hergebruikt.
Tot zover alles goed: ze hebben zelfs images met vooraf geïnstalleerde DE's zoals GNOME. Omdat ik op mijn hoofdcomputer een intensieve GNOME-gebruiker ben, koos ik daarvoor. Helaas bleek dat een fout. GNOME kan vrij veel resources verbruiken, wat niet paste bij de zwakke CPU en 4 GiB RAM van het apparaat. Het was traag en er was te veel wrijving om het echt bruikbaar te maken.
Ik vind GNOME bovendien ongeschikt voor een "minimalistische" workflow; het neigt ernaar alles volledig in te vullen. Terugdenkend zou een tiling window manager met een terminal-centrische workflow zeker beter zijn geweest voor dit apparaat.
Toch bleef ik het af en toe gebruiken. Uiteindelijk update ik naar postmarketOS 26.06, waarna het apparaat weigerde te herstarten. Ik lukte het om de oude 25.12-versie op te starten, maar zelfs het schrijven van 26.06 naar een USB-stick en daarvan booten leek niet te werken. Er moet iets zijn veranderd tussen de twee versies dat het bootproces op de Duet stilzwijgend breekt.
Tijdens het onderzoeken merkte ik dat alle nodige drivers inmiddels in de mainline kernel zitten. Daarnaast werk ik nu bij Canonical aan de ontwikkeling van Ubuntu, waar ik me bezighoud met de bring-up van RISC-V-apparaten. Gezien de staat van de Duet leek een Ubuntu bring-up echt haalbaar. Dus... ik heb een weekend. Laten we het doen!
Ubuntu installeren?
Op dit punt heb ik bijna geen idee hoe postmarketOS boot op het apparaat, hoe de boot-flow eruitziet, of dat er überhaupt iets zal werken met Ubuntu. Een van de gemakkelijkste manieren om "iets dat op Ubuntu lijkt" te krijgen, is door de postmarketOS 26.06 rootfs te vervangen door een Ubuntu rootfs. Dat is wat ik deed.
Ik bootte naar mijn postmarketOS 25.12 USB-stick. Eerst inspecteerde ik de staat van de interne eMMC-opslag met fdisk op /dev/mmcblk0. Er is een mysterieuze "ChromeOS Kernel"-partitie, een /boot EFI-partitie, en vervolgens de rootfs-partitie.
Een eenvoudige manier om de Ubuntu rootfs te installeren is door simpelweg die ene partitie te vervangen:
# mount /dev/mmcblk0p3 /mnt
# ls /mnt/boot
# rm -rf /mnt/*
# curl -L -O https://cdimage.ubuntu.com/.../ubuntu-base-26.04-base-arm64.tar.gz
# tar -C /mnt -xzf ubuntu-base-26.04-base-arm64.tar.gz
Hiermee was ik klaar om via chroot in /mnt mijn nieuwe Ubuntu-systeem te gebruiken. Ik installeerde zelfs enkele pakketten, zoals fastfetch. Opvallend was de 6.12.87-mt81 kernel: dat is de postmarketOS-kernel van de USB-stick.
Het is echter niet ideaal om eeuwig in een chroot te leven en vanaf USB te moeten booten om de interne opslag te mounten. Ik wilde kijken of we direct in deze nieuwe rootfs konden booten. Tot mijn verbazing werkte dit foutloos. Dit betekent dat het probleem met versie 26.06 waarschijnlijk in de opstartprogramma's van de rootfs zat, die ergens stilzwijgend crashten.
Na het booten vanaf de interne opslag naar de Ubuntu rootfs op /dev/mmcblk0p3, zag ik de nieuwe 6.18.44-mt81 kernel (de postmarketOS-kernel van de 26.06-installatie). Het apparaat noemde zichzelf nu localhost en de Wi-Fi werkte niet omdat de nieuwe rootfs de firmware voor de postmarketOS-kernel miste. Dit zou eenvoudig opgelost kunnen worden door de firmware uit de 26.06-image te kopiëren.
Maar dat zou betekenen dat ik vast blijf zitten aan de postmarketOS-kernel en niet kan updaten. Ik wil volledige Ubuntu, inclusief de kernel. Dus ik installeerde het linux-generic pakket uit het archief. Dit kopieerde vmlinuz en initrd naar /dev/mmcblk0p2 (de /boot partitie). Ik kopieerde ook de upstream DTB (Device Tree Blob) voor het apparaat naar de /boot partitie. Ik verwachtte dat alles zou crashen omdat ik de bootloader niet kende en aannam dat de kernel de juiste DTB niet zou krijgen. Ik herstartte.
Tot mijn verbazing bootte de machine normaal. Nog beter: ik draaide opnieuw fastfetch en alles was identiek, inclusief de kernelversie. Wacht, wat? Ik had net die kernel in de /boot partitie overschreven. Wordt die partitie helemaal niet gebruikt?
De Ubuntu kernel gebruiken
De boot-flow begrijpen
Ik moest een stap terugdoen en echt begrijpen hoe de machine boot. Na het raadplegen van bronnen en overleg met Claude (AI), werd de boot-flow van Chromebooks duidelijk.
De firmware boot-chain voor Chromebooks werkt als volgt:
- De Boot ROM bevat de FSBL (First Stage Boot Loader).
- Deze laadt Coreboot, dat upstream ondersteuning heeft voor de Duet.
- Coreboot laadt de ARM Trusted Firmware.
- Deze laadt vervolgens Depthcharge, de bootloader voor ChromeOS-apparaten.
Depthcharge is dus ook degene die het 'Developer Mode'-menu toont. Maar wat laadt Depthcharge vervolgens? Bij het booten vanaf interne opslag kijkt Depthcharge alleen naar de eerste, mysterieuze "ChromeOS kernel"-partitie. Deze bevat een FIT-image: een bundel van kernel, initrd en device tree.
Dit lost het mysterie op! De /boot partitie werd nooit gebruikt tijdens het bootproces, alleen door postmarketOS om de Depthcharge FIT-image voor te bereiden (met de tool mkdepthcharge). Het installeren van mijn nieuwe kernel in /boot was dus niet voldoende; ik zou de Depthcharge-image opnieuw moeten genereren.
Noodzaak voor een EFI-omgeving
Hoewel dit eenvoudig zou zijn, zou het niet werken. In deze boot-flow is er geen EFI-compatibele firmware betrokken. Er wordt geen EDK2 of U-Boot gebruikt; Depthcharge laadt en boot de kernel direct vanuit de FIT-image. Dat werkt voor de postmarketOS-kernel, maar niet voor de Ubuntu-kernel, die een EFI-executable is en een EFI-omgeving verwacht. Ubuntu boot alleen via UEFI.
We hebben dus UEFI-compatibele firmware nodig. De makkelijkste optie is U-Boot. U-Boot heeft al ondersteuning voor de mt8183 SoC, maar alleen als configuratie voor het 'Pumpkin' referentie-ontwikkelingsbord. Er is dus werk nodig in U-Boot om het op de Duet te krijgen. Het doel is om U-Boot via Depthcharge te chainloaden, om zo een UEFI-omgeving te creëren die Grub kan booten, wat vervolgens onze Ubuntu-kernel start.
U-Boot
Ons doel
Het doel is om U-Boot als Depthcharge-payload te chainloaden, zodat het de eMMC kan benaderen en Grub kan laden. Grub gebruikt dan de UEFI-opslagsdrivers (U-Boot drivers via de EFI API) om de kernel en initrd te laden.
U-Boot heeft al ondersteuning voor de mt8183, dus we hoeven geen drivers te schrijven, maar wel bord-specifieke aanpassingen voor de Lenovo IdeaPad Duet (codenaam kukui-krane).
Om U-Boot te kunnen debuggen heb ik logging nodig. Er zijn twee opties:
- Seriële output: Mogelijk via een 'SuzyQable' voor CCD (Closed-Case Debug), wat een UART-interface geeft. Ik had deze kabel nog niet in huis.
- Het scherm direct gebruiken: Moeilijker, want we hebben een driver nodig voor dat specifieke paneel of de framebuffer zoals geïnitialiseerd door Coreboot.
Ik heb Claude gevraagd om mee te denken. Het idee was om een assembly stub te schrijven die kleuren op het scherm toont door de "LBIO"-tabel van Coreboot te lezen voor framebuffer-details.
krane-fb-stub
Ik gebruikte OhMyPi verbonden met GLM-5.3-Flash om de stub te implementeren. De agent schreef ARM-assembly voor het instappunt en C-code voor het tonen van kleuren door Coreboot-tabellen en de device tree te parsen.
Het proces was iteratief: de agent maakte een build, bundelde deze in een Depthcharge-image en flashte deze naar de interne opslag. Na 10 pogingen hadden we een werkende stub. Een groot probleem was dat Depthcharge het scherm blank maakte en de backlight uitschakelde; de agent loste dit op door een "revival"-stap toe te voegen die precies ongedaan maakte wat Depthcharge had gedaan.
Uiteindelijk toonde de stub een kleursequentie: rood (vroege init), geel (DTB geparsed), groen (LBIO geparsed) en blauw (alles ok). Tekenen naar de framebuffer werkte nu!
U-Boot via de framebuffer
Met de framebuffer-hack kon ik U-Boot-logs op het scherm zien. Ik vroeg de agent om mainline U-Boot op het bord werkend te krijgen, inclusief grafische output.
Dit was veel moeilijker dan de stub. Het kostte 27 pogingen voordat U-Boot correct draaide. Het grootste probleem was een aligneringsconflict:
- Depthcharge geeft controle over op een vaste offset (0x40 bytes in de payload), wat op dit apparaat geen veelvoud is van 4096.
- U-Boot (gebouwd als positie-onafhankelijke code) vereist dat het instappunt op een 4096-byte grens ligt.
De oplossing was een shim loader: een klein stukje code op de plek die Depthcharge wil, dat vervolgens springt naar U-Boot op een correcte grens. Uiteindelijk verscheen U-Boot op het display, hoewel in portretmodus in plaats van landschap.
Console en USB-toetsenbord
Ik heb de schermoriëntatie aangepast met de parameter rot=3 voor landschapsmodus en de UEFI-ondersteuning ingeschakeld. Om interactie mogelijk te maken, moest het USB-toetsenbord (dat via pogo-pins is verbonden) werken in de U-Boot-console. Dit vereiste een pre-init sequentie voor de USB-setup.
Tijdens het testen merkte ik dat de console niet correct scrollde; de tekst overschreef zichzelf. Dit bleek een upstream bug te zijn. Ik heb een patch voorbereid die scrolling herstelt op schermen met een vreemde rotatie (rot=1 of rot=3).
Booten van de eMMC
Met bootefi bootmgr probeerde ik te booten, maar ik had nog geen EFI-executable op de partitie. Ik ben via de postmarketOS USB-stick teruggegaan naar de Ubuntu chroot om Grub te installeren:
$ sudo apt install grub-efi-arm64
$ sudo grub-install --target=arm64-efi --efi-directory=/boot/efi --boot-directory=/boot
Nu leidde bootefi bootmgr naar Grub. Er was aanvankelijk een probleem waarbij Grub de eMMC niet kon benaderen (mogelijk omdat U-Boot deze nog bezet hield), maar na handmatige interventie in de Grub-console lukte het. Na aanpassingen in de Grub-configuratie kreeg ik een schoon landschapsmenu.
Tijdens het booten van Ubuntu begon het scherm echter corrupt te raken (knipperende banden, verdwijnende logs). Dit kwam door de framebuffer-hack uit Coreboot; de kernel accepteerde deze staat niet wanneer hij zelf de display wilde overnemen.
De paneeldriver
Om dit op te lossen was een echte paneeldriver nodig. Ik startte een nieuwe agent-sessie voor de implementatie van de MIPI DSI host, de PHY en de driver voor het BOE TV101WUM-NL6 paneel.
Dit betekende dat ik opnieuw "in het donker" werkte (geen schermoutput bij fouten). Gelukkig arriveerde mijn SuzyQ-adapter, waardoor ik seriële toegang kreeg. Na het aanpassen van de baudrate naar 115200 in de defconfig, kon ik de fouten in de paneeldriver spotten. De agent was een eigenschap vergeten tijdens de initialisatie, wat leidde tot een NULL-pointer dereference. Na de fix werkte de paneeldriver.
De eerste kernel-boot
Een ontbrekende module
Bij de eerste poging om Ubuntu 7.0 te booten, bleef de kernel hangen in de initrd. Dracut gaf een emergency console, maar kon geen apparaten zien. Het bleek dat specifieke modules ontbraken in de initrd: mtkpmicwrap, mt6397 en mt6358_regulator. Ik heb hooks toegevoegd aan dracut om deze modules geforceerd te laden.
De hang: repareren met een agent
Na de module-fix bleef de kernel nog steeds hangen, soms voor en soms na de login-prompt. Dit was een nachtmerrie om te debuggen. GPT-6 Astra wees me op een upstream kernel-patch ("pmdomain: mediatek: Fix mt8183 hang on boot"). Ik probeerde dit met kernel 7.0 en later met Ubuntu 26.10 (kernel 7.2), maar de hang bleef bestaan. De patch was niet de oplossing voor mijn specifieke probleem.
Meer betrokkenheid bij het proces
Ik besloot de bootlogs chronologisch te analyseren. In de U-Boot log zag ik dat er slechts 2 GiB RAM werd gerapporteerd, terwijl het apparaat 4 GiB heeft. Ik liet een agent dit repareren door de Coreboot-tabellen correct uit te lezen.
Na het oplossen van diverse kleine fouten in de logs, merkte ik dat het bootproces plotseling vlekkeloos verliep en ik het systeem kon gebruiken.
Ubuntu werkt (?)
Ik draai nu de Ubuntu 26.10 development release met kernel 7.2. Het systeem werkt, maar ik merk dat hij nog steeds hangt bij CPU-intensieve taken (zoals apt upgrade tijdens het genereren van de initramfs).
Opschonen van de U-Boot tree
De U-Boot tree was een puinhoop met 43 commits. Na refactoring en "statische analyse" door AI-modellen heb ik dit teruggebracht naar 20 commits.
Overzicht van de belangrijkste wijzigingen:
| Commit | Beschrijving |
|---|---|
| 1 | Voegt MT8183 kukui krane bord-ondersteuning toe |
| 4 | Fix voor scrolling en cursorpositie op geroteerde consoles (verstuurd upstream) |
| 8 | Voegt MMSYS display clock gates toe |
| 9 | Voegt MediaTek MT8183 GPIO driver toe |
| 10 | Voegt MediaTek MT8183 MIPI TX D-PHY driver toe |
| 11 | Voegt MediaTek MT8183 MIPI DSI host driver toe |
| 12 | Voegt cold display bring-up toe voor krane met BOE paneel |
| 13 | Schakelt display uit bij ExitBootServices zodat kernel kan overnemen |
| 14 | Vergroot pagetabel-budget voor dynamische display mappings |
| 15 | Bepaalt werkelijke DRAM-grootte vanuit vorige bootloader |
| 16 | Bounce DMA boven 4 GiB om U-Boot binnen 32-bit window te houden |
| 18 | Sluit T-PHY af bij verwijdering |
| 19 | Beschrijft SSUSB blok voor U-Boot xhci driver |
| 20 | Finaliseert krane configuratie voor EFI boot |
Ik ben van plan deze patches zorgvuldig te reviewen en te versturen naar de upstream maintainers, waarbij ik zal vermelden dat het proces AI-ondersteund was.
De kernel repareren
Weg met de hang
Ik kon de hang deterministisch reproduceren met een stress-benchmark. Door de memory maps te vergelijken tussen de postmarketOS kernel (via Depthcharge) en de Ubuntu kernel (via U-Boot), vond de agent het verschil: een geheugenareaal dat bij postmarketOS als gereserveerd voor ARM Trusted Firmware stond, maar bij Ubuntu als bruikbaar geheugen. Wanneer programma's veel geheugen vroegen, schreef de kernel in dit gereserveerde gebied, wat leidde tot een security exception en een freeze.
De oplossing was om de volledige memory map uit de Coreboot-tabellen te extraheren en deze te vertrouwen. Na vier pogingen werkte dit, en de stress-test werd succesvol voltooid.
Bluetooth
Bluetooth werkt momenteel niet op de upstream kernel. Er is een specifieke patch in de postmarketOS kernel die dit oplost, maar dit is een revert van een andere upstream patch. Voorlopig gebruik ik de generic kernel zonder Bluetooth.
Een opstartbaar Ubuntu-image
Ik heb al dit werk gecristalliseerd in een bootbaar Ubuntu-image. Dit bevat de gemodificeerde U-Boot in een eerste Depthcharge-partitie. Iedereen met een Lenovo IdeaPad Duet kan dit image downloaden, op een USB-stick zetten en booten via de "USB boot" optie in Developer Mode.
Disclaimer: Dit is geen officieel Ubuntu-image, maar een persoonlijk experiment.
Conclusie
AI gebruiken voor hardware bring-up/debugging
Ik was aanvankelijk sceptisch over berichten dat AI "met één prompt" volledige Linux-ondersteuning voor hardware kon creëren. Mijn ervaring bevestigt dit: het kostte 10 flashes voor een simpele kleur-stub en 27 voor U-Boot. Modellen zijn vaak zelfverzekerd fout.
Echter, als software engineer is AI een formidabel hulpmiddel. Het kan ontbrekende kennis invullen (zoals de boot-flow van Chromebooks) en code genereren voor specifieke taken (zoals de framebuffer driver). De barrière voor instap is lager, maar je hebt nog steeds programmeerervaring en doorzettingsvermogen nodig om de output te reviewen en te onderhouden.
Volgende stappen
De directe volgende stap is het upstreamen van het U-Boot werk. Daarna wil ik proberen het hinderlijke Depthcharge-menu te verwijderen door een volledig nieuwe Coreboot + Depthcharge firmware-image te flashen via de SuzyQ-adapter.
Groetjes,