Files
lezyne-rear-light-firmware/main.c

556 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file main.c
* @brief Bike rear light implementation for ATTINY202.
*
*/
#include <stdint.h>
#include <stdbool.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
#define BUTTON_PIN_MASK 0x01 // PA0 used as RESET/UPDI pin
#define PA1_SET_MASK 0x02 ///< LED 12
#define PA2_SET_MASK 0x04 ///< LED 36
#define PA3_SET_MASK 0x08 ///< LED 78
#define PA6_SET_MASK 0x40 ///< Green LED pin
#define PA7_SET_MASK 0x80 ///< Red LED
#define MAIN_LOOP_SLEEP 10U // Main loop delay in ms
#define BUTTON_LONG_PRESS_DURATION_MS 1000U // Long press threshold
#define BUTTON_SHORT_PRESS_DURATION_MS 50U // Short press threshold
#define BUTTON_IGNORE_DURATION_MS 2000U // Time button ignored after long press
#define BUTTON_CONFIRMATION_BLINK_LOOPS 10U // Blink animation for confirmation
#define BUTTON_CONFIRMATION_BLINK_DURATION_MS 50U
#define GLOW_BRIGHTNESS_MIN 10U
#define GLOW_BRIGHTNESS_MAX 100U
/** Convert milliseconds to system ticks */
#define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP)
typedef enum _Mode
{
ANIMATION_BLINK,
ANIMATION_GLOW,
STATIC_FULL,
MAX_COUNT,
} eMode;
/** Global flags */
volatile bool bLedEnabled = true;
volatile bool bBtnPressed = false;
volatile eMode eModeCurrent = ANIMATION_BLINK;
// Forward declarations
ISR(PORTA_PORT_vect);
static void software_reset(void);
static void configureLowPower(void);
void initPWM(void);
void setPWM_PA2(uint8_t duty);
static inline void leds_off(void);
static inline void leds_on(void);
static void battery_level_indicator(void);
static bool handleSwitch(void);
static inline void switchMode(void);
void ledAnimationBlink(bool resetCounters);
void ledAnimationGlow(void);
void ledStaticFull(void);
/**
* @brief Main entry point
*/
int main(void)
{
// Disable unused peripherals for power saving
configureLowPower();
// Configure LED pins as outputs
VPORTA.DIR = (PA1_SET_MASK | PA2_SET_MASK | PA3_SET_MASK | PA6_SET_MASK | PA7_SET_MASK);
initPWM();
// Configure PA0 as input with pull-up
VPORTA.DIR &= ~BUTTON_PIN_MASK; // Input
PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // Pull-up enabled
leds_off(); // Ensure all LEDs off at startup
bool bLedEnabledOld = bLedEnabled;
eModeCurrent = ANIMATION_BLINK; // Set the mode to start with
while (true)
{
battery_level_indicator();
bBtnPressed = handleSwitch(); // Check switch state
// Light LEDs while button is pressed
if (bBtnPressed)
{
leds_on();
}
else
{
leds_off();
}
// Long press detected --> show confirmation blink
if (bLedEnabledOld != bLedEnabled)
{
bLedEnabledOld = bLedEnabled;
for (uint8_t i = 0U; i < BUTTON_CONFIRMATION_BLINK_LOOPS; i++)
{
leds_off();
_delay_ms(BUTTON_CONFIRMATION_BLINK_DURATION_MS);
leds_on();
_delay_ms(BUTTON_CONFIRMATION_BLINK_DURATION_MS);
leds_off();
}
// Give time until button is released
_delay_ms(BUTTON_IGNORE_DURATION_MS);
// Activate the interrupt for PA0
PORTA.PIN0CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
set_sleep_mode(SLEEP_MODE_STANDBY); // Deepest sleep mode (standby)
sleep_enable(); // Enable sleep
sei(); // Re-enable interrupts
sleep_cpu(); // MCU sleeps here
}
else
{
if (bLedEnabled && !bBtnPressed)
{
switch (eModeCurrent)
{
case ANIMATION_BLINK:
ledAnimationBlink(false); // run normal blink
break;
case ANIMATION_GLOW:
ledAnimationGlow();
break;
case STATIC_FULL:
ledStaticFull();
break;
default:
break;
}
}
}
// Sleep during delay instead of busy-wait
sleep_enable();
_delay_ms(MAIN_LOOP_SLEEP);
sleep_disable();
}
}
/**
* @brief Move to next mode
*/
static inline void switchMode(void)
{
eModeCurrent = (eModeCurrent + 1) % MAX_COUNT;
}
/**
* @brief Configure for lowest power consumption
*/
static void configureLowPower(void)
{
return;
// Set unused pins as outputs LOW to prevent floating inputs
// Floating inputs can cause extra current consumption
PORTA.DIRSET = PIN4_bm | PIN5_bm; // Set PA4, PA5 as outputs if unused
PORTA.OUTCLR = PIN4_bm | PIN5_bm; // Drive them LOW
// Configure sleep mode
set_sleep_mode(SLEEP_MODE_IDLE); // Use IDLE when TCA0 PWM needs to run
// Use SLEEP_MODE_STANDBY for deep sleep when LEDs are off
// Disable unused timers
TCB0.CTRLA = 0; // Disable TCB0 if not used
// TCA0 is used for PWM, keep it enabled
// Disable ADC (Analog-to-Digital Converter)
ADC0.CTRLA &= ~ADC_ENABLE_bm;
// Disable AC (Analog Comparator)
AC0.CTRLA &= ~AC_ENABLE_bm;
// Disable unused USART
USART0.CTRLB = 0;
// Disable TWI (I2C) if not used
TWI0.MCTRLA = 0;
TWI0.SCTRLA = 0;
// Disable SPI
SPI0.CTRLA = 0;
// Disable Watchdog Timer (if not needed)
// Note: WDT can only be disabled during first 4 clock cycles after reset
// CCP = CCP_IOREG_gc;
// WDT.CTRLA = 0;
// Disable BOD (Brown-Out Detection) in sleep modes for lower power
// This is done via fuses, not runtime configurable
// Disable digital input buffers on unused pins to save power
// Only needed if pins are truly unused (floating)
// PORTA.PIN4CTRL = PORT_ISC_INPUT_DISABLE_gc; // PA4 if unused
// PORTA.PIN5CTRL = PORT_ISC_INPUT_DISABLE_gc; // PA5 if unused
}
/**
* @brief Init PWM for PA2
*/
void initPWM(void)
{
// No PORTMUX needed - PA2 is WO2 by default
// TCA0 in normal mode (single slope PWM)
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
// Set period for ~19.5kHz PWM (5MHz / 256 = ~19.5kHz)
TCA0.SINGLE.PER = 0xFF;
// Start timer with DIV1 (no prescaler)
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
}
/**
* @brief Set PWM duty cycle for PA2
*/
void setPWM_PA2(uint8_t duty)
{
TCA0.SINGLE.CMP2 = duty;
}
/**
* @brief Turn off all controlled LEDs (PA1, PA2, PA3)
*/
static inline void leds_off(void)
{
VPORTA.OUT &= (uint8_t) ~(PA1_SET_MASK | PA3_SET_MASK);
setPWM_PA2(0U);
}
/**
* @brief Turn on all controlled LEDs (PA1, PA2, PA3)
*/
static inline void leds_on(void)
{
VPORTA.OUT |= (PA1_SET_MASK | PA3_SET_MASK);
setPWM_PA2(255U);
}
/**
* @brief Read battery voltage using internal 1.1V reference
* @return Estimated battery voltage in millivolts
*/
uint16_t readBatteryVoltage(void)
{
// Enable ADC
ADC0.CTRLA = ADC_ENABLE_bm;
// Select internal voltage reference as input
ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc;
// Use VCC as reference (default)
ADC0.CTRLC = ADC_PRESC_DIV4_gc; // Prescaler for 5MHz/4 = 1.25MHz ADC clock
// Start conversion
ADC0.COMMAND = ADC_STCONV_bm;
// Wait for conversion complete
while (!(ADC0.INTFLAGS & ADC_RESRDY_bm))
;
uint16_t adcResult = ADC0.RES;
// Disable ADC to save power
ADC0.CTRLA = !ADC_ENABLE_bm;
// Calculate VCC voltage
// V_battery = 1.1V × 1023 / ADC_result
// Result in millivolts: 1100 × 1023 / ADC_result
uint32_t voltage_mv = (1100UL * 1023UL) / adcResult;
return (uint16_t)voltage_mv;
}
/**
* @brief Battery monitoring
*/
static void battery_level_indicator(void)
{
uint16_t voltage = readBatteryVoltage();
// 1S LiPo voltage ranges:
// Good: >=3700mV
// Low: >=3500mV
// VPORTA.OUT &= ~(PA6_SET_MASK | PA7_SET_MASK); // Turn off both LEDs first
if (voltage >= 3700)
{
// Green ON, Red OFF - Good battery
VPORTA.OUT &= ~PA6_SET_MASK; // Green ON (active low)
}
else if (voltage >= 3500)
{
// Both ON (yellow/orange) - Medium battery
VPORTA.OUT &= ~(PA6_SET_MASK | PA7_SET_MASK);
}
else
{
// Green OFF, Red ON - Low battery
VPORTA.OUT &= ~PA7_SET_MASK; // Red ON (active low)
}
VPORTA.OUT &= ~(PA6_SET_MASK | PA7_SET_MASK);
}
/**
* @brief Handle momentary switch input on PA0
*
* A long press toggles bLedEnabled.
* A short press swiches the mode.
*/
static bool handleSwitch(void)
{
static uint16_t pressTicks = 0; ///< Press duration counter
static bool prevPressed = false; ///< Previous button state
bool pressed = !(VPORTA.IN & BUTTON_PIN_MASK); // Active-low
if (pressed)
{
if (pressTicks < 0xFFFF)
pressTicks++; // Prevent overflow
}
else
{
// Button released
if (prevPressed)
{
// Check if it was a short press (not a long press)
if (pressTicks >= MS_TO_TICKS(BUTTON_SHORT_PRESS_DURATION_MS) &&
pressTicks < MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS))
{
switchMode();
}
}
pressTicks = 0;
}
if (pressed && pressTicks >= MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS))
{
bLedEnabled = !bLedEnabled; // Toggle LED blinking
}
prevPressed = pressed;
return pressed;
}
/**
* @brief Perform software reset
*/
static void software_reset(void)
{
CCP = CCP_IOREG_gc; // unlock protected registers
RSTCTRL.SWRR = 1; // trigger software reset
while (1)
; // wait for reset
}
/**
* @brief Interrupt service routine
*/
ISR(PORTA_PORT_vect)
{
// Clear interrupt flags for PA0
PORTA.INTFLAGS = BUTTON_PIN_MASK;
if (!(VPORTA.IN & BUTTON_PIN_MASK)) // check PA0 low
{
// Turn off all LEDs
software_reset();
}
}
/**
* @brief LED blink state machine (bike rear light style)
*
* Normal cycle:
* 0: all off, wait 250 ms
* 2: LED 12 + 78 ON, wait 50 ms
* 5: all off, wait 100 ms
* 7: LED 12 + 78 ON, wait 50 ms
* 10: all off, wait 250 ms
* 12: LED 36 ON, wait 50 ms
* 14: all off, wait 100 ms
* 16: LED 36 ON, wait 50 ms
* 18: all off, wait 250 ms → restart
* Special: every 3rd cycle, all LEDs ON for 250 ms
*/
void ledAnimationBlink(bool resetCounters)
{
const uint8_t T50 = MS_TO_TICKS(50);
const uint8_t T100 = MS_TO_TICKS(100);
const uint8_t T250 = MS_TO_TICKS(250);
static uint16_t counter = 0;
static uint8_t state = 2; // start with LEDs-on state
static uint8_t cycle = 0;
if (resetCounters)
{
counter = 0;
state = 12; // start with LEDs on
cycle = 0;
}
counter++;
switch (state)
{
case 0: // all LEDs off, wait 250 ms
leds_off();
if (counter >= T250)
{
counter = 0;
state = 2;
}
break;
case 2: // LED 12 + 78 on, wait 50 ms
VPORTA.OUT |= (PA1_SET_MASK | PA3_SET_MASK);
if (counter >= T50)
{
counter = 0;
state = 5;
}
break;
case 5: // all LEDs off, wait 100 ms
leds_off();
if (counter >= T100)
{
counter = 0;
state = 7;
}
break;
case 7: // LED 12 + 78 on, wait 50 ms
VPORTA.OUT |= (PA1_SET_MASK | PA3_SET_MASK);
if (counter >= T50)
{
counter = 0;
state = 10;
}
break;
case 10: // all LEDs off, wait 250 ms
leds_off();
if (counter >= T250)
{
counter = 0;
state = 12;
}
break;
case 12: // LED 36 on, wait 50 ms
setPWM_PA2(255U);
if (counter >= T50)
{
counter = 0;
state = 14;
}
break;
case 14: // all LEDs off, wait 100 ms
leds_off();
if (counter >= T100)
{
counter = 0;
state = 16;
}
break;
case 16: // LED 36 on, wait 50 ms
setPWM_PA2(255U);
if (counter >= T50)
{
counter = 0;
state = 18;
}
break;
case 18: // all LEDs off, wait 250 ms
leds_off();
if (counter >= T250)
{
counter = 0;
if (++cycle >= 3)
{
cycle = 0;
state = 20; // special all-LEDs-on state
}
else
{
state = 0; // restart normal cycle
}
}
break;
case 20: // special: all LEDs on for 250 ms
leds_on();
if (counter >= T250)
{
counter = 0;
state = 0; // restart normal sequence
}
break;
}
}
/**
* @brief All LEDs with static full power
*/
void ledStaticFull(void)
{
leds_on();
}
/**
* @brief Inner LEDs with glow animation
*/
void ledAnimationGlow(void)
{
static uint8_t brightness = 0;
static int8_t direction = 1;
// Update brightness level every call (10ms)
brightness += direction;
// Reverse direction at limits
if (brightness >= GLOW_BRIGHTNESS_MAX)
{
brightness = GLOW_BRIGHTNESS_MAX;
direction = -1;
}
else if (brightness == GLOW_BRIGHTNESS_MIN)
{
brightness = GLOW_BRIGHTNESS_MIN;
direction = 1;
}
// Apply PWM brightness to LED 3-6
setPWM_PA2(brightness * 255 / 100); // Scale 0-100 to 0-255
}