Silicon Labs Silicon Labs Community
silabs.com
Language
  • 简体中文
  • 日本語
  • MCUs
    • 8-bit MCU
    • 32-bit MCU
  • Wireless
    • Bluetooth
    • Proprietary
    • Wi-Fi
    • Zigbee & Thread
    • Z-Wave
  • More Products
    • Interface
    • Isolation
    • Power
    • Sensors
    • Timing
  • Development Tools
    • Simplicity Studio
    • Third Party Tools
    • Thunderboard
  • Expert's Corner
    • Announcements
    • Blog
    • General Interest
    • Projects
  1. Community - Silicon Labs
  2. Blog

Chapter 8.4: Communicating Asynchronously: Debug Printing

12/351/2015 | 01:49 PM
lynchtron

Level 5


makers-iot_-_background_8.4.png

 

 

Debug Print Messages

Now that you have the ability to send messages out of your MCU, you can build yourself a nice little library of print functions that you can monitor from an outside terminal.  This is a common practice in the industry to monitor an embedded device in real time. 

 

In the days before Simplicity Studio, I would use print statements in my code to debug what went wrong.  I didn’t have the luxury of breaking in during debug mode and inspecting the value of variables in my live embedded software.  The debugger has replaced my reliance on print statements for debugging, but I cannot easily monitor the overall summary of events with breakpoints and watch lists.  I still need to print things and monitor the progress from a computer that has full display capability.  Just be aware that print statements alter the timing of your code, so it can become a problem that things run differently when you try to observe them, similar to the “observer effect” of quantum physics!

 

Here is a way to create a print function with printf-like formatting as you may have come to expect in C programming on a desktop computer:

 

#include <stdio.h>
#include <stdarg.h>
void send_string(char * string)
{
      while (*string != 0)
      {
            if (*string == '\n')
            {
                  USART_Tx(USART0, '\r');
            }
            USART_Tx(USART0, *string++);
      }
}
 
void print(const char* format, ...)
{
    char       msg[130];
    va_list    args;
 
    va_start(args, format);
    vsnprintf(msg, sizeof(msg), format, args); // do check return value
    va_end(args);
 
    send_string(msg);
}

The use of the ellipsis or three dots and the va_list, va_start, vsnprintf, and va_end are all part of the C standard libraries stdio.h and stdarg.h that I learned once but since forgot why or how it works.  The end result is that you can have an unlimited number of arguments which are the printf formatters.  Note however that your message size is fixed in this implementation to no more than 130 characters!

 

This print function can then be used inside your main program, after a small startup delay. 

 

  for (volatile int i=0; i < 10000; i++)
        ;
 
  print("\n");
  print("Hello World %d!", 2015);

This should show up on your terminal emulator as Hello World 2015! on a new line.  All the while, the embedded system should continue echoing any typed letters to the screen.

 

Debug Print Messages with Buffering

There is a big problem with this simplistic print function.  The USART_Tx function blocks until the next byte can be sent on the serial link, so It will take some time to print out your messages and that could make your embedded device operate slowly when printing characters to the terminal.  It is better to create a buffer that will be filled by the send_string function, then implement the USART’s TX interrupt handler to process more characters whenever a byte completes a transfer on the serial link.  This way, your program can continue working as quickly as possible and the characters will be dumped to the terminal in the background whenever the USART’s TX buffers are empty.

 

The first thing that I will create is a data structure that contains everything that I need for a circular print buffer.  It is circular because I want to keep placing strings into the buffer and let it wrap around to the front of the buffer, so I need to keep track of the head and tail pointers of the buffer.  The head pointer points to the next index where a new character will be placed in the buffer.  The tail pointer points to the next index that will be printed by the interrupt handler.

8.4_ring_buffer.png

 

 

#define PRINT_BUFFER_SIZE     1024
typedef struct print_buffer
{
      uint16_t head;
      uint16_t tail;
      uint8_t data[PRINT_BUFFER_SIZE];
      bool now_printing;
} print_buffer_struct;
 
print_buffer_struct buffer = { .head=0, .tail=0, .data=0, .now_printing=false };

I picked an arbitrary size of 1024 for the size of my circular print buffer. If I print ten 130-count strings one after another, I might overrun the print buffer.  You have to be cognizant of the sizes of things and test until you find the right values.  On some of the smaller designs like Zero Gecko and Tiny Gecko, this buffer is looking huge!  It may not compile if you overrun the size of your available RAM.

 

Next, I create a helper function to deal with print buffer index overruns.  These are my number one cause of bugs, and there is no operating system to help out when you reference memory beyond the bounds of an array.  Therefore, I create these helper functions to help me deal with those conditions.

uint16_t fix_overflow(uint16_t index)
{
	if (index >= PRINT_BUFFER_SIZE)
	{
		return 0;
	}
	return index;
}

 

I then modified the send_string function to simply place characters in my circular buffer rather than send the characters out of the USART TX port.  Note that I do have to get the whole process started when the buffer is not actively being printed, but after that, the TX interrupt handler will take care of sending characters out when the last character has completed its transfer.

 

void send_string(char * string)
{
      while (*string != 0)
      {
            if (*string == '\n')
            {
                  //USART_Tx(USART0, '\r');
                  buffer.data[buffer.head++] = '\r';
                  buffer.head = fix_overflow(buffer.head);
            }
            //USART_Tx(USART0, *string++);
 
            buffer.data[buffer.head++] = *string++;
            buffer.head = fix_overflow(buffer.head);
      }
 
      // We need to kick off the first transfer sometimes to get things going
      INT_Disable();
      if (!buffer.now_printing)
      {
            buffer.now_printing = true;
            USART_Tx(USART0, buffer.data[buffer.tail++]);
            buffer.tail = fix_overflow(buffer.tail);
      }
      INT_Enable();
}
void USART0_TX_IRQHandler(void)
{
      if (USART0->IF & USART_IF_TXC)
      {
            // This flag is not automatically cleared like RXDATAV
            USART_IntClear(USART0, USART_IF_TXC);
 
            if (buffer.tail != buffer.head)
            {
                  USART_Tx(USART0, buffer.data[buffer.tail++]);
                  buffer.tail = fix_overflow(buffer.tail);
            }
            else
            {
                  buffer.now_printing = false;
            }
      }
}

I disabled interrupts in the send_string function because the interrupt handler modifies the buffer.now_printing flag and we have no idea when that interrupt handler is going to occur.  If the interrupt handler were to set buffer.now_printing to false right after the moment that the send_string function checked to see that the buffer.now_printing is true, then the logic to start the transfer would be missed, and data would sit in the buffer.data memory until the next print function call.  It would create a very intermittent print issue where some data is not seen on the console sometimes until some later print message shows up to get things moving again.  How frustrating!  By disabling interrupts momentarily and then enabling them again after the if block completes, I have created an atomic operation, which is an operation that will take place with the variables frozen in time until the block of code is done manipulating them.  Don’t forget to add #include “em_int.h” to the top of your file and add the em_int.c file to your emlib directory.

 

There is just a bit of code that I added in the main function to enable interrupts for the USART TX interrupts like we did for the RX interrupts.

 

 

  USART_IntClear(USART0, USART_IF_TXC);
  USART_IntEnable(USART0, USART_IF_TXC);
 
  NVIC_ClearPendingIRQ(USART0_TX_IRQn);
  NVIC_EnableIRQ(USART0_TX_IRQn);

One of these interrupts are for the module, and one is for the ARM core's NVIC.  This allows you to have multiple interrupts funnel into the same interrupt vector in the NVIC.

 

This will create a functional circular print buffer that takes very little time away from your embedded program.  Just be careful to pick the size of your print buffer and the number of print operations you do at once to avoid overrunning the buffer.  Also beware that strings are compiled into your program and consume a lot of memory.  The Wonder Gecko is available with up to 256k of flash memory, so you will probably be OK, but the strings can add up fast.   If you are working with a Zero or Tiny Gecko, you could have as little as 4k of flash at your disposal.  One trick that I use to reduce the size of my production code is create #define codes for my strings like so:

 

#ifdef RUN_DEBUG
#define HELLO_WORLD_STRING "Hello World %d!"
#ifdef
#define HELLO_WORLD_STRING
#endif
 
  print(HELLO_WORLD_STRING, 2015);

You could place all of your RUN_DEBUG strings in this section of a header file and likewise make the print statement an empty function and the compiler will optimize it the function away in release builds when RUN_DEBUG is not defined.

 

void print(const char* format, ...)
{
#ifdef RUN_DEBUG 
 
    char       msg[130];
    va_list    args;
 
    va_start(args, format);
    vsnprintf(msg, sizeof(msg), format, args); // do check return value
    va_end(args);
 
    send_string(msg);
 
#endif
}

 

Note on UART and LEUART Peripherals

All EFM32 models contain at least one USART, and some designs have up to three at your disposal.  But all of the models also contain at least one LEUART, which is a Low Energy UART, and some EFM32 models also contain a dedicated UART or two, which are less flexible than the USARTs since they have no option for synchronous mode. 

 

The UART is very similar in operation to the USART and actually uses the USART’s em_usart.h library.  The only difference is that the UART cannot perform synchronously.  You only need to change your USART0 reference to UART0 or UART1, and the code from this lesson will work for your initialization.

 

The LEUART is a little bit different from the USART and UART.  It has its own library API in em_leuart.h which operates very much the same way as the USART API.  The big difference is that LEUART is meant to be operated in low power energy modes as low as EM2, where the HF clock is unavailable.  A LF clock source limits your baud rate to 300 to 9600 baud, and you must use the LFXO crystal-based clock source for that to work.  The application note “AN0017 Low Energy UART” specifies that LFRCO is a choice for LEUART in order to operate your design in a low power mode without a crystal.  In my experience, I have simply not been able to get LFRCO to work at any baud rate, even after performing clock tree tuning algorithms.  You must use LFXO for low power operation.  I have had success running LEUART on HFRCO, but not all baud rates are available.

 

LEUART can be used as a full-speed UART in situations where simply more UART channels are needed.  If using HFXO as your source, you should expect similar performance to the UART and USART peripherals. 

 

The other difference with LEUART is that you need to configure the LFB clock with the LFXO or HFCORECLK/2 clock sources.

 

  CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_LFXO);

or, if using a HFXO or HFRCO for the HF clock:

  CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_CORELEDIV2);

The LEUART is not configured directly through the CMU_ClockSelectSet function, as it derives its clock from the LFB clock.

 

This wraps up the lesson on asynchronous communication. The hardest part is getting the baud rate that you want and then things are pretty basic from that point onwards.  The serial port interface doesn’t have layers upon layers of software stacks like seemingly all of the communication standards that erupted later, so things are relatively simple to follow. 

 

By the way, did you know that the EFM32 peripherals for asynchronous communication support single-wire operation?   You don’t always need a TX/RX pair.  If you have a dedicated host device that will always initialize the conversation with a dedicated client device, and the client device only speaks in response to a command from the host, you can use a single wire to communicate between two devices. This is known as half-duplex.  It also assumes a common ground of course.  Then again, if utilizing the IrDA mode which uses infrared LEDs and detectors, you can convert the electric pulses into light pulses, and then you don’t even need a single wire or ground connection between the devices.  These are just a few of the many capabilities offered by the uber-capable EFM32 MCUs.  I plan to cover some of these cases in future lessons.

 

PREVIOUS | NEXT

 

  • Blog Posts
  • Makers
  • erikm

    Pathfinder
    Level 7


    Replied Dec 23 2015, 5:40 PM

     Just be aware that print statements alter the timing of your code, so it can become a problem that things run differently when you try to observe them

     

    should have been in bold. I have heard many times "when I debug the problem disappear" and "when I debug the problem changes" In cases where I'm the leader print debugging is outlawed. The methods described above reduce the effect but do not remove it.

     

    if you need something other than breakpoints, use breadcrumbs

    0
  • vanmierlo

    Pathfinder
    Level 7


    Replied Dec 25 2015, 12:02 PM

    Allthough print statements can alter timing, hitting a breakpoint does so much harder. I consider debug printing very effective, esp. if you need to stay synchronized to another system.

     

    I have a problem with the implementation of the ring buffer though.

        buffer.data[buffer.head++] = *string++;
        buffer.head = fix_overflow(buffer.head);

    The buffer.head can be read by the ISR in between the two lines in an unfixed state. I would suggest:

        buffer.data[buffer.head] = *string++;
        buffer.head = fix_overflow(buffer.head+1);

    And you probably want that fix_overflow function to be inline.

     

    I also suggest to do something to prevent overflowing the ring buffer. Either drop incoming characters when there's no more space or wait for space to become available. Beware that the latter will stall your application just like before using a buffer.

     

    Then some final notes about the LEUART. Using the LFRCO for it is in general a bad idea, but it does work if you have a stable 3V3 supply and work at room temperature. If all you need from it is debug printing and have no LFXO on your board, this is feasable. But don't use it for your actual process. Also using the HFRCO for the LEUART works OK in my experience. We use it upto 921.6 kbaud. If you have no crystal and can take the energy consumption, use this. If you are going for the lowest energy consumption, you can even use DMA on the LEUART and drop the ISR. This way you only need to wake up on DMA interrupt instead of on every character sent.

    0

Tags

  • Wireless
  • High Performance Jitter Attenuators
  • EFR32MG21 Series 2 SoCs
  • Blue Gecko Series 2
  • Zigbee SDK
  • ZigBee and Thread
  • Internet Infrastructure
  • Sensors
  • Blue Gecko Bluetooth Low Energy SoCs
  • Z-Wave
  • Micrium OS
  • Blog Posts
  • Low Jitter Clock Generators
  • Bluetooth Classic
  • Makers
  • Flex SDK
  • Tips and Tricks
  • Smart Homes
  • IoT Heroes
  • Reviews
  • RAIL
  • Simplicity Studio
  • Mighty Gecko SoCs
  • Timing
  • Blue Gecko Bluetooth Low Energy Modules
  • Clocks
  • Ultra Low Jitter Clock Generators
  • General Purpose Clock Generators
  • Industry 4.0
  • Giant Gecko
  • 32-bit MCUs
  • blue-gecko-xpress-modules
  • Bluetooth Low Energy
  • 32-bit MCU SDK
  • Gecko
  • Microcontrollers
  • News and Events
  • Industrial Automation
  • Wi-Fi
  • Bluetooth SDK
  • Community Spotlight
  • Biometric Sensors
  • General Purpose Jitter Attenuators
  • Giant Gecko S1
  • Flex Gecko
  • Internet of Things
  • 8-bit MCUs
  • Isolation
  • Powered Devices

Top Authors

  • Avatar image Mark Mulrooney
  • Avatar image Siliconlabs
  • Avatar image Nari Shin
  • Avatar image lynchtron
  • Avatar image deirdrewalsh
  • Avatar image Lance Looper
  • Avatar image lethawicker

Archives

  • 2014 December
  • 2015 January
  • 2015 February
  • 2015 March
  • 2015 April
  • 2015 May
  • 2015 June
  • 2015 July
  • 2015 August
  • 2015 September
  • 2015 October
  • 2015 November
  • 2015 December
  • 2016 January
  • 2016 February
  • 2016 March
  • 2016 April
  • 2016 May
  • 2016 June
  • 2016 July
  • 2016 August
  • 2016 September
  • 2016 October
  • 2016 November
  • 2016 December
  • 2017 January
  • 2017 February
  • 2017 March
  • 2017 April
  • 2017 May
  • 2017 June
  • 2017 July
  • 2017 August
  • 2017 September
  • 2017 October
  • 2017 November
  • 2017 December
  • 2018 January
  • 2018 February
  • 2018 March
  • 2018 April
  • 2018 May
  • 2018 June
  • 2018 July
  • 2018 August
  • 2018 September
  • 2018 October
  • 2018 November
  • 2018 December
  • 2019 January
  • 2019 February
  • 2019 March
  • 2019 April
  • 2019 May
  • 2019 June
  • 2019 July
  • 2019 August
  • 2019 September
  • 2019 October
  • 2019 November
Silicon Labs
  • About Us
  • In the News
  • Email Newsletter
  • Cookies
  • Contact Us
  • Community
  • Site Feedback
  • Investor Relations
  • Blog
  • Privacy and Terms
  • Corporate Citizenship
Copyright © Silicon Laboratories. All rights reserved.
粤ICP备15107361号-1