uart.md 6.8 KB


title: UART Manager author: Pat Beirne email: patb@pbeirne.com date: 2025/01/30

licence: MIT

Back

This is not a state machine, per se. This page is included because I found it very common to need UART communication, and I found I could fit it into the event-dispatcher system pretty easily.

Suppose we have a project which needs to use the UART to send and receive information. The outbound stream contains diagnostic information and measured parameters; the report stream is composed of chunks of up to 100 bytes at a time. Output reports can be generated by any task, and the chunks of data must not be interleaved.

The input stream consists of command chunks of 1-10 bytes, used for testing, for remote control and for injecting input stimuli.

When data arrives (a command), all tasks are informed with an EVT_CHAR. The tasks can then query the UART service routines to see if they need to process the input.

Reality Check I recommend sending ASCII chars through the UART. It makes debugging ***way*** easier. For some of my projects, I use fixed-length packets, where the first characters determine the packet length. For example: **Master -> UART -> Slave (this processor) ** | command | syntax | meaning | | --- | --- | --- | | light LED | Lnf | set LED {n} either {f==0} off or {f==1} on | | beep | Bn | issue a beep for {n} x100 ms | | inject key press | kn | simulate key {n} press | | inject key release | Kn | simulate key {n} release | **Slave (this processor) -> UART -> Master ** | report | syntax | meaning | | --- | --- | --- | | keypress | kn | key {n} is pressed | | keyrelease | Kn | key {n} is released | | debug string | Dnxxxxxxxx | general debugging, of length {n} | | init finished | I | the startup process is finished, processor ready | > {n} is one of `"0123456789ABCDE...XYZabcd.....xyz"`

We create some buffers in RAM to hold the characters until they can be sent or processed:

#define TX_SIZE 256
#define RX_SIZE 32

/* circular buffers */

char tx_buffer[TX_SIZE];        
char *tx_next_empty = tx_buffer, *tx_next_send = tx_buffer;
const char * const tx_end = tx_buffer + TX_SIZE;


char rx_buffer[RX_SIZE];       
char *rx_next_empty = rx_buffer, *rx_next_rcv = rx_buffer;
const char * const rx_end = rx_buffer + RX_SIZE;

This task will have a few service routines which any task can access at any time:

const char uartGetChar(void);       // returns -1 for no-char-available
const char uartCheckChar(void);     // leave char in buffer
const int  uartGetCharCount(void);
void       uartDiscardChars(int n);
const char uartSendChar(char c);    // return -1 if buffer is full
const int  uartSendString(const char* p, int len);

The UART itself operates at the interrupt level; we get distinct interrupts for rx data ready and tx buffer empty.

The uartSendChar() pushes a char into the tx buffer. The first character inserted causes the UART hardware to send; subsequent chars are simply pushed into the tx_buffer. When the UART is finished sending, it triggers an interrupt which checks the tx_buffer for more bytes and sends them, until the buffer is emptied.

Characters are inserted into the tx_buffer in order, and as long as the entire packet of data is inserted during the same task process (EVT_TICK or whatever), then those bytes will remain adjacent.

The rx_buffer is filled automatically by the rx interrupt. The rx interrupt also generates the EVT_CHAR message to inform all the tasks that rx data is ready.

The uartGetChar() checks the rx buffer, and returns a char if one is available (-1 otherwise). The uartCheckChar() and uartGetCharCount() let the designer check the first character of a command string, decide if it is relevant to a specific task, plus determine whether the command has been fully loaded into the rxBuffer.

Here is a fully functional module that implements the above:


#define TX_SIZE 256
#define RX_SIZE 32

/* circular buffers */

char tx_buffer[TX_SIZE];      
char *tx_next_empty = tx_buffer, *tx_next_send = tx_buffer;
const char * const tx_end = tx_buffer + TX_SIZE;


char rx_buffer[RX_SIZE];     
char *rx_next_empty = rx_buffer, *rx_next_rcv = rx_buffer;
const char * const rx_end = rx_buffer + RX_SIZE;
STM32F0xx specific init code ```C void initUart(void) { USART2->CR2 = 0; // no auto baud on USART2 :( USART2->CR3 = 0; // USART2->BRR = 0x341; // from 8MHz clock 16x oversample USART2->BRR = 0x44; // for 115200 USART2->CR1 = 0x016AD; // 8 bits, even parity, tx/rx/usart enable + tx/rx interrupts NVIC->ISER[0] |= (1<<28); // and enable the NVIC } ```
const char uartGetChar(void) {
    if (rx_next_rcv == rx_next_empty)
        return -1;      // no char available
    else {
        char c = *rx_next_rcv++;
        if (rx_next_rcv >= rx_end)
            rx_next_rcv = rx_buffer; // wrap to beginning
        return c;
    }
} 

const char uartCheckChar(void) {
    if (rx_next_rcv == rx_next_empty)
        return -1;      // no char available
    else 
        return *rx_next_rcv;
} 

int uartGetCharCount(void) {
    int ret = rx_next_empty - rx_next_rcv;
    if (ret<0)
        ret += RX_SIZE;
    return ret;
}

void uartDiscardChar(int i) {
    while (i--)
        uartGetChar();
}

/** add character to the tx buffer
 * @return -1 if the buffer is full
 * */
char uartSendChar(char c) {
    *tx_next_empty = c;             // increment AFTER adding to queue
    if (++tx_next_empty >= tx_end)
        tx_next_empty = tx_buffer;
    if (tx_next_empty == tx_next_send) 
        c = -1;
    // if the USART is empty, trigger the first send
    if (USART2->ISR & 0x80) 
        USART2->CR1 |= 0x80;            // enable the tx interrupt
    return c;
}

/** send a string a specified length out the uart */
void uartSendString(const char* p, int len) {
        while (len--)
                uartSendChar(*p++);
}       

/* ============ INTERRUPTS =================== */

/* the for a RX character
 * deposit it into the buffer, issue a EVT_CHAR and return
 */ 
void uart_rx_isr(void) {
  // rx data is ready, read it.....status bit auto-clears
  *rx_next_empty++ = USART2->RDR;
  if (rx_next_empty >= rx_end)  // wrap back to beginning
    rx_next_empty = rx_buffer;
  newEvent(EVT_CHAR);     // inform all tasks that there is 
                          // something in the rx buffer
}

void uart_tx_isr(void) {
    // the uart tx is empty
    // if there is tx data available to send, push it into the uart
    if (tx_next_send != tx_next_empty) {
      USART2->TDR = *tx_next_send;
      if (++tx_next_send >= tx_end) // wrap back to beginning
        tx_next_send = tx_buffer;
    }
    // else, just disable the interrupt
    else
      USART2->CR1 &= ~0x80;  
}