-
Notifications
You must be signed in to change notification settings - Fork 6
/
locomotive.ino
522 lines (454 loc) · 15 KB
/
locomotive.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
// MPED DIY: Basic Multifunction Locomotive Decoder
//
// Version: 1.0
// Author: Marko Pinteric 2021-08-30.
// Based on the work by Alex Shepherd.
//
// This sketch requires the NmraDcc Library, which can be found and installed via the Arduino IDE Library Manager.
//
// This is a simple sketch for controlling motor speed and direction according to NMRA recommendations using PWM and a DRV8870 type H-bridge.
// It uses the values vStart vHigh, Acc and Dec CV, to regulate the PWM values to the motor, and the value Timout CV, to stop the motor when the signal is lost.
// It also uses the Headling Function to drive two LEDs for Directional Headlights.
// Other than that, there's nothing fancy like Lighting Effects or a function matrix or Speed Tables.
#include <NmraDcc.h>
// 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_DEACCELERATION
//#define DEBUG_PRINT
#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_DEACCELERATION)
#define DEBUG_PRINT
#endif
// Actual number of speed steps in one direction 9 * 28 = 2 * 126
#define SPEED_STEPS 252
// This is the default DCC Address
#define DEFAULT_DECODER_ADDRESS 3
// This section defines the Arduino UNO Pins to use
#if defined(__AVR_ATmega328__) or defined(__AVR_ATmega328P__)
#define DCC_PIN 2
#define LED_PIN_FWD 9
#define LED_PIN_REV 10
#define MOTOR_PIN_FWD 5
#define MOTOR_PIN_REV 6
// This section defines the Arduino ATTiny85 Pins to use
#elif defined(ARDUINO_AVR_ATTINYX5)
#define DCC_PIN 2
#define LED_PIN_FWD 0
#define LED_PIN_REV 1
#define MOTOR_PIN_FWD 3
#define MOTOR_PIN_REV 4
#else
#error "Unsupported CPU, you need to add another configuration section for your CPU"
#endif
// Some global state variables
uint8_t newLedState;
uint8_t lastLedState = 0;
uint8_t lastDirection = 1;
uint8_t newSpeed;
uint8_t lastSpeed = 0;
uint8_t numSpeedSteps = SPEED_STEP_128;
int16_t currentStep = 0;
int16_t targetStep = 0;
uint8_t absStep;
uint8_t tableElement;
uint8_t motorPwm;
uint16_t periodAcc; // time in ms between two PWM changes when accelerating
uint16_t periodDec; // time in ms between two PWM changes when decelerating
uint32_t timePwm = 0; // time in ms for next PWM change
bool changeLight = false; // change FWD/REV light
bool speedTableSelect = false; // use speed table
uint32_t timeSignal = 0xFFFFFFFF; // time in ms to expect DCC Signal (speed packet), 0xFFFFFFFF means no Signal
uint8_t vStart;
uint8_t vHigh;
uint8_t vMid;
uint32_t Timeout; // large number for calculation purposes
uint8_t speedTable[29];
// Structure for CV Values Table
struct CVPair
{
uint16_t CV;
uint8_t Value;
};
// CV Addresses we will be using
#define CV_VSTART 2
#define CV_ACCELERATION_RATE 3
#define CV_DECELERATION_RATE 4
#define CV_VHIGH 5
#define CV_VMID 6
#define CV_SIGNAL_TIMEOUT 11
#define CV_SPEED_TABLE 67
// #define CV_DECODER_MASTER_RESET 120
// Default CV Values Table
CVPair FactoryDefaultCVs [] =
{
{CV_MULTIFUNCTION_PRIMARY_ADDRESS, DEFAULT_DECODER_ADDRESS},
{CV_MULTIFUNCTION_EXTENDED_ADDRESS_MSB, 0},
{CV_MULTIFUNCTION_EXTENDED_ADDRESS_LSB, 0},
{CV_VSTART, 3}, // Zimo=1 Lais=0(9)
{CV_ACCELERATION_RATE, 5}, // Zimo=2 Lais=1(3)
{CV_DECELERATION_RATE, 2}, // Zimo=1 Lais=1(3)
{CV_VHIGH, 0}, // Zimo=1 Lais=0(0)
{CV_VMID, 0}, // Zimo=1 Lais=0(-)
{CV_VERSION_ID, 1},
{CV_MANUFACTURER_ID, MAN_ID_DIY},
{CV_SIGNAL_TIMEOUT, 5},
// ONLY uncomment one 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
{CV_SPEED_TABLE, 12},
{CV_SPEED_TABLE+1, 21},
{CV_SPEED_TABLE+2, 30},
{CV_SPEED_TABLE+3, 39},
{CV_SPEED_TABLE+4, 48},
{CV_SPEED_TABLE+5, 57},
{CV_SPEED_TABLE+6, 66},
{CV_SPEED_TABLE+7, 75},
{CV_SPEED_TABLE+8, 84},
{CV_SPEED_TABLE+9, 93},
{CV_SPEED_TABLE+10, 102},
{CV_SPEED_TABLE+11, 111},
{CV_SPEED_TABLE+12, 120},
{CV_SPEED_TABLE+13, 129},
{CV_SPEED_TABLE+14, 138},
{CV_SPEED_TABLE+15, 147},
{CV_SPEED_TABLE+16, 156},
{CV_SPEED_TABLE+17, 165},
{CV_SPEED_TABLE+18, 174},
{CV_SPEED_TABLE+19, 183},
{CV_SPEED_TABLE+20, 192},
{CV_SPEED_TABLE+21, 201},
{CV_SPEED_TABLE+22, 210},
{CV_SPEED_TABLE+23, 219},
{CV_SPEED_TABLE+24, 228},
{CV_SPEED_TABLE+25, 237},
{CV_SPEED_TABLE+26, 246},
{CV_SPEED_TABLE+27, 255},
};
NmraDcc Dcc ;
// 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;
speedTable[0] = Value;
break;
case CV_ACCELERATION_RATE:
periodAcc = (uint16_t) Value*896/SPEED_STEPS;
break;
case CV_DECELERATION_RATE:
periodDec = (uint16_t) Value*896/SPEED_STEPS;
break;
case CV_VHIGH:
vHigh = Value;
break;
case CV_VMID:
vMid = Value;
break;
case CV_29_CONFIG:
if((Value & 0b00010000) == 0) speedTableSelect = false;
else speedTableSelect = true;
case CV_SIGNAL_TIMEOUT:
Timeout = Value;
timeSignal = 0xFFFFFFFF;
break;
}
if ((CV>=67) and (CV<=94)) speedTable[CV-66] = Value;
}
uint8_t FactoryDefaultCVIndex = 0;
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
if (FactoryDefaultCVIndex == 0) FactoryDefaultCVIndex = sizeof(FactoryDefaultCVs)/sizeof(CVPair);
};
// 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 newSpeed, DCC_DIRECTION newDirection, DCC_SPEED_STEPS numSpeedSteps )
{
if (Timeout != 0) timeSignal = millis() + 1000 * Timeout;
#ifdef DEBUG_SPEED
Serial.print("notifyDccSpeed: Addr: ");
Serial.print(Addr,DEC);
Serial.print( (AddrType == DCC_ADDR_SHORT) ? "-S" : "-L" );
Serial.print(" Speed: ");
Serial.print(newSpeed,DEC);
Serial.print(" Steps: ");
Serial.print(numSpeedSteps,DEC);
Serial.print(" Dir: ");
Serial.println( (newDirection == DCC_DIR_FWD) ? "Forward" : "Reverse" );
#endif
if((lastSpeed != newSpeed) || (lastDirection != newDirection))
{
uint8_t vScaleFactor;
if (newSpeed == 0)
{
// changing direction at standstill
if (currentStep == 0) changeLight = true;
// emergency stop
currentStep = 0;
targetStep = 0;
#ifdef DEBUG_DEACCELERATION
Serial.print(" motorPwm: ");
Serial.println(0,DEC);
#endif
digitalWrite(MOTOR_PIN_REV, LOW);
digitalWrite(MOTOR_PIN_FWD, LOW);
}
else if (newSpeed == 1)
{
// changing direction at standstill
if (currentStep == 0) changeLight = true;
targetStep = 0;
}
else
{
targetStep=(newSpeed-1)*SPEED_STEPS/(numSpeedSteps-1);
if (newDirection == 0) targetStep = -targetStep;
#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(" targetStep: ");
Serial.println(targetStep);
#endif
}
// change PWM immediately
timePwm = millis();
lastSpeed = newSpeed;
lastDirection = newDirection;
}
};
// 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
if(FuncGrp == FN_0_4)
{
newLedState = (FuncState & FN_BIT_00) ? 1 : 0;
#ifdef DEBUG_FUNCTIONS
Serial.print(" FN 0: ");
Serial.print(newLedState);
#endif
}
#ifdef DEBUG_FUNCTIONS
Serial.println();
#endif
}
// This call-back function is called whenever we receive a DCC Packet
#ifdef DEBUG_DCC_MSG
void notifyDccMsg( DCC_MSG * Msg)
{
Serial.print("notifyDccMsg: ") ;
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.
void notifyCVAck(void)
{
#ifdef DEBUG_DCC_ACK
Serial.println("notifyCVAck") ;
#endif
digitalWrite(MOTOR_PIN_FWD, HIGH);
delay( 8 );
digitalWrite(MOTOR_PIN_FWD, LOW);
}
void setup()
{
#ifdef DEBUG_PRINT
Serial.begin(115200);
Serial.println("NMRA Dcc Multifunction Motor Decoder");
#endif
// Setup the Pins for the Fwd/Rev LED for Function 0 Headlight
pinMode(LED_PIN_FWD, OUTPUT);
pinMode(LED_PIN_REV, OUTPUT);
// Setup the Pins for the Motor H-Bridge Driver
pinMode(MOTOR_PIN_FWD, OUTPUT);
pinMode(MOTOR_PIN_REV, OUTPUT);
// Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up
Dcc.pin(DCC_PIN, 0);
// Call the main DCC Init function to enable the DCC Receiver
//!!!!!!!!!!!!!!!!!! FLAGS_AUTO_FACTORY_DEFAULT: Call notifyCVResetFactoryDefault() if CV 7 & 8 == 255
Dcc.init( MAN_ID_DIY, 1, FLAGS_MY_ADDRESS_ONLY | FLAGS_AUTO_FACTORY_DEFAULT, 0 );
// trigger decoder master reset
// if(Dcc.getCV(CV_DECODER_MASTER_RESET) == CV_DECODER_MASTER_RESET) notifyCVResetFactoryDefault();
// Read the current CV values
vStart = Dcc.getCV(CV_VSTART);
speedTable[0] = Dcc.getCV(CV_VSTART);
periodAcc = (uint16_t) Dcc.getCV(CV_ACCELERATION_RATE)*896/SPEED_STEPS;
periodDec = (uint16_t) Dcc.getCV(CV_DECELERATION_RATE)*896/SPEED_STEPS;
vHigh = Dcc.getCV(CV_VHIGH);
vMid = Dcc.getCV(CV_VMID);
Timeout = Dcc.getCV(CV_SIGNAL_TIMEOUT);
if((Dcc.getCV(CV_29_CONFIG) & 0b00010000) == 0) speedTableSelect = false;
else speedTableSelect = true;
for(uint8_t i = 0; i < 28; i++) speedTable[i+1] = Dcc.getCV(CV_SPEED_TABLE + i);
}
void loop()
{
// You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation
Dcc.process();
// Handle lost Signal
if (millis() > timeSignal)
{
// emergency stop
currentStep = 0;
targetStep = 0;
lastSpeed = 0;
#ifdef DEBUG_DEACCELERATION
Serial.print(" motorPwm: ");
Serial.println(0,DEC);
#endif
digitalWrite(MOTOR_PIN_REV, LOW);
digitalWrite(MOTOR_PIN_FWD, LOW);
timeSignal = 0xFFFFFFFF;
}
// Handle Speed changes
if((currentStep != targetStep) && (millis() >= timePwm))
{
if (targetStep > currentStep)
{
currentStep++;
if (currentStep > 0)
{
timePwm = millis() + periodAcc;
}
else
{
timePwm = millis() + periodDec;
}
}
else // targetStep < currentStep
{
currentStep--;
if (currentStep < 0)
{
timePwm = millis() + periodAcc;
}
else
{
timePwm = millis() + periodDec;
}
}
absStep = abs(currentStep);
if(absStep == 0)
{
motorPwm = 0;
changeLight = true;
}
else if(speedTableSelect)
{
tableElement = trunc(currentStep/9);
motorPwm = (uint8_t) speedTable[tableElement] + (absStep - 9 * tableElement) * (speedTable[tableElement+1] - speedTable[tableElement]) / 9;
}
else if ((vMid > 0) && (vMid > vStart) && (vHigh > vMid))
{
if (absStep < SPEED_STEPS/2)
{
motorPwm = (uint8_t) vStart + (uint16_t) (vMid - vStart) * absStep/(SPEED_STEPS/2);
}
else
{
motorPwm = (uint8_t) vMid + (uint16_t) (vHigh - vMid) * (absStep-SPEED_STEPS/2)/(SPEED_STEPS/2);
}
}
else if ((vHigh > 0) && (vHigh > vStart))
{
motorPwm = (uint8_t) vStart + (uint16_t) (vHigh - vStart) * absStep/SPEED_STEPS;
}
else
{
motorPwm = (uint8_t) vStart + (255 - vStart) * absStep/SPEED_STEPS;
}
#ifdef DEBUG_DEACCELERATION
Serial.print(" vMid: ");
Serial.print(vMid,DEC);
Serial.print(" vHigh: ");
Serial.print(vHigh,DEC);
Serial.print(" motorPwm: ");
Serial.print(motorPwm,DEC);
Serial.print(" currentStep: ");
Serial.println(currentStep,DEC);
#endif
if (currentStep > 0)
{
digitalWrite(MOTOR_PIN_REV, LOW);
analogWrite(MOTOR_PIN_FWD, motorPwm);
}
else if (currentStep < 0)
{
digitalWrite(MOTOR_PIN_FWD, LOW);
analogWrite(MOTOR_PIN_REV, motorPwm);
}
else
{
digitalWrite(MOTOR_PIN_REV, LOW);
digitalWrite(MOTOR_PIN_FWD, LOW);
}
}
// Handle Direction and Headlight changes
if((changeLight) || (lastLedState != newLedState))
{
if(newLedState)
{
#ifdef DEBUG_FUNCTIONS
Serial.println("LED On");
#endif
if(currentStep == 0)
{
digitalWrite(LED_PIN_FWD, lastDirection ? HIGH : LOW);
digitalWrite(LED_PIN_REV, lastDirection ? LOW : HIGH);
}
else if(currentStep > 0)
{
digitalWrite(LED_PIN_FWD, HIGH);
digitalWrite(LED_PIN_REV, LOW);
}
else
{
digitalWrite(LED_PIN_FWD, LOW);
digitalWrite(LED_PIN_REV, HIGH);
}
}
else
{
#ifdef DEBUG_FUNCTIONS
Serial.println("LED Off");
#endif
digitalWrite(LED_PIN_FWD, LOW);
digitalWrite(LED_PIN_REV, LOW);
}
changeLight = false;
lastLedState = newLedState;
}
// 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);
}
}