ATTiny45 Quadrature Decoder

Renoster has 4 Lynxmotion GHM-15 gear head motors each with a hall effect quadrature encoder. The quadrature encoders outputs two square waves 90 degrees apart (see Fig. 1) as the motor turns. Using these signals one can determine the speed and direction of the motor and from that the robots relative movement.

Acquiring the encoded signals can be a challenge. Each encoder requires two free IO lines on your MCU, since the signals can trigger relatively quick it is preferred that at least one of the inputs is an interrupt. Renoster's REV 2.3 controller board made use of an IO extender to read these, unfortunately several things conspired together to make this a less than ideal solution. To overcome these challenges the TinyQED was created.

TinyQED is a simple ATTiny45 based widget that counts the signals from the encoders and makes them available via I2C.

Sparkfun's BatchPCB service was used to create the PCB.

Implementing the I2C slave interface was a bit of a challenge but as with everything AVR, AVRFreaks had the answer in a thread where Dan Gates implemented I2C slave code from Don Blake, which formed the starting point for TinyQED.


AVR Studio Project Files: TinyQED.zip

  1. /***************************************************************************
  2. * File : TinyQED
  3. * Compiler : AVRstudio 4
  4. * Revision : 1.0
  5. * Date : Tuesday, June 11, 2007
  6. * Revised by : Adriaan Swanepoel
  7. * : Adapted from Dan Gates I2C analogue slave
  8. * and Jim Remington's Quadrature encoder for the O
  9. *
  10. * Target device : ATtiny45
  11. *
  12. * Connections
  13. * ATTiny45
  14. * +--------------------------------+
  15. * | 1 pb5 reset VCC 8 | VCC
  16. * Motor A | 2 pb3 pb2 7 | SCL
  17. * Motor B | 3 pb4 pb1 6 |
  18. * GND | 4 GND pb0 5 | SDA
  19. * +--------------------------------+
  20. ****************************************************************************/
  21.  
  22. #include <stdlib.h>
  23. #include <avr/io.h>
  24. #include <avr/interrupt.h>
  25. #include <avr/eeprom.h>
  26. #include "usiTwiSlave.h"
  27.  
  28. #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
  29. #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
  30.  
  31. union doublebyte
  32. {
  33. unsigned int value;
  34. unsigned char bytes[2];
  35. };
  36.  
  37. union doublebyte enc_pos;
  38. unsigned char enc_dir;
  39. unsigned char enc_last=0;
  40. unsigned char enc_now;
  41. unsigned char EEMEM slaveaddress = 0x36; //Default address
  42.  
  43. /******************************************************************************
  44.  *
  45.  *
  46.  *
  47.  * Description:
  48.  * ARGS: none
  49.  * RETURN: none
  50.  *
  51.  *****************************************************************************/
  52. ISR (PCINT0_vect)
  53. {
  54. enc_now = (PINB & (3 << 3)) >> 3; //read the port pins and shift result to bottom bits
  55. enc_dir = (enc_last & 1) ^ ((enc_now & 2) >> 1); //determine direction of rotation
  56.  
  57. if(enc_dir == 0)
  58. enc_pos.value++;
  59.  
  60. else
  61.  
  62. enc_pos.value--; //update encoder position
  63.  
  64. enc_last = enc_now; //remember last state
  65. }
  66.  
  67. /******************************************************************************
  68.  *
  69.  * setaddress
  70.  *
  71.  * Description: Updates the I2C address in the internal eeprom
  72.  * ARGS: The new I2C address
  73.  * RETURN: none
  74.  *
  75.  *****************************************************************************/
  76. void setaddress(unsigned char address)
  77. {
  78. eeprom_write_byte(&slaveaddress, (uint8_t*)address);
  79. }
  80.  
  81. /******************************************************************************
  82.  *
  83.  * readaddress
  84.  *
  85.  * Description: Reads the preprogrammed I2C address from the internal eeprom
  86.  * ARGS: none
  87.  * RETURN: The devices I2C address
  88.  *
  89.  *****************************************************************************/
  90. unsigned char readaddress()
  91. {
  92. return eeprom_read_byte((uint8_t*)&slaveaddress);
  93. }
  94.  
  95. /******************************************************************************
  96.  *
  97.  * main
  98.  *
  99.  * Description: Where it all starts...
  100.  * ARGS: none
  101.  * RETURN: none
  102.  *
  103.  *****************************************************************************/
  104. int main(void)
  105. {
  106. unsigned char temp;
  107. enc_pos.value = 0;
  108.  
  109. PCMSK |= (1 << PCINT3); // tell pin change mask to listen to pin2
  110. GIMSK |= (1 << PCIE); // enable PCINT interrupt in the general interrupt mask
  111.  
  112. sei();
  113.  
  114. cbi(DDRB, DDB3); // PB3 set up as input
  115. cbi(DDRB, DDB4); // PB4 set up as input
  116. sbi(PORTB, PB3); // Set PB3 internal pullup
  117. sbi(PORTB, PB4); // Set PB4 internal pullup
  118.  
  119. usiTwiSlaveInit(readaddress());
  120.  
  121. for (;;)
  122. {
  123. if (usiTwiDataInReceiveBuffer())
  124. {
  125. temp = usiTwiReceiveByte();
  126.  
  127. //which register requested
  128. //1..9 Reserved Commands
  129. //10..19 Reserved Data
  130. switch (temp)
  131. {
  132. //Reset the counter
  133. case 1 : enc_pos.value = 0;
  134. break;
  135.  
  136. //Center counter value
  137. case 2 : enc_pos.value = 32767;
  138. break;
  139.  
  140. //Set address
  141. case 3 : setaddress(usiTwiReceiveByte());
  142. break;
  143.  
  144. //Send the counter
  145. case 10: usiTwiTransmitByte(enc_pos.bytes[0]);
  146. usiTwiTransmitByte(enc_pos.bytes[1]);
  147. break;
  148.  
  149. default : //Do nothing
  150. break;
  151. }
  152. }
  153.  
  154. asm volatile ("NOP"::);
  155. }
  156. }