/* RC engine sound & LED controller for Arduino ESP32. Written by TheDIYGuy999
Based on the code for ATmega 328: https://github.com/TheDIYGuy999/Rc_Engine_Sound
* ***** ESP32 CPU frequency must be set to 240MHz! *****
Sound files converted with: https://github.com/TheDIYGuy999/Rc_Engine_Sound_ESP32/blob/master/Audio2Header.html
Original converter code by bitluni (send him a high five, if you like the code)
Parts of automatic transmision code from Wombii's fork: https://github.com/Wombii/Rc_Engine_Sound_ESP32
*/
// Volker Frauenstein 06.01.2021 Migration Version 1.0 all for Sound output not needed functions deleted,
// - interface to DualDriveMixer engine speed as throttle simulator added
// - added functions to switch sounds on and off with multiswitch
// =======================================================================================================
// ! ! I M P O R T A N T ! ! SETTINGS (ADJUST THEM BEFORE CODE UPLOAD), REQUIRED ESP32 BOARD DEFINITION
// =======================================================================================================
//
// DEBUG options can slow down the playback loop! Only uncomment them for debugging, may slow down your system!
// #define ENGIN_DEBUG // uncomment it for input signal debugging informations
// #define DRIVE_DEBUG
// #define SWITCH_DEBUG
// #define AUTO_TRANS_DEBUG
// Definitions for debug
#ifdef ENGIN_DEBUG
static unsigned long lastStateTime;
#endif
#ifdef DRIVE_DEBUG
static unsigned long lastStateTime;
#endif
// =======================================================================================================
// LIRBARIES & HEADER FILES
// =======================================================================================================
//
// Header files
#include "headers/curves.h" // Nonlinear throttle curve arrays
// Libraries (you have to install all of them in the "Arduino sketchbook"/libraries folder)
#include "driver/rmt.h" // No need to install this, comes with ESP32 board definition (used for PWM signal detection)
// =======================================================================================================
// PIN ASSIGNMENTS & GLOBAL VARIABLES (Do not play around here)
// =======================================================================================================
//
// Pin assignment and wiring instructions ****************************************************************
#define DAC1 25 // connect pin25 (do not change the pin) to a 10kOhm resistor
#define DAC2 26 // connect pin26 (do not change the pin) to a 10kOhm resistor
// both outputs of the resistors above are connected together and then to the outer leg of a
// 10kOhm potentiometer. The other outer leg connects to GND. The middle leg connects to both inputs
// of a PAM8403 amplifier and allows to adjust the volume. This way, two speakers can be used.
// Global variables **********************************************************************
// Sound
volatile boolean engineOn = false; // Signal for engine on / off
volatile boolean engineStart = false; // Active, if engine is starting up
volatile boolean engineRunning = false; // Active, if engine is running
volatile boolean engineStop = false; // Active, if engine is shutting down
volatile boolean jakeBrakeRequest = false; // Active, if engine jake braking is requested
volatile boolean engineJakeBraking = false; // Active, if engine is jake braking
volatile boolean wastegateTrigger = false; // Trigger wastegate (blowoff) after rapid throttle drop
volatile boolean dieselKnockTrigger = false; // Trigger Diesel ignition "knock"
volatile boolean dieselKnockTriggerFirst = false; // The first Diesel ignition "knock" per sequence
volatile boolean airBrakeTrigger = false; // Trigger for air brake noise
volatile boolean parkingBrakeTrigger = false; // Trigger for air parking brake noise
volatile boolean shiftingTrigger = false; // Trigger for shifting noise
volatile boolean hornTrigger = false; // Trigger for horn on / off
volatile boolean sirenTrigger = false; // Trigger for siren on / off
volatile boolean sound1trigger = false; // Trigger for sound1 on / off
volatile boolean indicatorSoundOn = false; // active, if indicator bulb is on
// Sound latches
volatile boolean hornLatch = false; // Horn latch bit
volatile boolean sirenLatch = false; // Siren latch bit
// Sound volumes
volatile uint16_t throttleDependentVolume = 0; // engine volume according to throttle position
volatile uint16_t throttleDependentRevVolume = 0; // engine rev volume according to throttle position
volatile uint16_t rpmDependentJakeBrakeVolume = 0; // Engine rpm dependent jake brake volume
volatile uint16_t throttleDependentKnockVolume = 0; // engine Diesel knock volume according to throttle position
volatile uint16_t throttleDependentTurboVolume = 0; // turbo volume according to rpm
volatile uint16_t throttleDependentFanVolume = 0; // cooling fan volume according to rpm
volatile uint16_t throttleDependentChargerVolume = 0; // cooling fan volume according to rpm
volatile uint16_t throttleDependentWastegateVolume = 0; // wastegate volume according to rpm
volatile int16_t masterVolume = 100; // Master volume percentage
volatile uint8_t dacOffset = 0; // 128, but needs to be ramped up slowly to prevent popping noise, if switched on
// Throttle
int16_t currentThrottle = 0; // 0 - 500 (Throttle trigger input)
int16_t currentThrottleFaded = 0; // faded throttle for volume calculations etc.
int16_t ThrottleHistory = 0;
const uint8_t ThrottleZone = 30;
// Drivestate
volatile boolean escIsBraking = false; // Vehicle is in a braking state
volatile boolean escIsDriving = false; // Vehicle is in a driving state
int8_t driveState = 0; // for vehicle state machine
// Engine
const int16_t maxRpm = 500; // always 500
const int16_t minRpm = 0; // always 0
int32_t currentRpm = 0; // 0 - 500 (signed required!)
volatile uint8_t engineState = 0; // 0 = off, 1 = starting, 2 = running, 3 = stopping
int16_t engineLoad = 0; // 0 - 500
volatile uint16_t engineSampleRate = 0; // Engine sample rate
int32_t speedLimit = maxRpm; // The speed limit, depending on selected virtual gear
// Clutch
boolean clutchDisengaged = true; // Active while clutch is disengaged
// Transmission
uint8_t selectedGear = 1; // The currently used gear of our shifting gearbox
uint8_t selectedAutomaticGear = 1; // The currently used gear of our automatic gearbox
boolean gearUpShiftingInProgress; // Active while shifting upwards
boolean gearDownShiftingInProgress; // Active while shifting downwards
boolean gearUpShiftingPulse; // Active, if shifting upwards begins
boolean gearDownShiftingPulse; // Active, if shifting downwards begins
volatile boolean neutralGear = false; // Transmission in neutral
// interface definitions to handle throttle value for engine sound -> definition in main.ino needed
// int16_t power = 0; // value from 0 to 500
// uint8_t drivestate = 0; // Vehicle state for sound - standstill, forward ...
// Sampling intervals for interrupt timer (adjusted according to your sound file sampling rate)
uint32_t maxSampleInterval = 4000000 / sampleRate;
uint32_t minSampleInterval = 4000000 / sampleRate * 100 / MAX_RPM_PERCENTAGE;
// Interrupt timer for variable sample rate playback (engine sound)
hw_timer_t * variableTimer = NULL;
portMUX_TYPE variableTimerMux = portMUX_INITIALIZER_UNLOCKED;
volatile uint32_t variableTimerTicks = maxSampleInterval;
// Interrupt timer for fixed sample rate playback (horn etc., playing in parallel with engine sound)
hw_timer_t * fixedTimer = NULL;
portMUX_TYPE fixedTimerMux = portMUX_INITIALIZER_UNLOCKED;
volatile uint32_t fixedTimerTicks = maxSampleInterval;
//
// =======================================================================================================
// INTERRUPT FOR VARIABLE SPEED PLAYBACK (Engine sound, turbo sound)
// =======================================================================================================
//
void IRAM_ATTR variablePlaybackTimer() {
static uint32_t attenuatorMillis;
static uint32_t curEngineSample; // Index of currently loaded engine sample
static uint32_t curRevSample; // Index of currently loaded engine rev sample
static uint32_t curTurboSample; // Index of currently loaded turbo sample
static uint32_t curFanSample; // Index of currently loaded fan sample
static uint32_t curChargerSample; // Index of currently loaded charger sample
static uint32_t curStartSample; // Index of currently loaded start sample
static uint32_t curJakeBrakeSample; // Index of currently loaded jake brake sample
static uint32_t lastDieselKnockSample; // Index of last Diesel knock sample
static uint16_t attenuator; // Used for volume adjustment during engine switch off
static uint16_t speedPercentage; // slows the engine down during shutdown
static int32_t a, a1, a2, a3, b, c, d, e; // Input signals for mixer: a = engine, b = additional sound, c = turbo sound, d = fan sound, e = supercharger sound
uint8_t a1Multi; // Volume multipliers
//portENTER_CRITICAL_ISR(&variableTimerMux);
switch (engineState) {
case 0: // Engine off -----------------------------------------------------------------------
variableTimerTicks = 4000000 / startSampleRate; // our fixed sampling rate
timerAlarmWrite(variableTimer, variableTimerTicks, true); // // change timer ticks, autoreload true
a = 0; // volume = zero
if (engineOn) {
engineState = 1;
engineStart = true;
}
break;
case 1: // Engine start --------------------------------------------------------------------
variableTimerTicks = 4000000 / startSampleRate; // our fixed sampling rate
timerAlarmWrite(variableTimer, variableTimerTicks, true); // // change timer ticks, autoreload true
if (curStartSample < startSampleCount - 1) {
a = (startSamples[curStartSample] * throttleDependentVolume / 100 * startVolumePercentage / 100);
curStartSample ++;
}
else {
curStartSample = 0;
engineState = 2;
engineStart = false;
engineRunning = true;
airBrakeTrigger = true;
}
break;
case 2: // Engine running ------------------------------------------------------------------
// Engine idle & revving sounds (mixed together according to engine rpm, new in v5.0)
variableTimerTicks = engineSampleRate; // our variable idle sampling rate!
timerAlarmWrite(variableTimer, variableTimerTicks, true); // // change timer ticks, autoreload true
if (!engineJakeBraking) {
if (curEngineSample < sampleCount - 1) {
a1 = (samples[curEngineSample] * throttleDependentVolume / 100 * idleVolumePercentage / 100); // Idle sound
a3 = 0;
curEngineSample ++;
// Optional rev sound, recorded at medium rpm. Note, that it needs to represent the same number of ignition cycles as the
// idle sound. For example 4 or 8 for a V8 engine. It also needs to have about the same length. In order to adjust the length
// or "revSampleCount", change the "Rate" setting in Audacity until it is about the same.
#ifdef REV_SOUND
a2 = (revSamples[curRevSample] * throttleDependentRevVolume / 100 * revVolumePercentage / 100); // Rev sound
if (curRevSample < revSampleCount) curRevSample ++;
#endif
// Trigger throttle dependent Diesel ignition "knock" sound (played in the fixed sample rate interrupt)
if (curEngineSample - lastDieselKnockSample > (sampleCount / dieselKnockInterval)) {
dieselKnockTrigger = true;
dieselKnockTriggerFirst = false;
lastDieselKnockSample = curEngineSample;
}
}
else {
curEngineSample = 0;
if (jakeBrakeRequest) engineJakeBraking = true;
#ifdef REV_SOUND
curRevSample = 0;
#endif
lastDieselKnockSample = 0;
dieselKnockTrigger = true;
dieselKnockTriggerFirst = true;
}
curJakeBrakeSample = 0;
}
else { // Jake brake sound ----
#ifdef JAKE_BRAKE_SOUND
a3 = (jakeBrakeSamples[curJakeBrakeSample] * rpmDependentJakeBrakeVolume / 100 * jakeBrakeVolumePercentage / 100); // Jake brake sound
a2 = 0;
a1 = 0;
if (curJakeBrakeSample < jakeBrakeSampleCount - 1) curJakeBrakeSample ++;
else {
curJakeBrakeSample = 0;
if (!jakeBrakeRequest) engineJakeBraking = false;
}
curEngineSample = 0;
curRevSample = 0;
#endif
}
// Engine sound mixer ----
#ifdef REV_SOUND
// Mixing the idle and rev sounds together, according to engine rpm
// Below the "revSwitchPoint" target, the idle volume precentage is 90%, then falling to 0% @ max. rpm.
// The total of idle and rev volume percentage is always 100%
if (currentRpm > revSwitchPoint) a1Multi = map(currentRpm, idleEndPoint, revSwitchPoint, 0, idleVolumeProportionPercentage);
else a1Multi = idleVolumeProportionPercentage; // 90 - 100% proportion
if (currentRpm > idleEndPoint) a1Multi = 0;
a1 = a1 * a1Multi / 100; // Idle volume
a2 = a2 * (100 - a1Multi) / 100; // Rev volume
a = a1 + a2 + a3; // Idle and rev sounds mixed together
#else
a = a1 + a3; // Idle sound only
#endif
// Turbo sound ----------------------------------
if (curTurboSample < turboSampleCount - 1) {
c = (turboSamples[curTurboSample] * throttleDependentTurboVolume / 100 * turboVolumePercentage / 100);
curTurboSample ++;
}
else {
curTurboSample = 0;
}
// Fan sound -----------------------------------
if (curFanSample < fanSampleCount - 1) {
d = (fanSamples[curFanSample] * throttleDependentFanVolume / 100 * fanVolumePercentage / 100);
curFanSample ++;
}
else {
curFanSample = 0;
}
#if defined GEARBOX_WHINING
if (neutralGear) d = 0; // used for gearbox whining simulation, so not active in gearbox neutral
#endif
// Supercharger sound --------------------------
if (curChargerSample < chargerSampleCount - 1) {
e = (chargerSamples[curChargerSample] * throttleDependentChargerVolume / 100 * chargerVolumePercentage / 100);
curChargerSample ++;
}
else {
curChargerSample = 0;
}
if (!engineOn) {
speedPercentage = 100;
attenuator = 1;
engineState = 3;
engineStop = true;
engineRunning = false;
}
break;
case 3: // Engine stop --------------------------------------------------------------------
variableTimerTicks = 4000000 / sampleRate * speedPercentage / 100; // our fixed sampling rate
timerAlarmWrite(variableTimer, variableTimerTicks, true); // // change timer ticks, autoreload true
if (curEngineSample < sampleCount - 1) {
a = (samples[curEngineSample] * throttleDependentVolume / 100 * idleVolumePercentage / 100 / attenuator);
curEngineSample ++;
}
else {
curEngineSample = 0;
}
// fade engine sound out
if (millis() - attenuatorMillis > 100) { // Every 50ms
attenuatorMillis = millis();
attenuator ++; // attenuate volume
speedPercentage += 20; // make it slower (10)
}
if (attenuator >= 50 || speedPercentage >= 500) { // 50 & 500
a = 0;
speedPercentage = 100;
parkingBrakeTrigger = true;
engineState = 4;
engineStop = false;
}
break;
case 4: // parking brake bleeding air sound after engine is off ----------------------------
if (!parkingBrakeTrigger) {
engineState = 0;
}
break;
} // end of switch case
// DAC output (groups a, b, c mixed together) ************************************************************************
dacWrite(DAC1, constrain(((a * 8 / 10) + (b / 2) + (c / 5) + (d / 5) + (e / 5)) * masterVolume / 100 + dacOffset, 0, 255)); // Mix signals, add 128 offset, write result to DAC
//portEXIT_CRITICAL_ISR(&variableTimerMux);
}
//
// =======================================================================================================
// INTERRUPT FOR FIXED SPEED PLAYBACK (Horn etc., played in parallel with engine sound)
// =======================================================================================================
//
void IRAM_ATTR fixedPlaybackTimer() {
static uint32_t curHornSample; // Index of currently loaded horn sample
static uint32_t curSirenSample; // Index of currently loaded siren sample
static uint32_t curSound1Sample; // Index of currently loaded sound1 sample
static uint32_t curReversingSample; // Index of currently loaded reversing beep sample
static uint32_t curIndicatorSample; // Index of currently loaded indicator tick sample
static uint32_t curWastegateSample; // Index of currently loaded wastegate sample
static uint32_t curBrakeSample; // Index of currently loaded brake sound sample
static uint32_t curParkingBrakeSample; // Index of currently loaded brake sound sample
static uint32_t curShiftingSample; // Index of currently loaded shifting sample
static uint32_t curDieselKnockSample; // Index of currently loaded Diesel knock sample
static int32_t a, a1, a2; // Input signals "a" for mixer
static int32_t b, b0, b1, b2, b3, b4, b5, b6, b7, b8; // Input signals "b" for mixer
static boolean knockSilent; // This knock will be more silent
static uint8_t curKnockCylinder; // Index of currently ignited zylinder
//portENTER_CRITICAL_ISR(&fixedTimerMux);
// Group "a" (horn & siren) ******************************************************************
if (hornTrigger || hornLatch) {
fixedTimerTicks = 4000000 / hornSampleRate; // our fixed sampling rate
timerAlarmWrite(fixedTimer, fixedTimerTicks, true); // // change timer ticks, autoreload true
if (curHornSample < hornSampleCount - 1) {
a1 = (hornSamples[curHornSample] * hornVolumePercentage / 100);
curHornSample ++;
#ifdef HORN_LOOP // Optional "endless loop" (points to be defined manually in horn file)
if (hornTrigger && curHornSample == hornLoopEnd) curHornSample = hornLoopBegin; // Loop, if trigger still present
#endif
}
else { // End of sample
curHornSample = 0;
a1 = 0;
hornLatch = false;
}
}
if (sirenTrigger || sirenLatch) {
fixedTimerTicks = 4000000 / sirenSampleRate; // our fixed sampling rate
timerAlarmWrite(fixedTimer, fixedTimerTicks, true); // // change timer ticks, autoreload true
if (curSirenSample < sirenSampleCount - 1) {
a2 = (sirenSamples[curSirenSample] * sirenVolumePercentage / 100);
curSirenSample ++;
#ifdef SIREN_LOOP // Optional "endless loop" (points to be defined manually in siren file)
if (sirenTrigger && curSirenSample == sirenLoopEnd) curSirenSample = sirenLoopBegin; // Loop, if trigger still present
#endif
}
else { // End of sample
curSirenSample = 0;
a2 = 0;
sirenLatch = false;
}
}
// Group "b" (other sounds) **********************************************************************
// Sound 1 "b0" ----
if (sound1trigger) {
fixedTimerTicks = 4000000 / sound1SampleRate; // our fixed sampling rate
timerAlarmWrite(fixedTimer, fixedTimerTicks, true); // // change timer ticks, autoreload true
if (curSound1Sample < sound1SampleCount - 1) {
b0 = (sound1Samples[curSound1Sample] * sound1VolumePercentage / 100);
curSound1Sample ++;
}
else {
sound1trigger = false;
}
}
else {
curSound1Sample = 0; // ensure, next sound will start @ first sample
b0 = 0;
}
// Wastegate (blowoff) sound, triggered after rapid throttle drop -----------------------------------
if (wastegateTrigger) {
if (curWastegateSample < wastegateSampleCount - 1) {
b3 = (wastegateSamples[curWastegateSample] * throttleDependentWastegateVolume / 100 * wastegateVolumePercentage / 100);
curWastegateSample ++;
}
else {
wastegateTrigger = false;
}
}
else {
b3 = 0;
curWastegateSample = 0; // ensure, next sound will start @ first sample
}
// Air brake release sound, triggered after stop -----------------------------------------------
if (airBrakeTrigger) {
if (curBrakeSample < brakeSampleCount - 1) {
b4 = (brakeSamples[curBrakeSample] * brakeVolumePercentage / 100);
curBrakeSample ++;
}
else {
airBrakeTrigger = false;
}
}
else {
b4 = 0;
curBrakeSample = 0; // ensure, next sound will start @ first sample
}
// Air parking brake attaching sound, triggered after engine off --------------------------------
if (parkingBrakeTrigger) {
if (curParkingBrakeSample < parkingBrakeSampleCount - 1) {
b5 = (parkingBrakeSamples[curParkingBrakeSample] * parkingBrakeVolumePercentage / 100);
curParkingBrakeSample ++;
}
else {
parkingBrakeTrigger = false;
}
}
else {
b5 = 0;
curParkingBrakeSample = 0; // ensure, next sound will start @ first sample
}
// Pneumatic gear shifting sound, triggered while shifting the TAMIYA 3 speed transmission ------
if (shiftingTrigger && engineRunning && !automatic && !doubleClutch) {
if (curShiftingSample < shiftingSampleCount - 1) {
b6 = (shiftingSamples[curShiftingSample] * shiftingVolumePercentage / 100);
curShiftingSample ++;
}
else {
shiftingTrigger = false;
}
}
else {
b6 = 0;
curShiftingSample = 0; // ensure, next sound will start @ first sample
}
// Diesel ignition "knock" played in fixed sample rate section, because we don't want changing pitch! ------
if (dieselKnockTriggerFirst) {
dieselKnockTriggerFirst = false;
curKnockCylinder = 0;
}
if (dieselKnockTrigger) {
dieselKnockTrigger = false;
curKnockCylinder ++; // Count ignition sequence
curDieselKnockSample = 0;
}
#ifdef V8 // (former ADAPTIVE_KNOCK_VOLUME, rename it in your config file!)
// Ford or Scania V8 ignition sequence: 1 - 5 - 4 - 2* - 6 - 3 - 7 - 8* (* = louder knock pulses, because 2nd exhaust in same manifold after 90°)
if (curKnockCylinder == 4 || curKnockCylinder == 8) knockSilent = false;
else knockSilent = true;
#endif
#ifdef V2
// V2 engine: 1st and 2nd knock pulses (of 4) will be louder
if (curKnockCylinder == 1 || curKnockCylinder == 2) knockSilent = false;
else knockSilent = true;
#endif
#ifdef R6
// R6 inline 6 engine: 6th knock pulse (of 6) will be louder
if (curKnockCylinder == 6) knockSilent = false;
else knockSilent = true;
#endif
if (curDieselKnockSample < knockSampleCount) {
b7 = (knockSamples[curDieselKnockSample] * dieselKnockVolumePercentage / 100 * throttleDependentKnockVolume / 100);
curDieselKnockSample ++;
if (knockSilent) b7 = b7 * dieselKnockAdaptiveVolumePercentage / 100; // changing knock volume according to engine type and cylinder!
}
// Mixing sounds together ----
a = a1 + a2; // Horn & siren
b = b0 * 5 + b1 + b2 / 2 + b3 + b4 + b5 + b6 + b7; // Other sounds
// DAC output (groups a + b mixed together) ****************************************************************************
dacWrite(DAC2, constrain(((a * 8 / 10) + (b * 2 / 10)) * masterVolume / 100 + dacOffset, 0, 255)); // Mix signals, add 128 offset, write result to DAC
//portEXIT_CRITICAL_ISR(&fixedTimerMux);
}
//
// =======================================================================================================
// MAIN ARDUINO SETUP (1x during startup)
// =======================================================================================================
//
void sound_setup() {
// Refresh sample intervals (important, because MAX_RPM_PERCENTAGE was probably changed above)
maxSampleInterval = 4000000 / sampleRate;
minSampleInterval = 4000000 / sampleRate * 100 / MAX_RPM_PERCENTAGE;
// Task 1 setup (running on core 0)
TaskHandle_t Task1;
//create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
xTaskCreatePinnedToCore(
Task1code, /* Task function. */
"Task1", /* name of task. */
100000, /* Stack size of task (10000) */
NULL, /* parameter of the task */
1, /* priority of the task (1 = low, 3 = medium, 5 = highest)*/
&Task1, /* Task handle to keep track of created task */
0); /* pin task to core 0 */
// Interrupt timer for variable sample rate playback
variableTimer = timerBegin(0, 20, true); // timer 0, MWDT clock period = 12.5 ns * TIMGn_Tx_WDT_CLK_PRESCALE -> 12.5 ns * 20 -> 250 ns = 0.25 us, countUp
timerAttachInterrupt(variableTimer, &variablePlaybackTimer, true); // edge (not level) triggered
timerAlarmWrite(variableTimer, variableTimerTicks, true); // autoreload true
timerAlarmEnable(variableTimer); // enable
// Interrupt timer for fixed sample rate playback
fixedTimer = timerBegin(1, 20, true); // timer 1, MWDT clock period = 12.5 ns * TIMGn_Tx_WDT_CLK_PRESCALE -> 12.5 ns * 20 -> 250 ns = 0.25 us, countUp
timerAttachInterrupt(fixedTimer, &fixedPlaybackTimer, true); // edge (not level) triggered
timerAlarmWrite(fixedTimer, fixedTimerTicks, true); // autoreload true
timerAlarmEnable(fixedTimer); // enable
}
//
// =======================================================================================================
// DAC OFFSET FADER
// =======================================================================================================
//
static unsigned long dacOffsetMicros;
boolean dacInit;
void dacOffsetFade() {
if (!dacInit) {
if (micros() - dacOffsetMicros > 100) { // Every 0.1ms
dacOffsetMicros = micros();
dacOffset ++; // fade DAC offset slowly to prevent it from popping, if ESP32 powered up after amplifier
if (dacOffset == 128) dacInit = true;
}
}
}
void mapThrottle(int16_t gas) {
// Input is global value out of DualdriveMixer with output 0-500 for forward and backwards (VF 06.01.2021)
currentThrottle = gas;
// Auto throttle --------------------------------------------------------------------------
// Auto throttle while gear shifting (synchronizing the Tamiya 3 speed gearbox)
if (!escIsBraking && escIsDriving && shiftingAutoThrottle) {
if (gearUpShiftingInProgress) currentThrottle = 0; // No throttle
if (gearDownShiftingInProgress) currentThrottle = 500; // Full throttle
}
// Volume calculations --------------------------------------------------------------------------
// As a base for some calculations below, fade the current throttle to make it more natural
static unsigned long throttleFaderMicros;
if (micros() - throttleFaderMicros > 500) { // Every 0.5ms
throttleFaderMicros = micros();
if (currentThrottleFaded < currentThrottle && currentThrottleFaded < 499) currentThrottleFaded += 2;
if (currentThrottleFaded > currentThrottle && currentThrottleFaded > 2) currentThrottleFaded -= 2;
//Serial.println(currentThrottleFaded);
}
// Calculate throttle dependent engine idle volume
if (!escIsBraking && engineRunning) throttleDependentVolume = map(currentThrottleFaded, 0, 500, engineIdleVolumePercentage, fullThrottleVolumePercentage);
else throttleDependentVolume = engineIdleVolumePercentage;
// Calculate throttle dependent engine rev volume
if (!escIsBraking && engineRunning) throttleDependentRevVolume = map(currentThrottleFaded, 0, 500, engineRevVolumePercentage, fullThrottleVolumePercentage);
else throttleDependentRevVolume = engineRevVolumePercentage;
// Calculate engine rpm dependent jake brake volume
if (engineRunning) rpmDependentJakeBrakeVolume = map(currentRpm, 0, 500, jakeBrakeIdleVolumePercentage, 100);
else rpmDependentJakeBrakeVolume = jakeBrakeIdleVolumePercentage;
// Calculate throttle dependent Diesel knock volume
if (!escIsBraking && engineRunning && (currentThrottleFaded > dieselKnockStartPoint)) throttleDependentKnockVolume = map(currentThrottleFaded, dieselKnockStartPoint, 500, dieselKnockIdleVolumePercentage, 100);
else throttleDependentKnockVolume = dieselKnockIdleVolumePercentage;
// Calculate engine rpm dependent turbo volume
if (engineRunning) throttleDependentTurboVolume = map(currentRpm, 0, 500, turboIdleVolumePercentage, 100);
else throttleDependentTurboVolume = turboIdleVolumePercentage;
// Calculate engine rpm dependent cooling fan volume
if (engineRunning && (currentRpm > fanStartPoint)) throttleDependentFanVolume = map(currentRpm, fanStartPoint, 500, fanIdleVolumePercentage, 100);
else throttleDependentFanVolume = fanIdleVolumePercentage;
// Calculate throttle dependent supercharger volume
if (!escIsBraking && engineRunning && (currentRpm > chargerStartPoint)) throttleDependentChargerVolume = map(currentThrottleFaded, chargerStartPoint, 500, chargerIdleVolumePercentage, 100);
else throttleDependentChargerVolume = chargerIdleVolumePercentage;
// Calculate engine rpm dependent wastegate volume
if (engineRunning) throttleDependentWastegateVolume = map(currentRpm, 0, 500, wastegateIdleVolumePercentage, 100);
else throttleDependentWastegateVolume = wastegateIdleVolumePercentage;
// Calculate engine load (used for torque converter slip simulation)
engineLoad = currentThrottle - currentRpm;
if (engineLoad < 0 || escIsBraking) engineLoad = 0; // Range is 0 - 500
if (engineLoad > 180) engineLoad = 180;
}
//
// =======================================================================================================
// ENGINE MASS SIMULATION
// =======================================================================================================
//
void engineMassSimulation() {
static int32_t targetRpm = 0; // The engine RPM target
static int32_t lastThrottle;
uint16_t converterSlip;
static unsigned long throtMillis;
static unsigned long printMillis;
static unsigned long wastegateMillis;
uint8_t timeBase;
#ifdef SUPER_SLOW
timeBase = 6; // super slow running, heavy engines, for example locomotive diesels
#else
timeBase = 2;
#endif
if (millis() - throtMillis > timeBase) { // Every 2 or 6ms
throtMillis = millis();
// Virtual clutch **********************************************************************************
if ((currentThrottle < clutchEngagingPoint && currentRpm < maxClutchSlippingRpm) || gearUpShiftingInProgress || gearDownShiftingInProgress || neutralGear || currentRpm < 200) {
clutchDisengaged = true;
}
else {
clutchDisengaged = false;
}
// Transmissions ***********************************************************************************
// automatic transmission ----
if (automatic) {
// Torque converter slip calculation
if (selectedAutomaticGear < 2) converterSlip = engineLoad * 2; // more slip in first and reverse gear
else converterSlip = engineLoad;
if (!neutralGear) targetRpm = currentThrottle * gearRatio[selectedAutomaticGear] / 10 + converterSlip; // Compute engine RPM
else targetRpm = reMap(curveLinear, currentThrottle);
}
else if (doubleClutch) {
// double clutch transmission
if (!neutralGear) targetRpm = currentThrottle * gearRatio[selectedAutomaticGear] / 10; // Compute engine RPM
else targetRpm = reMap(curveLinear, currentThrottle);
}
else {
// Manual transmission ----
if (clutchDisengaged) { // Clutch disengaged: Engine revving allowed
#if defined VIRTUAL_16_SPEED_SEQUENTIAL
targetRpm = currentThrottle;
#else
targetRpm = reMap(curveLinear, currentThrottle);
#endif
}
else { // Clutch engaged: Engine rpm synchronized with ESC power (speed)
#if defined VIRTUAL_3_SPEED || defined VIRTUAL_16_SPEED_SEQUENTIAL // Virtual 3 speed or sequential 16 speed transmission
targetRpm = currentThrottle * virtualManualGearRatio[selectedGear] / 10; // Add virtual gear ratios
if (targetRpm > 500) targetRpm = 500;
#else // Real 3 speed transmission
targetRpm = reMap(curveLinear, currentThrottle);
#endif
}
}
// Engine RPM **************************************************************************************
if (escIsBraking && currentThrottle < clutchEngagingPoint) targetRpm = 0; // keep engine @idle rpm, if braking at very low speed
if (targetRpm > 500) targetRpm = 500;
// Accelerate engine
if (targetRpm > (currentRpm + acc) && (currentRpm + acc) < maxRpm && engineState == 2 && engineRunning) {
if (!airBrakeTrigger) { // No acceleration, if brake release noise still playing
if (!gearDownShiftingInProgress) currentRpm += acc;
else currentRpm += acc / 2; // less aggressive rpm rise while downshifting
if (currentRpm > maxRpm) currentRpm = maxRpm;
}
}
// Decelerate engine
if (targetRpm < currentRpm) {
currentRpm -= dec;
if (currentRpm < minRpm) currentRpm = minRpm;
}
#if defined VIRTUAL_3_SPEED || defined VIRTUAL_16_SPEED_SEQUENTIAL
// Limit top speed, depending on manual gear ratio. Ensures, that the engine will not blow up!
if (!automatic && !doubleClutch) speedLimit = maxRpm * 10 / virtualManualGearRatio[selectedGear];
#endif
// Speed (sample rate) output
engineSampleRate = map(currentRpm, minRpm, maxRpm, maxSampleInterval, minSampleInterval); // Idle
}
// Prevent Wastegate from being triggered while downshifting
if(gearDownShiftingInProgress) wastegateMillis = millis();
// Trigger Wastegate, if throttle rapidly dropped
if (lastThrottle - currentThrottle > 70 && millis() - wastegateMillis > 1000) {
wastegateMillis = millis();
wastegateTrigger = true;
}
lastThrottle = currentThrottle;
}
//
// =======================================================================================================
// SWITCH ENGINE ON OR OFF WITH MULTISWITCH A
// =======================================================================================================
//
void engineOnOff(bool swap) {
if (swap) { // Engine switched on or off depending on multiswitch (a) position and 1s delay time
engineOn = true; // start the engine
//Serial.print(" - trigger switch on ");
}
else {
engineOn = false; // switch engine off
// Serial.print(" - trigger switch off ");
}
#ifdef ENGIN_DEBUG
if (millis() - lastStateTime > 300) { // Print the data every 300ms
lastStateTime = millis();
Serial.print("engineState: ");Serial.print(engineState);
Serial.print(", engineOn: ");Serial.print(engineOn);
Serial.print(", engineRunning: ");Serial.print(engineRunning);
Serial.print(", engineStop: ");Serial.print(engineStop);
}
#endif
}
//
// =======================================================================================================
// LOOP TIME MEASUREMENT
// =======================================================================================================
//
unsigned long loopDuration() {
static unsigned long timerOld;
unsigned long loopTime;
unsigned long timer = millis();
loopTime = timer - timerOld;
timerOld = timer;
return loopTime;
}
//
// =======================================================================================================
// SIMULATED States for standing, braking and running
// =======================================================================================================
//
void DriveState() {
static int8_t driveRampRate;
static int8_t driveRampGain;
static int8_t brakeRampRate;
uint8_t escRampTime;
// Gear dependent ramp speed for acceleration & deceleration
#if defined VIRTUAL_3_SPEED
escRampTime = escRampTimeThirdGear * 10 / virtualManualGearRatio[selectedGear];
#elif defined VIRTUAL_16_SPEED_SEQUENTIAL
//escRampTime = escRampTimeThirdGear;// * 10 / map(virtualManualGearRatio[selectedGear], 155, 10, 23, 10);
escRampTime = escRampTimeThirdGear * virtualManualGearRatio[selectedGear] / 5;
#else // TAMIYA 3 speed shifting transmission
if (selectedGear == 1) escRampTime = escRampTimeFirstGear; // about 20
if (selectedGear == 2) escRampTime = escRampTimeSecondGear; // about 50
if (selectedGear == 3) escRampTime = escRampTimeThirdGear; // about 75
#endif
// calulate throttle dependent brake & acceleration steps
brakeRampRate = map (currentThrottle, 0, 500, 1, escBrakeSteps);
driveRampRate = map (currentThrottle, 0, 500, 1, escAccelerationSteps);
#ifdef DRIVE_DEBUG
if (millis() - lastStateTime > 300) { // Print the data every 300ms
lastStateTime = millis();
Serial.print("driveState: ");Serial.println(driveState);
Serial.print("brakeRampRate: ");Serial.println(brakeRampRate);
Serial.print("currentRpm: ");Serial.println(currentRpm);
// Serial.print("targetRpm: ");Serial.println(targetRpm);
Serial.print("currentThrottle: ");Serial.println(currentThrottle);
Serial.print("airBrakeTrigger: ");Serial.println(airBrakeTrigger);
}
#endif
// Drive state state machine **********************************************************************************
switch (driveState) {
case 0: // Standing still ---------------------------------------------------------------------
escIsBraking = false;
escIsDriving = false;
if (currentThrottle > 0) driveState = 1; // Driving forward
break;
case 1: // Driving forward or Reverse ---------------------------------------------------------
escIsBraking = false;
escIsDriving = true;
if (gearUpShiftingPulse && shiftingAutoThrottle) { // lowering RPM, if shifting up transmission
gearUpShiftingPulse = false;
}
if (gearDownShiftingPulse && shiftingAutoThrottle) { // increasing RPM, if shifting down transmission
gearDownShiftingPulse = false;
}
if (ThrottleHistory < (currentThrottle - ThrottleZone)) driveState = 2; // Braking forward or reverse
if (currentThrottle == 0) driveState = 0; // standing still
break;
case 2: // Braking forward ---------------------------------------------------------------------
escIsBraking = true;
escIsDriving = false;
if (currentThrottle > ThrottleHistory - ThrottleZone) {
driveState = 1; // Driving forward
airBrakeTrigger = true;
}
if (currentThrottle == 0) {
driveState = 0; // standing still
airBrakeTrigger = true;
}
break;
} // End of state machine **********************************************************************************
// Gain for drive ramp rate, depending on clutchEngagingPoint
if (currentThrottle < clutchEngagingPoint) {
if (!automatic && !doubleClutch) driveRampGain = 2; // prevent clutch from slipping too much (2)
else driveRampGain = 4; // Automatic transmission needs to catch immediately (4)
}
else driveRampGain = 1;
ThrottleHistory = currentThrottle; // prepair for next state calculation
}
//
// =======================================================================================================
// SIMULATED AUTOMATIC TRANSMISSION GEAR SELECTOR
// =======================================================================================================
//
void automaticGearSelector() {
static unsigned long gearSelectorMillis;
static unsigned long lastUpShiftingMillis;
static unsigned long lastDownShiftingMillis;
uint16_t downShiftPoint = 200;
uint16_t upShiftPoint = 490;
if (millis() - gearSelectorMillis > 100) { // Waiting for 100ms is very important. Otherwise gears are skipped!
gearSelectorMillis = millis();
// compute load dependent shift points (less throttle = less rpm before shifting up, kick down will shift back!)
upShiftPoint = map(engineLoad, 0, 180, 390, 490); // 390, 490
downShiftPoint = map(engineLoad, 0, 180, 150, 250); // 150, 250
// Adaptive shift points
if (millis() - lastDownShiftingMillis > 500 && currentRpm >= upShiftPoint && engineLoad < 5) { // 500ms locking timer!
selectedAutomaticGear ++; // Upshifting (load maximum is important to prevent gears from oscillating!)
lastUpShiftingMillis = millis();
}
if (millis() - lastUpShiftingMillis > 1000 && selectedAutomaticGear > 1 && (currentRpm <= downShiftPoint || engineLoad > 100)) { // 1000ms locking timer!
selectedAutomaticGear --; // Downshifting incl. kickdown
lastDownShiftingMillis = millis();
}
selectedAutomaticGear = constrain(selectedAutomaticGear, 1, NumberOfAutomaticGears);
#ifdef AUTO_TRANS_DEBUG
Serial.print("currentThrottle: ");Serial.println(currentThrottle);
Serial.print("selectedAutomaticGear: ");Serial.println(selectedAutomaticGear);
Serial.print("engineLoad: ");Serial.println(engineLoad);
Serial.print("upShiftPoint: ");Serial.println(upShiftPoint);
Serial.print("currentRpm: ");Serial.println(currentRpm);
Serial.print("downShiftPoint: ");Serial.println(downShiftPoint);
Serial.print("");
#endif
}
}
// =======================================================================================================
// ADDED: Volker Frauenstein 06.02.2021 functions to trigger sounds with multiswitch
// =======================================================================================================
// =======================================================================================================
// HORN TRIGGERING (NOT USED BY MULTISWITCH)
// =======================================================================================================
void triggerHorn(bool swap) {
// detect horn trigger
if (swap) {
hornTrigger = true;
hornLatch = true;
#ifdef SWITCH_DEBUG
Serial.print("Horn Trigger");
#endif
}
else {
hornTrigger = false;
#ifdef SWITCH_DEBUG
Serial.print("Horn false"));
#endif
}
}
// =======================================================================================================
// SIREN TRIGGERING BY MULTISWITCH B
// =======================================================================================================
void triggerSiren(bool swap) {
// detect siren trigger
if (swap) {
sirenTrigger = true;
sirenLatch = true;
#ifdef SWITCH_DEBUG
Serial.print("Siren Trigger");
#endif
}
else {
sirenTrigger = false;
#ifdef SWITCH_DEBUG
Serial.print("Siren false"));
#endif
}
}
// =======================================================================================================
// Gunfire TRIGGERING BY MULTISWITCH C
// =======================================================================================================
void triggerSound1(bool swap) {
// detect MG fire trigger
if (swap) {
sound1trigger = true;
#ifdef SWITCH_DEBUG
Serial.print("MG Trigger");
#endif
}
else {
sound1trigger = false;
#ifdef SWITCH_DEBUG
Serial.print("Siren false"));
#endif
}
} // End added functions