SkillAgentSearch skills...

Kocherga

Robust platform-agnostic Cyphal/DroneCAN bootloader for deeply embedded systems

Install / Use

/learn @Zubax/Kocherga

README

Kochergá

CI Forum Forum

Kochergá is a robust platform-agnostic Cyphal bootloader for deeply embedded systems.

Technical support is provided on the OpenCyphal Forum.

A standard-compliant implementation of the software update server is provided in Yakut.

Features

  • Portability -- Kochergá is written in standard C++17 and is distributed as a header-only library with no external dependencies.

  • Robustness -- Kochergá is brick-proof. The application (i.e., firmware) update process can be interrupted at any point (e.g., by turning off the power supply or by disconnecting the interface), and it is guaranteed that the device will always end up in a known valid state. If a dysfunctional application image is uploaded, Kochergá can regain control after a watchdog reset.

  • Safety -- Kochergá verifies the correctness of the application image with a 64-bit hash before every boot. Kochergá's own codebase features extensive test coverage.

  • Multiple supported transports:

    • Cyphal/CAN + DroneCAN -- the protocol version is auto-detected at runtime.
    • Cyphal/serial
    • More may appear in the future -- new transports are easy to add.

Usage

Integration

The entire library is contained in the header file kocherga.hpp; protocol implementations are provided each in a separate header file named kocherga_*.hpp. Kochergá does not have any compilation units of its own.

To integrate Kochergá into your application, just include this repository as a git subtree/submodule, or simply copy-paste the required header files into your source tree.

For reference, a typical implementation on an ARM Cortex M4 MCU supporting Cyphal/serial (USB+UART), Cyphal/CAN, and DroneCAN (autodetection) would set you back by about ~32K of flash.

Application signature

The bootloader looks for an instance of the AppInfo structure located in the ROM image of the application at every boot. Only if a valid AppInfo structure is found the application will be launched. It is recommended to allocate the structure closer to the beginning of the image in order to speed up its verification. The structure is defined as follows:

| Offset | Type | Description | |--------|------------|--------------------------------------------------------------------------------------------------| | -16 | uint64 | Constant value 0x5E4415146FC0C4C7 used for locating the descriptor and detecting the byte order. | | -8 | uint8[8] | Set to APDesc00; used for compatibility with legacy deployments. | | 0 | uint64 | CRC-64-WE of the entire application image when this field itself is set to zero. | | 8 | uint32 | Size of the application image, in bytes. Note that the image must be padded to eight bytes. | | 12 | void32 | Reserved. Used to contain the 32-bit version control system revision ID; see replacement below. | | 16 | uint8[2] | Major and minor semantic version numbers. | | 18 | uint8 | Flags: 1 - this is a release build; 2 - this is a dirty build (uncommitted changes present). | | 19 | void8 | Reserved; set to 0. | | 20 | uint32 | UNIX UTC build timestamp; i.e., the number of seconds since 1970-01-01T00:00:00Z. | | 24 | uint64 | Version control system (VCS) revision ID (e.g., the git commit hash). | | 32 | void64 | Reserved. | | 40 | void64 | Reserved. |

When computing the application image CRC, the process will eventually encounter the location where the CRC itself is stored. In order to avoid recursive dependency, the CRC storage location must be replaced with zero bytes when computing/verifying the CRC. The parameters of the CRC-64 algorithm are the following:

  • Initial value: 0xFFFF'FFFF'FFFF'FFFF
  • Polynomial: 0x42F0'E1EB'A9EA'3693
  • Reverse: no
  • Output xor: 0xFFFF'FFFF'FFFF'FFFF
  • Check: 0x62EC'59E3'F1A4'F00A

The CRC and size fields cannot be populated until after the application binary is compiled and linked. One possible way to populate these fields is to initialize them with zeroes in the source code, and then use the script tools/kocherga_image.py after the binary is generated to update the fields with their actual values. The script can be invoked from the build system (e.g., from a Makefile rule) trivially as follows:

kocherga_image.py application-name-goes-here.bin

The output will be stored in a file whose name follows the pattern expected by the firmware update server implemented in the Yakut CLI tool.

State machine

The following diagram documents the state machine of the bootloader:

Kochergá State Machine Diagram

The bootloader states are mapped onto Cyphal node states as follows:

| Bootloader state | Node mode | Node health | Vendor-specific status code | |---------------------|-------------------|-------------|------------------------------------| | NoAppToBoot | SOFTWARE_UPDATE | WARNING | 0 | | BootDelay | SOFTWARE_UPDATE | NOMINAL | 0 | | BootCancelled | SOFTWARE_UPDATE | ADVISORY | 0 | | AppUpdateInProgress | SOFTWARE_UPDATE | NOMINAL | number of read requests, always >0 |

API usage

The following snippet demonstrates how to integrate Kochergá into your bootloader executable. User-provided functions are shown in SCREAMING_SNAKE_CASE(). This is a stripped-down example; the full API documentation is available in the header files.

The integration test application available under /tests/integration/bootloader/ may also be a good reference.

Configuring Kochergá

Random number generation

Kochergá needs a source of random numbers regardless of the transport used. You need to provide a definition of kocherga::getRandomByte() -> std::uint8_t for the library to build successfully. You can use this implementation based on std::rand():

#include <cstdlib>

auto kocherga::getRandomByte() -> std::uint8_t
{
    const auto product =
        static_cast<std::uint64_t>(std::rand()) * static_cast<std::uint64_t>(std::numeric_limits<std::uint8_t>::max());
    return static_cast<std::uint8_t>(product / RAND_MAX);
}

int main()
{
    std::srand(GET_ENTROPY());
    // bootloader implementation below
    return 0;
}

An alternative is to use a generator from C++ standard library:

#include <random>

auto kocherga::getRandomByte() -> std::uint8_t
{
    static std::mt19937 rd{GET_ENTROPY()};
    return static_cast<std::uint8_t>(rd() * std::numeric_limits<std::uint8_t>::max() / std::mt19937::max());
}

In both cases beware that you need to initialize the psudorandom sequence with GET_ENTROPY(). This function should retrieve a sufficiently random or unique value (such as the number of seconds since epoch). Look for more information in the respective documentation of both std::srand and std::mt19937.

Providing custom assert macros

Kochergá uses the assert macro from the stadard C library to check its invariants. If this is undesireable in your project, you can redefine the following macros. You can do this before including Kochergá or globally in your build system.

#define KOCHERGA_ASSERT(x) some_other_assert(x, ...);
#include <kocherga.hpp>

You can disable all internal assertions like this:

#define KOCHERGA_ASSERT(x) (void)(x);
#include <kocherga.hpp>
Compatibility with environments with missing operator delete

Kocherga does not require heap but some toolchains may refuse to link the code if operator delete is not available. If your environment does not define operator delete, you can provide a custom definition in your code like this:

void operator delete(void*) noexcept { std::abort(); }

This is needed as Kochergá uses virtual destructors, code generation for which includes an operator delete even if deleting an object through pointer to its base class is not used in your entire application.

ROM interface

The ROM backend abstracts the specifics of reading and writing your ROM (usually this is the on-chip flash memory). Be sure to avoid overwriting the bootloader while modifying the ROM.

class MyROMBackend final : public kocherga::IROMBackend
{
    auto write(const std::size_t offset, const std::byte* const data, const std::size_t size) override
        -> std::optional<std::size_t>
    {
        if (WRITE_ROM(offset, data, size))
        {
            return size;
        }
        return {};  // Failure case
    }

    auto read(const std::size_t offset, std::byte* const out_data, const std::size_t size) const override
        -> std::size_t
    {
        return READ_ROM(offset, out_data, size);  // Return the number of bytes read (may be less than size).
    }
};

Media layer interfaces

Transport implemen

View on GitHub
GitHub Stars53
CategoryDevelopment
Updated13d ago
Forks15

Languages

C++

Security Score

100/100

Audited on Mar 15, 2026

No findings