DCC++Arduino: Bare-bones DCC++ generator

Travis Farmer Mar 8, 2018

  1. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    bit of trouble understanding the direct port manipulation and timing for the DCC++ signal.
    here it is for the main track:
    Code:
    bitSet(TCCR1A, WGM10);     // set Timer 1 to FAST PWM, with TOP=OCR1A
        bitSet(TCCR1A, WGM11);
        bitSet(TCCR1B, WGM12);
        bitSet(TCCR1B, WGM13);
    
        bitSet(TCCR1A, COM1B1);    // set Timer 1, OC1B (pin 10/UNO, pin 12/MEGA) to inverting toggle (actual direction is arbitrary)
        bitSet(TCCR1A, COM1B0);
    
        bitClear(TCCR1B, CS12);    // set Timer 1 prescale=1
        bitClear(TCCR1B, CS11);
        bitSet(TCCR1B, CS10);
    
        OCR1A = DCC_ONE_BIT_TOTAL_DURATION_TIMER1;
        OCR1B = DCC_ONE_BIT_PULSE_DURATION_TIMER1;
    
        pinMode(DCCppConfig::SignalEnablePinMain, OUTPUT);   // master enable for motor channel A
    
        mainRegs.loadPacket(1, RegisterList::idlePacket, 2, 0);    // load idle packet into register 1   
    
        bitSet(TIMSK1, OCIE1B);    // enable interrupt vector for Timer 1 Output Compare B Match (OCR1B)   
        digitalWrite(DCCppConfig::SignalEnablePinMain, LOW);
    and here it is for the programming track:
    Code:
    bitSet(TCCR0A, WGM00);     // set Timer 0 to FAST PWM, with TOP=OCR0A
        bitSet(TCCR0A, WGM01);
        bitSet(TCCR0B, WGM02);
    
        bitSet(TCCR0A, COM0B1);    // set Timer 0, OC0B (pin 5) to inverting toggle (actual direction is arbitrary)
        bitSet(TCCR0A, COM0B0);
    
        bitClear(TCCR0B, CS02);    // set Timer 0 prescale=64
        bitSet(TCCR0B, CS01);
        bitSet(TCCR0B, CS00);
    
        OCR0A = DCC_ONE_BIT_TOTAL_DURATION_TIMER0;
        OCR0B = DCC_ONE_BIT_PULSE_DURATION_TIMER0;
    
        pinMode(DCCppConfig::SignalEnablePinProg, OUTPUT);   // master enable for motor channel B
    
        progRegs.loadPacket(1, RegisterList::idlePacket, 2, 0);    // load idle packet into register 1   
    
        bitSet(TIMSK0, OCIE0B);    // enable interrupt vector for Timer 0 Output Compare B Match (OCR0B)
    i am using PWM pins PB1 (OC0B/OC1A/PCINT1), though i am not great with direct port manipulation, so i am unsure of the bitset values. i will be setting both programming and main track values to the same, as the ATtiny85 will only support one at a time anyway. i will use hardware detection constants to make the DCCpp library still backwards compatible with other boards. so i ask those that more "fully" understand the direct port manipulation portion ( ;) ), what changes are made here?

    ~Travis
     
    Scott Eric Catalano likes this.
  2. SP_fan_1951

    SP_fan_1951 TrainBoard Member

    93
    86
    6
    Looking at the datasheets for the ATTiny85 and the ATMega328, I think you are going to have a problem. The DCC++ code uses the 16 bit timer 1 in the 328 with no prescaler at 16 MHz. The ATtiny only has 2 8 bit timers and no 16 bit timers. You are going to have to do some clever programming to count multiple 8 bit counts, followed by the remainder. The zero total time count is 3199, and the zero "On" time count is 1599. The one bit total time count is 1855 and the "on" time count is 927. For example 3199 divided by 256 is 12 with a remainder of 127, so you need to have the timer count to completion 12 times and then count to 127, less the time required for you ISR to reset the counter each time it counts down. If you use the internal PLL to upscale the internal 8 MHz clock to 64 MHz, and then prescale it by 4, you get the same 16MHz clock the Uno uses. You might consider just using a Uno with the DIP socket as a programmer, and use a ATMega328P. They are almost as cheap as the ATTiny85. If you use the prescaler to drop the clock frequency, I am not sure you could get the required resolution to meet DCC timing requirements.
     
  3. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    thank you very much for that. apparently i should have read the datasheet a little better. ;)
    i will change gears back to beginning, and go from there. as i still want to have a separate generator, i will use a 328P (i actually have a few on hand), and work on adding in I2C control support. at least that solves the flash memory issue. :D

    ~Travis
     
    Scott Eric Catalano likes this.
  4. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    I had a look at the ATTiny85 datasheet, so, let me have a go at this.

    A while back I build a timer for my UV Exposure light box. I used an Atmega 32U4 for the MCU. It has a Timer0, 1, 3 and 4. Timer0 and 1 are the same as for the UNO and MEGA. It has no Timer2, Timer1 is 16bit and 4 is 8bit. Since I decided already to build a single board DCC++ Base Station using the 32U4, I experimented a bit with the board, before putting it into the light box, to see how it works with DCC++. I spent a lot of time understanding the timer functions and the way they are used in DCC++. The 8 bit Timer4 turned out to be the best option for this, leaving timer0 intact while nicely matching the output pins. Like the ATTiny85 it is a "High Speed" timer. So here is how I would use it in the ATTiny85.

    Since the most logical pin indeed seems to PB1, you need to use Timer1. This leaves Timer0 unchanged to do its normal thing.

    Use the Synchronous mode, which is the default mode implying no changes to any register, so the Timer is clocked at 16MHz (I assume this is what you intend using). We then divide by 16 - the beauty of this timer is the range of pre-scaling factors - which gives exactly 1us per count. So for a 0 we use 200 and 100, and for a 1 we use 116 and 58 - clean and accurate.

    The Timer1 registers for the ATTiny85 are very different from any of the others, so here is my interpretation - check with the datasheet and see if you agree.

    TCCR1: Bit 7 = 0, do not reset on Compare Match. Bit 6 = 1, enable PWM A with output on pin PB1, reset on match with OCR1C. Bit 5:4 = 11, clock output line change on match with OCR1A (as well as generate an interrupt). Bits 3:0 = 0101, pre-scale = 16. So, TCCR1 = 01110101. You can just straight assign that to the register.

    GTCCR: No changes from default which leaves PWM1B disabled.

    TIMSK: Bit6 = 1, enable interrupt on Match 1A.

    Load the longer time, ie 200 or 116, into OCR1C. And, oh yes, Output enable pin PB1.

    You will need to update the interrupt routine to load Registers OCR1A and C instead of A and B as for the UNO etc. Also TOTAL_DURATION goes to C and PULSE_DURATION goes to A. Seeing you only need one interrupt routine it may be easier to just expand the macro manually.

    I do not have an ATTiny85 so could not test all of the above. You can try a breadboard version with the <D> diagnostic to see if a LED on PB1 responds as expected.

    Hope that helps.

    Willem.
     
  5. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    so... most of that was a bit over my head, but lets see if i have this right. here is the revised block of code from the main track in post #21.
    Code:
    TCCR1 = b01110101;
    
        OCR1C = DCC_ONE_BIT_TOTAL_DURATION_TIMER1;
        OCR1A = DCC_ONE_BIT_PULSE_DURATION_TIMER1;
    
        pinMode(DCCppConfig::SignalEnablePinMain, OUTPUT);   // master enable for motor channel A
    
        mainRegs.loadPacket(1, RegisterList::idlePacket, 2, 0);    // load idle packet into register 1
    
        bitSet(TIMSK, 6);    // enable interrupt on Match 1A.
        digitalWrite(DCCppConfig::SignalEnablePinMain, LOW);
    unfortunately, i don't have an ATtiny85 handy to test anything (will be on order when next i get paid).

    not sure what to do with the interrupt routines...

    Thank you very much for your help so far. :D

    ~Travis
     
    Scott Eric Catalano likes this.
  6. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    here is what i interperated with the interrupt macro:
    Code:
    #define DCC_SIGNAL(R,N) \
      if(R.currentBit==R.currentReg->activePacket->nBits){    /* IF no more bits in this DCC Packet */ \
        R.currentBit=0;                                       /*   reset current bit pointer and determine which Register and Packet to process next--- */ \
        if (R.nRepeat>0 && R.currentReg == R.reg) {               /*   IF current Register is first Register AND should be repeated */ \
            R.nRepeat--;                                        /*     decrement repeat count; result is this same Packet will be repeated */ \
        } \
        else if (R.nextReg != NULL) {                           /*   ELSE IF another Register has been updated */ \
            R.currentReg = R.nextReg;                             /*     update currentReg to nextReg */ \
            R.nextReg = NULL;                                     /*     reset nextReg to NULL */ \
            R.tempPacket = R.currentReg->activePacket;            /*     flip active and update Packets */ \
            R.currentReg->activePacket = R.currentReg->updatePacket; \
            R.currentReg->updatePacket = R.tempPacket; \
        } \
        else {                                               /*   ELSE simply move to next Register */ \
            if (R.currentReg == R.maxLoadedReg)                    /*     BUT IF this is last Register loaded */ \
                R.currentReg = R.reg;                               /*       first reset currentReg to base Register, THEN */ \
                R.currentReg++;                                     /*     increment current Register (note this logic causes Register[0] to be skipped when simply cycling through all Registers) */ \
            }                                                     /*   END-ELSE */ \
        }                                                       /* END-IF: currentReg, activePacket, and currentBit should now be properly set to point to next DCC bit */ \
        \
        if (R.currentReg->activePacket->buf[R.currentBit / 8] & R.bitMask[R.currentBit % 8]) {     /* IF bit is a ONE */ \
            OCR ## N ## C = DCC_ONE_BIT_TOTAL_DURATION_TIMER ## N;                               /*   set OCRA for timer N to full cycle duration of DCC ONE bit */ \
        OCR ## N ## A=DCC_ONE_BIT_PULSE_DURATION_TIMER ## N;                               /*   set OCRB for timer N to half cycle duration of DCC ONE but */ \
      } else{                                                                              /* ELSE it is a ZERO */ \
        OCR ## N ## C=DCC_ZERO_BIT_TOTAL_DURATION_TIMER ## N;                              /*   set OCRA for timer N to full cycle duration of DCC ZERO bit */ \
        OCR ## N ## A=DCC_ZERO_BIT_PULSE_DURATION_TIMER ## N;                              /*   set OCRB for timer N to half cycle duration of DCC ZERO bit */ \
      }                                                                                    /* END-ELSE */ \
        \
        R.currentBit++;     /* point to next bit in current Packet */
    and the interrupt itself i am unsure about. will it work to call for both main and prog tracks in one ISR?
    Code:
    ISR(TIMER1_COMPB_vect) {
        DCC_SIGNAL(DCCpp::mainRegs, 1);
        DCC_SIGNAL(DCCpp::progRegs, 1);
    }
    ~Travis
     
    Scott Eric Catalano likes this.
  7. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    Revised ISR:
    Code:
    ISR(TIMER1_COMPB_vect) {              // set interrupt service for OCR1B of TIMER-1 which flips direction bit of Motor Shield Channel A controlling Main Track
        if (DCCppConfig::SignalEnablePinProg != UNDEFINED_PIN)
        {
    #if defined(ARDUINO_ATTINY)
            DCC_SIGNAL(DCCpp::progRegs, 1);
    #endif // defined
        }
        if (DCCppConfig::SignalEnablePinMain != UNDEFINED_PIN)
        {
            DCC_SIGNAL(DCCpp::mainRegs, 1);
        }
    
    
    }
    may work with less conflicts. ;)
    ~Travis
     
    Scott Eric Catalano likes this.
  8. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    It looks as if you have most of it correct.

    It will be ISR(TIMER1_COMPA_vect). There is only one interrupt routine so it will run as either Main or as Prog at any one time. It will be either
    DCC_SIGNAL(DCCpp::mainRegs, 1) or DCC_SIGNAL(DCCpp::progRegs, 1), depending on the current requirement. Somehow the system will need to be switched between the modes as and when required. Other than that I think it should work. I think some commercial systems also get switched between Main and Programming as required.

    This little chip seems to be more powerful than initial casual inspection. I am going to order some myself and play around with them. I will get an 8 Pin DIL for breadboard work and then some for SMD mounting - it is available as both at almost the same price. What is nice is that you can run the internal clock as system clock at 16MHz, might need a bit of tweaking to get an accurate speed. I do not know what the tolerance for the DCC pulse widths are but ATMEL claim only within 10% accuracy with factory calibration.

    I have not yet looked at the I2C instruction requirement.

    Willem.

    Edit: I am slow in replying. Your version of the ISR seems OK. That assumes you are compiling the version as either Main or Program. I was thinking more along the line of having the ability to switch at any time for the same BS.
     
    Last edited: Mar 22, 2018
    Scott Eric Catalano likes this.
  9. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    How do I prevent the smiley replacing ": p" (without the space)?

    Willem.
     
    Scott Eric Catalano likes this.
  10. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    above the post textbox, there is a "+" icon. it will drop a menu, and the "</> Code" option lets you put source code in the message.

    here is what i have done to the ISR so only one should be run at any given moment:
    Code:
    #if defined(ARDUINO_ATTINY) // Checks if we are on an ATtiny
    ISR(TIMER1_COMPA_vect) { // only runs on ATtiny
    #else
    ISR(TIMER1_COMPB_vect) { // runs other than ATtiny
    #end if
        if (DCCppConfig::SignalEnablePinProg != UNDEFINED_PIN) // if the pin is defined for the prog track, run the macro
        {
    #if defined(ARDUINO_ATTINY)
            DCC_SIGNAL(DCCpp::progRegs, 1); // only runs if on an ATtiny, and if the prog track pin is defined
    #endif // defined
        }
        if (DCCppConfig::SignalEnablePinMain != UNDEFINED_PIN) // if the pin is defined for the main track, run the macro
        {
            DCC_SIGNAL(DCCpp::mainRegs, 1);
        }
    }
    so in effect, only one is run at given time, depending on the configuration.

    ~Travis
     
    Scott Eric Catalano likes this.
  11. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    i am working on that, but the progress is rather slow. i think i know how i want the instruction sets to work, but i got side tracked by the actual DCC++ protocol. it is just on a side burner right now. i want the instructions to be all bytes, rather than textual, as it will speed up the transfer quite a bit. the hang-up is that the interface is text command based, so i may have to re-write a few things, and isolate them to just the ATtiny.

    ~Travis
     
    Scott Eric Catalano likes this.
  12. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    Sometimes when trying to be everything to everyone we incur huge overheads in coding. In this case, with all the specialised requirements, it may eventually be quicker, and cleaner, to just forego the cross compatibility and create an ATTiny version as a stand alone project. Especially since it will be dedicatied to train control only (no memory for anything else).

    If I build such a unit it will be on a single board including a single motor bridge and all associated circuits, like current sense and maybe an ESP8266 for wireless communication. Then again the ESP8266 is probably powerfull enough to do everything itself.

    I did get some ESP8266-12Fs and have designed a development board for it. I am waiting for two components before I can assemble it. I will then play with that and see what it can do. The ESP8266 is, however, quite a bit more expensive than the ATTiny, the rest being equal.

    Willem.
     
  13. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    point well taken ;) i do tend to over-complicate things.
    i will create one version for the main track, and one for the programming track, and clean out the unnecessary bits of code.

    ~Travis
     
    Scott Eric Catalano and WillemT like this.
  14. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    Would it not be possible to create one version with a switch to change between Main and Program. The only real change is the buffer we use when calling the ISR and the instructions going into it. Even if it is a mechanical switch. I do not think there is a spare pin but it may be possible to check it via the I2C bus, or even an instruction via I2C since that is required in any case.

    The advantage would be that you can operate or program with the same Base Station using the same motor bridge. One will need to check if there is enough memory for both sets of instructions.

    Willem
     
    Last edited: Mar 22, 2018
    Scott Eric Catalano likes this.
  15. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    interesting idea... that would simplify things, as there is only one code set... right now i am stripping down the code to see what it uses for memory in it's most basic form. after that i will know what other features can be added.

    ~Travis
     
    Scott Eric Catalano likes this.
  16. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    i trimmed a bunch of "stuff", and i got it down to be small enough to fit.
    now i just have to build up the I2C commands. i found that I2C support is not available through the standard wire.h library, but i found this: https://playground.arduino.cc/Code/USIi2c
    i will be using the slave library from there, and see where that gets me.

    originally i had botched up the DCCpp library, but i restored it from the original ZIP, and put my ATtiny85 fixes back in. no way to debug it yet, only that it compiles. but i think i have a foundation now.

    ~Travis
     
    Scott Eric Catalano likes this.
  17. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    Have a look here:
    https://github.com/SpenceKonde/ATTinyCore

    It seems to provide most of what may be needed - even UART.

    Willem.
     
    Scott Eric Catalano likes this.
  18. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    currently i have code written for the other library, but i will keep that in mind if this library i have fails. thank you for the link. :D

    hopefully i will get a paycheck pretty soon (seasonal lay-off), so i can order up some ATtiny85s. i kinda feel like just going by if it compiles or not is a rough way to debug. good thing i am not building a commercial airplane. ;)

    ~Travis
     
    Scott Eric Catalano likes this.
  19. WillemT

    WillemT TrainBoard Member

    55
    40
    7
    My link is not a library as such. It is an entire IDE environment extension for the ATTiny family. It provides for configuring and setting the fuses etc for it, while including the routines to handle the SPI, I2C and UART (serial) support. Let us see where you go with what you presently have.

    I know exactly what you mean. I will only be able to order chips myself sometime next week. Also waiting for some money to come in.

    Willem.
     
    Last edited: Mar 23, 2018
  20. Travis Farmer

    Travis Farmer TrainBoard Member

    352
    320
    14
    i have updated my GitHub to reflect my current progress.
    https://github.com/travisfarmer/tinyDCCpp
    it is basically a copy of @Trusty 's library, with some modifications. the INO file i am using is in the examples folder, as I2CDcc.ino, again, using the I2C library i mentioned above.
    i have only programmed for the throttle and cab functions so far, but the command protocol may or may not be visible by reading the loop() function. ;)
    to me, the I2C protocol used in the library i have looks to be different than regular I2C, so i am not sure if it is compatible. if somebody has a ATtiny85, and another board to control it via I2C, i need to know if the protocols are compatible, and if it actually puts out a DCC++ signal... in retrospect, i haven't added commands to turn on track power yet... but i think it should still run the DCC++ signal pin.
    this is the result of several variations, but not set in stone yet. but it does describe the I2C command structure i was trying for. simple bytes, no long ASCII strings of command data. bear in mind, this will NOT interface directly as the original DCC++ library. it is intended to be controlled by another MCU/CPU via I2C, such as a Raspberry Pi, or an ESP32.

    if it doesn't work at all, please kindly say so. ;)

    ~Travis
     
    Scott Eric Catalano likes this.

Share This Page