Skip to content

feat: add build two target and serial flasher example#26

Merged
YanKE01 merged 1 commit into
masterfrom
feat/p4_flash_tool
Dec 10, 2025
Merged

feat: add build two target and serial flasher example#26
YanKE01 merged 1 commit into
masterfrom
feat/p4_flash_tool

Conversation

@YanKE01
Copy link
Copy Markdown
Owner

@YanKE01 YanKE01 commented Nov 22, 2025

Summary by CodeRabbit

Release Notes

  • New Features
    • Added example project demonstrating serial flashing between ESP32 targets, including real-time progress monitoring and comprehensive error handling during the flashing process.
    • Includes configuration support for ESP32-P4 with 16MB flash storage and requires the esp-serial-flasher component.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 22, 2025

Walkthrough

This PR introduces a complete build-two-target example for ESP-IDF that demonstrates configuring and building one target (ESP32-C6) as a binary, embedding it, and using a second host target as a serial flasher to program the embedded binary to the slave device.

Changes

Cohort / File(s) Summary
Build Two Target Main CMake Configuration
examples/system/build_two_target/CMakeLists.txt
Adds top-level CMake boilerplate (3.16 minimum), creates spiffs directory, defines a custom command that builds example_c6 target, generates merged-binary.bin, and creates a build_example_c6 custom target for default inclusion.
Main Component Build Configuration
examples/system/build_two_target/main/CMakeLists.txt
Registers main component with idf_component_register, embeds the generated merged-binary.bin file, and adds conditional dependency on build_example_c6 target.
Main Component Manifest
examples/system/build_two_target/main/idf_component.yml
Defines component dependencies including esp-serial-flasher (^1.10.0) and requires ESP-IDF ≥4.1.0.
Example C6 Target CMake Setup
examples/system/build_two_target/example_c6/CMakeLists.txt, examples/system/build_two_target/example_c6/main/CMakeLists.txt
Adds CMake boilerplate and component registration for the C6 example sub-project.
Serial Flasher Application
examples/system/build_two_target/main/build_two_target.c
Implements a serial-based flasher application that configures UART, connects to a slave device, erases and programs the embedded merged binary, and spawns a monitor task to forward slave serial output. Includes error mapping, baud-rate negotiation fallback, and real-time progress tracking.
Example C6 Application
examples/system/build_two_target/example_c6/main/example_c6.c
Defines a simple FreeRTOS app_main() that prints "Hello, World!" every second.
Project Configuration
examples/system/build_two_target/sdkconfig.defaults
Sets default SDK config with esp32p4 target and 16MB flash size.

Sequence Diagram

sequenceDiagram
    participant CMake as CMake Build System
    participant Host as Host (ESP32-P4)
    participant Slave as Slave (ESP32-C6)
    
    rect rgb(200, 220, 255)
    Note over CMake: Build Configuration Phase
    CMake->>CMake: Build example_c6 target
    CMake->>CMake: Generate merged-binary.bin
    CMake->>Host: Embed merged-binary.bin into Host app
    end
    
    rect rgb(220, 200, 255)
    Note over Host,Slave: Runtime Execution
    Host->>Host: app_main() initializes serial flasher
    Host->>Slave: Connect via UART
    Host->>Slave: Establish connection & detect target
    Host->>Slave: Negotiate baud rate (or fallback)
    Host->>Slave: Erase flash region
    Host->>Host: Stream merged-binary.bin in chunks
    Host->>Slave: Program chunks to flash memory
    Host->>Host: Update progress indicator
    Host->>Slave: Reset target
    Slave->>Slave: Boot and run app_main()
    Host->>Host: Spawn slave_monitor task
    Slave-->>Host: Serial output (Hello, World!...)
    Host->>Host: Forward slave output to console
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • build_two_target.c — Logic-dense serial flasher implementation with UART configuration, connection establishment, error mapping, baud-rate negotiation with fallback, flash programming with chunking and progress tracking, and serial monitor task spawning. Verify error handling paths, buffer management, and UART state transitions.
  • CMakeLists.txt files — Custom command construction and target dependencies; verify build artifact paths and command sequencing.
  • idf_component.yml — Dependency specification; confirm version constraints are correct.

Poem

🐰 Two targets hopping in the CMake dawn,
One builds binaries, the other flashes on,
A serial dance of programs merged with care,
While Hello World echoes through the air! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: it adds a new example that demonstrates building two targets and includes a serial flasher application.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/p4_flash_tool

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
examples/system/build_two_target/main/CMakeLists.txt (1)

1-9: if(TARGET build_example_c6) block likely never fires due to CMake ordering

main/CMakeLists.txt is processed as part of project(build_two_target) in the top-level CMakeLists, while the build_example_c6 custom target is only created after that project() call. That means, on first configure, if(TARGET build_example_c6) will evaluate to false, so add_dependencies(${COMPONENT_LIB} build_example_c6) is never applied, despite the comment saying it should enforce the dependency. (cmake.org)

You already make build_example_c6 an ALL target and generate spiffs/merged-binary.bin via an add_custom_command, so this doesn’t currently break the build, but it does make the intent here misleading.

Consider one of these cleanups:

  • Move the dependency wiring into the top-level examples/system/build_two_target/CMakeLists.txt, after add_custom_target(build_example_c6 ...), e.g.:
-# Ensure that merged-binary.bin has been generated before building the main component
-# Add a dependency to make sure example_c6 is built before building the main component
-if(TARGET build_example_c6)
-    add_dependencies(${COMPONENT_LIB} build_example_c6)
-endif()
+if(TARGET build_example_c6 AND TARGET ${COMPONENT_LIB})
+    add_dependencies(${COMPONENT_LIB} build_example_c6)
+endif()
  • Or, alternatively, define build_example_c6 earlier in the top-level CMake so the existing if(TARGET ...) here becomes true on first configure.
examples/system/build_two_target/CMakeLists.txt (1)

5-28: Make the idf.py custom command more portable and robust

The overall flow (build example_c6, run idf.py merge-bin, then copy merged-binary.bin into spiffs/) is good, but the current implementation has a couple of portability/robustness issues:

  • COMMAND bash -c "cd ... && idf.py ..." assumes a POSIX shell and idf.py on PATH, which will be brittle on Windows and in some IDF setups.
  • Embedding cd and a long shell pipeline in a single string also makes quoting/space-in-path issues more likely.

You can achieve the same effect in a more CMake/IDF‑idiomatic and cross‑platform way by leveraging ${IDF_PY} (set by IDF’s project.cmake) and separate COMMANDs, e.g.:

-add_custom_command(
-    OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/spiffs/merged-binary.bin
-    COMMAND bash -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/example_c6 && idf.py set-target esp32c6 && idf.py build && idf.py merge-bin"
-    COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/spiffs
-    COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/example_c6/build/merged-binary.bin ${CMAKE_CURRENT_SOURCE_DIR}/spiffs/merged-binary.bin
-    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-    COMMENT "Building example_c6 with target esp32c6 and copying merged-binary.bin to spiffs"
-    VERBATIM
-)
+add_custom_command(
+    OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/spiffs/merged-binary.bin
+    COMMAND ${IDF_PY} -C ${CMAKE_CURRENT_SOURCE_DIR}/example_c6 set-target esp32c6
+    COMMAND ${IDF_PY} -C ${CMAKE_CURRENT_SOURCE_DIR}/example_c6 build
+    COMMAND ${IDF_PY} -C ${CMAKE_CURRENT_SOURCE_DIR}/example_c6 merge-bin
+    COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/spiffs
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+            ${CMAKE_CURRENT_SOURCE_DIR}/example_c6/build/merged-binary.bin
+            ${CMAKE_CURRENT_SOURCE_DIR}/spiffs/merged-binary.bin
+    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+    COMMENT "Building example_c6 with target esp32c6 and copying merged-binary.bin to spiffs"
+    VERBATIM
+)

This keeps the logic the same but avoids hard‑coding a shell and improves behavior on non‑POSIX hosts.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e6edc7 and 0e5a050.

⛔ Files ignored due to path filters (1)
  • examples/system/build_two_target/spiffs/merged-binary.bin is excluded by !**/*.bin
📒 Files selected for processing (8)
  • examples/system/build_two_target/CMakeLists.txt (1 hunks)
  • examples/system/build_two_target/example_c6/CMakeLists.txt (1 hunks)
  • examples/system/build_two_target/example_c6/main/CMakeLists.txt (1 hunks)
  • examples/system/build_two_target/example_c6/main/example_c6.c (1 hunks)
  • examples/system/build_two_target/main/CMakeLists.txt (1 hunks)
  • examples/system/build_two_target/main/build_two_target.c (1 hunks)
  • examples/system/build_two_target/main/idf_component.yml (1 hunks)
  • examples/system/build_two_target/sdkconfig.defaults (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
examples/system/build_two_target/example_c6/main/example_c6.c (1)
examples/system/build_two_target/main/build_two_target.c (1)
  • app_main (46-163)
examples/system/build_two_target/main/build_two_target.c (1)
examples/system/build_two_target/example_c6/main/example_c6.c (1)
  • app_main (5-11)
🪛 Clang (14.0.6)
examples/system/build_two_target/example_c6/main/example_c6.c

[error] 1-1: 'stdio.h' file not found

(clang-diagnostic-error)

examples/system/build_two_target/main/build_two_target.c

[error] 1-1: 'sys/param.h' file not found

(clang-diagnostic-error)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
examples/system/build_two_target/example_c6/main/example_c6.c (1)

5-10: Simple target app looks good

The example app_main is minimal, uses vTaskDelay correctly, and is fine as a slave “Hello, World!” payload.

examples/system/build_two_target/example_c6/main/CMakeLists.txt (1)

1-2: Component registration is idiomatic

idf_component_register setup matches the single‑source component and local includes; nothing to change here.

examples/system/build_two_target/example_c6/CMakeLists.txt (1)

1-6: Boilerplate CMake is correct

The IDF project boilerplate is in the expected order and should work fine for the standalone example_c6 project.

examples/system/build_two_target/sdkconfig.defaults (1)

1-5: Defaults are coherent with the example

Target and flash size defaults are consistent and minimal; this is fine for a sample. Just ensure your actual host board matches esp32p4 + 16MB flash.

examples/system/build_two_target/main/idf_component.yml (1)

1-17: Component manifest is well-formed

YAML structure and minimal idf version declaration look correct for use with the Component Manager.

Comment on lines +1 to +25
#include <sys/param.h>
#include <string.h>
#include "esp_err.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp32_port.h"
#include "esp_loader.h"

static const char *TAG = "serial_flasher";
extern const uint8_t merged_binary_bin_start[] asm("_binary_merged_binary_bin_start");
extern const uint8_t merged_binary_bin_end[] asm("_binary_merged_binary_bin_end");

static const char *get_error_string(const esp_loader_error_t error)
{
const char *mapping[ESP_LOADER_ERROR_INVALID_RESPONSE + 1] = {
"NONE", "UNKNOWN", "TIMEOUT", "IMAGE SIZE",
"INVALID MD5", "INVALID PARAMETER", "INVALID TARGET",
"UNSUPPORTED CHIP", "UNSUPPORTED FUNCTION", "INVALID RESPONSE"
};

assert(error <= ESP_LOADER_ERROR_INVALID_RESPONSE);

return mapping[error];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add explicit headers for assert, FreeRTOS, and printf

This file uses assert(), vTaskDelay(), xTaskCreate(), portTICK_PERIOD_MS, and printf() but doesn’t include the corresponding headers locally. That relies on transitive includes from other IDF headers, which is fragile and will often fail with -Werror=implicit-function-declaration in stricter builds.

To make this unit self‑contained and robust, add the explicit headers near the top:

-#include <sys/param.h>
-#include <string.h>
-#include "esp_err.h"
-#include "esp_log.h"
-#include "driver/uart.h"
-#include "driver/gpio.h"
-#include "esp32_port.h"
-#include "esp_loader.h"
+#include <sys/param.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_err.h"
+#include "esp_log.h"
+#include "driver/uart.h"
+#include "driver/gpio.h"
+#include "esp32_port.h"
+#include "esp_loader.h"

This avoids any dependence on indirect includes and keeps the example more portable across IDF versions and toolchains.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <sys/param.h>
#include <string.h>
#include "esp_err.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp32_port.h"
#include "esp_loader.h"
static const char *TAG = "serial_flasher";
extern const uint8_t merged_binary_bin_start[] asm("_binary_merged_binary_bin_start");
extern const uint8_t merged_binary_bin_end[] asm("_binary_merged_binary_bin_end");
static const char *get_error_string(const esp_loader_error_t error)
{
const char *mapping[ESP_LOADER_ERROR_INVALID_RESPONSE + 1] = {
"NONE", "UNKNOWN", "TIMEOUT", "IMAGE SIZE",
"INVALID MD5", "INVALID PARAMETER", "INVALID TARGET",
"UNSUPPORTED CHIP", "UNSUPPORTED FUNCTION", "INVALID RESPONSE"
};
assert(error <= ESP_LOADER_ERROR_INVALID_RESPONSE);
return mapping[error];
}
#include <sys/param.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp32_port.h"
#include "esp_loader.h"
static const char *TAG = "serial_flasher";
extern const uint8_t merged_binary_bin_start[] asm("_binary_merged_binary_bin_start");
extern const uint8_t merged_binary_bin_end[] asm("_binary_merged_binary_bin_end");
static const char *get_error_string(const esp_loader_error_t error)
{
const char *mapping[ESP_LOADER_ERROR_INVALID_RESPONSE + 1] = {
"NONE", "UNKNOWN", "TIMEOUT", "IMAGE SIZE",
"INVALID MD5", "INVALID PARAMETER", "INVALID TARGET",
"UNSUPPORTED CHIP", "UNSUPPORTED FUNCTION", "INVALID RESPONSE"
};
assert(error <= ESP_LOADER_ERROR_INVALID_RESPONSE);
return mapping[error];
}
🧰 Tools
🪛 Clang (14.0.6)

[error] 1-1: 'sys/param.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
In examples/system/build_two_target/main/build_two_target.c around lines 1 to
25, the file uses assert(), vTaskDelay(), xTaskCreate(), portTICK_PERIOD_MS and
printf() but lacks their direct headers; add explicit includes near the top:
<assert.h>, <stdio.h>, "freertos/FreeRTOS.h", and "freertos/task.h" so the file
is self-contained and no longer depends on transitive IDF headers.

Comment on lines +27 to +42
// Max line size
#define BUF_LEN 128
static uint8_t buf[BUF_LEN] = {0};

void slave_monitor(void *arg)
{
#if (HIGHER_BAUDRATE != 115200)
uart_flush_input(UART_NUM_1);
uart_flush(UART_NUM_1);
uart_set_baudrate(UART_NUM_1, 115200);
#endif
while (1) {
int rxBytes = uart_read_bytes(UART_NUM_1, buf, BUF_LEN, 100 / portTICK_PERIOD_MS);
buf[rxBytes] = '\0';
printf("%s", buf);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix buffer underflow/overflow in slave_monitor when handling UART reads

slave_monitor currently assumes uart_read_bytes always returns a value in [0, BUF_LEN - 1]:

int rxBytes = uart_read_bytes(UART_NUM_1, buf, BUF_LEN, 100 / portTICK_PERIOD_MS);
buf[rxBytes] = '\0';
printf("%s", buf);

However, per ESP‑IDF docs, uart_read_bytes returns -1 on error and otherwise a value in [0, length] (inclusive). (docs.espressif.com)

That means:

  • If rxBytes == -1, buf[rxBytes] underflows and corrupts memory.
  • If rxBytes == BUF_LEN, buf[rxBytes] writes one byte past the end of the buffer.
  • If rxBytes == 0, you end up writing buf[0] = '\0' and printing an empty string, which is harmless but still doesn’t guard against the error case.

This is a real memory‑safety bug that can crash or destabilize the host.

A safe pattern is to reserve space for the terminator and only null‑terminate when you’ve read a positive number of bytes within range:

-// Max line size
-#define BUF_LEN 128
-static uint8_t buf[BUF_LEN] = {0};
+// Max line size (not counting terminator)
+#define BUF_LEN 128
+static uint8_t buf[BUF_LEN + 1] = {0};

...

-    while (1) {
-        int rxBytes = uart_read_bytes(UART_NUM_1, buf, BUF_LEN, 100 / portTICK_PERIOD_MS);
-        buf[rxBytes] = '\0';
-        printf("%s", buf);
-    }
+    while (1) {
+        int rxBytes = uart_read_bytes(UART_NUM_1, buf, BUF_LEN, 100 / portTICK_PERIOD_MS);
+        if (rxBytes > 0 && rxBytes <= BUF_LEN) {
+            buf[rxBytes] = '\0';
+            printf("%s", (char *)buf);
+        }
+        // Optionally handle rxBytes < 0 (error) with a log if you want visibility.
+    }

This guards both the negative error case and the full‑buffer case while keeping behavior unchanged for normal reads.

🤖 Prompt for AI Agents
In examples/system/build_two_target/main/build_two_target.c around lines 27-42,
the code writes buf[rxBytes] without validating rxBytes which can be -1 or equal
to BUF_LEN; change the logic to check the return: if rxBytes <= 0 handle the
error/empty read (e.g., continue or set buf[0] = '\0' and skip printing),
otherwise if rxBytes >= BUF_LEN clamp rxBytes to BUF_LEN-1, then null-terminate
with buf[rxBytes] = '\0' and call printf; this prevents underflow and overflow
while preserving behavior for normal reads.

@YanKE01 YanKE01 merged commit 4d4338f into master Dec 10, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant