Embedded C track

Lesson 11 · Part 2: 8051 (Keil C51 / AT89C52)

UART Serial Communication

Serial transmit and receive

Part 2: 8051 (Keil C51 / AT89C52)10 min readFree lesson

Until now, we have interacted with the chip using LEDs and buttons, but we were not able to exchange values in "human-readable text." To print debugging messages or send sensor values to the PC, a communication method is needed. The most basic is UART (serial). Over two wires (TXD/RXD), the chip and PC exchange text.

UART is asynchronous communication. Since there is no separate clock line, both sender and receiver pre-agree on the baud rate and transmit and read bits at that speed. On the 8051, SCON sets the serial mode, Timer1 generates the baud rate clock, and data is put in and taken out using a one-byte register called SBUF. In this chapter, we learn how to send a string to the PC at 9600bps·8N1 and how to process received characters.

Learning Objectives

  • Synchronous/Asynchronous Explains the difference in communication and the UART frame (start·data·stop).
  • SCON Sets the serial mode (Mode1, 8-bit UART) and enables reception (REN).
  • Timer1 (Mode2 auto-reload) creates the baud rate and TH1 calculates the reload value.
  • SBUF/TI/RI Transmit and receive one byte.
  • Understand why an 11.0592MHz crystal is used for UART.

Target Environment

  • Compiler: Keil C51.
  • Device: AT89C52 (μVision simulator). UART assumes an 11.0592MHz crystal.
  • Verification: μVision simulator (observe transmission and reception on View → Serial Windows → UART #1). Baud rate calculation is on PC gcc (pc_test.c).

For convenience in time calculation, Timer 9 assumed 12MHz, but for UART, it is proper to use 11.0592MHz to avoid baud rate error (see below 3).

Key Concepts

1) UART — Asynchronous Serial

A method of sending data one bit at a time on one line is called serial. UART works in an asynchronous way without a separate clock line, so it is required that both sides match the same baud rate (bits per second, e.g., 9600bps). Connections are usually three wires: TXD (transmit, 8051 P3.1), RXD (receive, P3.0), GND.

Synchronous (SPI/I2C) Asynchronous (UART)
Clock line Yes No
Speed agreement Automatic with clock Agree on baud rate in advance
Number of wires Many Few (TXD/RXD/GND)

2) Frame — start / data / stop (8N1)

The idle line is High (1). The order the line takes when sending one byte is:

Idle(1) → [start=0] → [d0 d1 d2 d3 d4 d5 d6 d7] → [stop=1] → Idle(1)
↑ Data is from LSB(d0)!
  • Start bit: always 0. Drops the line from High to Low to indicate "start" (synchronize receiver).
  • Data bits: send 8 bits from LSB.
  • Parity: none (N).
  • Stop bit: always 1.

Data 8 bits, no parity (None), 1 stop bit → commonly called "8N1". In Assignment 2, you directly unfold a byte to see what bit sequence it goes out as.

3) Baud Rate and Timer1 — Why 11.0592MHz

The 8051 generates the baud clock using the overflow rate of Timer1. Set Timer1 to Mode2 (8-bit auto-reload) and load the reload value into TH1. When SMOD=0:

Baul = Fosc / (384 × (256 - TH1))
→ n = 256 - TH1 = Fosc / (384 × Baul)

At 11.0592MHz for 9600bps:

n   = 11,059,200 / (384 × 9600) = 3   (It comes out as an exact integer!)
TH1 = 256 - 3 = 253 = 0xFD

11.0592MHz falls exactly as an integer in the standard baud rates (1200/2400/4800/9600…), so error is 0. On the other hand, 12MHz at 9600 → rounding n = 3.26… to an integer 3 actually gives about 10417bps, an error of ~8.5%, which disrupts communication. That’s why UART boards often use an 11.0592MHz crystal.

4) SCON — Serial Control Register

Bit Name Function
SM0/SM1 Mode Select 01=Mode1 (8-bit UART, variable baud rate)
SM2 Multi-processor Usually 0
REN Receive Enable 1 to enable reception
TB8/RB8 9th Bit Used by In-Mode-2/3
TI Transmit Complete Flag 1 when a byte is fully sent (cleared by software)
RI Receive Complete Flag 1 when a byte is fully received (cleared by software)

Standard setting SCON = 0x50 → 0101 0000: SM0=0, SM1=1(Mode1), REN=1(receive enabled), others 0.

5) SBUF and Transmission/Reception Flow

SBUF is a one-byte register with the same name, but writing is for transmission, reading is for reception.

Transmission: Writing to SBUF, the hardware pushes out the bits, and when finished, TI=1. Software waits for TI and clears it to 0.

void uart_tx(unsigned char c) {
    SBUF = c;            /*Start transmission*/
    while (TI == 0);     /*Wait until completion*/
    TI = 0;              /*Clear flag*/
}

Reception: When one byte comes in, RI=1. Software reads SBUF and clears RI.

unsigned char uart_rx(void) {
    while (RI == 0);     /*Waiting for reception*/
    RI = 0;              /*Clear flag*/
    return SBUF;
}

TI/RI are only set by the hardware and not cleared automatically. Software must clear to 0 each time to process the next byte.

6) (Reference) Sending printf via UART

In Keil C51, printf internally outputs one character to putchar. If putchar is redirected to UART transmission, printf is sent directly to serial.

char putchar(char c) {     /*Keil C51 library call*/
    SBUF = c;
    while (TI == 0);
    TI = 0;
    return c;
}

The 8051 has small RAM, so printf is heavy. Except for debugging, custom uart_tx/uart_puts are lightweight.

Creating a μVision Project

  1. Project → New µVision Project… → name it c51_uart.
  2. Device: Atmel → AT89C52. Include STARTUP.A51 (Yes).
  3. Set the Xtal(MHz) on the Options for Target → Target tab to 11.0592 (baud rate accuracy).
  4. After adding main.c, copy and paste the code below.
  5. Options for Target → Debug → Use Simulator.
  6. Rebuild(F7) → Start Debug(Ctrl+F5).
  7. Open the View → Serial Windows → UART #1 window (transmission/reception text will appear here).

Examples

Example 1 — main.c: Sending a String via UART

Initialize at 9600bps · 8N1 and repeatedly transmit "Hello, 8051!".

#include <reg52.h>

/*
 *Lesson 11 Example 1 — Sending a string to a PC via UART (9600bps, 8N1).
 *Target: AT89C52, Compiler: Keil C51. Assuming an 11.0592MHz crystal for UART.
 *
 *Asynchronous serial (UART) communicates bits at a 'baud rate' agreed upon by both sides, without a clock line.
 *The 8051 generates the baud clock using the overflow of Timer1.
 *   9600bps @ 11.0592MHz, SMOD=0 → TH1 = 256 - 11059200/(384*9600) = 0xFD
 *
 *Transmission flow: When one byte is written to SBUF, the hardware pushes out the bits, and when all are sent,
 *it sets TI (transmission complete flag) to 1. The software waits for TI and clears it to 0.
 */

/*Initialize UART to 9600bps, 8 bits, 1 stop bit, no parity (8N1).*/
void uart_init(void)
{
    SCON = 0x50;          /*SM1=1 (Mode 1, 8-bit UART) + REN=1 (enable reception)*/
    TMOD &= 0x0F;         /*Only clear Timer1 bits (preserve Timer0 settings)*/
    TMOD |= 0x20;         /*Timer1 = Mode 2 (8-bit auto-reload) → generates baud rate*/
    TH1  = 0xFD;          /*Reload value: 9600bps @ 11.0592MHz*/
    TL1  = 0xFD;
    TR1  = 1;             /*Start Timer1*/
}

/*Send one byte (wait until complete).*/
void uart_tx(unsigned char c)
{
    SBUF = c;             /*Writing a byte to the buffer starts transmission*/
    while (TI == 0) {     /*Wait until transmission completes (TI=1)*/
    }
    TI = 0;               /*Clear the flag for the next transmission*/
}

/*Send a null-terminated string.*/
void uart_puts(const char *s)
{
    while (*s) {
        uart_tx((unsigned char)*s++);
    }
}

/*Pause briefly between transmissions for easy observation (software delay, precision not required).*/
static void delay(void)
{
    unsigned int i;
    for (i = 0; i < 60000; i++) {
    }
}

void main(void)
{
    uart_init();
    while (1) {
        uart_puts("Hello, 8051!\r\n");   /*It appears line by line in the Serial window*/
        delay();
    }
}

Example 2 — pc_test.c: Baud Rate Reload (TH1) Calculation Verification

Calculate Timer1 reload values for standard baud rates and check if they result in integers at 11.0592MHz.

#include <stdio.h>

/*
 *Lecture 11 Example 2 — Verification of Timer1 reload (TH1) calculation matching UART baud rate.
 *Based on 11.0592MHz crystal, SMOD=0, Timer1 Mode2 (8-bit auto-reload).
 *n   = Fosc / (384 * baud)   (Number counted until Timer1 overflows once)
 *   TH1 = 256 - n
 *For 11.0592MHz, n becomes an 'integer' at standard baud rates, so the baud error is 0. Therefore,
 *we often use 11.0592MHz instead of 12MHz as the crystal for UART (12MHz causes about 8.5% error at 9600 baud,
 *so communication fails).
 */
int main(void)
{
    unsigned long fosc = 11059200UL;       /* 11.0592 MHz */
    unsigned int bauds[] = { 1200, 2400, 4800, 9600 };
    int count = (int)(sizeof(bauds) / sizeof(bauds[0]));
    int k;

    for (k = 0; k < count; k++) {
        unsigned int baud = bauds[k];
        unsigned int n = (unsigned int)(fosc / (384UL * baud));
        unsigned char th1 = (unsigned char)(256 - n);
        printf("%5u baud -> n=%3u  TH1=0x%02X\n", baud, n, th1);
    }

    return 0;
}

Checking with Simulator

  • Open the View → Serial Windows → UART #1 window and press F5 (run).
  • Expected behavior: Hello, 8051! is repeatedly printed line by line in the UART #1 window.
  • Immediately after transmission, you can see in the register window that the TI bit of SCON is set to 1 and then cleared to 0 by code.
  • (Reception experiment) Even without Peripherals, when you type in the UART #1 window, it goes to RXD. Echo will be covered in Assignment 1.

Verification on a PC

gcc pc_test.c -o pc_test && ./pc_test

Expected output (stdout):

 1200 baud -> n= 24  TH1=0xE8
 2400 baud -> n= 12  TH1=0xF4
 4800 baud -> n=  6  TH1=0xFA
 9600 baud -> n=  3  TH1=0xFD

Common Mistakes

Q. Only one character appears, but it stops from the second one.

A. It is likely that TI (transmission complete flag) was not cleared. Hardware only sets TI to 1. After each transmission, you must manually clear it with TI = 0; for the next while (TI == 0) to work. Reception works the same way with RI.

Q. Characters appear corrupted (□ or strange symbols).

A. The baud rate does not match. Check: ① if the Simulator/Terminal-s baud rate is set to 9600, ② if the Xtal is set to 11.0592MHz. If left at 12MHz, even TH1=0xFD will transmit at an actual baud rate of ~10417bps, causing corruption.

Q. Transmission works, but reception does not.

A. You may not have turned on the REN (receive enable) bit of SCON. SCON = 0x50 includes REN=1. Also, check if you cleared it after reading RI.

Q. I only put in TH1, but the baud rate is strange.

A. Check whether Timer1 is set to Mode2 (auto reload) (TMOD |= 0x20) and whether you started it with TR1 = 1. If it’s not Mode2, reload does not occur each time, so the baud rate will be off.

Q. printf is too heavy or doesn’t come out.

A. To send printf via UART in Keil C51, you need to redefine putchar (core concept 6). Even so, it’s heavy on the 8051, so except for debugging, use your own uart_tx/uart_puts.

Summary

  • UART is asynchronous serial without a clock line. Both sides must agree on the same baud rate.
  • Frames are start(0) → 8-bit data (LSB first) → stop(1). If there’s no parity, it’s 8N1.
  • Generate baud rate with SCON = 0x50 using Mode1·REN, and Timer1 using Mode2 (TMOD |= 0x20).
  • Reload baud rate with TH1-256---Fosc/384-baud-rate. At 11.0592MHz, standard baud rates are integers.
  • Writing to SBUF transmits, reading receives. TI/RI are cleared each time by software.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/c51_uart/main.cC
#include <reg52.h>

/*
 *Lesson 11 Example 1 — Sending a string to a PC via UART (9600bps, 8N1).
 *Target: AT89C52, Compiler: Keil C51. Assuming an 11.0592MHz crystal for UART.
 *
 *Asynchronous serial (UART) communicates bits at a 'baud rate' agreed upon by both sides, without a clock line.
 *The 8051 generates the baud clock using the overflow of Timer1.
 *   9600bps @ 11.0592MHz, SMOD=0 → TH1 = 256 - 11059200/(384*9600) = 0xFD
 *
 *Transmission flow: When one byte is written to SBUF, the hardware pushes out the bits, and when all are sent,
 *it sets TI (transmission complete flag) to 1. The software waits for TI and clears it to 0.
 */

/*Initialize UART to 9600bps, 8 bits, 1 stop bit, no parity (8N1).*/
void uart_init(void)
{
    SCON = 0x50;          /*SM1=1 (Mode 1, 8-bit UART) + REN=1 (enable reception)*/
    TMOD &= 0x0F;         /*Only clear Timer1 bits (preserve Timer0 settings)*/
    TMOD |= 0x20;         /*Timer1 = Mode 2 (8-bit auto-reload) → generates baud rate*/
    TH1  = 0xFD;          /*Reload value: 9600bps @ 11.0592MHz*/
    TL1  = 0xFD;
    TR1  = 1;             /*Start Timer1*/
}

/*Send one byte (wait until complete).*/
void uart_tx(unsigned char c)
{
    SBUF = c;             /*Writing a byte to the buffer starts transmission*/
    while (TI == 0) {     /*Wait until transmission completes (TI=1)*/
    }
    TI = 0;               /*Clear the flag for the next transmission*/
}

/*Send a null-terminated string.*/
void uart_puts(const char *s)
{
    while (*s) {
        uart_tx((unsigned char)*s++);
    }
}

/*Pause briefly between transmissions for easy observation (software delay, precision not required).*/
static void delay(void)
{
    unsigned int i;
    for (i = 0; i < 60000; i++) {
    }
}

void main(void)
{
    uart_init();
    while (1) {
        uart_puts("Hello, 8051!\r\n");   /*It appears line by line in the Serial window*/
        delay();
    }
}
src/c51_uart/pc_test.cC
#include <stdio.h>

/*
 *Lecture 11 Example 2 — Verification of Timer1 reload (TH1) calculation matching UART baud rate.
 *Based on 11.0592MHz crystal, SMOD=0, Timer1 Mode2 (8-bit auto-reload).
 *n   = Fosc / (384 * baud)   (Number counted until Timer1 overflows once)
 *   TH1 = 256 - n
 *For 11.0592MHz, n becomes an 'integer' at standard baud rates, so the baud error is 0. Therefore,
 *we often use 11.0592MHz instead of 12MHz as the crystal for UART (12MHz causes about 8.5% error at 9600 baud,
 *so communication fails).
 */
int main(void)
{
    unsigned long fosc = 11059200UL;       /* 11.0592 MHz */
    unsigned int bauds[] = { 1200, 2400, 4800, 9600 };
    int count = (int)(sizeof(bauds) / sizeof(bauds[0]));
    int k;

    for (k = 0; k < count; k++) {
        unsigned int baud = bauds[k];
        unsigned int n = (unsigned int)(fosc / (384UL * baud));
        unsigned char th1 = (unsigned char)(256 - n);
        printf("%5u baud -> n=%3u  TH1=0x%02X\n", baud, n, th1);
    }

    return 0;
}

Assignment

Question 1 is for C51 (AT89C52, μVision simulator), and Question 2 is for pure logic (PC gcc verification).


Problem 1 — UART Echo (Lowercase to Uppercase)

  • File name: homework_01/main.c
  • Key concepts: UART receive (RI/SBUF), transmit (TI/SBUF), character processing
  • Platform: C51 (AT89C52)

Requirements

  • Initialize UART at 9600bps·8N1 (SCON, Timer1 Mode2, TH1=0xFD, assuming 11.0592MHz).
  • When receiving a character, lowercase letters (a~z) are converted to uppercase and sent back as is (echo).
  • Other characters are sent back unchanged.

Expected Operation (Simulator)

  • If you input abc into the UART #1 window, ABC is returned; if you input Hi 8051, HI 8051 is returned.

Hint

  • Receive: while (RI == 0); RI = 0; c = SBUF;. Transmit: SBUF = c; while (TI == 0); TI = 0;.
  • Uppercase conversion: if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';.
  • Be sure to turn on REN (enable receive) — it is included in SCON = 0x50.

Problem 2 — UART 8N1 Frame Expansion (PC)

  • File name: homework_02/main.c
  • Key concepts: serial frame structure (start/data/stop), LSB-first transmission
  • Platform: Common (PC gcc)

Requirements

  • For each character in the string "Hi", output the 8N1 bit sequence that goes on the line.
  • The order is start(0) → 8 data bits (LSB first) → stop(1). Data bits start from LSB (b0).
  • The output format must match the "expected output" per character (start and stop are separated by spaces).

Expected output

'H' 0x48: 0 00010010 1
'i' 0x69: 0 10010110 1

Hint

  • Data bits are output for (b = 0; b < 8; b++) printf("%d", (c >> b) & 1); from LSB.
  • Line format: printf("'%c' 0x%02X: 0 ", c, c); → 8-bit data → printf(" 1\n");.
  • Let's manually check that the LSB-first data of 'H'=0x48 is 00010010.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <reg52.h>

/*
 *Answer 1 (C51, AT89C52) — UART Echo (sends back the received character, lowercase to uppercase).
 *
 *Key Points:
 *- Reception: Wait until RI becomes 1, then read SBUF and clear RI.
 *- Transmission: Write to SBUF and wait until TI becomes 1, then clear TI.
 *- Baud rate generation uses Timer1 Mode2 (8-bit auto-reload), 9600bps @ 11.0592MHz → TH1=0xFD.
 *
 *Common mistake:
 *- A case where it stops from the second character because the TI/RI flag is not cleared (the hardware automatically
 *It does not let it go down. The software must be erased to 0).
 *- If reception does not occur because REN (the receive enable bit of SCON) is not turned on (SCON=0x50).
 *
 *Expected behavior: When characters are entered in the μVision Serial window, they are returned as is (lowercase letters are converted to uppercase).
 */

void uart_init(void)
{
    SCON = 0x50;          /*Mode1 (8-bit UART) + REN (Receive Enable)*/
    TMOD &= 0x0F;         /*Clear only Timer1 configuration bits (preserve Timer0)*/
    TMOD |= 0x20;         /*Timer1 = Mode2 (8-bit auto reload)*/
    TH1  = 0xFD;          /* 9600bps @ 11.0592MHz */
    TL1  = 0xFD;
    TR1  = 1;             /*Start Timer1 (supply clock with prescaler)*/
}

unsigned char uart_rx(void)
{
    while (RI == 0) {     /*Wait until receiving is complete*/
    }
    RI = 0;               /*Clear receive flag*/
    return SBUF;
}

void uart_tx(unsigned char c)
{
    SBUF = c;             /*Start transmission*/
    while (TI == 0) {     /*Wait until transmission is complete*/
    }
    TI = 0;               /*Clear transmit flag*/
}

void main(void)
{
    unsigned char c;

    uart_init();
    while (1) {
        c = uart_rx();
        if (c >= 'a' && c <= 'z') {
            c = (unsigned char)(c - 'a' + 'A');   /*Lowercase → Uppercase*/
        }
        uart_tx(c);
    }
}
homework_02/main.cC
#include <stdio.h>

/*
 *Answer 2 (Pure Logic, PC) — Try unfolding a UART 8N1 frame into bits.
 *Asynchronous serial sends bits on the line in the following order when transmitting one byte:
 *start (always 0) → 8 data bits (LSB first!) → stop (always 1)
 *No parity (N), 8 data bits, 1 stop bit = "8N1".
 *
 *Key Points:
 *- Data is sent starting from LSB (b0). It's easy to mistakenly think it starts from MSB.
 *- The idle line is High (1), and only the start bit drops Low (0) so the receiver can synchronize.
 *
 *Common mistake:
 *- If data bits are output starting from MSB, the order gets reversed.
 */
int main(void)
{
    const char *msg = "Hi";
    int i, b;

    for (i = 0; msg[i] != '\0'; i++) {
        unsigned char c = (unsigned char)msg[i];
        printf("'%c' 0x%02X: 0 ", c, c);    /*Start bit 0*/
        for (b = 0; b < 8; b++) {           /*Data: starting from LSB (b0)*/
            printf("%d", (c >> b) & 1);
        }
        printf(" 1\n");                     /*Stop bit 1*/
    }

    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now