From abebab76a8d13ff68e916d93e2b86ee39b0482f0 Mon Sep 17 00:00:00 2001 From: Steve Lascos Date: Mon, 3 Sep 2018 13:16:17 -0400 Subject: [PATCH] Added Expansion demo for SOS, DmaSPi is now internal --- .../AnalogDelayDemoExpansion.ino | 141 ++- .../SoundOnSoundExpansionDemo.ino | 219 ++++ .../Delay/SoundOnSoundExpansionDemo/name.c | 19 + src/AudioEffectAnalogDelay.h | 11 + src/AudioEffectSOS.h | 3 + src/BAAudioControlWM8731.h | 4 + src/BAPhysicalControls.h | 166 ++- src/DmaSpi.h | 1024 +++++++++++++++++ src/effects/AudioEffectAnalogDelay.cpp | 40 +- src/peripherals/BAAudioControlWM8731.cpp | 49 +- src/peripherals/BAPhysicalControls.cpp | 76 +- src/peripherals/DmaSpi.cpp | 13 + 12 files changed, 1673 insertions(+), 92 deletions(-) create mode 100644 examples/Delay/SoundOnSoundExpansionDemo/SoundOnSoundExpansionDemo.ino create mode 100644 examples/Delay/SoundOnSoundExpansionDemo/name.c create mode 100644 src/DmaSpi.h create mode 100644 src/peripherals/DmaSpi.cpp diff --git a/examples/Delay/AnalogDelayDemoExpansion/AnalogDelayDemoExpansion.ino b/examples/Delay/AnalogDelayDemoExpansion/AnalogDelayDemoExpansion.ino index b4bcb32..5fcff26 100644 --- a/examples/Delay/AnalogDelayDemoExpansion/AnalogDelayDemoExpansion.ino +++ b/examples/Delay/AnalogDelayDemoExpansion/AnalogDelayDemoExpansion.ino @@ -1,11 +1,22 @@ -#include - -#define TGA_PRO_REVB -#define TGA_PRO_EXPAND_REV2 +/************************************************************************* + * This demo uses the BAGuitar library to provide enhanced control of + * the TGA Pro board. + * + * The latest copy of the BA Guitar library can be obtained from + * https://github.com/Blackaddr/BAGuitar + * + * This demo combines the Blackaddr Audio Expansion board to add physical controls + * to the BAAudioEffectAnalogDelay. + * + * You can control the amount of delay, feedback and mix in realtime, as well as cycle + * through the various analog filters built into the effect. + * + */ +#define TGA_PRO_REVB // Set which hardware revision of the TGA Pro we're using +#define TGA_PRO_EXPAND_REV2 // pull in the pin definitions for the Blackaddr Audio Expansion Board. #include "BAGuitar.h" -using namespace midi; using namespace BAEffects; using namespace BALibrary; @@ -16,7 +27,7 @@ BAAudioControlWM8731 codec; //#define USE_EXT // uncomment this line to use External MEM0 #ifdef USE_EXT -// If using external SPI memory, we will instantiance an SRAM +// If using external SPI memory, we will instantiate a SRAM // manager and create an external memory slot to use as the memory // for our audio delay ExternalSramManager externalSram; @@ -42,30 +53,60 @@ AudioConnection delayOut(analogDelay, 0, cabFilter, 0); AudioConnection leftOut(cabFilter,0, i2sOut, 0); AudioConnection rightOut(cabFilter,0, i2sOut, 1); + ////////////////////////////////////////// // SETUP PHYSICAL CONTROLS +// - POT1 (left) will control the amount of delay +// - POT2 (right) will control the amount of feedback +// - POT3 (centre) will control the wet/dry mix. +// - SW1 (left) will be used as a bypass control +// - LED1 (left) will be illuminated when the effect is ON (not bypass) +// - SW2 (right) will be used to cycle through the three built in analog filter styles available. +// - LED2 (right) will illuminate when pressing SW2. ////////////////////////////////////////// -BAPhysicalControls controls(BA_EXPAND_NUM_SW, BA_EXPAND_NUM_POT, 0); +// To get the calibration values for your particular board, first run the +// BAExpansionCalibrate.ino example and +constexpr int potCalibMin = 1; +constexpr int potCalibMax = 1018; +constexpr bool potSwapDirection = true; + +// Create a control object using the number of switches, pots, encoders and outputs on the +// Blackaddr Audio Expansion Board. +BAPhysicalControls controls(BA_EXPAND_NUM_SW, BA_EXPAND_NUM_POT, BA_EXPAND_NUM_ENC, BA_EXPAND_NUM_LED); int loopCount = 0; +unsigned filterIndex = 0; // variable for storing which analog filter we're currently using. +constexpr unsigned MAX_HEADPHONE_VOL = 10; +unsigned headphoneVolume = MAX_HEADPHONE_VOL; // control headphone volume from 0 to 10. + +// BAPhysicalControls returns a handle when you register a new control. We'll uses these handles when working with the controls. +int bypassHandle, filterHandle, delayHandle, feedbackHandle, mixHandle, led1Handle, led2Handle; // Handles for the various controls void setup() { - delay(100); + delay(100); // wait a bit for serial to be available Serial.begin(57600); // Start the serial port delay(100); - // Setup the controls - controls.addSwitch(BA_EXPAND_SW1_PIN); - controls.addSwitch(BA_EXPAND_SW2_PIN); - controls.addPot(BA_EXPAND_POT1_PIN, - - // Disable the codec first + // Setup the controls. The return value is the handle to use when checking for control changes, etc. + // pushbuttons + bypassHandle = controls.addSwitch(BA_EXPAND_SW1_PIN); // will be used for bypass control + filterHandle = controls.addSwitch(BA_EXPAND_SW2_PIN); // will be used for stepping through filters + // pots + delayHandle = controls.addPot(BA_EXPAND_POT1_PIN, potCalibMin, potCalibMax, potSwapDirection); // control the amount of delay + feedbackHandle = controls.addPot(BA_EXPAND_POT2_PIN, potCalibMin, potCalibMax, potSwapDirection); + mixHandle = controls.addPot(BA_EXPAND_POT3_PIN, potCalibMin, potCalibMax, potSwapDirection); + // leds + led1Handle = controls.addOutput(BA_EXPAND_LED1_PIN); + led2Handle = controls.addOutput(BA_EXPAND_LED2_PIN); // will illuminate when pressing SW2 + + // Disable the audio codec first codec.disable(); AudioMemory(128); - // Enable the codec + // Enable and configure the codec Serial.println("Enabling codec...\n"); codec.enable(); + codec.setHeadphoneVolume(1.0f); // Max headphone volume // If using external memory request request memory from the manager // for the slot @@ -77,8 +118,8 @@ void setup() { Serial.println("Using INTERNAL memory"); #endif - // Besure to enable the delay. When disabled, audio is is completely blocked - // to minimize resources to nearly zero. + // Besure to enable the delay. When disabled, audio is is completely blocked by the effect + // to minimize resource usage to nearly to nearly zero. analogDelay.enable(); // Set some default values. @@ -89,18 +130,79 @@ void setup() { ////////////////////////////////// // AnalogDelay filter selection // - // Uncomment to tryout the 3 different built-in filters. + // These are commented out, in this example we'll use SW2 to cycle through the different filters //analogDelay.setFilter(AudioEffectAnalogDelay::Filter::DM3); // The default filter. Naturally bright echo (highs stay, lows fade away) //analogDelay.setFilter(AudioEffectAnalogDelay::Filter::WARM); // A warm filter with a smooth frequency rolloff above 2Khz //analogDelay.setFilter(AudioEffectAnalogDelay::Filter::DARK); // A very dark filter, with a sharp rolloff above 1Khz - // Setup 2-stages of LPF, cutoff 4500 Hz, Q-factor 0.7071 (a 'normal' Q-factor) + // Guitar cabinet: Setup 2-stages of LPF, cutoff 4500 Hz, Q-factor 0.7071 (a 'normal' Q-factor) cabFilter.setLowpass(0, 4500, .7071); cabFilter.setLowpass(1, 4500, .7071); } void loop() { + float potValue; + + // Check if SW1 has been toggled (pushed) + if (controls.isSwitchToggled(bypassHandle)) { + bool bypass = analogDelay.isBypass(); // get the current state + bypass = !bypass; // change it + analogDelay.bypass(bypass); // set the new state + controls.setOutput(led1Handle, !bypass); // Set the LED when NOT bypassed + Serial.println(String("BYPASS is ") + bypass); + } + + // Use SW2 to cycle through the filters + controls.setOutput(led2Handle, controls.getSwitchValue(led2Handle)); + if (controls.isSwitchToggled(filterHandle)) { + filterIndex = (filterIndex + 1) % 3; // update and potentionall roll the counter 0, 1, 2, 0, 1, 2, ... + // cast the index between 0 to 2 to the enum class AudioEffectAnalogDelay::Filter + analogDelay.setFilter(static_cast(filterIndex)); // will cycle through 0 to 2 + Serial.println(String("Filter set to ") + filterIndex); + } + + // Use POT1 (left) to control the delay setting + if (controls.checkPotValue(delayHandle, potValue)) { + // Pot has changed + Serial.println(String("New DELAY setting: ") + potValue); + analogDelay.delayFractionMax(potValue); + } + + // Use POT2 (right) to control the feedback setting + if (controls.checkPotValue(feedbackHandle, potValue)) { + // Pot has changed + Serial.println(String("New FEEDBACK setting: ") + potValue); + analogDelay.feedback(potValue); + } + + // Use POT3 (centre) to control the mix setting + if (controls.checkPotValue(mixHandle, potValue)) { + // Pot has changed + Serial.println(String("New MIX setting: ") + potValue); + analogDelay.mix(potValue); + } + + // Use the 'u' and 'd' keys to adjust volume across ten levels. + if (Serial) { + if (Serial.available() > 0) { + while (Serial.available()) { + char key = Serial.read(); + if (key == 'u') { + headphoneVolume = (headphoneVolume + 1) % MAX_HEADPHONE_VOL; + Serial.println(String("Increasing HEADPHONE volume to ") + headphoneVolume); + } + else if (key == 'd') { + headphoneVolume = (headphoneVolume - 1) % MAX_HEADPHONE_VOL; + Serial.println(String("Decreasing HEADPHONE volume to ") + headphoneVolume); + } + codec.setHeadphoneVolume(static_cast(headphoneVolume) / static_cast(MAX_HEADPHONE_VOL)); + } + } + } + + // Use the loopCounter to roughly measure human timescales. Every few seconds, print the CPU usage + // to the serial port. About 500,000 loops! if (loopCount % 524288 == 0) { Serial.print("Processor Usage, Total: "); Serial.print(AudioProcessorUsage()); Serial.print("% "); @@ -109,5 +211,4 @@ void loop() { } loopCount++; - } diff --git a/examples/Delay/SoundOnSoundExpansionDemo/SoundOnSoundExpansionDemo.ino b/examples/Delay/SoundOnSoundExpansionDemo/SoundOnSoundExpansionDemo.ino new file mode 100644 index 0000000..85a8d88 --- /dev/null +++ b/examples/Delay/SoundOnSoundExpansionDemo/SoundOnSoundExpansionDemo.ino @@ -0,0 +1,219 @@ +/************************************************************************* + * This demo uses the BAGuitar library to provide enhanced control of + * the TGA Pro board. + * + * The latest copy of the BA Guitar library can be obtained from + * https://github.com/Blackaddr/BAGuitar + * + * THIS DEMO REQUIRES BOTH THE EXTERNAL SRAM AND EXPANSION BOARD ADD-ONS + * + * This demo combines the Blackaddr Audio Expansion board with the BAAudioEffectSOS, + * which provides sound-on-sound. The pushbuttons control the opening of the effect + * gate, as well as clearing the sound being held. + * + * The pots control the feedback, as well as the gate opening and close times. + * + */ +#define TGA_PRO_REVB // Set which hardware revision of the TGA Pro we're using +#define TGA_PRO_EXPAND_REV2 // pull in the pin definitions for the Blackaddr Audio Expansion Board. + +#include "BAGuitar.h" + +using namespace BAEffects; +using namespace BALibrary; + +AudioInputI2S i2sIn; +AudioOutputI2S i2sOut; +BAAudioControlWM8731 codec; + +// External SRAM is required for this effect due to the very long +// delays required. +ExternalSramManager externalSram; +ExtMemSlot delaySlot; // Declare an external memory slot. + +AudioEffectSOS sos(&delaySlot); + +// Add some effects for our soloing channel +AudioEffectDelay delayModule; // we'll add a little slapback echo +AudioMixer4 gainModule; // This will be used simply to reduce the gain before the reverb +AudioEffectReverb reverb; // Add a bit of 'verb to our tone +AudioFilterBiquad cabFilter; // We'll want something to cut out the highs and smooth the tone, just like a guitar cab. +AudioMixer4 mixer; + +// Connect the input +AudioConnection inputToSos(i2sIn, 0, sos, 0); +AudioConnection inputToSolo(i2sIn, 0, delayModule, 0); + +// Patch cables for the SOLO channel +AudioConnection inputToGain(delayModule, 0, gainModule, 0); +AudioConnection inputToReverb(gainModule, 0, reverb, 0); + +// Output Mixer +AudioConnection mixer0input(i2sIn, 0, mixer, 0); // SOLO Dry Channel +AudioConnection mixer1input(reverb, 0, mixer, 1); // SOLO Wet Channel +AudioConnection mixer2input(sos, 0, mixer, 2); // SOS Channel +AudioConnection inputToCab(mixer, 0, cabFilter, 0); + +// CODEC Outputs +AudioConnection outputLeft(cabFilter, 0, i2sOut, 0); +AudioConnection outputRight(cabFilter, 0, i2sOut, 1); + +////////////////////////////////////////// +// SETUP PHYSICAL CONTROLS +// - POT1 (left) will control the GATE OPEN time +// - POT2 (right) will control the GATE CLOSE TIME +// - POT3 (centre) will control the EFFECT VOLUME +// - SW1 (left) will be used as the GATE TRIGGER +// - LED1 (left) will be illuminated while the GATE is open +// - SW2 (right) will be used as the CLEAR FEEDBACK TRIGGER +// - LED2 (right) will illuminate when pressing SW2. +////////////////////////////////////////// +// To get the calibration values for your particular board, first run the +// BAExpansionCalibrate.ino example and +constexpr int potCalibMin = 1; +constexpr int potCalibMax = 1018; +constexpr bool potSwapDirection = true; + +// Create a control object using the number of switches, pots, encoders and outputs on the +// Blackaddr Audio Expansion Board. +BAPhysicalControls controls(BA_EXPAND_NUM_SW, BA_EXPAND_NUM_POT, BA_EXPAND_NUM_ENC, BA_EXPAND_NUM_LED); + +int loopCount = 0; +constexpr unsigned MAX_HEADPHONE_VOL = 10; +unsigned headphoneVolume = MAX_HEADPHONE_VOL; // control headphone volume from 0 to 10. +constexpr float MAX_GATE_TIME_MS = 4000.0f; // set maximum gate time of 4 seconds. + +// BAPhysicalControls returns a handle when you register a new control. We'll uses these handles when working with the controls. +int gateHandle, clearHandle, openHandle, closeHandle, volumeHandle, led1Handle, led2Handle; // Handles for the various controls + + +void setup() { + +delay(100); + delay(100); // wait a bit for serial to be available + Serial.begin(57600); // Start the serial port + delay(100); // wait a bit for serial to be available + + // Setup the controls. The return value is the handle to use when checking for control changes, etc. + // pushbuttons + gateHandle = controls.addSwitch(BA_EXPAND_SW1_PIN); // will be used for bypass control + clearHandle = controls.addSwitch(BA_EXPAND_SW2_PIN); // will be used for stepping through filters + // pots + openHandle = controls.addPot(BA_EXPAND_POT1_PIN, potCalibMin, potCalibMax, potSwapDirection); // control the amount of delay + closeHandle = controls.addPot(BA_EXPAND_POT2_PIN, potCalibMin, potCalibMax, potSwapDirection); + volumeHandle = controls.addPot(BA_EXPAND_POT3_PIN, potCalibMin, potCalibMax, potSwapDirection); + // leds + led1Handle = controls.addOutput(BA_EXPAND_LED1_PIN); + led2Handle = controls.addOutput(BA_EXPAND_LED2_PIN); // will illuminate when pressing SW2 + + // Disable the audio codec first + codec.disable(); + AudioMemory(128); + + // Enable the codec + Serial.println("Enabling codec...\n"); + codec.enable(); + codec.setHeadphoneVolume(1.0f); // Max headphone volume + + // We have to request memory be allocated to our slot. + externalSram.requestMemory(&delaySlot, SPI_MEM0_SIZE_BYTES, MemSelect::MEM0, true); + + // Configure the LED to indicate the gate status, this is controlled directly by SOS effect, not by + // by BAPhysicalControls + sos.setGateLedGpio(BA_EXPAND_LED1_PIN); + + // Besure to enable the delay. When disabled, audio is is completely blocked + // to minimize resources to nearly zero. + sos.enable(); + + // Set some default values. + // These can be changed by sending MIDI CC messages over the USB using + // the BAMidiTester application. + sos.bypass(false); + sos.gateOpenTime(3000.0f); + sos.gateCloseTime(1000.0f); + sos.feedback(0.9f); + + // Setup effects on the SOLO channel + gainModule.gain(0, 0.25); // the reverb unit clips easily if the input is too high + delayModule.delay(0, 50.0f); // 50 ms slapback delay + + // Setup 2-stages of LPF, cutoff 4500 Hz, Q-factor 0.7071 (a 'normal' Q-factor) + cabFilter.setLowpass(0, 4500, .7071); + cabFilter.setLowpass(1, 4500, .7071); + + // Setup the Mixer + mixer.gain(0, 0.5f); // SOLO Dry gain + mixer.gain(1, 0.5f); // SOLO Wet gain + mixer.gain(1, 1.0f); // SOS gain + +} + + + +void loop() { + + float potValue; + + // Check if SW1 has been toggled (pushed) and trigger the gate + // LED1 will be directly control by the SOS effect, not by BAPhysicalControls + if (controls.isSwitchToggled(gateHandle)) { + sos.trigger(); + Serial.println("GATE OPEN is triggered"); + } + + // Use SW2 to clear out the SOS delayline + controls.setOutput(led2Handle, controls.getSwitchValue(led2Handle)); + if (controls.isSwitchToggled(clearHandle)) { + sos.clear(); + Serial.println("GATE CLEAR is triggered"); + } + + // Use POT1 (left) to control the OPEN GATE time + if (controls.checkPotValue(openHandle, potValue)) { + // Pot has changed + sos.gateOpenTime(potValue * MAX_GATE_TIME_MS); + Serial.println(String("New OPEN GATE setting (ms): ") + (potValue * MAX_GATE_TIME_MS)); + } + + // Use POT2 (right) to control the feedback setting + if (controls.checkPotValue(closeHandle, potValue)) { + // Pot has changed + sos.gateOpenTime(potValue * MAX_GATE_TIME_MS); + Serial.println(String("New CLOSE GATE setting (ms): ") + (potValue * MAX_GATE_TIME_MS)); + } + + // Use POT3 (centre) to control the sos effect volume + if (controls.checkPotValue(volumeHandle, potValue)) { + // Pot has changed + Serial.println(String("New SOS VOLUME setting: ") + potValue); + sos.volume(potValue); + } + + // Use the 'u' and 'd' keys to adjust headphone volume across ten levels. + if (Serial) { + if (Serial.available() > 0) { + while (Serial.available()) { + char key = Serial.read(); + if (key == 'u') { + headphoneVolume = (headphoneVolume + 1) % MAX_HEADPHONE_VOL; + Serial.println(String("Increasing HEADPHONE volume to ") + headphoneVolume); + } + else if (key == 'd') { + headphoneVolume = (headphoneVolume - 1) % MAX_HEADPHONE_VOL; + Serial.println(String("Decreasing HEADPHONE volume to ") + headphoneVolume); + } + codec.setHeadphoneVolume(static_cast(headphoneVolume) / static_cast(MAX_HEADPHONE_VOL)); + } + } + } + if (loopCount % 524288 == 0) { + Serial.print("Processor Usage, Total: "); Serial.print(AudioProcessorUsage()); + Serial.print("% "); + Serial.print(" sos: "); Serial.print(sos.processorUsage()); + Serial.println("%"); + } + loopCount++; + +} + diff --git a/examples/Delay/SoundOnSoundExpansionDemo/name.c b/examples/Delay/SoundOnSoundExpansionDemo/name.c new file mode 100644 index 0000000..5ea00fe --- /dev/null +++ b/examples/Delay/SoundOnSoundExpansionDemo/name.c @@ -0,0 +1,19 @@ +// To give your project a unique name, this code must be +// placed into a .c file (its own tab). It can not be in +// a .cpp file or your main sketch (the .ino file). + +#include "usb_names.h" + +// Edit these lines to create your own name. The length must +// match the number of characters in your custom name. + +#define MIDI_NAME {'B','l','a','c','k','a','d','d','r',' ','A','u','d','i','o',' ','T','G','A',' ','P','r','o'} +#define MIDI_NAME_LEN 23 + +// Do not change this part. This exact format is required by USB. + +struct usb_string_descriptor_struct usb_string_product_name = { + 2 + MIDI_NAME_LEN * 2, + 3, + MIDI_NAME +}; diff --git a/src/AudioEffectAnalogDelay.h b/src/AudioEffectAnalogDelay.h index e094a8d..86d4ea6 100644 --- a/src/AudioEffectAnalogDelay.h +++ b/src/AudioEffectAnalogDelay.h @@ -86,11 +86,22 @@ public: /// @param delaySamples the request delay in audio samples. Must be less than max delay. void delay(size_t delaySamples); + /// Set the delay as a fraction of the maximum delay. + /// The value should be between 0.0f and 1.0f + void delayFractionMax(float delayFraction); + /// Bypass the effect. /// @param byp when true, bypass wil disable the effect, when false, effect is enabled. /// Note that audio still passes through when bypass is enabled. void bypass(bool byp) { m_bypass = byp; } + /// Get if the effect is bypassed + /// @returns true if bypassed, false if not bypassed + bool isBypass() { return m_bypass; } + + /// Toggle the bypass effect + void toggleBypass() { m_bypass = !m_bypass; } + /// Set the amount of echo feedback (a.k.a regeneration). /// @param feedback a floating point number between 0.0 and 1.0. void feedback(float feedback) { m_feedback = feedback; } diff --git a/src/AudioEffectSOS.h b/src/AudioEffectSOS.h index d72f5ef..2ab7e8d 100644 --- a/src/AudioEffectSOS.h +++ b/src/AudioEffectSOS.h @@ -75,6 +75,9 @@ public: /// Activate the gate automation. Input gate will open, then close. void trigger() { m_inputGateAuto.trigger(); } + /// Activate the delay clearing automation. Input signal will mute, gate will open, then close. + void clear() { m_clearFeedbackAuto.trigger(); } + /// Set the output volume. This affect both the wet and dry signals. /// @details The default is 1.0. /// @param vol Sets the output volume between -1.0 and +1.0 diff --git a/src/BAAudioControlWM8731.h b/src/BAAudioControlWM8731.h index 6a38797..f0e5d6c 100644 --- a/src/BAAudioControlWM8731.h +++ b/src/BAAudioControlWM8731.h @@ -72,6 +72,10 @@ public: ///param val when true, channels are swapped, else normal. void setLeftRightSwap(bool val); + /// Set the volume for the codec's built-in headphone amp + /// @param volume the input volume level from 0.0f to 1.0f; + void setHeadphoneVolume(float volume); + /// Mute/unmute the output DAC, affects both Left and Right output channels. /// @param when true, output DAC is muted, when false, unmuted. void setDacMute(bool val); diff --git a/src/BAPhysicalControls.h b/src/BAPhysicalControls.h index a5110d8..86abcb4 100644 --- a/src/BAPhysicalControls.h +++ b/src/BAPhysicalControls.h @@ -4,7 +4,7 @@ * @company Blackaddr Audio * * BAPhysicalControls is a general purpose class for handling an array of - * pots and switches. + * pots, switches, rotary encoders and outputs (for LEDs or relays). * * @copyright 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 @@ -28,58 +28,115 @@ namespace BALibrary { +constexpr bool SWAP_DIRECTION = true; ///< Use when specifying direction should be swapped +constexpr bool NOSWAP_DIRECTION = false; ///< Use when specifying direction should not be swapped + +/// Convenience class for handling a digital output with regard to setting and toggling stage class DigitalOutput { public: + DigitalOutput() = delete; // delete the default constructor + + /// Construct an object to control the specified pin + /// @param pin the "Logical Arduino" pin number (not the physical pin number DigitalOutput(uint8_t pin) : m_pin(pin) {} + + /// Set the output value + /// @param val when zero output is low, otherwise output his high void set(int val); + + /// Toggle the output value current state void toggle(void); private: - uint8_t m_pin; - int m_val = 0; + uint8_t m_pin; ///< store the pin associated with this output + int m_val = 0; ///< store the value to support toggling }; +/// Convenience class for handling an analog pot as a control. When calibrated, +/// returns a float between 0.0 and 1.0. class Potentiometer { public: + /// Calibration data for a pot includes it's min and max value, as well as whether + /// direction should be swapped. Swapping depends on the physical orientation of the + /// pot. struct Calib { - unsigned min; - unsigned max; - bool swap; + unsigned min; ///< The value from analogRead() when the pot is fully counter-clockwise (normal orientation) + unsigned max; ///< The value from analogRead() when the pot is fully clockwise (normal orientation) + bool swap; ///< when orientation is such that fully clockwise would give max reading, swap changes it to the min }; - Potentiometer(uint8_t pin, unsigned minCalibration, unsigned maxCalibration) - : m_pin(pin), m_swapDirection(false), m_minCalibration(minCalibration), m_maxCalibration(maxCalibration) {} - Potentiometer(uint8_t pin, unsigned minCalibration, unsigned maxCalibration, bool swapDirection) - : m_pin(pin), m_swapDirection(swapDirection), m_minCalibration(minCalibration), m_maxCalibration(maxCalibration) {} + Potentiometer() = delete; // delete the default constructor + + /// Construction requires the Arduino analog pin number, as well as calibration values. + /// @param analogPin The analog Arduino pin literal. This the number on the Teensy pinout preceeeded by an A in the local pin name. E.g. "A17". + /// @param minCalibration See Potentiometer::calibrate() + /// @param maxCalibration See Potentiometer::calibrate() + /// @param swapDirection Optional param. See Potentiometer::calibrate() + Potentiometer(uint8_t analogPin, unsigned minCalibration, unsigned maxCalibration, bool swapDirection = false) + : m_pin(analogPin), m_swapDirection(swapDirection), m_minCalibration(minCalibration), m_maxCalibration(maxCalibration) {} + /// Get new value from the pot. + /// @param value reference to a float, the new value will be written here. Value is between 0.0 and 1.0f. + /// @returns true if the value has changed, false if it has not bool getValue(float &value); + + /// Get the raw int value directly from analogRead() + /// @returns an integer between 0 and 1023. int getRawValue(); + + /// Adjust the calibrate threshold. This is a factor that shrinks the calibration range slightly to + /// ensure the full range from 0.0f to 1.0f can be obtained. + /// @details temperature change can slightly alter the calibration values. This factor causes the value + /// to hit min or max just before the end of the pots travel to compensate. + /// @param thresholdFactor typical value is 0.01f to 0.05f void adjustCalibrationThreshold(float thresholdFactor); - static Calib calibrate(uint8_t pin); + + /// Set the amount of feedback in the IIR filter used to smooth the pot readings + /// @details actual filter reponse deptnds on the rate you call getValue() + /// @param filterValue typical values are 0.80f to 0.95f + void setFeedbackFitlerValue(float fitlerValue); + + /// Call this static function before creating the object to obtain calibration data. The sequence + /// involves prompts over the Serial port. + /// @details E.g. call Potentiometer::calibrate(PIN). See BAExpansionCalibrate.ino in the library examples. + /// @param analogPin the Arduino analog pin number connected to the pot you wish to calibraate. + /// @returns populated Potentiometer::Calib structure + static Calib calibrate(uint8_t analogPin); private: - uint8_t m_pin; - bool m_swapDirection; - unsigned m_minCalibration; - unsigned m_maxCalibration; - unsigned m_lastValue = 0; - unsigned m_lastValue2 = 0; + uint8_t m_pin; ///< store the Arduino pin literal, e.g. A17 + bool m_swapDirection; ///< swap when pot orientation is upside down + unsigned m_minCalibration; ///< stores the min pot value + unsigned m_maxCalibration; ///< stores the max pot value + unsigned m_lastValue = 0; ///< stores previous value + float m_feedbackFitlerValue = 0.9f; ///< feedback value for POT filter }; -constexpr bool ENCODER_SWAP = true; -constexpr bool ENCODER_NOSWAP = false; - +/// Convenience class for rotary (quadrature) encoders. Uses Arduino Encoder under the hood. class RotaryEncoder : public Encoder { public: + RotaryEncoder() = delete; // delete default constructor + + /// Constructor an encoder with the specified pins. Optionally swap direction and divide down the number of encoder ticks + /// @param pin1 the Arduino logical pin number for the 'A' on the encoder. + /// @param pin2 the Arduino logical pin number for the 'B' on the encoder. + /// @param swapDirection (OPTIONAL) set to true or false to obtain clockwise increments the counter-clockwise decrements + /// @param divider (OPTIONAL) controls the sensitivity of the divider. Use powers of 2. E.g. 1, 2, 4, 8, etc. RotaryEncoder(uint8_t pin1, uint8_t pin2, bool swapDirection = false, int divider = 1) : Encoder(pin1,pin2), m_swapDirection(swapDirection), m_divider(divider) {} + /// Get the delta (as a positive or negative number) since last call + /// @returns an integer representing the net change since last call int getChange(); + + /// Set the divider on the internal counter. High resolution encoders without detents can be overly sensitive. + /// This will helf reduce sensisitive by increasing the divider. Default = 1. + /// @pram divider controls the sensitivity of the divider. Use powers of 2. E.g. 1, 2, 4, 8, etc. void setDivider(int divider); private: - bool m_swapDirection; - int32_t m_lastPosition = 0; - int32_t m_divider; + bool m_swapDirection; ///< specifies if increment/decrement should be swapped + int32_t m_lastPosition = 0; ///< store the last recorded position + int32_t m_divider; ///< divides down the magnitude of change read by the encoder. }; /// Specifies the type of control @@ -91,9 +148,17 @@ enum class ControlType : unsigned { UNDEFINED = 255 ///< undefined or uninitialized }; +/// Tjis class provides convenient interface for combinary an arbitrary number of controls of different types into +/// one object. Supports switches, pots, encoders and digital outputs (useful for LEDs, relays). class BAPhysicalControls { public: BAPhysicalControls() = delete; + + /// Construct an object and reserve memory for the specified number of controls. Encoders and outptus are optional params. + /// @param numSwitches the number of switches or buttons + /// @param numPots the number of analog potentiometers + /// @param numEncoders the number of quadrature encoders + /// @param numOutputs the number of digital outputs. E.g. LEDs, relays, etc. BAPhysicalControls(unsigned numSwitches, unsigned numPots, unsigned numEncoders = 0, unsigned numOutputs = 0); ~BAPhysicalControls() {} @@ -124,24 +189,55 @@ public: /// @param swapDirection reverses the which direction is considered pot minimum value /// @param range the pot raw value will be mapped into a range of 0 to range unsigned addPot(uint8_t pin, unsigned minCalibration, unsigned maxCalibration, bool swapDirection); + + /// add an output to the controls + /// @param pin the pin number connected to the Arduino output + /// @returns a handle (unsigned) to the added output. Use this to access the output. unsigned addOutput(uint8_t pin); - void setOutput(unsigned index, int val); - void toggleOutput(unsigned index); - int getRotaryAdjustUnit(unsigned index); - bool checkPotValue(unsigned index, float &value); - bool isSwitchToggled(unsigned index); + + /// Set the output specified by the provided handle + /// @param handle the handle that was provided previously by calling addOutput() + /// @param val the value to set the output. 0 is low, not zero is high. + void setOutput(unsigned handle, int val); + + /// Toggle the output specified by the provided handle + /// @param handle the handle that was provided previously by calling addOutput() + void toggleOutput(unsigned handle); + + /// Retrieve the change in position on the specified rotary encoder + /// @param handle the handle that was provided previously by calling addRotary() + /// @returns an integer value. Positive is clockwise, negative is counter-clockwise rotation. + int getRotaryAdjustUnit(unsigned handle); + + /// Check if the pot specified by the handle has been updated. + /// @param handle the handle that was provided previously by calling addPot() + /// @param value a reference to a float, the pot value will be written to this variable. + /// @returns true if the pot value has changed since previous check, otherwise false + bool checkPotValue(unsigned handle, float &value); + + /// Check if the switch has been toggled since last call + /// @param handle the handle that was provided previously by calling addSwitch() + /// @returns true if the switch changed state, otherwise false + bool isSwitchToggled(unsigned handle); + + /// Check if the switch is currently being pressed (held) + /// @param handle the handle that was provided previously by calling addSwitch() + /// @returns true if the switch is held in a pressed or closed state + bool isSwitchHeld(unsigned handle); + + /// Get the value of the switch + /// @param handle the handle that was provided previously by calling addSwitch() + /// @returns the value at the switch pin, either 0 or 1. + int getSwitchValue(unsigned handle); private: - std::vector m_pots; - std::vector m_encoders; - std::vector m_switches; - std::vector m_outputs; + std::vector m_pots; ///< a vector of all added pots + std::vector m_encoders; ///< a vector of all added encoders + std::vector m_switches; ///< a vector of all added switches + std::vector m_outputs; ///< a vector of all added outputs }; - - - } // BALibrary diff --git a/src/DmaSpi.h b/src/DmaSpi.h new file mode 100644 index 0000000..9f57c25 --- /dev/null +++ b/src/DmaSpi.h @@ -0,0 +1,1024 @@ +#ifndef DMASPI_H +#define DMASPI_H + +#include +#include + +#if(!defined(__arm__) && defined(TEENSYDUINO)) + #error This library is for teensyduino 1.21 on Teensy 3.0, 3.1 and Teensy LC only. +#endif + +#include +#include "DMAChannel.h" +#include + + +/** \brief Specifies the desired CS suppression +**/ +enum TransferType +{ + NORMAL, //*< The transfer will use CS at beginning and end **/ + NO_START_CS, //*< Skip the CS activation at the start **/ + NO_END_CS //*< SKip the CS deactivation at the end **/ +}; + +/** \brief An abstract base class that provides an interface for chip select classes. +**/ +class AbstractChipSelect +{ + public: + /** \brief Called to select a chip. The implementing class can do other things as well. + **/ + virtual void select(TransferType transferType = TransferType::NORMAL) = 0; + + /** \brief Called to deselect a chip. The implementing class can do other things as well. + **/ + virtual void deselect(TransferType transferType = TransferType::NORMAL) = 0; + + /** \brief the virtual destructor needed to inherit from this class **/ + virtual ~AbstractChipSelect() {} +}; + + +/** \brief "do nothing" chip select class **/ +class DummyChipSelect : public AbstractChipSelect +{ + void select(TransferType transferType = TransferType::NORMAL) override {} + + void deselect(TransferType transferType = TransferType::NORMAL) override {} +}; + +/** \brief "do nothing" chip select class that + * outputs a message through Serial when something happens +**/ +class DebugChipSelect : public AbstractChipSelect +{ + void select(TransferType transferType = TransferType::NORMAL) override {Serial.println("Debug CS: select()");} + void deselect(TransferType transferType = TransferType::NORMAL) override {Serial.println("Debug CS: deselect()");} +}; + +/** \brief An active low chip select class. This also configures the given pin. + * Warning: This class is hardcoded to manage a transaction on SPI (SPI0, that is). + * If you want to use SPI1: Use AbstractChipSelect1 (see below) + * If you want to use SPI2: Create AbstractChipSelect2 (adapt the implementation accordingly). + * Something more flexible is on the way. +**/ +class ActiveLowChipSelect : public AbstractChipSelect +{ + public: + /** Configures a chip select pin for OUTPUT mode, + * manages the chip selection and a corresponding SPI transaction + * + * The chip select pin is asserted \e after the SPI settings are applied + * and deasserted before the SPI transaction ends. + * \param pin the CS pin to use + * \param settings which SPI settings to apply when the chip is selected + **/ + ActiveLowChipSelect(const unsigned int& pin, const SPISettings& settings) + : pin_(pin), + settings_(settings) + { + pinMode(pin, OUTPUT); + digitalWriteFast(pin, 1); + } + + /** \brief begins an SPI transaction selects the chip (sets the pin to low) and + **/ + void select(TransferType transferType = TransferType::NORMAL) override + { + SPI.beginTransaction(settings_); + if (transferType == TransferType::NO_START_CS) { + return; + } + digitalWriteFast(pin_, 0); + } + + /** \brief deselects the chip (sets the pin to high) and ends the SPI transaction + **/ + void deselect(TransferType transferType = TransferType::NORMAL) override + { + if (transferType == TransferType::NO_END_CS) { + } else { + digitalWriteFast(pin_, 1); + } + SPI.endTransaction(); + } + private: + const unsigned int pin_; + const SPISettings settings_; + +}; + +#if defined(__MK66FX1M0__) +class ActiveLowChipSelect1 : public AbstractChipSelect +{ + public: + /** Equivalent to AbstractChipSelect, but for SPI1. + **/ + ActiveLowChipSelect1(const unsigned int& pin, const SPISettings& settings) + : pin_(pin), + settings_(settings) + { + pinMode(pin, OUTPUT); + digitalWriteFast(pin, 1); + } + + /** \brief begins an SPI transaction selects the chip (sets the pin to low) and + **/ + void select(TransferType transferType = TransferType::NORMAL) override + { + SPI1.beginTransaction(settings_); + if (transferType == TransferType::NO_START_CS) { + return; + } + digitalWriteFast(pin_, 0); + } + + /** \brief deselects the chip (sets the pin to high) and ends the SPI transaction + **/ + void deselect(TransferType transferType = TransferType::NORMAL) override + { + if (transferType == TransferType::NO_END_CS) { + } else { + digitalWriteFast(pin_, 1); + } + SPI1.endTransaction(); + } + private: + const unsigned int pin_; + const SPISettings settings_; + +}; +#endif + + +//#define DEBUG_DMASPI 1 + +#if defined(DEBUG_DMASPI) + #define DMASPI_PRINT(x) do {Serial.printf x ; Serial.flush();} while (0); +#else + #define DMASPI_PRINT(x) do {} while (0); +#endif + +namespace DmaSpi +{ + /** \brief describes an SPI transfer + * + * Transfers are kept in a queue (intrusive linked list) until they are processed by the DmaSpi driver. + * + **/ + class Transfer + { + public: + /** \brief The Transfer's current state. + * + **/ + enum State + { + idle, /**< The Transfer is idle, the DmaSpi has not seen it yet. **/ + eDone, /**< The Transfer is done. **/ + pending, /**< Queued, but not handled yet. **/ + inProgress, /**< The DmaSpi driver is currently busy executing this Transfer. **/ + error /**< An error occured. **/ + }; + + /** \brief Creates a Transfer object. + * \param pSource pointer to the data source. If this is nullptr, the fill value is used instead. + * \param transferCount the number of SPI transfers to perform. + * \param pDest pointer to the data sink. If this is nullptr, data received from the slave will be discarded. + * \param fill if pSource is nullptr, this value is sent to the slave instead. + * \param cs pointer to a chip select object. + * If not nullptr, cs->select() is called when the Transfer is started and cs->deselect() is called when the Transfer is finished. + **/ + Transfer(const uint8_t* pSource = nullptr, + const uint16_t& transferCount = 0, + volatile uint8_t* pDest = nullptr, + const uint8_t& fill = 0, + AbstractChipSelect* cs = nullptr, + TransferType transferType = TransferType::NORMAL + ) : m_state(State::idle), + m_pSource(pSource), + m_transferCount(transferCount), + m_pDest(pDest), + m_fill(fill), + m_pNext(nullptr), + m_pSelect(cs), + m_transferType(transferType) + { + DMASPI_PRINT(("Transfer @ %p\n", this)); + }; + + /** \brief Check if the Transfer is busy, i.e. may not be modified. + **/ + bool busy() const {return ((m_state == State::pending) || (m_state == State::inProgress) || (m_state == State::error));} + + /** \brief Check if the Transfer is done. + **/ + bool done() const {return (m_state == State::eDone);} + +// private: + volatile State m_state; + const uint8_t* m_pSource; + uint16_t m_transferCount; + volatile uint8_t* m_pDest; + uint8_t m_fill; + Transfer* m_pNext; + AbstractChipSelect* m_pSelect; + TransferType m_transferType; + }; +} // namespace DmaSpi + + +template +class AbstractDmaSpi +{ + public: + using Transfer = DmaSpi::Transfer; + + /** \brief arduino-style initialization. + * + * During initialization, two DMA channels are allocated. If that fails, this function returns false. + * If the channels could be allocated, those DMA channel fields that don't change during DMA SPI operation + * are initialized to the values they will have at runtime. + * + * \return true if initialization was successful; false otherwise. + * \see end() + **/ + static bool begin() + { + if(init_count_ > 0) + { + return true; // this is not particularly bad, so we can return true + } + init_count_++; + DMASPI_PRINT(("DmaSpi::begin() : ")); + // create DMA channels, might fail + if (!createDmaChannels()) + { + DMASPI_PRINT(("could not create DMA channels\n")); + return false; + } + state_ = eStopped; + // tx: known destination (SPI), no interrupt, finish silently + begin_setup_txChannel(); + if (txChannel_()->error()) + { + destroyDmaChannels(); + DMASPI_PRINT(("tx channel error\n")); + return false; + } + + // rx: known source (SPI), interrupt on completion + begin_setup_rxChannel(); + if (rxChannel_()->error()) + { + destroyDmaChannels(); + DMASPI_PRINT(("rx channel error\n")); + return false; + } + + return true; + } + + static void begin_setup_txChannel() {DMASPI_INSTANCE::begin_setup_txChannel_impl();} + static void begin_setup_rxChannel() {DMASPI_INSTANCE::begin_setup_rxChannel_impl();} + + /** \brief Allow the DMA SPI to start handling Transfers. This must be called after begin(). + * \see running() + * \see busy() + * \see stop() + * \see stopping() + * \see stopped() + **/ + static void start() + { + DMASPI_PRINT(("DmaSpi::start() : state_ = ")); + switch(state_) + { + case eStopped: + DMASPI_PRINT(("eStopped\n")); + state_ = eRunning; + beginPendingTransfer(); + break; + + case eRunning: + DMASPI_PRINT(("eRunning\n")); + break; + + case eStopping: + DMASPI_PRINT(("eStopping\n")); + state_ = eRunning; + break; + + default: + DMASPI_PRINT(("unknown\n")); + state_ = eError; + break; + } + } + + /** \brief check if the DMA SPI is in running state. + * \return true if the DMA SPI is in running state, false otherwise. + * \see start() + * \see busy() + * \see stop() + * \see stopping() + * \see stopped() + **/ + static bool running() {return state_ == eRunning;} + + /** \brief register a Transfer to be handled by the DMA SPI. + * \return false if the Transfer had an invalid transfer count (zero or greater than 32767), true otherwise. + * \post the Transfer state is Transfer::State::pending, or Transfer::State::error if the transfer count was invalid. + **/ + static bool registerTransfer(Transfer& transfer) + { + DMASPI_PRINT(("DmaSpi::registerTransfer(%p)\n", &transfer)); + if ((transfer.busy()) + || (transfer.m_transferCount == 0) // no zero length transfers allowed + || (transfer.m_transferCount >= 0x8000)) // max CITER/BITER count with ELINK = 0 is 0x7FFF, so reject + { + DMASPI_PRINT((" Transfer is busy or invalid, dropped\n")); + transfer.m_state = Transfer::State::error; + return false; + } + addTransferToQueue(transfer); + if ((state_ == eRunning) && (!busy())) + { + DMASPI_PRINT((" starting transfer\n")); + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + beginPendingTransfer(); + } + } + return true; + } + + + /** \brief Check if the DMA SPI is busy, which means that it is currently handling a Transfer. + \return true if a Transfer is being handled. + * \see start() + * \see running() + * \see stop() + * \see stopping() + * \see stopped() + **/ + static bool busy() + { + return (m_pCurrentTransfer != nullptr); + } + + /** \brief Request the DMA SPI to stop handling Transfers. + * + * The stopping driver may finish a current Transfer, but it will then not start a new, pending one. + * \see start() + * \see running() + * \see busy() + * \see stopping() + * \see stopped() + **/ + static void stop() + { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + switch(state_) + { + case eStopped: + break; + case eRunning: + if (busy()) + { + state_ = eStopping; + } + else + { + // this means that the DMA SPI simply has nothing to do + state_ = eStopped; + } + break; + case eStopping: + break; + default: + state_ = eError; + break; + } + } + } + + /** \brief See if the DMA SPI is currently switching from running to stopped state + * \return true if the DMA SPI is switching from running to stopped state + * \see start() + * \see running() + * \see busy() + * \see stop() + * \see stopped() + **/ + static bool stopping() { return (state_ == eStopping); } + + /** \brief See if the DMA SPI is stopped + * \return true if the DMA SPI is in stopped state, i.e. not handling pending Transfers + * \see start() + * \see running() + * \see busy() + * \see stop() + * \see stopping() + **/ + static bool stopped() { return (state_ == eStopped); } + + /** \brief Shut down the DMA SPI + * + * Deallocates DMA channels and sets the internal state to error (this might not be an intelligent name for that) + * \see begin() + **/ + static void end() + { + if (init_count_ == 0) + { + state_ = eError; + return; + } + if (init_count_ == 1) + { + init_count_--; + destroyDmaChannels(); + state_ = eError; + return; + } + else + { + init_count_--; + return; + } + } + + /** \brief get the last value that was read from a slave, but discarded because the Transfer didn't specify a sink + **/ + static uint8_t devNull() + { + return m_devNull; + } + + protected: + enum EState + { + eStopped, + eRunning, + eStopping, + eError + }; + + static void addTransferToQueue(Transfer& transfer) + { + transfer.m_state = Transfer::State::pending; + transfer.m_pNext = nullptr; + DMASPI_PRINT((" DmaSpi::addTransferToQueue() : queueing transfer\n")); + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + if (m_pNextTransfer == nullptr) + { + m_pNextTransfer = &transfer; + } + else + { + m_pLastTransfer->m_pNext = &transfer; + } + m_pLastTransfer = &transfer; + } + } + + static void post_finishCurrentTransfer() {DMASPI_INSTANCE::post_finishCurrentTransfer_impl();} + + static void finishCurrentTransfer() + { + if (m_pCurrentTransfer->m_pSelect != nullptr) + { + m_pCurrentTransfer->m_pSelect->deselect(m_pCurrentTransfer->m_transferType); + } + else + { + m_Spi.endTransaction(); + } + m_pCurrentTransfer->m_state = Transfer::State::eDone; + DMASPI_PRINT((" finishCurrentTransfer() @ %p\n", m_pCurrentTransfer)); + m_pCurrentTransfer = nullptr; + post_finishCurrentTransfer(); + } + + static bool createDmaChannels() + { + if (txChannel_() == nullptr) + { + return false; + } + if (rxChannel_() == nullptr) + { + delete txChannel_(); + return false; + } + return true; + } + + static void destroyDmaChannels() + { + if (rxChannel_() != nullptr) + { + delete rxChannel_(); + } + if (txChannel_() != nullptr) + { + delete txChannel_(); + } + } + + static DMAChannel* rxChannel_() + { + static DMAChannel* pChannel = new DMAChannel(); + return pChannel; + } + + static DMAChannel* txChannel_() + { + static DMAChannel* pChannel = new DMAChannel(); + return pChannel; + } + + static void rxIsr_() + { + DMASPI_PRINT(("DmaSpi::rxIsr_()\n")); + rxChannel_()->clearInterrupt(); + // end current transfer: deselect and mark as done + finishCurrentTransfer(); + + DMASPI_PRINT((" state = ")); + switch(state_) + { + case eStopped: // this should not happen! + DMASPI_PRINT(("eStopped\n")); + state_ = eError; + break; + case eRunning: + DMASPI_PRINT(("eRunning\n")); + beginPendingTransfer(); + break; + case eStopping: + DMASPI_PRINT(("eStopping\n")); + state_ = eStopped; + break; + case eError: + DMASPI_PRINT(("eError\n")); + break; + default: + DMASPI_PRINT(("eUnknown\n")); + state_ = eError; + break; + } + } + + static void pre_cs() {DMASPI_INSTANCE::pre_cs_impl();} + static void post_cs() {DMASPI_INSTANCE::post_cs_impl();} + + static void beginPendingTransfer() + { + if (m_pNextTransfer == nullptr) + { + DMASPI_PRINT(("DmaSpi::beginNextTransfer: no pending transfer\n")); + return; + } + + m_pCurrentTransfer = m_pNextTransfer; + DMASPI_PRINT(("DmaSpi::beginNextTransfer: starting transfer @ %p\n", m_pCurrentTransfer)); + m_pCurrentTransfer->m_state = Transfer::State::inProgress; + m_pNextTransfer = m_pNextTransfer->m_pNext; + if (m_pNextTransfer == nullptr) + { + DMASPI_PRINT((" this was the last in the queue\n")); + m_pLastTransfer = nullptr; + } + + // configure Rx DMA + if (m_pCurrentTransfer->m_pDest != nullptr) + { + // real data sink + DMASPI_PRINT((" real sink\n")); + rxChannel_()->destinationBuffer(m_pCurrentTransfer->m_pDest, + m_pCurrentTransfer->m_transferCount); + } + else + { + // dummy data sink + DMASPI_PRINT((" dummy sink\n")); + rxChannel_()->destination(m_devNull); + rxChannel_()->transferCount(m_pCurrentTransfer->m_transferCount); + } + + // configure Tx DMA + if (m_pCurrentTransfer->m_pSource != nullptr) + { + // real data source + DMASPI_PRINT((" real source\n")); + txChannel_()->sourceBuffer(m_pCurrentTransfer->m_pSource, + m_pCurrentTransfer->m_transferCount); + } + else + { + // dummy data source + DMASPI_PRINT((" dummy source\n")); + txChannel_()->source(m_pCurrentTransfer->m_fill); + txChannel_()->transferCount(m_pCurrentTransfer->m_transferCount); + } + + pre_cs(); + + // Select Chip + if (m_pCurrentTransfer->m_pSelect != nullptr) + { + m_pCurrentTransfer->m_pSelect->select(m_pCurrentTransfer->m_transferType); + } + else + { + m_Spi.beginTransaction(SPISettings()); + } + + post_cs(); + } + + static size_t init_count_; + static volatile EState state_; + static Transfer* volatile m_pCurrentTransfer; + static Transfer* volatile m_pNextTransfer; + static Transfer* volatile m_pLastTransfer; + static volatile uint8_t m_devNull; + //static SPICLASS& m_Spi; +}; + +template +size_t AbstractDmaSpi::init_count_ = 0; + +template +volatile typename AbstractDmaSpi::EState AbstractDmaSpi::state_ = eError; + +template +typename AbstractDmaSpi::Transfer* volatile AbstractDmaSpi::m_pNextTransfer = nullptr; + +template +typename AbstractDmaSpi::Transfer* volatile AbstractDmaSpi::m_pCurrentTransfer = nullptr; + +template +typename AbstractDmaSpi::Transfer* volatile AbstractDmaSpi::m_pLastTransfer = nullptr; + +template +volatile uint8_t AbstractDmaSpi::m_devNull = 0; + +#if defined(KINETISK) + +class DmaSpi0 : public AbstractDmaSpi +{ +public: + static void begin_setup_txChannel_impl() + { + txChannel_()->disable(); + txChannel_()->destination((volatile uint8_t&)SPI0_PUSHR); + txChannel_()->disableOnCompletion(); + txChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_TX); + } + + static void begin_setup_rxChannel_impl() + { + rxChannel_()->disable(); + rxChannel_()->source((volatile uint8_t&)SPI0_POPR); + rxChannel_()->disableOnCompletion(); + rxChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_RX); + rxChannel_()->attachInterrupt(rxIsr_); + rxChannel_()->interruptAtCompletion(); + } + + static void pre_cs_impl() + { + SPI0_SR = 0xFF0F0000; + SPI0_RSER = SPI_RSER_RFDF_RE | SPI_RSER_RFDF_DIRS | SPI_RSER_TFFF_RE | SPI_RSER_TFFF_DIRS; + } + + static void post_cs_impl() + { + rxChannel_()->enable(); + txChannel_()->enable(); + } + + static void post_finishCurrentTransfer_impl() + { + SPI0_RSER = 0; + SPI0_SR = 0xFF0F0000; + } + +private: +}; + +extern DmaSpi0 DMASPI0; + +#if defined(__MK66FX1M0__) + +class DmaSpi1 : public AbstractDmaSpi +{ +public: + static void begin_setup_txChannel_impl() + { + txChannel_()->disable(); + txChannel_()->destination((volatile uint8_t&)SPI1_PUSHR); + txChannel_()->disableOnCompletion(); + txChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI1_TX); + } + + static void begin_setup_rxChannel_impl() + { + rxChannel_()->disable(); + rxChannel_()->source((volatile uint8_t&)SPI1_POPR); + rxChannel_()->disableOnCompletion(); + rxChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI1_RX); + rxChannel_()->attachInterrupt(rxIsr_); + rxChannel_()->interruptAtCompletion(); + } + + static void pre_cs_impl() + { + SPI1_SR = 0xFF0F0000; + SPI1_RSER = SPI_RSER_RFDF_RE | SPI_RSER_RFDF_DIRS | SPI_RSER_TFFF_RE | SPI_RSER_TFFF_DIRS; + } + + static void post_cs_impl() + { + rxChannel_()->enable(); + txChannel_()->enable(); + } + + static void post_finishCurrentTransfer_impl() + { + SPI1_RSER = 0; + SPI1_SR = 0xFF0F0000; + } + +private: +}; + +/* +class DmaSpi2 : public AbstractDmaSpi +{ +public: + static void begin_setup_txChannel_impl() + { + txChannel_()->disable(); + txChannel_()->destination((volatile uint8_t&)SPI2_PUSHR); + txChannel_()->disableOnCompletion(); + txChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI2_TX); + } + + static void begin_setup_rxChannel_impl() + { + rxChannel_()->disable(); + rxChannel_()->source((volatile uint8_t&)SPI2_POPR); + rxChannel_()->disableOnCompletion(); + rxChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI2_RX); + rxChannel_()->attachInterrupt(rxIsr_); + rxChannel_()->interruptAtCompletion(); + } + + static void pre_cs_impl() + { + SPI2_SR = 0xFF0F0000; + SPI2_RSER = SPI_RSER_RFDF_RE | SPI_RSER_RFDF_DIRS | SPI_RSER_TFFF_RE | SPI_RSER_TFFF_DIRS; + } + + static void post_cs_impl() + { + rxChannel_()->enable(); + txChannel_()->enable(); + } + + static void post_finishCurrentTransfer_impl() + { + SPI2_RSER = 0; + SPI2_SR = 0xFF0F0000; + } + +private: +}; +*/ + +extern DmaSpi1 DMASPI1; +//extern DmaSpi2 DMASPI2; +#endif // defined(__MK66FX1M0__) + +#elif defined(KINETISL) +class DmaSpi0 : public AbstractDmaSpi +{ +public: + static void begin_setup_txChannel_impl() + { + txChannel_()->disable(); + txChannel_()->destination((volatile uint8_t&)SPI0_DL); + txChannel_()->disableOnCompletion(); + txChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_TX); + } + + static void begin_setup_rxChannel_impl() + { + rxChannel_()->disable(); + rxChannel_()->source((volatile uint8_t&)SPI0_DL); + rxChannel_()->disableOnCompletion(); + rxChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_RX); + rxChannel_()->attachInterrupt(rxIsr_); + rxChannel_()->interruptAtCompletion(); + } + + static void pre_cs_impl() + { + // disable SPI and enable SPI DMA requests + SPI0_C1 &= ~(SPI_C1_SPE); + SPI0_C2 |= SPI_C2_TXDMAE | SPI_C2_RXDMAE; + } + + static void post_cs_impl() + { + rxChannel_()->enable(); + txChannel_()->enable(); + } + + static void post_finishCurrentTransfer_impl() + { + SPI0_C2 = 0; + txChannel_()->clearComplete(); + rxChannel_()->clearComplete(); + } + +private: +}; + +class DmaSpi1 : public AbstractDmaSpi +{ +public: + static void begin_setup_txChannel_impl() + { + txChannel_()->disable(); + txChannel_()->destination((volatile uint8_t&)SPI1_DL); + txChannel_()->disableOnCompletion(); + txChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI1_TX); + } + + static void begin_setup_rxChannel_impl() + { + rxChannel_()->disable(); + rxChannel_()->source((volatile uint8_t&)SPI1_DL); + rxChannel_()->disableOnCompletion(); + rxChannel_()->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI1_RX); + rxChannel_()->attachInterrupt(rxIsr_); + rxChannel_()->interruptAtCompletion(); + } + + static void pre_cs_impl() + { + // disable SPI and enable SPI DMA requests + SPI1_C1 &= ~(SPI_C1_SPE); + SPI1_C2 |= SPI_C2_TXDMAE | SPI_C2_RXDMAE; + } + +// static void dumpCFG(const char *sz, uint32_t* p) +// { +// DMASPI_PRINT(("%s: %x %x %x %x \n", sz, p[0], p[1], p[2], p[3])); +// } + + static void post_cs_impl() + { + DMASPI_PRINT(("post_cs S C1 C2: %x %x %x\n", SPI1_S, SPI1_C1, SPI1_C2)); +// dumpCFG("RX", (uint32_t*)(void*)rxChannel_()->CFG); +// dumpCFG("TX", (uint32_t*)(void*)txChannel_()->CFG); + rxChannel_()->enable(); + txChannel_()->enable(); + } + + static void post_finishCurrentTransfer_impl() + { + SPI1_C2 = 0; + txChannel_()->clearComplete(); + rxChannel_()->clearComplete(); + } +private: +}; + +extern DmaSpi0 DMASPI0; +extern DmaSpi1 DMASPI1; + +#else + +#error Unknown chip + +#endif // KINETISK else KINETISL + +class DmaSpiGeneric +{ +public: + using Transfer = DmaSpi::Transfer; + + DmaSpiGeneric() { + m_spiDma0 = &DMASPI0; +#if defined(__MK66FX1M0__) + m_spiDma1 = &DMASPI1; +#endif + } + DmaSpiGeneric(int spiId) : DmaSpiGeneric() { + m_spiSelect = spiId; + } + + bool begin () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->begin(); + default : + return m_spiDma0->begin(); + } + } + + void start () { + switch(m_spiSelect) { + case 1 : m_spiDma1->start(); return; + default : + m_spiDma0->start(); return; + } + } + + bool running () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->running(); + default : + return m_spiDma0->running(); + } + } + + bool registerTransfer (Transfer& transfer) { + switch(m_spiSelect) { + case 1 : return m_spiDma1->registerTransfer(transfer); + default : + return m_spiDma0->registerTransfer(transfer); + } + } + + + bool busy () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->busy(); + default : + return m_spiDma0->busy(); + } + } + + void stop () { + switch(m_spiSelect) { + case 1 : m_spiDma1->stop(); return; + default : + m_spiDma0->stop(); return; + } + } + + bool stopping () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->stopping(); + default : + return m_spiDma0->stopping(); + } + } + + bool stopped () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->stopped(); + default : + return m_spiDma0->stopped(); + } + } + + void end () { + switch(m_spiSelect) { + case 1 : m_spiDma1->end(); return; + default : + m_spiDma0->end(); return; + } + } + + uint8_t devNull () { + switch(m_spiSelect) { + case 1 : return m_spiDma1->devNull(); + default : + return m_spiDma0->devNull(); + } + } + +private: + int m_spiSelect = 0; + DmaSpi0 *m_spiDma0 = nullptr; +#if defined(__MK66FX1M0__) + DmaSpi1 *m_spiDma1 = nullptr; +#else + // just make it Spi0 so it compiles atleast + DmaSpi0 *m_spiDma1 = nullptr; +#endif + +}; + + +#endif // DMASPI_H diff --git a/src/effects/AudioEffectAnalogDelay.cpp b/src/effects/AudioEffectAnalogDelay.cpp index 52a0438..fa12c15 100644 --- a/src/effects/AudioEffectAnalogDelay.cpp +++ b/src/effects/AudioEffectAnalogDelay.cpp @@ -177,11 +177,11 @@ void AudioEffectAnalogDelay::delay(float milliseconds) if (!m_externalMemory) { // internal memory - QueuePosition queuePosition = calcQueuePosition(milliseconds); - Serial.println(String("CONFIG: delay:") + delaySamples + String(" queue position ") + queuePosition.index + String(":") + queuePosition.offset); + //QueuePosition queuePosition = calcQueuePosition(milliseconds); + //Serial.println(String("CONFIG: delay:") + delaySamples + String(" queue position ") + queuePosition.index + String(":") + queuePosition.offset); } else { // external memory - Serial.println(String("CONFIG: delay:") + delaySamples); + //Serial.println(String("CONFIG: delay:") + delaySamples); ExtMemSlot *slot = m_memory->getSlot(); if (!slot) { Serial.println("ERROR: slot ptr is not valid"); } @@ -200,17 +200,43 @@ void AudioEffectAnalogDelay::delay(size_t delaySamples) if (!m_externalMemory) { // internal memory - QueuePosition queuePosition = calcQueuePosition(delaySamples); - Serial.println(String("CONFIG: delay:") + delaySamples + String(" queue position ") + queuePosition.index + String(":") + queuePosition.offset); + //QueuePosition queuePosition = calcQueuePosition(delaySamples); + //Serial.println(String("CONFIG: delay:") + delaySamples + String(" queue position ") + queuePosition.index + String(":") + queuePosition.offset); } else { // external memory - Serial.println(String("CONFIG: delay:") + delaySamples); + //Serial.println(String("CONFIG: delay:") + delaySamples); ExtMemSlot *slot = m_memory->getSlot(); if (!slot->isEnabled()) { slot->enable(); } } - m_delaySamples= delaySamples; + m_delaySamples = delaySamples; +} + +void AudioEffectAnalogDelay::delayFractionMax(float delayFraction) +{ + size_t delaySamples = static_cast(static_cast(m_memory->getMaxDelaySamples()) * delayFraction); + + if (delaySamples > m_memory->getMaxDelaySamples()) { + // this exceeds max delay value, limit it. + delaySamples = m_memory->getMaxDelaySamples(); + } + + if (!m_memory) { Serial.println("delay(): m_memory is not valid"); } + + if (!m_externalMemory) { + // internal memory + //QueuePosition queuePosition = calcQueuePosition(delaySamples); + //Serial.println(String("CONFIG: delay:") + delaySamples + String(" queue position ") + queuePosition.index + String(":") + queuePosition.offset); + } else { + // external memory + //Serial.println(String("CONFIG: delay:") + delaySamples); + ExtMemSlot *slot = m_memory->getSlot(); + if (!slot->isEnabled()) { + slot->enable(); + } + } + m_delaySamples = delaySamples; } void AudioEffectAnalogDelay::m_preProcessing(audio_block_t *out, audio_block_t *dry, audio_block_t *wet) diff --git a/src/peripherals/BAAudioControlWM8731.cpp b/src/peripherals/BAAudioControlWM8731.cpp index e1960e3..f2d635e 100644 --- a/src/peripherals/BAAudioControlWM8731.cpp +++ b/src/peripherals/BAAudioControlWM8731.cpp @@ -60,6 +60,26 @@ constexpr int WM8731_RIGHT_INPUT_MUTE_SHIFT = 7; constexpr int WM8731_LINK_RIGHT_LEFT_IN_ADDR = 1; constexpr int WM8731_LINK_RIGHT_LEFT_IN_MASK = 0x100; constexpr int WM8731_LINK_RIGHT_LEFT_IN_SHIFT = 8; +// Register 2 +constexpr int WM8731_LEFT_HEADPHONE_VOL_ADDR = 2; +constexpr int WM8731_LEFT_HEADPHONE_VOL_MASK = 0x7F; +constexpr int WM8731_LEFT_HEADPHONE_VOL_SHIFT = 0; +constexpr int WM8731_LEFT_HEADPHONE_ZCD_ADDR = 2; +constexpr int WM8731_LEFT_HEADPHONE_ZCD_MASK = 0x80; +constexpr int WM8731_LEFT_HEADPHONE_ZCD_SHIFT = 7; +constexpr int WM8731_LEFT_HEADPHONE_LINK_ADDR = 2; +constexpr int WM8731_LEFT_HEADPHONE_LINK_MASK = 0x100; +constexpr int WM8731_LEFT_HEADPHONE_LINK_SHIFT = 8; +// Register 3 +constexpr int WM8731_RIGHT_HEADPHONE_VOL_ADDR = 3; +constexpr int WM8731_RIGHT_HEADPHONE_VOL_MASK = 0x7F; +constexpr int WM8731_RIGHT_HEADPHONE_VOL_SHIFT = 0; +constexpr int WM8731_RIGHT_HEADPHONE_ZCD_ADDR = 3; +constexpr int WM8731_RIGHT_HEADPHONE_ZCD_MASK = 0x80; +constexpr int WM8731_RIGHT_HEADPHONE_ZCD_SHIFT = 7; +constexpr int WM8731_RIGHT_HEADPHONE_LINK_ADDR = 3; +constexpr int WM8731_RIGHT_HEADPHONE_LINK_MASK = 0x100; +constexpr int WM8731_RIGHT_HEADPHONE_LINK_SHIFT = 8; // Register 4 constexpr int WM8731_ADC_BYPASS_ADDR = 4; constexpr int WM8731_ADC_BYPASS_MASK = 0x8; @@ -157,11 +177,11 @@ void BAAudioControlWM8731::enable(void) setRightInMute(false); setDacMute(false); // unmute the DAC - // mute the headphone outputs - write(WM8731_REG_LHEADOUT, 0x00); // volume off - regArray[WM8731_REG_LHEADOUT] = 0x00; - write(WM8731_REG_RHEADOUT, 0x00); - regArray[WM8731_REG_RHEADOUT] = 0x00; + // link, but mute the headphone outputs + regArray[WM8731_REG_LHEADOUT] = WM8731_LEFT_HEADPHONE_LINK_MASK; + write(WM8731_REG_LHEADOUT, regArray[WM8731_REG_LHEADOUT]); // volume off + regArray[WM8731_REG_RHEADOUT] = WM8731_RIGHT_HEADPHONE_LINK_MASK; + write(WM8731_REG_RHEADOUT, regArray[WM8731_REG_RHEADOUT]); /// Configure the audio interface write(WM8731_REG_INTERFACE, 0x02); // I2S, 16 bit, MCLK slave @@ -251,6 +271,25 @@ void BAAudioControlWM8731::setLeftRightSwap(bool val) write(WM8731_LRSWAP_ADDR, regArray[WM8731_LRSWAP_ADDR]); } +void BAAudioControlWM8731::setHeadphoneVolume(float volume) +{ + // the codec volume goes from 0x30 to 0x7F. Anything below 0x30 is mute. + // 0dB gain is 0x79. Total range is 0x50 (80) possible values. + unsigned vol; + constexpr unsigned RANGE = 80.0f; + if (volume < 0.0f) { + vol = 0; + } else if (volume > 1.0f) { + vol = 0x7f; + } else { + vol = 0x2f + static_cast(volume * RANGE); + } + regArray[WM8731_LEFT_HEADPHONE_VOL_ADDR] &= ~WM8731_LEFT_HEADPHONE_VOL_MASK; // clear the volume first + regArray[WM8731_LEFT_HEADPHONE_VOL_ADDR] |= + ((vol << WM8731_LEFT_HEADPHONE_VOL_SHIFT) & WM8731_LEFT_HEADPHONE_VOL_MASK); + write(WM8731_LEFT_HEADPHONE_VOL_ADDR, regArray[WM8731_LEFT_HEADPHONE_VOL_ADDR]); +} + // Dac output mute control void BAAudioControlWM8731::setDacMute(bool val) { diff --git a/src/peripherals/BAPhysicalControls.cpp b/src/peripherals/BAPhysicalControls.cpp index cf91185..a973c1c 100644 --- a/src/peripherals/BAPhysicalControls.cpp +++ b/src/peripherals/BAPhysicalControls.cpp @@ -78,21 +78,21 @@ unsigned BAPhysicalControls::addOutput(uint8_t pin) { return m_outputs.size()-1; } -void BAPhysicalControls::setOutput(unsigned index, int val) { - if (index >= m_outputs.size()) { return; } - m_outputs[index].set(val); +void BAPhysicalControls::setOutput(unsigned handle, int val) { + if (handle >= m_outputs.size()) { return; } + m_outputs[handle].set(val); } -void BAPhysicalControls::toggleOutput(unsigned index) { - if (index >= m_outputs.size()) { return; } - m_outputs[index].toggle(); +void BAPhysicalControls::toggleOutput(unsigned handle) { + if (handle >= m_outputs.size()) { return; } + m_outputs[handle].toggle(); } -int BAPhysicalControls::getRotaryAdjustUnit(unsigned index) { - if (index >= m_encoders.size()) { return 0; } // index is greater than number of encoders +int BAPhysicalControls::getRotaryAdjustUnit(unsigned handle) { + if (handle >= m_encoders.size()) { return 0; } // handle is greater than number of encoders - int encoderAdjust = m_encoders[index].getChange(); + int encoderAdjust = m_encoders[handle].getChange(); if (encoderAdjust != 0) { // clip the adjust to maximum abs value of 1. int encoderAdjust = (encoderAdjust > 0) ? 1 : -1; @@ -101,14 +101,14 @@ int BAPhysicalControls::getRotaryAdjustUnit(unsigned index) { return encoderAdjust; } -bool BAPhysicalControls::checkPotValue(unsigned index, float &value) { - if (index >= m_pots.size()) { return false;} // index is greater than number of pots - return m_pots[index].getValue(value); +bool BAPhysicalControls::checkPotValue(unsigned handle, float &value) { + if (handle >= m_pots.size()) { return false;} // handle is greater than number of pots + return m_pots[handle].getValue(value); } -bool BAPhysicalControls::isSwitchToggled(unsigned index) { - if (index >= m_switches.size()) { return 0; } // index is greater than number of switches - Bounce &sw = m_switches[index]; +bool BAPhysicalControls::isSwitchToggled(unsigned handle) { + if (handle >= m_switches.size()) { return 0; } // handle is greater than number of switches + Bounce &sw = m_switches[handle]; if (sw.update() && sw.fallingEdge()) { return true; @@ -117,6 +117,26 @@ bool BAPhysicalControls::isSwitchToggled(unsigned index) { } } +bool BAPhysicalControls::isSwitchHeld(unsigned handle) +{ + if (handle >= m_switches.size()) { return 0; } // handle is greater than number of switches + Bounce &sw = m_switches[handle]; + + if (sw.read()) { + return true; + } else { + return false; + } +} + +int BAPhysicalControls::getSwitchValue(unsigned handle) +{ + if (handle >= m_switches.size()) { return 0; } // handle is greater than number of switches + Bounce &sw = m_switches[handle]; + + return sw.read(); +} + /////////////////////////// void DigitalOutput::set(int val) { @@ -129,25 +149,31 @@ void DigitalOutput::toggle(void) { digitalWriteFast(m_pin, m_val); } +void Potentiometer::setFeedbackFitlerValue(float fitlerValue) +{ + m_feedbackFitlerValue = fitlerValue; +} bool Potentiometer::getValue(float &value) { + bool newValue = true; + unsigned val = analogRead(m_pin); // read the raw value - // Return false if the value is the same as the last sample, or 2 samples ago. The second check is to - // prevent oscillating between two values. - if ((val == m_lastValue) || (val == m_lastValue2)) { - return false; - } + // Use an IIR filter to smooth out the noise in the pot readings + unsigned valFilter = ( (1.0f - m_feedbackFitlerValue)*val + m_feedbackFitlerValue*m_lastValue); - // Otherwise update the last value - m_lastValue = val; // constrain it within the calibration values, them map it to the desired range. - val = constrain(val, m_minCalibration, m_maxCalibration); - value = static_cast(val - m_minCalibration) / static_cast(m_maxCalibration); + valFilter = constrain(valFilter, m_minCalibration, m_maxCalibration); + if (valFilter == m_lastValue) { + newValue = false; + } + m_lastValue = valFilter; + + value = static_cast(valFilter - m_minCalibration) / static_cast(m_maxCalibration); if (m_swapDirection) { value = 1.0f - value; } - return true; + return newValue; } int Potentiometer::getRawValue() { diff --git a/src/peripherals/DmaSpi.cpp b/src/peripherals/DmaSpi.cpp new file mode 100644 index 0000000..2653f6f --- /dev/null +++ b/src/peripherals/DmaSpi.cpp @@ -0,0 +1,13 @@ +#include "DmaSpi.h" + +#if defined(KINETISK) +DmaSpi0 DMASPI0; +#if defined(__MK66FX1M0__) +DmaSpi1 DMASPI1; +//DmaSpi2 DMASPI2; +#endif +#elif defined (KINETISL) +DmaSpi0 DMASPI0; +DmaSpi1 DMASPI1; +#else +#endif // defined