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:
- You already have your development environment setup, and have been able to run "helloworld" style programs with a blinking light or similar.
- 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.
- 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:
- Right-click on the project in the workspace, click "Properties",
- Navigate to "C/C++ Build Settings", then "Includes",
- 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.
- Create an output callback to write to the serial port, and feed inbound serial data to Electric UI.
- Setup Electric UI and track some variables.
- 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.
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:
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:
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.
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:
- The interface(s) you are planning on using, so manual outbound message transmissions have a default interface,
- A unique identifier, so the user interface can distinguish between otherwise identical boards connected to the same computer,
- 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.
// Pass the interface we declared above to set it as the defaulteui_setup_interface( &serial_comms );// Provide a identifier so the UI can distinguish this board from other boardseui_setup_identifier( "unique_strings", 14 );// Provide a pointer to the tracked variable arrayeui_setup_tracked( &tracked_vars, EUI_ARR_ELEM(tracked_vars) );
// OR, use these macros to achieve the same effectEUI_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.
// Declare variables we'll be sharing with the UIuint8_t blink_enable = 1;uint8_t led_state = 0;uint16_t glow_time = 1;// Track them with helper macroseui_message_t tracked_vars[] = { EUI_UINT8( "led_blink", blink_enable ), EUI_UINT8( "led_state", led_state ), EUI_UINT16( "lit_time", glow_time ),};
// 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.
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 typeeui_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.
#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 behaviouruint8_t blink_enable = 1; // if the blinker should be runninguint8_t led_state = 0; // track if the LED is on or offuint16_t glow_time = 200; // in millisecondsuint32_t led_timer = 0; // Timestamp of last led on/off changechar nickname[15] = "STM32 UART/USB";eui_interface_t eui_serial = EUI_INTERFACE( &eui_uart_out );// Electric UI manages variables referenced in this arrayeui_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!