556 lines
14 KiB
C
556 lines
14 KiB
C
/**
|
||
* @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 1–2
|
||
#define PA2_SET_MASK 0x04 ///< LED 3–6
|
||
#define PA3_SET_MASK 0x08 ///< LED 7–8
|
||
#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 1–2 + 7–8 ON, wait 50 ms
|
||
* 5: all off, wait 100 ms
|
||
* 7: LED 1–2 + 7–8 ON, wait 50 ms
|
||
* 10: all off, wait 250 ms
|
||
* 12: LED 3–6 ON, wait 50 ms
|
||
* 14: all off, wait 100 ms
|
||
* 16: LED 3–6 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 1–2 + 7–8 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 1–2 + 7–8 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 3–6 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 3–6 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
|
||
} |