Drum rack and lights firmware examples #1
6 changed files with 428 additions and 7 deletions
2
firmware/.gitignore
vendored
Normal file
2
firmware/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
micropython
|
||||||
|
pico-sdk
|
||||||
|
|
@ -5,13 +5,18 @@ Custom micropython build for the Olimex RP2350B-XL. Comes with some features ena
|
||||||
## build instructions
|
## build instructions
|
||||||
|
|
||||||
0. Make sure to have a compiler toolchain supporting the RP2 architecture installed (Mac: `brew install gcc-arm-embedded`)
|
0. Make sure to have a compiler toolchain supporting the RP2 architecture installed (Mac: `brew install gcc-arm-embedded`)
|
||||||
1. Clone the `micropython` repo
|
1. Clone *[pico-sdk](https://github.com/raspberrypi/pico-sdk)* (to this folder or somewhere else on your machine)
|
||||||
2. From within the `micropython` repo root, run `make -C mpy-cross`
|
2. Change to the *pico-sdk* folder and run `git submodule update --init`
|
||||||
3. Copy the `OLIMEX_PICO2_XL` folder to `micropython/ports/rp2/boards` if it doesn't exist; overwrite it if it does
|
3. Point an environment variable `PICO_SDK_PATH` to the *pico-sdk* folder
|
||||||
4. Change to the `boards/rp2` folder
|
4. Install *[picotool](https://github.com/raspberrypi/picotool)*
|
||||||
5. Run `make BOARD=OLIMEX_PICO2_XL submodules` to install dependencies
|
5. Clone the `micropython` repo (to this folder or somewhere else on your machine)
|
||||||
6. Run `make BOARD=OLIMEX_PICO2_XL clean` to remove previous build artefacts (if any)
|
6. From within the `micropython` repo root, run `make -C mpy-cross`
|
||||||
7. Run `make BOARD=OLIMEX_PICO2_XL` to build the firmware
|
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
|
## flashing
|
||||||
|
|
||||||
|
|
|
||||||
20
firmware/examples/nanogrid-drum-rack/README.md
Normal file
20
firmware/examples/nanogrid-drum-rack/README.md
Normal file
|
|
@ -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
|
||||||
246
firmware/examples/nanogrid-drum-rack/main.py
Normal file
246
firmware/examples/nanogrid-drum-rack/main.py
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
C_ACTIVE = (140, 60, 140)
|
||||||
|
C_INACTIVE = (0, 0, 0)
|
||||||
|
|
||||||
|
def shader(x, y, t):
|
||||||
|
st_x = float(x + 0.5) / NUM_COL
|
||||||
|
st_y = float(y + 0.5) / NUM_ROW
|
||||||
|
inv_t = 0.5 * (1.0 - t)
|
||||||
|
|
||||||
|
if st_x < t or st_y < t or (1.0 - st_x) < t or (1.0 - st_y) < t:
|
||||||
|
if t < 0.5:
|
||||||
|
return (
|
||||||
|
int(C_ACTIVE[0] * 0.5 * t),
|
||||||
|
int(C_ACTIVE[1] * 0.5 * t),
|
||||||
|
int(C_ACTIVE[2] * 0.5 * t),
|
||||||
|
)
|
||||||
|
if t >= 0.5:
|
||||||
|
return (
|
||||||
|
int(C_ACTIVE[0] * inv_t),
|
||||||
|
int(C_ACTIVE[1] * inv_t),
|
||||||
|
int(C_ACTIVE[2] * inv_t),
|
||||||
|
)
|
||||||
|
|
||||||
|
return C_INACTIVE
|
||||||
|
|
||||||
|
def play_animation(dur_s, fps):
|
||||||
|
cur_ms = float(0)
|
||||||
|
dur_ms = dur_s * 1000
|
||||||
|
interval_ms = int(1000 / fps)
|
||||||
|
processing_ms = 200
|
||||||
|
|
||||||
|
while cur_ms < dur_ms:
|
||||||
|
for r in range(0, NUM_ROW):
|
||||||
|
for c in range(0, NUM_COL):
|
||||||
|
t = cur_ms / dur_ms
|
||||||
|
color = shader(c, r, t)
|
||||||
|
set_led(c + r * NUM_COL, color)
|
||||||
|
show_all()
|
||||||
|
|
||||||
|
cur_ms += interval_ms + processing_ms
|
||||||
|
time.sleep_ms(interval_ms)
|
||||||
|
|
||||||
|
for l in range(NUM_LEDS):
|
||||||
|
set_led(l, C_INACTIVE)
|
||||||
|
show_all()
|
||||||
|
|
||||||
|
play_animation(3, 10)
|
||||||
|
|
||||||
|
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, C_ACTIVE)
|
||||||
|
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, C_INACTIVE)
|
||||||
|
|
||||||
|
show_all()
|
||||||
|
|
||||||
|
print("USB host has reset device, example done.")
|
||||||
|
|
||||||
4
firmware/examples/nanogrid-drum-rack/manifest.py
Normal file
4
firmware/examples/nanogrid-drum-rack/manifest.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
metadata(version="0.1.0")
|
||||||
|
include("$(PORT_DIR)/boards/manifest.py")
|
||||||
|
require("usb-device")
|
||||||
|
require("usb-device-midi")
|
||||||
144
firmware/examples/nanogrid-lights-demo/main.py
Executable file
144
firmware/examples/nanogrid-lights-demo/main.py
Executable file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue
This is a (minor) memory
leakallocation inefficiency as it will create a newsetobject on every loop iteration. It's fine to returnpressed_keysdirectly.Yes, makes sense to remove it.
Out of curiousity, shouldn't python take care of cleaning up that memory once the references to the new set go out of scope?just saw your correction :)Also, not sure if it's overoptimizing things, but we could use a single integer instead of a set here. Each bit representing an on/off state of a btn. We'd have to deal with bitwise operations, but the memory footprint would likely be smaller. Maybe the amount of cycles too?
E.g.