diff --git a/.github/build_summary.md b/.github/build_summary.md
new file mode 100644
index 0000000..ce113f7
--- /dev/null
+++ b/.github/build_summary.md
@@ -0,0 +1,13 @@
+### Files included in the build artifact
+
+Most users should use one of the **`picoboot_full_*.uf2`** files (full package). Other files are intended for advanced users to update only specific parts of the firmware or payload.
+
+| File Name | Platform | Description |
+| ------------------------- | ------------------------------------ | --------------------------------- |
+| `picoboot_full_pico.uf2` | Pico
Pico W | firmware + payload (full package) |
+| `picoboot_full_pico2.uf2` | Pico 2
Pico 2 W | firmware + payload (full package) |
+| `picoboot_pico.uf2` | Pico
Pico W | firmware only |
+| `picoboot_pico2.uf2` | Pico 2
Pico 2 W | firmware only |
+| `payload_pico.uf2` | Pico
Pico W | payload |
+| `payload_pico2.uf2` | Pico 2
Pico 2 W | payload |
+| `payload_universal.uf2` | Pico
Pico W
Pico 2
Pico 2 W | payload |
diff --git a/.github/release_description.md b/.github/release_description.md
new file mode 100644
index 0000000..4197668
--- /dev/null
+++ b/.github/release_description.md
@@ -0,0 +1,14 @@
+@DESCRIPTION@
+
+## Changes in this release
+
+@CHANGELOG@
+
+## Useful Links
+
+✨ [Software features](https://github.com/redolution/gekkoboot/) | 🚀 [Installation Guide](https://support.webhdx.dev/gc/picoboot/installation-guide) | ⬆️ [Update Guide](https://support.webhdx.dev/gc/picoboot/update-picoboot) | 🛠️ [Troubleshooting](https://support.webhdx.dev/gc/picoboot/troubleshooting)
+
+## Available files
+
+* `picoboot_full_pico.uf2`: Compatible with Raspberry Pi Pico and Raspberry Pi Pico W.
+* `picoboot_full_pico2.uf2`: Compatible with Raspberry Pi Pico 2 and Raspberry Pi Pico 2 W.
diff --git a/.github/workflows/10-build.yml b/.github/workflows/build.yml
similarity index 72%
rename from .github/workflows/10-build.yml
rename to .github/workflows/build.yml
index f290242..a03f7e4 100644
--- a/.github/workflows/10-build.yml
+++ b/.github/workflows/build.yml
@@ -1,4 +1,4 @@
-name: Build
+name: Build PicoBoot
on:
push:
branches:
@@ -32,6 +32,8 @@ jobs:
- name: Checkout PicoBoot code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Set outputs
id: vars
@@ -40,21 +42,19 @@ jobs:
- uses: robinraju/release-downloader@v1
id: gekkoboot-download
with:
- repository: 'webhdx/iplboot'
+ repository: 'webhdx/gekkoboot'
latest: true
fileName: '*.zip'
out-file-path: gekkoboot
extract: true
- - name: Copy gekkoboot uf2 payload
+ - name: Copy gekkoboot.dol
run: |
cd gekkoboot
- if [ -f "gekkoboot_pico.uf2" ]; then
- cp gekkoboot_pico.uf2 ${{ github.workspace }}/payload.uf2
- elif [ -f "iplboot_pico.uf2" ]; then
- cp iplboot_pico.uf2 ${{ github.workspace }}/payload.uf2
+ if [ -f "gekkoboot.dol" ]; then
+ cp gekkoboot.dol ${{ github.workspace }}/payload.dol
else
- echo "Neither gekkoboot_pico.uf2 nor iplboot_pico.uf2 found."
+ echo "gekkoboot.dol not found"
exit 1
fi
@@ -63,15 +63,15 @@ jobs:
with:
imageName: ghcr.io/webhdx/picoboot
cacheFrom: ghcr.io/webhdx/picoboot
+ push: never
runCmd: |
- cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE .
- make
+ tools/build.sh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: picoboot-${{ steps.vars.outputs.sha_short }}
- path: |
- picoboot_full.uf2
- picoboot.uf2
- payload.uf2
+ path: dist/
+
+ - name: Add build summary
+ run: echo "$(cat .github/build_summary.md)" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/20-devcontainer.yml b/.github/workflows/devcontainer.yml
similarity index 100%
rename from .github/workflows/20-devcontainer.yml
rename to .github/workflows/devcontainer.yml
diff --git a/.github/workflows/00-release.yml b/.github/workflows/release.yml
similarity index 66%
rename from .github/workflows/00-release.yml
rename to .github/workflows/release.yml
index 45e3550..828342b 100644
--- a/.github/workflows/00-release.yml
+++ b/.github/workflows/release.yml
@@ -6,7 +6,7 @@ on:
version:
description: 'Version to tag'
required: true
- default: 'v0.4'
+ default: 'v0.5.0'
type: string
is_draft:
description: 'Is draft release?'
@@ -47,7 +47,7 @@ jobs:
build:
needs: tag
- uses: ./.github/workflows/10-build.yml
+ uses: ./.github/workflows/build.yml
release:
runs-on: ubuntu-latest
@@ -78,17 +78,40 @@ jobs:
with:
name: picoboot-${{ steps.vars.outputs.sha_short }}
path: dist/
+
+ - name: Build Changelog
+ id: build_changelog
+ uses: mikepenz/release-changelog-builder-action@v5
+ with:
+ mode: "COMMIT"
+ toTag: ${{ github.event.inputs.version }}
+ configurationJson: |
+ {
+ "template": "#{{UNCATEGORIZED}}",
+ "commit_template": "* #{{TITLE}} by @#{{AUTHOR}} (#{{MERGE_SHA}})",
+ "empty_template": "_No notable changes in this version._",
+ "categories": []
+ }
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Prepare release description
- run: |
- touch release-description.md
- echo "${{ github.event.inputs.release_description }}" >> release-description.md
+ - name: Prepare release description
+ env:
+ DESCRIPTION: ${{ github.event.inputs.release_description }}
+ RELEASE_NAME: ${{ steps.determine_name.outputs.name }}
+ CHANGELOG: ${{ steps.build_changelog.outputs.changelog }}
+ run: |
+ content=$(cat .github/release_description.md)
+ content=${content//@DESCRIPTION@/$DESCRIPTION}
+ content=${content//@CHANGELOG@/$CHANGELOG}
+ echo "$content" > release-description.md
- name: Calculate artifact checksum
run: |
echo '```' >> release-description.md
cd dist/
- sha256sum picoboot_full.uf2 >> ../release-description.md
+ sha256sum picoboot_full_pico.uf2 >> ../release-description.md
+ sha256sum picoboot_full_pico2.uf2 >> ../release-description.md
cd ../
echo '```' >> release-description.md
@@ -98,23 +121,13 @@ jobs:
git config --global user.email "actions@github.com"
- name: Create Release
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: softprops/action-gh-release@v2
with:
+ name: ${{ steps.determine_name.outputs.name }}
tag_name: ${{ github.event.inputs.version }}
- release_name: ${{ steps.determine_name.outputs.name }}
- body_path: ${{ github.workspace }}/release-description.md
draft: ${{ github.event.inputs.is_draft }}
prerelease: ${{ github.event.inputs.is_prerelease }}
-
- - name: Upload Release Asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.create_release.outputs.upload_url }}
- asset_path: ${{ github.workspace }}/dist/picoboot_full.uf2
- asset_name: picoboot_full.uf2
- asset_content_type: application/octet-stream
+ body_path: ${{ github.workspace }}/release-description.md
+ files: |
+ dist/picoboot_full_pico.uf2
+ dist/picoboot_full_pico2.uf2
diff --git a/.gitignore b/.gitignore
index f7d8346..384ba27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ _deps
# Build environment
build/
+dist/
CMakeFiles/
elf2uf2/
generated/
@@ -20,3 +21,6 @@ picoboot.elf.map
picoboot.hex
*.uf2
picoboot.pio.h
+gekkoboot.dol
+payload.dol
+src/version.h
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 33e9484..e997a02 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,28 +3,25 @@ cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
-find_package(Git)
-execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
- OUTPUT_VARIABLE GIT_REPO_VERSION
- OUTPUT_STRIP_TRAILING_WHITESPACE)
-string(REGEX REPLACE "v([0-9]+\\.[0-9]+).*" "\\1" CMAKE_GIT_REPO_VERSION ${GIT_REPO_VERSION})
-string(REGEX REPLACE "^(.......-.*)|(.......)$" "0.0.0" CMAKE_GIT_REPO_VERSION ${CMAKE_GIT_REPO_VERSION})
-configure_file("src/version.h.in" "src/version.h")
-message("GIT_REPO_VERSION is ${GIT_REPO_VERSION}")
-message("CMAKE_GIT_REPO_VERSION is ${CMAKE_GIT_REPO_VERSION}")
+include(cmake/extract_version.cmake)
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
-project(picoboot LANGUAGES C CXX ASM VERSION ${CMAKE_GIT_REPO_VERSION})
+project(picoboot LANGUAGES C CXX ASM VERSION ${PROJECT_VERSION_STRING})
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/dist)
+
add_executable(picoboot
src/picoboot.c
+ src/hw.c
src/pio.c
+ src/status_led.c
+ src/status_led/gpio.c
+ src/status_led/cyw43.c
)
pico_generate_pio_header(picoboot
@@ -33,10 +30,9 @@ pico_generate_pio_header(picoboot
pico_set_program_name(picoboot "PicoBoot")
pico_set_program_description(picoboot "RP2040 based modchip for Nintendo GameCube")
-pico_set_program_version(picoboot ${GIT_REPO_VERSION})
+pico_set_program_version(picoboot ${FW_VER_STRING})
pico_set_program_url(picoboot "https://github.com/webhdx/PicoBoot")
-pico_set_binary_type(picoboot copy_to_ram)
target_link_options(pico_standard_link INTERFACE "LINKER:--script=${CMAKE_CURRENT_LIST_DIR}/memmap_picoboot.ld")
pico_enable_stdio_uart(picoboot 0)
@@ -44,20 +40,12 @@ pico_enable_stdio_usb(picoboot 1)
target_include_directories(picoboot PRIVATE src)
-target_link_libraries(picoboot PRIVATE pico_stdlib hardware_pio hardware_dma)
+target_link_libraries(picoboot PRIVATE
+ hardware_adc
+ hardware_dma
+ hardware_pio
+ pico_cyw43_arch_none
+ pico_stdlib
+)
pico_add_extra_outputs(picoboot)
-
-function(merge_uf2 NAME BASE_TARGET INPUTS)
- get_target_property(BASE_TARGET_NAME ${BASE_TARGET} OUTPUT_NAME)
- if(BASE_TARGET_NAME STREQUAL "BASE_TARGET_NAME-NOTFOUND")
- get_target_property(BASE_TARGET_NAME ${BASE_TARGET} NAME)
- endif()
-
- add_custom_target(${NAME} ALL
- COMMAND ${CMAKE_CURRENT_LIST_DIR}/merge_uf2.py ${NAME}.uf2 ${BASE_TARGET_NAME}.uf2 ${INPUTS}
- DEPENDS ${BASE_TARGET} ${INPUTS}
- COMMAND_EXPAND_LISTS)
-endfunction()
-
-merge_uf2(picoboot_full picoboot "${CMAKE_CURRENT_LIST_DIR}/payload.uf2")
diff --git a/cmake/extract_version.cmake b/cmake/extract_version.cmake
new file mode 100644
index 0000000..b616007
--- /dev/null
+++ b/cmake/extract_version.cmake
@@ -0,0 +1,42 @@
+# CMake script to extract version information from Git and configure version.h
+
+find_package(Git REQUIRED)
+
+execute_process(
+ COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ OUTPUT_VARIABLE GIT_DESCRIBE_OUTPUT
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ RESULT_VARIABLE GIT_DESCRIBE_RESULT
+)
+
+set(FW_VER_STRING "0.0.0-unknown")
+set(FW_VER_MAJOR 0)
+set(FW_VER_MINOR 0)
+set(FW_VER_PATCH 0)
+
+if(GIT_DESCRIBE_RESULT EQUAL 0 AND GIT_DESCRIBE_OUTPUT)
+ set(FW_VER_STRING ${GIT_DESCRIBE_OUTPUT})
+ if(FW_VER_STRING MATCHES "^v([0-9]+)\.([0-9]+)\.([0-9]+)")
+ set(FW_VER_MAJOR ${CMAKE_MATCH_1})
+ set(FW_VER_MINOR ${CMAKE_MATCH_2})
+ set(FW_VER_PATCH ${CMAKE_MATCH_3})
+ else()
+ message(STATUS "Git version string '${FW_VER_STRING}' does not match vX.Y.Z format for MAJOR/MINOR/PATCH. Using 0.0.0 for these.")
+ endif()
+else()
+ message(WARNING "git describe failed or returned empty. Using default version: ${FW_VER_STRING}, MAJOR: ${FW_VER_MAJOR}, MINOR: ${FW_VER_MINOR}, PATCH: ${FW_VER_PATCH}")
+endif()
+
+set(PROJECT_VERSION_STRING "${FW_VER_MAJOR}.${FW_VER_MINOR}.${FW_VER_PATCH}")
+
+configure_file(
+ "${CMAKE_SOURCE_DIR}/src/version.h.in"
+ "${CMAKE_SOURCE_DIR}/src/version.h"
+)
+
+message(STATUS "FW_VER_STRING: ${FW_VER_STRING}")
+message(STATUS "FW_VER_MAJOR: ${FW_VER_MAJOR}")
+message(STATUS "FW_VER_MINOR: ${FW_VER_MINOR}")
+message(STATUS "FW_VER_PATCH: ${FW_VER_PATCH}")
+message(STATUS "PROJECT_VERSION_STRING: ${PROJECT_VERSION_STRING}")
\ No newline at end of file
diff --git a/src/hw.c b/src/hw.c
new file mode 100644
index 0000000..31c1c2c
--- /dev/null
+++ b/src/hw.c
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include "hardware/adc.h"
+#include "pico/stdlib.h"
+#include "pico/platform.h"
+
+#include "hw.h"
+
+#define ADC_PIN_VSYS 29
+#define ADC_INPUT_VSYS 3
+
+// Threshold for differentiating Pico from Pico W based on raw ADC reading.
+// Pico W typically has a much lower reading on VSYS ADC (e.g., ~0x01c).
+// Pico typically has a higher reading (e.g., ~0x2cd).
+// A value around 0x100 should be a safe threshold.
+#define PICO_W_ADC_THRESHOLD 0x100
+
+hw_board_type_t hw_detect_board_type(void) {
+ adc_gpio_init(ADC_PIN_VSYS);
+ adc_select_input(ADC_INPUT_VSYS);
+
+ uint16_t adc_result = adc_read();
+
+ if (adc_result < PICO_W_ADC_THRESHOLD) {
+ #if defined(PICO_RP2350)
+ return HW_BOARD_TYPE_PICO_2_W;
+ #else
+ return HW_BOARD_TYPE_PICO_W;
+ #endif
+ } else {
+ #if defined(PICO_RP2350)
+ return HW_BOARD_TYPE_PICO_2;
+ #else
+ return HW_BOARD_TYPE_PICO;
+ #endif
+ }
+}
+
+const char* hw_board_type_to_string(hw_board_type_t board_type) {
+ switch (board_type) {
+ case HW_BOARD_TYPE_PICO:
+ return "Raspberry Pi Pico";
+ case HW_BOARD_TYPE_PICO_W:
+ return "Raspberry Pi Pico W";
+ case HW_BOARD_TYPE_PICO_2:
+ return "Raspberry Pi Pico 2";
+ case HW_BOARD_TYPE_PICO_2_W:
+ return "Raspberry Pi Pico 2 W";
+ default:
+ return "Unknown";
+ }
+}
diff --git a/src/hw.h b/src/hw.h
new file mode 100644
index 0000000..2c54ff5
--- /dev/null
+++ b/src/hw.h
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifndef HW_H
+#define HW_H
+
+// Defines the possible board types
+typedef enum {
+ HW_BOARD_TYPE_PICO,
+ HW_BOARD_TYPE_PICO_W,
+ HW_BOARD_TYPE_PICO_2,
+ HW_BOARD_TYPE_PICO_2_W,
+ HW_BOARD_TYPE_UNKNOWN
+} hw_board_type_t;
+
+/**
+ * @brief Detects the type of Raspberry Pi Pico board.
+ *
+ * This function reads the ADC on GPIO29 (ADC3) to differentiate
+ * between a Pico and a Pico W. Pico W typically shows a much lower
+ * voltage on VSYS (connected to ADC3) when the wireless circuitry is present.
+ *
+ * @note The ADC system must be initialized by calling adc_init() before this function.
+ *
+ * @return hw_board_type_t The detected board type (HW_BOARD_TYPE_PICO or HW_BOARD_TYPE_PICO_W).
+ */
+hw_board_type_t hw_detect_board_type(void);
+
+/**
+ * @brief Converts a hw_board_type_t enum to its string representation.
+ *
+ * @param board_type The board type enum.
+ *
+ * @return const char* The string representation of the board type.
+ */
+const char* hw_board_type_to_string(hw_board_type_t board_type);
+
+#endif // HW_H
\ No newline at end of file
diff --git a/src/picoboot.c b/src/picoboot.c
index a11b75d..273dcb5 100644
--- a/src/picoboot.c
+++ b/src/picoboot.c
@@ -1,27 +1,34 @@
/**
- * Copyright (c) 2024 Maciej Kobus
+ * Copyright (c) 2025 Maciej Kobus
*
* SPDX-License-Identifier: GPL-2.0-only
*/
#include
-#include "pico/stdlib.h"
-#include "hardware/pio.h"
+#include
+
+#include "hardware/adc.h"
#include "hardware/clocks.h"
#include "hardware/dma.h"
+#include "hardware/pio.h"
#include "hardware/structs/bus_ctrl.h"
-#include "pio.h"
-#include "picoboot.pio.h"
-#include "endian.h"
+#include "pico/stdlib.h"
-const uint PIN_LED = 25; // Status LED
+#include "endian.h"
+#include "hw.h"
+#include "picoboot.pio.h"
+#include "pio.h"
+#include "status_led.h"
+#include "version.h"
extern const uint32_t __payload[];
extern const uint32_t __payload_end[];
-const uint32_t payload_magic0 = 0x49504C42; // "IPLB"
-const uint32_t payload_magic1 = 0x4F4F5420; // "OOT "
-const uint32_t payload_magic2 = 0x5049434F; // "PICO"
+static const uint32_t payload_magic0 = 0x49504C42; // "IPLB"
+static const uint32_t payload_magic1 = 0x4F4F5420; // "OOT "
+static const uint32_t payload_magic2 = 0x5049434F; // "PICO"
+
+static hw_board_type_t s_board_type;
size_t validate_payload() {
if (BigEndian32(__payload[0]) != payload_magic0) {
@@ -51,18 +58,21 @@ size_t validate_payload() {
void main()
{
- // Initialize and light up builtin LED, it will basically
- // act as a power LED.
- // TODO: Use the LED to signalize system faults?
- gpio_init(PIN_LED);
- gpio_set_dir(PIN_LED, GPIO_OUT);
- gpio_put(PIN_LED, true);
+ stdio_init_all();
+ adc_init();
+ s_board_type = hw_detect_board_type();
+
+ printf("PicoBoot (%s) by webhdx (c) 2025\n", FW_VER_STRING);
+ printf("Board Type: %s\n", hw_board_type_to_string(s_board_type));
size_t payload_size = validate_payload();
if (payload_size == SIZE_MAX) {
+ printf("PicoBoot: Invalid payload. Entering infinite loop.\n");
+ status_led_init(s_board_type);
+
while (true) {
sleep_ms(500);
- gpio_xor_mask(1 << PIN_LED);
+ status_led_toggle();
}
}
@@ -136,6 +146,11 @@ void main()
pio_sm_set_enabled(pio, transfer_start_sm, true);
pio_sm_set_enabled(pio, clocked_output_sm, true);
+ printf("PicoBoot: Finished injecting payload. Entering infinite loop.\n");
+
+ status_led_init(s_board_type);
+ status_led_on();
+
while (true) {
tight_loop_contents();
}
diff --git a/src/picoboot.pio b/src/picoboot.pio
index 28b6018..1bb30fb 100644
--- a/src/picoboot.pio
+++ b/src/picoboot.pio
@@ -1,4 +1,4 @@
- ; Copyright (c) 2024 Maciej Kobus
+ ; Copyright (c) 2025 Maciej Kobus
;
; SPDX-License-Identifier: GPL-2.0-only
diff --git a/src/pio.c b/src/pio.c
index db65be8..fecb2bc 100644
--- a/src/pio.c
+++ b/src/pio.c
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2022 Maciej Kobus
+ * Copyright (c) 2025 Maciej Kobus
*
* SPDX-License-Identifier: GPL-2.0-only
*/
diff --git a/src/pio.h b/src/pio.h
index 0d8eec8..375d6f5 100644
--- a/src/pio.h
+++ b/src/pio.h
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2022 Maciej Kobus
+ * Copyright (c) 2025 Maciej Kobus
*
* SPDX-License-Identifier: GPL-2.0-only
*/
diff --git a/src/status_led.c b/src/status_led.c
new file mode 100644
index 0000000..861a960
--- /dev/null
+++ b/src/status_led.c
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include
+#include
+
+#include "hw.h"
+#include "status_led.h"
+#include "status_led/cyw43.h"
+#include "status_led/gpio.h"
+
+typedef int (*led_init_func_p)(void);
+typedef void (*led_op_func_p)(void);
+
+typedef struct {
+ led_init_func_p init;
+ led_op_func_p on;
+ led_op_func_p off;
+ led_op_func_p toggle;
+} status_led_driver_t;
+
+static int led_init_noop(void) {
+ return 0;
+}
+
+static void led_op_noop(void) {
+ // Do nothing
+}
+
+static status_led_driver_t s_led_driver = {
+ .init = led_init_noop,
+ .on = led_op_noop,
+ .off = led_op_noop,
+ .toggle = led_op_noop,
+};
+
+void status_led_init(hw_board_type_t board_type) {
+ if (board_type == HW_BOARD_TYPE_PICO_W || board_type == HW_BOARD_TYPE_PICO_2_W) {
+ s_led_driver.init = status_led_cyw43_init;
+ s_led_driver.on = status_led_cyw43_on;
+ s_led_driver.off = status_led_cyw43_off;
+ s_led_driver.toggle = status_led_cyw43_toggle;
+ } else {
+ s_led_driver.init = status_led_gpio_init;
+ s_led_driver.on = status_led_gpio_on;
+ s_led_driver.off = status_led_gpio_off;
+ s_led_driver.toggle = status_led_gpio_toggle;
+ }
+
+ if (s_led_driver.init() != 0) {
+ s_led_driver.init = led_init_noop;
+ s_led_driver.on = led_op_noop;
+ s_led_driver.off = led_op_noop;
+ s_led_driver.toggle = led_op_noop;
+ }
+}
+
+void status_led_on(void) {
+ s_led_driver.on();
+}
+
+void status_led_off(void) {
+ s_led_driver.off();
+}
+
+void status_led_toggle(void) {
+ s_led_driver.toggle();
+}
diff --git a/src/status_led.h b/src/status_led.h
new file mode 100644
index 0000000..c097ce4
--- /dev/null
+++ b/src/status_led.h
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifndef STATUS_LED_H
+#define STATUS_LED_H
+
+#include "hw.h"
+
+void status_led_init(hw_board_type_t board_type);
+
+void status_led_on(void);
+
+void status_led_off(void);
+
+void status_led_toggle(void);
+
+#endif // STATUS_LED_H
diff --git a/src/status_led/cyw43.c b/src/status_led/cyw43.c
new file mode 100644
index 0000000..4b21b6e
--- /dev/null
+++ b/src/status_led/cyw43.c
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include
+
+#include "pico/cyw43_arch.h"
+#ifdef PICO_RP2040
+#include "boards/pico_w.h"
+#else
+#include "boards/pico2_w.h"
+#endif
+
+#include "cyw43.h"
+
+static bool led_state_cyw43 = false;
+
+int status_led_cyw43_init(void) {
+ if (cyw43_arch_init()) {
+ return 1;
+ }
+
+ led_state_cyw43 = false;
+
+ return 0;
+}
+
+void status_led_cyw43_on(void) {
+ cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
+
+ led_state_cyw43 = true;
+}
+
+void status_led_cyw43_off(void) {
+ cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false);
+
+ led_state_cyw43 = false;
+}
+
+void status_led_cyw43_toggle(void) {
+ if (led_state_cyw43) {
+ status_led_cyw43_off();
+ } else {
+ status_led_cyw43_on();
+ }
+}
\ No newline at end of file
diff --git a/src/status_led/cyw43.h b/src/status_led/cyw43.h
new file mode 100644
index 0000000..3d380af
--- /dev/null
+++ b/src/status_led/cyw43.h
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifndef STATUS_LED_CYW43_DRIVER_H
+#define STATUS_LED_CYW43_DRIVER_H
+
+int status_led_cyw43_init(void);
+
+void status_led_cyw43_on(void);
+
+void status_led_cyw43_off(void);
+
+void status_led_cyw43_toggle(void);
+
+#endif
\ No newline at end of file
diff --git a/src/status_led/gpio.c b/src/status_led/gpio.c
new file mode 100644
index 0000000..1687cf6
--- /dev/null
+++ b/src/status_led/gpio.c
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include "gpio.h"
+#ifdef PICO_RP2040
+#include "boards/pico.h"
+#else
+#include "boards/pico2.h"
+#endif
+
+static bool led_state_gpio = false;
+
+int status_led_gpio_init(void) {
+ gpio_init(PICO_DEFAULT_LED_PIN);
+ gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
+
+ led_state_gpio = false;
+
+ return 0;
+}
+
+void status_led_gpio_on(void) {
+ gpio_put(PICO_DEFAULT_LED_PIN, true);
+
+ led_state_gpio = true;
+}
+
+void status_led_gpio_off(void) {
+ gpio_put(PICO_DEFAULT_LED_PIN, false);
+
+ led_state_gpio = false;
+}
+
+void status_led_gpio_toggle(void) {
+ if (led_state_gpio) {
+ status_led_gpio_off();
+ } else {
+ status_led_gpio_on();
+ }
+}
diff --git a/src/status_led/gpio.h b/src/status_led/gpio.h
new file mode 100644
index 0000000..8cf34c0
--- /dev/null
+++ b/src/status_led/gpio.h
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifndef STATUS_LED_GPIO_DRIVER_H
+#define STATUS_LED_GPIO_DRIVER_H
+
+#include "pico/stdlib.h"
+
+int status_led_gpio_init(void);
+
+void status_led_gpio_on(void);
+
+void status_led_gpio_off(void);
+
+void status_led_gpio_toggle(void);
+
+#endif
\ No newline at end of file
diff --git a/src/version.h.in b/src/version.h.in
index c4e94c9..1a6ea1b 100644
--- a/src/version.h.in
+++ b/src/version.h.in
@@ -1,3 +1,22 @@
-#pragma once
+/**
+ * Copyright (c) 2025 Maciej Kobus
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
-#define PICOBOOT_VERSION "${GIT_REPO_VERSION}"
\ No newline at end of file
+#ifndef VERSION_H
+#define VERSION_H
+
+#include
+
+#define FW_VER_STRING "${FW_VER_STRING}"
+
+static const uint8_t FW_VER_MAJOR = ${FW_VER_MAJOR};
+static const uint8_t FW_VER_MINOR = ${FW_VER_MINOR};
+static const uint8_t FW_VER_PATCH = ${FW_VER_PATCH};
+
+static const uint32_t FW_VER_FULL = ((uint32_t)FW_VER_MAJOR << 24) |
+ ((uint32_t)FW_VER_MINOR << 16) |
+ ((uint32_t)FW_VER_PATCH << 8);
+
+#endif // VERSION_H
diff --git a/tools/build.sh b/tools/build.sh
new file mode 100755
index 0000000..80214cc
--- /dev/null
+++ b/tools/build.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+# -----------------------------------------------------------------------
+# build.sh - Builds PicoBoot firmware files for different platforms
+# -----------------------------------------------------------------------
+# Purpose:
+# Builds PicoBoot firmware files for both Raspberry Pi Pico and Pico 2
+# platforms. Processes gekkoboot.dol into payload files and creates
+# universal payload that works on both boards.
+# Outputs uf2 files into dist/ directory.
+#
+# Usage:
+# ./build.sh
+# -----------------------------------------------------------------------
+
+set -e
+
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+BLUE='\033[0;34m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+platforms=("rp2040" "rp2350")
+boards=("pico_w" "pico2_w")
+boards_arch=("pico" "pico2")
+families=("rp2040" "rp2350-arm-s")
+build_type="RelWithDebInfo"
+
+num_configs=${#platforms[@]}
+
+if [ ! -f "payload.dol" ]; then
+ echo -e "${RED}Error: payload.dol file not found${NC}"
+ exit 1
+fi
+
+echo -e "${BLUE}##########################################################${NC}"
+echo -e "🚀 ${YELLOW}Generating payload uf2 files:${NC}"
+echo -e "📂 ${YELLOW}Input file:${NC} ${GREEN}payload.dol${NC}"
+echo -e "${BLUE}##########################################################${NC}"
+
+if [ ! -d "dist" ]; then
+ mkdir dist
+fi
+
+echo -e "\n🔨 ${YELLOW}Building payload uf2 file for Pico...${NC}"
+tools/process_ipl.py dist/payload_pico.uf2 payload.dol rp2040
+
+echo -e "\n🔨 ${YELLOW}Building payload uf2 file for Pico 2...${NC}"
+tools/process_ipl.py dist/payload_pico2.uf2 payload.dol rp2350
+
+echo -e "\n🔨 ${YELLOW}Building universal payload uf2 file...${NC}"
+cat dist/payload_pico.uf2 dist/payload_pico2.uf2 > dist/payload_universal.uf2
+
+for (( i=0; i ")
+ print(f"Usage: {sys.argv[0]}