// DCCMFL1616 - DIY DCC Multifunction Loco decoder // licensed under GPL v3 // // based on NMRA Dcc Multifunction Motor Decoder Demo by Alex Shepherd // // This firmware requires these Arduino Libraries: // // 1) The NmraDcc Library from: http://mrrwa.org/download/ // // These libraries can be found and installed via the Arduino IDE Library Manager // // Functionality implemented: // - motor control (speed & direction) with start and maximum PWM values defined in CV2 & CV5 // - integrated acceleration / deceleration profile controlled by CV3 & CV4 // - exterieur lighting (F0 white head lights, F1 red rear lights, in combination with F5 white head and rear lights) in regards to driving direction // - defined default Decoder address: 3 according to NMRA & NEM standard // - factory reset by setting CV8 to 255 // - debugging on UART (TX) can be actived (may slow down reaction of the decoder to updated controls) // // Functionality NOT implemented yet: // analog DC driving mode --> DCC-timeout? // #include // Uncomment any of the lines below to enable debug messages for different parts of the code //#define DEBUG_FUNCTIONS //#define DEBUG_SPEED //#define DEBUG_PWM //#define DEBUG_DCC_ACK //#define DEBUG_DCC_MSG //#define DEBUG_DCC_RESET #if defined(DEBUG_FUNCTIONS) or defined(DEBUG_SPEED) or defined(DEBUG_PWM) or defined(DEBUG_DCC_ACK) or defined(DEBUG_DCC_MSG) or defined(DEBUG_DCC_RESET) #define DEBUG_PRINT #endif // This is the default DCC Address #define DEFAULT_DECODER_ADDRESS 3 // default Version #define DEFAULT_VERSION_ID 1 // This section defines the Arduino UNO Pins to use --> for testing purposes; not used in production #ifdef __AVR_ATmega328P__ // Define the Arduino input Pin number for the DCC Signal #define DCC_PIN 2 #define LED_PIN_WHITE_FRONT 5 #define LED_PIN_WHITE_REAR 6 #define LED_PIN_RED_FRONT 12 #define LED_PIN_RED_REAR 13 #define LED_PIN_CABIN 9 #define MOTOR_PIN_1 11 #define MOTOR_PIN_2 10 // This section defines the Arduino ATTiny1616/1626/3216/3226 Pins to use #elif defined __AVR_ATtinyxy6__ // Define the Arduino input Pin number for the DCC Signal #define DCC_PIN PIN_PA1 #define LED_PIN_WHITE_FRONT PIN_PB0 #define LED_PIN_WHITE_REAR PIN_PB1 #define LED_PIN_RED_FRONT PIN_PA5 #define LED_PIN_RED_REAR PIN_PC0 #define LED_PIN_CABIN PIN_PA7 #define MOTOR_PIN_1 PIN_PA3 #define MOTOR_PIN_2 PIN_PA4 #else #error "Unsupported CPU, you need to add another configuration section for your CPU" #endif // Some global state variables byte FN_0_4_state = 0; byte FN_5_8_state = 0; byte FN_9_12_state = 0; byte FN_13_20_state = 0; byte FN_21_28_state = 0; // if cabin LED are connected to white LED pins pull cabin LED pin low bool invert_cabin_light_logic = true; uint8_t newDirection = 0; uint8_t lastDirection = 0; //speed variables uint8_t newSpeed = 0; uint8_t targetSpeed = 0; uint32_t speedChangeTime = 0; uint8_t numSpeedSteps = SPEED_STEP_128; uint8_t currentPwm = 0; uint8_t targetPwm = 0; uint8_t lastPwm = 0; uint8_t accRate = 0; uint8_t decRate = 0; uint8_t vStart; uint8_t vHigh; // Structure for CV Values Table struct CVPair { uint16_t CV; uint8_t Value; }; // CV Addresses we will be using according to NMRA 9.2.2 Table 1 #define CV_VSTART 2 #define CV_ACC_RATE 3 #define CV_DEC_RATE 4 #define CV_VHIGH 5 // Default CV Values Table CVPair FactoryDefaultCVs [] = { // The CV Below defines the Short DCC Address {CV_MULTIFUNCTION_PRIMARY_ADDRESS, DEFAULT_DECODER_ADDRESS}, // Three Step Speed Table {CV_VSTART, 120}, {CV_VHIGH, 255}, //Acceleration and deceleration; defaults to 0 (deactivated) {CV_ACC_RATE, 0}, {CV_DEC_RATE, 0}, // CV7 Manufacturer Version ID { CV_VERSION_ID, DEFAULT_VERSION_ID}, // CV8 --> Decoder Reset? (set CV to 255 to initiate factory reset) {CV_MANUFACTURER_ID, MAN_ID_DIY}, // These two CVs define the Long DCC Address {CV_MULTIFUNCTION_EXTENDED_ADDRESS_MSB, CALC_MULTIFUNCTION_EXTENDED_ADDRESS_MSB(DEFAULT_DECODER_ADDRESS)}, {CV_MULTIFUNCTION_EXTENDED_ADDRESS_LSB, CALC_MULTIFUNCTION_EXTENDED_ADDRESS_LSB(DEFAULT_DECODER_ADDRESS)}, // ONLY uncomment 1 CV_29_CONFIG line below as approprate // {CV_29_CONFIG, 0}, // Short Address 14 Speed Steps {CV_29_CONFIG, CV29_F0_LOCATION}, // Short Address 28/128 Speed Steps // {CV_29_CONFIG, CV29_EXT_ADDRESSING | CV29_F0_LOCATION}, // Long Address 28/128 Speed Steps }; NmraDcc Dcc ; uint8_t FactoryDefaultCVIndex = 0; // This call-back function is called when a CV Value changes so we can update CVs we're using void notifyCVChange( uint16_t CV, uint8_t Value) { switch(CV) { case CV_VSTART: vStart = Value; break; case CV_VHIGH: vHigh = Value; break; case CV_ACC_RATE: accRate = Value; break; case CV_DEC_RATE: decRate = Value; break; } } #ifdef DEBUG_DCC_RESET void notifyDccReset(uint8_t hardReset) { Serial.printf(F("notifyDccReset: %6s.\n"), hardReset ? "HARD" : "NORMAL"); } #endif void notifyCVResetFactoryDefault() { // Make FactoryDefaultCVIndex non-zero and equal to num CV's to be reset // to flag to the loop() function that a reset to Factory Defaults needs to be done FactoryDefaultCVIndex = sizeof(FactoryDefaultCVs)/sizeof(CVPair); #ifdef DEBUG_DCC_RESET Serial.println("Factory reset"); #endif }; // This call-back function is called whenever we receive a DCC Speed packet for our address void notifyDccSpeed( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps ) { #ifdef DEBUG_SPEED Serial.print("notifyDccSpeed: Addr: "); Serial.print(Addr,DEC); Serial.print( (AddrType == DCC_ADDR_SHORT) ? "-S" : "-L" ); Serial.print(" Speed: "); Serial.print(Speed,DEC); Serial.print(" Steps: "); Serial.print(SpeedSteps,DEC); Serial.print(" Dir: "); Serial.println( (Dir == DCC_DIR_FWD) ? "Forward" : "Reverse" ); #endif newDirection = Dir; newSpeed = Speed; numSpeedSteps = SpeedSteps; }; // This call-back function is called whenever we receive a DCC Function packet for our address void notifyDccFunc(uint16_t Addr, DCC_ADDR_TYPE AddrType, FN_GROUP FuncGrp, uint8_t FuncState) { #ifdef DEBUG_FUNCTIONS Serial.print("notifyDccFunc: Addr: "); Serial.print(Addr,DEC); Serial.print( (AddrType == DCC_ADDR_SHORT) ? 'S' : 'L' ); Serial.print(" Function Group: "); Serial.print(FuncGrp,DEC); #endif /* Liste der Funktionen aus NEM 608 Betriebsart 1 (eine Ebene) / List of functions according to NEM608 operation mode 1 (one layer) Funk | Traktionsart | Dampf | Verbrennung | Elektrisch | SchaltFunktion | Bemerkung | Kategorie | | | | | F0 | Beleuchtung | Fahrtrichtung vorwärts / rückwärts | Fahrtrichtung vorwärts / rückwärts | Fahrtrichtung vorwärts / rückwärts | ein / aus | F1 | Beleuchtung | Rückwärtiges Licht | Rückwärtiges Licht / Schlusslicht | Rückwärtiges Licht / Schlusslicht | ein / aus | F2 | Betrieb | Achtungspfiff | Achtungspfiff / Signalhorn | Achtungspfiff | Moment | Pfiff oder Horn entsprechend der Ausrüstung. Länge des Pfiffs / Horns wird von der Zeit der Betätigung der Taste bestimmt. F3 | Geräusche | Stand, Anfahren, Fahren, Bremsen | Anlassen, Motor(en), Stand, Anfahren, Fahren, Bremsen, Abschalten Motor(en) | Stand, Anfahren, Fahren, Bremsen | ein / aus | F4 | Betrieb | Entkuppeln | Entkuppeln | Entkuppeln |Moment | F5 | Betrieb | Rangiergang | Rangiergang | Rangiergang | ein / aus | F0 und F1 schalten Beleuchtung ein / aus F6 | Beleuchtung | Führerstand | Führerstand | Führerstand | ein / aus | Bei zwei Führerständen entsprechend der Fahrtrichtung F7 | Beleuchtung | Feuerbüchse | Motorraum- / Innenbeleuchtung | Motorraum- / Innenbeleuchtung | ein / aus | F8 | Auf- /Abrüsten | Dampferzeuger | Abgaserzeuger | Pantograph | ein / aus bzw. auf / ab | Bei zwei Pantographen entsprechend der Fahrtrichtung F9 | Geräusche | Luftpumpe | Kompressor | Kompressor | ein / aus | */ switch(FuncGrp) { case FN_0_4: FN_0_4_state = FuncState; #ifdef DEBUG_FUNCTIONS Serial.print(" FuncState: "); Serial.print(FuncState); Serial.print(" FN 0: "); Serial.print((FuncState & FN_BIT_00) ? 1 : 0); Serial.print(" FN 1: "); Serial.print((FuncState & FN_BIT_01) ? 1 : 0); Serial.print(" FN 2: "); Serial.print((FuncState & FN_BIT_02) ? 1 : 0); Serial.print(" FN 3: "); Serial.print((FuncState & FN_BIT_03) ? 1 : 0); Serial.print(" FN 4: "); Serial.print((FuncState & FN_BIT_04) ? 1 : 0); #endif break; case FN_5_8: FN_5_8_state = FuncState; #ifdef DEBUG_FUNCTIONS Serial.print(" FuncState: "); Serial.print(FuncState); Serial.print(" FN 5: "); Serial.print((FuncState & FN_BIT_05) ? 1 : 0); Serial.print(" FN 6: "); Serial.print((FuncState & FN_BIT_06) ? 1 : 0); Serial.print(" FN 7: "); Serial.print((FuncState & FN_BIT_07) ? 1 : 0); Serial.print(" FN 8: "); Serial.print((FuncState & FN_BIT_08) ? 1 : 0); #endif break; case FN_9_12: FN_9_12_state = FuncState; #ifdef DEBUG_FUNCTIONS Serial.print(" FuncState: "); Serial.print(FuncState); Serial.print(" FN 9: "); Serial.print((FuncState & FN_BIT_09) ? 1 : 0); Serial.print(" FN 10: "); Serial.print((FuncState & FN_BIT_10) ? 1 : 0); Serial.print(" FN 11: "); Serial.print((FuncState & FN_BIT_11) ? 1 : 0); Serial.print(" FN 12: "); Serial.print((FuncState & FN_BIT_12) ? 1 : 0); #endif break; case FN_13_20: FN_13_20_state = FuncState; #ifdef DEBUG_FUNCTIONS Serial.print(" FuncState: "); Serial.print(FuncState); Serial.print(" FN 13: "); Serial.print((FuncState & FN_BIT_13) ? 1 : 0); Serial.print(" FN 14: "); Serial.print((FuncState & FN_BIT_14) ? 1 : 0); Serial.print(" FN 15: "); Serial.print((FuncState & FN_BIT_15) ? 1 : 0); Serial.print(" FN 16: "); Serial.print((FuncState & FN_BIT_16) ? 1 : 0); Serial.print(" FN 17: "); Serial.print((FuncState & FN_BIT_17) ? 1 : 0); Serial.print(" FN 18: "); Serial.print((FuncState & FN_BIT_18) ? 1 : 0); Serial.print(" FN 19: "); Serial.print((FuncState & FN_BIT_19) ? 1 : 0); Serial.print(" FN 20: "); Serial.print((FuncState & FN_BIT_20) ? 1 : 0); #endif break; case FN_21_28: FN_21_28_state = FuncState; #ifdef DEBUG_FUNCTIONS Serial.print(" FuncState: "); Serial.print(FuncState); Serial.print(" FN 21: "); Serial.print((FuncState & FN_BIT_21) ? 1 : 0); Serial.print(" FN 22: "); Serial.print((FuncState & FN_BIT_22) ? 1 : 0); Serial.print(" FN 23: "); Serial.print((FuncState & FN_BIT_23) ? 1 : 0); Serial.print(" FN 24: "); Serial.print((FuncState & FN_BIT_24) ? 1 : 0); Serial.print(" FN 25: "); Serial.print((FuncState & FN_BIT_25) ? 1 : 0); Serial.print(" FN 26: "); Serial.print((FuncState & FN_BIT_26) ? 1 : 0); Serial.print(" FN 27: "); Serial.print((FuncState & FN_BIT_27) ? 1 : 0); Serial.print(" FN 28: "); Serial.print((FuncState & FN_BIT_28) ? 1 : 0); #endif break; default: break; } #ifdef DEBUG_FUNCTIONS Serial.println(); #endif } // This call-back function is called whenever we receive a DCC Packet and message-debugging is activated #ifdef DEBUG_DCC_MSG void notifyDccMsg( DCC_MSG * Msg) { Serial.print("notifyDccMsg, Number of Preamble Bits: ") ; Serial.print (Msg->PreambleBits); Serial.print("; Data Bytes: "); for(uint8_t i = 0; i < Msg->Size; i++) { Serial.print(Msg->Data[i], HEX); Serial.write(' '); } Serial.println(); } #endif // This call-back function is called by the NmraDcc library when a DCC ACK needs to be sent // Calling this function should cause an increased 60ma current drain on the power supply for 6ms to ACK a CV Read // So we will just turn the motor on for 8ms and then turn it off again. // Pay attention to your control station, as it may limit power consumption on the programming track // (e.g. EX-CommandStation needs the command sent before programming to allow a // power consumption of more than 250mA and ACK working properly) void notifyCVAck(void) { #ifdef DEBUG_DCC_ACK Serial.println("notifyCVAck") ; #endif digitalWrite(MOTOR_PIN_1, HIGH); digitalWrite(MOTOR_PIN_2, LOW); delay( 8 ); digitalWrite(MOTOR_PIN_1, LOW); digitalWrite(MOTOR_PIN_2, LOW); } void setup() { #ifdef DEBUG_PRINT Serial.begin(115200); uint8_t maxWaitLoops = 255; while(!Serial && maxWaitLoops--) delay(20); Serial.println("DCCMFL1616 Debugging interface"); Serial.print("Decoder-Address: "); Serial.println(Dcc.getCV(CV_MULTIFUNCTION_PRIMARY_ADDRESS), DEC); #endif // Setup the Pins for the Fwd/Rev LED for Function 0 Headlight pinMode(LED_PIN_WHITE_FRONT, OUTPUT); pinMode(LED_PIN_WHITE_REAR, OUTPUT); pinMode(LED_PIN_RED_FRONT, OUTPUT); pinMode(LED_PIN_RED_REAR, OUTPUT); pinMode(LED_PIN_CABIN, OUTPUT); // Setup the Pins for the Motor H-Bridge Driver pinMode(MOTOR_PIN_1, OUTPUT); pinMode(MOTOR_PIN_2, OUTPUT); // Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up // Many Arduino Cores now support the digitalPinToInterrupt() function that makes it easier to figure out the // Interrupt Number for the Arduino Pin number, which reduces confusion. #ifdef digitalPinToInterrupt Dcc.pin(DCC_PIN, 0); #else Dcc.pin(0, DCC_PIN, 1); #endif Dcc.init( MAN_ID_DIY, DEFAULT_VERSION_ID, FLAGS_MY_ADDRESS_ONLY | FLAGS_AUTO_FACTORY_DEFAULT, 0 ); // Uncomment to force CV Reset to Factory Defaults; usually not needed // notifyCVResetFactoryDefault(); // Read the current CV values vStart = Dcc.getCV(CV_VSTART); vHigh = Dcc.getCV(CV_VHIGH); accRate = Dcc.getCV(CV_ACC_RATE); decRate = Dcc.getCV(CV_DEC_RATE); } void loop() { // You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation Dcc.process(); // Handle Speed changes if(targetSpeed != newSpeed) { targetSpeed = newSpeed; if (newSpeed <= 1) { targetPwm = 0; } else { // Calculate PWM value in the range 1..255 uint8_t vScaleFactor; vScaleFactor = ((vHigh > 1) && (vHigh > vStart)) ? vHigh - vStart : 255 - vStart; uint8_t modSpeed = newSpeed - 1; uint8_t modSteps = numSpeedSteps - 1; targetPwm = (uint8_t) vStart + modSpeed * vScaleFactor / modSteps; #ifdef DEBUG_PWM Serial.print("New Speed: vStart: "); Serial.print(vStart); Serial.print(" vHigh: "); Serial.print(vHigh); Serial.print(" modSpeed: "); Serial.print(modSpeed); Serial.print(" vScaleFactor: "); Serial.print(vScaleFactor); Serial.print(" modSteps: "); Serial.print(modSteps); Serial.print(" targetPwm: "); Serial.println(targetPwm); #endif } lastPwm = currentPwm; speedChangeTime = millis(); } if(targetPwm > currentPwm) { currentPwm = (accRate == 0) ? targetPwm : lastPwm + floor( (vHigh - vStart) * (millis() - speedChangeTime) / (accRate * 0.896 * 1000) + 0.5); if (currentPwm < vStart) { currentPwm = vStart; lastPwm = vStart; } else if (currentPwm > targetPwm) currentPwm = targetPwm; #ifdef DEBUG_PWM Serial.print("increase speed; accRate: "); Serial.print(accRate); Serial.print(" currentPwm: "); Serial.print(currentPwm); Serial.println(); #endif } else if(targetPwm < currentPwm) { currentPwm = (decRate == 0) ? targetPwm : lastPwm - floor( (vHigh - vStart) * (millis() - speedChangeTime) / (decRate * 0.896 * 1000) + 0.5); if (currentPwm < targetPwm) currentPwm = targetPwm; if (currentPwm < vStart) currentPwm = 0; #ifdef DEBUG_PWM Serial.print("reduce Speed; decRate: "); Serial.print(decRate); Serial.print(" currentPwm: "); Serial.print(currentPwm); Serial.println(); #endif } analogWrite(MOTOR_PIN_1, newDirection ? currentPwm : 0); analogWrite(MOTOR_PIN_2, newDirection ? 0 : currentPwm); /* * lighting */ //F0 & F5 white LEDs front digitalWrite(LED_PIN_WHITE_FRONT, ( FN_0_4_state & FN_BIT_00 & (newDirection << 4 | FN_5_8_state << 4 ) ) ? HIGH : LOW); #ifdef DEBUG_FUNCTIONS Serial.print("whithe LEDs front: "); Serial.println( ( FN_0_4_state & FN_BIT_00 & (newDirection << 4 | FN_5_8_state << 4 ) ) ? 1 : 0); #endif //F0 & F5 white LEDs rear digitalWrite(LED_PIN_WHITE_REAR, ( FN_0_4_state & FN_BIT_00 & (~(newDirection << 4) | FN_5_8_state << 4 ) ) ? HIGH : LOW); #ifdef DEBUG_FUNCTIONS Serial.print("whithe LEDs rear: "); Serial.println( ( FN_0_4_state & FN_BIT_00 & (~(newDirection << 4) | FN_5_8_state << 4 ) ) ? 1 : 0); #endif //F1 red LEDs front digitalWrite(LED_PIN_RED_FRONT, ( FN_0_4_state & FN_BIT_00 & ~(newDirection << 4) & FN_0_4_state << 4 & ~(FN_5_8_state << 4) ) ? HIGH : LOW); #ifdef DEBUG_FUNCTIONS Serial.print("RED LEDs front: "); Serial.println( ( FN_0_4_state & FN_BIT_00 & ~(newDirection << 4) & FN_0_4_state << 4 & ~(FN_5_8_state << 4) ) ? 1 : 0); #endif //F1 red LEDs rear digitalWrite(LED_PIN_RED_REAR, ( FN_0_4_state & FN_BIT_00 & newDirection << 4 & FN_0_4_state << 4 & ~(FN_5_8_state << 4) ) ? HIGH : LOW); #ifdef DEBUG_FUNCTIONS Serial.print("RED LEDs rear: "); Serial.println( ( FN_0_4_state & FN_BIT_00 & newDirection << 4 & FN_0_4_state << 4 & ~(FN_5_8_state << 4) ) ? 1 : 0); #endif //F6 cabin light (standard: only works if white LED front/rear active) digitalWrite(LED_PIN_CABIN, (FN_5_8_state & FN_BIT_06 & invert_cabin_light_logic << 1) ? LOW : HIGH ); #ifdef DEBUG_FUNCTIONS Serial.print("CABIN LEDs: "); Serial.println( (FN_5_8_state & FN_BIT_06 & invert_cabin_light_logic << 1) ? LOW : HIGH ); #endif // Handle resetting CVs back to Factory Defaults if( FactoryDefaultCVIndex && Dcc.isSetCVReady()) { FactoryDefaultCVIndex--; // Decrement first as initially it is the size of the array Dcc.setCV( FactoryDefaultCVs[FactoryDefaultCVIndex].CV, FactoryDefaultCVs[FactoryDefaultCVIndex].Value); #ifdef DEBUG_DCC_RESET Serial.print("Factory reset: "); Serial.print("CV"); Serial.print(FactoryDefaultCVs[FactoryDefaultCVIndex].CV); Serial.print(" = "); Serial.println(FactoryDefaultCVs[FactoryDefaultCVIndex].Value); #endif } }