2025/01/30
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.
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;
void initUart(void) {
->CR2 = 0; // no auto baud on USART2 :(
USART2->CR3 = 0;
USART2// USART2->BRR = 0x341; // from 8MHz clock 16x oversample
->BRR = 0x44; // for 115200
USART2->CR1 = 0x016AD; // 8 bits, even parity, tx/rx/usart enable + tx/rx interrupts
USART2->ISER[0] |= (1<<28); // and enable the NVIC
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_buffer; // wrap to beginning
rx_next_rcv 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)
+= RX_SIZE;
ret 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_buffer;
tx_next_empty if (tx_next_empty == tx_next_send)
= -1;
c // if the USART is empty, trigger the first send
if (USART2->ISR & 0x80)
->CR1 |= 0x80; // enable the tx interrupt
USART2return c;
}
/** send a string a specified length out the uart */
void uartSendString(const char* p, int len) {
while (len--)
(*p++);
uartSendChar}
/* ============ 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_buffer;
rx_next_empty (EVT_CHAR); // inform all tasks that there is
newEvent// 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) {
->TDR = *tx_next_send;
USART2if (++tx_next_send >= tx_end) // wrap back to beginning
= tx_buffer;
tx_next_send }
// else, just disable the interrupt
else
->CR1 &= ~0x80;
USART2}