/* gcc402-heartlight
* main.c
*
* Created: 3/24/2026
* Author : Sherman Stebbins
*
* Updated 3/25/2026 - reworked due to timing conflicts. Used some ai
* (chatgpt) but had to fix several areas of that.
* built state machine
* now just need to add more functions.
* Updated 4/19/2026 - added 2 new modes, MODE_IDLE_REVERSE and MODE_IDLE_BOTH
* and added new fuction idleStepReverse(). I use the idleStep and
* idleStepReverse for MODE_IDLE_BOTH.
*
* This is from:
* ATtiny402-5pin-pwm-analog-out.c
*
* I deleted all the analog stuff, If I do any it will be sudo pwm.
*
* Works Great..
*
*/
#define F_CPU 1250000UL
//#define F_CPU 5000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include <util/delay.h>
#define P3 PIN3_bm
#define P6 PIN6_bm
#define P7 PIN7_bm
#define P1 PIN1_bm
//3000000UL = about 25 seconds
#define MODE_DURATION 300000UL // adjust this for time on (65000=about 4sec)
#define IDLE_STEP_MAX 400 //changes speed of steps lower faster //300
void ledOn(uint8_t light);
void ledsOff(void);
void idleStep(void);
void idleStepReverse(void);
void heartbeatStep(void);
uint8_t gamma8(uint8_t x);
void allLed(void);
void allLedPwm(void);
void twinkleStep(void);
void goToSleep(void);
volatile uint32_t modeTicks = 0;
volatile uint8_t startflag = 0;
volatile uint8_t stepLoopFlag = 1;
static volatile int8_t idleStepLed = 0;
static volatile uint16_t idleStepLoops = 0;
volatile uint8_t isSleeping = 0;
volatile uint8_t globalBrightness = 0; // 0–255 for sudo pwm
// ================= LED TABLES =================
const uint8_t led_dir[12] = {
( P3 | P1), //LED - 1
( P3 | P1), //LED - 2
( P7 | P1), //LED - 3
( P7 | P1), //LED - 4
( P1 | P6), //LED - 5
( P1 | P6), //LED - 6
( P3 | P7), //LED - 7
( P3 | P7), //LED - 8
( P6 | P3), //LED - 9
( P6 | P3), //LED - 10
( P6 | P7), //LED - 11
( P6 | P7) //LED - 12
};
const uint8_t led[12] = {
( P3 ), //LED 0 1
( P1 ), //LED 1 2
( P7 ), //LED 2 3
( P1 ), //LED 3 4
( P6 ), //LED 4 5
( P1 ), //LED 5 6
( P7 ), //LED 6 7
( P3 ), //LED 7 8
( P6 ), //LED 8 9
( P3 ), //LED 9 10
( P6 ), //LED 10 11
( P7)}; //LED 11 12
// ================= STATE MACHINE =================
typedef enum {
MODE_IDLE = 0,
MODE_IDLE_REVERSE,
MODE_IDLE_BOTH,
MODE_HEARTBEAT,
MODE_ALL,
MODE_TWINKLE,
//MODE_SLEEP,
MODE_MAX
} systemMode_t;
volatile systemMode_t mode = MODE_IDLE;
// ================= MAIN =================
int main(void)
{
CCP = 0xD8;
//CLKCTRL_MCLKCTRLB = 0b00000011; //5mHz
CLKCTRL_MCLKCTRLB = 0b00000111; //1.25mHz
while (CLKCTRL.MCLKSTATUS & CLKCTRL_SOSC_bm){;}//wait for clock change
PORTA.DIR = 0b11111011; //set ports to output accept PA2 (mode button)
PORTA.PIN2CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
sei();
// shows 1 loop of lights as test on boot:
for(int8_t i = 11; i >= 0; i--){
ledsOff();
ledOn(i);
_delay_ms(80);
}
// ================= MAIN LOOP =================
while (1){
switch(mode){
case MODE_IDLE:
idleStep();
break;
case MODE_IDLE_REVERSE:
idleStepReverse();
break;
case MODE_IDLE_BOTH:
if(stepLoopFlag==1){
idleStep();
//startflag=1;
//stepLoopFlag=2;
}else{
//startflag=0;
idleStepReverse();
//stepLoopFlag=1;
}
break;
case MODE_HEARTBEAT:
heartbeatStep();
break;
case MODE_ALL:
allLed();
break;
case MODE_TWINKLE:
twinkleStep();
break;
default:
mode = MODE_IDLE;
break;
}
// --- time tracking ---------------------------
modeTicks++;
//_delay_us(100); //with 65000 on ticks take about 10sec. to jerky
_delay_us(10); //with 65000 on ticks take about 4sec.
//sleep logic:
if( modeTicks >= MODE_DURATION){
isSleeping = 1;
goToSleep();
modeTicks=0;
stepLoopFlag=0;
startflag=1;
continue;
}
}
}
// ================= LED CONTROL =================
void ledOn(uint8_t light){
PORTA.DIRSET = led_dir[light];
PORTA.OUT = led[light];
}
void ledsOff(void){
PORTA.DIRCLR = (P3|P6|P7|P1);
}
// ================= STEP FUNCTIONS =================
// --- IDLE: slow walking LED ---
void idleStep(void){
if(idleStepLoops++>IDLE_STEP_MAX){
idleStepLoops=0;
ledsOff();
ledOn(idleStepLed++);
if(idleStepLed >= 12){
if(mode==MODE_IDLE_BOTH){
startflag=1;
stepLoopFlag = 2; //flag that idleStep did one full cycle set to
//next one
idleStepLed = 11;
}else{
idleStepLed = 0;
}
}
}
//_delay_us(50);
}
// --- IDLE: slow walking LED REVERSE ---
void idleStepReverse(void){
if(startflag){
idleStepLoops=IDLE_STEP_MAX;
idleStepLed=11;
startflag=0;
}
if(idleStepLoops-- == 0){
idleStepLoops=IDLE_STEP_MAX;
ledsOff();
ledOn(idleStepLed);
idleStepLed--;
if(idleStepLed < 0){
if(mode == MODE_IDLE_BOTH){
idleStepLed=0;
stepLoopFlag = 1; //flag that idleStepReverse did one full cycle
//set to next one
}else{
idleStepLed = 12-1;
}
}
}
//_delay_us(250);
}
// --- ALL ON: fast sweep ---
void allLed(void){
static uint8_t i = 0;
ledOn(i++);
if(i >= 12) i = 0;
//_delay_us(30);
}
// --- ALL ON PWM for heartbeat
void allLedPwm(void){
static uint8_t i = 0;
static uint8_t pwmCounter = 0;
pwmCounter += 2;
if(globalBrightness > pwmCounter){
ledOn(i);
} else {
ledsOff();
}
i++;
if(i >= 12) i = 0;
}
// --- MODE_HEARTBEAT ---
void heartbeatStep(void){
static uint16_t t = 0;
static uint16_t slow = 0;
// ALWAYS run PWM
allLedPwm();
// slow down animation only
if(++slow < 50) return;
slow = 0;
// wrap cleanly
t++;
if(t >= 550) t = 0; //change t for time change or pace default 800
// heartbeat curve
if(t < 200){
globalBrightness = (t / 2); // ramp up
}
else if(t < 300){
globalBrightness = (100 - (t - 200)); // quick drop
}
else if(t < 400){
globalBrightness = ((t - 300) / 2); // second bump
}
else if(t < 550){
globalBrightness = (75 - (t - 400) / 2); // fade out
}
else{
globalBrightness = 0; // short pause
}
}
// gamma for heartbeat
uint8_t gamma8(uint8_t x){
return (x * x) >> 8;
}
// --- TWINKLE RANDOM ---
void twinkleStep(void){
static uint8_t randLight;
static uint8_t randTime = 0;
if(randTime == 0){
randLight = rand() % 12;
randTime = (rand() % 180) + 10;
ledOn(randLight);
}
//_delay_us(200);
if(--randTime == 0){
ledsOff();
}
}
// ============= SLEEP ===================
void goToSleep(void){
ledsOff();
PORTA.DIRSET = 0;
PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
PORTA.PIN1CTRL = PORT_PULLUPEN_bm;
PORTA.PIN7CTRL = PORT_PULLUPEN_bm;
PORTA.PIN6CTRL = PORT_PULLUPEN_bm;
// clear any pending interrupt before sleeping
PORTA.INTFLAGS = PORT_INT2_bm;
SLPCTRL.CTRLA = SLPCTRL_SMODE_PDOWN_gc | SLPCTRL_SEN_bm;
asm("sleep");
SLPCTRL.CTRLA &= ~SLPCTRL_SEN_bm;
PORTA.PIN3CTRL &= ~(PORT_PULLUPEN_bm);
PORTA.PIN1CTRL &= ~(PORT_PULLUPEN_bm);
PORTA.PIN7CTRL &= ~(PORT_PULLUPEN_bm);
PORTA.PIN6CTRL &= ~(PORT_PULLUPEN_bm);
}
// ================= ISR =================
ISR(PORTA_PORT_vect){
PORTA.INTFLAGS = PORT_INT2_bm;
if(isSleeping){
// just wake up, DO NOT change mode
isSleeping = 0;
} else {
// normal mode change
mode++;
if(mode >= MODE_MAX){
mode = MODE_IDLE;
}
}
startflag=1; //for reverse runs
idleStepLoops=1; //reset since using for new modes to count loops
stepLoopFlag=0;
modeTicks = 0; // reset to start time over
}