Computer interfacing challenge

Hi everyone,

I am trying to interface a physical knob interface with my computer using an Arduino. I am using external interrupts to capture encoder pulses from the knob (which measure its rotation), and keep an internal count of these for transfer to the PC via serial. After each transmission I reset the count, and keep a true count on the PC side (a Java program.)

The knob also has a motor (i.e., it can turn itself). Serial comes in from the PC occasionally to dictate the desired force of this motor.

As you can see I have a closed loop where the knob is both an input and output device, and serial transfer to and from the PC is what keeps things in sync. The behaviour of the knob is regulated entirely by the PC on the basis of incoming encoder data and outgoing force data. I have a few problems with my code that I can’t resolve, and would love some guidance:

  • When the knob turns at high enough speeds (generally when the motor turns it) the interrupts that handle encoder pulses dominate the rest of the program, precluding serial transfer to/from the PC. To handle this, I disable interrupts from within themselves if the encoder count gets to high without a transfer. This, of course, causes me to lose encoder pulses as data is sent and received from the PC.

  • For maximum performance, I want to send data to the PC every time an encoder pulse is received. The problem is, data can’t possibly be sent via serial as fast as the interrupts are activated, so I have to send a “sum” of encoder pulses less regularly. This makes the data sent over less than 100% up-to-date, which means it’s hard to program cool effects into the knob with high fidelity (such as for simulating springs). All of this was possible before when it was a parallel interface.

So really, I need a way to force data through serial as frequently and as quickly as possible. Can anyone suggest some optimizations to my code that might result in some improvements?

void timedCallback() {
  // receive
  while (Serial.available() > 0) {
    byte b = Serial.read();
    if (b == 'n')  // right
      direction = 1;
    else if (b == 'p') { // left 
      direction = 0;
    }
    else  {
      TDes = b;
    }
    
        if (direction) {
      digitalWrite(9, LOW);
      digitalWrite(8, HIGH);
    }
    else {
      digitalWrite(9, HIGH);
      digitalWrite(8, LOW);
    }
    analogWrite(10, TDes);
  }
  
  // send
  if (delta != 0) {
    if (delta > 250) delta = 250;
    if (delta < -250) delta = -250;
    Serial.print(delta < 0 ? 'n' : 'p');
    if (delta < 0) {
      delta *= -1;
    }

    byte d = byte(delta);
    Serial.print(d);
    delta=0;
    
  }

  enableAgain = true;
}

void setup() {
  
  delta = 0;
  
  Serial.begin(115200);

  pinMode(2, INPUT); //encoder A input, interrupt pin
  pinMode(3, INPUT); //encoder B input, interrupt pin

  pinMode(8, OUTPUT); //H-bridge enable, PWM pin
  pinMode(9, OUTPUT); //H-bridge direction 1 pin
  pinMode(10, OUTPUT); //H-bridge direction 2 pin
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);

  attachInterrupt(0, isr_onencoderpulseA, CHANGE);
  attachInterrupt(1, isr_onencoderpulseB, CHANGE);

  a_state = digitalRead(2);
  b_state = digitalRead(3);

  TCCR1B &= 0b11111000; //Zero out the first three bits of the register
  int prescalerVal = 1; //disables prescaler, timer runs at 32kHz
  TCCR1B |= prescalerVal; //Sets the register*/
 
 MsTimer2::set(1, timedCallback);
 MsTimer2::start();

}

void loop() {
    time = millis();
  
    if (enableAgain) {
      enableAgain = false;
      attachInterrupt(0, isr_onencoderpulseA, CHANGE);
      attachInterrupt(1, isr_onencoderpulseB, CHANGE);
    }
  
}


/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Interrupt Service
 Routines ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
void isr_onencoderpulseA() {
  delta += ((bitRead(PIND,2)^bitRead(PIND,3)) << 1) - 1;
  if (delta > 200 || delta < -200) {
    detachInterrupt(0);
    detachInterrupt(1);
  }
}

void isr_onencoderpulseB() {
  delta -= ((bitRead(PIND,2)^bitRead(PIND,3)) << 1) - 1;
  if (delta > 200 || delta < -200) {
    detachInterrupt(0);
    detachInterrupt(1);
  }
}

The digitalWrite function is relatively slow. You could speed things up using direct port manipulation:

http://www.arduino.cc/en/Reference/PortManipulation

Couple of thoughts...

Why are you setting up Timer1 for no prescaling (and then a comment that it runs at 32kHz, which it doesn't)? I don't see Timer1 being used anywhere.

I see another potential issue: timedCallback() is setup through MsTimer2 to execute when Timer2 overflow interrupt is thrown. timedCallback() is a relatively lengthy function call, and while it is being run interrupts are disabled. Thus all the encoder pulse interrupts that might occur while you're trying to push data off-chip are being missed (except for the trivial first one that sets the flag and gets serviced when timedCallback() finishes).

I would suggest instead that you stick to using interrupts for encoder pulses only. Handle communications in the main loop without resorting to interrupts.

Next step: you could move some smarts onto the Arduino. Rather than have it report a delta that is constantly re-zeroed, instead track the relative encoder position as a long int. Then your main program could communicate in a "target" value. Arduino could decide whether to run your motor fwd or rev to chase the target position on it's own. You'd no longer need to stream out the encoder delta's to decide externally when to switch direction.

Being a hardware sort of chap I would enlist the help of a few extra chips:- http://www.thebox.myzen.co.uk/Workshop/Rotary_Max.html

Thanks for your replies, everyone. I will give some of your suggestions a try.

The reason I am using interrupts for the serial I/O component is that at high speeds, external interrupts literally dominate and nothing in the main loop gets to run. The only way (that I can see) to ensure that data gets sent and received once in a while in those cases is to disable interrupts temporarily. I realize that this means that encoder pulses will be missed, but I'm not sure what else to do...I think I may have to change my architecture somewhat and move some smarts to the Arduino, as Mitch_CA suggests...I'll try the direct port manipulation as well to save a few more clock cycles.

Thanks again!