Drum rack and lights firmware examples #1

Open
flpvsk wants to merge 3 commits from flpvsk/neogrid:example/drum-rack into main
6 changed files with 428 additions and 7 deletions

2
firmware/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
micropython
pico-sdk

View file

@ -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

View 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

View 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)

This is a (minor) memory leak allocation inefficiency as it will create a new set object on every loop iteration. It's fine to return pressed_keys directly.

This is a (minor) memory ~~leak~~ allocation inefficiency as it will create a new `set` object on every loop iteration. It's fine to return `pressed_keys` directly.

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.

# no btns pressed
pressed_keys = 0

# btn 3 is pressed
pressed_keys = pressed_keys | (1 << 3)

# btn 4 is not pressed
pressed_keys = pressed_keys & ~(1 << 4)

# check if btn 2 is pressed
(pressed_keys >> 2) & 1

# set intersection
pressed_keys_1 & pressed_keys_2

# set difference
pressed_keys_1 & ~pressed_keys_2

# get all the pressed btns
pressed_list = []
for i in range(NUM_BTNS):
  if (pressed_keys >> i) & 1:
    pressed_list.append(i)
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. ```python # no btns pressed pressed_keys = 0 # btn 3 is pressed pressed_keys = pressed_keys | (1 << 3) # btn 4 is not pressed pressed_keys = pressed_keys & ~(1 << 4) # check if btn 2 is pressed (pressed_keys >> 2) & 1 # set intersection pressed_keys_1 & pressed_keys_2 # set difference pressed_keys_1 & ~pressed_keys_2 # get all the pressed btns pressed_list = [] for i in range(NUM_BTNS): if (pressed_keys >> i) & 1: pressed_list.append(i) ```
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.")

View file

@ -0,0 +1,4 @@
metadata(version="0.1.0")
include("$(PORT_DIR)/boards/manifest.py")
require("usb-device")
require("usb-device-midi")

View 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)