SPI + rotary encoder = frozen arduino

Hi all, My project involves controlling a quad 16-bit dac through spi from a single rotary encoder. I've spent quite a few hours trying to figure this out but am getting nowhere.

I've got my dac(TI 8555) working just fine using the SPI library, and for the most part have my encoder(ENC1J-D28-L00128L-ND) working fine by itself.

The problem I'm having is that when I combine the code for the dac and the encoder my arduino appears to be freezing after ~4s regardless of whether or not I turn the encoder. I've tried polling the encoder but due to my lack of foresight I purchased an encoder that has too many steps/rev and at moderate-high speeds I begin to lose steps.

I've read that delay() does not work inside of interrupts. This could be a possible hangup if I assume the SPI library must use some kind of delay in order for setClockDivider() to function.

Another thought I had was that interrupts are being temporarily disabled and qued during the function associated with the interrupt. I know that SPI uses some form of interrupt and this also could be causing an issue.

A few other constraints that need to be kept in mind:

I will be adding in a 4x40 char LCD. I've also got this working fine by itself but am worried the time needed to run the SPI code and update the LCD will limit my options using either interrupts or polling of the encoder.

I don't need the dac to be updated for every increment/decrement of the encoder. I do, however, need the encoder position to be updated fairly accurately.

I don't mind doing the leg work on this but a little bit of insight or a kick in the right direction would greatly appreciated. Thanks!

The only thing I can tell you for sure is that you won't get any specific help unless you post your code. Use the # to place it into a code window.

Lefty

I've tried several different takes on this, this just happens to be what I have saved at the moment.

#include <SPI.h>

enum PinAssignments {
  encoderPinA = 2,
  encoderPinB = 3
};

#define SCK 13 //Clock
#define MOSI 11 //MOSI
#define RST 12 

//channel address'
static byte DACA = 0x10;
static byte DACB = 0x12;
static byte DACC = 0x14;
static byte DACD = 0x16;

//Channel values
volatile unsigned int valA, valB, valC, valD; //not in use yet
volatile unsigned int encoderPos;
unsigned int lastReportedPos = 1;

boolean A_set = false;
boolean B_set = false;


void setup(){
  //=====DAC
  DDRB = B00111100; //sets my SPI ports as outputs
  digitalWrite(RST,LOW); //required for the dac
  SPI.begin();
  SPI.setDataMode(SPI_MODE1);
  SPI.setBitOrder(MSBFIRST); 
  SPI.setClockDivider(SPI_CLOCK_DIV4); //I've tried 2-16 w/ the same results
  digitalWrite(RST,HIGH); //Starts all dac channels at 0x00
  
  //=====Encoder
  pinMode(encoderPinA, INPUT); 
  pinMode(encoderPinB, INPUT);
  digitalWrite(encoderPinA, HIGH);  // turn on pullup resistor
  digitalWrite(encoderPinB, HIGH);  // turn on pullup resistor
  attachInterrupt(0, doEncoderA, CHANGE);
  attachInterrupt(1, doEncoderB, CHANGE);
}

void loop(){
  if (lastReportedPos != encoderPos) {
    dacWrite(DACA,encoderPos);
    lastReportedPos = encoderPos;
  }
}

dacWrite function

void dacWrite(byte dacActive, int dacValue){
  PORTB = 0x04; //toggles latch, Pin 10
  SPI.transfer(dacActive); //command byte
  SPI.transfer(highByte(dacValue)); 
  SPI.transfer(lowByte(dacValue));
  PORTB = 0x00; 
}

doEncoder function from the playground.

void doEncoderA(){
  A_set = digitalRead(encoderPinA) == HIGH;
  encoderPos += (A_set != B_set) ? +1 : -1;
}

void doEncoderB(){
  B_set = digitalRead(encoderPinB) == HIGH;
  encoderPos += (A_set == B_set) ? +1 : -1;
}

I've also tried using direct port manipulation for the doEncoder function. I believe retrolefty posted this in a different thread. Obviously the rest of the sketch was changed to use this code. In this setup I was just using a single interrupt to lessen its impact.

void doEncoderA() {
    if (PIND & 0x04) {                              // test for a low-to-high interrupt on channel A, same as if(digitalRead(encoderPinA) == HIGH)
        if ( !(PIND & 0x10)) {                      // check channel B for which way encoder turned, same as if(digitalRead(encoderPinB) == LOW)
           encoderPos = ++encoderPos;               // CW rotation
           PORTD = PIND | 0x40;                     // set direction output pin to 1 = forward, same as digitalWrite(encoderdir, HIGH);
          }
        else {
           encoderPos = --encoderPos;               // CCW rotation
           PORTD = PIND & 0xBF;                      // Set direction output pin to 0 = reverse, same as digitalWrite(encoderdir, LOW);
          }
    }
    else {                                          // it was a high-to-low interrupt on channel A
        if (PIND & 0x10) {                          // check channel B for which way encoder turned, same as if(digitalRead(encoderPinB)==HIGH)
           encoderPos = ++encoderPos;               // CW rotation
           PORTD = PIND | 0x40;                     // Set direction output pin to 1 = forward, same as digitalWrite(encoderdir, HIGH);
           }
        else {
           encoderPos = --encoderPos;               // CCW rotation
           PORTD = PIND & 0xBF;                      // Set direction output pin to 0 = reverse, same as digitalWrite(encoderdir, LOW);
           }
         }
    PORTD = PIND | 0x80;                            //  digitalWrite(encoderstep, HIGH);   generate step pulse high
    PORTD = PIND | 0x80;                            //  digitalWrite(encoderstep, HIGH);   add a small delay
    PORTD = PIND & 0x7F;
    
    dacWrite(DACA,encoderPos);                            //  digitalWrite(encoderstep, LOW);    reset step pulse
}

Another thought I had was that interrupts are being temporarily disabled and qued during the function associated with the interrupt. I know that SPI uses some form of interrupt and this also could be causing an issue.

Yes, once you are inside a ISR all other interrupts are disable. Now having encoder interrupts locked out is no big thing, you would just lose a step now and then, however if your SPI does indeed use interrupts I have no idea what the impact might be for a delayed response to an interrupt due to the encoder ISR being serviced would be.

It is possible to re-enable interrupt inside a active ISR thus allowing nested interrupts, it's not recommended unless you really know what's going on and the impacts it might have. That's not something I have tried to experiment with.

One thing you could do is to cut down on the frequency of encoder interrupts. As you are presently configured you are generating four interrupts for each 'step' of the encoder. Just comment out the attachInterrupt(1, doEncoderB, CHANGE); statement and change
attachInterrupt(0, doEncoderA, CHANGE); to attachInterrupt(0, doEncoderA, FALLING); and then lastly comment out the whole void doEncoderB() function. This should cut down the encoder interrupts by 1/4 while still allowing steps and direction to function.

At least it's a simple quick test to see if it changes the symptoms you are seeing.

Lefty

Another thought I had was that interrupts are being temporarily disabled and qued during the function associated with the interrupt.

Interrupts are queued when interrupts are disabled, but only one for any given interrupt type.
So, it's not like all interrupts eventually get processed. Some get lost if there is another of the same type already waiting to be processed.

I'm a bit embarrassed to say this but my problem was with my SPI function. When I migrated from digitalWrite to port manipulation I neglected to leave the dac's reset pin high. It does seem odd though that it would respond for a few seconds before shutting down.

After getting this straight I decided to trying polling the encoder again and it proved to be more than adequate.

Thanks for the help and clarification.