Compare commits

...

3 Commits

Author SHA256 Message Date
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 163 additions and 81 deletions

2
.gitignore vendored
View File

@ -257,3 +257,5 @@ cython_debug/
.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

100
main.c
View File

@ -9,62 +9,54 @@
#include <avr/io.h> #include <avr/io.h>
#include <util/delay.h> #include <util/delay.h>
/** @defgroup LED_Masks LED bitmasks for PORTA #define BUTTON_PIN_MASK 0x01 // PA0 TODO: using RESET/UPDI pin
* @{
*/
#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 pin
#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 10U // Main loop delay 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 // Time button ignored after long press
/** @brief Convert milliseconds to system ticks (integer division). */ /** Convert milliseconds to system ticks */
#define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP) #define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP)
/** @brief Global flags */ /** Global flags */
volatile bool bLedEnabled = true; volatile bool bLedEnabled = true;
volatile bool bBtnPressed = false; volatile bool bBtnPressed = false;
// Function forward declarations // Forward declarations
void blinkLed(bool resetCounters); 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);
/** /**
* @brief Main entry point. * @brief Main entry point
*
* Initializes I/O ports and runs the main loop, periodically calling @ref blinkLed().
*
* @return never returns
*/ */
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) while (true)
{ {
handleSwitch(); // check switch state handleSwitch(); // Check switch state
// Light LEDs while button is pressed
if (bBtnPressed) if (bBtnPressed)
{ {
leds_on(); leds_on();
@ -74,23 +66,25 @@ 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
} }
} }
@ -99,9 +93,7 @@ int main(void)
} }
/** /**
* @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 +101,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 +109,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 +143,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 +166,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)
{ {

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/"
}
]
}