Nie jest to prosty projekt dla początkujących, przynajmniej na obecnym etapie rozwoju oprogramowania, jak i dokumentacji dostarczonej przez autora urządzenia. Częściowo za trudności z budową odpowiadać może brak dostosowanej płytki drukowanej, ale przy odpowiednio dużym zainteresowaniu być może autor taką zaprojektuje.
Wszystkie pliki projektowe znajdują się w repozytorium na GitHubie, więc należy zacząć od sklonowania dokumentacji, aby zobaczyć, o co chodzi. Jest to projekt bazujacy na PlatformIO korzystający z frameworka Arduino, który napisano z myślą o module BlackPill z mikrokontrolerem STM32F411.
W projekcie dostępny jest katalog o nazwie KiCAD zawierający pliki projektowe wykonane przy użyciu programu KiCAD 6.0. Z kolei katalog OpenSCAD zawiera pliki projektów 3D obudowy wraz z plikami STL gotowymi do zaimportowania do oprogramowania przygotowującego G-kod dla drukarki 3D. Jeśli chcemy edytować pliki projektowe OpenSCAD, potrzebne będą dalsze biblioteki, jakie udostępnia autor.
Potrzebne elementy
Do budowy urządzenia potrzebnych jest kilka modułów oraz elementów pasywnych.
- moduł BlackPill - płytka deweloperska z mikrokontrolerem STM32F411,
- kodek Adafruit VS1053,
- moduł NavKey - joystick z enkoderem i przyciskiem z interfejsem I²C,
- klawiatura Adafruit NeoTrellis z elastomerowymi klawiszami i kablem ze złączem JST,
- moduł z LCD 320×240 px o przekątnej 2,4 cala, z interfejsem SPI (dowolny moduł, który pasuje mechanicznie, będzie nadawał się do tego projektu. Jeśli nasz moduł ma inne wymiary, konieczne mogą być zmiany w projekcie obudowy),
- moduł wzmacniacza z układem PAM8403,
- miniaturowy głośnik 40 mm,
- dwie przetwornice typu buck - jedna do stabilizacji napięcia 5 V, druga do 3,3 V,
- dwa enkodery obrotowe z przyciskami.
Ponadto potrzebne będą elementy dyskretne: dwa oporniki 10 kΩ, opornik 1 kΩ i trzy kondensatory elektrolityczne o pojemności 1 μF każdy. Ich napięcie pracy nie jest zbyt istotne, o ile będzie wyższe od napięcia występującego w układzie. Z uwagi na to, że większość kondensatorów elektrolitycznych dostępna jest z napięciem pracy 6,3 V i wyżej, nie powinno być to problemem.
Finalnie potrzebna jest płytka uniwersalna, która jest w stanie pomieścić wszystkie te moduły. Potrzebne są też przewody do wykonania połączeń na płytce, złącza do podłączenia elementów, takich jak klawiatura, wyświetlacz itp.
Decyzje projektowe
Projekt daje sporą swobodę w realizacji. Konstrukcja jest wstępnie rozplanowana, a poszczególne elementy dobrane do siebie, ale każdy ma sporo możliwości w zakresie modyfikacji zaproponowanej konstrukcji. Zanim rozpocznie się prace, należy podjąć kilka kluczowych decyzji:
- Czy nasz budżet pozwala na zakup wszystkich wymienionych modułów, czy potrzebujemy tańszych opcji?
- Czy głośnik 40 mm jest wystarczająco duży, czy potrzebujemy coś głośniejszego?
- Czy ekran 2,4 cala jest dla nas wystarczający, czy może potrzebujemy większego ekranu, aby dobrze widzieć menu? A może możemy zastosować mniejszy (tańszy) ekran, by zredukować koszt urządzenia.
Istnieją trzy rozwiązania, dzięki którym można zaoszczędzić na kosztach budowy urządzenia:
Zastosowanie mniejszego ekranu.
Zastosowanie innego modułu z układem VS1053. Na wielu portalach aukcyjnych można zakupić płytki z wlutowanym takim układem. W takiej sytuacji konieczne może być wprowadzenie modyfikacji przy nim, aby poprawnie uruchomił się w trybie MIDI. Może też nie brzmieć tak dobrze, jak moduł z Adafruit.
Pominięcie przycisku NavKey. Jeśli wyeliminujemy z układu moduł NavKey, nadal będzie można korzystać z BassMate. Bez tego modułu kontrolować można głośność układu, tempo pracy, można go wyciszać i pauzować. Brak przycisku nawigacyjnego niestety uniemożliwia zmianę głosów czy zapis/ładowanie ustawień.
Oczywiście, przy tego rodzaju zmianach, konieczne są zmiany w modelu 3D obudowy - oryginalny projekt nie będzie już pasował. Na szczęście pakiet, w którym autor zaprojektował obudowę - OpenSCAD - jest darmowy, więc bez problemu (jak już tylko poradzimy sobie z obsługą tego narzędzia) można dokonać koniecznych zmian.
Finalną decyzją jest rozplanowanie połączenia poszczególnych elementów w systemie. Autor w swojej konstrukcji zamontował większość modułów na dwóch dużych płytkach uniwersalnych, ale można użyć kilku mniejszych, jednej dużej, a można również połączyć poszczególne moduły ze sobą za pomocą przewodów. Zastosowanie dużej płytki uniwersalnej będzie oznaczało tworzenie mniejszej liczby połączeń przewodowych, ale może utrudnić montaż urządzeniai jego debugowanie. Z drugiej strony, duża ilość połączeń przewodowych przekłada się na pogorszenie niezawodności.
Sprzęt
Autor rozwój i budowę sprzętu w systemie podzielił na kilka osobnych części, dzięki czemu łatwiej jest zrozumieć działanie poszczególnych kluczowych komponentów. Dzięki temu można poprawnie zamontować każdą część i ją prawidłowo podłączyć. Na rysunku 1 pokazano schemat całego urządzenia, przyjrzyjmy się mu po kolei.
W pierwszej kolejności należy przeanalizować działanie sekcji zasilania, pomimo, że autor zaprojektował tę sekcję jako ostatnią. W pierwszej kolejności chciał zastosować stabilizatory liniowe (7805 i 78M33), ale odkrył, że robią się one zbyt gorące, gdy są zasilane z zasilacza 9 V (typowy zasilacz dla efektów gitarowych). Dlatego też umieścił przed sekcją stabilizatorów liniowych dodatkową przetwornicę buck, aby zmniejszyć napięcie wejściowe dla stabilizatorów. Dodatkowo, przed przetwornicą można umieścić jeszcze mostek prostowniczy, co pozwoli zabezpieczyć układ przed np. odwrotnym podłączeniem zasilania.
Teraz do układu dodać można dwa kluczowe moduły - BlackPill z mikrokontrolerem oraz dwa moduły NeoTrelli do obsługi klawiatury układu. Moduły te trzeba połączyć ze sobą na krawędzi lub w inny sposób połączyć ze sobą, a następnie ustawić ich adresy. Moduły te komunikują się z mikrokontrolerem poprzez interfejs I²C. Moduły mają pięć pinów adresowych (A0...A4), co pozwala na podłączenie do 32 takich modułów do pojedynczego interfejsu I²C. Poprzez ten interfejs mikrokontroler może kontrolować przyciski (każdy moduł ma ich 16 sztuk w układzie 4×4) oraz diody LED, podświetlające poszczególne klawisze.
Adresy obu modułów w systemie to 0x2E dla tego po lewej i 0x2F dla tego po prawej. Zapamiętajmy te wartości, gdyż będziemy ich potrzebowali w dalszej części projektu, na etapie pisania oprogramowania (linia 76 w pliku main.cpp - listing 1).
Dobrze jest na tym etapie zaprogramować mikrokontroler w Black Pill. Uprości to uruchamianie poszczególnych dalszych modułów. Opis firmware i sposób programowania modułu znaleźć można w dalszej części artykułu.
Kolejnymi integrowanymi modułami jest sekcja audio. W pierwszej kolejności należy podłączyć moduł wzmacniacza PAM8403 i podłączyć do niego głośnik. Wejście należy pozostawić na ten moment pływające. Po dotknięciu wejścia głośnik powinien brzęczeć. To dowodzi, że wzmacniacz działa. Teraz podłączamy do wzmacniacza moduł z układem VS1053. Po jego połączeniu z mikrokontrolerem można do systemu podać zasilanie. Po podłączeniu zasilania moduły NeoTrelli powinny zamigać, po czym z głośnika rozlegnie się pojedynczy głośny dźwięk wszystkich grających razem głosów. Wynika to trochę z pewnych niedoróbek w oprogramowaniu… ale pozwala w łatwy sposób przekonać się, czy układ został zmontowany poprawnie.
Finalnie do mikrokontrolera podłączyć należy oba enkodery. Na tym etapie kończy się montaż podstawowych elementów BassMate. Jednak autor przewidział jeszcze jeden element - kontroler NavKey. Jest to zaawansowany moduł do obsługi graficznego menu, który pozwala na nawigowanie w nim. Sposób użycia kontrolera opisano dokładniej w dalszej części artykułu, która mówi o obsłudze BassMate.
Na fotografii otwierającej pokazano zmontowane urządzenie. Widoczne są poszczególne moduły na płytkach drukowanych.
Oprogramowanie
Oprogramowanie BassMate napisane zostało w środowisku PlatformIO. Na listingu 1 pokazano główny plik programu (fragment). Szczególną uwagę należy zwrócić na, znane z Arduino, funkcje setup() oraz loop().
#include “Controller.h”
#include “Events/NavKeyEventDispatcher.h”
#include “Hardware/STM_SPIDMA.h”
#include “STM32/STM32F411_Timer1Encoder.h”
#include “STM32/STM32F411_Timer2Encoder.h”
#include “Model.h”
#include “NavKeyView.h”
#include “NoteGrid.h”
#include <Wire.h>
#include “configuration.h”
using namespace bassmate;
SPIClass spi2(MOSI, MISO, SCK, SC_CS);
Adafruit_ILI9341_DMA tft =
Adafruit_ILI9341_DMA(&spi2, SC_DC, SC_CS, SC_RESET);
volatile bool spiDmaTransferComplete = true;
STM_SPIDMA stmdma(&tft, LCD_DMA_BUFFER_SIZE, SPI2, DMA1_Stream4);
STM32F411_Timer1Encoder volumeEncoder(VOLUME_ENCODER_PUSH_PIN);
STM32F411_Timer2Encoder tempoEncoder(TEMPO_ENCODER_PUSH_PIN);
AD5204 digipot(DP_CS_PIN, DP_SCK_PIN, DP_SDI_PIN);
extern “C” {
void DMA1_Stream4_IRQHandler() {
HAL_DMA_IRQHandler(&stmdma._dma);
// spiDmaTransferComplete = true;
}
}
extern “C” {
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef* hspi) {
spiDmaTransferComplete = true;
}
}
/* Hardware interface */
SPIClass spi1(VS_MOSI, VS_MISO, VS_SCK);
Adafruit_NeoTrellis t_array[1][2] = {
{ Adafruit_NeoTrellis(0x2E), Adafruit_NeoTrellis(0x2F) }
};
Adafruit_MultiTrellis trellis((Adafruit_NeoTrellis*)t_array, 1, 2);
NoteGrid noteGrid(trellis, NT_INT);
/* Model */
/* Default address when no jumper are soldered */
i2cNavKey navkey(0b0010000);
VS1053 midi(VS_CS, VS_DCS, VS_RESET, spi1, Serial2);
Sequencer sequencer;
Storage* storage = new Storage();
uint8_t instrumentGroupMap[4];
Model model(navkey, NK_INT, midi, sequencer, storage, digipot);
/* View */
NavKeyView view(&tft, noteGrid, storage);
NavKeyEventDispatcher navKeyEventDispatcher(&view);
Controller controller(model, view, noteGrid);
void UP_Button_Pressed(i2cNavKey* p) {
navKeyEventDispatcher.UP_Button_Pressed();
}
void DOWN_Button_Pressed(i2cNavKey* p) {
navKeyEventDispatcher.DOWN_Button_Pressed(); }
void LEFT_Button_Pressed(i2cNavKey* p) {
navKeyEventDispatcher.LEFT_Button_Pressed(); }
void RIGHT_Button_Pressed(i2cNavKey* p) {
navKeyEventDispatcher.RIGHT_Button_Pressed(); }
void Encoder_Increment(i2cNavKey* p) {
navKeyEventDispatcher.Encoder_Increment(); }
void Encoder_Decrement(i2cNavKey* p) {
navKeyEventDispatcher.Encoder_Decrement(); }
void Encoder_Push(i2cNavKey* p) {
navKeyEventDispatcher.Encoder_Push(); }
void Encoder_Release(i2cNavKey* p) {
navKeyEventDispatcher.Encoder_Release(); }
void beatCallback(uint8_t beat) {
controller.handleBeat(beat); }
void noteOnCallback(uint8_t beat, MIDINote note) {
controller.handleNoteOn(beat, note); }
void noteOffCallback(uint8_t beat, MIDINote note) {
controller.handleNoteOff(beat, note); }
void noteGridEventHandler(keyEvent e) {
controller.handleNoteGridEvent(e); }
void noteGridKeyPressCallback(uint8_t x, uint8_t y) {
controller.handleNoteGridKeyPress(x, y); }
void noteGridKeyLongPressCallback(uint8_t x, uint8_t y)
void setupNavKey() {
navkey.begin(i2cNavKey::FLOAT_DATA
| i2cNavKey::WRAP_ENABLE
| i2cNavKey::DIRE_RIGHT
| i2cNavKey::IPUP_ENABLE);
navkey.writeCounter((float)0); /* Reset the counter value */
navkey.writeMax((float)0.95); /* Set the maximum threshold*/
navkey.writeMin((float)0.05); /* Set the minimum threshold */
navkey.writeStep((float)0.05); /* Set the step to 1*/
navkey.writeDoublePushPeriod(5); // not used yet..
navkey.onUpPush = UP_Button_Pressed;
navkey.onDownPush = DOWN_Button_Pressed;
navkey.onLeftPush = LEFT_Button_Pressed;
navkey.onRightPush = RIGHT_Button_Pressed;
navkey.onIncrement = Encoder_Increment;
navkey.onDecrement = Encoder_Decrement;
navkey.onCentralPush = Encoder_Push;
navkey.onCentralRelease = Encoder_Release;
navkey.autoconfigInterrupt();
}
void DMACallback(DMA_HandleTypeDef* _hdma) { spiDmaTransferComplete = true; }
void setup(){
noteGrid.attachEventHandler(noteGridEventHandler);
noteGrid.attachKeyPressCallback(noteGridKeyPressCallback);
noteGrid.attachKeyLongPressCallback(noteGridKeyLongPressCallback);
sequencer.attachNoteOnCallback(noteOnCallback);
sequencer.attachNoteOffCallback(noteOffCallback);
sequencer.attachBeatCallback(beatCallback);
volumeEncoder.onDecrement(&view, &NavKeyView::volumeUp);
volumeEncoder.onIncrement(&view, &NavKeyView::volumeDown);
volumeEncoder.onClick(&view, &NavKeyView::toggleMute);
tempoEncoder.onIncrement(&view, &NavKeyView::faster);
tempoEncoder.onDecrement(&view, &NavKeyView::slower);
tempoEncoder.onClick(&view, &NavKeyView::togglePause);
Serial.begin(9600);
spi1.begin(48000000);
spi2.begin(48000000);
pinMode(VS_MOSI, OUTPUT);
pinMode(VS_MISO, INPUT);
pinMode(VS_SCK, OUTPUT);
pinMode(VS_CS, OUTPUT);
pinMode(VS_DCS, OUTPUT);
pinMode(VS_RESET, OUTPUT);
digitalWrite(VS_MOSI, HIGH);
digitalWrite(VS_SCK, HIGH);
digitalWrite(VS_CS, HIGH);
digitalWrite(VS_DCS, HIGH);
digitalWrite(VS_RESET, HIGH);
pinMode(PA2, OUTPUT);
pinMode(NK_INT, INPUT_PULLUP);
pinMode(NT_INT, INPUT_PULLUP);
pinMode(SC_RESET, OUTPUT);
digitalWrite(SC_RESET, 0);
pinMode(SCL, OUTPUT);
pinMode(SDA, OUTPUT);
digitalWrite(SCL, 1);
digitalWrite(SDA, 1);
volumeEncoder.setupIO(true);
tempoEncoder.setupIO(true);
Wire.begin((uint32_t)SDA, (uint32_t)SCL);
Wire.setClock(400000);
Serial2.begin(31250);
setupNavKey();
tft.setDMA(&stmdma);
view.fontRenderer->setDMA(&stmdma);
HAL_DMA_RegisterCallback(
&stmdma._dma, HAL_DMA_XFER_CPLT_CB_ID, DMACallback);
spiDmaTransferComplete = true;
stmdma.begin();
tft.begin(48000000);
tft.setRotation(3);
view.setController(&controller);
view.begin();
trellis.begin();
noteGrid.setFlashBeat(true);
noteGrid.begin();
sequencer.setTempo(70);
model.begin();
controller.begin();
volumeEncoder.begin();
tempoEncoder.begin();
pinMode(DP_SDI_PIN, OUTPUT);
}
void loop(){
volumeEncoder.tick();
tempoEncoder.tick();
view.run();
model.run();
}
Architektura programu jest dosyć zaawansowana. Zawiera obiekty skojarzone z poszczególnymi manipulatorami systemu (volumeEncoder, tempoEncoder) czy innymi elementami, takimi jak interfejs i sterowanie nim (view) czy sama zasadnicza część działania sekwencera i syntezatora (model). Obiekty te są okresowo odświeżane w sekcji loop(), co oferuje, zasadniczo, pracę wielowątkową. Kod tych obiektów rozsiany jest po różnych bibliotekach i plikach nagłówkowych, które z uwagi na brak miejsca nie zostaną zaprezentowane w artykule ale na końcu artykułu znajduje się link do repozytorium na GitHubie.
Konfiguracja programu zapisywana jest w pliku nagłówkowym configuration.h (listing 2). Tutaj wprowadzać możemy zmiany, jeżeli podłączamy poszczególne moduły np. do innych pinów, zmienimy adresy modułów klawiatury itp.
#ifndef BASSMATE_CONFIG_H
#define BASSMATE_CONFIG_H
#define LEN(N) (sizeof(N) / sizeof(N[0]))
#define SCK PB13
#define MISO PB14
#define MOSI PB15
#define SC_CS PB12
#define SC_DC PB3
#define SC_RESET PB5
#define SCL PB6
#define SDA PB7
#define NK_INT PB8
#define NT_INT PB9
#define VS_MOSI PA7
#define VS_MISO PA6
#define VS_SCK PA5
#define VS_CS PB1
#define VS_DCS PB10
#define VS_RESET PB0
#define VOLUME_ENCODER_PUSH_PIN PA10
#define TEMPO_ENCODER_PUSH_PIN PC15
#define DP_CS_PIN PB10
#define DP_SCK_PIN PA4
#define DP_SDI_PIN PA3
#define LCD_DMA_BUFFER_SIZE 32768
#define LEFT_TRELLIS_ADDRESS 0x2E
#define RIGHT_TRELLIS_ADDRESS 0x2F
#endif
Black Pill można zaprogramować przy użyciu dowolnej metody programowania mikrokontrolerów z rodziny STM32. Można w tym celu użyć interfejsu SWD lub JTAG, w zależności od programatora. Autor projektu zakupił do tego projektu taniego klona programatora ST-Link V2, który w pełni wystarcza do tego projektu.
Po podłączeniu zasilania do zaprogramowanego modułu klawisze powinny przez chwilę migać na fioletowo. W tym momencie naciśnięcie przycisku powinno przełączyć go z wyłączonego na zielony i następnie ponownie na wyłączony. Oczywiście, tak jak napisano powyżej, odtwarzana będzie też muzyka.
Obudowa
Finalnie, całą elektronikę należy zamknąć w jakiejś obudowie. Autor sięgnął po technologię druku 3D, aby wyprodukować obudowę dla tego urządzenia. Jeśli nie mamy drukarki 3D lub nie mamy do takiej dostępu, to poradzić musimy sobie jakoś inaczej. Jeśli mamy drukarkę 3D, nie ma zbyt wielu problemów. Wystarczy pobrać pliki STL ze strony z projektem - jeden dla korpusu i jeden dla panelu przedniego. Pliki te należy włożyć do swojego wybranego slicera, by wygenerować odpowiedni kod dla naszej drukarki (autor korzysta z klasycznej Cury). Autor drukował z eSun PLA+, który daje dobry wizualny efekt i jest niemalże równie wytrzymały, jak PETG, ale znacznie łatwiejszy do drukowania (m.in. nie tworzy nitek ani kropelek). Otwory na kratkę głośnika będą wymagały podpór.
Użytkowanie i zaawansowane funkcje
Obsługa urządzenia jest bardzo intuicyjna. Wystarczy nacisnąć kilka przycisków klawiatury NeoTrelli i nacisnąć enkoder tempa, by BassMate zaczął działać. Światła migają i wydawane są dźwięki. Obrócenie enkodera tempa w jedną stronę powinno przyspieszyć, a w drugą stronę spowolnić odtwarzany rytm. Jeśli działa odwrotnie, trzeba zamienić przewody enkodera ze sobą. To samo dotyczy regulacji głośności za pomocą drugiego enkodera (naciśnięcie go wyciszy urządzenie).
Dostępne są cztery elementy kontrolne w układzie:
- przyciski NeoTrellis zapalają się na zielono po naciśnięciu, aby pokazać, że dany głos zostanie zagrany w tym rytmie,
- enkoder głośności reguluje głośność po obróceniu i wycisza po naciśnięciu,
- enkoder tempa dostosowuje tempo po przekręceniu i zatrzymuje rytm po naciśnięciu,
- klawisz NavKey służy do poruszania się po interfejsie użytkownika, wyświetlanym na ekranie i dostosowywania różnych ustawień w nim zawartych.
Za pomocą klawisza nawigacyjnego można przesuwać żółte podświetlenie po ekranie. Ekran jest podzielony na dwa obszary: górny pasek pokazuje główny motyw ekranu, a poniżej znajdują się ustawienia sekwencera. Domyślny układ pozwala kontrolować głośność, tempo i głosy dołączone do każdego rzędu przycisków NeoTrellis. Głosy są wybierane z jednej z 4 rodzin - Cymbals, Drums, Wood i Special.
Przekręcenie NavKey w prawo prowadzi do ustawień wstępnych, w których można zapisać bieżące ustawienia lub załadować poprzednie ustawienia z pamięci. Jeśli chcemy zapisać ustawienia pod nazwą, która już istnieje, interfejs zapyta nas, czy zastąpić poprzednio zapisany plik.
Nikodem Czechowski, EP
Bibliografia