---
git clone --recursive https://github.com/Brokezawa/nimphea cd nimphea
nimble init_libdaisy
All wrappers follow this structure:
1. Type Definitions (in module)
type MyPeripheral* {.importcpp: "daisy::MyPeripheral", header: "per/myperipheral.h".} = object ## Brief description MyConfig* {.importcpp: "daisy::MyPeripheral::Config".} = object ## Configuration structure field1*: cint field2*: bool
2. Constants and Enums
type MyMode* = enum MODE_A = 0 MODE_B = 1 MODE_C = 2
3. Procedures
proc init*(this: var MyPeripheral, config: MyConfig): bool {.importcpp: "#.Init(#)".} = ## Initialize the peripheral ## ## **Parameters:** ## - `config` - Configuration structure ## ## **Returns:** true on success discard proc read*(this: var MyPeripheral): cint {.importcpp: "#.Read()".} = ## Read from peripheral ## ## **Returns:** Read value discard
4. Macro for Includes
In nimphea_macros.nim, add:
macro emitMyPeripheralIncludes*(): untyped = when defined(useMyPeripheral): result = quote do: {.emit: """ #include "per/myperipheral.h" """.} else: result = newStmtList()
5. Module Setup
At top of your module file:
import nimphea_macros {.define: useMyPeripheral.} emitMyPeripheralIncludes()
Let's wrap the DAC as an example:
Step 1: Study the C++ Interface
Look at libDaisy/src/per/dac.h:
namespace daisy { class DacHandle { public: enum Channel { CHN_1, CHN_2, CHN_BOTH }; struct Config { Channel chn; Mode mode; }; void Init(Config config); void WriteValue(Channel channel, uint16_t value); }; }
Step 2: Create Nim Types
# In src/per/dac.nim import nimphea_macros type DacChannel* = enum DAC_CHN_1 = 0 DAC_CHN_2 = 1 DAC_CHN_BOTH = 2 DacHandle* {.importcpp: "daisy::DacHandle", header: "per/dac.h".} = object DacConfig* {.importcpp: "daisy::DacHandle::Config".} = object chn*: DacChannel
Step 3: Add Procedures
proc init*(this: var DacHandle, config: DacConfig) {.importcpp: "#.Init(#)".} = ## Initialize DAC discard proc writeValue*(this: var DacHandle, channel: DacChannel, value: uint16) {.importcpp: "#.WriteValue(#, #)".} = ## Write value to DAC channel discard
Step 4: Add Macro
In src/nimphea_macros.nim:
macro emitDacIncludes*(): untyped = when defined(useDAC): result = quote do: {.emit: """ #include "per/dac.h" """.} else: result = newStmtList()
Step 5: Use Macro in Module
At top of per/dac.nim:
{.define: useDAC.} emitDacIncludes()
Step 6: Create Example
Create examples/dac_simple.nim:
import ../src/nimphea import ../src/per/dac var hw = newDaisySeed() var dac: DacHandle proc main() = hw.init() var dacCfg: DacConfig dacCfg.chn = DAC_CHN_1 dac.init(dacCfg) var value: uint16 = 0 while true: dac.writeValue(DAC_CHN_1, value) value = (value + 100) mod 4096 hw.delayMs(10) when isMainModule: main()
Step 7: Test
cd nimphea # Syntax check your new wrapper nimble test # Build your new wrapper example for ARM nimble make dac_simple # Flash to hardware (requires Daisy in bootloader mode) nimble flash dac_simple
Expected output:
Step 8: Document
Add to API_REFERENCE.md:
### DAC (per/dac.nim) Digital to Analog Converter for CV outputs. **Types:** - `DacHandle` - DAC controller - `DacChannel` - Channel selection (CHN_1, CHN_2, CHN_BOTH) - `DacConfig` - Configuration structure **Functions:** - `init(dac, config)` - Initialize DAC - `writeValue(dac, channel, value)` - Write 12-bit value (0-4095) **Example:** See `examples/dac_simple.nim`
1. Forgetting the Macro
If you forget emitDacIncludes(), you'll get:
Error: undeclared identifier: 'DacHandle'
Solution: Add the macro call at module top.
2. Wrong C++ Signature
If the importcpp pattern is wrong:
# WRONG proc init*(this: var DacHandle) {.importcpp: "Init".} # RIGHT proc init*(this: var DacHandle) {.importcpp: "#.Init()".}
The # is crucial - it represents the object.
3. Type Mismatches
Ensure Nim types match C++ types:
4. Missing Exports
Remember the * for public symbols:
# WRONG (not exported) proc init(this: var DacHandle) # RIGHT (exported) proc init*(this: var DacHandle)
Nimphea uses a two-tier testing approach:
What: Pure logic testing for data structures and utilities Where: Runs on your development computer (no hardware required) Framework: nim-unittest2
# Run all unit tests nimble test_unit # Expected output: # [Suite] FixedStr - Basics ...... (0.00s) # [Suite] FixedStr - Edge Cases .... (0.00s) # [Summary] 22 tests run (0.00s): 22 OK, 0 FAILED, 0 SKIPPED # All unit tests passed!
When to write unit tests:
When NOT to write unit tests:
For detailed unit testing information, see tests/README.md.
What: Full functionality testing on real Daisy Seed hardware Where: Runs on physical Daisy boards Framework: Manual testing with documented examples
All examples must pass compilation checks. We use a two-tier testing approach:
Tier 1: Quick Syntax Check (required for all PRs)
# Fast (~7 seconds) - checks syntax and types only nimble test
What it checks:
Tier 2: Full Build Test (required before releases)
# Slow (~45 minutes) - builds all examples with ARM toolchain nimble test_build
What it checks:
For Contributors:
Expected output from `nimble test`:
=== Quick Syntax Check (all examples) === ============================================================ Checking blink ... PASS Checking audio_demo ... PASS Checking pod_demo ... PASS ... ============================================================ SUMMARY: Passed: 43 Failed: 0 ============================================================ All examples passed syntax check! Note: This only checks syntax. For full build validation: nimble test_build # Compile all examples with ARM toolchain
If you have hardware, test your feature:
Document test results in your PR.
Ensure you haven't broken existing functionality:
# Quick syntax check (required) nimble test # Run unit tests (required if you modified testable modules) nimble test_unit # For major changes: full build validation (optional but recommended) nimble test_build # Build and test 2-3 existing examples on hardware (if available) nimble make blink nimble flash blink # Try another example nimble make audio_demo nimble flash audio_demo
All existing examples should:
When adding testable modules, create corresponding unit tests:
1. Create test file: tests/test_yourmodule.nim
import unittest2 import ../src/nimphea_yourmodule suite "YourModule: Basic Functionality": test "should initialize correctly": var obj = initYourModule() check obj.someProperty == expectedValue suite "YourModule: Edge Cases": test "should handle empty state": var obj = initYourModule() check obj.isEmpty() == true
2. Add to master runner: In tests/all_tests.nim:
import test_yourmodule
3. Run tests:
nimble test_unit
Test Organization:
See tests/test_fixedstr.nim for a comprehensive example.
Community hardware testing is essential to validate examples on different Daisy boards and peripherals.
Testing Levels:
Compilation Testing (Required for all PRs)
cd /path/to/nimphea nimble testFast syntax checking without ARM compilation. All 40+ examples must pass.
Basic Hardware Testing
nimble make blink nimble flash blinkLED, GPIO, Audio, SDRAM, USB, RNG, Timers
How to Help:
See the testing section in Contributing Guide for detailed testing procedures.
# Types: PascalCase with * for export type DaisySeed* = object # Enums: PascalCase (use descriptive names) type PinMode* = enum INPUT OUTPUT ANALOG # Procedures: camelCase with * for export proc setLed*(hw: var DaisySeed, state: bool) # Constants: UPPER_SNAKE_CASE const MAX_BUFFER_SIZE* = 1024 # Variables: camelCase var myValue = 42
File Structure:
## Module documentation ## ## Detailed description of what this module does. ## Explains what C++ functionality it wraps. # 1. Imports import nimphea_macros # 2. Type definitions type MyType* = object # 3. Constants const MY_CONST* = 42 # 4. Macro calls {.define: useMyFeature.} emitMyFeatureIncludes() # 5. Procedures proc myProc*() = discard
Location Convention:
Use ## for documentation:
proc importantFunction*(param: int): bool = ## Brief one-line description ## ## More detailed explanation if needed. ## Multiple paragraphs okay. ## ## **Parameters:** ## - `param` - Description of parameter ## ## **Returns:** Description of return value ## ## **Example:** ##nim ## if importantFunction(42): ## echo "Success!" ##
result = true
# Good import std/strutils import std/sequtils import nimphea_macros import per/adc import per/gpio # Procedures with one blank line between them proc foo*() = let x = 42 echo x proc bar*() = let y = 24 echo y # Section separator (two blank lines before major sections) const CONSTANT = 1
1. Module-Level
Every .nim file should start with:
## ModuleName ## ========== ## ## Brief one-line description of what this module provides. ## ## Detailed explanation of what C++ functionality it wraps, ## and how to use it. Include any important constraints or ## performance considerations. ## ## **Example:** ##nim ## import src/per/dac ## ## var dac: DacHandle ## var cfg: DacConfig ## cfg.chn = DAC_CHN_1 ## dac.init(cfg) ## dac.writeValue(DAC_CHN_1, 2048) ##
2. Type Definitions
type MyType* = object ## Description of what this type represents ## ## **Fields:** ## - `field1` - Description field1*: int
3. Procedures
All public procs need documentation (see above).
4. Examples
Every new peripheral wrapper needs a corresponding example in examples/:
5. API Reference
Add entry to docs/API_REFERENCE.md for new modules/features:
### DAC (per/dac.nim) Digital to Analog Converter for CV outputs. **Types:** - `DacHandle` - DAC controller - `DacChannel` - Channel selection (CHN_1, CHN_2, CHN_BOTH) - `DacConfig` - Configuration structure **Functions:** - `init(dac, config)` - Initialize DAC - `writeValue(dac, channel, value)` - Write 12-bit value (0-4095) **Example:** See `examples/dac_simple.nim` **Performance:** Negligible overhead (~1% of audio callback)
The Nim wrapper should have minimal performance overhead. For audio code, this is critical.
Measurement Areas:
Best Practices:
Use `{.inline.}` pragma for hot-path functions
proc getValue*(buffer: Buffer, index: int): float32 {.inline.} = result = buffer.data[index]
Avoid heap allocation in real-time audio code
# BAD - allocates on heap every call proc process*(data: seq[float32]) = let temp = newSeq[float32](data.len) # GOOD - use stack or pre-allocated buffers proc process*(data: var openArray[float32]) = # Process in-place
Minimize branching in audio callback
# AVOID - branch prediction misses hurt performance for sample in audio: if someCondition: sample = process1() else: sample = process2() # BETTER - loop unrolling or SIMD-friendly code
Profile before optimizing - use actual measurements
# Build with performance monitoring enabled nimble make myexample # Check flash and RAM usage arm-none-eabi-size build/myexample.elf
Provide both checked and unchecked variants when appropriate:
# Safe version (bounds checked) proc get*(buffer: var Buffer, index: int): float32 = assert index >= 0 and index < buffer.len result = buffer.data[index] # Fast version (no checks) proc getUnchecked*(buffer: var Buffer, index: int): float32 {.inline.} = result = buffer.data[index] # Unsafe version (for extreme performance needs) proc getUnsafe*(buffer: var Buffer, index: int): float32 {.inline.} = {.emit: "return ((float*)#1)[#2];".}
At v1.0.0 and beyond:
Breaking Change Requirements (if necessary for major version):
When breaking changes are absolutely necessary:
Example Breaking Change (v2.0.0 hypothetically):
### Breaking Changes in v2.0.0
**per/adc.nim:**
- `initAdc()` → `init()` (renamed for API consistency)
- Old: `initAdc(hw, pin, channel)`
- New: `var adc: AdcHandle; adc.init(pin, channel)`
- Migration: see the migration notes below for step-by-step guidance
**Migration path:**
- v1.8.0: Old API marked `{.deprecated.}`, new API available
- v1.9.0: Deprecation warnings shown
- v2.0.0: Old API removed entirely
Checklist:
Use clear, descriptive commit messages:
# Good git commit -m "Add DAC wrapper with example" git commit -m "Fix I2C timeout handling" git commit -m "Update SPI documentation" # Bad git commit -m "stuff" git commit -m "wip" git commit -m "fix"
Format:
Short summary (50 chars or less) More detailed explanation if needed. Wrap at 72 characters. Explain what changed and why. - Bullet points okay - For multiple changes Fixes #123 # Reference issue if applicable
1. Push to your fork
git push origin feature/my-new-feature
2. Create PR on GitHub
## Description Brief description of what this PR does and why. ## Type of Change - New feature (peripheral wrapper, board support, etc.) - Bug fix (compilation, runtime, logic error) - Documentation (README, API_REFERENCE, guides) - Performance improvement - Refactoring (no functional change) - Other (describe) ## Related Issues Fixes #123 (if applicable) Relates to #456 (if applicable) ## Changes Made - Bullet point describing each change - Second change - Third change ## Testing Done - All examples compile. - New example works and follows conventions. - Tested on hardware. - Ran nimble clear and rebuilt from scratch. ## Checklist - Code follows Nim style guidelines. - Code is documented with ## comments. - New public APIs added to API_REFERENCE.md. - No breaking changes to existing API. - Commits are clean and descriptive. - No hardcoded paths or personal configurations. ## Additional Notes Any additional context or notes for reviewers...
4. Respond to Reviews
5. After Approval
Current needs include:
1. Hardware Testing
2. More Examples
3. Documentation
4. Bug Fixes & Improvements
Questions about contributing?
Stuck implementing a feature?
Found a Bug?
Contributors will be:
Thank you for contributing to Nimphea!