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 objectint updownservo = PB0; // pin for vertical servoint side2sideservo = PB1; // pin for horizontal servo// limits of movement for horizontal servoint sidelower =125;
int sidemiddle =90;
int sideupper =55;
// limits of movement for vertical servoint 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 jittervoidmovetheservo(int movepos, int whichservo) {
myservo.attach(whichservo);
myservo.write(movepos);
delay(movementdelay);
myservo.detach();
}
voidsetup() {
// 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);
}
voidloop() {
// go find some random positionint 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?
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!
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)
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 objectint potpin = A1; // potentiometer adjustment pinint 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 delayint leftbutton = PB4; // pins for button connectionint rightbutton = PB3;
volatile boolean leftpress =false; // which button pressedvolatile 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*/intreadpin() {
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;
elseif (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 μCvoid 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?!
}
elseif (!eepromwritten) {
movetheservo(rightpos); // default if no EEProm
}
delay(100); // settle time, get ready!
}
// left button pressed, short press = change// tracks, long press = adjust servo positionvoid 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 positionvoid 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 accordinglyvoid loop() {
sleep();
if (leftpress) {
goleftbutton();
leftpress =false; // reset button press
}
elseif (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.
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.
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 3uint16_t myrand =2901; // happy birthday// global variables randomised later for flickering, using the// "waveslow" and "wavefast" arraysuint8_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 cyclesbool fastup =true;
bool slowup =true;
bool medup =true;
voidmydelay(uint8_t counter) {
for (uint8_t thiscount =0; thiscount <= counter; thiscount++) {
_delay_ms(1);
}
}
uint16_tgimmerand(uint16_t small, uint16_t big) {
myrand ^= (myrand <<13);
myrand ^= (myrand >>9);
myrand ^= (myrand <<7);
return abs(myrand) %23* (big - small) /23+ small;
}
voidgetnewslow() {
slowstart = gimmerand(waveslow[0], waveslow[1]);
slowend = gimmerand(waveslow[2], waveslow[3]);
}
voidgetnewmed() {
medstart = gimmerand(wavemed[0], wavemed[1]);
medend = gimmerand(wavemed[2], wavemed[3]);
}
// initialise a new fast cycle including the new speed of cyclevoidgetnewfast() {
faststart = gimmerand(wavefast[0], wavefast[1]);
fastend = gimmerand(wavefast[2], wavefast[3]);
faster = gimmerand(1, 4);
}
// Main programvoidmain() {
// 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 loopwhile (1) {
// ramp up slowif (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 medif (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 fastif (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 clockunsignedchar_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);
return0; // 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!