Hello,
I want to share an experience I had on an embedded sound project with a friend. We are building a wavetable oscillator: it’s a Eurorack module for musicians. It was designed as a C project only.
Background
We chose a STM32. The project uses CMake as a build system and I use two different compilers:
- GCC for debug mode: : The debugging experience is great with GCC. It is the default compiler used when generating a prject.
- Clang for release mode: : It optimizes better than GCC in our case; my main loop runs faster.
At one point in the project, I needed to manage the volume gain with an ADC, but not on a linear scale. I needed a logarithmic scale, which is more faithful to human hearing.
To be faster at runtime, I started to generate a Look-up Table (LUT) at startup.
static uint16_t volume_factors[VOLUME_TABLE_SIZE];
static void generateVolumeFactorsLookupTable(void) {
#define VOLUME_STEP_DB ((VOLUME_MAX_DB - VOLUME_MIN_DB) / (VOLUME_TABLE_SIZE - 1))
for (int i = 0; i < VOLUME_TABLE_SIZE; i++) {
double db = VOLUME_MIN_DB + (i * VOLUME_STEP_DB);
// G_db = 20 * log10 (G_linear)
// G_linear (amplitude)= 10 ^ (G_db / 20)
double gain = pow(10.0, db / 20.0);
// Binary range from 0 to 2 ^ VOLUME_BIT_RESOLUTION at 0 dB
volume_factors[i] = (uint16_t)(gain * (1 << VOLUME_BIT_RESOLUTION));
}
}
It was not the best thing I could do, since I knew I could just put all the values in an array and compile it. One day, I had a revelation while randomly reading the official website
Add a Zig compilation unit to C/C++ projects, exposing the rich standard library to your C/C++ code.
Of course! Why not build a Zig library to generate what I need at comptime and integrate it to CMake. The comptime feature feels to open a lot of possibilities.
Integrate Zig
After digging around, I figured out how to integrate Zig, a solution was to add this build step in compile.cmake. After some trials and tests :
set(ZIG_SRC ${CMAKE_SOURCE_DIR}/user/app/comptime.zig)
set(ZIG_OBJ ${CMAKE_BINARY_DIR}/comptime.o)
set(ZIG_OBJ_H ${CMAKE_BINARY_DIR}/comptime.h)
# Theses application headers can be used by Zig. Rebuild on change.
file(GLOB ZIG_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/user/app/*.h)
add_custom_command(
OUTPUT ${ZIG_OBJ}
COMMAND zig build-obj ${ZIG_SRC} -static -target thumb-freestanding-eabihf -mcpu=cortex_m4+vfp4d16sp+dsp -fsingle-threaded
-femit-bin=${ZIG_OBJ} -femit-h={ZIG_OBJ_H} -I${CMAKE_CURRENT_SOURCE_DIR}/user/app
-fno-unwind-tables -fno-ubsan-rt -OReleaseFast
DEPENDS ${ZIG_SRC} ${ZIG_HEADERS}
COMMENT "Compiling Zig code"
)
add_library(comptime STATIC ${ZIG_OBJ})
set_target_properties(comptime PROPERTIES LINKER_LANGUAGE C)
Note about argument:
-target thumb-freestanding-eabihf -mcpu=cortex_m4+vfp4d16sp+dsp -fsingle-threaded: The target description.-fno-unwind-tables -fno-ubsan-rt: Removes unwanted symbols.-I${CMAKE_CURRENT_SOURCE_DIR}/user/appThe C includes path for my Zig library.
Then, update your main CMakeLists.txt with the following:
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/comptime.cmake)
target_link_libraries(${EXECUTABLE} comptime)
target_link_options(-Wl,-z noexecstack) # Add -Wl,-z noexecstack
If you use a Zig library with GCC, you need to add -Wl,-z noexecstack as a linker argument, or ld will complain about a missing .note.GNU-stack section.
Here is the Zig code to generate the volume gain factors, which replaces the C implementation:
const std = @import("std");
const cfg = @cImport({
@cInclude("system_configs.h");
});
export const volume_factors: [cfg.VOLUME_TABLE_SIZE]u16 = generateVolumeFactorsLookupTable();
fn generateVolumeFactorsLookupTable() [cfg.VOLUME_TABLE_SIZE]u16 {
var table: [cfg.VOLUME_TABLE_SIZE]u16 = undefined;
const step_db: f64 = (cfg.VOLUME_MAX_DB - cfg.VOLUME_MIN_DB) / (@as(f64, cfg.VOLUME_TABLE_SIZE) - 1.0);
@setEvalBranchQuota(100000);
for (&table, 0..cfg.VOLUME_TABLE_SIZE) |*elem, i| {
// G_db = 20 * log10 (G_linear)
// G_linear (amplitude)= 10 ^ (G_db / 20)
const db = cfg.VOLUME_MIN_DB + (i * step_db);
const gain = std.math.pow(f64, 10.0, db / 20.0);
// Binary range from 0 to 2 ^ VOLUME_BIT_RESOLUTION at 0 dB
elem.* = @intFromFloat(gain * @as(f64, (1 << cfg.VOLUME_BIT_RESOLUTION)));
}
return table;
}
The project uses a header configuration file (system_configs.h) for many options, and the cool thing is that I can easily import it into my Zig code with cImport
Header comptime.h to include by the C application:
#include <stdint.h>
#include "system_configs.h"
extern const uint16_t volume_factors[VOLUME_TABLE_SIZE];
Problem encountered and notes
-
libc: Linking libc with the freestanding target is not implemented. In my initial trials, I got errors when the command tried to link it. In my case, I don’t need it, and I couldn’t find the CLI equivalent of.link_libc = falsein mu build script. If I need libc, I would want to use my own implementation withpicolibc. I don’t know why it not require it anymore too. -
Header Generation: I had to manually update my header file. The
-femit-h=option seems to not generate the file. Is this disabled a at this time? -
@cImportFuture:@cImportwill be removed in the future. I expect I will need a step withzig translate-cto handle this.
Opening
Since embedded components or projects can use custom build systems based on CMake (pico-sdk, Zephyr RTOS, ESP-IDF, etc.), I was wondering how to add Zig in combination with these tools without rewriting the entire build process.
Some people have tried this, but it seems like a lot of effort to maintain. Here are some examples I found:
I will probably use the method described in this article when working with a CMake-based SDK.
If you have any suggestions or improvements for my implementation or ways to integrate Zig, I’d love to hear them
I’m not yet aware of all the limitations I might encounter with this method.
For now, I have my compile-time content generator, and it works well. I will add more content over time.
I had been using the preprocessor and X Macros to generate code at compile-time in C, but with Zig, the code feels much more readable and has opened my mind to more complex possibilities. It is pretty cool. I also get access to the Zig standard library! ![]()