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.

Post History

60%
+1 −0
Q&A Declaring a callback function in usart.h, using it in usart.c but defining it in main.c

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 complic...

posted 6d ago by Olin Lathrop‭  ·  edited 6d ago by Olin Lathrop‭

Answer
#3: Post edited by user avatar Olin Lathrop‭ · 2026-09-11T15:26:27Z (6 days ago)
  • 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:<ul>
  • <li><b>uart_put</b>
  • 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.
  • <li>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.
  • <li>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 &micro;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.
  • <li><b>uart_get</b>
  • 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.
  • </ul>
  • 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.
  • <h2>About ring buffers</h2>
  • <h3>No send/receive mutex needed</h3>
  • 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 <a href="https://github.com/EmbedInc/dspic/blob/master/std.ins.dspic">STD.INS.DSPIC in the Embed DSPIC GIT repository</a>. 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:
  • <pre>
  • // 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
  • </pre>
  • <pre>
  • // 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
  • </pre>
  • 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.
  • <h3>Buffer size is a constant</h3>
  • 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
  • <pre>
  • mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index</pre>
  • 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 <code>[arg 1]</code>. 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
  • <pre>
  • fifow_get uart_in
  • </pre>
  • and <code>[arg 1]</code> is replaced with <code>uart_in</code> by the preprocessor.
  • The outer preprocessor function then becomes <code>[v fifow_uart_in_bufsz]</code>. The V function returns the value of a preprocessor constant or variable. In this case it returns the value of the constant <code>fifow_uart_in_bufz</code>. 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:
  • <pre>
  • // 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
  • </pre>
  • 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:<ul>
  • <li><b>uart_put</b>
  • 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.
  • <li>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.
  • <li>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 &micro;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.
  • <li><b>uart_get</b>
  • 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.
  • </ul>
  • 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.
  • <h2>About ring buffers</h2>
  • <h3>No send/receive mutex needed</h3>
  • 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 <a href="https://github.com/EmbedInc/dspic/blob/master/std.ins.dspic">STD.INS.DSPIC in the Embed DSPIC GIT repository</a>. 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:
  • <pre>
  • // 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
  • </pre>
  • <pre>
  • // 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
  • </pre>
  • 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.
  • <h3>Buffer size is a constant</h3>
  • 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
  • <pre>
  • mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index</pre>
  • 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 <code>[arg 1]</code>. 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
  • <pre>
  • fifow_get uart_in
  • </pre>
  • and <code>[arg 1]</code> is replaced with <code>uart_in</code> by the preprocessor.
  • The outer preprocessor function then becomes <code>[v fifow_uart_in_bufsz]</code>. The V function returns the value of a preprocessor constant or variable. In this case it returns the value of the constant <code>fifow_uart_in_bufz</code>. 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:
  • <pre>
  • // 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
  • </pre>
  • Note that the FIFO is actually allocated 1 word larger than the requested size (in the <code>/CONST</code> preprocessor command). This is to account for the one unused word mentioned before.
#2: Post edited by user avatar Olin Lathrop‭ · 2026-09-11T15:15:46Z (6 days ago)
  • 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:<ul>
  • <li><b>uart_put</b>
  • 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.
  • <li>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.
  • <li>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 &micro;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.
  • <li><b>uart_get</b>
  • 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.
  • </ul>
  • 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.
  • <h2>About ring buffers</h2>
  • 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 <a href="https://github.com/EmbedInc/dspic/blob/master/std.ins.dspic">STD.INS.DSPIC in the Embed DSPIC GIT repository</a>. 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:
  • <pre>
  • // 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
  • </pre>
  • <pre>
  • // 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
  • </pre>
  • 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.
  • 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:<ul>
  • <li><b>uart_put</b>
  • 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.
  • <li>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.
  • <li>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 &micro;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.
  • <li><b>uart_get</b>
  • 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.
  • </ul>
  • 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.
  • <h2>About ring buffers</h2>
  • <h3>No send/receive mutex needed</h3>
  • 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 <a href="https://github.com/EmbedInc/dspic/blob/master/std.ins.dspic">STD.INS.DSPIC in the Embed DSPIC GIT repository</a>. 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:
  • <pre>
  • // 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
  • </pre>
  • <pre>
  • // 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
  • </pre>
  • 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.
  • <h3>Buffer size is a constant</h3>
  • 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
  • <pre>
  • mov #[v fifow_[arg 1]_bufsz], w1 ;get first invalid buffer index</pre>
  • 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 <code>[arg 1]</code>. 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
  • <pre>
  • fifow_get uart_in
  • </pre>
  • and <code>[arg 1]</code> is replaced with <code>uart_in</code> by the preprocessor.
  • The outer preprocessor function then becomes <code>[v fifow_uart_in_bufsz]</code>. The V function returns the value of a preprocessor constant or variable. In this case it returns the value of the constant <code>fifow_uart_in_bufz</code>. 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:
  • <pre>
  • // 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
  • </pre>
#1: Initial revision by user avatar Olin Lathrop‭ · 2026-09-10T18:00:44Z (6 days ago)
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:<ul>

  <li><b>uart_put</b>

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.

  <li>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.

  <li>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 &micro;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.

  <li><b>uart_get</b>

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.

  </ul>

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.

<h2>About ring buffers</h2>

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 <a href="https://github.com/EmbedInc/dspic/blob/master/std.ins.dspic">STD.INS.DSPIC in the Embed DSPIC GIT repository</a>.  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:

<pre>
//   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
</pre>

<pre>
//   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
</pre>

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.