diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..397951f --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,2 @@ +micropython +pico-sdk diff --git a/firmware/README.md b/firmware/README.md index 2f4d306..9a6213b 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -5,13 +5,18 @@ Custom micropython build for the Olimex RP2350B-XL. Comes with some features ena ## build instructions 0. Make sure to have a compiler toolchain supporting the RP2 architecture installed (Mac: `brew install gcc-arm-embedded`) -1. Clone the `micropython` repo -2. From within the `micropython` repo root, run `make -C mpy-cross` -3. Copy the `OLIMEX_PICO2_XL` folder to `micropython/ports/rp2/boards` if it doesn't exist; overwrite it if it does -4. Change to the `boards/rp2` folder -5. Run `make BOARD=OLIMEX_PICO2_XL submodules` to install dependencies -6. Run `make BOARD=OLIMEX_PICO2_XL clean` to remove previous build artefacts (if any) -7. Run `make BOARD=OLIMEX_PICO2_XL` to build the firmware +1. Clone *[pico-sdk](https://github.com/raspberrypi/pico-sdk)* (to this folder or somewhere else on your machine) +2. Change to the *pico-sdk* folder and run `git submodule update --init` +3. Point an environment variable `PICO_SDK_PATH` to the *pico-sdk* folder +4. Install *[picotool](https://github.com/raspberrypi/picotool)* +5. Clone the `micropython` repo (to this folder or somewhere else on your machine) +6. From within the `micropython` repo root, run `make -C mpy-cross` +7. Copy the `OLIMEX_PICO2_XL` folder to `micropython/ports/rp2/boards` if it doesn't exist; overwrite it if it does +8. Change to the `micropython/ports/rp2` folder +9. Run `make BOARD=OLIMEX_PICO2_XL submodules` to install dependencies +10. Run `make BOARD=OLIMEX_PICO2_XL clean` to remove previous build artefacts (if any) +11. Run `make BOARD=OLIMEX_PICO2_XL` to build the firmware +12. To rebuild: `make BOARD=OLIMEX_PICO2_XL clean && make BOARD=OLIMEX_PICO2_XL` ## flashing diff --git a/firmware/examples/nanogrid-drum-rack/README.md b/firmware/examples/nanogrid-drum-rack/README.md new file mode 100644 index 0000000..a4a24ca --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/README.md @@ -0,0 +1,20 @@ +# MIDI Drum Rack example + +Press to send a MIDI note in the range from C1 to D#2. Buttons are +ordered in the same way the pads are in Ableton Drum Rack: with the USB +port poiting from the player left-to-right, bottom-to-top. + + +## build + +1. Make a micropython build with the libraries added: + +```bash +cd $MICROPYTHON_PATH/ports/rp2 +make BOARD=OLIMEX_PICO2_XL clean && make V=1 BOARD=OLIMEX_PICO2_XL FROZEN_MANIFEST={path-to-neogrid-project}/firmware/examples/nanogrid-drum-rack/manifest.py +``` + +2. Flush the board with the resulting uf2 (see + [firmware/README.md](/firmware/README.md)) + +3. Copy the `main.py` to the board diff --git a/firmware/examples/nanogrid-drum-rack/main.py b/firmware/examples/nanogrid-drum-rack/main.py new file mode 100644 index 0000000..53bd603 --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/main.py @@ -0,0 +1,210 @@ +# MicroPython USB MIDI example +# +# This example demonstrates creating a custom MIDI device. +# +# To run this example: +# +# 1. Make sure `usb-device-midi` is installed via: mpremote mip install usb-device-midi +# +# 2. Run the example via: mpremote run midi_example.py +# +# 3. mpremote will exit with an error after the previous step, because when the +# example runs the existing USB device disconnects and then re-enumerates with +# the MIDI interface present. At this point, the example is running. +# +# 4. To see output from the example, re-connect: mpremote connect PORTNAME +# +# +# MIT license; Copyright (c) 2023-2024 Angus Gratton +import usb.device +from usb.device.midi import MIDIInterface +import time + +from machine import Pin + +import array, time +import rp2 + +NUM_COL = 4 +NUM_ROW = 4 +NUM_LEDS = NUM_COL * NUM_ROW +PIN_NUM = 16 +brightness = 0.2 + +@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24) +def ws2812(): + T1 = 2 + T2 = 5 + T3 = 3 + wrap_target() + label("bitloop") + out(x, 1) .side(1) [T3 - 1] + jmp(not_x, "do_zero") .side(0) [T1 - 1] + jmp("bitloop") .side(0) [T2 - 1] + label("do_zero") + nop() .side(1) [T2 - 1] + wrap() + +def init_leds(): + # Create the StateMachine with the ws2812 program, outputting on pin + sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM)) + + # Start the StateMachine, it will wait for data on its FIFO. + sm.active(1) + + # Display a pattern on the LEDs via an array of LED RGB values. + ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + + ########################################################################## + def show_all(): + dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + for i, c in enumerate(ar): + r = int(((c >> 8) & 0xFF) * brightness) + g = int(((c >> 16) & 0xFF) * brightness) + b = int((c & 0xFF) * brightness) + dimmer_ar[i] = (g<<16) + (r<<8) + b + sm.put(dimmer_ar, 8) + + def set_led(i, color): + row = i // NUM_COL + col = i - row * NUM_COL + new_col = NUM_COL - col - 1 + new_row = NUM_ROW - row - 1 if new_col % 2 == 0 else row + ar[new_col * NUM_ROW + new_row] = (color[1]<<16) + (color[0]<<8) + color[2] + + return (set_led, show_all) + +class MIDIExample(MIDIInterface): + # Very simple example event handler functions, showing how to receive note + # and control change messages sent from the host to the device. + # + # If you need to send MIDI data to the host, then it's fine to instantiate + # MIDIInterface class directly. + + def on_open(self): + super().on_open() + print("Device opened by host") + + def on_note_on(self, channel, pitch, vel): + print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}") + + def on_note_off(self, channel, pitch, vel): + print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}") + + def on_control_change(self, channel, controller, value): + print(f"RX Control channel {channel} controller {controller} value {value}") + + +m = MIDIExample() +# Remove builtin_driver=True if you don't want the MicroPython serial REPL available. +usb.device.get().init(m, builtin_driver=True) + +print("Waiting for USB host to configure the interface...") + +while not m.is_open(): + time.sleep_ms(100) + +# TX constants +CHANNEL = 0 +PITCH_C1 = 37 +CONTROLLER = 64 + +control_val = 0 + +def init_btns(): + row_pins = [ + Pin(0, Pin.OUT), + Pin(1, Pin.OUT), + Pin(2, Pin.OUT), + Pin(3, Pin.OUT), + ] + + col_pins = [ + Pin(32, Pin.IN, Pin.PULL_UP), + Pin(33, Pin.IN, Pin.PULL_UP), + Pin(34, Pin.IN, Pin.PULL_UP), + Pin(35, Pin.IN, Pin.PULL_UP), + ] + + pressed_keys = set() + + # reset cols + for pin in row_pins: + pin.value(1) + + def loop(): + pressed_keys.clear() + for row, row_pin in enumerate(row_pins): + # set only active pin low, keep rest high + row_pin.value(0) + for col, col_pin in enumerate(col_pins): + key = ( + (NUM_ROW - row - 1) + + (NUM_COL - col - 1) * NUM_ROW + ) + if not col_pin.value(): + pressed_keys.add(key) + row_pin.value(1) + return set(pressed_keys) + + return loop + +btn_loop = init_btns() +turned_on = set() + +(set_led, show_all) = init_leds() + +# for i in range(NUM_LEDS): +# set_led(i, CYAN) +# for j in range(NUM_LEDS): +# if i != j: +# set_led(j, BLACK) +# show_all() +# time.sleep(2) +# + +CYAN = (0, 255, 255) +BLACK = (0, 0, 0) + +print("Starting loop...") +while m.is_open(): + time.sleep_ms(5) + # print(f"TX Note On channel {CHANNEL} pitch {PITCH_C1}") + # m.note_on(CHANNEL, PITCH_C1) # Velocity is an optional third argument + # time.sleep(0.5) + # print(f"TX Note Off channel {CHANNEL} pitch {PITCH_C1}") + # m.note_off(CHANNEL, PITCH_C1) + # time.sleep(1) + # print(f"TX Control channel {CHANNEL} controller {CONTROLLER} value {control_val}") + # m.control_change(CHANNEL, CONTROLLER, control_val) + # control_val += 1 + # if control_val == 0x7F: + # control_val = 0 + # time.sleep(1) + + pressed = btn_loop() + + to_turn_on = pressed.difference(turned_on) + to_turn_off = turned_on.difference(pressed) + + if len(to_turn_on): + print("to_turn_on", to_turn_on) + + if len(to_turn_off): + print("to_turn_off", to_turn_off) + + + for key in to_turn_on: + m.note_on(CHANNEL, PITCH_C1 + key - 1) + set_led(key, CYAN) + turned_on.add(key) + + for key in to_turn_off: + m.note_off(CHANNEL, PITCH_C1 + key - 1) + turned_on.remove(key) + set_led(key, BLACK) + + show_all() + +print("USB host has reset device, example done.") + diff --git a/firmware/examples/nanogrid-drum-rack/manifest.py b/firmware/examples/nanogrid-drum-rack/manifest.py new file mode 100644 index 0000000..e53101e --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/manifest.py @@ -0,0 +1,4 @@ +metadata(version="0.1.0") +include("$(PORT_DIR)/boards/manifest.py") +require("usb-device") +require("usb-device-midi") diff --git a/firmware/examples/nanogrid-lights-demo/main.py b/firmware/examples/nanogrid-lights-demo/main.py new file mode 100755 index 0000000..6d753fb --- /dev/null +++ b/firmware/examples/nanogrid-lights-demo/main.py @@ -0,0 +1,144 @@ +# Example using PIO to drive a set of WS2812 LEDs. + +import array, time +from machine import Pin +import rp2 + +# Configure the number of WS2812 LEDs. +NUM_LEDS = 16 +PIN_NUM = 16 +brightness = 0.2 + +@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24) +def ws2812(): + T1 = 2 + T2 = 5 + T3 = 3 + wrap_target() + label("bitloop") + out(x, 1) .side(1) [T3 - 1] + jmp(not_x, "do_zero") .side(0) [T1 - 1] + jmp("bitloop") .side(0) [T2 - 1] + label("do_zero") + nop() .side(1) [T2 - 1] + wrap() + + +# Create the StateMachine with the ws2812 program, outputting on pin +sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM)) + +# Start the StateMachine, it will wait for data on its FIFO. +sm.active(1) + +# Display a pattern on the LEDs via an array of LED RGB values. +ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + +########################################################################## +def pixels_show(): + dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + for i,c in enumerate(ar): + r = int(((c >> 8) & 0xFF) * brightness) + g = int(((c >> 16) & 0xFF) * brightness) + b = int((c & 0xFF) * brightness) + dimmer_ar[i] = (g<<16) + (r<<8) + b + sm.put(dimmer_ar, 8) + time.sleep_ms(10) + +def pixels_set(i, color): + ar[i] = (color[1]<<16) + (color[0]<<8) + color[2] + +def pixels_fill(color): + for i in range(len(ar)): + pixels_set(i, color) + +def color_chase(color, wait): + for i in range(NUM_LEDS): + pixels_set(i, color) + time.sleep(wait) + pixels_show() + time.sleep(0.2) + +def wheel(pos): + # Input a value 0 to 255 to get a color value. + # The colours are a transition r - g - b - back to r. + if pos < 0 or pos > 255: + return (0, 0, 0) + if pos < 85: + return (255 - pos * 3, pos * 3, 0) + if pos < 170: + pos -= 85 + return (0, 255 - pos * 3, pos * 3) + pos -= 170 + return (pos * 3, 0, 255 - pos * 3) + +def rainbow_cycle(wait): + for j in range(255): + for i in range(NUM_LEDS): + rc_index = (i * 256 // NUM_LEDS) + j + pixels_set(i, wheel(rc_index & 255)) + pixels_show() + time.sleep(wait) + +BLACK = (0, 0, 0) +RED = (255, 0, 0) +YELLOW = (255, 150, 0) +GREEN = (0, 255, 0) +CYAN = (0, 255, 255) +BLUE = (0, 0, 255) +PURPLE = (180, 0, 255) +WHITE = (255, 255, 255) +COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE) + +print("fills") +for color in COLORS: + pixels_fill(color) + pixels_show() + time.sleep(0.2) + +print("chases") +for color in COLORS: + color_chase(color, 0.01) + +print("rainbow") +rainbow_cycle(0) + +row_pins = [ + Pin(0, Pin.OUT), + Pin(1, Pin.OUT), + Pin(2, Pin.OUT), + Pin(3, Pin.OUT), +] + +col_pins = [ + Pin(32, Pin.IN, Pin.PULL_UP), + Pin(33, Pin.IN, Pin.PULL_UP), + Pin(34, Pin.IN, Pin.PULL_UP), + Pin(35, Pin.IN, Pin.PULL_UP), +] + +# reset cols + +for pin in row_pins: + pin.value(1) + + +row_count = len(row_pins) +col_count = len(col_pins) +key_num = 0 +pressed_keys = set() +while True: + pressed_keys.clear() + for row, row_pin in enumerate(row_pins): + # set only active pin low, keep rest high + row_pin.value(0) + # for col, col_pin in enumerate(col_pins): + # col_pin.value(1) + # time.sleep_ms(10) + for col, col_pin in enumerate(col_pins): + key = col_count * row + col + 1 + if not col_pin.value(): + pressed_keys.add(key) + # print("key", key, "pin", col, ":", row, col_pin.value()) + row_pin.value(1) + print("pressed", pressed_keys) + time.sleep_ms(50)