Introduction to Embedded Programming

Lab 6: Embedded Programming

Overview of Lab

The objective of this laboratory is to learn how to create a C program for an embedded processor – in this case the stm32l476, download that program to a development board, the Nucleo-l476rg, and communicate with the program from a host computer.

  1. You will begin with a “canned” program (echo) that you will build and download with make from within vscode. You will then communicate with this program through a small program running in a terminal window.

  2. Next you will create a program (shell) that can execute simple commands.

  3. You will integrate shell with node-red.

The goal of this sequence of programming assignments is to learn about the process of creating embedded programs, integrating an embedded device into a larger system, and a bit about the software resources available for embedded program development.

In future labs you will learn how to communicate with external sensors and create working systems with these resources.

Deliverables

In this lab you will create two separate embedded programs and a node-red configuration to interact with them. The skeleton directory for this lab has the following form (the subdirectory contents have been elided):

.
├── cfg
│   └── ... elided ...
├── config.mk
├── echo
│   └── ... elided ...
├── make.mk
└── shell
    └── ... elided ...

As before, your report should document your efforts and describe any code that you created. The report does not need to be verbose, but should be a coherent snapshot of your work and any difficulties/issues that arose.

The grading rubric for this lab is

  • Completion of tasks 60%
    • Building and working with the echo program: 10%
    • Simple shell : 25%
    • Node Red integration : 25%
  • Lab report 40%
    • Overview 10%
    • Explaination of your code 20%
    • Explaination of node-red with screen shots 20%
    • Issues : 10%

Background

There are a number of essential differences between writing a C program for a host and writing one for an embedded processor. Most importantly, the target environment is seriously impoverished; embedded processors are resource starved – they generally have limited memory, limited compute power, and, for battery powered devices, limited energy. A typical Cortex-M processor has 8-64KBytes of RAM and between 16-512K Flash memory. This greatly restricts the use of conventional software libraries – the standard C library is too large for most applications. While some embedded processors have hardware floating-point support, in general these are not suitable for complex computations such as image processing or machine learning. These resource limits are all about cost – for many applications pennies in cost are the difference between profit and loss. In addition, many embedded applications involve battery power; computation, communication, and storage all consume significant amounts of energy. Thus, embedded programs generally have to be designed to minimize energy use.

Another consequence of the memory limitations is that our applications will not have access to a “real” operating system, rather we will be restricted to a thread package. Operating systems require a lot of resources and hardware support for virtual memory. You may be familiar with the Raspberry Pi single board computers which run a version of Linux. These have 512MB-1GB of RAM and external flash cards, and execute at 1 Ghz. In addition to being (relatively) expensive, these boards are energy hogs – requiring Watts of power vs. microWatts for typical embedded processors.

A final difference is in the development environment. For embedded processors, we must “cross-compile” the program on a host with a different processor architecture and, as a separate step, download the code to the processor in order to execute it. These extra steps complicate the “build process”. For the ARM processors there is a really good open source GCC based ARM toolchain. In contrast with the C environment on OS X, this tool chain supports all ARM processors and hence we must provide a lot of additional configuration information in order to compile a program. This configuration information includes compiler options and a dedicated linker script for use in generating the target binary. To simplify the process of building and downloading programs we will use a Makefile to automate the process.

Hardware Environment

In this class we will use a development board produced by ST Microelectronics - the Nucleo-L476rg. This board, illustrated in the following figure, is one of a series of similar boards designed to jump-start software development with a family of stm32 processors.

The board consists of two parts. The larger, bottom half, has the processor, a few support components, and large expansion connectors. The smaller, upper half, has an additional processor and USB connector. This second processor provides the communication interface (called ST-LINK) through which the target processor can be programmed and debugged, and through which the target processor can communicate with a host. The conceptual architecture of this board is illustrated as follows:

Notice that the board has a single user button (B1), and a single user LED (LD2) that you will use in this lab for output (we’ll use B1 in a future lab). The remaining processor “pins” are uncommitted and are brought out to two sets of connectors. The inner, single row connectors are compatible with the pinout of Arduino Uno boards – a popular if somewhat primitive development board. The outer, dual row connectors provide access to the full set of processor pins. In later labs we will add an expansion board that uses the Arduino connectors to provide access to a number of sensor chips.

Microcontrollers in general, and the STM32 family in particular, have exceptionally configurable hardware. The microcontroller pins can be configured to support a wide variety of I/O devices. To get a sense of this configurability, the following figure illustrates some of the many functions for which the various pins may be used. The different colored boxes indicate different functions. In future labs we will discuss some of these functions.

  • Complete documentation for the Nucleo boards is found here, although you do not need to read this now.

  • The processor that we are using in this class is the stm32l476rg processor. This is a sophisticated 32-bit Cortex-M4 based microcontroller with a large number of I/O peripherals.

Software Environment

For developing software for the Nucleo board, we will use the ARM gcc toolchain, and a small RTOS (real-time OS) called ChibiOS. ChibiOS is an open-source real-time thread package with an excellent set of hardware libraries for the STM32 devices. ChibiOS is notable in its configurability – through a set of configuration files, the resources required are selected and only the necessary support code is linked into a binary. It is possible to create extremely small ChibiOS applications.

There are other open-source RTOS packages that I could have selected – the most notable are mbed and a recent contender is zephyr. ChibiOS has the advantage of being a relatively compact distribution that requires few other tools.

In order to program and debug the embedded processor we will use a package called openocd. This package supports both a wide range of target processors and programming devices such as the ST-LINK interface provided on the Nucleo board.

2. Building and Executing a “Canned” Program

The first program you will build, echo, is fully defined in your lab6 template, and has the following directory structure.

echo
├── makefile
├── project.mk
└── src
    └── main.c

The directory contains, a Makefile, a project specific .mk file, and a single source file, main.c. The configuration files in ../cfg define all of the ChibiOS options that we will use in this lab. As mentioned, ChibiOS is very configurable; this configurability is implemented with the C preprocessor using definitions provided in these three “.h” files. The Makefile plays another important role in the configuration of ChibiOS – again, you can ignore this for now.

The main.c file is displayed (in slightly reformated form below). This program implements two threadsblinker, which blinks the single user LED, and a main thread that echoes characters received on a serial port.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static THD_WORKING_AREA(waThread1, 256); // blinker thread stack
static THD_FUNCTION(Thread1, arg) {      // blinker thread code
  while (true) {
    palSetLine(LINE_LED_GREEN);          // LED on
    chThdSleepMilliseconds(500);         // sleep 0.5 seconds
    palClearLine(LINE_LED_GREEN);        // LED off
    chThdSleepMilliseconds(500);
  }
}

int main(void) {
  halInit();                             // initialize ChibiOS
  chSysInit();
  sdStart(&SD2, NULL);        

  // create blinker thread

  chThdCreateStatic(waThread1,           // stack pointer
                    sizeof(waThread1),   // size of stack
                    NORMALPRIO,          // thread priority
                    Thread1,             // thread code
                    NULL);               // thread argument

  // main thread

  while (true) {
    uint8_t c = sdGet(&SD2);  // read character
    sdPut(&SD2, c);           // write character
  }
}

The code defining the blinker thread is in lines 1–9 (the #include directives are omitted). A thread is defined by two components – a region of memory for the thread stack, and a routine to execute. As with many thread packages (e.g. pthreads), the thread function takes a single, optional argument. This code makes use of a number of ChibiOS library functions which we will not define for the moment – understanding the comments is sufficient for now.

The main routine, which is called when the processor “boots” from reset, initializes the ChibiOS system consisting of the “hardware abstraction layer” (HAL) and the underlying thread package (ch) (lines 12-13). It then initializes the serial port (line 14), creates the blinker thread (lines 18-22) and begins echoing characters (lines 26-28).

One notable aspect of the blinker code is the direct connection between I/O lines (pins on the processor) and external devices – in this case the LED. If you were to track down the definition of LINE_LED_GREEN, you would find that it is connected to processor pin PA5 which is electrically configured to “drive” the LED (light emitting diode). This can further be traced to the circuit associated with PA5
If this circuit makes no sense to you – don’t worry, we won’t be looking at things at such a low level in this class. However, if you are interested, come talk to me in office hours and we can discuss this path from software to circuits as far as you care to go.

Compiling and downloading Echo

  • Connect your nucleo board through a USB cable to your computer
  • In a terminal ‘cd’ to the echo directory
  • Execute make at the command line.

This will generate a lot of output as every file in ChibiOS is compiled (although many will be empty)

$ make 
Compiler Options
  ... more output elided ...
Compiling crt0_v7m.S
Compiling vectors.S
Compiling chcoreasm_v7m.S
  ... more output elided ...
Linking build/ch.elf
Creating build/ch.hex
Creating build/ch.bin
Creating build/ch.dmp

   text	   data	    bss	    dec	    hex	filename
  11620	      0	  65536	  77156	  12d64	build/ch.elf
Creating build/ch.list

Done

The make process creates two directories – build and .deps. The former is where all the binaries are created while the second is a set of auto-generated make dependencies. The final lines of the build process give some statistics about the size of the resulting binary – in this case approximately ~11 KBytes (11620).

Now you can download your binary as follows:

$ make  download
/usr/local/bin/openocd -s /usr/share/openocd/scripts -f board/st_nucleo_f3.cfg \
    -c "program build/ch.elf verify reset exit" || true
Open On-Chip Debugger 0.10.0+dev-00953-g382f4f06 (2019-10-18-20:10)
 ... more output elided ...
 ** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
shutdown command invoked

The download program, openocd, generates a lot of output which may be useful to help diagnose when something goes wrong. At this point you should be rewarded with a blinking green led.

You can discard all the created files by executing

$ make clean
Cleaning
- ./.dep
- build

Done

Generally, this is only done when purging the directory of unnecessary files, for example, to avoid adding these files to a repo.

Talking to the Echo Program

As discussed above, the main thread of the echo program retransmits characters that it receives on its serial port. Serial port 2 (there are several) of the STM32L476 on the nucleo board is connected to the ST-LINK interface which implements a “virtual com port” (COM port) over the USB connection. On a Mac, this virtual port is listed as “/dev/tty.usmodemXXX” (the XXX may vary depending upon the set of devices connected to the Mac). In order to communicate with the STM32 we need a program that will provide an interface to this COM port. There are a variety of terminal programs that can do this, but I find them a bit clunky. I have provided a program that provides this function

lab-instructions/bin/goshell

The program is written in go and has several options

$ goshell -h
Usage of /Users/geobrown/Teaching/po181u/go/src/goshell/goshell:
  -baud int
    	the baud rate (default 115200)
  -port string
    	name of the port (default "/dev/tty.usbmodem14603")

You will not need to change the defaults unless the COM port has a different name. Assuming this isn’t needed, execute goshell and type some input (you’ll need to include the correct path to the goshell binary). Every time you type enter, the preceding input will be echoed.

$ path-to-goshell/goshell 
the quick brown fox
the quick brown fox
jumped over the lazy dog
jumped over the lazy dog

To exit the program, type ctl-D.

You may find that the port name isn’t correct. In this case do the following:
$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem14203
$ path-to-goshell/goshell -port /dev/tty.usbmodem14203 

3. A simple shell

Now you will modify the echo program to make it respond to two commands – on which will enable the blinker and off which will disable the blinker.

  1. Create a copy of the echo directory called shell.
  2. Add a global variable “on” to main.c and change the blinker code so that it only turns on the led if this variable is 1.
  3. Modify the main thread so that it collects a whole line of text in a string variable char cmd[16];
    • The \n character should be detected as a line terminator and discarded.
    • If the input line starts with “on”, set the on variable and print (see below) the string “blinker on\n”
    • If the input line starts with “off”, clear the on variable and print the string “blinker off\n”
    • If the input line starts with anything else, print “bad command: -cmd-\n”

You can use the standard string routine strncmp (see OS X man page) to test your input.

ChibiOS has a simplified fprintf function which you can use as follows:

 // at the beginning of your main.c add
BaseSequentialStream *chp = (BaseSequentialStream *) &SD2;
       ...
 //  in your main thread
chprintf(chp,"a format string\n",...)

by default, only strings, characters, integers, hexadecimal, and unsigned integer formats are supported.

Test your code with the goshell command.

A natural way to structure your “shell” code is as follows:

   if (/*test 1*/) {
     /* action 1*/
     continue;
   }
   if (/*test 2*/) {
     /* action 2*/
     continue;
   }
   /* default action */

You’ll find it convenient to use a standard C string compare function.

man strcmp

I recommend always using the strncmp variant since it is more robust in the face of “illegal” strings

4. Integrating with Node Red

As a final exercise, you will build a simple node-red flow that communicates with your shell through the serial port.

  1. Copy the node-red-template to lab6/node-red
  2. In the lab6/node-red directory execute ./start-node-red
  3. Open a browser window with localhost:1880
  4. select the manage palette option from the node-red menu.
  5. Install node-red-node-serialport
  6. Add a pair of serial port nodes
  7. Configure the serial port (you will need to select the correct port)
  • The baud rate should be 115200, other defaults are ok
  • Add a \n to output – your shell expects this
  • Split input \n – your shell produces lines with line-feeds
  1. Add three inject nodes and a debug node
  • The “topics” for the inject nodes should be blank
  • For the on/off nodes – the text should be “on”/“off”, respectively.
  1. After testing your nodes, add a dashboard with a switch and a gauge.
Note:

  • You don’t need to remove any of your existing nodes
  • You’ll have to add a filter between the serial input and the gauge to convert from strings of the form led on\n to a number. The filter will be a fragment of javascript; here’s a bit of javascript to lead you in the right direction
if (msg.payload == 'some string') {
    msg.payload = a_value;
    return msg;
}
  • You’ll have to configure the switch to emit the correct messages for the on/off cases

As a general rule, I recommend proceeding in stages and testing each stage

  1. Add your filter and a debug node, and test.
  2. Add a gauge (set the colors and range appropriately) and test.
  3. Add your switch and test.

Notice again, every step ended with “and test”.

Please show me a demo of your final system.

5. Wrapping Up

In future labs we’ll add some more interesting commands to the Nucleo as well as move from line-feed terminated messages to something a bit more robust. In the process, you’ll learn how to work with sensors, timers, and events to create something more like a real sensor-node. Eventually, we will bypass the nucleo/node-red interface by introducing a small program that provides a bridge between the nucleo node and an mqtt server.