LPC2148 UART0 Serial Communication: Baud Rate Setup and printf in Keil
A serial port is the most useful debugging tool on a small microcontroller. Once your LPC2148 can print text to a PC terminal, you can display sensor readings, trace program flow and accept commands from the keyboard. The LPC2148 has two UARTs: UART0 on pins P0.0 and P0.1, and UART1 on P0.8 and P0.9 with extra modem control lines. UART0 is also the port used by the ISP bootloader, so on most boards it is already wired to a DB9 connector through a MAX232 or to a USB-UART chip.
This tutorial configures UART0 at 9600 baud, 8 data bits, no parity and one stop bit (8N1), explains how the baud rate divisor is calculated and how large the error is, and builds a small echo program with printf support in Keil.
UART0 Registers You Need
| Register | Purpose |
|---|---|
| PINSEL0 | Bits 1:0 = 01 selects TXD0 on P0.0; bits 3:2 = 01 selects RXD0 on P0.1 |
| U0LCR | Line control: word length, stop bits, parity and the DLAB bit (bit 7) that exposes the divisor latch |
| U0DLL, U0DLM | Divisor latch low and high bytes, accessible only while DLAB = 1 |
| U0THR | Transmit holding register; write a byte here to send it (DLAB = 0) |
| U0RBR | Receive buffer register; read the received byte here (DLAB = 0) |
| U0LSR | Line status: bit 0 RDR means a byte has arrived, bit 5 THRE means the transmitter can accept a byte |
| U0FDR | Fractional divider for finer baud rate adjustment; its reset value leaves it disabled |
| PCONP | Bit 3 powers UART0; it is on after reset but setting it explicitly is good practice |
U0THR, U0RBR and U0DLL share the same address. The DLAB bit in U0LCR decides which one you reach, which is why the initialisation sets DLAB, writes the divisor and then clears DLAB again.
Calculating the Baud Rate Divisor
The UART clock is the peripheral clock PCLK. With a 12 MHz crystal and Keil's default Startup.s, the PLL runs the CPU at 60 MHz, and the VPBDIV register keeps its reset value, which divides by four. So PCLK is 15 MHz. The UART divides PCLK by 16 times the 16-bit divisor:
Baud = PCLK / (16 x Divisor), Divisor = 256 x U0DLM + U0DLL Divisor = PCLK / (16 x Baud) PCLK = 15 MHz, Baud = 9600: Divisor = 15000000 / (16 x 9600) = 97.66 -> use 98 Actual = 15000000 / (16 x 98) = 9566 baud Error = (9566 - 9600) / 9600 = -0.35 %
| Target baud | Divisor (PCLK 15 MHz) | Actual baud | Error |
|---|---|---|---|
| 9600 | 98 | 9566 | -0.35 % |
| 19200 | 49 | 19133 | -0.35 % |
| 38400 | 24 | 39063 | +1.73 % |
| 115200 | 8 | 117188 | +1.73 % |
An error below about 2 percent usually works, but errors from both ends add up, so keep it as small as possible. For 115200 baud, either raise PCLK to 60 MHz by writing VPBDIV = 0x01, or use the fractional divider U0FDR. If you change VPBDIV, recalculate the divisor for every peripheral that depends on PCLK.
Connecting to the PC
- Boards with a DB9 connector and MAX232: use a USB-to-RS232 cable. The MAX232 converts the 3.3 V logic levels to RS-232 levels.
- Boards with a USB-UART chip (CP2102, CH340 or FT232): just connect the USB cable and note the new COM port.
- Bare chip with a 3.3 V USB-TTL adapter: connect adapter RX to P0.0 (TXD0), adapter TX to P0.1 (RXD0) and GND to GND. Never connect RS-232 levels directly to the pins.
- Open a terminal such as PuTTY or Tera Term at 9600 baud, 8 data bits, no parity, 1 stop bit, no flow control.
UART0 Driver and Echo Program
The program prints a banner, then echoes every character typed in the terminal and counts the characters received. The fputc function connects printf to UART0. In Keil, open Options for Target and tick Use MicroLIB so that printf uses this function without needing semihosting.
#include <lpc214x.h>
#include <stdio.h>
#define PCLK_HZ 15000000u /* 60 MHz CCLK / 4 (VPBDIV reset value) */
#define BAUD 9600u
static void uart0_init(void)
{
unsigned int div = (PCLK_HZ + 8u * BAUD) / (16u * BAUD); /* rounded */
PCONP |= (1u << 3); /* power UART0 */
PINSEL0 = (PINSEL0 & ~0x0Fu) | 0x05u; /* P0.0 = TXD0, P0.1 = RXD0 */
U0LCR = 0x83; /* 8 bits, no parity, 1 stop, DLAB = 1 */
U0DLL = div & 0xFF;
U0DLM = (div >> 8) & 0xFF;
U0LCR = 0x03; /* DLAB = 0, keep 8N1 */
U0FCR = 0x07; /* enable and reset FIFOs */
}
static void uart0_putc(char c)
{
while (!(U0LSR & (1u << 5))) /* wait for THRE */
;
U0THR = c;
}
static char uart0_getc(void)
{
while (!(U0LSR & (1u << 0))) /* wait for RDR */
;
return U0RBR;
}
static void uart0_puts(const char *s)
{
while (*s) {
if (*s == '\n')
uart0_putc('\r'); /* terminals expect CR LF */
uart0_putc(*s++);
}
}
/* Retarget printf to UART0 (requires Use MicroLIB in Keil) */
int fputc(int ch, FILE *f)
{
(void)f;
if (ch == '\n')
uart0_putc('\r');
uart0_putc((char)ch);
return ch;
}
int main(void)
{
unsigned int count = 0;
char c;
uart0_init();
uart0_puts("LPC2148 UART0 ready\n");
printf("Divisor used: %u\n", (PCLK_HZ + 8u * BAUD) / (16u * BAUD));
while (1) {
c = uart0_getc();
count++;
if (c == '\r') {
printf("\nReceived %u characters so far\n", count);
} else {
uart0_putc(c); /* echo */
}
}
}
How the Code Works
The divisor is calculated at run time with rounding: adding half of the denominator before dividing gives 98 instead of the truncated 97, which reduces the error. Writing 0x83 to U0LCR selects 8-bit words (bits 1:0 = 11), one stop bit, no parity and sets DLAB. After the divisor is loaded, 0x03 keeps the same format with DLAB cleared.
Transmission polls THRE (bit 5 of U0LSR), which is 1 when the transmit holding register is empty. Reception polls RDR (bit 0), which is 1 when at least one byte is waiting in the receive FIFO. Reading U0RBR removes the byte and clears RDR when the FIFO is empty. Writing 0x07 to U0FCR enables the 16-byte FIFOs and clears any stale data left from the bootloader.
Polling is simple but blocks the CPU while waiting. For a busy application, enable the receive data available interrupt in U0IER and handle bytes in an interrupt service routine registered with the VIC, storing them in a ring buffer.
Common Problems
- Garbage characters on the terminal: the baud rate does not match, usually because PCLK is not 15 MHz. Check the PLL and VPBDIV settings in Startup.s.
- Nothing received at all: TX and RX are swapped, the board is still in ISP mode, or PINSEL0 was overwritten later in the program.
- printf prints nothing: Use MicroLIB is not ticked, so the library tries to use semihosting and the program may stop at a breakpoint instruction.
- Each line starts in the middle of the screen: the program sends only LF; add CR before LF as the driver does.
- Echo works but long pastes lose characters: polling cannot keep up; switch to interrupt-driven reception.
Summary
UART0 on the LPC2148 needs four steps: select TXD0 and RXD0 in PINSEL0, set DLAB in U0LCR, load the divisor PCLK divided by 16 times baud into U0DLL and U0DLM, then clear DLAB. Poll THRE before sending and RDR before reading. With a 15 MHz PCLK, 9600 baud uses a divisor of 98 with only 0.35 percent error, and retargeting fputc gives you printf for every later experiment.