Simple printf for SAM microcontrollers

print.c/h is a simple DMA based UART driver for debugging on Atmel SAM series microcontrollers. It includes functions for printing decimal, hexadecimal and strings. If you don't have an in-circuit debugger, using the UART for debugging is the next best thing and usually all you need.



This is not a traditional printf(). You have to call separate print functions for each output format you want to print. e.g. prints("My age is "); printi(78, '\n');

Setup with your project

  1. Define PF_DEV to equal the UART/USART you want to use. eg. UART0, USART0, USART2....
  2. Define PF_ID_UART to equal the ID for the UART/USART you selected above. eg. ID_UART0, ID_USART0, ID_USART2....
  3. Define BAUD to be your desired baud rate. eg. 115200
  4. Define MCLK to equal the peripherial clock rate
  5. Make sure to assign the IO port to the UART your are using.
  6. Uncomment PF_INT or PF_HEX or both if you want to print decimal ints or in hex. String printing can not be disabled.
  7. Define HANDLE_FULL - how txbytes() behaves when the buffer is full.
            PF_BLOCK       -> blocks until the buffer has enough room.
            PF_RETURN      -> returns if there is no room on the buffer.
            PF_REMAINING   -> puts just what it can fit on the buffer.
  8. In your initialization / startup, add a call to setup_print_uart()

Using the print functions

Function Size Description 1st parameter 2nd parameter
setup_print_uart() 58 Sets up the UART/USART
txbytes 204 Copies a string/data to the DMA buffer to be sent Pointer to the string to print length
prints Macro to txbytes() - puts in the length Pointer to the string to print
printi 132 prints a signed integer in base 10 int32_t to print append character
printx 148 prints a unsigned integer in hexadecimal uint32_t to print append character

Notes Example
Code Output
printi(x++, '\t');
printi(x++, '\n');
printx(x++, '\t');
printx(x++, '\n');
14      15
0x10    0x11

Tested on


How it works

The main motivation for publishing this was to show the simplicity of the Atmel Sam's PDC (Peripheral DMA Controller) with a circular buffer. Each PDC enabled peripheral has its own PDC channel so there is minimal setup to use the PDC. There is one register (TPR) that points to the buffer in memory and another register (TCR) that holds the remaining count of bytes to transfer. When the UART is signalling for another data byte, if TCR does not equal zero, it moves the data from where TPR points to, to the UART tx data register. TCR is decremented and TPR is incremented. For example, if you wanted to transfer a string 10 bytes long, you would copy the string to the buffer, set TPR to point to the first byte in the buffer, set TCR to 10 and enable the PDC TX.

To get the PDC to work nicely with a circular buffer, we also need to use TNPR and TNCR (next pointer and next counter) register. TNPR and TNCR are not incremented or decremented like TPR and TCR are. When TCR is about to decrement to zero, the hardware copies TNPR to TPR and TNCR to TCR and sets TNCR to zero. This lets you "queue up" a second transfer.

Function txbytes() is used to add data to the circular buffer. There are four cases to consider when adding to the buffer....

  1. adding when no transfer active
  2. adding during at transfer
  3. adding during at transfer and the circular buffer wraps
  4. adding during at transfer after the circular buffer has wrapped
Cases 2-4 are for when a transfer is already in progress. Before any coping takes place, if TCR is zero then no transfer is active (case #1), variable in is set to zero and TPR is set to the beginning of the buffer. This simplifies handling the other three cases.
        if (!PF_UART->UART_TCR) {
               in = 0;
               PF_UART->UART_TPR = (uint32_t) tx_buff;

The variable length is the length of the data to add to the buffer. At the very beginning of txbytes() the "HANDLE_FULL" behaviour makes sure length is never bigger than the remaining size of the buffer. Variable in is the buffer fill position and is a static uint8_t. The array tx_buff is the circular buffer and is 256 bytes so that in naturally wraps to the size of tx_buff. The code to add data to the buffer is the same for all four cases....

        while (length--)
               tx_buff[in++] = data[i++];

Case 1 & 2
Variable i ends up being the same as the initial value of length. Since length now equals zero, i is now used as the reference for the length of data copied. In case #1, no transfers are active (TCR will be 0). For case #1, TCR just equals i. Since TCR equals zero to start, TCR+= i can be used as it works nicely for case #2 as well. For case #2, if the PDC had 7 more bytes to transfer when the transfer was disabled (TCR = 7), three bytes are added (i=3), with TCR+= i, TCR is now 10.

Case 3
remaining is defined to be the free space on the buffer, front and back. What is meant by front and back....if a transfer is not active and 56 bytes are copied to the buffer (TCR = 56), there is room for 200 bytes on the back. Another example, if the transfer is stopped after 10 bytes have been transferred (TCR now = 46), then there are 10 bytes free on the front. remaining would equal 256 - 46 = 210. With the transfer still disabled, if another 205 bytes are now copied to the buffer, 200 would be put on the back and the last 5 on the front. The PDC must now transfer the 46+200 bytes on the back and also the 5 bytes on the front. in would have wrapped and would now equal 5 as well. TNCR needs to equal the size of the front. TNCR is set to in. out is defined as (TPR - tx_buff) and works out to be the index of tx_buff where the PDC is taking bytes off the buffer. The size of back works out to be 256 - out.

So when in wraps around, TNCR = in and TCR = 256 - out, otherwise TCR+= i.....

        if (in < out || !(remaining-i)) {                   //cases 3 & 4
               REG_USART0_TCR = PF_TX_SIZE - out;
               REG_USART0_TNCR = in;
        else                                           //cases 1 & 2
               REG_USART0_TCR+= i;

There is one problem...if in the above case 3 example the buffer was completely filled (210 bytes were copied instead of the 205) in would equal out (10) and the wrong code to set TCR would execute. !(remaining-i) is in the if statement to correct for this situation. When the buffer is completely full remaining-i equals zero, which triggers the right code to set TCR and TNCR. There is one catch....there is another time when remaining-i equals zero. If the buffer was completely filled in case #1, then out = in = zero. Fortunately because in equals zero, TNCR will get set to zero and because out also equals zero, TCR will get set to 256, so no harm done.

Case 4 is mostly the same as case 3, but TCR does not need to be set, only TNCR to the new value of in. Setting TCR anyway doesn't hurt anything and makes the code simpler.



Content is available under CC-BY-SA.