Bare-metal C Setup Guide

This guide will help get Electric UI running with your microcontroller and development environment of choice. electricui-embedded will work on any micro that supports C or C++ code, over any bi-directional communications method you provide.

Just feed it incoming bytes, and help write responses back out.

Preface

The countless combinations of microcontroller, toolchain, build scripts and makefiles, and IDE flavoured ecosystems makes it hard to provide a conclusive guide, so we make the following assumptions:

  1. You already have your development environment setup, and have been able to run "helloworld" style programs with a blinking light or similar.
  2. Can print text to a console (such as Putty, minicom, etc) and send characters or bytes to the microcontroller, using a serial, USB with VCP, or other communications interface.
  3. You can follow generalised instructions and adapt basic actions to your specific toolchain or IDE.

The main library is written with C targets in mind, and tested against predominantly with gcc based C99 toolchains.

The library uses extern C { } on import for C++ use.

We provide a full tutorial for a STM32F4 using CubeMX which covers project creation, peripheral configuration and implementation of (simple) circular buffers for interrupt driven data input and output.

For other platforms, see reference implementations.

Download and include

The library is available from our Github repository, download the current release here. We recommend using Git.

git clone https://github.com/electricui/electricui-embedded.git

Import steps differ across IDE's and toolchains. For our example system (STM32 CubeMX generated project structure), I pulled the electricui-embedded library into the projectname/middlewares/ directory. For Eclipse based IDE's:

  1. Right-click on the project in the workspace, click "Properties",
  2. Navigate to "C/C++ Build Settings", then "Includes",
  3. Ensure the projectname/middlewares/electricui-embedded directory is included.

At the top of your code, #include "electricui.h".

Ignoring the tests directory

Some IDEs with build-script generation may automatically try to build the unit tests or example projects. If this happens, errors will appear.

For most IDE's, right-click on the electricui-embedded/tests folder, and ignore it or mark as excluded to prevent building tests automatically. Or you can delete those folders from disk.

Now the library has been included in the build, we'll configure a bare working project.

  1. Create an output callback to write to the serial port, and feed inbound serial data to Electric UI.
  2. Setup Electric UI and track some variables.
  3. Control a blinking light with the UI.

Setting up an interface

Electric UI's library is stateless by design, this means the applications developer (you) needs to declare and manage a few structures which are passed to electricui-embedded during setup.

As data arrives on your serial port, the packet is decoded and buffered in a eui_interface_t structure which belongs to your scope. By providing a callback function to the interface, it can send data back to the UI.

This opens up the ability to use one or many distinct communications interfaces on the same hardware for wired+wireless systems, and enables some fun cross-interface and load balancing tricks.

For now, we'll start by declaring a single serial interface with the EUI_INTERFACE macro. The macro helps provide the callback pointer when we declare the interface.

c
eui_interface_t serial_comms = EUI_INTERFACE( &serial_write_cb );

Now lets get that interface talking!

If you prefer a verbose declaration, see the interface section of the API docs.

After a blank structure declaration, set the callback at run-time with:

c
serial_comms.output_cb = &serial_write_cb;

Outputting data

When Electric UI needs to send data to the UI, it calls the serial_write_cb function we just setup. The function is passed a buffer of data to write to the serial port (or an intermediate buffer).

Create the following function, and hook up the relevant Serial.write() or uart_write_bytes() function:

c
void serial_write_cb( uint8_t *data, uint16_t len)
{
// send the bytes to the UART peripheral/library
// or insert the data into an outbound FIFO etc
uart_write_bytes( data, len );
}

Ingesting data

Feed the library data by passing the eui_parse( uint8_t byte, eui_interface_t* link ) inbound data.

electricui-embedded doesn't impose any timing or length input requirements. Pass in a large buffer, smaller chunks, or single bytes as they arrive.

c
uint16_t inbound_bytes = is_data_available( &huart1 );
for( uint16_t i = 0; i < inbound_bytes; i++ ) {
eui_parse( get_data_byte( &huart1 ), &serial_comms );
}

Ingesting data inside your interrupt handler isn't recommended practice, as Electric UI runs it's parsing state machine on receive and may attempt to emit responses to relevant packets.

General recommended practice for embedded platforms suggests buffering inbound data and handling it as part of your loop or in a separate task.

Completing Setup

The communications plumbing is now done. To finish up, we recommended telling the library:

  1. The interface(s) you are planning on using, so manual outbound message transmissions have a default interface,
  2. A unique identifier, so the user interface can distinguish between otherwise identical boards connected to the same computer,
  3. Any variables you want the library to manage for you (we'll cover this in the next section).

These processes are best done during initialisation stages, typically near UART or USB data handler setup code.

c
// Pass the interface we declared above to set it as the default
eui_setup_interface( &serial_comms );
// Provide a identifier so the UI can distinguish this board from other boards
eui_setup_identifier( "unique_strings", 14 );
// Provide a pointer to the tracked variable array
eui_setup_tracked( &tracked_vars, EUI_ARR_ELEM(tracked_vars) );
c
// OR, use these macros to achieve the same effect
EUI_LINK( serial_comms );
EUI_TRACK( tracked_vars );

Now's probably a good time to learn about tracked variables!

Tracking variables

Tracked variables make sharing variables between the UI and micro-controller easier. The library will announce these variables to the UI during a handshake, and handles incoming queries or writes as needed.

Variables are defined and used as normal, and we 'track' them by adding entries into a eui_message_t array before startup. Each eui_message_t contains a pointer to a variable, and some simple type information.

c
// Declare variables we'll be sharing with the UI
uint8_t blink_enable = 1;
uint8_t led_state = 0;
uint16_t glow_time = 1;
// Track them with helper macros
eui_message_t tracked_vars[] = {
EUI_UINT8( "led_blink", blink_enable ),
EUI_UINT8( "led_state", led_state ),
EUI_UINT16( "lit_time", glow_time ),
};
c
// Or without macros (3 variations; designated initalisers, manually sized, auto-sized)
eui_message_t tracked_vars[] = {
{ .msgID = "led_blink", .type = TYPE_UINT8, .size = 1, {.data = &blink_enable} },
{ "led_state", TYPE_UINT8, 1, {&led_state} },
{ "lit_time", TYPE_UINT16, sizeof(glow_time), {&glow_time} },
};

And you're done! Inbound serial data will be passed to Electric UI, which has access to our shared variables, and it will form responses and send them through our callback function.

But my variables look different?

This guide is a bit short, so we'll just mention that Electric UI handles the full range of <stdint.h types, arrays, custom structures and can let the UI trigger your own callback functions.

Also, because lots of data isn't mutable we support marking a tracked variable as read-only. Register configurations, button states, timers, or sensor values are common examples.

c
int8_t thruster_commands[4] = { 0, 0, 0, 0};
float thrust_kg = 0;
typedef struct {
float roll;
float pitch;
float yaw;
uint8_t temperature;
} imu_data_t;
imu_data_t gyro_sensor = {0}
void write_settings( void ) {
// Save data to non-volatile storage
// Buzz the beeper to tell the user the write succeeded, etc
}
// Array, read-only, custom type
eui_message_t tracked_vars[] = {
EUI_INT8_ARRAY( "power", thruster_commands ),
EUI_FLOAT_RO( "thrust", thrust_kg ),
EUI_CUSTOM( "gyro", gyro_sensor ),
EUI_FUNC( "save", write_settings ),
};

When tracking standard types, or arrays of standard types, no additional work is required on the UI side.

For bitfields and custom types, a codec is required to decode and encode the custom type correctly.

Complete example code

Here's a 'complete' code block which provides UI control over a blinking LED. Adjust as needed for your embedded platform.

c
#include "main.h"
#include "gpio.h"
#include "uart.h"
#include "electricui.h"
#define MICRO_UUID ((uint32_t *)0xDEADBEEF) // Base address of chip UUID
// Simple variables to control the LED behaviour
uint8_t blink_enable = 1; //if the blinker should be running
uint8_t led_state = 0; //track if the LED is on or off
uint16_t glow_time = 200; //in milliseconds
uint32_t led_timer = 0; // Timestamp of last led on/off change
char nickname[15] = "STM32 UART/USB";
eui_interface_t eui_serial = EUI_INTERFACE( &eui_uart_out );
// Electric UI manages variables referenced in this array
eui_message_t tracked_vars[] =
{
EUI_UINT8( "led_blink", blink_enable ),
EUI_UINT8( "led_state", led_state ),
EUI_UINT16( "lit_time", glow_time ),
EUI_CHAR_ARRAY_RO( "name", nickname ),
};
void eui_uart_out( uint8_t *data, uint16_t len)
{
uart_write( data, len );
}
int main(void)
{
HAL_Init();
HAL_Setup_Clock();
// Initialize peripherals
HAL_Init_GPIO();
HAL_Init_UART();
// Setup Electric UI
eui_setup_identifier( (char *)MICRO_UUID, 12 ); //96-bit UUID
EUI_LINK( eui_serial );
EUI_TRACK( tracked_vars );
led_timer = HAL_GetTick_ms(); //kickstart the led timestamp
// Superloop
while(1)
{
while( uart_check_buffer() )
{
eui_parse( uart_get_byte(), &eui_serial );
}
if( blink_enable )
{
// Check if the LED has been on for the configured duration
if( HAL_GetTick_ms() - led_timer >= glow_time )
{
led_state = !led_state; //invert led state
led_timer = HAL_GetTick_ms();
}
}
HAL_GPIO_Write( LED_PORT, LED_PIN, led_state );
}
}

With the LED on your board blinking, its time to make a UI to control it!