Oh hey, it's super cool that you're interested in contributing. We love contributors! Before you jump in, take a minute to:
- Read our Code of Conduct
- Read our license - it's important to know what rights you have and cede when contributing to this project.
- For anything that can't be explained in one sentence, please file an issue about what you want to contribute. This gives us a chance to talk before you get too deep into things. We promise that it'll help make your experience more fun. If there's an existing issue that's related, feel free to comment on it.
- Read through our commit and pull request conventions
- If you want to help out with the user guide, read through the contributing to the user guide section.
- If contributing changes to the firmware (Gemini), read through the contributing to the firmware section.
- If contributing changes to the hardware designs, read through contributing to the hardware section.
We aren't super rigid here and we're happy to rename your PR/commit to the right format, but, if you wanna help us out you can write your commits in this form:
{category}: {short description}
{long description}
Fixes / Related to / Part of #{issue number}
For category
, add whichever of these fits:
docs
for anything related to the user guidehardware
for anything related to the hardwarefirmware
for anything related to the firmware that don't fit into any other categorybuild
for anything related to building the firmware, such as changes to the Makefiles or generated code.third_party
for anything that touches code in firmware/third_partysettings editor
for changes related to the WebMIDI settings editor at user_guide/docs/settings.md.factory
for changes related to the factory setup scripts.meta
for changes to project-wide things like the README, this file, scripts, etc
You can add multiple categories or leave it out and we'll figure it out together. :)
For pull requests, please keep in mind we'd rather you send one that's not quite ready and start a discussion that keep a big change secret and send it all at once! That's also why we recommend filing an issue before getting starting on changing the code.
Did you find a typo? Got a hot tip you want to add? Wonderful! The user guide is located in the user_guide directory. It's built using MkDocs so as long as you know Markdown you can contribute!
You don't have to test your changes to the user guide locally, but, if you want to you'll need to install some Python packages:
$ cd user_guide
$ python3 -m pip install -r requirements.txt
After that you can run MkDocs to test out your changes:
$ mkdocs serve
The hardware design was created using KiCAD. That should be all you need to edit the hardware design files.
When sending pull requests that make hardware changes, please be very descriptive about what your change does. This helps us out because it's actually kind of hard to review changes to KiCAD files.
The firmware is the most complicated bit to contribute to, but, we're happy to guide you along the way. Please take a moment to look over the guidelines here- we don't expect you to get them perfectly right the first time, but you'll have to make sure that your pull request follows these guidelines before we can merge.
Castor & Pollux's firmware is called Gemini. It's written in C using the GNU Dialect of ISO C17.
Gemini runs on a Microchip SAM D21 microcontroller. It's a 48 MHz, 32-bit ARM Cortex-M0+. The specific part used in Castor & Pollux is the ATSAMD21G18A. It has 256 kB of built-in flash memory and 32 kB of SRAM.
The primary purpose of Gemini is to read the status of the input jacks and potentiometers on the front panel and control the two oscillators based on their state. Gemini uses the built-in 12-bit multichannel ADC to read the state of the module and uses two 24-bit timers and an external 12-bit DAC to control the oscillators. You can read more about how the oscillators are controlled in Stargirl's article on Juno oscillators.
The majority of the code in Gemini is used for the input/output loop but there's some additional code for animating the LEDs, storing the user-configurable settings, and enabling configuration over USB (via MIDI SysEx).
Note that Gemini is a bare-metal ARM project. It doesn't use anything like the Arduino framework, platform.io, or any vendor-supplied frameworks like ASF4 or Harmony. It might seem a little intimidating but we've done a lot to try to make it approachable for contributors.
The firmware is organized into several directories:
src/
contains the primary headers and implementation files for the firmware. You'll notice that files are prefixed withgem_
throughout this directory.src/drivers/
contains code used to communicate with devices external to the MCU - such as the ADC and the LEDs.src/hw/
contains a very thin hardware abstraction layer (HAL) over the SAM D21 peripherals used by Gemini.src/lib/
contains non-hardware specific modules and helpers. This directory will eventually be moved to another repository (and which point it'll be inthird_party
).tests/
contains some basic tests for some of Gemini's functionality. The tests are definitely not comprehensive at this point- but it's something we plan on improving over time.third_party/
contains all dependencies needed to build the firmware. See third-party code for more details.
Try to follow these guidelines when submitting code. It's okay if you don't get them all right - that's what code review is for and we don't bite! The golden rule is:
Look at the code nearby and try to look like it
Silly stuff like whitespace and formatting is done using a formatter, so don't sweat that too much.
Names:
- Files should be named
gem_{thing}.h
&gem_{thing}.c
. - Public functions should be named using
lower_snake_case()
and should contain thegem_
prefix. The general format for function isgem_{noun}_{verb}()
Examples:gem_adc_read()
,gem_dotstar_set()
. - Static functions should be named using
lower_snake_case()
. They don't need to have any prefix unless they have a name collision. - Public structs and enums should be named using
PascalCase
and should have theGem
prefix. Examples:GemADCInput
,GemSettings
. - Structs and enums should not have
typedef
aliases, we prefer to use the fullstruct StructName
. - Macros and constants should be
SHOUTY_SNAKE_CASE
. - Static variables should have a trailing
_
. For example:sysex_callbacks_
. - Give variables descriptive names and don't try to optimize for name length. Abbreviations for common stuff like
idx
,buf
,len
, andnum
are fine if they're used in a descriptive name. For example, prefercurrent_input_idx
over justidx
.
Types:
- Use fixed-width integer types from
stdint.h
such asuint8_t
,int16_t
, etc. - Use
stdbool.h
for booleans. - Use
size_t
for anything that stores the size of something and anything that is an offset / array index. - Never use
float
ordouble
. The Cortex-M0+ does not have floating-point hardware. Use fixed-point arithmetic formfix16.h
instead.
Comments:
- Prefer
/* */
, but we don't have anything against//
. - Comment every public function and comment on anything that's non-obvious or surprisingly. Err on the side of commenting too much rather than too little- it's easy to remove in the future but it's hard to pull the context back out of your head later.
- Some of this codebase is written in the Literate Programming style. This isn't a requirement for contributors.
Printing
- Instead of using
printf
and friends fromstdio.h
use the equivalent functions fromprintf.h
. Printing tostdout
in debug builds will print to the RTT buffer. In release builds RTT is disabled.
Compiler warnings & errors:
- Gemini is compiled with almost all available warnings enabled and warnings are treated as errors.
Assertions:
- Use
static_assert
fromassert.h
for any compile-time assertions. - Use
WNTR_ASSERT
fromwntr_assert.h
for run-time asserts.WNTR_ASSERT
will restart the MCU if an assert fails in a release build which is preferable to continuing in an undefined state.
Other stuff:
- We use
#pragma once
over header include guards. - Gemini does not support dynamic memory allocation - malloc & friends are not available.
Rather than waste people's time with manually checking for silly things like whitespace or bracket locations, we just use clang-format.
You can run it against all of the code in firmware
using:
$ make format
We also use clang-tidy to check for pitfalls that the C language leaves around for us. It's not required for clang-tidy to pass. You can run it with:
$ make tidy
Gemini is built using the GNU ARM Compiler Toolchain. We highly recommend installing it directly from ARM instead of relying on package managers since they tend to be pretty out of date.
Gemini uses Python 3.9+ & Ninja build the firmware. You'll need to run the configure script to generate the ninja build file and then run ninja to build the firmware.
$ cd firmware
$ python3 configure.py
$ ninja
The firmware is built in firmware/build
and is available as gemini-firmware.elf
(it also creates .bin
and .uf2
versions as well.)
To create a release build use:
$ cd firmware
$ python3 configure.py --config release
$ ninja
If you want to try your code out quickly and don't really want/need a debugger, you can flash the gemini-firmware.uf2
file using the updating the firmware instructions in the user guide.
If you want to debug and do more advanced stuff, you'll need a hardware debugger. Adafruit has a great guide on debugging SAM D-based MCUs that is recommended reading.
You'll need a debugger such as a J-Link EDU. We highly recommend the J-Link since we use Segger's RTT for printing information in debug builds.
The pins used for debugging/programming are located on the bottom of the module:
Castor & Pollux uses the Tag-Connect TC2030 for programming. If you have a Tag-Connect TC2030-CTX-NL cable just connect it to your debugging and Castor & Pollux and you're good to go. If you don't have one, you can solder to the individual pads. The pads are numbered like this:
And correspond to the following SWD signals:
VCC
SWDIO
RESET
SWCLK
GND
SWO
(not connected)
Once you've physically connected to Castor & Pollux you can use arm-none-eabi-gdb to load, run, and debug your code. You can use JLinkRTTViewer to view debug messages. Gemini also has the Micro Trace Buffer enabled for better stack tracing.
Oh god you made it all the way down here. Well, since you're here let's talk about third-party code. Everything that isn't a direct part of Gemini's firmware should be placed in firmware/third_party
. This includes other things written by Winterbloom that are used here (like structy
for example).
We don't use git submodules, instead, we import the parts of the third-party code that we need into this repository. When adding third-party code you absolutely must include the code's LICENSE
/COPYING
/NOTICE
/etc. Copyright information must be preserved. You should also update firmware/LICENSE
to incorporate the copyright info from the imported library.