Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
830 changes: 50 additions & 780 deletions CLAUDE.md

Large diffs are not rendered by default.

268 changes: 54 additions & 214 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,272 +5,112 @@ A modern C++23 embedded systems project demonstrating best practices for hardwar
## Overview

This project explores:
1. **Modern C++ in Embedded Systems** - Applying C++23 features and software engineering principles to microcontroller development
2. **Host-Side Simulation** - Desktop development and testing environment with Python-based hardware emulation
3. **Correct-by-Construction Design** - Type-safe abstractions and compile-time verification
- **Modern C++ in Embedded Systems** - C++23 features and software engineering principles for microcontrollers
- **Host-Side Simulation** - Desktop development with Python-based hardware emulation via ZeroMQ
- **Correct-by-Construction Design** - Type-safe abstractions and compile-time verification

**Status**: Educational/demonstrative project (not production-ready)

## Key Features

- ✅ **Multi-Platform Support**: Host (x86_64), ARM Cortex-M4, ARM Cortex-M7
- ✅ **Hardware Abstraction Layers**: Clean separation between MCU, Board, and Application layers
- ✅ **Host Emulation**: ZeroMQ-based IPC with Python hardware simulator
- ✅ **Comprehensive Testing**: C++ unit tests (Google Test) + Python integration tests (pytest)
- ✅ **DevContainer Support**: Full VS Code DevContainer configuration
- ✅ **CI/CD Pipeline**: GitHub Actions for automated builds and tests
- 🚧 **Multi-Board**: STM32F3 Discovery, STM32F7 Nucleo, nRF52832 DK (partial implementation)

## Quick Start

### Option 1: VS Code DevContainer (Recommended)
### VS Code DevContainer (Recommended)

1. Install [VS Code](https://code.visualstudio.com/) and the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Open this repository in VS Code
3. Press `Ctrl+Shift+P` and select "Dev Containers: Reopen in Container"
4. Wait for the container to build (first time only)
5. Open a terminal and run:
```bash
cmake --workflow --preset=host-debug
```

That's it! The DevContainer includes all tools pre-installed.
3. Press `Ctrl+Shift+P` → "Dev Containers: Reopen in Container"
4. Run: `cmake --workflow --preset=host-debug`

### Option 2: Docker Compose
### Docker Compose

```bash
# Clone the repository
git clone <repository-url>
cd embedded-cpp

# Run host-debug workflow (build + test)
docker compose run --rm host-debug

# Or run host-release workflow
docker compose run --rm host-release
```

### Option 3: Local Build
### Local Build

**Requirements**:
- CMake 3.27+
- Ninja
- Clang 18+ (host builds) or ARM GCC (embedded builds)
- Python 3.10+ (for integration tests)
- ZeroMQ (libzmq3-dev)
**Requirements**: CMake 3.27+, Ninja, Clang 18+, Python 3.10+, ZeroMQ (libzmq3-dev)

```bash
# Configure for host platform
cmake --preset=host

# Build (Debug)
cmake --build --preset=host --config Debug

# Run tests
ctest --preset=host -C Debug --output-on-failure

# Or use workflow preset (configure + build + test)
cmake --workflow --preset=host-debug
cmake --workflow --preset=host-debug # Configure + build + test
```

## Project Structure
## Architecture

```
embedded-cpp/
├── src/
│ ├── apps/
│ │ ├── blinky/ # Example LED blink application
│ │ └── uart_echo/ # Example UART echo with RxHandler
│ ├── libs/
│ │ ├── common/ # Error handling (std::expected)
│ │ ├── mcu/ # MCU abstraction (Pin, UART, I2C, Delay)
│ │ │ └── host/ # Host emulation with ZeroMQ
│ │ └── board/ # Board abstraction (LEDs, buttons, UART, peripherals)
│ │ ├── host/ # Host board implementation
│ │ ├── stm32f3_discovery/
│ │ ├── stm32f767zi_nucleo/
│ │ └── nrf52832_dk/
├── py/host-emulator/ # Python hardware emulator
│ ├── src/emulator.py # Virtual device simulator
│ └── tests/ # Integration tests (pytest)
├── cmake/toolchain/ # Cross-compilation toolchains
├── .devcontainer/ # VS Code DevContainer config
├── .github/workflows/ # GitHub Actions CI/CD
└── CLAUDE.md # Comprehensive project documentation
Application (apps/) → Board (libs/board/) → MCU (libs/mcu/) → Platform Implementations
```

## Technology Stack
- **apps/**: Example applications (blinky, uart_echo)
- **libs/mcu/**: Hardware abstractions (Pin, UART, I2C, Delay) with host emulation
- **libs/board/**: Board-specific implementations (host, STM32F3, STM32F7, nRF52)
- **py/host-emulator/**: Python hardware simulator for desktop testing

| Category | Technology |
|----------|------------|
| **Language** | C++23 |
| **Build System** | CMake 3.27+ with Ninja Multi-Config |
| **Compilers** | Clang 18 (host), ARM GCC (embedded) |
| **Testing** | Google Test (C++), pytest (Python) |
| **IPC** | ZeroMQ with JSON serialization |
| **Embedded Targets** | STM32F3, STM32F7, nRF52832 |
| **Architectures** | ARM Cortex-M4, ARM Cortex-M7 |
| **Development** | Docker, VS Code DevContainers |
| **CI/CD** | GitHub Actions |

## Building for Different Platforms
## Build Commands

```bash
# Host platform (development/testing)
# Host (development/testing)
cmake --workflow --preset=host-debug
cmake --workflow --preset=host-release

# STM32F3 Discovery (ARM Cortex-M4)
# ARM targets
cmake --workflow --preset=stm32f3_discovery-release

# ARM Cortex-M7 (base preset)
cmake --workflow --preset=arm-cm7-release
```

## Running Tests

### C++ Unit Tests

```bash
# Run all tests
# All tests
ctest --preset=host -C Debug --output-on-failure

# Run specific test
# Single C++ test
ctest --preset=host -C Debug -R test_zmq_transport
```

### Python Integration Tests

```bash
cd py/host-emulator

# Install dependencies
pip install -r requirements.txt

# Run tests (requires built executables)
pytest tests/ --blinky=../../build/host/bin/blinky --uart-echo=../../build/host/bin/uart_echo
# Python integration tests
cd py/host-emulator && pytest tests/ -v
```

## Example Applications

### Blinky

The `blinky` application demonstrates:
- LED blinking at 500ms intervals
- Button interrupt handling (rising edge detection)
- Dependency injection with board abstraction
- Error handling with `std::expected`
## Example: Running Blinky

**Run on host emulator**:
```bash
# Terminal 1: Start Python emulator
cd py/host-emulator
python -m src.emulator
# Terminal 1: Start emulator
cd py/host-emulator && python -m src.emulator

# Terminal 2: Run blinky
cd build/host/bin
./blinky
# Terminal 2: Run application
./build/host/bin/Debug/blinky
```

The emulator will print pin state changes as the application runs.

### UART Echo

The `uart_echo` application demonstrates:
- UART initialization and configuration
- Event-driven reception with RxHandler callback (similar to Pin interrupts)
- Asynchronous data echoing
- LED toggling on data received
- Greeting message on startup

**Run on host emulator**:
```bash
# Terminal 1: Run uart_echo
cd build/host/bin
./uart_echo

# Terminal 2: Send data via Python
python
>>> from src.emulator import DeviceEmulator
>>> emu = DeviceEmulator()
>>> emu.start()
>>> emu.uart1().send_data([72, 101, 108, 108, 111]) # "Hello"
>>> bytes(emu.uart1().rx_buffer) # See echoed data
>>> emu.uart1().rx_buffer.clear()
```

## Software Engineering Principles

This project demonstrates:

1. **Strong Types** - Enums and type aliases prevent errors (PinState, PinDirection, Error)
2. **Type Safety** - Explicit casting, no implicit conversions
3. **Compile-Time Checks** - `constexpr`, templates, concepts
4. **Correct by Construction** - APIs designed to prevent misuse
5. **Separate Calculation from Doing** - Pure functions separate from side effects
6. **Dependency Inversion** - High-level code depends on abstractions
7. **Single Responsibility** - Each class/function has one clear purpose
8. **Interface Segregation** - Small, focused interfaces (InputPin, OutputPin, BidirectionalPin)
9. **Error Handling** - `std::expected<T, Error>` instead of exceptions (RTTI disabled)
## Technology Stack

See [Correct-by-Construction](https://youtu.be/nLSm3Haxz0I) and [Separate Calculating from Doing](https://youtu.be/b4p_tcLYDV0) for more details.
| Category | Technology |
|----------|------------|
| Language | C++23 |
| Build | CMake 3.27+ / Ninja |
| Compilers | Clang 18 (host), ARM GCC (embedded) |
| Testing | Google Test, pytest |
| IPC | ZeroMQ + JSON |
| Targets | STM32F3, STM32F7, nRF52832 |

## Code Quality

- **Formatting**: `.clang-format` (Google style, pointer-left alignment)
- **Linting**: `.clang-tidy` (comprehensive checks with naming convention enforcement)
- **Warnings**: All warnings are errors (`-Werror`)
- **Standard**: C++23 with strict compliance
- **RTTI**: Disabled (no exceptions, embedded-friendly)

## Development Workflow

1. **Work in DevContainer** (or use Docker)
2. **Implement on Host First** - Faster iteration, easier debugging
3. **Write Tests** - Unit tests (C++) and integration tests (Python)
4. **Run Linting** - `clang-tidy` enforces conventions
5. **Format Code** - `clang-format` on modified files
6. **Verify CI** - Ensure GitHub Actions passes
7. **Port to Hardware** - Test on actual embedded boards

## Documentation

- **[CLAUDE.md](CLAUDE.md)** - Comprehensive project documentation (architecture, conventions, workflows)
- **[test/README.md](test/README.md)** - Testing infrastructure details
- **[py/host-emulator/README.md](py/host-emulator/README.md)** - Python emulator documentation

## Contributing

This is an educational project. If you'd like to contribute or experiment:

1. Use the DevContainer for a consistent environment
2. Follow the coding conventions (enforced by clang-tidy)
3. Write tests for new features
4. Ensure CI passes before submitting PRs
5. See [CLAUDE.md](CLAUDE.md) for detailed guidelines
- **No exceptions** - Uses `std::expected<T, Error>` (RTTI disabled)
- **clang-tidy** - Enforced during build with strict naming conventions
- **clang-format** - Google style with left pointer alignment
- **-Werror** - All warnings are errors

## Implementation Status

| Component | Status |
|-----------|--------|
| Host emulation | ✅ Fully working |
| Blinky app | ✅ Fully working |
| UART echo app | ✅ Fully working |
| C++ unit tests | ✅ Fully working |
| Python integration tests | ✅ Fully working |
| Docker/DevContainer | ✅ Fully working |
| CI/CD | ✅ Fully working |
| STM32F3 Discovery | 🚧 Partial (hardware files present) |
| STM32F7 Nucleo | 🚧 Partial (hardware files present) |
| nRF52832 DK | ⚠️ Placeholder only |

## License

See LICENSE file for details.

## Acknowledgments

- Built with [CMake](https://cmake.org/), [Embedded Template Library](https://www.etlcpp.com/), [ZeroMQ](https://zeromq.org/)
- Inspired by modern C++ embedded practices and correct-by-construction design
| Host emulation | ✅ Working |
| Example apps | ✅ Working |
| C++ unit tests | ✅ Working |
| Python integration tests | ✅ Working |
| Docker/DevContainer | ✅ Working |
| CI/CD | ✅ Working |
| STM32F3/F7 | 🚧 Partial |
| nRF52832 | ⚠️ Placeholder |

## Resources

- [Correct-by-Construction](https://youtu.be/nLSm3Haxz0I)
- [Separate Calculating from Doing](https://youtu.be/b4p_tcLYDV0)
15 changes: 10 additions & 5 deletions src/apps/i2c_demo/i2c_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ auto I2CDemo::Run() -> std::expected<void, common::Error> {
const std::array<std::byte, 4> test_pattern{std::byte{0xDE}, std::byte{0xAD},
std::byte{0xBE}, std::byte{0xEF}};

// Buffer for receiving data (caller-provided, no heap allocation)
std::array<std::byte, 4> receive_buffer{};

// Main loop - write pattern, read it back, verify
while (true) {
// Write test pattern to I2C device
Expand All @@ -53,9 +56,8 @@ auto I2CDemo::Run() -> std::expected<void, common::Error> {
// Small delay between write and read
mcu::Delay(50ms);

// Read data back from I2C device
auto read_result{
board_.I2C1().ReceiveData(kDeviceAddress, test_pattern.size())};
// Read data back from I2C device into our buffer
auto read_result{board_.I2C1().ReceiveData(kDeviceAddress, receive_buffer)};
if (!read_result) {
// Turn off LED1 on read error
std::ignore = board_.UserLed1().SetLow();
Expand All @@ -64,8 +66,11 @@ auto I2CDemo::Run() -> std::expected<void, common::Error> {
}

// Verify received data matches test pattern
const auto received_span{read_result.value()};
const bool data_matches{std::ranges::equal(received_span, test_pattern)};
const size_t bytes_received{read_result.value()};
const bool data_matches{
bytes_received == test_pattern.size() &&
std::ranges::equal(std::span{receive_buffer.data(), bytes_received},
test_pattern)};

// Toggle LED1 based on verification result
if (data_matches) {
Expand Down
6 changes: 2 additions & 4 deletions src/apps/uart_echo/uart_echo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <chrono>
#include <expected>
#include <functional>
#include <span>

#include "apps/app.hpp"
#include "libs/board/board.hpp"
Expand Down Expand Up @@ -47,10 +48,7 @@ auto UartEcho::Init() -> std::expected<void, common::Error> {
auto UartEcho::Run() -> std::expected<void, common::Error> {
// Send initial greeting message
const std::string greeting{"UART Echo ready! Send data to echo it back.\n"};
const auto* greeting_bytes{
reinterpret_cast<const std::byte*>(greeting.data())};
auto send_result{board_.Uart1().Send(
std::span<const std::byte>{greeting_bytes, greeting.size()})};
auto send_result{board_.Uart1().Send(std::as_bytes(std::span{greeting}))};
if (!send_result) {
return std::unexpected(send_result.error());
}
Expand Down
Loading