Motion Controller Design
Routing-Preflight und Regelprüfung durchführen
USB-Differenzialpfad kontrolliert routen und prüfen
Power-Pfade und Buck-Boost-Schleifen routen
DRC, Impedanz und Rückstrompfade abschließend prüfen
Table
| Funktion | Netz | U1-Pin | Gegenstelle / Designator | Elektrik |
|---|---|---|---|---|
| USB D− | USB_D- | GPIO19 | R3 → U6 → J1 | Native USB; 22 Ω R3 |
| USB D+ | USB_D+ | GPIO20 | R4 → U6 → J1 | Native USB; 22 Ω R4 |
| IMU CS | IMU_SPI_CS_N | GPIO10 | U2 AP_CS, R16 | aktiv Low; externer 10-kΩ-Pull-up |
| IMU MOSI | IMU_SPI_MOSI | GPIO11 | U2 AP_SDI | SPI-Ausgang |
| IMU SCLK | IMU_SPI_SCLK | GPIO12 | U2 AP_SCLK | SPI Mode 0 im Starter |
| IMU MISO | IMU_SPI_MISO | GPIO13 | U2 AP_SDO | SPI-Eingang |
| IMU INT1 | IMU_INT1 | GPIO9 | U2 INT1 | im Starter Push-Pull, aktiv High |
| I2C SDA | I2C_SDA | GPIO8 | U5 SDA, R14 | 4,7-kΩ-Pull-up nach 3V3 |
| I2C SCL | I2C_SCL | GPIO7 | U5 SCL, R15 | 4,7-kΩ-Pull-up nach 3V3 |
| Fuel Alert | FUEL_ALERT_N | GPIO6 | U5 ~ALERT, R26 | Open-Drain, aktiv Low, 10-kΩ-Pull-up |
| Taste 1 | BTN1_N | GPIO4 | SW2, R17 | aktiv Low, externer 10-kΩ-Pull-up |
| Taste 2 | BTN2_N | GPIO5 | SW3, R18 | aktiv Low |
| Taste 3 | BTN3_N | GPIO14 | SW4, R19 | aktiv Low |
| Taste 4 | BTN4_N | GPIO15 | SW5, R20 | aktiv Low |
| Taste 5 | BTN5_N | GPIO16 | SW6, R21 | aktiv Low |
| Taste 6 | BTN6_N | GPIO17 | SW7, R22 | aktiv Low |
| Trigger 1 | TRIGGER1_N | GPIO18 | J3 Pin 1, R23 | aktiv Low; J3 Pin 2 = GND |
| Trigger 2 | TRIGGER2_N | GPIO21 | J4 Pin 1, R24 | aktiv Low; J4 Pin 2 = GND |
| Charger PGOOD | CHG_PGOOD | GPIO41 | U3 ~PGOOD, R10 | Open-Drain, aktiv Low; 100-kΩ-Pull-up |
| Status-LED | STATUS_LED_K | GPIO42 | Kathode D2 | aktiv Low; Anode über R25 an 3V3 |
| UART0 TX | UART_TX | GPIO43/TXD0 | TP8 | 3,3-V-UART-Ausgang |
| UART0 RX | UART_RX | GPIO44/RXD0 | TP9 | 3,3-V-UART-Eingang |
| BOOT | MCU_BOOT | GPIO0 | SW9, R13, TP11 | aktiv Low; Strapping-Pin |
| Reset/Enable | MCU_EN | EN | SW8, R12, C10, TP10 | 10 kΩ + 1 µF; Reset aktiv Low |
~CHG von U3; die Anode wird über R9 aus SYS gespeist. D1 zeigt daher autonom den Ladezustand des BQ24074 an. D2 ist die einzige MCU-gesteuerte LED.VCELL 0x02: 78,125 µV/LSB.SOC 0x04: 1/256 %/LSB.VERSION 0x08: Produktionsversion; typisches Muster 0x001x, nicht auf einen einzelnen Revisionswert festnageln.CONFIG 0x0C: POR 0x971C; ALRT ist Bit 5 des Low-Bytes; Standard-Empty-Alert ist 4 %.STATUS 0x1A: Alert-Ursachen und Reset-Indikator.DEVICE_CONFIG 0x11, Soft Reset Bit 0INT_CONFIG 0x14TEMP_DATA1 0x1DPWR_MGMT0 0x4EGYRO_CONFIG0 0x4FACCEL_CONFIG0 0x50INT_SOURCE0 0x65WHO_AM_I 0x75, Sollwert 0x6FText
firmware/ ├── platformio.ini └── src/ └── main.cpp
platformio.iniIni
[env:midi-motion-controller] platform = espressif32 board = rymcu-esp32-s3-devkitc-1 framework = arduino ; U1 ist ESP32-S3-WROOM-1-N8R2: 8 MB Flash, 2 MB PSRAM. board_build.flash_size = 8MB board_build.partitions = default_8MB.csv ; Native USB als TinyUSB/OTG-Device, CDC und MIDI als Composite Device. build_flags = -DARDUINO_USB_MODE=0 -DARDUINO_USB_CDC_ON_BOOT=1 -DBOARD_HAS_PSRAM lib_deps = h2zero/NimBLE-Arduino@^2.3.6 monitor_speed = 115200 upload_speed = 921600
rymcu-esp32-s3-devkitc-1 entspricht N8R2 (8 MB Flash, 2 MB PSRAM). Der Schaltplan ist kein DevKit; der Eintrag liefert jedoch die passende ESP32-S3-N8R2-Speicherkonfiguration. Pinbelegungen werden ausschließlich im Quelltext festgelegt.src/main.cppCpp
#include <Arduino.h> #include <SPI.h> #include <Wire.h> #include "USB.h" #include "USBMIDI.h" #include <NimBLEDevice.h> #if ARDUINO_USB_MODE #error "ARDUINO_USB_MODE muss 0 sein: U1 GPIO19/20 sind als native USB-OTG-Datenpins verdrahtet." #endif // ---------------- Reales Schaltplan-Mapping ---------------- static constexpr uint8_t PIN_IMU_CS = 10; static constexpr uint8_t PIN_IMU_MOSI = 11; static constexpr uint8_t PIN_IMU_SCLK = 12; static constexpr uint8_t PIN_IMU_MISO = 13; static constexpr uint8_t PIN_IMU_INT1 = 9; static constexpr uint8_t PIN_I2C_SDA = 8; static constexpr uint8_t PIN_I2C_SCL = 7; static constexpr uint8_t PIN_FUEL_ALERT_N = 6; static constexpr uint8_t PIN_CHG_PGOOD_N = 41; static constexpr uint8_t PIN_STATUS_LED_N = 42; static constexpr uint8_t PIN_UART_TX = 43; static constexpr uint8_t PIN_UART_RX = 44; static constexpr uint8_t INPUT_PINS[8] = {4, 5, 14, 15, 16, 17, 18, 21}; static constexpr uint8_t INPUT_NOTES[8] = {60, 61, 62, 63, 64, 65, 66, 67}; static constexpr const char *INPUT_NAMES[8] = { "BTN1", "BTN2", "BTN3", "BTN4", "BTN5", "BTN6", "TRIGGER1", "TRIGGER2" }; USBMIDI usbMidi("MIDI Motion Controller"); HardwareSerial serviceUart(0); // ---------------- BLE-MIDI ---------------- static constexpr char BLE_MIDI_SERVICE_UUID[] = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"; static constexpr char BLE_MIDI_CHAR_UUID[] = "7772E5DB-3868-4112-A1A9-F2669D106BF3"; static NimBLEServer *bleServer = nullptr; static NimBLECharacteristic *bleMidiChar = nullptr; static volatile bool bleConnected = false; class MidiServerCallbacks final : public NimBLEServerCallbacks { void onConnect(NimBLEServer *, NimBLEConnInfo &) override { bleConnected = true; } void onDisconnect(NimBLEServer *, NimBLEConnInfo &, int) override { bleConnected = false; NimBLEDevice::startAdvertising(); } }; static void startBleMidi() { NimBLEDevice::init("MIDI Motion Controller"); bleServer = NimBLEDevice::createServer(); bleServer->setCallbacks(new MidiServerCallbacks()); NimBLEService *service = bleServer->createService(BLE_MIDI_SERVICE_UUID); bleMidiChar = service->createCharacteristic( BLE_MIDI_CHAR_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY); service->start(); NimBLEAdvertising *advertising = NimBLEDevice::getAdvertising(); advertising->setName("MIDI Motion Controller"); advertising->addServiceUUID(BLE_MIDI_SERVICE_UUID); advertising->enableScanResponse(true); advertising->start(); } static void bleMidiSend3(uint8_t status, uint8_t data1, uint8_t data2) { if (!bleConnected || bleServer == nullptr || bleMidiChar == nullptr || bleServer->getConnectedCount() == 0) { return; } const uint16_t timestamp = static_cast<uint16_t>(millis() & 0x1FFF); // 13 Bit, 1 ms const uint8_t packet[5] = { static_cast<uint8_t>(0x80 | ((timestamp >> 7) & 0x3F)), static_cast<uint8_t>(0x80 | (timestamp & 0x7F)), status, data1, data2}; bleMidiChar->setValue(packet, sizeof(packet)); bleMidiChar->notify(); } static void sendNoteOn(uint8_t note, uint8_t velocity = 100, uint8_t channel = 1) { usbMidi.noteOn(note, velocity, channel); bleMidiSend3(static_cast<uint8_t>(0x90 | ((channel - 1) & 0x0F)), note, velocity); } static void sendNoteOff(uint8_t note, uint8_t velocity = 0, uint8_t channel = 1) { usbMidi.noteOff(note, velocity, channel); bleMidiSend3(static_cast<uint8_t>(0x80 | ((channel - 1) & 0x0F)), note, velocity); } static void sendControlChange(uint8_t cc, uint8_t value, uint8_t channel = 1) { usbMidi.controlChange(cc, value, channel); bleMidiSend3(static_cast<uint8_t>(0xB0 | ((channel - 1) & 0x0F)), cc, value); } // ---------------- IIM-42652: echte SPI-Registerzugriffe ---------------- static constexpr uint8_t IIM_REG_DEVICE_CONFIG = 0x11; static constexpr uint8_t IIM_REG_INT_CONFIG = 0x14; static constexpr uint8_t IIM_REG_TEMP_DATA1 = 0x1D; static constexpr uint8_t IIM_REG_PWR_MGMT0 = 0x4E; static constexpr uint8_t IIM_REG_GYRO_CONFIG0 = 0x4F; static constexpr uint8_t IIM_REG_ACCEL_CONFIG0 = 0x50; static constexpr uint8_t IIM_REG_INT_SOURCE0 = 0x65; static constexpr uint8_t IIM_REG_WHO_AM_I = 0x75; static constexpr uint8_t IIM_WHO_AM_I_VALUE = 0x6F; static SPISettings iimSpiSettings(8000000, MSBFIRST, SPI_MODE0); static volatile bool imuDataReady = false; struct ImuSample { int16_t temperature; int16_t ax, ay, az; int16_t gx, gy, gz; }; static void IRAM_ATTR onImuDataReady() { imuDataReady = true; } static uint8_t iimReadRegister(uint8_t reg) { SPI.beginTransaction(iimSpiSettings); digitalWrite(PIN_IMU_CS, LOW); SPI.transfer(static_cast<uint8_t>(reg | 0x80)); const uint8_t value = SPI.transfer(0x00); digitalWrite(PIN_IMU_CS, HIGH); SPI.endTransaction(); return value; } static void iimWriteRegister(uint8_t reg, uint8_t value) { SPI.beginTransaction(iimSpiSettings); digitalWrite(PIN_IMU_CS, LOW); SPI.transfer(static_cast<uint8_t>(reg & 0x7F)); SPI.transfer(value); digitalWrite(PIN_IMU_CS, HIGH); SPI.endTransaction(); } static void iimReadBurst(uint8_t firstReg, uint8_t *data, size_t length) { SPI.beginTransaction(iimSpiSettings); digitalWrite(PIN_IMU_CS, LOW); SPI.transfer(static_cast<uint8_t>(firstReg | 0x80)); for (size_t i = 0; i < length; ++i) data[i] = SPI.transfer(0x00); digitalWrite(PIN_IMU_CS, HIGH); SPI.endTransaction(); } static bool initIim42652() { pinMode(PIN_IMU_CS, OUTPUT); digitalWrite(PIN_IMU_CS, HIGH); pinMode(PIN_IMU_INT1, INPUT); SPI.begin(PIN_IMU_SCLK, PIN_IMU_MISO, PIN_IMU_MOSI, PIN_IMU_CS); iimWriteRegister(IIM_REG_DEVICE_CONFIG, 0x01); // SOFT_RESET_CONFIG delay(2); // Datenblatt: mindestens 1 ms const uint8_t who = iimReadRegister(IIM_REG_WHO_AM_I); if (who != IIM_WHO_AM_I_VALUE) { Serial.printf("FAIL U2 WHO_AM_I: 0x%02X, erwartet 0x6F\n", who); serviceUart.printf("FAIL U2 WHO_AM_I: 0x%02X\r\n", who); return false; } // INT1: gepulst, Push-Pull, aktiv High. iimWriteRegister(IIM_REG_INT_CONFIG, 0x03); // ±2000 dps und 200 Hz; ±16 g und 200 Hz. iimWriteRegister(IIM_REG_GYRO_CONFIG0, 0x07); iimWriteRegister(IIM_REG_ACCEL_CONFIG0, 0x07); // UI data-ready auf INT1 routen, übrige Registerbits erhalten. const uint8_t intSource0 = iimReadRegister(IIM_REG_INT_SOURCE0); iimWriteRegister(IIM_REG_INT_SOURCE0, static_cast<uint8_t>(intSource0 | 0x08)); // Gyro LN (11), Accel LN (11), Temperatur aktiv: 0b00001111. iimWriteRegister(IIM_REG_PWR_MGMT0, 0x0F); delay(45); // Gyro muss nach Aktivierung mindestens 45 ms eingeschaltet bleiben. attachInterrupt(digitalPinToInterrupt(PIN_IMU_INT1), onImuDataReady, RISING); Serial.println("PASS U2 IIM-42652 WHO_AM_I=0x6F, 200 Hz LN"); return true; } static ImuSample readIim42652() { uint8_t raw[14]; iimReadBurst(IIM_REG_TEMP_DATA1, raw, sizeof(raw)); auto be16 = [&](size_t i) -> int16_t { return static_cast<int16_t>((static_cast<uint16_t>(raw[i]) << 8) | raw[i + 1]); }; return {be16(0), be16(2), be16(4), be16(6), be16(8), be16(10), be16(12)}; } // TODO datenblatt- und anwendungsabhängig: AAF/Notch/UI-Filter, FIFO/Wasserzeichen, // Timestamp-Synchronisierung, Self-Test sowie Nullpunkt-/Temperaturkalibrierung. // ---------------- MAX17048 ---------------- static constexpr uint8_t MAX17048_ADDR = 0x36; static constexpr uint8_t MAX_REG_VCELL = 0x02; static constexpr uint8_t MAX_REG_SOC = 0x04; static constexpr uint8_t MAX_REG_VERSION = 0x08; static constexpr uint8_t MAX_REG_CONFIG = 0x0C; static constexpr uint8_t MAX_REG_STATUS = 0x1A; static volatile bool fuelAlertPending = false; static void IRAM_ATTR onFuelAlert() { fuelAlertPending = true; } static bool maxRead16(uint8_t reg, uint16_t &value) { Wire.beginTransmission(MAX17048_ADDR); Wire.write(reg); if (Wire.endTransmission(false) != 0) return false; if (Wire.requestFrom(MAX17048_ADDR, static_cast<uint8_t>(2)) != 2) return false; value = static_cast<uint16_t>(Wire.read() << 8); value |= static_cast<uint16_t>(Wire.read()); return true; } static bool maxWrite16(uint8_t reg, uint16_t value) { Wire.beginTransmission(MAX17048_ADDR); Wire.write(reg); Wire.write(static_cast<uint8_t>(value >> 8)); Wire.write(static_cast<uint8_t>(value & 0xFF)); return Wire.endTransmission() == 0; } static bool initMax17048() { Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 400000); pinMode(PIN_FUEL_ALERT_N, INPUT); // externer R26 = 10 kΩ nach 3V3 attachInterrupt(digitalPinToInterrupt(PIN_FUEL_ALERT_N), onFuelAlert, FALLING); uint16_t version = 0; if (!maxRead16(MAX_REG_VERSION, version)) { Serial.println("FAIL U5 MAX17048 antwortet nicht auf 0x36"); return false; } Serial.printf("PASS U5 MAX17048 VERSION=0x%04X\n", version); return true; } static void serviceFuelAlert() { if (!fuelAlertPending && digitalRead(PIN_FUEL_ALERT_N) != LOW) return; fuelAlertPending = false; uint16_t status = 0, config = 0; if (!maxRead16(MAX_REG_STATUS, status) || !maxRead16(MAX_REG_CONFIG, config)) return; Serial.printf("U5 ALERT: STATUS=0x%04X CONFIG=0x%04X\n", status, config); // Alle gelesenen Alert-Ursachen/RI quittieren und anschließend CONFIG.ALRT löschen. maxWrite16(MAX_REG_STATUS, static_cast<uint16_t>(status & 0x00FF)); maxWrite16(MAX_REG_CONFIG, static_cast<uint16_t>(config & ~(1u << 5))); } static bool readFuel(float &voltageV, float &socPercent) { uint16_t vcell = 0, soc = 0; if (!maxRead16(MAX_REG_VCELL, vcell) || !maxRead16(MAX_REG_SOC, soc)) return false; voltageV = static_cast<float>(vcell) * 78.125e-6f; socPercent = static_cast<float>(soc) / 256.0f; return true; } // ---------------- Taster/Trigger ---------------- struct DebouncedInput { bool rawPressed = false; bool stablePressed = false; uint32_t changedAtMs = 0; }; static DebouncedInput inputs[8]; static constexpr uint32_t DEBOUNCE_MS = 12; static void initInputs() { for (size_t i = 0; i < 8; ++i) { pinMode(INPUT_PINS[i], INPUT); // externe R17–R24 sind vorhanden const bool pressed = digitalRead(INPUT_PINS[i]) == LOW; inputs[i].rawPressed = pressed; inputs[i].stablePressed = pressed; inputs[i].changedAtMs = millis(); } } static void serviceInputs() { const uint32_t now = millis(); for (size_t i = 0; i < 8; ++i) { const bool pressed = digitalRead(INPUT_PINS[i]) == LOW; if (pressed != inputs[i].rawPressed) { inputs[i].rawPressed = pressed; inputs[i].changedAtMs = now; } if ((now - inputs[i].changedAtMs) >= DEBOUNCE_MS && inputs[i].stablePressed != inputs[i].rawPressed) { inputs[i].stablePressed = inputs[i].rawPressed; if (inputs[i].stablePressed) sendNoteOn(INPUT_NOTES[i]); else sendNoteOff(INPUT_NOTES[i]); Serial.printf("%s=%s\n", INPUT_NAMES[i], inputs[i].stablePressed ? "PRESSED" : "RELEASED"); } } } // ---------------- Arduino-Start ---------------- void setup() { pinMode(PIN_STATUS_LED_N, OUTPUT); digitalWrite(PIN_STATUS_LED_N, HIGH); // D2 aus; aktiv Low pinMode(PIN_CHG_PGOOD_N, INPUT); // externer R10 = 100 kΩ Serial.begin(115200); // USB CDC im Composite Device serviceUart.begin(115200, SERIAL_8N1, PIN_UART_RX, PIN_UART_TX); delay(250); usbMidi.begin(); USB.begin(); startBleMidi(); initInputs(); const bool fuelOk = initMax17048(); const bool imuOk = initIim42652(); Serial.printf("START fuel=%s imu=%s PGOOD_N=%d\n", fuelOk ? "OK" : "FAIL", imuOk ? "OK" : "FAIL", digitalRead(PIN_CHG_PGOOD_N)); serviceUart.printf("START fuel=%s imu=%s\r\n", fuelOk ? "OK" : "FAIL", imuOk ? "OK" : "FAIL"); } void loop() { serviceInputs(); serviceFuelAlert(); if (imuDataReady) { imuDataReady = false; const ImuSample s = readIim42652(); // Starter-Mapping: X-Beschleunigung (±16 g) auf MIDI CC 16. // ±16 g entsprechen nominal ±32768 Counts; auf 0..127 begrenzen. const int32_t shifted = static_cast<int32_t>(s.ax) + 32768; const uint8_t cc = static_cast<uint8_t>(constrain(shifted >> 9, 0L, 127L)); static uint8_t lastCc = 255; if (cc != lastCc) { lastCc = cc; sendControlChange(16, cc); } } static uint32_t lastFuelMs = 0; if (millis() - lastFuelMs >= 1000) { lastFuelMs = millis(); float voltage = 0.0f, soc = 0.0f; if (readFuel(voltage, soc)) { Serial.printf("BAT %.3f V, SOC %.1f %%, PGOOD_N=%d, BLE=%d\n", voltage, soc, digitalRead(PIN_CHG_PGOOD_N), bleConnected ? 1 : 0); serviceUart.printf("BAT %.3f V SOC %.1f%% PGOOD_N=%d BLE=%d\r\n", voltage, soc, digitalRead(PIN_CHG_PGOOD_N), bleConnected ? 1 : 0); } } // D2: dauerhaft an bei BLE-Verbindung, sonst 2-Hz-Blinken. const bool ledOn = bleConnected || ((millis() / 250) & 1u); digitalWrite(PIN_STATUS_LED_N, ledOn ? LOW : HIGH); delay(1); }
firmware/ aus:Bash
pio run pio run -t upload pio device monitor -b 115200
MCU_BOOT/GPIO0 = Low).MCU_EN = Low → High).pio run -t upload erneut starten.MCU_EN nach 3V3; C10 = 1 µF erzeugt den empfohlenen Einschalt-Reset. SW8 zieht EN nach GND. TP10 ist der EN-Messpunkt.MCU_BOOT nach 3V3; SW9 zieht GPIO0 nach GND. TP11 ist der BOOT-Messpunkt.REG_EN von U4. In OFF bleibt die Lade-/Fuel-Gauge-Domäne an BAT/VBUS aktiv, während 3V3 und U1 abgeschaltet werden.Text
PASS U5 MAX17048 VERSION=0x001x PASS U2 IIM-42652 WHO_AM_I=0x6F, 200 Hz LN START fuel=OK imu=OK PGOOD_N=0 BAT 3.xxx V, SOC xx.x %, PGOOD_N=0, BLE=0
PGOOD_N=0 ist bei gültigem USB-VBUS korrekt; ohne USB ist PGOOD_N=1 zu erwarten. Der erste gültige MAX17048-SOC-Wert benötigt nach POR ungefähr eine Sekunde.USB, USBMIDI, SPI, Wire und HardwareSerial bereit.NimBLE-Arduino 2.x.Zweck und Hardwarebasis
Echtes GPIO- und Signal-Mapping
Wichtige reale Adressen und Register
MAX17048 U5
IIM-42652 U2
PlatformIO-Projekt
`platformio.ini`
`src/main.cpp`
Build, Flash und Monitor
Boot- und Reset-Verhalten
Erwartete Startausgaben
Funktionszuordnung im Starter
Abhängigkeiten und Grenzen