What

Last time around we added wheel odometry sensing using a custom optical encoder disk, debouncing circuitry and two PCF8583 I2C realtime clock chips for counting. The mechanics worked well, but the circuit occasionally misscounted and debouncing signals in hardware is tricky. Another approach is to use a microcontroller and debounce in software. This is what I am now moving to for more reliability.

Contents

Contents

When

Almost exactly one year ago I had made some progress on the wheel odometry sensors of the robot. At the time I wanted to use a more or less discrete circuitry using mostly off-the-shelf components instead of programming an additional microcontroller. It seemed overkill for the application.

While in principle the combination of a LM393 rotary counter board with a PCF8583 I2C realtime clock chip works, it would often over-count. I added an resistor-capacitor (RC)-circuit to debounce, but calibrating that, even with the oscilloscope turned out to be tedious.

A year has passed since and I suddenly felt motivated to solder an attiny2313 circuit to a perfboard. With an ISP pin header we can program the chip in-circuit with the AVR AVRISP mkII programmer. Just like in the early days before Arduino became popular.

Background

Design Goals

To recap I imposed some design goals for this robot in order to ensure

For this I aim for

Even though the circuit for the wheel odometry counter needs to be a little more complex due to the use of an auxilary microcontroller, we will stay true to these design goals. The auxilary microcontroller will communicate with the main microcontroller as an I2C slave.

Hardware

Let’s first have a look at the modifactions I made to the hardware.

Old circuit uses a realtime clock counter

The old circuit we will be replacing using the debouncing circuit, LM393 rotary counter board and PCF8583 I2C realtime clock.

New circuit uses an Attiny2313 as a custom I2C slave

The new board sits in the back of the robot. I’ve removed the two old boards from my previous blog post.

The circuit became a little more complex due to the use of the microcontroller.

We need

But still very manageable.

Wiring

After almost a year I had of course completly forgotton the pin configurations of the old board.

What I usually do is create a drawing in kolour paint and just screenshot all the information I need to have at a glance during soldering.

Tests

On the hardware side we can run test three things immediatly:

Programmable

If all is connected correctly, then the AVR mkII programmer will be able to write software to the chip.

Wheel Ticks

Probing the input pin of the two light barriers with the motors running reveals a clean enough signal the microcontroller shouldn’t have much trouble picking up. The signal is similar for both wheels.

Software

The software side was a little annoying:

With those issues fixed the rest was relatively straight forward.

Script to upload code

To conviniently push code to the microcontroller I use a bash script. It installs everything that is required: the arduino-cli and the attiny core. It then compiles and uploads the sketch. All conveniently from command-line in an easily reprocudable manner.

#!/bin/bash
set -e

# -- enter project directory
cd $HOME/odometry/

# -- get arduino-cli
wget -O $HOME/arduino-cli_1.3.1.tar.gz https://github.com/arduino/arduino-cli/releases/download/v1.3.1/arduino-cli_1.3.1_Linux_64bit.tar.gz
tar -xvf tar -xvf arduino-cli_1.3.1.tar.gz

# -- setup tool paths
ACLI=$HOME/arduino-cli
AVRD=$HOME/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude
ACRDC=$HOME/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/etc/avrdude.conf

# -- add board manager url for attiny
$ACLI config add board_manager.additional_urls http://drazzy.com/package_drazzy.com_index.json

# -- install the core
$ACLI core install ATTinyCore:avr

# -- compile code
$ACLI compile --verbose --fqbn ATTinyCore:avr:attinyx313:chip=2313 software/software.ino

# -- upload with mkii programmer
$ACLI upload --verbose --fqbn ATTinyCore:avr:attinyx313:chip=2313 --programmer avrispmkii software/software.ino

If writing software to the microcontroller works it will look like this in the terminal:

Some additional helpful commands

In order to figure out the command-lines there are some not so intuitive paramters to arduino-cli and avrdude for setting fuses, if required.

Finding the correct part specifiers

In order to figure out the fqbn, after having the board manager setup, we can search for a core to install like this:

# -- search for core
$ACLI core search attiny

and then search for compatible boards within the cores

# -- search compatible board
$ACLI board listall 2313

This yields ATTinyCore:avr:attinyx313. Interestingly, with the ATTinyCore core we the attinyx313 is for the almost identical attiny4313 and attiny2313. We need to use the chip option.

Using the details parameter we can get such options.

# -- search option to select chip
$ACLI board details --fqbn --fqbn ATTinyCore:avr:attinyx313

This prints ATtiny2313/ATtiny2313A with the option chip=2313 as seen in the script above.

Reading and writing fuses

We may want to read the internal clock settings (fuses)

# -- read fuses
$AVRD -C ../etc/avrdude.conf -c avrispmkII -p attiny2313 -v

Set the fuses. There is a convenient online calculator to get the fuse values here [1].

# -- set fuses
$AVRD -C $ACRDC -c avrispmkII -p attiny2313 -U lfuse:w:0x62:m -U hfuse:w:0xDF:m -U efuse:w:0xFF:m -U lock:w:0xFF:m 

If we additionaly connect the TTL pins to the FTDI and use Serial.print(...) in the software we may want to open the serial console.

# -- open serial console
$ACLI monitor -p /dev/ttyUSB0 -c baudrate=115200

Tests

I ran some simple test cases to make sure the circuit behaves as expected. The goal was to check if the chip can be programmed, the clock is configured correctly and that the built-in hardware I2C functionality works.

At some point the bring-up worked out and I got that LED to blink. Measuring the signal at the LED pin revealed the internal oscillator in the micro-controller is correctly configured.

#define F_CPU 1000000UL
#define LED_PIN PB4

#include <avr/io.h>
#include <util/delay.h>

int main (void) {
  DDRB = (1 << LED_PIN); // PB3 output

  while(1) {
    PORTB |= (1 << LED_PIN);  // on
    _delay_ms(500);

    PORTB &= ~(1 << LED_PIN); // off
    _delay_ms(500);
  }
} 

Having the LED blink is an important milestone, because it demonstrated:

I2C Slave Test

The following code set the Attiny2313 to act as an I2C slave.

#define F_CPU 1000000UL
#define LED_PIN PB4

#include <avr/io.h>
#include <util/delay.h>
#include <Wire.h>
 
byte RxByte;
 
void I2C_RxHandler(int numBytes) {
  while(Wire.available()) {
    RxByte = Wire.read();
  }
}
 
void setup() {
  DDRB = (1 << LED_PIN); // PB3 output
  
  Wire.begin(0x55); // slave address 0x55
  Wire.onReceive(I2C_RxHandler);
}
 
void loop() {
    PORTB |= (1 << LED_PIN);  // on
    _delay_ms(100);

    PORTB &= ~(1 << LED_PIN); // off
    _delay_ms(100);
}

In the webinterface of the main processor we can then see an I2C slave show up under the adress 0x55.

Note the wheel counts shown are currently both at a fixed high number due to read failure, because the old wheel odometry sensors are removed and the new one has yet to be programmed.

Complete Firmware

With the basics out of the way it’s time to update the firmware on the main esp32cam processor and the attiny2313 auxiliary odometry controller.

Main Processor

In the code for the main processor I just read 4 consecutive bytes via I2C from the attiny2313.

String CWheelRotaryCounter::read() {
  // --request 4 bytes from I2C slave with adress 0x55
  uint16_t value;
  byte arr[4];
  Wire.requestFrom(0x55, 4); 

  // -- read 4 consecutive bytes
  for(int i = 0; i < 4; i++) {
    arr[i] = Wire.read();
  }
  
  // -- convert to 16bit unsigned integer
  if(m_wheelno == 0) {
    value = arr[0];
    value = (value << 8) | arr[1];
  } else if(m_wheelno == 1) {
    value = arr[2];
    value = (value << 8) | arr[3];
  } else {
    return String(-1);
  }
  return String(value);
}

Note: the value is an unsigned count of the interrupts of the light barrier disc. For odometry in ROS we need to use a signed integer and add/subtract the difference of the previous count depending on the direction the motor in turning in.

Odometry Controller

For the odometry controller I’ve enabled interrupts on for pins PB2 and PB3. In the interrupt handler I can then check if there was a change.

Normally we could use attachInterrupt(digitalPinToInterrupt(2), onWheelTickLeft, RISING); and define a function onWheelTickLeft, but on first try I wasn’t able to get that to work. So I went old fashioned route and set the flags on the registers manually. The variable PCIE isn’t defined, so I checked the datasheet of the attiny2313, saw that it is set to 5 and defined the value myself.

volatile uint16_t m_wheelTickLeftCount = 0;
volatile uint16_t m_wheelTickRightCount = 0;

volatile bool m_lastStateLeft = false;
volatile bool m_lastStateRight = false;

#define PCIE 5

void setup() {

  (..)
  
  // -- set pins for wheel ticks as input
  pinMode(PB2, INPUT);
  pinMode(PB3, INPUT);

  // -- configure external interrupt for PCINT2 (=PB2) and PCINT3 (=PB3)
  PCMSK |= (1<<PCINT2) | (1<<PCINT3);
  MCUCR |= (1<<ISC01) | (1<<ISC00);
  GIMSK |= (1<<PCIE);
}

ISR (PCINT0_vect) {
  bool currentStateLeft = false;
  bool currentStateRight = false;
  
  // -- read current states
  if (PINB & (1 << PCINT2)) {
    currentStateLeft = true;
  }
  if (PINB & (1 << PCINT3)) {
    currentStateRight = true;
  }
  
  // -- if there was a change, increment respective counter
  if(currentStateLeft != m_lastStateLeft) {
    m_wheelTickLeftCount++;
  }
  if(currentStateRight != m_lastStateRight) {
    m_wheelTickRightCount++;
  }
  
  // -- remember current state
  m_lastStateLeft = currentStateLeft;
  m_lastStateRight = currentStateRight;
}

Then to transfer the data via I2C I define a onI2CRequest function that sends converts both 16-bit tick counts to four bytes by bit-shifting and sends them out.

void onI2CRequest() { 
  byte arr[4];
  
  arr[0] = (m_wheelTickLeftCount >> 8) & 0xFF;
  arr[1] = m_wheelTickLeftCount & 0xFF;
  
  arr[2] = (m_wheelTickRightCount >> 8) & 0xFF;
  arr[3] = m_wheelTickRightCount & 0xFF;
  
  Wire.write(arr, 4);
}

In the setup() function we enable the I2C slave functionality with

  Wire.begin(0x55);
  Wire.onRequest(onI2CRequest);

Not much more is required for this to work. When driving the robot around I can now see the wheel_count_left and wheel_count_right changing.

Progress

Conclusion

Roughly a year ago I got the 360 degree lidar working in the robotic operating system (ROS). The SLAM algorithms additionally require wheel odometry. With the wheel odometry now working, I can move on to configuring the SLAM algorithms.


1] http://eleccelerator.com/fusecalc/fusecalc.php?chip=attiny2313