Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Comments on Declaring a callback function in `usart.h`, using it in `usart.c` but defining it in `main.c`
Parent
Declaring a callback function in usart.h, using it in usart.c but defining it in main.c
I have written a generic device driver for a uart peripheral with a source file usart.c and a header file usart.h. I don't want a user of these files have to tamper with them.
I want to use the usart peripheral in an interrupt driven fashion, where I store the incoming byte in a ring-buffer, but I'm a unsure of the correct way to do it. I am thinking of doing it like below, by declaring the callback function in the header file, using it in the source file, and then defining what the callback function does in main.c.
/* usart.h */
...
void usart_callback();
...
/* usart.c */
#include "usart.h"
uint8_t usart_recieve()
{
/* ... */
}
ISR(USART_RX_vect) //Interrupt automatically issued when new byte in UDR0 register
{
usart_callback();
}
/* main.c */
#include "usart.h"
#define RB_SZ 32
volatile bool usart_semaphore = false;
static struct usart_rb_t
{
uint8_t buf[RB_SZ];
volatile uint8_t head;
volatile uint8_t tail;
} usart_rb;
void rb_put(uint8_t val);
void usart_callback()
{
rb_put(UDR0); //Insert byte into ring-buffer.
}
int main(void)
{
for(;;)
{
/* Program execution */
}
}
But I have a gut feeling that this is wrong. It feels wrong that I handle an intrinsic uart construct UDRE0 in main.c when it feels like it belongs in usart.c. It also feels weird to me to define a function in main.c but the place it is called is in usart.c. Almost like spaghetti. I have also prematurely defined a semaphore construct to guard against a possible race condition but have not yet figured out how to apply that correctly either.
Post
I see you already have some general answers, so I'm going to pick on a few details.
Unless you're doing something unusual, don't use a callback function. Callback functions are in general complicated and prone to unconsidered details. For example, who's stack is current when the callback function is called? If it gets called in an interrupt routine, then it has to complete "quickly" to avoid more latency in other interrupts. You also don't know what stack and therefore how much stack space is available in the callback routine.
A good basic software interface to a hardware UART is four routines, all in the UART-specific module:
-
uart_put
Called by the application to send one byte out the UART. This routine actually stuffs the byte into the output ring buffer and makes sure the UART transmit interrupt is enabled. If the ring buffer is full, it blocks until it is empty.
- UART transmit interrupt routine.
Grabs the next byte from the output ring buffer and writes it to the UART. Disables UART transmit interrupt if the ring buffer is now empty.
- UART receive interrupt routine.
Gets the received byte from the UART and writes it to the input ring buffer. The only trickiness is what to do when the ring buffer is full. The simplest answer is to drop the byte and let the higher level protocol deal with it. If the UART is overrunning the firmware's ability to handle received characters, then something else is already seriously wrong. You might set an error flag too.
The fastest common and standard baud rate is 115.2 kBaud. With 10 bit times per character, that's 11.52 kbytes/s, which is one byte every 86.8 µs. That's a long time for a modern micro. At 50 MHz instruction rate, for example, that's 4340 instruction cycles.
If processing the UART input is bursty, you need to make sure the input ring buffer is long enough. To allow for a whole 10 ms of unprocessed input bytes, for example, requires only 115 bytes in the ring buffer.
If you really can't handle UART data at the maximum for the baud rate, then you need to consider a lower baud rate or flow control in the higher levels of the protocol. For most of the microcontroller projects I've done, 115.2 kbaud with a modest receive buffer is plenty to guarantee no overrun.
-
uart_get
Called by the application to get the next UART input byte. It blocks until there is a byte available, then removes it from the input ring buffer and passes it back to the caller.
Those are the basics that are good enough for quite a few projects. Note that "blocking" doesn't necessarily mean sitting in a busy-loop. Usually, especially when there is asynchronous input like from a UART, there is a multi-tasking system in use. Blocking means calling TASK_YIELD in a loop until it is possible to proceed. The task that is sending or receiving UART data will stall, but the rest of the system will keep running. When tasks are properly designed, there is nothing else for them to do until the UART operation completes.
Added bells and whistles include routines the application can call to find whether UART_PUT and UART_GET can complete immediately, whether all the buffers are empty and the UART completely idle, etc.
In general, it is good to dedicate a separate task for each asynchronous data input stream. A good example is a command processor. It is an infinite loop that gets the next UART input byte as a command opcode and vectors to the command routine for that opcode. Each command routine gets whatever data bytes go with the command, then returns back to the start of the main loop. A new byte not being available stalls this process until the byte is available. As long as the micro with the multi-tasking system can keep up averaged over the maximum input buffer time, everything keeps working.
About ring buffers
No send/receive mutex needed
I see Lundin mentioned disabling interrupts around accesses to the input and output buffers while state may be inconsistent. You can do that, but it is possible to create ring buffers (FIFOs) that don't need that protection.
The trick is to use a PUT and GET offset into the buffer. The PUT offset is for the buffer entry where the next data will be written. GET is the index where the next data will be read from. If you define the buffer to be empty when PUT = GET, then iterrupts do not need to be disabled as long as the indexes are updated in the right order on both reading and writing.
As an example, see my FIFOW_xxx macros in STD.INS.DSPIC in the Embed DSPIC GIT repository. The FIFO for 16 bit data starts on line 4139. There is also a FIFO for 8 bit data, but it is older and less well documented. In particular, here are the two macros that put and get data into and out of the FIFO:
// Macro FIFOW_PUT name
//
// Write the word in W0 to the named FIFO. It is the caller's responsibility
// to ensure the FIFO has room for the new word. Invoking this macro with the
// FIFO full makes a mess.
//
// Trashes: W1, W2
//
/macro fifow_put
mov #fifow_[arg 1]_buf, w1 ;point to start of buffer
mov fifow_[arg 1]_put, w2 ;get PUT word index into buffer
add w1, w2, w1 ;add byte offset to where to write the word
add w1, w2, w1
mov w0, [w1] ;write the word into the buffer
add #1, w2 ;make raw new PUT index
mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index
cp w2, w1
skip_ltu ;still within the buffer ?
mov #0, w2 ;no, wrap back to start of buffer
mov w2, fifow_[arg 1]_put ;update the PUT index
/endmac
// Macro FIFOW_GET name
//
// Get the next word from the named FIFO into W0. It is the caller's
// responsibility to ensure there is a word in the FIFO to read. Invoking
// this macro on a empty FIFO makes a mess.
//
// Trashes: W1, W2
//
/macro fifow_get
mov #fifow_[arg 1]_buf, w1 ;point to start of buffer
mov fifow_[arg 1]_get, w2 ;get GET word index
add w1, w2, w1 ;add byte offset to where to read the word from
add w1, w2, w1
mov [w1], w0 ;read the word from the FIFO buffer
add #1, w2 ;make raw new GET index
mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index
cp w2, w1
skip_ltu ;still within the buffer ?
mov #0, w2 ;no, wrap back to start of buffer
mov w2, fifow_[arg 1]_get ;update the GET index
/endmac
Note how the order of checks and operations guarantee there are no race conditions between reading and writing.
This method "wastes" one FIFO entry, since there is always at least one empty entry. However, this type of FIFO only uses two state variables other than the data values themselves. Any other scheme that allows for the FIFO buffer to be completely full would require another state variable. Unless FIFO entries are larger than the state variables, there would be no savings.
Buffer size is a constant
Another point to note is that the size of the buffer is not stored in RAM. It is fixed at build time, so will always be the same value at run time. For example, examine the first parameter to the MOV opcode in the line
mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index
of the FIFOW_GET macro, above. Expressions in brackets are preprocessor functions. The first parameter is therefore two nested preprocessor functions, preceeded by "#". The "#" indicates a literal value following. In this case, the result of the literal value is loaded into register W1.
The inner preprocessor function is [arg 1]. That expands to the first argument passed to the macro, which is the name of the FIFO. For example, if the FIFO was called "uart_in", then the macro would be invoked
fifow_get uart_in
and [arg 1] is replaced with uart_in by the preprocessor.
The outer preprocessor function then becomes [v fifow_uart_in_bufsz]. The V function returns the value of a preprocessor constant or variable. In this case it returns the value of the constant fifow_uart_in_bufz. That constant was created and set to the number of slots in the FIFO buffer when the FIFO was created by the FIFOW_DEFINE macro. Here is that macro:
// Macro FIFOW_DEFINE name, size // // Define a word (16 bit data) FIFO. NAME will be used to create unique // symbols for this FIFO. All these symbols have the form FIFOW_name_xxx, // where XXX refers to particular symbols. All the interactions with the FIFO // via the macros here are only by using NAME. The various symbols created // and the exact details of the FIFO data structure and read/write protocol // should be considered private to these macros. Put another way, a FIFO // should only be accessed thru the macros here. // // NAME is the name characters directly, not a string. // // SIZE is the maximum number of words the FIFO must be able to hold. // /macro fifow_define /const fifow_[arg 1]_bufsz integer = [+ [arg 2] 1] ;buffer size, words alloc fifow_[arg 1]_put alloc fifow_[arg 1]_get alloc fifow_[arg 1]_buf, [* fifow_[arg 1]_bufsz 2] /endmac
Note that the FIFO is actually allocated 1 word larger than the requested size (in the /CONST preprocessor command). This is to account for the one unused word mentioned before.

1 comment thread