netzstaub

beatz & funkz

Friday, August 22, 2008

AVR GUI framework

On my MIDI controller, I have 5 encoders that can work as push buttons as well, and an additional push button. I wanted to have a quick way to access GUI parameters (which button is pressed, which encoder was turned). I designed a small GUI framework to do that. The rotary encoders send out a digital phase code using two wires, A and B. Depending on which line is pulled down first, one can access in which direction the encoder was turned. The push-button just provide a high value when up, and a low value when down. All these digital lines are connected to two 74hc165 which allow the atmel to read them out using a serial connection. The 165s are polled in a timer interrupt routine which runs at 500 hz, which sounds like a pretty slow polling, but it’s actually largely enough for the quickest knob turns I manage to make. The result of polling the 165s is a 16 bit value that contains the status of the encoder A and Bs (5 * 2 bits) and the status of the 6 buttons (6 bits). The interrupt routine also updates a 16 bit counter called timer2_slowclock used to time things like flashing the display or how long a button can be pushed. The routines handle_buttons and handle_encoders are the actual framework and interpret the hardware data into something usable by my application code. The button handling code has to be quick and goes before clear_buttons. That’s because some button events are edge-triggered (for example BUTTON_PRESSED or BUTTON_RELEASED). Most of it just sets flag that the main loop then interprets. Finally, the current button status is cleared to reset it for the next hardware polling.

ISR(TIMER2_OVF_vect) {
  timer2_slowclock++;

  uint8_t but = 0;
  uint16_t srenc  = 0;;
  static uint16_t srenc_old = 0;
  
  asm volatile("rcall sr165_read16" "\n\t"
	       "mov %0, r25" "\n\t"
	       "movw %A1, r24" "\n\t"
	       : "=r" (but), "=w" (srenc) : : "r24", "r25");

  /* main button */
  handle_buttons(but);
  handle_encoders(srenc_old, srenc);
  srenc_old = srenc;

  /* button code comes here */

  clear_buttons();
}

The framework is based around two data structures that represent the current status of an encoder and the current status of a button.

typedef struct button_s {
  uint8_t status;
  uint16_t press_time;
  uint16_t last_press_time;
} button_t;

extern volatile button_t buttons[NUM_BUTTONS];

typedef struct encoder_s {
  int8_t normal;
  int8_t shift;
  int8_t button;
  int8_t button_shift;
} encoder_t;

extern volatile encoder_t encoders[NUM_ENCODERS];

The status of a button is represented as a bit field stored in the status variable. The flags for it are:

#define B_BIT_CURRENT      0
#define B_BIT_OLD          1
#define B_BIT_PRESSED_ONCE 2
#define B_BIT_DOUBLE_CLICK 3
#define B_BIT_CLICK        4
#define B_BIT_LONG_CLICK   5

I use a range of macros to access these fields (mostly I played with different memory representations of the button status to save on space.

#define BUTTON_DOWN(button)           (!(B_CURRENT(button)))
#define BUTTON_UP(button)             (B_CURRENT(button))
#define OLD_BUTTON_DOWN(button)       (!(B_OLD(button)))
#define OLD_BUTTON_UP(button)         (B_OLD(button))
#define BUTTON_PRESSED(button)        (OLD_BUTTON_UP(button) && BUTTON_DOWN(button))
#define BUTTON_DOUBLE_CLICKED(button) (B_DOUBLE_CLICK(button))
#define BUTTON_LONG_CLICKED(button)   (B_LONG_CLICK(button))
#define BUTTON_CLICKED(button)        (B_CLICK(button))
#define BUTTON_RELEASED(button)       (OLD_BUTTON_DOWN(button) && BUTTON_UP(button))
#define BUTTON_PRESS_TIME(button)     (clock_diff(B_PRESS_TIME(button), timer2_slowclock))

I access the button status in the following way (inside the interrupt routine, before clear_buttons):

if (BUTTON_PRESSED(0)) {
   set_flash_string("BUTTON 0", "PRESSED");
}

It is important to not do anything CPU intensive in the button handling code. set_flash_string just copies a string into the flash-text buffer.

The encoder status is a relative number that is added to in each polling loop until is cleared by the main application. It can be both positive (right turn) or negative (left turn). Also, I differentiate between different button statuses while incrementing the encoder counters. Normal is when nothing is pressed and the encoder is turned. Shift is when the encoder is turned while the Shift button is pressed. Button is when the encoder is pressed and then turned, while button_shift is for when both Shift and the encoder itself are pressed.

The encoder data is accessed like this in the main routine:

static int16_t value = 0;
cli();
value += ENCODER_NORMAL(0);
clear_encoders();
sei();

The encoder handling code has to be executed while interrupts are disabled in order to not interfere with the hardware polling code. Thus it has to be quick as well. Most of the time the encoder values are read to update variables. Once all the encoders have been handled, clear_encoders is called to set all the encoder counters to 0.

Let’s have a look at the actual hardware handling code (this is the C version, I actually use an optimised assembler version):

void handle_buttons(uint8_t but) {
  uint8_t but_tmp = but;
  uint8_t i;
  but_tmp >>= 2;
  for (i = 0; i < NUM_BUTTONS; i++) {
    STORE_B_CURRENT(i, IS_BIT_SET8(but_tmp, 0));

    if (BUTTON_PRESSED(i)) {
      B_PRESS_TIME(i) =  timer2_slowclock;

      if (B_PRESSED_ONCE(i)) {
	uint16_t diff = clock_diff(B_LAST_PRESS_TIME(i), B_PRESS_TIME(i));
	if (diff < DOUBLE_CLICK_TIME) {
	  SET_B_DOUBLE_CLICK(i);
	  CLEAR_B_PRESSED_ONCE(i);
	}
      } else {
	B_LAST_PRESS_TIME(i) = B_PRESS_TIME(i);
	SET_B_PRESSED_ONCE(i);
      }
    }

    if (BUTTON_DOWN(i) && B_PRESSED_ONCE(i)) {
      uint16_t diff = clock_diff(B_LAST_PRESS_TIME(i), timer2_slowclock);
      if (diff > LONG_CLICK_TIME) {
	SET_B_LONG_CLICK(i);
	CLEAR_B_PRESSED_ONCE(i);
      }
    }

    if (BUTTON_UP(i) && B_PRESSED_ONCE(i)) {
      uint16_t diff = clock_diff(B_LAST_PRESS_TIME(i), timer2_slowclock);
      if (diff > LONG_CLICK_TIME) {
	CLEAR_B_PRESSED_ONCE(i);
      } else if (diff > DOUBLE_CLICK_TIME) {
	CLEAR_B_PRESSED_ONCE(i);
	SET_B_CLICK(i);
      }
    }

    but_tmp >>= 1;
  }
}

This is not so complicated as it looks. We first skip the 2 encoder bits (remember, 10 bits of encoder data and 6 bits of button data). The for each button, we store the current status (if the button is up or down). If the button was pressed (that means it was up before and down now), we record the time it was pressed by reading timer2_slowclock. If it has already been pressed once, and the second press was a short time after the first press (shorter than DOUBLE_CLICK_TIME), we record a double click by clearing the press flag and setting the double click flag. Else, we record that this is the first press.

If the button is held down and has already been pressed, we check if the time of “holding down” is long enough to qualify as a long clck. If it is, we set the appropriate flag. If the button is released and was pressed once, we record it as a “clicked” event. The different between clicked and pressed is that you have to use clicked when you have both a short press and a long press function for the knob.

We finally shift the button status and go on about handling the next button.

The encoder code is pretty similar in approach. We loop through the encoder bits, check if the knob was turned left or right, and record the offset according to the current button status. That’s why handle_encoders is called after handle_buttons.

void handle_encoders(uint16_t srold_tmp, uint16_t sr_tmp) {
  uint8_t i;

  for (i = 0; i < NUM_ENCODERS; i++) {
    if (IS_BIT_SET8(sr_tmp, 0) == !IS_BIT_SET8(srold_tmp, 0)) {
      volatile int8_t *val = &(ENCODER_NORMAL(i));
      if (BUTTON_DOWN(i)) {
	if (BUTTON_DOWN(SHIFT_BUTTON))
	  val = &(ENCODER_BUTTON_SHIFT(i));
	else
	  val = &(ENCODER_BUTTON(i));
      } else if (BUTTON_DOWN(SHIFT_BUTTON)) {
	val = &(ENCODER_SHIFT(i));
      }

      if (IS_BIT_SET8(sr_tmp, 1) == IS_BIT_SET8(sr_tmp, 0)) {
	if (*val < 64)
	  (*val)++;
      } else {
	if (*val > -64)
	  (*val)--;
      }
    }
    sr_tmp >>= 2;
    srold_tmp >>= 2;
  }
}

For each encoder (2 bits of status), we check if there is an edge on the A wire (either positive or negative edge). We then update a pointer val according to the current pressed buttons, so that val points at either normal, shift, button or button_shift in the encoder structure. We then check if B is has changed to determine if it’s a right turn or left turn. We then update the value, and make sure it doesn’t go above 64 or below -64. We then shift the status, et voila 🙂

posted by manuel at 12:45 am  

4 Comments »

  1. […] recorded first by Potemkhris on 2008-10-12→ netzstaub: AVR GUI framework […]

    Pingback by Recent URLs tagged Asm - Urlrecorder — October 12, 2008 @ 4:31 pm

  2. […] | user-saved public links | iLinkShare 4 votesAVR GUI framework>> saved by pminaa 1 days ago2 votesDVD Encoders>> saved by jonlowder 19 days ago2 votesWEB […]

    Pingback by User links about "encoders" on iLinkShare — October 18, 2008 @ 11:48 am

  3. […] The rackets: Doctors Wherewith The Interpret … Saved by trespuntocatorce16 on Wed 04-3-2009 netzstaub: AVR GUI framework Saved by nielux on Tue 03-3-2009 Envisioning Information Saved by NCLegoes on Wed 25-2-2009 The […]

    Pingback by Recent Links Tagged With "interpret" - JabberTags — March 13, 2009 @ 1:34 pm

  4. […] saved by dedalusjmmr | 5 days ago Baby L. is here! First saved by paulwoods | 10 days ago netzstaub: AVR GUI framework First saved by retarded0and0mental | 10 days ago ChucK meets Csound First saved by barnabas | […]

    Pingback by Recent Faves Tagged With "variables" : MyNetFaves — March 14, 2009 @ 2:50 am

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress