STM32H755 / FreeRTOS based project for testing, integrating and evaluating sensors and supporting embedded modules.
As of now it includes:
- Driver for the LIS3DHTR MEMS accelerometer.
- Driver for the PmodALS ambient light sensor.
- Driver for the internal temperature sensor connected on the on-chip ADCs.
- A custom lightweight logging module.
Driver implementation for the LIS3DHTR accelerometer providing both high level and low level APIs. It abstracts the underlying transport layer (I2C/SPI) and is easily portable to other MCUs.
The high level API exposes device functionality rather than register operations. It uses the ll API and expects that it is blocking.
Instead of manipulating registers and bitfields directly, the user interacts with device features through functions such as:
LIS3DHTR_enable_aux_adcs()LIS3DHTR_disable_temp_sensor()LIS3DHTR_get_acceleration()
The goal is to expose the device capabilities while hiding register layout and configuration details.
The low level API provides register level access. Two implementations are provided:
- No-OS: Blocks until communication with the device completes.
- FreeRTOS: The task yields execution and resumes when an interrupt notifies that the transfer has completed.
The ll API consists of the following functions:
LIS3DHTR_read_reg(): Reads from a register on the device.LIS3DHTR_write_reg(): Writes to a register on the device.reg_get_field(): Gets the value of a specific field of a register.reg_set_field(): Sets the value of a specific field of a register.
Example usage:
rv = LIS3DHTR_read_reg( device , LIS3DHTR_TEMP_CFG_REG , &data );
reg_set_field( &data , TEMP_CFG_ADC_EN , LIS3_ENABLE );
rv = LIS3DHTR_write_reg( device , LIS3DHTR_TEMP_CFG_REG , data );The user only specifies the registers and fields to access or modify, which are provided as C enumerations in the header files of the driver.
The primary design goal is a simple user-facing API. To achieve that, all device metadata (register mappings, field definitions, access rights, etc.) is stored internally by the driver.
Example:
enum {
LIS3DHTR_STATUS_REG_AUX ,
LIS3DHTR_OUT_ADC1_L ,
... ,
LIS3DHTR_NUM_REGS
};
LIS3DHTR_memory_map[LIS3DHTR_NUM_REGS] = {
{ 0x07 , REG_R }, // physical address & access rights of STATUS_REG_AUX
{ 0x08 , REG_R } // physical address & access rights of OUT_ADC1_L
...
}The above example, while simple, is problematic. The enum and the array are parallel 1 to 1 data structures. A modification in one structure may not be reflected in the other. To improve maintainability, X macros are used instead.
Example:
// All register metadata is defined in one place.
#define REG_MAP( X ) \
X( STATUS_REG_AUX , 0x07 , REG_R ) \
X( OUT_ADC1_L , 0x08 , REG_R ) \
...
// helper macros
#define GEN_ENUM( name , address , access ) LIS3DHTR_##name,
#define GEN_ARRAY( name , address , access ) { address , access } ,
enum {
REG_MAP( GEN_ENUM )
LIS3DHTR_NUM_REGS
};
LIS3DHTR_memory_map[LIS3DHTR_NUM_REGS] = {
REG_MAP( GEN_ARRAY )
};The X macros are the single point of truth, and all data structures are automatically generated by the preprocessor. Similar macros are used to generate the field bit positions and masks.
Driver implementation for the PmodALS ambient light sensor. The device is connected through SPI but it is read-only, thus there is no MISO line.
SPI must be configured as follows:
-
- 12-bit data size. Although the ADC output is 8 bits, the device transmits leading and trailing padding bits; the driver removes them.
- MSB first.
- Baud rate between 1 & 4 Mbps.
Two implementations are provided:
- No-OS: Blocks until communication with the device completes.
- FreeRTOS: The task yields execution and resumes when an interrupt notifies that the transfer has completed.
The API consists of the following functions:
pmodals_create_handle(): The user must specify the sensor supply voltage (Vcc) and load resistance (Rload).pmodals_get_lux(): Returns the ambient light intensity in lux.
Ambient light is calculated with the following formula:
where
The module provides the MCU temperature on demand. Internally, the built-in temperature sensor is sampled periodically through ADC3.
The following configuration is used:
- ADC conversions are triggered every 1 second using TIM2.
- Samples are transferred to CM7 through DMA1.
- DMA operates in double-buffer mode using the HAL multi-buffer DMA API. This provides deterministic low-latency access while avoiding torn reads.
- In addition to the temperature sensor, the Vrefint channel is also sampled to compensate measurements for supply-voltage variation.
- ADC3 is supplied with a 100 MHz clock. To achieve the ≥ 5 µs effective sampling time typically recommended by ST, the sampling time is configured to 810.5 cycles (~8.1 µs).
Three modes of calculating the temperature are provided:
- Floating-point arithmetic.
- Integer arithmetic.
- Fixed-point (Q2.30) arithmetic to reduce division overhead.
As the internal sensor is relatively noisy, smoothing is applied in all three modes.
LWL is a lightweight logging system designed for low runtime and memory overhead. It is intended primarily for crash and failure analysis, by enabling high-granularity logging during normal operation with minimal impact on system performance. It is most useful when combined with a more verbose, higher-level logging system.
LWL consists of a circular buffer stored in memory and a minimal API for writing and extracting log data. It supports variable-length records containing primitive data types.
lwl_init(): Initializes or resets the module by clearing the buffer.lwl_enter_record(): Main logging function. Appends a record to the buffer. See the following section for format requirements and limitations.dump_log(): Exports the buffer contents and next write position through UART in binary format for post-mortem decoding.
The implementation tracks only the next write position in the buffer. Records cannot be inspected during runtime; the buffer is intended to be extracted and decoded post-mortem.
lwl_enter_record() is a variadic function. To ensure that it functions correctly and it is compatible with the decoding script that parses the codebase, all log writes must use the following format:
lwl_enter_record( MODULE_ID, FUNCTION_ID, "argument_types", arguments... );Example:
lwl_enter_record(
LIS3DHTR_LWL_ID,
LIS3DHTR_READ_LWL_ID,
"cc",
device->i2c_address,
device->memory_map[reg_address].address
);MODULE_ID and FUNCTION_ID must be macros defined in lwl.h. MODULE_ID must be a single byte values, preferably single character literal to improve readability of the logs. FUNCTION_ID can be any string.
Example:
#define LIS3DHTR_LWL_ID 'L'
#define LIS3DHTR_READ_LWL_ID "RX"The ( MODULE_ID, FUNCTION_ID ) combination must be globally unique across the project.
The third argument is a format string describing the types of the remaining arguments.
Supported types:
| Type | Size | Meaning |
|---|---|---|
c |
1 byte | uint8_t / byte |
s |
2 bytes | int16_t |
h |
2 bytes | uint16_t |
d |
4 bytes | int32_t |
u |
4 bytes | uint32_t |
f |
4 bytes | float |
The number and order of format characters must match the provided arguments.
Examples:
lwl_enter_record( MOD, FUNC, "" );
lwl_enter_record( MOD, FUNC, "c", status );
lwl_enter_record( MOD, FUNC, "uh", timestamp, value );
lwl_enter_record( MOD, FUNC, "f", temperature );The script lwl_decoder.py decodes the binary dumps of the LWL circular buffers.
The script parses the firmware source tree and automatically builds a database of all lwl_enter_record() calls, including:
- module IDs and function IDs
- argument names
- format strings
- entry sizes
The decoder then reads a binary buffer dump (dump.bin) and reconstructs the logged records using the generated database.
Requirements:
-
The source tree used by the script must match the firmware version that generated the dump. Changes to log definitions, IDs, formats or sizes can make decoding invalid.
-
The dump directory must contain an
info.txtfile with a line:next_entry_index = <number>where
<number>is the buffer write index.
The script automatically:
- resolves ID macros from
lwl.h - unwraps the circular buffer
- detects the non-wrapped buffer case
- determines the most likely decoding start point
- decodes and prints the recovered log entries in tabular form.
code/
├── CM7/ # FreeRTOS + LwIP application
├── CM4/ # Secondary core application
├── sensor_playground.ioc # CubeMX project file
lwl_decoder.py # script that decodes lwl buffers
- STM32CubeMX 6.17.0
- STM32Cube FW_H7 V1.13.0
- STM32CubeIDE 2.1.1
- FreeRTOS 10.6.2
- CMSIS-RTOS 2.1.3