Handling Structs, Bitfields and Arrays
Structures underpin the representation of custom data types and provide logical grouping and syntactical sugar to developers. Structures or custom types allow developers to pass more complex data between functions and semantically improve variable access with dot notation. Use of these language features is a vital part of serious software projects.
Electric UI encourages the use of custom types to pipeline related variables into one packet, and reducing the number of message ID's in use.
Structures in C
If you are using the electricui-embedded
C library and protocol, connecting an existing structure to the UI is straight-forward.
This C example snippet is based on the hello-blink
example. We define and declare a structure variable, and change the tracked variables to use a EUI_CUSTOM
macro with the new variable. We'll call it led
.
typedef struct { uint8_t enable; uint16_t glow_time;} blinker_t;blinker_t led_settings = { 1, 350 };uint8_t led_state = 0;eui_message_t tracked_variables[] ={ EUI_CUSTOM( "led", led_settings ), EUI_UINT8( "led_state", led_state ),};
In the main loop, some little changes are needed to use the new structure members instead of discrete variables.
void loop(){ serial_rx_handler(); if( led_settings.enable ) { if( millis() - led_timer >= led_settings.glow_time ) { led_state = !led_state; led_timer = millis(); eui_send_tracked("led_state"); } } digitalWrite( LED_BUILTIN, led_state );}
After flashing this example to hardware (and using the default UI template as reference), you'll notice that the slider control doesn't work anymore.
When sending and receiving variables with a custom type, the design of Electric UI assumes that the UI has suitable encoders and decoders to handle unpacking the bytes matching the memory model of the microcontroller.
For tips on how to most effectively use structures with C, we recommend reading this article on structure padding optimisation techniques to get the most bang for your byte.
Structure safety advisory
In situations where exposing your structure memory is like holding a gun to your foot, we suggest pointing Electric UI to a buffer of bytes which are safe to serialize against over the project's lifecycle.
You'll want to do this when your project:
- Wants portability between microcontroller architectures and/or compilers,
- The compiler doesn't guarantee structure repacking or padding behaviour,
- You are using a higher level construct to declare your data structure in abstract memory (traits, C++ vector),
- Uses a language which doesn't respect developer structure ordering, such as Rust without
repr(C)
, - You want to be explicit, at the expense of complexity (and a bit of overhead).
Data Structure Alignment
One small catch when working with structures which differ from the above example; it's quite likely that your microcontroller and compiler doesn't actually shove your structured data hard against each element due to word boundaries.
Consider the following example:
typedef struct { uint8_t a; uint16_t b; int8_t c; float d;} sensor_settings_t;
We would probably think that the bytes of b
start right after our a
variable, and therefore our structure would be nicely represented in memory as written, as shown below.
b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | ||
---|---|---|---|---|---|---|---|---|---|
A | B | B | C | D | D | D | D |
Due to the architectural design of many processors its more efficient to read and write memory in multiples of bytes. As a result, the compiler inserts padding to ensure that b
starts on the word boundary, and then another is added to ensure d
starts with correct alignment. A 2-byte (16-bit) alignment was used for this example, which results in 2 additional padding bytes, but 4-byte (32-bit) alignment is more common with the popularity of modern 32-bit microcontrollers.
b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
---|---|---|---|---|---|---|---|---|---|
A | - | B | B | C | - | D | D | D | D |
Because the structure has two smaller members which can fit neatly inside the boundary, manually re-ordering the structure by moving c
forwards would allow the compiler to align the larger types of b
and d
without padding, reducing the memory impact and transfer overhead slightly.
b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | ||
---|---|---|---|---|---|---|---|---|---|
A | C | B | B | D | D | D | D |
Not all structures can be entirely free of padding (without special help from the compiler), and in these cases the UI codec just needs to be aware of these additional bytes, and you need to be aware of portability differences if running your embedded code on a mix of architectures.
Forced structure packing
This feature isn't supported by all compilers or platforms, and if you are running a non-C style language it might not be possible at all.
GCC can be prevented from adding padding data to structures with an explicit attribute, __attribute__((__packed__))
, which would look something like this
typedef struct { uint8_t a; uint16_t b; int8_t c; float d;} __attribute__((__packed__)) sensor_settings_t;
This would result in a data structure which contains no padding. If we wanted to, we could also request the particular padding alignment with the following tweak (2-byte alignment):
typedef struct { uint8_t a; uint16_t b; int8_t c; float d;} __attribute__(( packed, aligned(2) )) sensor_settings_t;
Some compilers use variations like #pragma pack(n)
to suppress or select the number of padding bytes. For our 32-bit architecture, this gives an un-padded structure in memory at the potential tradeoff of memory access speed[^1]. Neat!
This approach isn't the default recommendation due to the compiler specific nature of these features and the rare incompatibilities that might occur on certain platforms[^2]. Structure alignment settings are somewhat up to compiler implementation so be wary.
If you are using a resource constrained microcontroller and can't reorder the structure or manually manage a block of memory, this might be one of the optimisations to look into.
Bitfields
Bitfields are commonly used on embedded platforms to pack multiple settings into a single byte, saving space, or to load into a peripheral's register.
Bitfields behave the same way as structures with manually specified type sizing. Each field can have 1 or more bits assigned.
You can specify the size of each member in C by using the following syntax:
struct { // Members are 1-bit in size unsigned alarm_on : 1; unsigned light_on : 1;} UserSettings;
This bitfield would consume 1 byte in memory. If the number or summed size of bitfield elements exceeds 8-bits, the container will become a 2-byte type, and so on.
Arrays
The size of the data structure is silently grabbed as part of the macros used in the eui_message_t
array (assuming electricui-embedded
).
This means for any default typed array, the UI automatically handles the sizing and dissects the variable into elements.
User Interface Codecs
With our structure setup on the embedded side, we need to teach the UI how to read inbound packets and correctly form data into an outbound packet.
Read the corresponding structures interface guide
[^1]: Strict alignment architectures like SPARC don't allow it at all [^2]: ARM M0 cores are stricter about alignment than more recent ARM variants, which have less or no impact with poorly aligned data access.