The Boot Glitch: Registering an External UART on Linux 2.6.36 for ARMv5
How wrong interrupt pin configuration and a missing flag brought a TL16C2550 dual-UART to life on the AT91SAM9G45 — 15 years before the code went public.

The Mission: Board Bring-Up on ARMv5
In 2011, I was contracted by FITEC (Fundação para Inovações Tecnológicas) to bring up the board support for the Sweda TGM (Terminal Gráfico Multifuncional) — a new embedded terminal built on the AT91SAM9G45 (ARM926EJ-S, ARMv5TEJ, 400MHz). The full release infrastructure was led by Edgard Lima, who built the entire kernel, Buildroot, and U-Boot pipeline from scratch.
My piece was specific: register the TL16C2550 — an external dual-UART chip from Texas Instruments, mapped on the AT91SAM9G45’s SMC bus at AT91_CHIPSELECT_1 — as a platform device so the existing Linux 8250.c driver could talk to it. No new driver. Just the right platform registration, the right SMC bus timing, and the right interrupt pin configuration.
Except the interrupt pins were wrong. And that’s where the glitch was hiding.
The Hardware: Two Chips, One Bus
The TL16C2550 is a classic dual-channel 16550-compatible UART from Texas Instruments. On the TGM board, it was wired to the AT91SAM9G45’s SMC (Static Memory Controller) bus at chip select 1 (AT91_CHIPSELECT_1), operating at 3.3V with a fixed clock of 11.0592 MHz — a frequency chosen deliberately because it divides cleanly into standard baud rates.
The registration itself was straightforward. Instead of writing a new driver, I hooked into the existing 8250.c infrastructure via plat_serial8250:
static struct plat_serial8250_port tms_uart_data[] = {
[0] = {
.mapbase = AT91_CHIPSELECT_1,
.flags = SERIAL_FLAGS,
.uartclk = SERIAL_CLK, /* 11059200 Hz */
},The SMC bus timing also had to be configured precisely — setup, pulse, and cycle times for both read and write — so the AT91SAM9G45 memory controller could talk to the TL16C2550 without violating its datasheet timing requirements.
This was working code on paper. The chip was mapped. The driver was loaded. And then nothing worked.
The Glitch: Wrong Pins, False Interrupts
The TL16C2550 has two interrupt output lines — one per UART channel. On the TGM board, these were wired to PC0 and PC1 on the AT91SAM9G45. The original code configured them like this:
/* Interrupts */
at91_set_A_periph(AT91_PIN_PC0, 0);
at91_set_A_periph(AT91_PIN_PC1, 0);Wrong. at91_set_A_periph configures the pin as peripheral A function — handing control to the SoC’s internal peripheral multiplexer. But PC0 and PC1 weren’t connected to any internal peripheral. They were external interrupt lines from the TL16C2550. The kernel was trying to listen to a conversation happening on the wrong channel.
The fix was two changes:
at91_set_gpio_input(AT91_PIN_PC0, 0);
at91_set_deglitch(AT91_PIN_PC0, 1);
at91_set_gpio_input(AT91_PIN_PC1, 0);
at91_set_deglitch(AT91_PIN_PC1, 1);Configure as GPIO inputs, not peripheral A. And enable deglitch — the AT91SAM9G45 hardware filter that suppresses electrical noise on the line. Without it, startup transients on the interrupt pins looked like real interrupts. The UART appeared to fire before it had anything to say.
The second fix was in SERIAL_FLAGS:
- #define SERIAL_FLAGS (UPF_BOOT_AUTOCONF | UPF_IOREMAP)
+ #define SERIAL_FLAGS (UPF_BOOT_AUTOCONF | UPF_IOREMAP | UPF_SHARE_IRQ)The two TL16C2550 channels share one interrupt line to the SoC. Without UPF_SHARE_IRQ, the kernel refused to register the second channel — it saw the IRQ already claimed and gave up.
Three lines of code. Two wrong pin configurations and one missing flag. That was the entire glitch.
Epilogue: 15 Years Later
This code was never submitted upstream. It lived inside a private release tarball; opt-release-sweda-20110711.tgz, 1.8GB, dated August 23rd 2011; sitting on a backup disk in Recife.
In the years that followed, the AT91SAM9G45 moved from Linux 2.6.36 to Device Tree. The plat_serial8250 registration style we used was eventually replaced by DT bindings. The TL16C2550 itself faded from new designs as integrated SoCs gained more UART channels.
But the lesson didn’t fade. Wrong pin configuration and a missing IRQ flag are the kind of bugs that don’t announce themselves clearly. The UART loads. The driver initializes. And then nothing works until you read the schematic one more time, compare it against the code, and find three lines that were quietly lying about what the hardware was doing.
I’m publishing this now, 15 years later, as part of recovering and documenting work that was real but invisible. The full board support code was written by Edgard Lima, who led the entire TGM release infrastructure. This was my small piece of it.


