How we write C++ for embedded audio applications.
- Write clear, readable code
- No dynamic allocation or exceptions
- Use templates for compile-time optimization
- Platform independence through compile-time configuration
- Design for testability with template-based mocks
Style Philosophy
- Match the existing style unless there is a compelling reason
- Be explicit as a rule, verbose if necessary
- Names should give a clear indication of what something is.
- We don't have to worry about file size or length anymore, so let's be descriptive.
- Don't be clever, be readable and correct.
File Organization
File conventions:
- C++ headers:
.hpp
- Implementation:
.cpp
- Headers:
snake_case.hpp
- Tests:
test_component_name.hpp
- Directories:
snake_case or dash-case
Examples:
src/
sbl/components/display/led.hpp
src/
sbl/patterns/timing/non_blocking_delay.hpp
tests/unit/test_blinker.hpp
Root namespace for all Sound Byte Libs functionality.
Include Guards
Use path-based include guards. Do NOT use #pragma once.
#ifndef SBL_HAL_GPIO_HPP_
#define SBL_HAL_GPIO_HPP_
#endif
Pattern: SBL_PATH_FILENAME_HPP_
Why not #pragma once? While widely supported, it's non-standard and can have edge cases with symlinks and build systems. Traditional guards are explicit and portable.
NEVER use ../ includes or relative include paths as they are a code smell.
Naming Conventions
Naming Philosophy
Every word must add meaning.
Examples:
class Led { };
class Button { };
class CalibrationTable { };
class StatusLed { };
class ControlButton { };
class Manager { };
Led power_indicator;
Led activity_led;
Button encoder_button;
Naming conventions:
class GpioController { };
enum class PinMode { Input, Output };
using GpioPin = uint8_t;
bool set_tempo(uint32_t bpm);
void enable();
void delay_ms(uint32_t ms);
private:
uint32_t frequency_;
bool is_running_;
};
uint32_t sample_rate = 44100;
static constexpr uint32_t BUFFER_SIZE = 1024;
MCU driver implementations (from sbl-hardware)
Code Formatting
class Timer {
public:
void start() {
if (condition) {
do_something();
}
}
private:
uint32_t frequency_;
};
int result = a + b * c;
if (simple) {
return;
}
if (very_long_condition &&
another_condition) {
}
Documentation
void set_mode(uint8_t pin, PinMode mode);
timer0_init();
if (pressed_time_ > DEBOUNCE_THRESHOLD_MS) {
}
File headers:
#ifndef SBL_WIDGET_BLINKER_HPP_
#define SBL_WIDGET_BLINKER_HPP_
Template Usage
template<typename GpioImpl, typename TimerImpl>
class Blinker {
public:
Blinker(GpioImpl& gpio, TimerImpl& timer, uint8_t pin)
: gpio_(gpio), timer_(timer), pin_(pin) {}
void start() {
gpio_.set_mode(pin_, PinMode::Output);
is_running_ = true;
}
private:
GpioImpl& gpio_;
TimerImpl& timer_;
uint8_t pin_;
bool is_running_ = false;
};
using PlatformBlinker = Blinker<sbl::driver::Gpio, sbl::driver::Timer>;
Memory Management
- Static allocation of memory only
class Sequencer {
public:
Sequencer(Timer& timer, Gpio& gpio)
: timer_(timer), gpio_(gpio) {}
private:
Timer& timer_;
Gpio& gpio_;
};
class I2cTransaction {
public:
I2cTransaction(I2c& i2c, uint8_t address)
: i2c_(i2c), address_(address) {
i2c_.begin_transaction(address_);
}
~I2cTransaction() {
i2c_.end_transaction();
}
};
Error Handling
enum class Result {
Success,
InvalidParameter,
BufferFull,
HardwareError,
Timeout
};
Result configure_timer(uint32_t frequency) {
if (frequency == 0 || frequency > MAX_FREQUENCY) {
return Result::InvalidParameter;
}
return Result::Success;
}
Summary
We are pre-alpha with 0 users. This gives us an opportunity to break things and rebuild them correctly without regard for how things were or backwards compat.
Do:
- Use templates for compile-time optimization
- Static allocation only
- Write clear, testable code
- Follow RAII patterns
- Use meaningful names
Don't:
- Use dynamic allocation or exceptions
- Use virtual functions in HAL
- Hardcode platform values in application code
- Write overly clever code
- Deprecate code when making changes
- Make code backwards compatible
ARM Cortex-M Constraints and Optimizations
Fixed-Point Arithmetic:
static constexpr int32_t VOLTAGE_SCALE_Q16 = 65536;
int32_t millivolts = (rawValue * VOLTAGE_SCALE_Q16) >> 16;
float voltage = rawValue * 0.001f;
Memory Constraints:
template<size_t MaxSize>
class Buffer {
static_assert(MaxSize <= 1024, "Buffer exceeds typical ARM Cortex-M RAM limits");
private:
uint8_t data_[MaxSize];
};
Real-Time Requirements:
- No division or modulo in performance-critical paths
- Use bit shifts for power-of-2 operations
- Bounded execution time for ISR-called functions
- Critical sections must be minimal (< 1μs typical)
Performance:
- Prefer
constexpr for compile-time computation
- Keep interrupt handlers minimal
- Choose algorithms optimized for ARM Cortex-M architecture
- Use lookup tables instead of complex mathematical functions