<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hayes Roach</title>
    <description>The latest articles on DEV Community by Hayes Roach (@hayesroach).</description>
    <link>https://dev.to/hayesroach</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015643%2Fbbda6f84-3b71-4131-a39b-817d69e58074.jpg</url>
      <title>DEV Community: Hayes Roach</title>
      <link>https://dev.to/hayesroach</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hayesroach"/>
    <language>en</language>
    <item>
      <title>Hacking a Dead Digital Frame Into a Linux Smart Display</title>
      <dc:creator>Hayes Roach</dc:creator>
      <pubDate>Sun, 05 Jul 2026 03:22:04 +0000</pubDate>
      <link>https://dev.to/hayesroach/hacking-a-dead-digital-frame-into-a-linux-smart-display-1d52</link>
      <guid>https://dev.to/hayesroach/hacking-a-dead-digital-frame-into-a-linux-smart-display-1d52</guid>
      <description>&lt;p&gt;&lt;em&gt;Our Nixplay digital frame bricked itself and was headed for the trash. I opened it up instead. Now it runs Linux with a custom clock, weather, calendar, daily bible verse, and photo dashboard.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My family's old Nixplay photo frame died. Power it on and you'd get a backlight, sometimes a red bar, and nothing else. Resets did nothing, power cycles did nothing. Something in the firmware had clearly died.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu2az0x777v0agzmho4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu2az0x777v0agzmho4k.png" alt="Corrupted Nixplay screen showing all black" width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted to see if I could fix it, so I opened it up. Inside was a surprisingly capable little computer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allwinner A20 (dual-core ARM)&lt;/li&gt;
&lt;li&gt;512 MB RAM&lt;/li&gt;
&lt;li&gt;18.5" AUO M185XTN01.2 LVDS panel (1366x768)&lt;/li&gt;
&lt;li&gt;NAND flash storage&lt;/li&gt;
&lt;li&gt;SD card slot&lt;/li&gt;
&lt;li&gt;USB port&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wf3w9j85ggoyo1b10gy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wf3w9j85ggoyo1b10gy.png" alt="Nixplay ARM chip" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The A20 is old but has great Linux support. This device was never a supported board anywhere, and nothing about its display wiring is documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a serial console
&lt;/h2&gt;

&lt;p&gt;Before anything else I needed a way to see what the board was doing. Boards like this usually have a debug serial port (UART), but nothing here was labeled. Just a few bare flat pads near the main chip.&lt;/p&gt;

&lt;p&gt;Finding it is a classic trick: probe the pads with a multimeter while the board boots. The transmit pad idles at a steady 3.3V and visibly dips while the board prints boot messages. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosxarqzztd3yb21avka8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosxarqzztd3yb21avka8.png" alt="Board with uart locations for Tx, Rx, and GND" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I found TX, RX, and ground, I soldered wires directly to the pads (no through-holes on this board), ran Dupont jumpers to a USB-to-TTL adapter (real FTDI chip, switchable to 3.3V, which is what the A20 needs), and had an instant boot console at 115200 baud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tlasqx161p3qin1g2bw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tlasqx161p3qin1g2bw.png" alt="Connection to from uart pints to laptop" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Booting Linux without breaking anything
&lt;/h2&gt;

&lt;p&gt;The A20 can boot from an SD card instead of the onboard NAND flash, which meant I could experiment without touching the original firmware.&lt;/p&gt;

&lt;p&gt;The closest supported board in Armbian (a Linux distro for ARM boards) is the &lt;a href="https://armbian.com/boards/lime2" rel="noopener noreferrer"&gt;Olimex LIME2&lt;/a&gt;, which uses a similar chip. I flashed it to an SD card.&lt;/p&gt;

&lt;p&gt;It didn't just boot on its own the first time, though. The bootloader stored on NAND grabbed control before the SD card could boot, so I had to spam the spacebar over the serial console to interrupt its autoboot countdown, then kick off the SD card boot manually from the bootloader prompt. Once Armbian was fully installed with its own bootloader on the SD card, the board started booting straight into Linux automatically and everything was working.&lt;/p&gt;

&lt;p&gt;Except the screen, which showed solid white.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i9n5vs8tehedmr5cpjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i9n5vs8tehedmr5cpjd.png" alt="Solid white display" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The white screen problem
&lt;/h2&gt;

&lt;p&gt;Here's the thing about this type of LCD: it's "normally white." Backlight on plus no valid video signal equals a pure white screen. So white means the panel isn't receiving anything it understands.&lt;/p&gt;

&lt;p&gt;The Armbian kernel I started with didn't have a working LVDS pipeline for this panel, so I had to build a custom kernel with additional A20 LVDS patches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forked Armbian's build system (&lt;a href="https://github.com/hayes-roach/build" rel="noopener noreferrer"&gt;github.com/hayes-roach/build&lt;/a&gt;) and added community kernel patches that add A20 LVDS support&lt;/li&gt;
&lt;li&gt;Built custom kernels in GitHub Actions&lt;/li&gt;
&lt;li&gt;Hand-edited the device tree (the config file that tells Linux what hardware exists) to describe the panel: resolution, timing, pins&lt;/li&gt;
&lt;li&gt;Found the backlight control pin by writing a loop that toggled every GPIO until the backlight blinked&lt;/li&gt;
&lt;li&gt;Even rebuilt the bootloader with display support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After all that, Linux reported a fully working display pipeline. Correct registers, panel driver loaded, backlight on.&lt;/p&gt;

&lt;p&gt;Still white.&lt;/p&gt;

&lt;p&gt;I then tried everything. Every data format, every timing, both of the chip's LVDS outputs, live register pokes over the serial console. The panel never reacted to anything. Not even garbage on screen. It behaved exactly like an unplugged panel.&lt;/p&gt;

&lt;p&gt;Except one fact didn't fit: the corrupted firmware's red bar was a real rendered image. The display path &lt;em&gt;worked&lt;/em&gt;. My software just couldn't reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broken firmware saves the day
&lt;/h2&gt;

&lt;p&gt;Then it hit me: this board has no eMMC. The firmware lives on a NAND flash chip that my Linux install never touched. The original firmware was still sitting there, corrupted OS and all.&lt;/p&gt;

&lt;p&gt;So I pulled the SD card and let the board boot from NAND, watching over serial. The logs basically explained the whole mystery. The OS partitions threw &lt;code&gt;read flash error&lt;/code&gt; (there's the reason the frame died), but the early boot stuff loaded fine, and the stock bootloader brought up the display. &lt;/p&gt;

&lt;p&gt;The panel went black with the backlight on.&lt;br&gt;
Black is good. With the backlight on, a black screen means the panel electronics are alive and responding to a video signal. So the hardware worked, and now I had something I never had before: a working display setup I could poke at over serial and compare against.&lt;/p&gt;

&lt;p&gt;Allwinner devices store board configuration in a binary file called &lt;code&gt;script.bin&lt;/code&gt;. The bootloader loads it into RAM during startup. From the bootloader prompt I dumped memory until I found the display section and could see exactly how the original firmware configured the panel.&lt;/p&gt;

&lt;p&gt;And there it was, the actual config this panel wants: 24-bit LVDS (I'd been sending 18-bit this entire time), a 75 MHz pixel clock, and the exact timing values. Not the numbers from the datasheet. Not the numbers from community configs. The real ones.&lt;/p&gt;

&lt;p&gt;I copied everything over to my Linux setup and rebooted.&lt;br&gt;
Still white.&lt;/p&gt;
&lt;h2&gt;
  
  
  One GPIO pin
&lt;/h2&gt;

&lt;p&gt;At this point I had a working display sitting live at the old bootloader prompt, so I figured: let's just start flipping GPIO pins and see what the panel actually depends on. I toggled each Port H bit individually from the bootloader prompt and watched for any visible change on the panel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bit 8 low  -&amp;gt; white screen
bit 8 high -&amp;gt; panel works
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin PH8 turned out to control panel power.&lt;/strong&gt; The original firmware turns it on through its config file. Nothing in my stack, not the kernel, not the bootloader, had ever touched it.&lt;br&gt;
The panel's electronics had been switched off the whole time. Days of debugging display timings while the panel electronics were effectively disabled.&lt;/p&gt;

&lt;p&gt;Back on the Linux side, one command to set PH8 high and the white screen collapsed to black. I wrote random data to the framebuffer and the screen filled with static. Alive. After days of chasing LVDS timings, the fix turned out to be a single GPIO bit.&lt;/p&gt;

&lt;p&gt;One last gotcha: I tried letting the Linux panel driver manage PH8, and everything broke again. Every display mode change toggled the pin off and back on faster than the panel's required 500 ms power-up time. The fix was a Device tree (a hardware description Linux uses to know what peripherals exist and how they're wired) "gpio-hog," which pins PH8 permanently on from the moment the board boots, plus disabling display power management so the video pipeline never cycles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F82t7x791tmm045fujm8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F82t7x791tmm045fujm8f.png" alt="Armbian" width="799" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The final recipe
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Found via&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Panel enable&lt;/td&gt;
&lt;td&gt;GPIO PH8, always on&lt;/td&gt;
&lt;td&gt;GPIO fuzzing at the stock bootloader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backlight&lt;/td&gt;
&lt;td&gt;GPIO PH7 + PWM&lt;/td&gt;
&lt;td&gt;GPIO sweep in Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LVDS format&lt;/td&gt;
&lt;td&gt;24-bit (vesa-24), 75 MHz&lt;/td&gt;
&lt;td&gt;script.bin dumped from NAND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;Armbian + A20 LVDS patches&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/hayes-roach/build" rel="noopener noreferrer"&gt;my fork&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Building the dashboard
&lt;/h2&gt;

&lt;p&gt;With the display working, I turned it into a wall dashboard: clock, Google Calendar, weather, rotating photos, a daily Bible verse, running 24/7.&lt;/p&gt;

&lt;p&gt;On 512 MB of RAM, heavyweight options like MagicMirror don't fit comfortably. So the whole dashboard is one HTML file with vanilla JavaScript, served by Python's built-in web server and displayed by Firefox in kiosk mode. The board boots straight into it: auto-login, X starts, Firefox launches fullscreen.&lt;/p&gt;

&lt;p&gt;The interesting bits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Firefox, not Chromium.&lt;/strong&gt; The Chromium packages available for this platform rendered every page as a blank white window, while Firefox ESR worked reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar without OAuth.&lt;/strong&gt; Google Calendar has a "secret iCal address" that works for private calendars. A cron job downloads it every 15 minutes and the page parses it in JavaScript into a week view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weather&lt;/strong&gt; from Open-Meteo, which is free and needs no API key. Current conditions plus the next 10 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photos&lt;/strong&gt; come from my NAS, mounted as a network share on the board. The page reads the web server's own directory listing to discover new files automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A watchdog&lt;/strong&gt; restarts Firefox if it crashes, and the backlight dims on a schedule at night.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total memory footprint: around 200-300 MB. Comfortable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ea4u0n73sakj8lfahdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ea4u0n73sakj8lfahdx.png" alt="Memory" width="799" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A white screen on a "normally white" panel means the panel isn't in the game at all.&lt;/strong&gt; Stop tuning video settings and go hunt for power and enable pins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never erase the original firmware, even a broken one.&lt;/strong&gt; The corrupted NAND couldn't boot its own OS, but its intact hardware config and working display init cracked the whole case. The firmware that killed the frame saved the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor-specific enable GPIOs are often more important than display timings.&lt;/strong&gt; Datasheets and device trees will never tell you about them. Brute-forcing a GPIO bank at a bootloader prompt takes five minutes and would have saved me days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get serial access first.&lt;/strong&gt; Multimeter, bare pads, soldering iron. Every breakthrough in this project happened at that console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnaq7p64damiadr65xedz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnaq7p64damiadr65xedz.png" alt="Working smart display running linux that shows time, weather, calendar, and photos" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not bad for a device that was about to be thrown away.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Hardware: Nixplay W18A (Allwinner A20, 512 MB), AUO M185XTN01.2 panel. Software: Armbian, Firefox ESR kiosk, single-file HTML dashboard. Patches and build workflow: &lt;a href="https://github.com/hayes-roach/build" rel="noopener noreferrer"&gt;https://github.com/hayes-roach/build&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/hayes-roach" rel="noopener noreferrer"&gt;https://github.com/hayes-roach&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>smartdisplay</category>
      <category>armbian</category>
      <category>software</category>
    </item>
  </channel>
</rss>
