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
9 changes: 9 additions & 0 deletions examples/atmega32u4-device-arduino/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# arduino-cli builds into ~/.cache/arduino by default; these appear only if
# you compile with --build-path . or export locally.
/build/
*.elf
*.hex
*.bin
*.eep
*.lss
*.map
205 changes: 205 additions & 0 deletions examples/atmega32u4-device-arduino/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# [midi2](../..) | Device MIDI 2.0
## A complete USB MIDI 2.0 device on the ATmega32U4 (Arduino Leonardo / Pro Micro / Micro)

[![Compliant with MIDI 2.0 Workbench](https://img.shields.io/badge/MIDI%202.0%20Workbench-compliant-0d9488?labelColor=17151f)](https://github.com/midi2-dev/MIDI2.0Workbench)

![Arduino Leonardo running the ATmega32U4 MIDI 2.0 device](board/atmega32u4-leonardo.jpg)

A full USB MIDI 2.0 device on the **ATmega32U4**, the Arduino-friendly sibling of
[atmega32u4-device-baremetal](../atmega32u4-device-baremetal/): same midi2 C99
core, same USB MIDI 2.0 class descriptors, but the USB transport is the
**[midi2duino](https://github.com/sauloverissimo/midi2duino)** library
(PluggableUSB, the stock Arduino core, no LUFA and no TinyUSB) instead of LUFA.
The midi2 core builds and parses the Universal MIDI Packets; midi2duino carries
them. It flashes with a normal Arduino upload, no manual reset ritual.

It enumerates as a MIDI 2.0 device (UMP), answers UMP Stream discovery and MIDI-CI
Discovery, cycles a readable showcase of the MIDI 2.0 message surface every three
seconds, and echoes back everything it receives. The whole device is 17 KB of
flash and 1.3 KB of SRAM on a 16 MHz 8-bit part.

## Always-UMP: what the Arduino core costs us

The stock Arduino AVR core (`USBCore.cpp`) accepts the USB `SET_INTERFACE`
request with an empty handler and never forwards it to PluggableUSB, so the
firmware cannot observe the host switching the MIDIStreaming interface from
alt 0 (MIDI 1.0) to alt 1 (MIDI 2.0). This device therefore **always speaks
UMP** on the bulk endpoints: the descriptors advertise both alternate settings
for correct enumeration, a MIDI 2.0 host selects alt 1 and receives UMP, and
that is the supported path. The LUFA baremetal recipe owns the USB stack and can
track the alt switch, so it additionally offers a MIDI 1.0 fallback on alt 0;
that fallback is not reachable here. Target a MIDI 2.0 host (Linux UMP, Windows
MIDI Services, macOS).

## USB identity

| Field | Value |
|---|---|
| VID:PID | `CAFE:40D1` (educational VID, development only) |
| Product | `ATmega32U4 MIDI 2.0` |
| Manufacturer | `midi2.diy` |
| UMP Endpoint Name | `ATmega32U4 MIDI 2.0` |
| FB 0 | `Main` (Bidirectional, 1 group, MIDI 1.0 + 2.0 protocols) |
| MIDI-CI | Discovery + NAK, capabilities `0x00` (nothing over-advertised), Manufacturer `{0x7D, 0x00, 0x00}`, Model `0x0002` |

PID `0x40D1` and Model `0x0002` distinguish this device from the LUFA baremetal
recipe (`0x40D0` / `0x0001`), so a host enumerating both side by side sees
distinct endpoints and separate caches. The USB descriptor VID/PID/strings come
from the Arduino build properties (see [Build](#build)); the UMP Endpoint Name
and the MIDI-CI identity live in the firmware and are reported regardless. Forks
into real products must replace both `idVendor` and `idProduct`.

## Layering

| Layer | Owns |
|---|---|
| Arduino AVR core (external, `arduino:avr`) | USB device core: enumeration, EP0, control transfers, bulk endpoint banks |
| [midi2duino](https://github.com/sauloverissimo/midi2duino) library (external) | the PluggableUSB node: raw USB-MIDI descriptors (alt 0 MIDI 1.0, alt 1 MIDI 2.0, Group Terminal Block via class GET_DESCRIPTOR), the two bulk endpoints, the UMP word pump (RX backpressure is a plain NAK) |
| midi2 C99 core ([`../../src`](../../src), vendored under `src/`) | typed dispatch, SysEx7 reassembly, MIDI-CI responder, UMP builders |
| `src/stream_responder.c` / `src/ci_responder.c` | device identity over UMP Stream and MIDI-CI |
| `src/midi2_usb.*` | the `Midi2Usb` veneer: wires the responders, pumps USB, exposes readable `send*` helpers. No `std::function`, no heap |
| `atmega32u4-device-arduino.ino` | the application: identity setup and the showcase loop |

## Build

Requires the **Arduino AVR Boards** core and the
**[midi2duino](https://github.com/sauloverissimo/midi2duino)** library (the midi2
core is vendored under `src/`):

```bash
arduino-cli core install arduino:avr
arduino-cli lib install midi2duino
```

Compile for the Leonardo (primary target), overriding the USB identity inline:

```bash
arduino-cli compile -b arduino:avr:leonardo \
--build-property build.vid=0xCAFE \
--build-property build.pid=0x40D1 \
--build-property 'build.usb_product="ATmega32U4 MIDI 2.0"' \
--build-property 'build.usb_manufacturer="midi2.diy"' \
.
```

Upload (the CDC serial port stays enabled, so the 1200 bps touch triggers the
Caterina bootloader automatically, no manual reset):

```bash
arduino-cli upload -b arduino:avr:leonardo -p /dev/ttyACM0 .
```

**Arduino IDE users**: copy [boards.local.txt](boards.local.txt) next to the
core's `boards.txt` to apply the identity override, then open the `.ino`, pick
your board, and click Upload. Without the override the device still works and
still reports `ATmega32U4 MIDI 2.0` on the UMP wire; only the raw USB descriptor
VID/PID/strings fall back to the board's Arduino defaults.

**Other ATmega32U4 boards**: `-b arduino:avr:micro` (Arduino Micro) works the
same way. The SparkFun Pro Micro needs the SparkFun AVR board package
(`-b SparkFun:avr:promicro`); the sketch is identical, only the FQBN changes.

## Footprint

Measured with avr-gcc 7.3.0, `-Os -flto --gc-sections`, full firmware
(Arduino core USB + CDC + descriptors + pump + midi2 dispatch/proc/CI + app):

| Region | Used | Available |
|---|---|---|
| Flash | 17.1 KB (61%) | 28 KB (32 KB minus Caterina) |
| SRAM | 1.26 KB (49%) | 2.5 KB |

The CDC serial interface is kept on for the automatic-reset upload flow; that is
the main SRAM cost over the LUFA baremetal recipe, and it is what buys the
click-to-upload experience.

## Spec coverage

A readable, representative tour of the MIDI 2.0 surface. Everything below is
emitted by the showcase in `atmega32u4-device-arduino.ino`, group 0, and built
with the core's `midi2_msg_*` helpers.

| UMP message type | What the showcase sends | Spec |
|---|---|---|
| MT 0x4 Channel Voice 2.0 | Note On/Off (16-bit velocity), CC (32-bit), Program Change, Pitch Bend (32-bit), Channel Pressure, Poly Pressure, Per-Note Pitch Bend | M2-104-UM 7.4 |
| MT 0xD Flex Data | Set Tempo, Set Time Signature | M2-104-UM 7.5 |
| MT 0x3 Data64 | SysEx7 single packet | M2-104-UM 7.7 |
| MT 0x5 Data128 | SysEx8 single packet, Mixed Data Set (header + payload) | M2-104-UM 7.8/7.9 |
| MT 0xF UMP Stream | Endpoint Info, Device Identity, Endpoint Name, Product Instance Id, FB Info/Name, Stream Config (answered on request) | M2-104-UM 7.1 |

| MIDI-CI surface | Status |
|---|---|
| Discovery | answered with a per-boot randomized MUID (EEPROM boot counter + timer/frame jitter) |
| Unsupported categories | NAK; capabilities advertised as `0x00`, so nothing is over-promised |

Inbound: SysEx7 is reassembled by the core (`midi2_proc`) and delivered to the
MIDI-CI responder; any other UMP received is echoed straight back.

### What this recipe does NOT cover (and why)

- **MIDI 1.0 fallback (alt 0).** The stock Arduino core hides `SET_INTERFACE`,
so the firmware cannot switch modes. Always-UMP only. Use the LUFA baremetal
sibling for a device that is also musical on a MIDI 1.0-only host.
- **Property Exchange / Profiles.** Capabilities are `0x00` by design; this is
a Discovery-and-play device, honest about advertising nothing it does not
answer.

## Validation

- Linux (ALSA UMP): the kernel selects alt 1, reads the GTB and creates a
native MIDI 2.0 endpoint. `/proc/asound/card*/midi0` shows `Num Blocks: 1`
and the Function Block named `Main` (bidirectional), not a generic
`Group 1-1`.
- UMP echo: notes sent to the device come straight back (`amidi -d`).
- MIDI-CI: Discovery Reply with a per-boot randomized MUID.
- Windows MIDI Services: enumerates as `ATmega32U4 MIDI 2.0`, Native data
format UMP, one bidirectional Function Block.
- LED_BUILTIN toggles once per showcase cycle as a heartbeat.

Enumerated and monitored with the Windows MIDI Services Console:

![Endpoint list](board/banner.png)

The device appears as a native MIDI 2.0 endpoint alongside the standard loopbacks.

![Endpoint properties](board/properties.png)

Native data format UMP, MIDI 1.0 + 2.0 protocols, one static bidirectional
Function Block (`Main`, Group 1).

![Message monitor](board/stream.png)

The showcase on the wire: MIDI 2.0 Channel Voice, Flex Data, SysEx7, SysEx8 and
Mixed Data Set. The console's Decoded column does not yet render SysEx8 / MDS /
Flex payloads, so those rows read blank there; the raw UMP words in the Data
column are correct.

Pair with a MIDI 2.0 host recipe such as the RP2040/RP2350 hosts under
[midi2cpp/examples](https://github.com/sauloverissimo/midi2cpp), or plug it into
the same host you validate the
[atmega32u4-device-baremetal](../atmega32u4-device-baremetal/) sibling with.

## What lives where

```
atmega32u4-device-arduino/
atmega32u4-device-arduino.ino application: identity + showcase loop
boards.local.txt USB identity override for the Arduino core
board/ board photo + Windows MIDI Services captures
src/
midi2_usb.h / .cpp Midi2Usb veneer (begin/task + send helpers)
stream_responder.c / .h UMP Stream identity responder
ci_responder.c / .h MIDI-CI Discovery responder
board.h UMP Endpoint Name
midi2_*.c / .h midi2 C99 core (symlinked from ../../src)
```

The USB transport comes from the [midi2duino](https://github.com/sauloverissimo/midi2duino)
library, installed separately; the midi2 C99 core is vendored under `src/` via
symlink.

## License

MIT, same as the midi2 library. Builds on the Arduino AVR core (LGPL, linked as
the platform) with a standard Arduino sketch; no core sources are redistributed
here.
67 changes: 67 additions & 0 deletions examples/atmega32u4-device-arduino/atmega32u4-device-arduino.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// midi2 / atmega32u4-device-arduino
// USB MIDI 2.0 device on the ATmega32U4 (Arduino Leonardo / Pro Micro / Micro).
//
// The Arduino-friendly sibling of examples/atmega32u4-device-baremetal: same
// midi2 C99 core, same USB MIDI 2.0 class descriptors, but the USB transport is
// the midi2duino library (PluggableUSB, stock Arduino core) instead of LUFA, so
// it flashes with a normal Arduino upload (no manual double-tap reset).
//
// Requires: Arduino AVR Boards core (arduino:avr) and the midi2duino library
// (Library Manager, or github.com/sauloverissimo/midi2duino). The midi2 C99
// core and the responder glue live under src/ and compile with the sketch.
//
// USB identity (VID 0xCAFE, PID 0x40D1, Manufacturer "midi2.diy",
// Product "ATmega32U4 MIDI 2.0") comes from boards.local.txt / the build
// properties in the README. On the UMP wire: Endpoint Name "ATmega32U4 MIDI 2.0",
// one bidirectional Function Block "Main" on group 0.
//
// Always-UMP: the stock AVR core does not surface SET_INTERFACE, so this device
// always speaks MIDI 2.0 (UMP) and targets MIDI 2.0 hosts. See the README.

#include "src/midi2_usb.h"

Midi2Usb midi;

static uint32_t lastDemo = 0;
static uint8_t demoNote = 60;

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
midi.begin();
}

void loop() {
midi.task(); // pump USB, answer Discovery / MIDI-CI, echo input

uint32_t now = millis();
if (now - lastDemo < 3000)
return;
lastDemo = now;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

// A readable tour of the MIDI 2.0 surface, one message family per line.
// Channel Voice 2.0 (32-bit resolution)
midi.sendNoteOn(0, demoNote, 0xC000);
delay(150);
midi.sendNoteOff(0, demoNote);
midi.sendControlChange(0, 74, 0x80000000); // filter cutoff, half scale
midi.sendPitchBend(0, 0xC0000000);
midi.sendChannelPressure(0, 0x60000000);
midi.sendProgramChange(0, 42);
midi.sendPolyPressure(0, demoNote, 0x40000000);
midi.sendPerNotePitchBend(0, demoNote, 0x90000000); // MIDI 2.0 exclusive

// Flex Data
midi.sendTempo(50000000); // 120 BPM in 10-ns ticks per quarter
midi.sendTimeSignature(4, 2); // 4/4

// Data messages: SysEx7 + SysEx8 + Mixed Data Set (educational mfr 0x7D)
static const uint8_t sx7[] = {0x7D, 0x01, 0x02, 0x03, 0x04, 0x05};
midi.sendSysEx7(sx7, sizeof sx7);
static const uint8_t sx8[] = {0x7D, 0x01, 0x02, 0x03, 0x04};
midi.sendSysEx8(0, sx8, sizeof sx8);
static const uint8_t mds[] = {0x4D, 0x44, 0x53, 0x21}; // "MDS!"
midi.sendMds(1, mds, sizeof mds);

demoNote = (demoNote >= 72) ? 60 : (uint8_t)(demoNote + 1);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions examples/atmega32u4-device-arduino/boards.local.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# USB identity override for the atmega32u4-device-arduino recipe.
#
# The Arduino AVR core takes the USB descriptor VID / PID / strings from these
# build properties. Copy this file next to the core's boards.txt to make the
# Arduino IDE (and arduino-cli) build the device with the midi2.diy identity:
#
# ~/.arduino15/packages/arduino/hardware/avr/<version>/boards.local.txt
# (Linux; adjust the path on macOS/Windows)
#
# arduino-cli users can skip the copy and pass the same values inline instead:
#
# --build-property build.vid=0xCAFE --build-property build.pid=0x40D1 \
# --build-property 'build.usb_product="ATmega32U4 MIDI 2.0"' \
# --build-property 'build.usb_manufacturer="midi2.diy"'
#
# Without the override the device still works and still reports "ATmega32U4
# MIDI 2.0" as its UMP Endpoint Name and MIDI-CI identity (that lives in the
# firmware); only the raw USB descriptor VID/PID/strings fall back to the
# board's Arduino defaults.

leonardo.build.vid=0xCAFE
leonardo.build.pid=0x40D1
leonardo.build.usb_product="ATmega32U4 MIDI 2.0"
leonardo.build.usb_manufacturer="midi2.diy"

micro.build.vid=0xCAFE
micro.build.pid=0x40D1
micro.build.usb_product="ATmega32U4 MIDI 2.0"
micro.build.usb_manufacturer="midi2.diy"
18 changes: 18 additions & 0 deletions examples/atmega32u4-device-arduino/src/board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* Board identity for the Arduino recipe.
*
* Only the UMP Endpoint Name lives here (consumed by stream_responder.c).
* The USB descriptor strings (Manufacturer / Product) come from the Arduino
* core build properties (USB_MANUFACTURER / USB_PRODUCT), set in
* boards.local.txt, not from a LUFA-style wide string. The user LED is driven
* from the sketch through LED_BUILTIN, so no register map is needed here.
*
* All ATmega32U4 boards (Leonardo, Pro Micro, Micro) share this one name:
* they are the same silicon and enumerate identically. The physical board is
* distinguished by its photos and README, not on the wire.
*/
#ifndef BOARD_H
#define BOARD_H

#define BOARD_NAME "ATmega32U4 MIDI 2.0"

#endif
63 changes: 63 additions & 0 deletions examples/atmega32u4-device-arduino/src/ci_responder.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include <avr/io.h>
#include <avr/eeprom.h>
#include "ci_responder.h"
#include <midi2duino.h>
#include "midi2_ci.h"

/* Same educational identity as the stream responder. NOTE: the CI builder
* serializes the manufacturer LSB-first (wire Byte 1 = value bits 0..7),
* the OPPOSITE of midi2_msg_stream_device_identity. 0x7D must sit in the
* low byte here to reach wire Byte 1. */
#define CI_MFR 0x0000007DUL /* wire: {0x7D, 0x00, 0x00} */
#define CI_FAMILY 0x0001
#define CI_MODEL 0x0002 /* distinct from the baremetal recipe (0x0001) */
#define CI_VERSION 0x00010000UL

static midi2_ci_state g_ci;
static uint8_t g_profiles[1][5]; /* slots exist, none added */
static midi2_ci_property g_props[1];

/* xorshift32 for the 28-bit MUID on a part with no TRNG. The seed mixes an
* EEPROM boot counter (monotonic across power cycles, so two boots can never
* repeat a seed) with Timer1 phase and the USB frame number (jitter). */
static uint32_t g_rng_state;
static uint32_t EEMEM ee_boot_count;

static uint32_t ci_rng(void *ctx) {
(void)ctx;
if (g_rng_state == 0) {
uint32_t boots = eeprom_read_dword(&ee_boot_count);
if (boots == 0xFFFFFFFFUL)
boots = 0; /* fresh EEPROM */
eeprom_update_dword(&ee_boot_count, boots + 1);
g_rng_state = ((boots + 1) * 0x9E3779B9UL) /* golden-ratio spread */
^ (((uint32_t)UDFNUMH << 24) | ((uint32_t)UDFNUML << 16) | TCNT1);
if (g_rng_state == 0)
g_rng_state = 1;
}
g_rng_state ^= g_rng_state << 13;
g_rng_state ^= g_rng_state >> 17;
g_rng_state ^= g_rng_state << 5;
return g_rng_state;
}

static uint32_t ci_write_fn(const uint32_t *words, uint32_t count, void *ctx) {
(void)ctx;
for (uint32_t i = 0; i < count; i++)
if (!midi2duino_write_word(words[i]))
return i; /* ring full: partial write reported */
Comment on lines +46 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Write MIDI-CI SysEx7 packets atomically

MIDI-CI replies are emitted as two-word SysEx7 UMP packets, but this callback queues each word separately; if the TX ring has only one free slot while a Discovery reply is being generated, the first word is accepted and the second fails, leaving an incomplete UMP at the head of the ring that can block or corrupt later USB output. Use the complete-message write path so either the whole packet is queued or none of it is.

Useful? React with 👍 / 👎.

return count;
}

void ci_responder_init(void) {
midi2_ci_init(&g_ci, ci_rng(0), g_profiles, 1, g_props, 1);
midi2_ci_set_rng(&g_ci, ci_rng, 0);
midi2_ci_set_write_fn(&g_ci, ci_write_fn, 0);
midi2_ci_set_identity(&g_ci, CI_MFR, CI_FAMILY, CI_MODEL, CI_VERSION);
midi2_ci_set_capabilities(&g_ci, 0x00); /* Discovery only, honest */
midi2_ci_set_nak_on_unknown(&g_ci, true);
}

void ci_responder_feed_sysex7(uint8_t group, const uint8_t *data, uint16_t len) {
(void)midi2_ci_process_sysex(&g_ci, group, data, len);
}
Loading
Loading