Debug Interface

Prerequisite Modules

Getting data in and out of an embedded system is important. Sending data out to a computer is very useful for status, debugging and datalogging; taking information in is a great way to attach a much more powerful interface to the embedded system (e.g. monitor and keyboard) and can be used to control the system or send configuration data.

The RS-232 serial port was once very common on PCs and lives on through USB-to-serial adapters. The interface is essentially the “standard out” of the embedded world so we can create a “printf” style output function. The stdio function “printf” is actually an extremely complicated function to implement and since we are limiting our code footprint, the API offers very simple text and numeric output functions.

A terminal application like Tera Term is required to interface to the development board. The Debug serial interface uses 115,200bps, 8 data bits, 1 stop bit, no parity, no flow control. The serial port settings of the PC terminal application you use must match these values for the two systems to communicate.

Reading data in takes some work to properly receive, parse and buffer each character that comes in. The Debug driver is responsible for reading input data from the terminal. It monitors the input stream and will respond to certain input strings. A menu of available services will be displayed by typing “en+c00” from a terminal window. This special string is purposely a bit strange and sort of stands for “Engenuics command number.” It is defined this way to attempt avoiding the string occurring randomly if there was other data coming in from the terminal.

The debug driver is always watching for the special string. Any other text that is entered and terminated with a carriage return will produce a message “Invalid command” but that does not mean the input is lost. The Debug driver stores characters in a global buffer that can be read with a scanf style input function so other tasks can access the input.


Type Definitions
There are no type definitions unique to this API.

Globals
The following Globals may be imported into tasks:

  • G_au8DebugScanfBuffer[] is the DebugScanf() input buffer that can be read directly
  • G_u8DebugScanfCharCount holds number of characters in Debug_au8ScanfBuffer
  • DEBUG_SCANF_BUFFER_SIZE is the size of G_au8DebugScanfBuffer and thus the max of G_u8DebugScanfCharCount

Public Functions
The following functions may be used by any application in the system:

  • u32 DebugPrintf(u8* u8String_) – Queues the string pointed to by u8String_ to the Debug port. The string must be null-terminated. It may contain control characters like newline (\n) and linefeed (\f).
  • void DebugPrintNumber(u32 u32Number_) – Formats a long into an ASCII string and queues to print. Leading zeros are not printed.
  • void DebugLineFeed(void) – Queues a sequence to the debug UART.
  • u8 DebugScanf(u8* au8Buffer_) – Copies the current input buffer to au8Buffer_ and returns the number of new characters. When DebugScanf is called, the G_au8DebugScanfBuffer buffer is cleared and G_u8DebugScanfCharCount is 0.

Examples

Code the following in UserApp1Initialize() and step through it to see the results. Try to predict what you will see with each line.

Run the program and observe the terminal output. Did you predict it correctly?

Let the program run through to main. Once the main loop is running, enter “en+c00” in the terminal window to display the menu. Activate the LED test by typing “en+c01”. Then try typing the letters GRBB and see what happens on the dev board as you type. Press enter to clear the entry then type “en+c01” again to toggle the LED test off.

Lastly, try “en+c02” which activates the warning when a 1ms violation occurs. For the ASCII LCD development board, no warning messages should be printed. When you are developing your own tasks, this feature is very useful to check if your code is violating the 1ms time rule. To test this, add the following code to UserApp2Idle:

Rebuild the code and start it running. Watch the boot sequence in the terminal and enter “en+c02” when the prompt is ready. Immediately you should see the timing warning messages.

The debug driver increments a counter every time a 1ms violation occurs. Since the violation is occurring every 3ms, the output buffer in the debug driver fills up very quickly and the messages will stop printing properly. The use case for this functionality expects only occasional time faults to occur, so constant timing violations really break the system. You might also notice that the heartbeat LED is much brighter — this is because the system is much busier trying to send all the debug messages while at the same time being stuck in the UserApp2SMIdle() “for” loop.

Exercise

To demonstrate all of the features of the Debug API, we will code a program that can read input values from the terminal. The following code should be written in UserAppSM_Idle().

First, make BUTTON0 display the current number of characters in the scanf buffer (i.e. the value of G_u8DebugScanfCharCount). To access the Global variables from Debug.c, expose them at the top of user_app1.c

Set up some strings to be used to make the messages look good. Getting spaces, carriage returns and line feeds in the correct place tends to be the hardest part. As you saw with the 1ms error warning messages, there is a finite number of messages that can be queued to DebugPrintf(), so your application must allow some time for buffered messages to get processed by the Debug task.

Build and test the code to make sure it works as expected. Pressing BUTTON0 when the code finishes booting should show “0”. Type some characters and press BUTTON0 again. You do not have to press Enter as you are typing characters — as soon as you type the character, the terminal will send it to the EiE dev board.

Now add BUTTON1 functionality that will read the input characters to a buffer in user_app1 and print the contents. Make this buffer global to user_app1.c and initialize it in UserAppInitialize(). The constant USER1_INPUT_BUFFER_SIZE is in user_app1.h and is specifically defined as the Debug scanf buffer size +1 to allow us to add a ‘\0’ at the end for use with DebugPrintf().

user_app1.h:

user_app1.c:

Declare the input buffer:

Initialize the buffer when the task initializes:

Now we need to use the Debug API functions to read the current scanf buffer into au8UserInputBuffer and print out the contents when BUTTON1 is pressed. It’s important to note that “DebugScanf() returns the number of characters transferred. After DebugScanf() runs, au8UserInputBuffer will have this number of characters. The plan is to use DebugPrintf() to first print the message “Buffer contents:” and then print au8UserInputBuffer. Thanks to the API, all of this is just a few lines of code. Try to write this yourself and then compare to the solution below. Note there is an error in the solution. Did you make it, also?

To reveal the error, follow the steps below exactly:

  • Build and run the code.
  • After you see the boot sequence complete, press BUTTON0 which should show there are no chars in the buffer.
  • Type “BUTTON0 worked” but do not press the “Enter” key.
  • Press BUTTON0: how many characters should be there? The output so far should look like this:
  • Press BUTTON1 which should print out “Buffer contents: BUTTON0 worked” with a linefeed between the two and the cursor should be on a new line.
  • Type “TEST” and then press BUTTON0 followed by BUTTON1. You should see this:

What happened? BUTTON0 correctly reported that 4 characters were in the scanf buffer, but BUTTON1 which sends the au8UserInputBfferto DebugPrintf() correctly shows “TEST” but also incorrectly shows remnants of the previous message. This is because DebugPrintf () requires a NULL-terminated character array (aka a “C-string”). If you repeated this exercise but pressed “Enter” after each string you typed, the code would work fine. It would be a very easy assumption that a user would press “Enter” after entering a line of text, but relying on assumptions is extremely poor practice. Firmware you write needs to be bullet-proof, which means every case must be handled.

The code below safely adds the NULL to terminate the string. “Safely” means to handle the edge case where the buffer is full.

In this case, the “safety” has been provided by the definition for USER1_INPUT_BUFFER_SIZE which is the scanf buffer size plus 1, which means thebuffer array will always have a memory location available at the end. If not, you would need to check for a full array and overwrite the last character with a “NULL” so you wouldn’t write to memory outside of au8UserInputBuffer[]. That would have disastrous results one day depending on what that memory location was being used for.

Repeat the test steps above to show that the error in reporting the “TEST” string is fixed. Then write a lot of characters to totally fill up the buffer to make sure it works with a full scanf buffer. The buffer needs to have 128 characters to be full. When you are trying to type that many characters, the debug driver will complain every 64 characters if the “Enter” key is not pressed because it is also watching the input stream into a different buffer meant for commands. Even though errors will be reported, the scanf buffer is filling up independently. The terminal sequence to check everything is shown below.

The various errors are explored to provide some very important lessons. First, always remember that an embedded system will do exactly what you tell it to do. On a bare metal system, this often includes letting you read or write any memory address you attempt.

Second, code can appear to work in many cases, but bugs can emerge once the program has run for a while and more things have happened. This kind of error is absolutely classic, yet continues to plague developers.

Third, test every condition and every edge case as soon as code is running, at some point after running for a while, and after long term operation. The latter is not necessarily possible since development time is finite. However, it is highly recommended to keep a production device after the product is released and keep using it to continue long term testing.

Finally, notice that the more code you add, the more potential problems you add. For a challenge, use the debugger to follow through the Debug driver code to see how it reads in characters. There is a lot going on that you never get to see by using the API.

Module Exercise

Part 1: Write code to detect every time your name is typed on the input and count how many times it’s typed. You do not need to press enter – just monitor the input buffer. Make sure the code works when you repeat letters in your name. Make sure you disable the other task that monitors user input by calling the DebugSetPassthrough() API function.

Part 2: Each time your name is detected, print the current name count surrounded by a box of ‘*’ characters. Ensure the box changes size with the number of digits. Use BUTTON3 to multiply the current name count by 10 so that you can easily test all of the digits.

[STATUS: RELEASED. LAST UPDATE: 2023-NOV-10]