How to structure UART code for portability, maintainability

Thread Starter

Embededd

Joined Jun 4, 2025
157
I’ve implemented a UART communication project using a PIC microcontroller, where data is sent from a laptop to the MCU and received back on a serial terminal. Right now, the code is structured in separate files like main.c for application logic and uart.c / uart.h for driver-level implementation. This separation works fine for a small project.

Now I’m trying to think a bit ahead and improve the design so it becomes more reusable and portable for larger embedded systems. The code should be easy to maintain and extend. It should be reusable across different projects. It should be portable across different MCUs (for example, moving from 8-bit PIC to 32-bit PIC) with minimal changes

For example:

  • If I want to change the baud rate (say from 9600 to 115200), I want it to be configurable in a simple way without touching multiple places in the code or rewriting logic. I’m also not fully sure whether this should be handled via a configuration structure or conditional compilation.
  • If I migrate to a different PIC family, I want most of the application code to remain unchanged and only the hardware-specific UART should change.

Along with this, I also want to add a UART diagnostic or loopback test mode so that I can independently verify TX/RX communication. This would help debug UART issues even if the main application is not working properly.

I’m using a 8 bit PIC microcontroller and C language for this project. if you want, I can also share my code with result
 

ci139

Joined Jul 11, 2016
1,995
what's the UART iF(interface) to your laptop = ← the most UART issues is to enable it from the Win/OS side client app
also the OS side mainboards have multifunction I/O chips that may differ by behaviour & capabilities

i don't quite think what you attempt pays off the effort

if you gather more experience on it - you may be able to decide how to modularize your code

https://www.google.com/search?channel=entpr&q=pic+uart+library+github
 

MrChips

Joined Oct 2, 2009
34,833
Specifying the baud is the easy part. This would be specified in a serial.h file.
On top of that, you need to specify the source of the UART clock and its frequency. Sometimes it is based on the MCU clock frequency, but most often it is derived from another source.

Then there is the UART clock divider. This is specific to the UART chip or module. What registers are used to divide the input clock?

Most advanced MCUs have multiple UARTs. Which UART module are you requesting?
Then you have I/O cross-switches which allows you to select which I/O ports are assigned to the UART.

As you see, it gets very complicated to design for code portability. One solution is to use a HAL IDE (Hardware Abstraction Layer) but this is specific to one MCU family.

It is difficult enough to plan for code portability. And much more difficult to plan for cross-platform portability
 

Thread Starter

Embededd

Joined Jun 4, 2025
157
I am sharing my complete code

Application code
C:
/**
    Device            : PIC18F45K22
    Crystal           : 20 MHz
    UART              : 9600
*/

#include <xc.h>
#include <stdint.h>
#include "uart.h"

#define _XTAL_FREQ 20000000UL

//====================================================
// CONFIG BITS (STAYS HERE)
//====================================================

// CONFIG1H
#pragma config FOSC = HSHP    // Oscillator Selection bits->HS oscillator (high power > 16 MHz)
#pragma config PLLCFG = OFF    // 4X PLL Enable->Oscillator used directly
#pragma config PRICLKEN = ON    // Primary clock enable bit->Primary clock is always enabled
#pragma config FCMEN = OFF    // Fail-Safe Clock Monitor Enable bit->Fail-Safe Clock Monitor disabled
#pragma config IESO = OFF    // Internal/External Oscillator Switchover bit->Oscillator Switchover mode disabled

// CONFIG2L
#pragma config PWRTEN = OFF    // Power-up Timer Enable bit->Power up timer disabled
#pragma config BOREN = SBORDIS    // Brown-out Reset Enable bits->Brown-out Reset enabled in hardware only (SBOREN is disabled)
#pragma config BORV = 190    // Brown Out Reset Voltage bits->VBOR set to 1.90 V nominal

// CONFIG2H
#pragma config WDTEN = OFF    // Watchdog Timer Enable bits->Watch dog timer is always disabled. SWDTEN has no effect.
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits->1:32768

// CONFIG3H
#pragma config CCP2MX = PORTC1    // CCP2 MUX bit->CCP2 input/output is multiplexed with RC1
#pragma config PBADEN = ON    // PORTB A/D Enable bit->PORTB<5:0> pins are configured as analog input channels on Reset
#pragma config CCP3MX = PORTB5    // P3A/CCP3 Mux bit->P3A/CCP3 input/output is multiplexed with RB5
#pragma config HFOFST = ON    // HFINTOSC Fast Start-up->HFINTOSC output and ready status are not delayed by the oscillator stable status
#pragma config T3CMX = PORTC0    // Timer3 Clock input mux bit->T3CKI is on RC0
#pragma config P2BMX = PORTD2    // ECCP2 B output mux bit->P2B is on RD2
#pragma config MCLRE = EXTMCLR    // MCLR Pin Enable bit->MCLR pin enabled, RE3 input pin disabled

// CONFIG4L
#pragma config STVREN = ON    // Stack Full/Underflow Reset Enable bit->Stack full/underflow will cause Reset
#pragma config LVP = ON    // Single-Supply ICSP Enable bit->Single-Supply ICSP enabled if MCLRE is also 1
#pragma config XINST = OFF    // Extended Instruction Set Enable bit->Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
#pragma config DEBUG = OFF    // Background Debug->Disabled

// CONFIG5L
#pragma config CP0 = OFF    // Code Protection Block 0->Block 0 (000800-001FFFh) not code-protected
#pragma config CP1 = OFF    // Code Protection Block 1->Block 1 (002000-003FFFh) not code-protected
#pragma config CP2 = OFF    // Code Protection Block 2->Block 2 (004000-005FFFh) not code-protected
#pragma config CP3 = OFF    // Code Protection Block 3->Block 3 (006000-007FFFh) not code-protected

// CONFIG5H
#pragma config CPB = OFF    // Boot Block Code Protection bit->Boot block (000000-0007FFh) not code-protected
#pragma config CPD = OFF    // Data EEPROM Code Protection bit->Data EEPROM not code-protected

// CONFIG6L
#pragma config WRT0 = OFF    // Write Protection Block 0->Block 0 (000800-001FFFh) not write-protected
#pragma config WRT1 = OFF    // Write Protection Block 1->Block 1 (002000-003FFFh) not write-protected
#pragma config WRT2 = OFF    // Write Protection Block 2->Block 2 (004000-005FFFh) not write-protected
#pragma config WRT3 = OFF    // Write Protection Block 3->Block 3 (006000-007FFFh) not write-protected

// CONFIG6H
#pragma config WRTC = OFF    // Configuration Register Write Protection bit->Configuration registers (300000-3000FFh) not write-protected
#pragma config WRTB = OFF    // Boot Block Write Protection bit->Boot Block (000000-0007FFh) not write-protected
#pragma config WRTD = OFF    // Data EEPROM Write Protection bit->Data EEPROM not write-protected

// CONFIG7L
#pragma config EBTR0 = OFF    // Table Read Protection Block 0->Block 0 (000800-001FFFh) not protected from table reads executed in other blocks
#pragma config EBTR1 = OFF    // Table Read Protection Block 1->Block 1 (002000-003FFFh) not protected from table reads executed in other blocks
#pragma config EBTR2 = OFF    // Table Read Protection Block 2->Block 2 (004000-005FFFh) not protected from table reads executed in other blocks
#pragma config EBTR3 = OFF    // Table Read Protection Block 3->Block 3 (006000-007FFFh) not protected from table reads executed in other blocks

// CONFIG7H
#pragma config EBTRB = OFF    // Boot Block Table Read Protection bit->Boot Block (000000-0007FFh) not protected from table reads executed in other blocks

//====================================================
// SYSTEM INITIALIZATION
//====================================================

void SYSTEM_Initialize(void)
{
    // Disable analog (VERY IMPORTANT)
    ANSELA = 0x00;
    ANSELB = 0x00;
    ANSELC = 0x00;
    ANSELD = 0x00;
    ANSELE = 0x00;

    // GPIO direction
    TRISCbits.TRISC6 = 0;   // TX
    TRISCbits.TRISC7 = 1;   // RX

    // Optional: clear ports
    LATA = LATB = LATC = LATD = LATE = 0x00;
}

//====================================================
// MAIN
//====================================================

void main(void)
{
    uint8_t data;

    SYSTEM_Initialize();
    UART_Init();

    UART_WriteString("UART Echo Ready\r\n");

    while(1)
    {
        if(UART_DataReady())
        {
            data = UART_Read();
            UART_Write(data);
        }
    }
}
UART Header file
C:
#ifndef UART_H
#define UART_H

#include <stdint.h>

// Initialize UART
void UART_Init(void);

// Transmit functions
void UART_Write(uint8_t data);
void UART_WriteString(const char *str);

// Receive functions
uint8_t UART_DataReady(void);
uint8_t UART_Read(void);

#endif
UART c
C:
#include <xc.h>
#include "uart.h"

#define _XTAL_FREQ 20000000UL

//====================================================
// UART INIT
//====================================================
void UART_Init(void)
{
    // 9600 baud @ 20MHz, BRG16=1, BRGH=1 ? SPBRG = 519

    BAUDCON1bits.BRG16 = 1;

    TXSTA1bits.SYNC = 0;
    TXSTA1bits.BRGH = 1;
    TXSTA1bits.TXEN = 1;

    RCSTA1bits.SPEN = 1;
    RCSTA1bits.CREN = 1;

    SPBRGH1 = 0x02;
    SPBRG1  = 0x07;
}

//====================================================
// UART WRITE BYTE
//====================================================
void UART_Write(uint8_t data)
{
    while(!TXSTA1bits.TRMT);
    TXREG1 = data;
}

//====================================================
// UART WRITE STRING
//====================================================
void UART_WriteString(const char *str)
{
    while(*str)
    {
        UART_Write(*str++);
    }
}

//====================================================
// DATA READY CHECK
//====================================================
uint8_t UART_DataReady(void)
{
    return PIR1bits.RC1IF;
}

//====================================================
// UART READ BYTE
//====================================================
uint8_t UART_Read(void)
{
    if(RCSTA1bits.OERR)
    {
        RCSTA1bits.CREN = 0;
        RCSTA1bits.CREN = 1;
    }

    return RCREG1;
}

If later I want to change the baud rate (like 9600 to 115200), what is the best way to design the UART driver so I don’t need to modify multiple places in code? Should this be done using a config structure or conditional compilation?

Also, if I move to another PIC family, I want only the low-level UART code to change while the application code remains same. And would adding a UART loopback/diagnostic mode be a good practice for debugging?
 

joeyd999

Joined Jun 6, 2011
6,324
Nit pick: you've already written code that I'd never use (or reuse). It blocks on transmit. Thoroughly useless.

Consider using recieve and transmit queues, and interrupts, to ensure your MCU can run off and do something else while waiting for the hardware to become ready.
 

nsaspook

Joined Aug 27, 2009
16,330
I am sharing my complete code

Application code
C:
/**
    Device            : PIC18F45K22
    Crystal           : 20 MHz
    UART              : 9600
*/

#include <xc.h>
#include <stdint.h>
#include "uart.h"

#define _XTAL_FREQ 20000000UL

//====================================================
// CONFIG BITS (STAYS HERE)
//====================================================

// CONFIG1H
#pragma config FOSC = HSHP    // Oscillator Selection bits->HS oscillator (high power > 16 MHz)
#pragma config PLLCFG = OFF    // 4X PLL Enable->Oscillator used directly
#pragma config PRICLKEN = ON    // Primary clock enable bit->Primary clock is always enabled
#pragma config FCMEN = OFF    // Fail-Safe Clock Monitor Enable bit->Fail-Safe Clock Monitor disabled
#pragma config IESO = OFF    // Internal/External Oscillator Switchover bit->Oscillator Switchover mode disabled

// CONFIG2L
#pragma config PWRTEN = OFF    // Power-up Timer Enable bit->Power up timer disabled
#pragma config BOREN = SBORDIS    // Brown-out Reset Enable bits->Brown-out Reset enabled in hardware only (SBOREN is disabled)
#pragma config BORV = 190    // Brown Out Reset Voltage bits->VBOR set to 1.90 V nominal

// CONFIG2H
#pragma config WDTEN = OFF    // Watchdog Timer Enable bits->Watch dog timer is always disabled. SWDTEN has no effect.
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits->1:32768

// CONFIG3H
#pragma config CCP2MX = PORTC1    // CCP2 MUX bit->CCP2 input/output is multiplexed with RC1
#pragma config PBADEN = ON    // PORTB A/D Enable bit->PORTB<5:0> pins are configured as analog input channels on Reset
#pragma config CCP3MX = PORTB5    // P3A/CCP3 Mux bit->P3A/CCP3 input/output is multiplexed with RB5
#pragma config HFOFST = ON    // HFINTOSC Fast Start-up->HFINTOSC output and ready status are not delayed by the oscillator stable status
#pragma config T3CMX = PORTC0    // Timer3 Clock input mux bit->T3CKI is on RC0
#pragma config P2BMX = PORTD2    // ECCP2 B output mux bit->P2B is on RD2
#pragma config MCLRE = EXTMCLR    // MCLR Pin Enable bit->MCLR pin enabled, RE3 input pin disabled

// CONFIG4L
#pragma config STVREN = ON    // Stack Full/Underflow Reset Enable bit->Stack full/underflow will cause Reset
#pragma config LVP = ON    // Single-Supply ICSP Enable bit->Single-Supply ICSP enabled if MCLRE is also 1
#pragma config XINST = OFF    // Extended Instruction Set Enable bit->Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
#pragma config DEBUG = OFF    // Background Debug->Disabled

// CONFIG5L
#pragma config CP0 = OFF    // Code Protection Block 0->Block 0 (000800-001FFFh) not code-protected
#pragma config CP1 = OFF    // Code Protection Block 1->Block 1 (002000-003FFFh) not code-protected
#pragma config CP2 = OFF    // Code Protection Block 2->Block 2 (004000-005FFFh) not code-protected
#pragma config CP3 = OFF    // Code Protection Block 3->Block 3 (006000-007FFFh) not code-protected

// CONFIG5H
#pragma config CPB = OFF    // Boot Block Code Protection bit->Boot block (000000-0007FFh) not code-protected
#pragma config CPD = OFF    // Data EEPROM Code Protection bit->Data EEPROM not code-protected

// CONFIG6L
#pragma config WRT0 = OFF    // Write Protection Block 0->Block 0 (000800-001FFFh) not write-protected
#pragma config WRT1 = OFF    // Write Protection Block 1->Block 1 (002000-003FFFh) not write-protected
#pragma config WRT2 = OFF    // Write Protection Block 2->Block 2 (004000-005FFFh) not write-protected
#pragma config WRT3 = OFF    // Write Protection Block 3->Block 3 (006000-007FFFh) not write-protected

// CONFIG6H
#pragma config WRTC = OFF    // Configuration Register Write Protection bit->Configuration registers (300000-3000FFh) not write-protected
#pragma config WRTB = OFF    // Boot Block Write Protection bit->Boot Block (000000-0007FFh) not write-protected
#pragma config WRTD = OFF    // Data EEPROM Write Protection bit->Data EEPROM not write-protected

// CONFIG7L
#pragma config EBTR0 = OFF    // Table Read Protection Block 0->Block 0 (000800-001FFFh) not protected from table reads executed in other blocks
#pragma config EBTR1 = OFF    // Table Read Protection Block 1->Block 1 (002000-003FFFh) not protected from table reads executed in other blocks
#pragma config EBTR2 = OFF    // Table Read Protection Block 2->Block 2 (004000-005FFFh) not protected from table reads executed in other blocks
#pragma config EBTR3 = OFF    // Table Read Protection Block 3->Block 3 (006000-007FFFh) not protected from table reads executed in other blocks

// CONFIG7H
#pragma config EBTRB = OFF    // Boot Block Table Read Protection bit->Boot Block (000000-0007FFh) not protected from table reads executed in other blocks

//====================================================
// SYSTEM INITIALIZATION
//====================================================

void SYSTEM_Initialize(void)
{
    // Disable analog (VERY IMPORTANT)
    ANSELA = 0x00;
    ANSELB = 0x00;
    ANSELC = 0x00;
    ANSELD = 0x00;
    ANSELE = 0x00;

    // GPIO direction
    TRISCbits.TRISC6 = 0;   // TX
    TRISCbits.TRISC7 = 1;   // RX

    // Optional: clear ports
    LATA = LATB = LATC = LATD = LATE = 0x00;
}

//====================================================
// MAIN
//====================================================

void main(void)
{
    uint8_t data;

    SYSTEM_Initialize();
    UART_Init();

    UART_WriteString("UART Echo Ready\r\n");

    while(1)
    {
        if(UART_DataReady())
        {
            data = UART_Read();
            UART_Write(data);
        }
    }
}
UART Header file
C:
#ifndef UART_H
#define UART_H

#include <stdint.h>

// Initialize UART
void UART_Init(void);

// Transmit functions
void UART_Write(uint8_t data);
void UART_WriteString(const char *str);

// Receive functions
uint8_t UART_DataReady(void);
uint8_t UART_Read(void);

#endif
UART c
C:
#include <xc.h>
#include "uart.h"

#define _XTAL_FREQ 20000000UL

//====================================================
// UART INIT
//====================================================
void UART_Init(void)
{
    // 9600 baud @ 20MHz, BRG16=1, BRGH=1 ? SPBRG = 519

    BAUDCON1bits.BRG16 = 1;

    TXSTA1bits.SYNC = 0;
    TXSTA1bits.BRGH = 1;
    TXSTA1bits.TXEN = 1;

    RCSTA1bits.SPEN = 1;
    RCSTA1bits.CREN = 1;

    SPBRGH1 = 0x02;
    SPBRG1  = 0x07;
}

//====================================================
// UART WRITE BYTE
//====================================================
void UART_Write(uint8_t data)
{
    while(!TXSTA1bits.TRMT);
    TXREG1 = data;
}

//====================================================
// UART WRITE STRING
//====================================================
void UART_WriteString(const char *str)
{
    while(*str)
    {
        UART_Write(*str++);
    }
}

//====================================================
// DATA READY CHECK
//====================================================
uint8_t UART_DataReady(void)
{
    return PIR1bits.RC1IF;
}

//====================================================
// UART READ BYTE
//====================================================
uint8_t UART_Read(void)
{
    if(RCSTA1bits.OERR)
    {
        RCSTA1bits.CREN = 0;
        RCSTA1bits.CREN = 1;
    }

    return RCREG1;
}

If later I want to change the baud rate (like 9600 to 115200), what is the best way to design the UART driver so I don’t need to modify multiple places in code? Should this be done using a config structure or conditional compilation?

Also, if I move to another PIC family, I want only the low-level UART code to change while the application code remains same. And would adding a UART loopback/diagnostic mode be a good practice for debugging?
https://forum.allaboutcircuits.com/...c18f57k42-and-pic18f47q84.157503/post-1968252

C:
/*
 * on the fly bps selection
 */
void UART1_Initialize19200(void)
{
    // Disable interrupts before changing states
    PIE4bits.U1RXIE = 0;
    UART1_SetRxInterruptHandler(UART1_Receive_ISR);
    PIE4bits.U1TXIE = 0;
    UART1_SetTxInterruptHandler(UART1_Transmit_ISR);
    PIE4bits.U1EIE = 0;


    // Set the UART1 module to the options selected in the user interface.

    // P1L 0; 
    U1P1L = 0x00;

    // P1H 0; 
    U1P1H = 0x00;

    // P2L 0; 
    U1P2L = 0x00;

    // P2H 0; 
    U1P2H = 0x00;

    // P3L 0; 
    U1P3L = 0x00;

    // P3H 0; 
    U1P3H = 0x00;

    // BRGS high speed; MODE Asynchronous 8-bit mode; RXEN enabled; TXEN enabled; ABDEN disabled; 
    U1CON0 = 0xB0;

    // RXBIMD Set RXBKIF on rising RX input; BRKOVR disabled; WUE disabled; SENDB disabled; ON enabled; 
    U1CON1 = 0x80;

    // TXPOL not inverted; FLO off; C0EN Checksum Mode 0; RXPOL not inverted; RUNOVF RX input shifter stops all activity; STP Transmit 1Stop bit, receiver verifies first Stop bit; 
    U1CON2 = 0x00;

    // BRGL 64; 
    U1BRGL = 0x40;

    // BRGH 3; 
    U1BRGH = 0x03;

    // STPMD in middle of first Stop bit; TXWRE No error; 
    U1FIFO = 0x00;

    // ABDIF Auto-baud not enabled or not complete; WUIF WUE not enabled by software; ABDIE disabled; 
    U1UIR = 0x00;

    // ABDOVF Not overflowed; TXCIF 0; RXBKIF No Break detected; RXFOIF not overflowed; CERIF No Checksum error; 
    U1ERRIR = 0x00;

    // TXCIE disabled; FERIE disabled; TXMTIE disabled; ABDOVE disabled; CERIE disabled; RXFOIE disabled; PERIE disabled; RXBKIE disabled; 
    U1ERRIE = 0x00;


    UART1_SetOverrunErrorHandler(UART1_DefaultOverrunErrorHandler);
    UART1_SetErrorHandler(UART1_DefaultErrorHandler);

    uart1RxLastError.status = 0;

    // initializing the driver state
    uart1TxHead = 0;
    uart1TxTail = 0;
    uart1TxBufferRemaining = sizeof(uart1TxBuffer);
    uart1RxHead = 0;
    uart1RxTail = 0;
    uart1RxCount = 0;

    // enable receive interrupt
    PIE4bits.U1RXIE = 1;
    // enable error interrupt
    PIE4bits.U1EIE = 1;
}
 
Last edited:

Thread Starter

Embededd

Joined Jun 4, 2025
157
I have a uart.c where UART can work in either polling or interrupt mode. Right now both are kind of mixed in the same file, but I’m not sure what’s the right way to handle this in a bigger project.

C:
#include <xc.h>
#include <stdint.h>
#include "uart.h"

volatile uint8_t rxData;
volatile uint8_t rxFlag = 0;

//====================================================
// UART Initialize
//====================================================
void EUSART1_Initialize(void)
{
    /*
     * UART configuration for asynchronous communication
     * Baud rate: 9600 @ 20 MHz
     *
     * Configuration uses 16-bit baud generator with high-speed mode.
     * Resulting baud error is within acceptable limits (~0.16%).
     */

    // ---------- Baud rate control configuration ----------
    BAUDCON1bits.ABDEN = 0;
    BAUDCON1bits.WUE   = 0;
    BAUDCON1bits.BRG16 = 1;
    BAUDCON1bits.CKTXP = 0;
    BAUDCON1bits.DTRXP = 0;

    // ---------- Transmit configuration ----------
    TXSTA1bits.TX9   = 0;
    TXSTA1bits.TXEN  = 1;
    TXSTA1bits.SYNC  = 0;
    TXSTA1bits.SENDB = 0;
    TXSTA1bits.BRGH  = 1;

    // ---------- Receive configuration ----------
    RCSTA1bits.SPEN  = 1;
    RCSTA1bits.RX9   = 0;
    RCSTA1bits.SREN  = 0;
    RCSTA1bits.CREN  = 1;
    RCSTA1bits.ADDEN = 0;

    // ---------- Baud rate generator value ----------
    SPBRGH1 = 0x02;
    SPBRG1  = 0x07;

    /*
     * UART receive interrupt enabled.
     *
     */
    PIE1bits.RC1IE = 1;

    INTCONbits.PEIE = 1;
    INTCONbits.GIE  = 1;
}

//====================================================
// UART Write Function
//====================================================
void EUSART1_Write(uint8_t data)
{
    while (PIR1bits.TX1IF == 0)
    {
        /* waiting for transmit buffer availability */
    }

    TXREG1 = data;
}

//====================================================
// UART Read Function
//====================================================
uint8_t EUSART1_Read(void)
{
    while (PIR1bits.RC1IF == 0)
    {
        /* waiting for received data */
    }

    return RCREG1;
}
If I go with polling, I’ll have to ignore or disable the interrupt part manually. So I was thinking in larger projects, is it actually OK to just enable/disable interrupts in code like this, or is it better to use something like conditional compilation so only the required mode gets built?

What’s the usual approach you guys follow for this?
 

Vihaan@123

Joined Oct 7, 2025
251
In general interrupt is the recommended approach for any transmission and reception, as polling has the disadvantage of waiting for the response.
 
Top