Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

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

+4
−0

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.

History

1 comment thread

Why callback function? (1 comment)
Post
+3
−0

Indeed this is not a good design since it doesn't use private encapsulation. It is using a global variable which means it is pretty much by definition spaghetti code.

I don't see why you would use callbacks in this case either, since the copying of data is no business of the program outside the driver (unlike lets say a timer driver where it can make perfect sense).

For a normal project you'll probably want to separate everything in 3 separated parts:

  • The raw "dumb" reception of data through a MCU-specific hardware peripheral. uart.h + uart.c
  • The protocol decoder that knows about the nature of the data - the protocol, but not necessarily anything about the specific hardware peripheral driver. Either it calls the driver to ask for data, or data is passed between the driver and the protocol handler by a middle man ie some application-tier code from main().
  • The application that uses the payload of the packet but otherwise doesn't care less about the protocol format, let alone the underlying UART hardware.

For certain smaller projects or hard real-time projects, you might want to merge the driver and protocol handling into one though. In case you want to instantly reject something upon reception, or instantly act on something etc. So in hard real-time projects you might want to decode the protocol on the fly as it is getting received.


As for how to design the driver and ring buffer:

  • The interrupt and ISR should be internal to the driver uart.c and how it moves data from hardware buffers to RAM is no business of the rest of the code. That's an entirely internal affair inside the UART driver.

    Depending on system, other parts of the program may need to register the ISR in a vector table etc, but that's about it. ARM systems typically solve this by having default ISRs with "weak linkage", meaning that if you define a ISR with the same name then it takes precedence and that's resolved at link-time. Then everything about the ISR can be encapsulated. You should document that the driver comes with a live ISR though.

  • The UART driver can declare the ring buffer type internally or otherwise include a separate ring buffer ADT. It is common that a ring buffer struct knows specifics about the hardware peripheral so that the safe access of data shared with an ISR can be handled from the ring buffer itself.

    For example if you know the baudrate in advance then you know how long a byte transmission takes and there's probably plenty of time to copy that byte until the next one arrives, meaning that you can temporarily disable interrupts while copying in order to protect against race conditions.

    An example of a generic ring buffer from an old project of mine, where the ring buffer has the possibility to set/clear interrupt flags through int_mask in an 8-bit register int_reg:

    typedef struct
    {
      void*              buffer;
      uint16_t           objects_n;
      uint16_t           object_size;
      uint16_t           object_count;
      uint8_t*           begin;
      uint8_t*           end;
      volatile uint8_t*  int_reg;
      uint8_t            int_mask;
      uint8_t*           end_of_buf;
    } rb_buf_t;
    

    This is rather over-engineered though, you might not need to make it this advanced and type-generic - this is from a gateway type of project that handled multiple UART and CAN peripherals all at once.

    But you should probably at least consider storing the size of valid data in the ring buffer struct, so that you can grab the size quickly, and so that you can tell the difference between a completely full and completely empty buffer.

  • From inside uart.c you have a place in RAM where the received data is stored. Either it is the ring buffer, or it could be another buffer not connected to the ISR ("double-buffering"), if you for example want to separate the packet getting received right now from the previously complete packet.

    You can chose to expose this internal RAM buffer/ring buffer to the caller, but that should be a read-only access then, since no other part of the program has any business writing to the UART rx buffer.

  • If you don't solve race condition bugs by enabling/disabling the specific interrupt, then yeah you can do it with a "poor man's semaphore" ie static volatile bool. Poor as in: in itself it is not guaranteed to be thread-safe/interrupt safe like the higher level concept of semaphores in an OS. No access in C code is otherwise ever atomic unless you actually use C11 atomic types.

    The idea behind this is that the access to the "poor man's semaphore" bool won't be atomic either, it might get interrupted right in the middle of updating the value. But we don't care, the data is either 1 or 0 and so data corruption isn't going to happen. We only care if the data transfer we are trying to protect gets interrupted while updating. The poor man's semaphore is always either on/off and at that point we can access the data. Basically we move the race condition problem away from the data to this bool, where it doesn't cause any harm. volatile variable access is not allowed to be re-ordered either.

    Some more info about volatile and "poor man's semaphore" here: Using volatile in embedded C development (In fact that answer probably answers this whole question in itself?)

  • DMA is often far less of a head ache to work with than this "old school" interrupt approach, and it is not nearly as interrupt-intense. So if your MCU supports DMA then give it some serious consideration.

History

1 comment thread

Ring buffer can be designed to not require read/write mutex. (4 comments)
Ring buffer can be designed to not require read/write mutex.
Olin Lathrop‭ wrote 6 days ago

A ring buffer (FIFO) can be designed so that no mutex is needed between reading and writing. See the "About ring buffers" section of my answer.

Also, the size of a FIFO is usually known at build time, so there is no need to store it in RAM. I'll add a note to how that is done in the FIFO macros I show, because it's not obvious at first glance.

Lundin‭ wrote 3 days ago

Olin Lathrop‭ The "mutex" is rather a feature in this case, meaning that the caller won't need to worry about it manually. Anyway, the question and this answer is about C. If you write assembler then obviously you have a whole lot more options, including guaranteed atomicity of operations, which is always the case of simple MCUs. (Advanced pipe-lining or multicore ISA might not necessarily always give such guarantees.)

Olin Lathrop‭ wrote 3 days ago

The method I show can be used in a higher level language too, as long as execution is sequential such that code is not moved between before and after dereferencing a pointer. That pretty much has to be the case since the compiler can't assume much about what is being accessed thru the pointer.

Lundin‭ wrote 3 days ago

Olin Lathrop‭ The problem is as illustrated in the linked post to EE.SE - C cannot guarantee atomicity unless you specifically use atomic language features from C11. And the resulting race condition bugs aren't necessarily always of a data corruption nature, it may also manifest itself as in the program using old data from before the most recent interrupt.