Saturday, April 10, 2021

0000 0000 0101 1100

Servos and Attiny chips

A while ago PileOfStuff was coding a model railway track switch with the aim of semi-automating the process using a servo and a microcontroller.

He started with an ATTiny85 and was using one of my programming PCBs that I sent to him for that purpose. At one point of frustration in the project he abandoned the ATTiny85 in favour of an Arduino Nano and THREW THE PROGRAMMING PCB AWAY!


Well, here in Tasmania the locals went wild! To redress the balance, I was naturally "forced" to spend hours at the bench attempting to reproduce the servo requirements of a model railway track switch with an ATTiny85 using the discarded PCB as a starting point.

The conventional wisdom concerning servo jitter is as follows:

  • buy a more expensive servo you cheapskate!
  • put a decent size (e.g. 470uF) electrolytic capacitor across VCC and GND close to the servo
  • put a toroidal iron core in the circuit for the servo wires to loop around to suppress transients
  • use code suitable for the chip (e.g. this code for ATTiny85)
  • after sweeping the servo, detach the servo to stop jitter
  • adjust time/delays/ISR/something-else™ to "finesse" the jitter
  • twist servo wires together to suppress transients
  • have a separate power supply for the servo (common ground)
  • set the fuse bits to change the clock speed of the Attiny85
  • do the following at some point (?!)
    • cli(); // disable interrupts
    • digitalWrite(ServoPin, HIGH);
    • // use micros() for delay
    • digitalWrite(ServoPin, LOW);
    • sei(); // enable interrupts
  • don't use an ATTiny85

Of all of those options (and I tried most) - I settled on the following approach.

  • Make sure that the ATTiny85 is running 8MHz internal clock (burn bootloader)
  • Slap a 470uF capacitor close to the servo (common practise for me anyway)
  • Use this library for the ATTiny85
  • After each servo sweep use detach and then re-attach the servo
  • Have a separate power supply for the servo

I will admit that there may be a possible problem with detaching the servo for some applications, as the servo may need to resist torque changes depending on the use. In this case, the servo is horizontally deployed with no external impressed forces, it should stay put. Also, a sleeping ATTiny85 doesn't fight back when you change the position anyway, and this thing mostly sleeps!

The setup works fine, and the code is as follows:


/*
   Attiny85 based servo code to adjust then switch tracks on a model
   railway. μC is asleep until button is pressed. If a short press
   (adjusted by variable "buttondelay") then the track just switches. If
   a longpress then user can adjust left and right "limits" for the
   movement.

   The EEProm is employed to "permanently" keep track (pun intended) of
   the right and left positions for the servo. When the adjustments are
   made they are stored and can be retrieved when powered up again.


   OneCircuit  www.onecircuit.blogspot.com
   Sunday 4 April  17:42:57 AEST 2021

*/

// Servo_ATTinyCore.h is part of the ATTinyCore family
// found at https://github.com/SpenceKonde/ATTinyCore
#include <Servo_ATTinyCore.h>

#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <EEPROM.h>

Servo myservo;         // create servo object

int potpin = A1;       // potentiometer adjustment pin

int theaverage = 0;    // average pot readings to reduce servo "jitter"
int leftpos = 256;     // default left position (256 = 45°/180*1023)
int rightpos = 767;    // default right position (767 = 135°/180*1023)
int buttondelay = 600; // longpress delay
int leftbutton = PB4;  // pins for button connection
int rightbutton = PB3;
volatile boolean leftpress = false;  // which button pressed
volatile boolean rightpress = false;
boolean eepromwritten = false;       // have we used EEProm?

int ledpin = PB1;      // debug LED for button pushing indication

/*
  to combat "jitter" read the analog pin 25 times and
  then average the result - this should give the reading
  some "weight" which adds stability
*/
int readpin() {
  for (int i = 0; i < 25; i++) {
    theaverage = theaverage + analogRead(potpin);
  }
  theaverage = theaverage / 25;
  return theaverage;
}

// button pressed! ...but which one?
ISR(PCINT0_vect) {
  if (digitalRead(leftbutton)) leftpress = true;
  else if (digitalRead(rightbutton)) rightpress = true;
}

// do we have values stored? First EEProm location is 
// "0" for no and "1" for yes
boolean checkeeprom() {
  int writ = 0;
  boolean used = false;
  EEPROM.get(0, writ);
  if (writ == 0) used = false;
  if (writ == 1) used = true;
  return used;
}

// standard sleep routines - goodnight μC
void sleep() {

  GIMSK |= _BV(PCIE);                     // Enable Pin Change Interrupts
  PCMSK |= _BV(PCINT3) | _BV(PCINT4);     // Use PB3/4 as interrupt pins
  ADCSRA &= ~_BV(ADEN);                   // ADC off
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);    // replaces above statement

  sleep_enable();                         // Sets the Sleep Enable bit in the MCUCR Register (SE BIT)
  sei();                                  // Enable interrupts
  sleep_cpu();                            // sleep

  cli();                                  // Disable interrupts
  PCMSK &= ~_BV(PCINT3) | _BV(PCINT4);    // Turn off the interrupt pins
  sleep_disable();                        // Clear SE bit
  ADCSRA |= _BV(ADEN);                    // ADC on
  delay(50);                              // setlle
  sei();                                  // Enable interrupts
}

/* 
  map the ADC (0-1023) result to degrees (0-180)
  detaching at the end is a contentious way of
  removing "jitter", but "sleep" effectively
  detaches anyway
*/
void movetheservo(int movepos) {
  myservo.attach(0);
  movepos = map(movepos, 0, 1023, 0, 180);
  myservo.write(movepos);
  delay(600);                     // generous movement time, adjust as you wish
  myservo.detach();
}

void setup() {
  pinMode(potpin, INPUT);
  pinMode(ledpin, OUTPUT);        // debug LED to output
  eepromwritten = checkeeprom();
  
  if (eepromwritten) {            // if data is present, recall it
    EEPROM.get(2, leftpos);
    EEPROM.get(4, rightpos);
    movetheservo(rightpos);       // or left, you choose?!
  }
  else if (!eepromwritten) {
    movetheservo(rightpos);       // default if no EEProm
  }
  delay(100);                     // settle time, get ready!
}

// left button pressed, short press = change
// tracks, long press = adjust servo position
void goleftbutton() {
  // debug LED flashing
  digitalWrite(PB1, HIGH);
  delay(50);
  digitalWrite(PB1, LOW);
  delay(50);
  digitalWrite(PB1, HIGH);
  delay(50);
  digitalWrite(PB1, LOW);
  delay(buttondelay);
  while (digitalRead(leftbutton) == HIGH) {
    theaverage = readpin();
    leftpos = theaverage;
    movetheservo(leftpos);
    EEPROM.put(2, leftpos);
    if (!eepromwritten) {
      EEPROM.put(0, 1);
      eepromwritten = true;
    }

  }
  movetheservo(leftpos);
}

// right button pressed, short press = change
// tracks, long press = adjust servo position
void gorightbutton() {
  // debug LED flashing
  digitalWrite(PB1, HIGH);
  delay(50);
  digitalWrite(PB1, LOW);
  delay(buttondelay);
  while (digitalRead(rightbutton) == HIGH) {
    theaverage = readpin();
    rightpos = theaverage;
    movetheservo(rightpos);
    EEPROM.put(4, rightpos);
    if (!eepromwritten) {
      EEPROM.put(0, 1);
      eepromwritten = true;
    }
  }
  movetheservo(rightpos);
}

// sleep little one, and if button pressed then 
// check which one and react accordingly
void loop() {
  sleep();
  if (leftpress) {
    goleftbutton();
    leftpress = false;     // reset button press
  }
  else if (rightpress) {
    gorightbutton();
    rightpress = false;    // reset button press
  }
}

Sketch uses 2712 bytes (33%) of program storage space. Maximum is 8192 bytes.

Global variables use 48 bytes (9%) of dynamic memory, leaving 464 bytes for local variables. Maximum is 512 bytes.

Tasmanian honour restored?






No comments:

Post a Comment