Saturday, April 24, 2021

0000 0000 0101 1110

Playing Laser Cat

I don't know why but we have two cats. At some points in our family life we have had three cats. I'm sure that there is medication available for this feline collection disease, but in the meantime we can easily distract them from destroying our furniture by using a small laser light playing around the room. They enthusiastically follow it - again, not sure why.

The problem is that I'm lasier (pun intended) than them, so I don't want to be wasting valuable datasheet reading time pretending to be interested in their pursuits. Recently I did some investigating around matching a servo to an ATTiny85 which got me thinking about maybe using two servos in a "pan and tilt" arrangement for this project. The resulting contraption (Cat Laser Toy?) might randomly move the laser around until the cats die are happy.

I researched pan and tilt camera mounts on Thingiverse and settled on the following design.


As I wanted to add some electronics to the project, and also thought a larger base might be more stable, I started by redesigning the base to attach a box underneath. In the end I did separate the box and the top because I could print the box "upside-down" and halve the printing time (and filament usage).

I added a hole on the side for power (which I had to expand in the end to fit the cable) and I also added a hole for laser/servo wires at the top. You can see some of these FreeCad techniques on an earlier blog.



/*
Attiny85 based servo code to drive the cats mad with random laser movement. OneCircuit www.onecircuit.blogspot.com Sunday 11 April 13:31:51 AEST 2021 Servo_ATTinyCore.h is part of the ATTinyCore family found at https://github.com/SpenceKonde/ATTinyCore */ #include <Servo_ATTinyCore.h> Servo myservo; // create servo object int updownservo = PB0; // pin for vertical servo int side2sideservo = PB1; // pin for horizontal servo // limits of movement for horizontal servo int sidelower = 125; int sidemiddle = 90; int sideupper = 55; // limits of movement for vertical servo int updownlower = 135; int updownmiddle = 125; int updownupper = 115; // slow these down for an older cat? int timedelay = 400; int movementdelay = 400; // generic servo moving - with detaching at the end // which eliminates jitter void movetheservo(int movepos, int whichservo) { myservo.attach(whichservo); myservo.write(movepos); delay(movementdelay); myservo.detach(); } void setup() { // a bit of overhead for a "random" number pinMode(A3, INPUT); int readanalog = analogRead(A3); randomSeed(readanalog); // initialise to centre of area movetheservo(updownmiddle, updownservo); movetheservo(sidemiddle, side2sideservo); delay(timedelay); } void loop() { // go find some random position int movevertical = random(updownupper, updownlower); int movehorizontal = random(sideupper, sidelower); // go find a random time to hold position timedelay = random(100, 200); // move the servos movetheservo(movevertical, updownservo); delay(timedelay); movetheservo(movehorizontal, side2sideservo); delay(timedelay); }


The project works well and at least one cat is happier some of the time - so, result?




Saturday, April 17, 2021

0000 0000 0101 1101

DIP8 converter ATTiny13 to PFS154

The ATTiny13 chip has been the mainstay of many of my projects for the last 5 years or so, but lately I have been straying a bit towards the PFS154, particularly now that I have a working programmer and a barely working knowledge of programming the chip.


One barrier to the transition (and there have been many) is the different pin layouts of the chips - like...very different!

In this blog and video I am going to attempt to "re-wire" a DIP8 adapter so that I can use the padauk PFS154 chip in projects designed for AVR chips. That is, I'm going to need to make sure that VCC and GND can be re-routed appropriately as follows.

The process started with a "double-decker" idea, and after a couple of false starts (but no smoke), I was able to achieve the following "FrankenChip" which performed fading duties as desired.


There was only one unexpected outcome, which was the PWM signal being able to "source" (perhaps OUTPUT and LOW?) for the connected LED. More experiments required I think to fully understand that side-effect (see video).

So at the moment this seems like a nice option to use existing projects but with a new chip, at least until I decide to re-imagine some projects, then it will be interesting to see which chip I design the PCB around. Watch this space!




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?






Saturday, April 3, 2021

0000 0000 0101 1011

Why PFS154? Part two...

In a previous blog I celebrated the unlocking of (not one, not two, but...) three independent PWM channels on the Pesky PFS154 Padauk microcontroller. This post will be about the code journey to that point. On the AVR version (ATTiny13) the process was as follows.

1. Choose lower and upper range values for PWM ramping
2. Generate LFSR random numbers
3. Adjust the PWM of each channel
4. Wait a bit
5. Rinse and repeat

The process for the PFS154 is exactly the same, excepting we have three channels and there are some changes to how this is coded and compiled under the FreePDK SDCC toolchain.

I was originally dreading this project because way back when I first achieved a working padauk programmer, I grabbed the FreePDK examples from github, compiled the code (success) and plugged in an LED (failure). No fading as expected.

I eventually (weeks of coding later) "fixed" it by trawling through the back alleys of the interwebs using Chinese to English translation services, reading and re-reading the datasheet and then used my patented "trial and error and error and error and error..." method of programming.

The original FadeLED code (not working):

  PWMG1C = (uint8_t)(PWMG1C_ENABLE | PWMG1C_INVERT_OUT | PWMG1C_OUT_PA4 | PWMG1C_CLK_IHRC);
  PWMG1S = 0x00;                  // No pre-scaler

Without going into the torturous process of elimination, I ended up chucking out the whole "(uint8_t)(PWMG...blah blah" approach (which relies on various *.h files scattered about) and went with the direct accessing of registers in binary. This is my tried and tested preferred approach which, despite the zealots, makes the most sense to me as it is straight from the datasheet.

The code that worked:

  PWMG1C = 0b10000111; // see datasheet


  PWMG1S = 0b00000000; // see datasheet

Once one PWM channel was behaving itself (ramping up and down gently), I turned my attention to the other two channels promised in the datasheet.

Honestly it was a bit of a "cut and paste" hit job and took only a few minutes. The sight of those little LEDs pulsing away in unity was quite a nice reward for all of that brow crinkling.

It was only days later that I realised I got a little lucky as each PWM is matched to a different pin depending on the binary number thrown to PWMG*C where * is either 0,1 or 2. So for instance if I throw PWMG2C = 0b10000111 then the datasheet shows the following "1"s highlighted:

So looking at the table above and the pinout, 0b10000111 enables the PWM (bit 7), 0b10000111 chooses PA3/pin5 (bits 3-1) and 0b10000111 chooses IHRC (Internal High RC oscillator) as the clock source (bit 0). See highlighting.

Similarly for PWMG0C and PWMG1C using 0b10000111 selects PA0 and PA4 as PWM outputs respectively. Clear as mud!

Sidenote: I threw in a PFS173 at this point to see if the chips are interchangeable. They are not - for although the code compiled OK, the registers for the PFS173 for PWM are different to the PFS154 - great!

The last hurdle was to see if the original code from the ATTiny13 candle project would just port across - and sure enough apart from the odd formatting/syntax issue (bool vs boolean as an example), not only did the old code compile for the PFS154, but also it uploaded and produced awesome candlely-goodness on the breadboard.


/*
 -----------------------------------------------------------------
 Description: A fake candle <sigh> running on a PFS154 padauk μC
 connected to 3 leds via three channels, one running fast, one
 medium and one slow.
 
 Author: OneCircuit                    Date: 02/04/2021
 www.onecircuit.blogspot.com
 -----------------------------------------------------------------

*/

#include <stdint.h>
#include <stdlib.h>
#include <pdk/device.h>
#include "auto_sysclock.h"
#include "delay.h"
#include <stdbool.h>

#define LED4_BIT 4
#define LED0_BIT 0
#define LED3_BIT 3

uint16_t myrand = 2901;  // happy birthday

// global variables randomised later for flickering, using the
// "waveslow" and "wavefast" arrays
uint8_t slowcounter = 0;
uint8_t medcounter = 0;
uint8_t fastcounter = 0;
uint8_t slowstart = 0;
uint8_t slowend = 0;
uint8_t medstart = 0;
uint8_t medend = 0;
uint8_t faststart = 0;
uint8_t fastend = 0;
uint8_t faster = 0;

uint8_t waveslow[] = {50, 100, 170, 200};
uint8_t wavemed[] = {40, 120, 140, 220};
uint8_t wavefast[] = {40, 80, 150, 240};

// booleans to keep track of "fading up" or "fading down"
// in each of the slow and fast cycles
bool fastup = true;
bool slowup = true;
bool medup = true;

void mydelay(uint8_t counter) {

  for (uint8_t thiscount = 0; thiscount <= counter; thiscount++) {
    _delay_ms(1);
  }

}

uint16_t gimmerand(uint16_t small, uint16_t big) {
  myrand ^= (myrand << 13);
  myrand ^= (myrand >> 9);
  myrand ^= (myrand << 7);
  return abs(myrand) % 23 * (big - small) / 23 + small;
}

void getnewslow() {
  slowstart = gimmerand(waveslow[0], waveslow[1]);
  slowend = gimmerand(waveslow[2], waveslow[3]);
}

void getnewmed() {
  medstart = gimmerand(wavemed[0], wavemed[1]);
  medend = gimmerand(wavemed[2], wavemed[3]);
}


// initialise a new fast cycle including the new speed of cycle
void getnewfast() {
  faststart = gimmerand(wavefast[0], wavefast[1]);
  fastend = gimmerand(wavefast[2], wavefast[3]);
  faster = gimmerand(1, 4);
}

// Main program
void main() {

  // Initialize hardware
  // Set LED as output (all pins are input by default)
  PAC |= (1 << LED4_BIT) | (1 << LED0_BIT) | (1 << LED3_BIT);

  // see datasheet
  PWMG1DTL = 0x00; 
  PWMG1DTH = 0x00;
  PWMG1CUBL = 0xff; 
  PWMG1CUBH = 0xff;
  PWMG1C = 0b10100111;
  PWMG1S = 0b00000000;

  PWMG0DTL = 0x00;   
  PWMG0DTH = 0x00;
  PWMG0CUBL = 0xff;
  PWMG0CUBH = 0xff;
  PWMG0C = 0b10100111;
  PWMG0S = 0b00000000;

  PWMG2DTL = 0x00; 
  PWMG2DTH = 0x00;
  PWMG2CUBL = 0xff;
  PWMG2CUBH = 0xff;
  PWMG2C = 0b10100111;
  PWMG2S = 0b00000000;

  getnewfast();
  getnewslow();
  getnewmed();
  slowcounter = slowstart;
  fastcounter = faststart;
  medcounter = medstart;

  // Main processing loop
  while (1) {

    // ramp up slow
    if (slowup) {
      slowcounter++;
      if (slowcounter > slowend) { // ramp finished so switch boolean
        slowup = !slowup;
      }
    }
    else {
      // ramp down slow
      slowcounter--;
      if (slowcounter < slowstart) { // ramp finished so switch boolean
        slowup = !slowup;
        getnewslow();
      }
    }

    // ramp up med
    if (medup) {
      medcounter++;
      if (medcounter > medend) { // ramp finished so switch boolean
        medup = !medup;
      }
    }
    else {
      // ramp down med
      medcounter--;
      if (medcounter < medstart) { // ramp finished so switch boolean
        medup = !medup;
        getnewmed();
      }
    }


    // ramp up fast
    if (fastup) {
      fastcounter = fastcounter + faster;
      if (fastcounter > fastend) { // ramp finished so switch boolean
        fastup = !fastup;
      }
    }
    else {
      // ramp down fast
      fastcounter = fastcounter - faster;
      if (fastcounter < faststart) { // ramp finished so switch boolean
        fastup = !fastup;
        getnewfast();
      }
    }
    // delay + a re-purposed random for ramp speeds
    mydelay(6 + faster);

    PWMG1DTL = slowcounter & 255;
    PWMG1DTH = slowcounter;
    PWMG0DTL = fastcounter & 255;
    PWMG0DTH = fastcounter;
    PWMG2DTL = medcounter & 255;
    PWMG2DTH = medcounter;

  }
}

// Startup code - Setup/calibrate system clock
unsigned char _sdcc_external_startup(void) {

  // Initialize the system clock (CLKMD register) with the IHRC, ILRC, or EOSC clock source and correct divider.
  // The AUTO_INIT_SYSCLOCK() macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC clock source and divider.
  // Alternatively, replace this with the more specific PDK_SET_SYSCLOCK(...) macro from pdk/sysclock.h
  AUTO_INIT_SYSCLOCK();

  // Insert placeholder code to tell EasyPdkProg to calibrate the IHRC or ILRC internal oscillator.
  // The AUTO_CALIBRATE_SYSCLOCK(...) macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC oscillator.
  // Alternatively, replace this with the more specific EASY_PDK_CALIBRATE_IHRC(...) or EASY_PDK_CALIBRATE_ILRC(...) macro from easy-pdk/calibrate.h
  AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV);

  return 0;   // Return 0 to inform SDCC to continue with normal initialization.
}

Next I think that I will make up a PCB and shoehorn the beast into a jar with a Solar Panel running the show. Can't wait!