// DirectRail to Control your Märklin (and other) trains - Public version - gelit@bluewin.ch - 13 mai 2020 // This software demonstrates how it is easy to control locomotive & turn compatible with MM2 protocol // It uses powerful PWM technology embedded in Arduino Due // Demo as proof of concept with Loc 42 & Turn 1 for hobbyst // User's Manual --> http://gelit.ch/Train/DirectRail.pdf // Thanks to Prof. Andrea Scorzoni : this beautiful document gives me stimulus to implement MM2 protocol during Covid-19 confinement ! // http://spazioinwind.libero.it/scorzoni/motorola.htm // I assume no responsability what you make with this Open Source software ! // Convention : Global var&const with first letter in Capital / local var with first letter in lowercase // LICENSE https://www.gnu.org/licenses/old-licenses/lgpl-2.1.fr.html const int Lo=42; // Set your Locomotive Address const int TuAdr=1; // Set your Turn Address const int PowerPin=3; // To enable H-Bridge volatile bool Power; // Rail Power manage by PWM_ISR to avoid parasite or too short impulse bool Dir; // Direction : 1=Forward 0=Reverse bool F0,F1,F2,F3,F4; // Function int Level; // Speed Level bool Pa[3][20]; // 3 Packets with Length = 18 + 1=Command-Response const int PaLo=1; // Packet Position for Locomotive const int PaTu=2; // Turn unsigned long T0,T1; // EXECUTION TIME int I; void Menu() { Serial.println("Enter:PowerControl +:Speed -:Speed 0:F0 1:F1 2:F2 3:F3 4:F4 y:Turn- no:Turn|"); Serial.println("p-->print Decoder = Idle + Loc + Turn"); } void setup() { pinMode(PowerPin, OUTPUT); digitalWrite(PowerPin,0); // disable L293D with Pin 3 (PowerOFF can't at this time) Serial.begin(115200); Dir=1; // Forward Direction F0=0; F1=0; F2=0; F3=0; F4=0; T1=0; Menu(); MM2_Init(); PWM_Init(); } void loop() { int z, y; byte in; // Input if (T1!=0) {Serial.print(" DeltaTime="); Serial.print(T1-T0); Serial.println(" ms"); T1=0;} if (Serial.available() > 0 && !Power) { in = Serial.read(); if (in=='\r') {Power=1; Serial.println("PowerON");} else {if (!Power) {Serial.println("Enter to give POWER !");}} } if (Serial.available() > 0 && Power) { in = Serial.read(); switch (in) { case '\r': if(Power) {Power=0; Serial.println("PowerOFF");} else {Power=1; Serial.println("PowerON");} // Toggle var - Action is managed by PWM to avoid parasite & short impulse break; case '+' : if (Level==0) {Level=2; if (!Dir) {Dir=1;}} // Change Dir else if (Dir) {Level++;} else {Level--; if (Level==1){Level=0;}} if (Level==15) {Level=14;} Serial.print("Lo="); Serial.print(Lo); Serial.print(" Level="); Serial.print(Level); Serial.print("Dir="); Serial.println(Dir); // OK Speed(Lo, Level, Dir); break; case '-' : if (Level==0) {Level=2; if (Dir) {Dir=0;}} // Change Dir else if (Dir) {Level--; if (Level==1){Level=0;}} else {Level++;} if (Level==15) {Level=14;} Serial.print("Lo="); Serial.print(Lo); Serial.print(" Level="); Serial.print(Level); Serial.print("Dir="); Serial.println(Dir); // OK Speed(Lo, Level, Dir); break; case 'p' : Serial.println(); for (y=0; y<=PaTu; y++) { if (y==0) {Serial.print(" IDLE ");} else if (y==PaLo) {Serial.print(" Loco ");} else {Serial.print(" Turn ");} for (z=0;z<8;z++) {Serial.print(" "); Serial.print(Pa[y][z]);} Serial.print(" "); // 9 first bit = Adr bit for (z=8;z<10;z++) {Serial.print(" "); Serial.print(Pa[y][z]);} Serial.print(" "); // F0 for (z=10;z<18;z++) {Serial.print(" "); Serial.print(Pa[y][z]);} Serial.print(" "); Serial.print(Pa[y][18]); Serial.println(); } break; case '0' : Serial.print("F0-"); Serial.println(F0); if (F0) {Fu(Lo,0,1); F0=0;} else {Fu(Lo,0,0); F0=1;} break; case '1' : Serial.print("F1-"); Serial.println(F1); if (F1) {Fu(Lo,1,1); F1=0;} else {Fu(Lo,1,0); F1=1;} break; case '2' : Serial.print("F2-"); Serial.println(F2); if (F2) {Fu(Lo,2,1); F2=0;} else {Fu(Lo,2,0); F2=1;} break; case '3' : Serial.print("F3-"); Serial.println(F3); if (F3) {Fu(Lo,3,1); F3=0;} else {Fu(Lo,3,0); F3=1;} break; case '4' : Serial.print("F4-"); Serial.println(F4); if (F4) {Fu(Lo,4,1); F4=0;} else {Fu(Lo,4,0); F4=1;} break; case 'y' : Serial.print("Turn-"); SeT(TuAdr,1); // Position - break; case 'n' : Serial.print("Turn|"); SeT(TuAdr,0); // Position | break; case 'h' : Menu(); break; default : Serial.println("BUG"); // Console Reset --> loop executes BEFORE setup // https://github.com/arduino/Arduino/pull/1267 break; // OK with hardware Reset } } } void PowerON() {digitalWrite(PowerPin,1); Power=1;} void PowerOFF() {digitalWrite(PowerPin,0); Power=0;} void MM2_Init() { // 3 packets (Idle - Loc - Turn) used / Idle & Loc sended continuously / Turn packet sended once // Length = 18 bit + X // Packet 0 : Idle = virtual Decoder 0 = mandatory to give POWER ! Pa[0][0] = 0; Pa[0][1] = 0; Pa[0][2] = 0; Pa[0][3] = 0; Pa[0][4] = 0; Pa[0][5] = 0; Pa[0][6] = 0; Pa[0][7] = 0; Pa[0][8] = 0; Pa[0][9] = 0; Pa[0][10] = 0; Pa[0][11] = 0; Pa[0][12] = 0; Pa[0][13] = 0; Pa[0][14] = 0; Pa[0][15] = 0; Pa[0][16] = 0; Pa[0][17] = 0; // Packet 1 : Loc with Light ON & Speed=0 SetAdr(Lo,PaLo); Pa[PaLo][8] = 1; Pa[PaLo][9] = 1; // Light ON Pa[PaLo][10] = 0; Pa[PaLo][11] = 0; Pa[PaLo][12] = 0; Pa[PaLo][13] = 1; Pa[PaLo][14] = 0; Pa[PaLo][15] = 0; Pa[PaLo][16] = 0; Pa[PaLo][17] = 1; // Speed=0 --> // A E B F C G D H= Scorzoni // Packet 2 : Turn for (int N=0; N<=18; N++) {Pa[PaTu][N]=0;} } // F1-F4 ================================================================================ void Fu(int l, int f, bool v) { // Loc, F0-F4, Value Serial.print("F"); Serial.print(f); Serial.print("-"); Serial.print(v); if (f>4) {Serial.print("Function not supported");} T1=0; T0=millis(); I=PaLo; // Loc Packet // EFGH=0101--> Forward Low // 0100 High // EFGH=1011--> Reverse Low // 1010 High switch (f) { case 0 : Pa[I][11]=0; Pa[I][13]=1; Pa[I][15]=0; Pa[I][17]=1; Pa[I][8]=v; Pa[I][9]=v; break; case 1 : Pa[I][11]=1; Pa[I][13]=1; Pa[I][15]=0; Pa[I][17]=v; break; // EFGH=110v --> F1 case 2 : Pa[I][11]=0; Pa[I][13]=0; Pa[I][15]=1; Pa[I][17]=v; break; // EFGH=001v --> F2 case 3 : Pa[I][11]=0; Pa[I][13]=1; Pa[I][15]=1; Pa[I][17]=v; break; // EFGH=011v --> F3 case 4 : Pa[I][11]=1; Pa[I][13]=1; Pa[I][15]=1; Pa[I][17]=v; break; // EFGH=111v --> F4 } Pa[I][16]=0; Pa[I][14]=0; Pa[I][12]=0; Pa[I][10]=0; // Level0 Pa[I][18]=1; // CMD to PWM } // SPEED ================================================================================ void Speed(int l, int le, int d) { // Loc Level Direction int leM; Serial.print("Speed Loc="); Serial.print(l); Serial.print(" Level="); Serial.print(le); T0=millis(); I=PaLo; // Loc Packet Inserted after Idle leM = le; if (le != 0) {le++;} // DCBA Pa[I][16]=0; Pa[I][14]=0; Pa[I][12]=0; Pa[I][10]=0; if (le>=8) {le-=8; Pa[I][16]=1;} if (le>=4) {le-=4; Pa[I][14]=1;} if (le>=2) {le-=2; Pa[I][12]=1;} if (le>=1) {le-=1; Pa[I][10]=1;} le = leM; if (d) { // Direction if (le<=6) {Pa[I][11]=0; Pa[I][13]=1; Pa[I][15]=0; Pa[I][17]=1;} // EFGH=0101--> Forward Low else {Pa[I][11]=0; Pa[I][13]=1; Pa[I][15]=0; Pa[I][17]=0;} // 0100 High } else { if (le<=6) {Pa[I][11]=1; Pa[I][13]=0; Pa[I][15]=1; Pa[I][17]=1;} // EFGH=1011--> Reverse Low else {Pa[I][11]=1; Pa[I][13]=0; Pa[I][15]=1; Pa[I][17]=0;} // 1010 High } Pa[I][18]=1; // CMD to PWM } // TURN ===================================== void SeT(int d, bool v) { // Device Value int p[10]; int a; int q[10]; T0=millis(); // to Tri & Bit // Bit 0-1 2-3 4-5 6-7 8-9 // tri --> 1+4 9+4 27+4 81+4 243+4 // Q2 Q3 Q4 Q5 Q6 I=PaTu; // Turn Packet Inserted after Idle if (d > 0 && d < 320) { // add 2 "bit" to address --> 4 combinaisons p[0]=1; p[1]=2; p[2]=1*4; p[3]=3*4; p[4]=3*3*4; p[5]=3*3*3*4; p[6]=3*3*3*3*4; // Märklin extend old format with 80 Turns to 4x80=320 Turns d+=3; for (a=6; a>=2; a--) { // tribit q[a]=0; if (d>=p[a]) {q[a]++; d=d-p[a];} if (d>=p[a]) {q[a]++; d=d-p[a];} switch (a) { // write to Pa from bit 0 to bit 10 case 2 : Tri(q[2], I, 0); break; // MM2 Adr become HIGH adr case 3 : Tri(q[3], I, 2); break; case 4 : Tri(q[4], I, 4); break; case 5 : Tri(q[5], I, 6); break; case 6 : Tri(q[6], I, 8); break; } } for (a=1; a>=0; a--) { // bit q[a]=0; if (d>=p[a]) {q[a]++; d=d-p[a];} switch (a) { // write to Pa from bit 12 to bit 15 case 0 : Tri(q[0], I, 12); break; // Factor 4 implemented with bit 12 - 15 case 1 : Tri(q[1], I, 14); break; } } Tri(v, I, 10); // Value Tri(1, I, 16); // always 1 Pa[I][18] = 1; // TURN CMD } } //================================================================================ MM2 void Tri(int v, int l, int b) { // Value, Loc, Bit switch (v) { case 0 : Pa[l][b] = 0; Pa[l][b+1] = 0; break; // MC 145026 encoding case 1 : Pa[l][b] = 1; Pa[l][b+1] = 1; break; case 2 : Pa[l][b] = 1; Pa[l][b+1] = 0; break; } } void SetAdr(int l, int po) { // Loc, Position int p[10]; int a; int q[6]; p[0]=1; p[1]=3; p[2]=3*3; p[3]=3*3*3; p[4]=3*3*3*3; // Adr = 5 Tribit - Poids for (a=4; a>=0; a--) { // Conversion Decimal to q[a] = 0; if (l >= p[a]) {q[a]++; l=l-p[a];} if (l >= p[a]) {q[a]++; l=l-p[a];} Tri(q[a], po, 2*a); // write to Pa from bit 0 to bit 17 } } //================================================================================ PWM // from https://forum.arduino.cc/index.php?topic=590202.25;wap // from https://forum.arduino.cc/index.php?topic=590202.0 // VOLATILE VAR used by Interrupt volatile int T_PWM; // Time PWM Handler var init volatile int Bi_PWM; // Bit PWM Handler var init volatile int Do_PWM; // Doppel packet PWM Handler var init volatile int SM_PWM; // State Machine PWM Handler var init volatile int N_PWM; // Nb of packet send by PWM Handler volatile int M_PWM; // Max packet to send by PWM Handler void PWM_Init() { T_PWM = 0; Bi_PWM = 0; Do_PWM = 0; SM_PWM = 0; N_PWM = 0; PMC->PMC_PCER1 |= PMC_PCER1_PID36; // Enable PWM (Power On) PIOC->PIO_PDR |= PIO_PDR_P3 | PIO_PDR_P2; // DUE Pin 34-35 to PWM Peripheral PIOC->PIO_ABSR |= PIO_ABSR_P3 | PIO_ABSR_P2; // Setting pins to Peripheral B PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84); // Set PWM clock = 1MHz (84MHz/84) PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA; // Period is left aligned, clock source is CLKA on Channel 0 PWM->PWM_SCM |= PWM_SCM_SYNC0; // Synchronizing of Channels 0 PWM->PWM_SCM |= PWM_SCM_UPDM_MODE1; // Manual Write of duty-cycle automatic trigger of the update NVIC_SetPriority(PWM_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for the PWM controller to 0 (highest) NVIC_EnableIRQ(PWM_IRQn); // Connect PWM Controller to Nested Vector Interrupt Controller (NVIC) PWM->PWM_IER1 = PWM_IER1_CHID0; // Enable interrupt on PWM channel 0 triggered at end of PWM period PWM->PWM_CH_NUM[0].PWM_CPRD = 208; // Channel 0 Period f = 1MHz/(2*CPRD) --> T=208 micros PWM->PWM_CH_NUM[0].PWM_CDTY = 0; // Channel 0 duty-cycle at 0% PWM->PWM_CH_NUM[1].PWM_CDTY = 0; // Channel 1 duty-cycle at 0% PWM->PWM_ENA = PWM_ENA_CHID0; // Enable synchronous PWM on Channel 0 PWM->PWM_SCUC = PWM_SCUC_UPDULOCK; // Set the update unlock bit to trigger an update at the end of the next PWM period IMPORTANT } void PWM_Handler() { // PWM ISR each 208 us = T if (PWM->PWM_ISR1 & PWM_ISR1_CHID0) { // update condition switch (SM_PWM) { // State Machine : 0=0Volt during 4 ms / 1=Idle or Loc Packet / 2=0Volt during 1.5 ms / 3=Turn Packet / 4=same 2 with Tunn time // Best time to ... case 0 : PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 0; T_PWM++; if (T_PWM==6) {if (Power) {PowerON();} else {PowerOFF();}} // enable/disable POWER on rail (avoid short pulse, ...) if (T_PWM==8) { if (Pa[PaLo][18]) { // execute Speed command Pa[PaLo][18]=0; // Cmd DONE; if (T0!=0) {T1=millis();} } PWM->PWM_CH_NUM[0].PWM_CPRD = 208; // Speed Timing } if (T_PWM==10) { if (Pa[PaTu][18]) { PWM->PWM_CH_NUM[0].PWM_CPRD = 104; // Turn Command with T = 104 us Pa[PaTu][18]=0; // Cmd DONE; if (T0!=0) {T1=millis();} SM_PWM = 3; } } if (T_PWM==20) { SM_PWM = 1; // 4,1 ms between doppel packet : measure with MS2 Bi_PWM = 0;} break; case 1 : if (Pa[N_PWM][Bi_PWM]) {PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 182;} // HIGH impulse else {PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 26;} // LOW impulse Bi_PWM++; if (Bi_PWM == 18) { // Packet sended Do_PWM++; Bi_PWM = 0; if (Do_PWM == 1) { SM_PWM = 2; // doppel packet T_PWM = 0; } else { SM_PWM = 0; // cycle Loc T_PWM = 0; Do_PWM = 0; N_PWM++; if (N_PWM == 2) { N_PWM = 0; } } } break; case 2 : PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 0; T_PWM++; if (T_PWM == 7) { SM_PWM = 1; // 1,5 ms inside doppel packet from MS2 - doppel packet --> §3.2 https://www.nxp.com/docs/en/data-sheet/MC145026.pdf Bi_PWM = 0; } break; case 3 : if (Pa[PaTu][Bi_PWM]) {PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 91;} // HIGH else {PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 13;} // LOW Bi_PWM++; if (Bi_PWM == 18) { // Packet sended Do_PWM++; Bi_PWM = 0; if (Do_PWM == 1) { SM_PWM = 4; // doppel packet T_PWM = 0; } else { SM_PWM = 0; T_PWM = 0; Do_PWM = 0; N_PWM = 0; } } break; case 4 : PWM->PWM_CH_NUM[0].PWM_CDTYUPD = 0; T_PWM++; if (T_PWM == 3) { SM_PWM = 3; Bi_PWM = 0; } break; } PWM->PWM_SCUC = PWM_SCUC_UPDULOCK; // IMPORTANT } }