From 7707f90d46d288005b4e3981edb602df2958a71f Mon Sep 17 00:00:00 2001 From: Rene Stange Date: Mon, 7 Mar 2022 20:14:38 +0100 Subject: [PATCH] Add initial optional rotary encoder support (#40) * Add driver for KY-040 rotary encoder This driver has been taken from the Circle develop branch and can be removed here, when the driver made it up into a circle-stdlib release. * Add initial optional rotary encoder support * Can change between voice and bank select mode by clicking the switch * Turn the knob to select the voice or bank according to current mode * Hold the switch for 3 seconds to reboot the system * Program change from MIDI is only displayed in voice select mode * Configuration for KY-040 added to CConfig and minidexed.ini * Fix: m_I2CMaster has not been initialized in CKernel --- src/Makefile | 2 +- src/config.cpp | 25 ++++ src/config.h | 12 ++ src/kernel.cpp | 17 ++- src/kernel.h | 2 + src/ky040.cpp | 315 ++++++++++++++++++++++++++++++++++++++++ src/ky040.h | 153 +++++++++++++++++++ src/minidexed.cpp | 28 ++-- src/minidexed.h | 14 +- src/minidexed.ini | 6 + src/sysexfileloader.cpp | 30 +++- src/sysexfileloader.h | 3 + src/userinterface.cpp | 127 +++++++++++++++- src/userinterface.h | 25 +++- 14 files changed, 733 insertions(+), 26 deletions(-) create mode 100644 src/ky040.cpp create mode 100644 src/ky040.h diff --git a/src/Makefile b/src/Makefile index 7d334d2..0c06d88 100644 --- a/src/Makefile +++ b/src/Makefile @@ -7,7 +7,7 @@ SYNTH_DEXED_DIR = ../Synth_Dexed/src OBJS = main.o kernel.o minidexed.o config.o userinterface.o \ mididevice.o midikeyboard.o serialmididevice.o pckeyboard.o \ - sysexfileloader.o perftimer.o \ + sysexfileloader.o perftimer.o ky040.o \ $(SYNTH_DEXED_DIR)/synth_dexed.o INCLUDE += -I $(SYNTH_DEXED_DIR) diff --git a/src/config.cpp b/src/config.cpp index 9eb18a9..a31e1a4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -52,6 +52,11 @@ void CConfig::Load (void) m_nLCDPinData6 = m_Properties.GetNumber ("LCDPinData6", 24); m_nLCDPinData7 = m_Properties.GetNumber ("LCDPinData7", 25); + m_bEncoderEnabled = m_Properties.GetNumber ("EncoderEnabled", 0) != 0; + m_nEncoderPinClock = m_Properties.GetNumber ("EncoderPinClock", 5); + m_nEncoderPinData = m_Properties.GetNumber ("EncoderPinData", 6); + m_nEncoderPinSwitch = m_Properties.GetNumber ("EncoderPinSwitch", 26); + m_bMIDIDumpEnabled = m_Properties.GetNumber ("MIDIDumpEnabled", 0) != 0; m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; } @@ -121,6 +126,26 @@ unsigned CConfig::GetLCDPinData7 (void) const return m_nLCDPinData7; } +bool CConfig::GetEncoderEnabled (void) const +{ + return m_bEncoderEnabled; +} + +unsigned CConfig::GetEncoderPinClock (void) const +{ + return m_nEncoderPinClock; +} + +unsigned CConfig::GetEncoderPinData (void) const +{ + return m_nEncoderPinData; +} + +unsigned CConfig::GetEncoderPinSwitch (void) const +{ + return m_nEncoderPinSwitch; +} + bool CConfig::GetMIDIDumpEnabled (void) const { return m_bMIDIDumpEnabled; diff --git a/src/config.h b/src/config.h index 698bca4..1846743 100644 --- a/src/config.h +++ b/src/config.h @@ -67,6 +67,13 @@ public: unsigned GetLCDPinData6 (void) const; unsigned GetLCDPinData7 (void) const; + // KY-040 Rotary Encoder + // GPIO pin numbers are chip numbers, not header positions + bool GetEncoderEnabled (void) const; + unsigned GetEncoderPinClock (void) const; + unsigned GetEncoderPinData (void) const; + unsigned GetEncoderPinSwitch (void) const; + // Debug bool GetMIDIDumpEnabled (void) const; bool GetProfileEnabled (void) const; @@ -90,6 +97,11 @@ private: unsigned m_nLCDPinData6; unsigned m_nLCDPinData7; + bool m_bEncoderEnabled; + unsigned m_nEncoderPinClock; + unsigned m_nEncoderPinData; + unsigned m_nEncoderPinSwitch; + bool m_bMIDIDumpEnabled; bool m_bProfileEnabled; }; diff --git a/src/kernel.cpp b/src/kernel.cpp index c658854..2ad706d 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -30,6 +30,7 @@ CKernel *CKernel::s_pThis = 0; CKernel::CKernel (void) : CStdlibAppStdio ("minidexed"), m_Config (&mFileSystem), + m_GPIOManager (&mInterrupt), m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE), m_pDexed (0) { @@ -52,6 +53,16 @@ bool CKernel::Initialize (void) mLogger.RegisterPanicHandler (PanicHandler); + if (!m_GPIOManager.Initialize ()) + { + return FALSE; + } + + if (!m_I2CMaster.Initialize ()) + { + return FALSE; + } + m_Config.Load (); // select the sound device @@ -60,19 +71,19 @@ bool CKernel::Initialize (void) { LOGNOTE ("I2S mode"); - m_pDexed = new CMiniDexedI2S (&m_Config, &mInterrupt, &m_I2CMaster); + m_pDexed = new CMiniDexedI2S (&m_Config, &mInterrupt, &m_GPIOManager, &m_I2CMaster); } else if (strcmp (pSoundDevice, "hdmi") == 0) { LOGNOTE ("HDMI mode"); - m_pDexed = new CMiniDexedHDMI (&m_Config, &mInterrupt); + m_pDexed = new CMiniDexedHDMI (&m_Config, &mInterrupt, &m_GPIOManager); } else { LOGNOTE ("PWM mode"); - m_pDexed = new CMiniDexedPWM (&m_Config, &mInterrupt); + m_pDexed = new CMiniDexedPWM (&m_Config, &mInterrupt, &m_GPIOManager); } assert (m_pDexed); diff --git a/src/kernel.h b/src/kernel.h index 5d626f9..a2b5cc3 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -21,6 +21,7 @@ #define _kernel_h #include "circle_stdlib_app.h" +#include #include #include "config.h" #include "minidexed.h" @@ -48,6 +49,7 @@ private: private: // do not change this order CConfig m_Config; + CGPIOManager m_GPIOManager; CI2CMaster m_I2CMaster; CMiniDexed *m_pDexed; diff --git a/src/ky040.cpp b/src/ky040.cpp new file mode 100644 index 0000000..24c040f --- /dev/null +++ b/src/ky040.cpp @@ -0,0 +1,315 @@ +// +// ky040.cpp +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2022 R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ky040.h" +#include + +static const unsigned SwitchDebounceDelayMillis = 50; +static const unsigned SwitchTickDelayMillis = 500; + +CKY040::TState CKY040::s_NextState[StateUnknown][2][2] = +{ + // {{CLK=0/DT=0, CLK=0/DT=1}, {CLK=1/DT=0, CLK=1/DT=1}} + + {{StateInvalid, StateCWStart}, {StateCCWStart, StateStart}}, // StateStart + + {{StateCWBothLow, StateCWStart}, {StateInvalid, StateStart}}, // StateCWStart + {{StateCWBothLow, StateInvalid}, {StateCWFirstHigh, StateInvalid}}, // StateCWBothLow + {{StateInvalid, StateInvalid}, {StateCWFirstHigh, StateStart}}, // StateCWFirstHigh + + {{StateCCWBothLow, StateInvalid}, {StateCCWStart, StateStart}}, // StateCCWStart + {{StateCCWBothLow, StateCCWFirstHigh}, {StateInvalid, StateInvalid}}, // StateCCWBothLow + {{StateInvalid, StateCCWFirstHigh}, {StateInvalid, StateStart}}, // StateCCWFirstHigh + + {{StateInvalid, StateInvalid}, {StateInvalid, StateStart}} // StateInvalid +}; + +CKY040::TEvent CKY040::s_Output[StateUnknown][2][2] = +{ + // {{CLK=0/DT=0, CLK=0/DT=1}, {CLK=1/DT=0, CLK=1/DT=1}} + + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}}, // StateStart + + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}}, // StateCWStart + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}}, // StateCWBothLow + {{EventUnknown, EventUnknown}, {EventUnknown, EventClockwise}}, // StateCWFirstHigh + + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}}, // StateCCWStart + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}}, // StateCCWBothLow + {{EventUnknown, EventUnknown}, {EventUnknown, EventCounterclockwise}}, // StateCCWFirstHigh + + {{EventUnknown, EventUnknown}, {EventUnknown, EventUnknown}} // StateInvalid +}; + +CKY040::TSwitchState CKY040::s_NextSwitchState[SwitchStateUnknown][SwitchEventUnknown] = +{ + // {SwitchEventDown, SwitchEventUp, SwitchEventTick} + + {SwitchStateDown, SwitchStateStart, SwitchStateStart}, // SwitchStateStart + {SwitchStateDown, SwitchStateClick, SwitchStateHold}, // SwitchStateDown + {SwitchStateDown2, SwitchStateClick, SwitchStateStart}, // SwitchStateClick + {SwitchStateDown2, SwitchStateClick2, SwitchStateInvalid}, // SwitchStateDown2 + {SwitchStateDown3, SwitchStateClick2, SwitchStateStart}, // SwitchStateClick2 + {SwitchStateDown3, SwitchStateClick3, SwitchStateInvalid}, // SwitchStateDown3 + {SwitchStateInvalid, SwitchStateClick3, SwitchStateStart}, // SwitchStateClick3 + {SwitchStateHold, SwitchStateStart, SwitchStateHold}, // SwitchStateHold + {SwitchStateInvalid, SwitchStateStart, SwitchStateInvalid} // SwitchStateInvalid +}; + +CKY040::TEvent CKY040::s_SwitchOutput[SwitchStateUnknown][SwitchEventUnknown] = +{ + // {SwitchEventDown, SwitchEventUp, SwitchEventTick} + + {EventUnknown, EventUnknown, EventUnknown}, // SwitchStateStart + {EventUnknown, EventUnknown, EventSwitchHold}, // SwitchStateDown + {EventUnknown, EventUnknown, EventSwitchClick}, // SwitchStateClick + {EventUnknown, EventUnknown, EventUnknown}, // SwitchStateDown2 + {EventUnknown, EventUnknown, EventSwitchDoubleClick}, // SwitchStateClick2 + {EventUnknown, EventUnknown, EventUnknown}, // SwitchStateDown3 + {EventUnknown, EventUnknown, EventSwitchTripleClick}, // SwitchStateClick3 + {EventUnknown, EventUnknown, EventSwitchHold}, // SwitchStateHold + {EventUnknown, EventUnknown, EventUnknown} // SwitchStateInvalid +}; + +CKY040::CKY040 (unsigned nCLKPin, unsigned nDTPin, unsigned nSWPin, CGPIOManager *pGPIOManager) +: m_CLKPin (nCLKPin, GPIOModeInputPullUp, pGPIOManager), + m_DTPin (nDTPin, GPIOModeInputPullUp, pGPIOManager), + m_SWPin (nSWPin, GPIOModeInputPullUp, pGPIOManager), + m_bPollingMode (!pGPIOManager), + m_bInterruptConnected (FALSE), + m_pEventHandler (nullptr), + m_State (StateStart), + m_hDebounceTimer (0), + m_hTickTimer (0), + m_nLastSWLevel (HIGH), + m_bDebounceActive (FALSE), + m_SwitchState (SwitchStateStart), + m_nSwitchLastTicks (0) +{ +} + +CKY040::~CKY040 (void) +{ + if (m_bInterruptConnected) + { + m_pEventHandler = nullptr; + + m_CLKPin.DisableInterrupt2 (); + m_CLKPin.DisableInterrupt (); + m_CLKPin.DisconnectInterrupt (); + + m_DTPin.DisableInterrupt2 (); + m_DTPin.DisableInterrupt (); + m_DTPin.DisconnectInterrupt (); + + m_SWPin.DisableInterrupt2 (); + m_SWPin.DisableInterrupt (); + m_SWPin.DisconnectInterrupt (); + } + + if (m_hDebounceTimer) + { + CTimer::Get ()->CancelKernelTimer (m_hDebounceTimer); + } + + if (m_hTickTimer) + { + CTimer::Get ()->CancelKernelTimer (m_hTickTimer); + } +} + +boolean CKY040::Initialize (void) +{ + if (!m_bPollingMode) + { + assert (!m_bInterruptConnected); + m_bInterruptConnected = TRUE; + + m_CLKPin.ConnectInterrupt (EncoderInterruptHandler, this); + m_DTPin.ConnectInterrupt (EncoderInterruptHandler, this); + m_SWPin.ConnectInterrupt (SwitchInterruptHandler, this); + + m_CLKPin.EnableInterrupt (GPIOInterruptOnFallingEdge); + m_CLKPin.EnableInterrupt2 (GPIOInterruptOnRisingEdge); + + m_DTPin.EnableInterrupt (GPIOInterruptOnFallingEdge); + m_DTPin.EnableInterrupt2 (GPIOInterruptOnRisingEdge); + + m_SWPin.EnableInterrupt (GPIOInterruptOnFallingEdge); + m_SWPin.EnableInterrupt2 (GPIOInterruptOnRisingEdge); + } + + return TRUE; +} + +void CKY040::RegisterEventHandler (TEventHandler *pHandler, void *pParam) +{ + assert (!m_pEventHandler); + m_pEventHandler = pHandler; + assert (m_pEventHandler); + m_pEventParam = pParam; +} + +unsigned CKY040::GetHoldSeconds (void) const +{ + return m_nHoldCounter / 2; +} + +void CKY040::Update (void) +{ + assert (m_bPollingMode); + + EncoderInterruptHandler (this); + + // handle switch + unsigned nTicks = CTimer::GetClockTicks (); + unsigned nSW = m_SWPin.Read (); + + if (nSW != m_nLastSWLevel) + { + m_nLastSWLevel = nSW; + + m_bDebounceActive = TRUE; + m_nDebounceLastTicks = CTimer::GetClockTicks (); + } + else + { + if ( m_bDebounceActive + && nTicks - m_nDebounceLastTicks >= SwitchDebounceDelayMillis * (CLOCKHZ / 1000)) + { + m_bDebounceActive = FALSE; + m_nSwitchLastTicks = nTicks; + + if (m_pEventHandler) + { + (*m_pEventHandler) (nSW ? EventSwitchUp : EventSwitchDown, + m_pEventParam); + } + + HandleSwitchEvent (nSW ? SwitchEventUp : SwitchEventDown); + } + + if (nTicks - m_nSwitchLastTicks >= SwitchTickDelayMillis * (CLOCKHZ / 1000)) + { + m_nSwitchLastTicks = nTicks; + + HandleSwitchEvent (SwitchEventTick); + } + } +} + +// generates the higher level switch events +void CKY040::HandleSwitchEvent (TSwitchEvent SwitchEvent) +{ + assert (SwitchEvent < SwitchEventUnknown); + TEvent Event = s_SwitchOutput[m_SwitchState][SwitchEvent]; + TSwitchState NextState = s_NextSwitchState[m_SwitchState][SwitchEvent]; + + if (NextState == SwitchStateHold) + { + if (m_SwitchState != SwitchStateHold) + { + m_nHoldCounter = 0; + } + + m_nHoldCounter++; + } + + m_SwitchState = NextState; + + if ( Event != EventUnknown + && (Event != EventSwitchHold || !(m_nHoldCounter & 1)) // emit hold event each second + && m_pEventHandler) + { + (*m_pEventHandler) (Event, m_pEventParam); + } +} + +void CKY040::EncoderInterruptHandler (void *pParam) +{ + CKY040 *pThis = static_cast (pParam); + assert (pThis != 0); + + unsigned nCLK = pThis->m_CLKPin.Read (); + unsigned nDT = pThis->m_DTPin.Read (); + assert (nCLK <= 1); + assert (nDT <= 1); + + assert (pThis->m_State < StateUnknown); + TEvent Event = s_Output[pThis->m_State][nCLK][nDT]; + pThis->m_State = s_NextState[pThis->m_State][nCLK][nDT]; + + if ( Event != EventUnknown + && pThis->m_pEventHandler) + { + (*pThis->m_pEventHandler) (Event, pThis->m_pEventParam); + } +} + +void CKY040::SwitchInterruptHandler (void *pParam) +{ + CKY040 *pThis = static_cast (pParam); + assert (pThis != 0); + + if (pThis->m_hDebounceTimer) + { + CTimer::Get ()->CancelKernelTimer (pThis->m_hDebounceTimer); + } + + pThis->m_hDebounceTimer = + CTimer::Get ()->StartKernelTimer (MSEC2HZ (SwitchDebounceDelayMillis), + SwitchDebounceHandler, pThis, 0); +} + +void CKY040::SwitchDebounceHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext) +{ + CKY040 *pThis = static_cast (pParam); + assert (pThis != 0); + + pThis->m_hDebounceTimer = 0; + + if (pThis->m_hTickTimer) + { + CTimer::Get ()->CancelKernelTimer (pThis->m_hTickTimer); + } + + pThis->m_hTickTimer = CTimer::Get ()->StartKernelTimer (MSEC2HZ (SwitchTickDelayMillis), + SwitchTickHandler, pThis, 0); + + unsigned nSW = pThis->m_SWPin.Read (); + + if (pThis->m_pEventHandler) + { + (*pThis->m_pEventHandler) (nSW ? EventSwitchUp : EventSwitchDown, + pThis->m_pEventParam); + } + + pThis->HandleSwitchEvent (nSW ? SwitchEventUp : SwitchEventDown); +} + +void CKY040::SwitchTickHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext) +{ + CKY040 *pThis = static_cast (pParam); + assert (pThis != 0); + + pThis->m_hTickTimer = CTimer::Get ()->StartKernelTimer (MSEC2HZ (SwitchTickDelayMillis), + SwitchTickHandler, pThis, 0); + + pThis->HandleSwitchEvent (SwitchEventTick); +} diff --git a/src/ky040.h b/src/ky040.h new file mode 100644 index 0000000..052b06c --- /dev/null +++ b/src/ky040.h @@ -0,0 +1,153 @@ +// +// ky040.h +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2022 R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#ifndef _sensor_ky040_h +#define _sensor_ky040_h + +#include +#include +#include +#include + +/// \note This driver supports an interrupt mode and a polling mode. + +class CKY040 /// Driver for KY-040 rotary encoder module +{ +public: + enum TEvent + { + EventClockwise, + EventCounterclockwise, + + EventSwitchDown, + EventSwitchUp, + EventSwitchClick, + EventSwitchDoubleClick, + EventSwitchTripleClick, + EventSwitchHold, ///< generated each second + + EventUnknown + }; + + typedef void TEventHandler (TEvent Event, void *pParam); + +public: + /// \param nCLKPin GPIO pin number of clock pin (encoder pin A) + /// \param nDTPin GPIO pin number of data pin (encoder pin B) + /// \param nSWPin GPIO pin number of switch pin + /// \param pGPIOManager Pointer to GPIO manager object (0 enables polling mode) + CKY040 (unsigned nCLKPin, unsigned nDTPin, unsigned nSWPin, CGPIOManager *pGPIOManager = 0); + + ~CKY040 (void); + + /// \brief Operation successful? + boolean Initialize (void); + + /// \brief Register a handler, to be called on an event from the encoder + /// \param pHandler Pointer to the handler + /// \param pParam Optional user parameter, handed over to the handler + void RegisterEventHandler (TEventHandler *pHandler, void *pParam = 0); + + /// \return Number of seconds, the switch is hold down + /// \note Only valid, when EventSwitchHold has been received. + unsigned GetHoldSeconds (void) const; + + /// \brief Has to be called very frequently in polling mode + void Update (void); + +private: + enum TState + { + StateStart, + StateCWStart, + StateCWBothLow, + StateCWFirstHigh, + StateCCWStart, + StateCCWBothLow, + StateCCWFirstHigh, + StateInvalid, + StateUnknown + }; + + enum TSwitchState + { + SwitchStateStart, + SwitchStateDown, + SwitchStateClick, + SwitchStateDown2, + SwitchStateClick2, + SwitchStateDown3, + SwitchStateClick3, + SwitchStateHold, + SwitchStateInvalid, + SwitchStateUnknown + }; + + enum TSwitchEvent + { + SwitchEventDown, + SwitchEventUp, + SwitchEventTick, + SwitchEventUnknown + }; + +private: + void HandleSwitchEvent (TSwitchEvent SwitchEvent); + + static void EncoderInterruptHandler (void *pParam); + static void SwitchInterruptHandler (void *pParam); + + static void SwitchDebounceHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext); + static void SwitchTickHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext); + +private: + CGPIOPin m_CLKPin; + CGPIOPin m_DTPin; + CGPIOPin m_SWPin; + + boolean m_bPollingMode; + boolean m_bInterruptConnected; + + TEventHandler *m_pEventHandler; + void *m_pEventParam; + + // encoder + TState m_State; + + static TState s_NextState[StateUnknown][2][2]; + static TEvent s_Output[StateUnknown][2][2]; + + // switch low level + TKernelTimerHandle m_hDebounceTimer; + TKernelTimerHandle m_hTickTimer; + + unsigned m_nLastSWLevel; + boolean m_bDebounceActive; + unsigned m_nDebounceLastTicks; + + // switch higher level + TSwitchState m_SwitchState; + unsigned m_nSwitchLastTicks; + unsigned m_nHoldCounter; + + static TSwitchState s_NextSwitchState[SwitchStateUnknown][SwitchEventUnknown]; + static TEvent s_SwitchOutput[SwitchStateUnknown][SwitchEventUnknown]; +}; + +#endif diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 14d7251..476a8f3 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -24,10 +24,10 @@ LOGMODULE ("minidexed"); -CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt) +CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, CGPIOManager *pGPIOManager) : CDexedAdapter (CConfig::MaxNotes, pConfig->GetSampleRate ()), m_pConfig (pConfig), - m_UI (this, pConfig), + m_UI (this, pGPIOManager, pConfig), m_PCKeyboard (this), m_SerialMIDI (this, pInterrupt, pConfig), m_bUseSerial (false), @@ -89,6 +89,11 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) } } +CSysExFileLoader *CMiniDexed::GetSysExFileLoader (void) +{ + return &m_SysExFileLoader; +} + void CMiniDexed::BankSelectLSB (unsigned nBankLSB) { if (nBankLSB > 127) @@ -96,10 +101,9 @@ void CMiniDexed::BankSelectLSB (unsigned nBankLSB) return; } - // MIDI numbering starts with 0, user interface with 1 - printf ("Select voice bank %u\n", nBankLSB+1); - m_SysExFileLoader.SelectVoiceBank (nBankLSB); + + m_UI.BankSelected (nBankLSB); } void CMiniDexed::ProgramChange (unsigned nProgram) @@ -118,8 +122,9 @@ void CMiniDexed::ProgramChange (unsigned nProgram) //// PWM ////////////////////////////////////////////////////////////////////// -CMiniDexedPWM::CMiniDexedPWM (CConfig *pConfig, CInterruptSystem *pInterrupt) -: CMiniDexed (pConfig, pInterrupt), +CMiniDexedPWM::CMiniDexedPWM (CConfig *pConfig, CInterruptSystem *pInterrupt, + CGPIOManager *pGPIOManager) +: CMiniDexed (pConfig, pInterrupt, pGPIOManager), CPWMSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), pConfig->GetChunkSize ()) { @@ -169,8 +174,8 @@ unsigned CMiniDexedPWM::GetChunk (u32 *pBuffer, unsigned nChunkSize) //// I2S ////////////////////////////////////////////////////////////////////// CMiniDexedI2S::CMiniDexedI2S (CConfig *pConfig, CInterruptSystem *pInterrupt, - CI2CMaster *pI2CMaster) -: CMiniDexed (pConfig, pInterrupt), + CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster) +: CMiniDexed (pConfig, pInterrupt, pGPIOManager), CI2SSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), pConfig->GetChunkSize (), false, pI2CMaster, pConfig->GetDACI2CAddress ()) @@ -218,8 +223,9 @@ unsigned CMiniDexedI2S::GetChunk (u32 *pBuffer, unsigned nChunkSize) //// HDMI ///////////////////////////////////////////////////////////////////// -CMiniDexedHDMI::CMiniDexedHDMI (CConfig *pConfig, CInterruptSystem *pInterrupt) -: CMiniDexed (pConfig, pInterrupt), +CMiniDexedHDMI::CMiniDexedHDMI (CConfig *pConfig, CInterruptSystem *pInterrupt, + CGPIOManager *pGPIOManager) +: CMiniDexed (pConfig, pInterrupt, pGPIOManager), CHDMISoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), pConfig->GetChunkSize ()) { diff --git a/src/minidexed.h b/src/minidexed.h index 5e82ebe..d817f87 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -39,12 +40,15 @@ class CMiniDexed : public CDexedAdapter { public: - CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt); + CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, + CGPIOManager *pGPIOManager); virtual bool Initialize (void); void Process (bool bPlugAndPlayUpdated); + CSysExFileLoader *GetSysExFileLoader (void); + void BankSelectLSB (unsigned nBankLSB); void ProgramChange (unsigned nProgram); @@ -69,7 +73,8 @@ protected: class CMiniDexedPWM : public CMiniDexed, public CPWMSoundBaseDevice { public: - CMiniDexedPWM (CConfig *pConfig, CInterruptSystem *pInterrupt); + CMiniDexedPWM (CConfig *pConfig, CInterruptSystem *pInterrupt, + CGPIOManager *pGPIOManager); bool Initialize (void); @@ -82,7 +87,7 @@ class CMiniDexedI2S : public CMiniDexed, public CI2SSoundBaseDevice { public: CMiniDexedI2S (CConfig *pConfig, CInterruptSystem *pInterrupt, - CI2CMaster *pI2CMaster); + CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster); bool Initialize (void); @@ -94,7 +99,8 @@ public: class CMiniDexedHDMI : public CMiniDexed, public CHDMISoundBaseDevice { public: - CMiniDexedHDMI (CConfig *pConfig, CInterruptSystem *pInterrupt); + CMiniDexedHDMI (CConfig *pConfig, CInterruptSystem *pInterrupt, + CGPIOManager *pGPIOManager); bool Initialize (void); diff --git a/src/minidexed.ini b/src/minidexed.ini index bf67453..ff1117b 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -21,6 +21,12 @@ LCDPinData5=23 LCDPinData6=24 LCDPinData7=25 +# KY-040 Rotary Encoder +EncoderEnabled=1 +EncoderPinClock=5 +EncoderPinData=6 +EncoderPinSwitch=26 + # Debug MIDIDumpEnabled=1 ProfileEnabled=1 diff --git a/src/sysexfileloader.cpp b/src/sysexfileloader.cpp index 63d8601..a22a9a5 100644 --- a/src/sysexfileloader.cpp +++ b/src/sysexfileloader.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include "voices.c" @@ -80,7 +81,7 @@ void CSysExFileLoader::Load (void) size_t nLen = strlen (pEntry->d_name); if ( nLen < 5 // "[NNNN]N[_name].syx" - || strcmp (&pEntry->d_name[nLen-4], ".syx") != 0 + || strcasecmp (&pEntry->d_name[nLen-4], ".syx") != 0 || sscanf (pEntry->d_name, "%u", &nBank) != 1) { LOGWARN ("%s: Invalid filename format", pEntry->d_name); @@ -119,6 +120,8 @@ void CSysExFileLoader::Load (void) && m_pVoiceBank[nBank]->StatusEnd == 0xF7) { LOGDBG ("Bank #%u successfully loaded", nBank); + + m_BankFileName[nBank] = pEntry->d_name; } else { @@ -140,6 +143,31 @@ void CSysExFileLoader::Load (void) closedir (pDirectory); } +std::string CSysExFileLoader::GetBankName (unsigned nBankID) +{ + if (nBankID <= MaxVoiceBankID) + { + std::string Result = m_BankFileName[nBankID]; + + size_t nLen = Result.length (); + if (nLen > 4) + { + Result.resize (nLen-4); // remove file extension + + unsigned nBank; + char BankName[30+1]; + if (sscanf (Result.c_str (), "%u_%30s", &nBank, BankName) == 2) + { + Result = BankName; + + return Result; + } + } + } + + return "NO NAME"; +} + void CSysExFileLoader::SelectVoiceBank (unsigned nBankID) { if (nBankID <= MaxVoiceBankID) diff --git a/src/sysexfileloader.h b/src/sysexfileloader.h index 8b0ebab..fb5d0e6 100644 --- a/src/sysexfileloader.h +++ b/src/sysexfileloader.h @@ -56,6 +56,8 @@ public: void Load (void); + std::string GetBankName (unsigned nBankID); // 0 .. 127 + void SelectVoiceBank (unsigned nBankID); // 0 .. 127 void GetVoice (unsigned nVoiceID, // 0 .. 31 @@ -68,6 +70,7 @@ private: std::string m_DirName; TVoiceBank *m_pVoiceBank[MaxVoiceBankID+1]; + std::string m_BankFileName[MaxVoiceBankID+1]; unsigned m_nBankID; diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 7281118..6cc5b90 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -21,22 +21,30 @@ #include "minidexed.h" #include #include +#include #include #include +#include #include LOGMODULE ("ui"); -CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CConfig *pConfig) +CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CConfig *pConfig) : m_pMiniDexed (pMiniDexed), + m_pGPIOManager (pGPIOManager), m_pConfig (pConfig), m_pLCD (0), - m_pLCDBuffered (0) + m_pLCDBuffered (0), + m_pRotaryEncoder (0), + m_UIMode (UIModeVoiceSelect), + m_nBank (0), + m_nProgram (0) { } CUserInterface::~CUserInterface (void) { + delete m_pRotaryEncoder; delete m_pLCDBuffered; delete m_pLCD; } @@ -70,6 +78,24 @@ bool CUserInterface::Initialize (void) LOGDBG ("LCD initialized"); } + if (m_pConfig->GetEncoderEnabled ()) + { + m_pRotaryEncoder = new CKY040 (m_pConfig->GetEncoderPinClock (), + m_pConfig->GetEncoderPinData (), + m_pConfig->GetEncoderPinSwitch (), + m_pGPIOManager); + assert (m_pRotaryEncoder); + + if (!m_pRotaryEncoder->Initialize ()) + { + return false; + } + + m_pRotaryEncoder->RegisterEventHandler (EncoderEventStub, this); + + LOGDBG ("Rotary encoder initialized"); + } + return true; } @@ -81,8 +107,31 @@ void CUserInterface::Process (void) } } +void CUserInterface::BankSelected (unsigned nBankLSB) +{ + assert (nBankLSB < 128); + m_nBank = nBankLSB; + + assert (m_pMiniDexed); + std::string BankName = m_pMiniDexed->GetSysExFileLoader ()->GetBankName (nBankLSB); + + // MIDI numbering starts with 0, user interface with 1 + printf ("Select voice bank %u: \"%s\"\n", nBankLSB+1, BankName.c_str ()); + + if (m_UIMode == UIModeBankSelect) + { + CString String; + String.Format ("\n\r%-12uBANK%s", nBankLSB+1, BankName.c_str ()); + + LCDWrite (String); + } +} + void CUserInterface::ProgramChanged (unsigned nProgram) { + assert (nProgram < 128); + m_nProgram = nProgram; + nProgram++; // MIDI numbering starts with 0, user interface with 1 // fetch program name from Dexed instance @@ -93,9 +142,13 @@ void CUserInterface::ProgramChanged (unsigned nProgram) printf ("Loading voice %u: \"%s\"\n", nProgram, ProgramName); - CString String; - String.Format ("\n\r%u\n\r%s", nProgram, ProgramName); - LCDWrite (String); + if (m_UIMode == UIModeVoiceSelect) + { + CString String; + String.Format ("\n\r%-11uVOICE%s", nProgram, ProgramName); + + LCDWrite (String); + } } void CUserInterface::LCDWrite (const char *pString) @@ -105,3 +158,67 @@ void CUserInterface::LCDWrite (const char *pString) m_pLCDBuffered->Write (pString, strlen (pString)); } } + +void CUserInterface::EncoderEventHandler (CKY040::TEvent Event) +{ + int nStep = 0; + + switch (Event) + { + case CKY040::EventClockwise: + nStep = 1; + break; + + case CKY040::EventCounterclockwise: + nStep = -1; + break; + + case CKY040::EventSwitchClick: + m_UIMode = static_cast (m_UIMode+1); + if (m_UIMode == UIModeUnknown) + { + m_UIMode = UIModeStart; + } + break; + + case CKY040::EventSwitchHold: + if (m_pRotaryEncoder->GetHoldSeconds () >= 3) + { + delete m_pLCD; // reset LCD + + reboot (); + } + return; + + default: + return; + } + + switch (m_UIMode) + { + case UIModeBankSelect: + if (m_nBank + nStep < 128) + { + m_pMiniDexed->BankSelectLSB (m_nBank + nStep); + } + break; + + case UIModeVoiceSelect: + if (m_nProgram + nStep < 32) + { + m_pMiniDexed->ProgramChange (m_nProgram + nStep); + } + break; + + default: + break; + } +} + +void CUserInterface::EncoderEventStub (CKY040::TEvent Event, void *pParam) +{ + CUserInterface *pThis = static_cast (pParam); + assert (pThis != 0); + + pThis->EncoderEventHandler (Event); +} diff --git a/src/userinterface.h b/src/userinterface.h index 5bb24f0..0ce874c 100644 --- a/src/userinterface.h +++ b/src/userinterface.h @@ -21,7 +21,9 @@ #define _userinterface_h #include "config.h" +#include "ky040.h" #include +#include #include class CMiniDexed; @@ -29,24 +31,45 @@ class CMiniDexed; class CUserInterface { public: - CUserInterface (CMiniDexed *pMiniDexed, CConfig *pConfig); + CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CConfig *pConfig); ~CUserInterface (void); bool Initialize (void); void Process (void); + void BankSelected (unsigned nBankLSB); // 0 .. 127 void ProgramChanged (unsigned nProgram); // 0 .. 127 private: void LCDWrite (const char *pString); // Print to optional HD44780 display + void EncoderEventHandler (CKY040::TEvent Event); + static void EncoderEventStub (CKY040::TEvent Event, void *pParam); + +private: + enum TUIMode + { + UIModeStart, + UIModeVoiceSelect = UIModeStart, + UIModeBankSelect, + UIModeUnknown + }; + private: CMiniDexed *m_pMiniDexed; + CGPIOManager *m_pGPIOManager; CConfig *m_pConfig; CHD44780Device *m_pLCD; CWriteBufferDevice *m_pLCDBuffered; + + CKY040 *m_pRotaryEncoder; + + TUIMode m_UIMode; + + unsigned m_nBank; + unsigned m_nProgram; }; #endif