Skip to content

Commit 3116920

Browse files
v0.2 (#9)
* VolatileStorage<>: Do not enforce std::is_trivial_v<Container> * Add optional support for legacy app descriptors * Add Bootloader::getNumberOfNodes() * Promote CRC64 to public API * Add kocherga::can::TxQueue<> * CANAcceptanceFilterConfig::makePromiscuous(): do not construct {0,0} to enhance compatibility * Improve logging messages * Drop the last log report because it is usually not transmitted: the app is booted before the message can be sent
1 parent 667d7ba commit 3116920

11 files changed

+343
-59
lines changed

.clang-tidy

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Checks: >-
2323
-cert-msc30-c,
2424
-cert-msc50-cpp,
2525
-*-easily-swappable-parameters,
26+
-*-owning-memory,
27+
-*-malloc,
2628
WarningsAsErrors: '*'
2729
HeaderFilterRegex: '.*'
2830
AnalyzeTemporaryDtors: false

README.md

+47-16
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
Technical support is provided on the [UAVCAN Forum](https://forum.uavcan.org/).
99

10-
A standard-compliant implementation of the firmware update server is provided in
10+
A standard-compliant implementation of the software update server is provided in
1111
[Yakut](https://github.com/UAVCAN/yakut#updating-node-software).
1212

1313
## Features
@@ -38,6 +38,9 @@ separate header file named `kocherga_*.hpp`. Kochergá does not have any compila
3838
To integrate Kochergá into your application, just include this repository as a git subtree/submodule, or simply
3939
copy-paste the required header files into your source tree.
4040

41+
For reference, a typical implementation on an ARM Cortex M4 MCU supporting
42+
UAVCAN/serial (USB+UART) and UAVCAN/CAN (v1+v0) would set you back by about ~32K of flash.
43+
4144
### Application signature
4245

4346
The bootloader looks for an instance of the `AppInfo` structure located in the ROM image of the application at every
@@ -110,7 +113,7 @@ The integration test application available under `/tests/integration/bootloader/
110113
The ROM backend abstracts the specifics of reading and writing your ROM (usually this is the on-chip flash memory).
111114

112115
```c++
113-
class MyROMBackend : public kocherga::IROMBackend
116+
class MyROMBackend final : public kocherga::IROMBackend
114117
{
115118
auto write(const std::size_t offset, const std::byte* const data, const std::size_t size) override
116119
-> std::optional<std::size_t>
@@ -136,7 +139,7 @@ Transport implementations --- UAVCAN/CAN, UAVCAN/serial, etc., depending on whic
136139
are interfaced with your hardware as follows.
137140

138141
```c++
139-
class MySerialPort : public kocherga::serial::ISerialPort
142+
class MySerialPort final : public kocherga::serial::ISerialPort
140143
{
141144
auto receive() -> std::optional<std::uint8_t> override
142145
{
@@ -152,12 +155,13 @@ class MySerialPort : public kocherga::serial::ISerialPort
152155
```
153156

154157
```c++
155-
class MyCANDriver : public kocherga::can::ICANDriver
158+
class MyCANDriver final : public kocherga::can::ICANDriver
156159
{
157160
auto configure(const Bitrate& bitrate,
158161
const bool silent,
159162
const kocherga::can::CANAcceptanceFilterConfig& filter) -> std::optional<Mode> override
160163
{
164+
tx_queue_.clear();
161165
CAN_CONFIGURE(bitrate, silent, filter);
162166
return Mode::FD; // Or Mode::Classic if CAN FD is not supported by the CAN controller.
163167
}
@@ -167,20 +171,37 @@ class MyCANDriver : public kocherga::can::ICANDriver
167171
const std::uint8_t payload_size,
168172
const void* const payload) -> bool override
169173
{
170-
if (force_classic_can)
171-
{
172-
return CAN_PUSH_CLASSIC(extended_can_id, payload_size, payload); // No DLE, no BRS -- Classic only.
173-
}
174-
else
175-
{
176-
return CAN_PUSH_FD(extended_can_id, payload_size, payload);
177-
}
174+
const std::chrono::microseconds now = GET_TIME_SINCE_BOOT();
175+
// You can use tx_queue_.size() to limit maximum depth of the queue.
176+
const bool ok = tx_queue_.push(now, force_classic_can, extended_can_id, payload_size, payload);
177+
pollTxQueue(now);
178+
return ok;
178179
}
179180

180181
auto pop(PayloadBuffer& payload_buffer) -> std::optional<std::pair<std::uint32_t, std::uint8_t>> override
181182
{
183+
pollTxQueue(); // Check if the HW TX mailboxes are ready to accept the next frame from the SW queue.
182184
return CAN_POP(payload_buffer.data()); // The return value is optional(can_id, payload_size).
183185
}
186+
187+
void pollTxQueue(const std::chrono::microseconds now)
188+
{
189+
if (const auto* const item = tx_queue_.peek()) // Take the top frame from the prioritized queue.
190+
{
191+
const bool expired = now > (item->timestamp + kocherga::can::SendTimeout); // Drop expired frames.
192+
if (expired || CAN_PUSH(item->force_classic_can, // force_classic_can means no DLE, no BRS.
193+
item->extended_can_id,
194+
item->payload_size,
195+
item->payload))
196+
{
197+
tx_queue_.pop(); // Enqueued into the HW TX mailbox or expired -- remove from the SW queue.
198+
}
199+
}
200+
}
201+
202+
// Some CAN drivers come with built-in queue (e.g., SocketCAN), in which case this will not be needed.
203+
// The recommended heap is https://github.com/pavel-kirienko/o1heap.
204+
kocherga::can::TxQueue<void*(*)(std::size_t), void(*)(void*)> tx_queue_(&MY_MALLOC, &MY_FREE);
184205
};
185206
```
186207
@@ -234,6 +255,9 @@ int main()
234255
MyROMBackend rom_backend;
235256
kocherga::SystemInfo system_info = GET_SYSTEM_INFO();
236257
kocherga::Bootloader boot(rom_backend, system_info, MaxAppSize, args && args->linger);
258+
// It's a good idea to check if the app is valid and safe to boot before adding the nodes.
259+
// This way you can skip the potentially slow or disturbing interface initialization on the happy path.
260+
// You can do it by calling poll() here once.
237261

238262
// Add a UAVCAN/serial node to the bootloader instance.
239263
MySerialPort serial_port;
@@ -245,9 +269,9 @@ int main()
245269
boot.addNode(&serial_node);
246270

247271
// Add a UAVCAN/CAN node to the bootloader instance.
248-
std::optional<kocherga::ICANDriver::Bitrate> can_bitrate;
249-
std::optional<std::uint8_t> uavcan_can_version;
250-
std::optional<kocherga::NodeID> uavcan_can_node_id;
272+
std::optional<kocherga::can::ICANDriver::Bitrate> can_bitrate;
273+
std::optional<std::uint8_t> uavcan_can_version;
274+
std::optional<kocherga::NodeID> uavcan_can_node_id;
251275
if (args)
252276
{
253277
if (args->uavcan_can_bitrate.first > 0)
@@ -350,10 +374,17 @@ Note that the application does not need to depend on the Kochergá library.
350374
It is recommended to copy-paste relevant pieces from Kochergá instead; specifically:
351375

352376
- `kocherga::VolatileStorage<>`
353-
- `kocherga::detail::CRC64`
377+
- `kocherga::CRC64`
354378

355379
## Revisions
356380

381+
### v0.2
382+
383+
- Add helper `kocherga::can::TxQueue`.
384+
- Promote `kocherga::CRC64` to public API.
385+
- Add optional support for legacy app descriptors to simplify integration into existing projects.
386+
- Minor doc and API enhancements.
387+
357388
### v0.1
358389

359390
The first revision to go public.

kocherga/kocherga.hpp

+50-31
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#include <type_traits>
1414

1515
#define KOCHERGA_VERSION_MAJOR 0 // NOLINT
16-
#define KOCHERGA_VERSION_MINOR 1 // NOLINT
16+
#define KOCHERGA_VERSION_MINOR 2 // NOLINT
1717

1818
namespace kocherga
1919
{
@@ -226,13 +226,6 @@ class IROMBackend
226226

227227
// --------------------------------------------------------------------------------------------------------------------
228228

229-
/// Internal use only.
230-
namespace detail
231-
{
232-
static constexpr auto BitsPerByte = 8U;
233-
234-
static constexpr std::chrono::microseconds DefaultTransferIDTimeout{2'000'000}; ///< Default taken from Specification.
235-
236229
/// This is used to verify integrity of the application and other data.
237230
/// Note that the firmware CRC verification is a computationally expensive process that needs to be completed
238231
/// in a limited time interval, which should be minimized. This class has been carefully manually optimized to
@@ -278,7 +271,7 @@ class CRC64
278271
for (auto it = std::rbegin(out); it != rend; ++it)
279272
{
280273
*it = static_cast<std::uint8_t>(x);
281-
x >>= BitsPerByte;
274+
x >>= 8U;
282275
}
283276
return out;
284277
}
@@ -297,6 +290,15 @@ class CRC64
297290
std::uint64_t crc_ = Xor;
298291
};
299292

293+
// --------------------------------------------------------------------------------------------------------------------
294+
295+
/// Internal use only.
296+
namespace detail
297+
{
298+
static constexpr auto BitsPerByte = 8U;
299+
300+
static constexpr std::chrono::microseconds DefaultTransferIDTimeout{2'000'000}; ///< Default taken from Specification.
301+
300302
/// Detects the application in the ROM, verifies its integrity, and retrieves the information about it.
301303
class AppLocator final
302304
{
@@ -306,17 +308,18 @@ class AppLocator final
306308
{}
307309

308310
/// Returns the AppInfo if the app is found and its integrity is intact. Otherwise, returns an empty option.
309-
[[nodiscard]] auto identifyApplication() const -> std::optional<AppInfo>
311+
/// If the allow_legacy parameter is set, legacy app descriptors will be accepted, too.
312+
[[nodiscard]] auto identifyApplication(const bool allow_legacy = false) const -> std::optional<AppInfo>
310313
{
311314
for (std::size_t offset = 0; offset < max_app_size_; offset += AppDescriptor::MagicSize)
312315
{
313316
AppDescriptor desc{};
314317
if (sizeof(desc) == backend_.read(offset, reinterpret_cast<std::byte*>(&desc), sizeof(desc)))
315318
{
316-
if (desc.isValid(max_app_size_) &&
317-
validateImageCRC(offset + AppDescriptor::CRCOffset,
318-
static_cast<std::size_t>(desc.getAppInfo().image_size),
319-
desc.getAppInfo().image_crc))
319+
const bool match = desc.isValid(max_app_size_) || (allow_legacy && desc.isValidLegacy(max_app_size_));
320+
if (match && validateImageCRC(offset + AppDescriptor::CRCOffset,
321+
static_cast<std::size_t>(desc.getAppInfo().image_size),
322+
desc.getAppInfo().image_crc))
320323
{
321324
return desc.getAppInfo();
322325
}
@@ -345,6 +348,14 @@ class AppLocator final
345348
((app_info.image_size % MagicSize) == 0);
346349
}
347350

351+
[[nodiscard]] auto isValidLegacy(const std::size_t max_app_size) const -> bool
352+
{
353+
static constexpr auto SizeAlignmentRequirement = 4U; ///< Relaxed requirement to enhance compatibility.
354+
return std::equal(signature.begin(), signature.end(), ReferenceSignature.begin()) &&
355+
(app_info.image_size > 0) && (app_info.image_size <= max_app_size) &&
356+
((app_info.image_size % SizeAlignmentRequirement) == 0);
357+
}
358+
348359
[[nodiscard]] auto getAppInfo() const -> const AppInfo& { return app_info; }
349360

350361
private:
@@ -542,6 +553,8 @@ class Presenter final : public IReactor
542553
return false;
543554
}
544555

556+
[[nodiscard]] auto getNumberOfNodes() const -> std::uint8_t { return num_nodes_; }
557+
545558
[[nodiscard]] auto trigger(const INode* const node,
546559
const NodeID file_server_node_id,
547560
const std::size_t app_image_file_path_length,
@@ -988,23 +1001,33 @@ class Bootloader : public detail::IController
9881001
/// sit idle until instructed otherwise, or if the application itself commands the bootloader to begin the update.
9891002
/// The flag affects only the initial verification and has no effect on all subsequent checks; for example,
9901003
/// after the application is updated and validated, it will be booted after BootDelay regardless of this flag.
1004+
///
1005+
/// If the allow_legacy_app_descriptors option is set, the bootloader will also accept legacy descriptors alongside
1006+
/// the new format. This option should be set only if the bootloader is introduced to a product that was using
1007+
/// the old app descriptor format in the past; refer to the PX4 Brickproof Bootloader for details. If you are not
1008+
/// sure, leave the default value.
9911009
Bootloader(IROMBackend& rom_backend,
9921010
const SystemInfo& system_info,
9931011
const std::size_t max_app_size,
9941012
const bool linger,
995-
const std::chrono::seconds boot_delay = std::chrono::seconds(0)) :
1013+
const std::chrono::seconds boot_delay = std::chrono::seconds(0),
1014+
const bool allow_legacy_app_descriptors = false) :
9961015
max_app_size_(max_app_size),
9971016
boot_delay_(boot_delay),
9981017
backend_(rom_backend),
9991018
presentation_(system_info, *this),
1000-
linger_(linger)
1019+
linger_(linger),
1020+
allow_legacy_app_descriptors_(allow_legacy_app_descriptors)
10011021
{}
10021022

10031023
/// Nodes shall be registered using this method after the instance is constructed.
10041024
/// The return value is true on success, false if there are too many nodes already or this node is already
10051025
/// registered (no effect in this case).
10061026
[[nodiscard]] auto addNode(INode* const node) -> bool { return presentation_.addNode(node); }
10071027

1028+
/// The number of nodes added with addNode(). Zero by default (obviously).
1029+
[[nodiscard]] auto getNumberOfNodes() const -> std::uint8_t { return presentation_.getNumberOfNodes(); }
1030+
10081031
/// Non-blocking periodic state update.
10091032
/// The outer logic should invoke this method after any hardware event (for example, if WFE/WFI is used on an
10101033
/// ARM platform), and periodically at least once per second. Typically, it would be invoked from the main loop.
@@ -1089,7 +1112,7 @@ class Bootloader : public detail::IController
10891112
{
10901113
backend_.endWrite();
10911114
}
1092-
app_info_ = detail::AppLocator(backend_, max_app_size_).identifyApplication();
1115+
app_info_ = detail::AppLocator(backend_, max_app_size_).identifyApplication(allow_legacy_app_descriptors_);
10931116
final_.reset();
10941117
if (app_info_)
10951118
{
@@ -1121,11 +1144,11 @@ class Bootloader : public detail::IController
11211144
if (State::AppUpdateInProgress == state_)
11221145
{
11231146
backend_.endWrite(); // Cycle the state to re-init ROM if needed.
1124-
pending_log_ = {detail::dsdl::Diagnostic::Severity::Warning, "Ongoing update process restarted"};
1147+
pending_log_ = {detail::dsdl::Diagnostic::Severity::Warning, "Ongoing software update restarted"};
11251148
}
11261149
else
11271150
{
1128-
pending_log_ = {detail::dsdl::Diagnostic::Severity::Notice, "Update started"};
1151+
pending_log_ = {detail::dsdl::Diagnostic::Severity::Notice, "Software update started"};
11291152
}
11301153
state_ = State::AppUpdateInProgress;
11311154
rom_offset_ = 0;
@@ -1139,7 +1162,8 @@ class Bootloader : public detail::IController
11391162
{
11401163
if (!response)
11411164
{
1142-
pending_log_ = {detail::dsdl::Diagnostic::Severity::Critical, "File request timeout or remote error"};
1165+
pending_log_ = {detail::dsdl::Diagnostic::Severity::Critical,
1166+
"Software image file request timeout or file server error"};
11431167
reset(false);
11441168
}
11451169
else
@@ -1153,11 +1177,7 @@ class Bootloader : public detail::IController
11531177
}
11541178
else
11551179
{
1156-
if (ok)
1157-
{
1158-
pending_log_ = {detail::dsdl::Diagnostic::Severity::Notice, "File transfer completed"};
1159-
}
1160-
else
1180+
if (!ok)
11611181
{
11621182
pending_log_ = {detail::dsdl::Diagnostic::Severity::Critical, "ROM write failure"};
11631183
}
@@ -1172,6 +1192,7 @@ class Bootloader : public detail::IController
11721192
IROMBackend& backend_;
11731193
detail::Presenter presentation_;
11741194
const bool linger_;
1195+
const bool allow_legacy_app_descriptors_;
11751196

11761197
std::chrono::microseconds uptime_{};
11771198
bool inited_ = false;
@@ -1226,15 +1247,15 @@ class VolatileStorage
12261247
{
12271248
public:
12281249
/// The amount of memory required to store the data. This is the size of the container plus 8 bytes for the CRC.
1229-
static constexpr auto StorageSize = sizeof(Container) + detail::CRC64::Size;
1250+
static constexpr auto StorageSize = sizeof(Container) + CRC64::Size; // NOLINT
12301251

12311252
explicit VolatileStorage(std::uint8_t* const location) : ptr_(location) {}
12321253

12331254
/// Checks if the data is available and reads it, then erases the storage to prevent deja-vu.
12341255
/// Returns an empty option if no data is available (in that case the storage is not erased).
12351256
[[nodiscard]] auto take() -> std::optional<Container>
12361257
{
1237-
detail::CRC64 crc;
1258+
CRC64 crc;
12381259
crc.update(ptr_, StorageSize);
12391260
if (crc.isResidueCorrect())
12401261
{
@@ -1250,15 +1271,13 @@ class VolatileStorage
12501271
void store(const Container& data)
12511272
{
12521273
(void) std::memmove(ptr_, &data, sizeof(Container));
1253-
detail::CRC64 crc;
1274+
CRC64 crc;
12541275
crc.update(ptr_, sizeof(Container));
12551276
const auto crc_ptr = ptr_ + sizeof(Container); // NOLINT NOSONAR pointer arithmetic
1256-
(void) std::memmove(crc_ptr, crc.getBytes().data(), detail::CRC64::Size);
1277+
(void) std::memmove(crc_ptr, crc.getBytes().data(), CRC64::Size);
12571278
}
12581279

12591280
protected:
1260-
static_assert(std::is_trivial_v<Container>, "Container shall be a trivial type.");
1261-
12621281
static constexpr std::uint8_t EraseFillValue = 0xCA;
12631282

12641283
std::uint8_t* const ptr_;

0 commit comments

Comments
 (0)