Reverse Engineering van de ongedocumenteerde Mikey-chip van de iPod Classic
De reden hiervoor staat direct in de broncode van Rockbox:
/* TODO:
* - detect jack accessory
* - support for remote buttons
*/
En eerlijk gezegd is dat begrijpelijk. Rockbox op deze iPod bestaat omdat vrijwilligers sinds januari 2011 Apple-hardware hebben gereverse-engineered zonder enige documentatie, en dat volledig gratis. Muziek, het klikwiel, opnamefuncties; alles is op de harde manier uitgezocht. De afstandsbediening van de oortjes is simpelweg nooit bovenaan iemands prioriteitenlijst gekomen: de commit uit 2014 die de basis voor knop-interrupts toevoegde, noemde remote-events "work in progress", en de TODO belandde in 2017 in de audio-driver. Niemand heeft het sindsdien opgepakt. Tot nu.
Samenvatting (TL;DR)
De chip die de afstandsbediening van de iPod-oortjes decodeert ("Mikey", I2C-adres 0x72) heeft geen openbare documentatie. Helemaal geen. Nergens.
De enige eerdere bron was een zestien jaar oud reverse engineering-verslag op een blog die niet meer bestaat. Een wiki-mirror redde de dag.
Ik heb de modusregister van de chip op het apparaat zelf door alle 256 waarden gescand, de waarde gevonden die knoprapportage inschakelt (0x2f) en de gebeurtenissen gedecodeerd.
De hardware liegt: de event-engine van de chip valt na ongeveer 5 seconden in slaap en rapporteert het loslaten van knoppen 1,8 seconden te laat.
Het resultaat is een driver die momenteel in review is voor de hoofdbron (upstream) als Gerrit-wijziging 7677. Alle drie de knoppen werken nu.
Hoe los ik dit op?
Als je niet geïnteresseerd bent in het verhaal over reverse engineering en gewoon werkende knoppen op je oortjes wilt: ik heb een vooraf gebouwd Rockbox-image gepubliceerd met de remote-driver als ipod6g-mikey-v1. Dit werkt op de iPod Classic 6G en 7e generatie, en gaat ervan uit dat je iPod al op Rockbox draait (bootloader geïnstalleerd via de officiële Rockbox Utility).
- Download
rockbox.zipuit de release. - Verbind de iPod in diskmodus en pak het bestand uit op de root van de schijf (dit vervangt de
.rockboxfirmware-bestanden; je instellingen, thema's, database en muziek blijven ongewijzigd). - Werp de schijf uit en herstart het apparaat.
De afspeel/pauze-knop werkt nu op elk scherm, volume werkt overal inclusief menu's (inhouden om volume te wijzigen), en de afstandsbediening blijft werken als de 'hold'-schakelaar is ingeschakeld. Dit raakt de bootloader niet, dus in het ergste geval verwijder je .rockbox in diskmodus en installeer je een officiële build terug. Zodra de wijziging upstream is samengevoegd, kun je terugkeren naar de officiële dagelijkse builds.
Bonus: custom boot-scherm. Terwijl ik hiermee bezig was, heb ik ook een Rockbox Boot Logo Patcher gebouwd waarmee je het opstartlogo van Rockbox kunt vervangen door elke gewenste afbeelding, direct in je browser. Dit werkt op elke iPod die door Rockbox wordt ondersteund.
Een chip genaamd Mikey, nergens gedocumenteerd
Even een overzicht van de spelers. De iPod Classic (de mijne is de 7e generatie; Rockbox noemt de hele familie "ipod6g" naar het origineel uit 2007) heeft een specifieke kleine controller voor de koptelefoonaansluiting die Apple's firmware "Mikey" noemt. Deze bevindt zich op I2C-bus 0 op adres 0x72, heeft 8 registers en voert twee taken uit: het voeden van de headset-microfoon en het decoderen van de knoppen van de inline-afstandsbediening. Rockbox wist altijd al dat deze chip bestond, maar gebruikte hem alleen voor één ding: het verhogen van de mic-bias spanning zodat opnames via de jack werken.
Voordat ik aan de hardware ging sleutelen, zocht ik of iemand dit eerder had gedaan. De zoektocht was grondig, maar leverde niets op:
- freemyipod: de community die deze iPod genoeg heeft gereverse-engineered om custom code erop te draaien, heeft geen Mikey-driver en geen register-documentatie.
- Rockbox’s Gerrit: ik kon geen verlaten pogingen vinden.
- GitHub code search: een zoekopdracht naar "mikey" levert de Atari Lynx sound chip en een Blue microfoon-accessoire op.
- openiBoot: het oude iPhone Linux-project heeft nooit headset-accessoires behandeld.
- macOS: verzendt een
AppleMikeyDriver.kext, wat bewijst dat de naam echt is, maar niemand heeft de interne werking gedocumenteerd, en op Macs leeft de equivalente logica in de audio-codec.
Een ongedocumenteerde chip, een onvoltooide driver en 15 jaar waarin niemand het heeft opgepakt. Uit een eerdere sessie had ik al een kleine overwinning: door de mic-bias te verhogen (schrijf 7 naar register 0, dezelfde waarde die het opnamepad gebruikt), verschijnt de middelste knop als een waarde ongelijk aan nul in register 4. Afspelen/pauzeren werkte dus. Volume was echter een muur. Mijn werktheorie was dat volume gebruikmaakte van een ongedocumenteerd serieel protocol, met een interrupt op GPIO E6 die nieuwe data aankondigde.
Die theorie was in elk detail onjuist, en de E6-interrupt is tijdens het hele project geen één keer afgegaan. Geen enkele keer.
Het 'DenverCoder9'-moment
Als je ooit obscure problemen hebt gedebugged, ken je xkcd 979. Tien jaar zoeken, één forumthread met exact jouw probleem, en de enige reactie is de auteur die zegt: "laat maar, heb het opgelost."
Het zoeken naar het bedrade protocol van de afstandsbediening leidde tot precies één bruikbare thread: een Hackaday-post uit februari 2010 en een bijbehorende zestien jaar oude Reddit-thread over David Carne, die de afstandsbediening van de iPod shuffle 3G reverse-engineerde. Dezelfde hardware als mijn oortjes.
Zijn site? Dood. david.carne.ca/shuffle_hax laadt niet meer. Logisch, het is zestien jaar geleden.
Maar in tegenstelling tot DenverCoder9 bleek de wijsheid van de ouderen herstelbaar. Een wiki-mirror van tinymicros.com had het volledige verslag bewaard, en een GitHub-repo (reverse-shuffle) bevat nog steeds een Arduino-reimplementatie van de accessory-handshake. De ouderen hadden toch aantekeningen achtergelaten.
Wat Carne in 2010 ontdekte
De gegevens van Carne vernietigden mijn theorie over het seriële protocol onmiddellijk. De afstandsbediening is veel dommer dan ik had aangenomen:
| Signaal | Wat het eigenlijk is |
|---|---|
| Middelste knop | Een directe kortsluiting van de mic-lijn. Dat is alles. |
| Volume omhoog | Een statische resistieve belasting die de mic-lijn laat zakken van ~2.08V naar ~1.68V |
| Volume omlaag | Een grotere belasting, die het laat zakken naar ~1.52V |
| Het "slimme" deel | Een eenmalige ultrasone ID-chirp 8,1ms na het inschakelen (1,5ms op 280kHz, daarna 4,6ms op 244kHz), beantwoord door een 4,8ms ACK-puls van de speler |
De chirp is voor identificatie, niet voor DRM. Carne bewees dit door de volumeknoppen na te bootsen met een simpel weerstandscircuit; de originele afstandsbediening hoefde alleen bij het inschakelen verbonden te zijn om zijn ID te "chirpen".
Mikey is dus een level-detector met een chirp-demodulator. De knoppen waren al die tijd analoog, en de taak werd plotseling veel kleiner: vind welk register-modus de chip deze niveaus rapporteert.
De chip register voor register scannen
Je kunt niet zomaar een debugger aansluiten op de jack-controller van een iPod, dus ik bouwde het volgende beste ding: een reverse engineering-scherm binnen Rockbox zelf. Dit bevindt zich in het debugmenu van het apparaat, pollt alle 8 Mikey-registers elke 20ms, toont deze live op het scherm en voegt een tijdstempelregel toe aan /.rockbox/mikey.log telkens wanneer er iets verandert. Het scrollwiel bedient register 0. Dit scherm blijft privé in mijn build, maar de kern is als volgt:
/* poll all 8 registers every ~20ms, log any change */
for (int i = 0; i < MIKEY_NREGS; i++)
cur[i] = mikey_read(i);
int e6 = (PDAT(14) >> 6) & 1; /* Mikey IRQ line (never fired!) */
if (changed && fd >= 0)
{
fdprintf(fd, "[%08lu] r0=%02x E6=%d ", USEC_TIMER, reg0, e6);
for (int i = 0; i < MIKEY_NREGS; i++)
fdprintf(fd, "%02x ", cur[i]);
fdprintf(fd, "\n");
}
Twee functies waren essentieel. Een auto-sweep-modus loopt register 0 door alle 256 waarden, ongeveer 200ms per waarde, terwijl je een knop op de oortjes ingedrukt houdt. En een MARK-toets stempelt het logboek wanneer ik op 'Menu' druk, zodat fysieke knopdrukken achteraf kunnen worden vergeleken met registerwijzigingen. Ik drukte op de knoppen, plugde de jack in en uit, flashte builds en draaide de sweeps, en liet Claude de logs diffen en het volgende experiment voorstellen. Elke conclusie werd eerst op het apparaat geverifieerd.
Bekentenis: de eerste batch sweep-data was waardeloos, en dat was de schuld van mijn driver. De bestaande polling-thread voor afspelen/pauzeren schreef stilletjes register 0 elke 20ms terug naar 7, waardoor hij in de achtergrond vocht met de sweep van het debugscherm. Alle resultaten van vóórdat ik een vlag toevoegde om de polling-thread stop te zetten, waren vervuild en zijn weggegooid.
Met schone data en drie volledige sweeps (niets aanraken, volume omhoog vasthouden, volume omlaag vasthouden) en een log-diff vond ik een actief venster: register 0 waarden 0x24-0x27, 0x2c-0x2f, 0x34-0x37 en 0x3c-0x3f, gespiegeld op +0x80 omdat de chip bits 7 en 3 niet opslaat. Binnen dat venster komen registers 5 en 6 tot leven. Overal anders gebeurt niets. Een gerichte pass over die 16 waarden leidde tot het antwoord: bij register 0 = 0x2f produceren de volumeknoppen schone, reproduceerbare gebeurtenissen.
De volledige decode:
| Signaal | Betekenis |
|---|---|
reg0 = 0x2f | Remote-reporting modus. De laagste 3 bits zijn mic-bias (dezelfde 7 als bij opname), 0x28 activeert de detector |
reg5 = 0x04 / 0x08 | Volume omhoog indrukken / loslaten (edge events, leesbaar voor ~100ms) |
reg5 = 0x01 / 0x02 | Volume omlaag indrukken / loslaten |
reg5 = 0x30 | Accessory-ID event bij modus-entry (negeren) |
reg4 & 0x05 | Middelste knop, een niveau op een basis van 0x40 |
| Uitpluggen | Chip reset volledig, alle registers lezen nul, modus moet opnieuw worden geactiveerd bij invoegen |
Een test met een hold van 5 seconden bevestigde dat de volume-events echte edges zijn: precies één press-event en één release-event, 6,3 seconden uit elkaar, met niets daartussen.
Eén protocoltest zorgde voor mijn favoriete domme moment van het project. Mijn eigen testnotities zeiden "klik 3 keer op de middelste knop", en midden in de sessie klikte ik drie keer op het midden van het klikwiel van de iPod in plaats van de knop van de oortjes. Het logboek registreerde trouw drie resets van het debugscherm en nul remote-events. Blijkt dat "middelste knop" een gevaarlijke term is in je notities als er twee apparaten op je bureau staan met middelste knoppen.
Upstream versturen
Driver versie 2: activeer modus 0x2f, poll elke 20ms, latch de volumestatus tussen press- en release-edges (Rockbox's normale knop-repeat mechanisme geeft dan gratis hold-to-ramp volume), lees de middelste knop via het niveau in register 4. Alle drie de knoppen werkten op het apparaat.
Nu, het versturen naar de hoofdbron. Rockbox accepteert geen GitHub pull requests; de GitHub-repo is een mirror. De echte review gebeurt op hun eigen self-hosted Gerrit. De setup is old-school maar pijnloos: OpenID-login, SSH-sleutel, een commit-msg hook die een Change-Id in je commit stempelt, en dan pushen naar refs/for/master.
Daarna volgde de opschoning voor indiening: verschillende build-configuraties, edge-cases bij het verwijderen van de jack, en wat er gebeurt als de opnamefunctie de chip nodig heeft. Deze pass bracht drie bugs aan het licht:
- Dev builds compileerden niet. Zowel
remotecontrolrx()als deBUTTONRC*codes bestaan alleen als het iAP accessory protocol is meegecompileerd. De oplossing was een properHAVEMIKEYREMOTEcapability macro. mikey_read()retourneerde ongeïnitialiseerde stack-garbage telkens wanneer de chip een I2C-transfer NAKte, wat gebeurt elke keer dat de jack wordt bewogen. Eén glitch kon een spook-volumeknop vastzetten.- De polling-thread vocht stilletjes met het opnamepad om het modusregister. Dit is opgelost met een expliciete overdracht: de remote pauzeert terwijl de jack-microfoon opneemt.
Een woord over de developer-experience: Rockbox levert een script (rockboxdev.sh) dat de pinned cross-compiler (arm-elf-eabi-gcc 9.5.0) bouwt, en vanaf daar is het simpelweg make. Het testen van een wijziging betekende het droppen van de verse rockbox.ipod in /.rockbox/ via diskmodus en herstarten. Voor een vrijwilligersproject dat al sinds 2001 bestaat en tientallen verschillende spelers ondersteunt, is de tooling in betere staat dan bij sommige producten waarvoor ik ben betaald.
Ingediend als Gerrit change 7677, "ipod6g: Add inline earphone remote support". Eén schone commit, 8 bestanden, 318 toevoegingen.
De hardware liegt
Toen ik het apparaat een dag in het echt gebruikte, begon het interessantste deel van het project: enkele klikken op de middelste knop zorgden soms ervoor dat afspelen/pauzeren twee keer werd getoggled.
Mijn eerste oplossing was een schoolvoorbeeld van overhaast handelen. Ik nam aan dat register 4 een live niveau was met incidentele uitval, dus ik paste debouncing toe op de release-kant. Dit maakte alles erger. Kliks werden gelezen als lange drukken, en in de keymap van Rockbox betekent een lange druk op afspelen "stop", wat je terugstuurt naar het hoofdmenu. Erger nog: het "werkende" gedrag dat ik eerder ervoer, bleek een toevalstreffer te zijn. De flikkering die mijn debounce verwijderde, maskeerde per ongeluk een veel groter probleem.
Dus, dit keer goed: een timing-sessie met instrumentatie, MARK-stempels op het exacte fysieke indrukken en loslaten, en analyse van de logs achteraf. De waarheid is vreemder dan een stuiterend contact. De event-engine van de chip doet een dutje. Na ongeveer 5 seconden zonder knopactiviteit wordt een druk nog steeds direct in register 4 gerapporteerd, maar het loslaten (de daling in register 4 en de events in register 6) wordt 1,7 tot 1,8 seconden te laat gerapporteerd, gebundeld zodra de engine weer wakker wordt. Recente activiteit houdt hem wakker, dus snel klikken werkt perfect, maar de eerste klik na een pauze wordt gelezen als een hold van 1,8 seconde.
Je kunt hardware die het verleden rapporteert niet simpelweg "debouncen". Daarom is de middelste knop nu click-only: elke gedebounced stijging zendt een vaste klik-puls van 60ms uit, en er is geen hold-semantiek meer, omdat de chip de duur van de druk niet betrouwbaar kan rapporteren.
/* Center-button handling: click-only. reg4's rising edge is always
* immediate, but after ~5s of inactivity the chip's event engine naps
* and reports the release ~1.8s late, so press duration is unreliable.
* Every press is a fixed-length click at the rise; no hold/long-press. */
#define MIKEY_CENTER_PULSE_POLLS 3 /* reported click length, 60ms */
#define MIKEY_CENTER_OFF_POLLS 3 /* release debounce, 60ms */
Nu de knop eindelijk betrouwbaar werkte, kwam er nog een software-verrassing naar boven. De standaard keymap van Rockbox was geschreven voor de oude dock-afstandsbedieningen, en die koppelt de afspeelknop aan 'Select' in menu's. Mijn oortjes-klikker selecteerde dus menu-items. De oplossing was eleganter dan het patchen van keymaps: Rockbox heeft al jaren een BUTTONMULTIMEDIAPLAYPAUSE code, bedoeld voor multimedia-toetsen op toetsenborden en headsets. De middelste knop zendt nu dat signaal, waardoor afspelen/pauzeren overal werkt. Volume in menu's en lijsten werd toegevoegd via HAVEVOLUMEIN_LIST.
Het bootlogo apart
Terwijl ik mijn build-pipeline had klaarliggen, heeft mijn iPod ook een gepersonaliseerd bootscherm gekregen: een neon "Hemant's iPod" woordmerk, geschaald naar de 320x98 BMP die Rockbox in het binary compileert. Het standaard bootscherm is wit met het logo bovenaan; een kleine patch maakt de achtergrond zwart en centreert het logo:
#ifdef IPOD_6G
/* the custom logo has a black background */
lcd_set_background(LCD_BLACK);
lcd_set_foreground(LCD_RGBPACK(0x60, 0x60, 0x60));
#endif
/* ... */
lcd_bmp(&bm_rockboxlogo, (LCD_WIDTH - BMPWIDTH_rockboxlogo) / 2,
(LCD_HEIGHT - BMPHEIGHT_rockboxlogo) / 2);
Die wijziging blijft persoonlijk, alleen op mijn apparaat. Sommige patches zijn voor de hoofdbron, en sommige zijn alleen voor jezelf.
De huidige status
Change 7677 is op het moment van schrijven in review, dus het einde van dit bericht is "in afwachting", niet "samengevoegd".
Een kanttekening: dit was allemaal nieuw voor mij. Ik had nooit eerder in de Rockbox-codebase gewerkt en alles wat ik weet over hoe een iPod van binnen werkt, heb ik tijdens dit project geleerd. Dus als iets hier oud nieuws is voor de embedded community, of als ik trots een wiel heb uitgevonden dat sinds 2005 een eigen naam heeft, excuseer me.
Nog op de "ooit"-lijst: multi-click gebaren (dubbelklikken voor het volgende nummer is de meest voor de hand liggende) en het gebruik van het accessory-ID event om de mic-lijn alleen te voeden wanneer er daadwerkelijk een afstandsbediening is aangesloten, wat een beetje batterij zou besparen bij gewone koptelefoons.
Dit is dezelfde drang die me aanzet tot het tot leven wekken van een 10 jaar oude Kindle of het ombouwen van een iPhone 8 tot een zonnedrijvende OCR-server: hardware van deze kwaliteit zou niet beperkt mogen worden door verlaten software. Een iPod uit 2009 met werkende oortjes-bediening in 2026 voelt juist goed.
De TODO is trouwens weg uit audio-6g.c. Drie jaar Rockbox op deze iPod, en de oortjes-klikker klikt eindelijk. Iemand moest gewoon de registers scannen.
Veel klikplezier! 🎧
Groetjes,