Compare commits

...

4 Commits

Author SHA256 Message Date
74a0780219 try to use timer interrupt 2025-09-13 17:51:19 +02:00
f40f533f02 cleanup Readme and generate SBOM 2025-09-06 11:55:19 +02:00
16913b5c7f disable battery level LEDs 2025-09-06 11:21:38 +02:00
10609e169f cleanup 2025-09-06 00:56:48 +02:00
6 changed files with 199 additions and 95 deletions

4
.gitignore vendored
View File

@ -256,4 +256,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
pyupdi-env/ pyupdi-env/
sbom.spdx.json

View File

@ -1,37 +1,79 @@
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
# Project
project(lezyne-rear-light-firmware C) project(lezyne-rear-light-firmware C)
# MCU and clock # MCU and clock
set(MCU attiny202) set(MCU attiny202)
set(F_CPU 5000000UL) # 5 MHz set(F_CPU 5000000UL)
# Toolchain executables # Toolchain
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER avr-gcc) set(CMAKE_C_COMPILER avr-gcc)
set(OBJCOPY avr-objcopy) set(OBJCOPY avr-objcopy)
# Compiler flags: optimize, warnings, treat warnings as errors
set(CMAKE_C_FLAGS "-mmcu=${MCU} -DF_CPU=${F_CPU} -Os -Wall -Werror")
# Sources # Sources
add_executable(main.elf main.c) add_executable(main.elf main.c)
# HEX file # Compiler and linker flags
add_custom_command( target_compile_options(main.elf PRIVATE -mmcu=${MCU} -DF_CPU=${F_CPU} -Os -Wall -Werror)
OUTPUT main.hex set_target_properties(main.elf PROPERTIES LINK_FLAGS "-mmcu=${MCU}")
# Create HEX and BIN after build
add_custom_command(TARGET main.elf POST_BUILD
COMMAND ${OBJCOPY} -O ihex -R .eeprom main.elf main.hex COMMAND ${OBJCOPY} -O ihex -R .eeprom main.elf main.hex
DEPENDS main.elf
)
# BIN file
add_custom_command(
OUTPUT main.bin
COMMAND ${OBJCOPY} -O binary -R .eeprom main.elf main.bin COMMAND ${OBJCOPY} -O binary -R .eeprom main.elf main.bin
DEPENDS main.elf
) )
# Targets # Optional: show size
add_custom_target(hex ALL DEPENDS main.hex) find_program(SIZE_TOOL avr-size)
add_custom_target(bin ALL DEPENDS main.bin) if(SIZE_TOOL)
add_custom_command(TARGET main.elf POST_BUILD
COMMAND ${SIZE_TOOL} --mcu=${MCU} --format=avr main.elf
)
endif()
# Flash target using pymcuprog
find_program(PYMCUPROG pymcuprog)
set(UPDI_PORT "/dev/ttyUSB0" CACHE STRING "Serial port for UPDI programming")
if(PYMCUPROG)
add_custom_target(flash
COMMAND ${PYMCUPROG} -t uart -u ${UPDI_PORT} -d ${MCU} write -f main.hex
DEPENDS main.hex
COMMENT "Flashing ${MCU} with pymcuprog..."
)
else()
message(WARNING "pymcuprog not found in PATH. 'make flash' will not be available.")
endif()
# --- SBOM Generation (SPDX JSON) ---
find_package(Git REQUIRED)
# Generate current timestamp in ISO 8601 format (UTC)
string(TIMESTAMP CMAKE_TIMESTAMP "%Y-%m-%dT%H:%M:%SZ" UTC)
# Get current git hash
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Get avr-gcc version
execute_process(
COMMAND ${CMAKE_C_COMPILER} --version
OUTPUT_VARIABLE AVR_GCC_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Where to write SBOM
set(SBOM_FILE ${CMAKE_SOURCE_DIR}/sbom.spdx.json)
# Generate from template
configure_file(${CMAKE_SOURCE_DIR}/sbom.template.json ${SBOM_FILE} @ONLY)
# Always regenerate on build
add_custom_target(sbom ALL
DEPENDS ${SBOM_FILE}
COMMENT "Generating SPDX SBOM..."
)

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 localhorst Copyright (c) 2025 Hendrik Schutter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including associated documentation files (the "Software"), to deal in the Software without restriction, including

View File

@ -2,6 +2,8 @@
**🚧 Work in progress. No complete firmware yet 🚧** **🚧 Work in progress. No complete firmware yet 🚧**
🚀 For pre-build binaries go to [Releases](https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/releases).
Open firmware for Lezyne bike rear lights based on ATTINY202 Open firmware for Lezyne bike rear lights based on ATTINY202
This repository contains a minimal firmware as a **C project** for the ATtiny202 microcontroller using **GCC**, **CMake**, and **VS Code**. It also includes instructions for programming the chip using an **FT232 USB-UART adapter** via the UPDI interface with `pymcuprog`. This repository contains a minimal firmware as a **C project** for the ATtiny202 microcontroller using **GCC**, **CMake**, and **VS Code**. It also includes instructions for programming the chip using an **FT232 USB-UART adapter** via the UPDI interface with `pymcuprog`.
@ -105,11 +107,11 @@ Hardware: FT232 USB-UART adapter connected to UPDI with a 4.7 kΩ resistor.
| Signal | PCB Usage | LOW | HIGH | | Signal | PCB Usage | LOW | HIGH |
|--------|-------------------|--------------------------|---------------------------| |--------|-------------------|--------------------------|---------------------------|
| PA6 | Green LED | LED **ON** | LED OFF | | PA6 | Green LED | LED **ON** | LED **OFF** |
| PA7 | Red LED | LED **ON** | LED OFF | | PA7 | Red LED | LED **ON** | LED **OFF** |
| PA1 | LED 12 | LED(s) **ON** | LED(s) OFF | | PA1 | LED 12 | LED(s) **OFF** | LED(s) **ON** |
| PA2 | LED 36 | LED(s) **ON** | LED(s) OFF | | PA2 | LED 36 | LED(s) **OFF** | LED(s) **ON** |
| PA3 | LED 78 | LED(s) **ON** | LED(s) OFF | | PA3 | LED 78 | LED(s) **OFF** | LED(s) **ON** |
| PA0 | Activation Button | Button **pressed** (GND) | Button released (pull-up) | | PA0 | Activation Button | Button **pressed** (GND) | Button released (pull-up) |
## License ## License

150
main.c
View File

@ -1,70 +1,82 @@
/** /**
* @file main.c * @file main.c
* @brief Bike rear light implementation for ATTINY202. * @brief Bike rear light implementation for ATTINY202 with low-power standby.
*
*/ */
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <avr/io.h> #include <avr/io.h>
#include <util/delay.h> #include <util/delay.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
/** @defgroup LED_Masks LED bitmasks for PORTA
* @{
*/
#define BUTTON_PIN_MASK 0x01 // PA0 #define BUTTON_PIN_MASK 0x01 // PA0
#define PA1_SET_MASK 0x02 ///< LED 12 #define PA1_SET_MASK 0x02 // LED 12
#define PA2_SET_MASK 0x04 ///< LED 36 #define PA2_SET_MASK 0x04 // LED 36
#define PA3_SET_MASK 0x08 ///< LED 78 #define PA3_SET_MASK 0x08 // LED 78
#define PA6_SET_MASK 0x40 ///< Green LED pin #define PA6_SET_MASK 0x40 // Green LED
#define PA7_SET_MASK 0x80 ///< Red LED #define PA7_SET_MASK 0x80 // Red LED
/** @} */
#define MAIN_LOOP_SLEEP 10U // Main loop delay in ms (system tick) #define MAIN_LOOP_SLEEP 50U // Loop period in ms
#define BUTTON_LONG_PRESS_DURATION_MS 1000U // Long press detection threshold #define BUTTON_LONG_PRESS_DURATION_MS 1000U // Long press threshold
#define BUTTON_IGNORE_DURATION_MS 1000U // Time that the button is ignored after a long press #define BUTTON_IGNORE_DURATION_MS 1000U // Ignore after long press
/** @brief Convert milliseconds to system ticks (integer division). */
#define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP) #define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP)
/** @brief Global flags */ volatile bool bLedEnabled = false;
volatile bool bLedEnabled = true;
volatile bool bBtnPressed = false; volatile bool bBtnPressed = false;
// Function forward declarations
void blinkLed(bool resetCounters);
static inline void leds_off(void); static inline void leds_off(void);
static inline void leds_on(void); static inline void leds_on(void);
static void battery_level_indicator(void);
static void handleSwitch(void); static void handleSwitch(void);
void blinkLed(bool resetCounters);
/** /* --- Timer init: Use RTC PIT for periodic wake-up --- */
* @brief Main entry point. void init_timer(void)
* {
* Initializes I/O ports and runs the main loop, periodically calling @ref blinkLed(). RTC.CLKSEL = RTC_CLKSEL_INT1K_gc; // 1 kHz ULP clock for RTC
* while (RTC.STATUS > 0)
* @return never returns {
*/ } // Wait for sync
RTC.PITINTCTRL = RTC_PI_bm; // Enable PIT interrupt
RTC.PITCTRLA = RTC_PERIOD_CYC64_gc // ≈64 ms wake-up (~50 ms)
| RTC_PITEN_bm; // Enable PIT
}
ISR(RTC_PIT_vect)
{
RTC.PITINTFLAGS = RTC_PI_bm; // Clear interrupt flag
}
/* --- MAIN --- */
int main(void) int main(void)
{ {
// --- configure LED pins as outputs --- // Configure LED pins as outputs
VPORTA.DIR = (PA1_SET_MASK | PA2_SET_MASK | PA3_SET_MASK | VPORTA.DIR = (PA1_SET_MASK | PA2_SET_MASK | PA3_SET_MASK | PA6_SET_MASK | PA7_SET_MASK);
/*PA6_SET_MASK |*/ // PA6 now input (switch)
PA7_SET_MASK);
// Configure PA0 as input with pull-up // Configure PA0 as input with pull-up
VPORTA.DIR &= ~BUTTON_PIN_MASK; // Input VPORTA.DIR &= ~BUTTON_PIN_MASK; // Input
PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // Pull-up enabled PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // Pull-up enabled
// --- ensure all LEDs off at startup --- // Ensure all LEDs off at startup
leds_off(); leds_off();
VPORTA.OUT &= (uint8_t) ~(PA7_SET_MASK); battery_level_indicator(); // TODO: Implement
bool bLedEnabledOld = bLedEnabled; bool bLedEnabledOld = bLedEnabled;
while (true) cli();
{ init_timer();
handleSwitch(); // check switch state sei();
set_sleep_mode(SLEEP_MODE_STANDBY);
while (1)
{
handleSwitch(); // Check switch state
// Light LEDs while button is pressed
if (bBtnPressed) if (bBtnPressed)
{ {
leds_on(); leds_on();
@ -74,34 +86,36 @@ int main(void)
leds_off(); leds_off();
} }
// Long press detected → show confirmation blink
if (bLedEnabledOld != bLedEnabled) if (bLedEnabledOld != bLedEnabled)
{ {
// A long press detected --> confirm with a blink
bLedEnabledOld = bLedEnabled; bLedEnabledOld = bLedEnabled;
leds_off(); leds_off();
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10); _delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
leds_on(); leds_on();
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10); _delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
leds_off(); leds_off();
_delay_ms(BUTTON_IGNORE_DURATION_MS); _delay_ms(BUTTON_IGNORE_DURATION_MS);
blinkLed(true); // reset the persistent counters
blinkLed(true); // reset blink state machine
} }
else else
{ {
if ((bLedEnabled) && (!bBtnPressed)) if (bLedEnabled && !bBtnPressed)
{ {
blinkLed(false); blinkLed(false); // run normal blink
} }
} }
_delay_ms(MAIN_LOOP_SLEEP); sleep_enable();
sleep_cpu(); // Sleep until PIT wakes
sleep_disable();
} }
} }
/** /**
* @brief Switch off all controlled LEDs (PA1, PA2, PA3). * @brief Turn off all controlled LEDs (PA1, PA2, PA3)
*
* @note Declared inline for speed (single instruction sequence).
*/ */
static inline void leds_off(void) static inline void leds_off(void)
{ {
@ -109,9 +123,7 @@ static inline void leds_off(void)
} }
/** /**
* @brief Switch on all controlled LEDs (PA1, PA2, PA3). * @brief Turn on all controlled LEDs (PA1, PA2, PA3)
*
* @note Declared inline for speed (single instruction sequence).
*/ */
static inline void leds_on(void) static inline void leds_on(void)
{ {
@ -119,26 +131,31 @@ static inline void leds_on(void)
} }
/** /**
* @brief Handle momentary switch input on PA6. TODO: Switch to PA0 * @brief Battery monitoring
*/
static void battery_level_indicator(void)
{
// TODO: Implement
VPORTA.OUT |= (PA6_SET_MASK | PA7_SET_MASK); // green + red OFF
}
/**
* @brief Handle momentary switch input on PA0
* *
* A press longer than 2 seconds toggles ::bLedEnabled. * A long press toggles ::bLedEnabled.
* Uses simple state and counters for debouncing and long-press detection.
*/ */
static void handleSwitch(void) static void handleSwitch(void)
{ {
static uint16_t pressTicks = 0; ///< press duration counter static uint16_t pressTicks = 0; ///< Press duration counter
static bool prevPressed = false; ///< previous switch state static bool prevPressed = false; ///< Previous button state
bool pressed = !(VPORTA.IN & BUTTON_PIN_MASK); // active-low bool pressed = !(VPORTA.IN & BUTTON_PIN_MASK); // Active-low
if (pressed) if (pressed)
{ {
bBtnPressed = true; bBtnPressed = true;
if (pressTicks < 0xFFFF) if (pressTicks < 0xFFFF)
{ pressTicks++; // Prevent overflow
// prevent overflow
pressTicks++;
}
} }
else else
{ {
@ -148,15 +165,14 @@ static void handleSwitch(void)
if (prevPressed && pressTicks >= MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS)) if (prevPressed && pressTicks >= MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS))
{ {
// long press detected → toggle blinking bLedEnabled = !bLedEnabled; // Toggle LED blinking
bLedEnabled = !bLedEnabled;
} }
prevPressed = pressed; prevPressed = pressed;
} }
/** /**
* @brief LED blink state machine (bike rear light style). * @brief LED blink state machine (bike rear light style)
* *
* Normal cycle: * Normal cycle:
* 0: all off, wait 250 ms * 0: all off, wait 250 ms
@ -172,15 +188,13 @@ static void handleSwitch(void)
*/ */
void blinkLed(bool resetCounters) void blinkLed(bool resetCounters)
{ {
// --- precomputed constants --- const uint8_t T50 = MS_TO_TICKS(50);
const uint8_t T50 = MS_TO_TICKS(50); ///< 50 ms in ticks const uint8_t T100 = MS_TO_TICKS(100);
const uint8_t T100 = MS_TO_TICKS(100); ///< 100 ms in ticks const uint8_t T250 = MS_TO_TICKS(250);
const uint8_t T250 = MS_TO_TICKS(250); ///< 250 ms in ticks
// --- persistent state --- static uint16_t counter = 0;
static uint16_t counter = 0; ///< ticks for current state static uint8_t state = 2; // start with LEDs-on state
static uint8_t state = 2; ///< start with first LEDs-on state static uint8_t cycle = 0;
static uint8_t cycle = 0; ///< cycle counter
if (resetCounters) if (resetCounters)
{ {
@ -287,7 +301,7 @@ void blinkLed(bool resetCounters)
if (counter >= T250) if (counter >= T250)
{ {
counter = 0; counter = 0;
state = 0; // restart normal sequence state = 0;
} }
break; break;
} }

44
sbom.template.json Normal file
View File

@ -0,0 +1,44 @@
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "lezyne-rear-light-firmware",
"documentNamespace": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/@GIT_HASH@",
"creationInfo": {
"created": "@CMAKE_TIMESTAMP@",
"creators": [
"Tool: CMake+SPDX"
]
},
"packages": [
{
"name": "main.c",
"SPDXID": "SPDXRef-mainc",
"downloadLocation": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/src/branch/main/main.c",
"filesAnalyzed": true,
"versionInfo": "@GIT_HASH@",
"licenseDeclared": "MIT License",
"homepage": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware"
},
{
"name": "avr-gcc",
"SPDXID": "SPDXRef-avrgcc",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"versionInfo": "@AVR_GCC_VERSION@",
"licenseDeclared": "GPL-3.0-or-later",
"supplier": "Organization: The GNU Project",
"homepage": "https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git"
},
{
"name": "avr-libc",
"SPDXID": "SPDXRef-avrlibc",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"versionInfo": "2.2.1-1.2",
"licenseDeclared": "Modified BSD License",
"supplier": "Organization: AVRDUDES Authors",
"homepage": "https://github.com/avrdudes/avr-libc/"
}
]
}