ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
ra8_epd_cal.c
Go to the documentation of this file.
1
24
25#include "ra8_epd_cal.h"
26
27#include <stddef.h>
28#include <stdint.h>
29
30#include "ra8_attributes.h"
31#include "ra8_check.h"
32#include "ra8_err.h"
33#include "ra8_log.h"
34
39static const char* const s_tag = "EPD_CAL";
40
41/*
42 * Bench escape hatch -- ``-DRA8_BENCH_VCOM_MV=<millivolts>``.
43 *
44 * This is the single place in the tree where a VCOM may come from a build
45 * flag, and it exists only so first light on a bench panel is not blocked
46 * behind provisioned storage. Three properties keep it from becoming the
47 * shipping failure mode a compiled-in VCOM otherwise is:
48 *
49 * 1. It is OFF unless someone types the number on the compiler command
50 * line. There is no default and no fallback value anywhere.
51 * 2. It is the LOWEST-authority source, consulted only after the
52 * controller, the per-device record and an operator value have all
53 * declined, so it can never override a real calibration.
54 * 3. It is rejected at compile time in a production build, and it is
55 * still range-checked like every other source -- a typo'd bench value
56 * is refused, not programmed.
57 *
58 * Every boot that uses it logs loudly, because a bench build that quietly
59 * reached a customer's panel is exactly the outcome being guarded against.
60 */
61#ifdef RA8_BENCH_VCOM_MV
62/* Single-line message on purpose: a backslash-continued string literal in a
63 * #error is what cppcheck's MISRA addon reports as a 4.1 escape-sequence
64 * violation, and a new finding is not worth a wrapped sentence. */
65#ifdef RA8_PRODUCTION_BUILD
66#error "RA8_BENCH_VCOM_MV must not ship -- provision the panel's real VCOM"
67#endif
68
79typedef enum : uint16_t {
80 k_ra8_epd_cal_bench_vcom_mv = (uint16_t)(RA8_BENCH_VCOM_MV),
81} ra8_epd_cal_bench_t;
82#endif
83
94typedef enum : uint32_t {
95 k_ra8_epd_cal_crc_init = 0xFFFFFFFFU,
96 k_ra8_epd_cal_crc_poly = 0xEDB88320U,
98
112
113/* ===========================================================================
114 * Local helpers
115 * ===========================================================================
116 */
117
133[[nodiscard]] static uint32_t internal_ra8_epd_cal_crc32(const uint8_t* data, size_t len)
134{
135 uint32_t crc = (uint32_t)k_ra8_epd_cal_crc_init;
136 for (size_t i = 0U; i < len; i++) {
137 crc ^= (uint32_t)data[i];
138 for (uint8_t bit = 0U; bit < (uint8_t)k_ra8_epd_cal_bits_per_byte; bit++) {
139 const uint32_t mask = (uint32_t)0U - (crc & 1U);
140 crc = (crc >> 1U) ^ ((uint32_t)k_ra8_epd_cal_crc_poly & mask);
141 }
142 }
143 return crc ^ (uint32_t)k_ra8_epd_cal_crc_init;
144}
145
169static void internal_ra8_epd_cal_pack_le16(uint8_t* dst, uint16_t val)
170{
171 dst[k_ra8_epd_cal_off_b0] = (uint8_t)(val & (uint16_t)k_ra8_epd_cal_byte_mask);
173 (uint8_t)((val >> (uint16_t)k_ra8_epd_cal_byte_shift) & (uint16_t)k_ra8_epd_cal_byte_mask);
174}
175
186[[nodiscard]] static uint16_t internal_ra8_epd_cal_unpack_le16(const uint8_t* src)
187{
188 return (uint16_t)((uint16_t)src[k_ra8_epd_cal_off_b0] |
189 ((uint16_t)src[k_ra8_epd_cal_off_b1] << (uint16_t)k_ra8_epd_cal_byte_shift));
190}
191
213static void internal_ra8_epd_cal_pack_le32(uint8_t* dst, uint32_t val)
214{
215 dst[k_ra8_epd_cal_off_b0] = (uint8_t)(val & (uint32_t)k_ra8_epd_cal_byte_mask);
217 (uint8_t)((val >> (uint32_t)k_ra8_epd_cal_byte_shift) & (uint32_t)k_ra8_epd_cal_byte_mask);
218 dst[k_ra8_epd_cal_off_b2] = (uint8_t)((val >> ((uint32_t)k_ra8_epd_cal_byte_shift * 2U)) &
219 (uint32_t)k_ra8_epd_cal_byte_mask);
220 dst[k_ra8_epd_cal_off_b3] = (uint8_t)((val >> ((uint32_t)k_ra8_epd_cal_byte_shift * 3U)) &
221 (uint32_t)k_ra8_epd_cal_byte_mask);
222}
223
234[[nodiscard]] static uint32_t internal_ra8_epd_cal_unpack_le32(const uint8_t* src)
235{
236 return (uint32_t)src[k_ra8_epd_cal_off_b0] |
237 ((uint32_t)src[k_ra8_epd_cal_off_b1] << (uint32_t)k_ra8_epd_cal_byte_shift) |
238 ((uint32_t)src[k_ra8_epd_cal_off_b2] << ((uint32_t)k_ra8_epd_cal_byte_shift * 2U)) |
239 ((uint32_t)src[k_ra8_epd_cal_off_b3] << ((uint32_t)k_ra8_epd_cal_byte_shift * 3U));
240}
241
252[[nodiscard]] static bool internal_ra8_epd_cal_magic_ok(const uint8_t* src)
253{
254 const uint8_t* m = &src[k_ra8_epd_cal_off_magic];
255 return (m[k_ra8_epd_cal_off_b0] == (uint8_t)k_ra8_epd_cal_magic_b0) &&
259}
260
280 ra8_epd_cal_record_t* out_rec)
281{
282 if (store->read == nullptr) {
284 }
285 uint8_t blob[k_ra8_epd_cal_blob_size] = {};
286 const ra8_err_t rerr = store->read(store->ctx, blob, (size_t)k_ra8_epd_cal_blob_size);
287 if (rerr != k_ra8_ok) {
288 ra8_log_warn(s_tag, "calibration record read failed");
289 return rerr;
290 }
291 const ra8_err_t derr = ra8_epd_cal_deserialize(blob, (size_t)k_ra8_epd_cal_blob_size, out_rec);
292 if (derr == k_ra8_err_crc_mismatch) {
293 /* Magic present but the body does not check out: the record was
294 * written and then damaged. Worth shouting about -- it means durable
295 * calibration was lost, not merely never provisioned. */
296 ra8_log_error(s_tag, "calibration record CRC mismatch -- record corrupt");
297 }
298 return derr;
299}
300
301/* ===========================================================================
302 * Public API -- record codec
303 * ===========================================================================
304 */
305
306[[nodiscard]] bool ra8_epd_cal_vcom_in_range(uint16_t mv, const ra8_epd_cal_limits_mv_t* limits)
307{
308 if (limits == nullptr) {
309 return false;
310 }
311 if ((mv < limits->min_mv) || (mv > limits->max_mv)) {
312 return false;
313 }
314 return true;
315}
316
317[[nodiscard]] ra8_err_t
318ra8_epd_cal_serialize(const ra8_epd_cal_record_t* rec, uint8_t* dst, size_t dst_size)
319{
320 RA8_CHECK_NULL_PTR(rec, s_tag, "serialize: rec null");
321 RA8_CHECK_NULL_PTR(dst, s_tag, "serialize: dst null");
322 if (dst_size < (size_t)k_ra8_epd_cal_blob_size) {
324 }
325 if (rec->vcom_mv == 0U) {
326 ra8_log_error(s_tag, "serialize: refusing a zero VCOM");
328 }
329
330 for (size_t i = 0U; i < (size_t)k_ra8_epd_cal_blob_size; i++) {
331 dst[i] = 0U;
332 }
339 (uint16_t)k_ra8_epd_cal_payload_len);
341
342 const uint32_t crc = internal_ra8_epd_cal_crc32(dst, (size_t)k_ra8_epd_cal_off_crc32);
344 return k_ra8_ok;
345}
346
347[[nodiscard]] ra8_err_t
348ra8_epd_cal_deserialize(const uint8_t* src, size_t src_size, ra8_epd_cal_record_t* out_rec)
349{
350 RA8_CHECK_NULL_PTR(src, s_tag, "deserialize: src null");
351 RA8_CHECK_NULL_PTR(out_rec, s_tag, "deserialize: out null");
352 if (src_size < (size_t)k_ra8_epd_cal_blob_size) {
354 }
356 /* Blank / never-written storage lands here. Not an error worth logging
357 * on every boot of an unprovisioned device. */
358 return k_ra8_err_not_found;
359 }
360 const uint8_t version = src[(size_t)k_ra8_epd_cal_off_version];
361 if (version > (uint8_t)k_ra8_epd_cal_schema_version) {
362 ra8_log_error(s_tag, "deserialize: record schema newer than this build");
364 }
365 const uint16_t payload_len =
367 if (payload_len != (uint16_t)k_ra8_epd_cal_payload_len) {
369 }
370 const uint32_t want = internal_ra8_epd_cal_unpack_le32(&src[(size_t)k_ra8_epd_cal_off_crc32]);
371 const uint32_t have = internal_ra8_epd_cal_crc32(src, (size_t)k_ra8_epd_cal_off_crc32);
372 if (want != have) {
374 }
375 const uint16_t vcom = internal_ra8_epd_cal_unpack_le16(&src[(size_t)k_ra8_epd_cal_off_vcom_mv]);
376 if (vcom == 0U) {
377 /* A CRC-valid record cannot legitimately hold zero -- serialize
378 * refuses to write one -- so this is a writer from another schema. */
380 }
381 out_rec->vcom_mv = vcom;
382 out_rec->schema_version = version;
383 return k_ra8_ok;
384}
385
386/* ===========================================================================
387 * Public API -- resolution
388 * ===========================================================================
389 */
390
411 ra8_epd_cal_result_t* out_result)
412{
413 if (cfg->panel.get == nullptr) {
415 }
416 uint16_t panel_mv = 0U;
417 if (cfg->panel.get(cfg->panel.ctx, &panel_mv) != k_ra8_ok) {
418 return k_ra8_err_hw_error;
419 }
420 if (!ra8_epd_cal_vcom_in_range(panel_mv, &cfg->limits)) {
421 ra8_log_warn(s_tag, "controller VCOM out of range -- ignored");
423 }
424 out_result->vcom_mv = panel_mv;
425 out_result->source = k_ra8_epd_cal_src_panel;
426 return k_ra8_ok;
427}
428
449 ra8_epd_cal_result_t* out_result)
450{
451 ra8_epd_cal_record_t rec = {};
452 if (internal_ra8_epd_cal_read_record(&cfg->store, &rec) != k_ra8_ok) {
453 return k_ra8_err_not_found;
454 }
455 if (!ra8_epd_cal_vcom_in_range(rec.vcom_mv, &cfg->limits)) {
456 ra8_log_warn(s_tag, "stored VCOM out of range -- ignored");
458 }
459 out_result->vcom_mv = rec.vcom_mv;
460 out_result->source = k_ra8_epd_cal_src_record;
461 return k_ra8_ok;
462}
463
465 ra8_epd_cal_result_t* out_result)
466{
467 RA8_CHECK_NULL_PTR(cfg, s_tag, "resolve: cfg null");
468 RA8_CHECK_NULL_PTR(out_result, s_tag, "resolve: out null");
469
470 *out_result = (ra8_epd_cal_result_t){.vcom_mv = 0U, .source = k_ra8_epd_cal_src_none};
471 if ((cfg->limits.min_mv == 0U) || (cfg->limits.min_mv > cfg->limits.max_mv)) {
472 ra8_log_error(s_tag, "resolve: VCOM limits are zero or inverted");
474 }
475
476 /* Descending order of authority; first in-range value wins. */
477 if (internal_ra8_epd_cal_try_panel(cfg, out_result) == k_ra8_ok) {
478 return k_ra8_ok;
479 }
480 if (internal_ra8_epd_cal_try_record(cfg, out_result) == k_ra8_ok) {
481 return k_ra8_ok;
482 }
484 out_result->vcom_mv = cfg->provisioned_mv;
486 return k_ra8_ok;
487 }
488
489#ifdef RA8_BENCH_VCOM_MV
490 /* Bench-only, last resort, still range-checked. See the flag's block
491 * comment at the top of this file for why it is allowed to exist at all. */
492 if (ra8_epd_cal_vcom_in_range((uint16_t)k_ra8_epd_cal_bench_vcom_mv, &cfg->limits)) {
493 ra8_log_warn(s_tag, "USING BENCH VCOM FROM RA8_BENCH_VCOM_MV -- NOT FOR SHIPPING");
494 out_result->vcom_mv = (uint16_t)k_ra8_epd_cal_bench_vcom_mv;
495 out_result->source = k_ra8_epd_cal_src_bench;
496 return k_ra8_ok;
497 }
498 ra8_log_error(s_tag, "RA8_BENCH_VCOM_MV is outside the panel's window -- ignored");
499#endif
500
501 /* Nothing trusted. Fail safe: the caller must leave the panel dark
502 * rather than drive it at an invented bias. */
503 ra8_log_error(s_tag, "no trusted VCOM -- refusing to drive the panel");
504 return k_ra8_err_not_found;
505}
506
508 const ra8_epd_cal_result_t* result)
509{
510 RA8_CHECK_NULL_PTR(cfg, s_tag, "apply: cfg null");
511 RA8_CHECK_NULL_PTR(result, s_tag, "apply: result null");
512 if (result->source == k_ra8_epd_cal_src_none) {
514 }
515 if (cfg->panel.set == nullptr) {
517 }
518 if (!ra8_epd_cal_vcom_in_range(result->vcom_mv, &cfg->limits)) {
519 ra8_log_error(s_tag, "apply: VCOM out of range at the point of use");
521 }
522 const ra8_err_t serr = cfg->panel.set(cfg->panel.ctx, result->vcom_mv);
523 if (serr != k_ra8_ok) {
524 return serr;
525 }
526
527 /* Confirm through the read seam that the value is actually in effect.
528 * The production ``set`` binding verifies its own write, but this module
529 * must not assume that of an arbitrary injected seam -- and this is the
530 * layer where the check is host-testable, so it is the layer that owns
531 * the vectors. Without a seam that can read back there is nothing to
532 * confirm, and an unconfirmable bias is not one to leave on a panel. */
533 if (cfg->panel.get == nullptr) {
534 ra8_log_error(s_tag, "apply: no read seam -- VCOM cannot be confirmed");
536 }
537 uint16_t readback = 0U;
538 if (cfg->panel.get(cfg->panel.ctx, &readback) != k_ra8_ok) {
539 return k_ra8_err_hw_error;
540 }
541 /* Equality alone, deliberately: ``result->vcom_mv`` was range-checked
542 * above, so an equal readback is in range by construction and a second
543 * range test here would be unreachable code dressed up as a safety
544 * check. Range-validating a value the panel *reports* matters where such
545 * a value is adopted rather than merely confirmed, which is
546 * ``internal_ra8_epd_cal_try_panel``, and it is checked there. */
547 if (readback != result->vcom_mv) {
548 ra8_log_error(s_tag, "apply: VCOM readback disagrees -- panel stays dark");
550 }
551 return k_ra8_ok;
552}
553
554[[nodiscard]] ra8_err_t ra8_epd_cal_provision(const ra8_epd_cal_cfg_t* cfg, uint16_t vcom_mv)
555{
556 RA8_CHECK_NULL_PTR(cfg, s_tag, "provision: cfg null");
557 if (cfg->store.write == nullptr) {
559 }
560 if (!ra8_epd_cal_vcom_in_range(vcom_mv, &cfg->limits)) {
561 ra8_log_error(s_tag, "provision: VCOM outside the panel's window");
563 }
564 const ra8_epd_cal_record_t rec = {.vcom_mv = vcom_mv,
565 .schema_version = (uint8_t)k_ra8_epd_cal_schema_version};
566 uint8_t blob[k_ra8_epd_cal_blob_size] = {};
567 const ra8_err_t serr = ra8_epd_cal_serialize(&rec, blob, (size_t)k_ra8_epd_cal_blob_size);
568 if (serr != k_ra8_ok) {
569 return serr;
570 }
571 return cfg->store.write(cfg->store.ctx, blob, (size_t)k_ra8_epd_cal_blob_size);
572}
static const char * s_tag
Logging / check tag.
Definition ra8_app.c:17
Annotation-attribute framework macros for ra8-firmware.
#define RA8_INTERNAL
Marker that a function is intended to be static (file-local).
Validation and Error-Checking Macros for ra8-firmware.
#define RA8_CHECK_NULL_PTR(ptr, tag, message)
Reject nullptr pointer, returning k_ra8_err_null_ptr.
Definition ra8_check.h:243
bool ra8_epd_cal_vcom_in_range(uint16_t mv, const ra8_epd_cal_limits_mv_t *limits)
Range-check a candidate VCOM against a panel's documented window.
ra8_epd_cal_crc_t
IEEE 802.3 CRC-32 parameters.
Definition ra8_epd_cal.c:94
@ k_ra8_epd_cal_crc_init
CRC seed / final XOR.
Definition ra8_epd_cal.c:95
@ k_ra8_epd_cal_crc_poly
Reversed IEEE 802.3 poly.
Definition ra8_epd_cal.c:96
static ra8_err_t internal_ra8_epd_cal_try_panel(const ra8_epd_cal_cfg_t *cfg, ra8_epd_cal_result_t *out_result)
Try the controller's own persisted VCOM (resolution source 1).
static void internal_ra8_epd_cal_pack_le32(uint8_t *dst, uint32_t val)
Store a little-endian 32-bit value.
ra8_err_t ra8_epd_cal_provision(const ra8_epd_cal_cfg_t *cfg, uint16_t vcom_mv)
Write a per-device calibration record to non-volatile storage.
static bool internal_ra8_epd_cal_magic_ok(const uint8_t *src)
Report whether a serialised record carries the 'EVCM' magic.
static uint32_t internal_ra8_epd_cal_crc32(const uint8_t *data, size_t len)
Compute the IEEE 802.3 CRC-32 of a byte span.
ra8_err_t ra8_epd_cal_resolve(const ra8_epd_cal_cfg_t *cfg, ra8_epd_cal_result_t *out_result)
Resolve the VCOM to drive this panel at, or fail safe.
ra8_err_t ra8_epd_cal_deserialize(const uint8_t *src, size_t src_size, ra8_epd_cal_record_t *out_rec)
Parse and integrity-check a serialised record.
static void internal_ra8_epd_cal_pack_le16(uint8_t *dst, uint16_t val)
Store a little-endian 16-bit value.
ra8_epd_cal_bits_t
Byte / word packing constants (no magic numbers).
@ k_ra8_epd_cal_bits_per_byte
CRC inner-loop bound.
@ k_ra8_epd_cal_off_b2
Little-endian byte 2.
@ k_ra8_epd_cal_byte_shift
Bits per byte.
@ k_ra8_epd_cal_off_b1
Little-endian byte 1.
@ k_ra8_epd_cal_off_b0
Little-endian byte 0.
@ k_ra8_epd_cal_byte_mask
Low-byte extraction mask.
@ k_ra8_epd_cal_off_b3
Little-endian byte 3.
ra8_err_t ra8_epd_cal_serialize(const ra8_epd_cal_record_t *rec, uint8_t *dst, size_t dst_size)
Serialise a record into its 32-byte on-flash form.
static ra8_err_t internal_ra8_epd_cal_try_record(const ra8_epd_cal_cfg_t *cfg, ra8_epd_cal_result_t *out_result)
Try the per-device record in storage (resolution source 2).
static uint16_t internal_ra8_epd_cal_unpack_le16(const uint8_t *src)
Load a little-endian 16-bit value.
ra8_err_t ra8_epd_cal_apply(const ra8_epd_cal_cfg_t *cfg, const ra8_epd_cal_result_t *result)
Programme a resolved VCOM onto the controller and confirm it took.
static uint32_t internal_ra8_epd_cal_unpack_le32(const uint8_t *src)
Load a little-endian 32-bit value.
static ra8_err_t internal_ra8_epd_cal_read_record(const ra8_epd_cal_store_t *store, ra8_epd_cal_record_t *out_rec)
Read and validate the per-device record through the store seam.
Per-device e-paper panel calibration (VCOM) – record, storage seam, resolution policy.
bool ra8_epd_cal_vcom_in_range(uint16_t mv, const ra8_epd_cal_limits_mv_t *limits)
Range-check a candidate VCOM against a panel's documented window.
@ k_ra8_epd_cal_blob_size
Serialised record size in bytes.
@ k_ra8_epd_cal_schema_version
Current on-flash schema version.
@ k_ra8_epd_cal_payload_len
Schema-1 payload bytes (vcom_mv).
@ k_ra8_epd_cal_src_panel
Read back from the controller.
@ k_ra8_epd_cal_src_record
From the per-device NV record.
@ k_ra8_epd_cal_src_provisioned
Operator-supplied this boot.
@ k_ra8_epd_cal_src_bench
RA8_BENCH_VCOM_MV; see below.
@ k_ra8_epd_cal_src_none
No trusted value – do NOT drive.
@ k_ra8_epd_cal_magic_b0
'E'
@ k_ra8_epd_cal_magic_b2
'C'
@ k_ra8_epd_cal_magic_b3
'M'
@ k_ra8_epd_cal_magic_b1
'V'
@ k_ra8_epd_cal_off_crc32
CRC-32 trailer, LE32.
@ k_ra8_epd_cal_off_payload_len
Payload length, LE16.
@ k_ra8_epd_cal_off_magic
'EVCM' magic.
@ k_ra8_epd_cal_off_version
Schema version byte.
@ k_ra8_epd_cal_off_vcom_mv
VCOM magnitude, LE16.
Error Code Definitions for ra8-firmware.
@ k_ra8_err_not_supported
Requested feature not compiled in, not wired, or not supported by this MCU variant.
Definition ra8_err.h:180
@ k_ra8_err_crc_mismatch
CRC mismatch detected on received data.
Definition ra8_err.h:423
@ k_ra8_err_invalid_arg
Invalid function argument.
Definition ra8_err.h:152
@ k_ra8_err_invalid_state
Module in wrong state for requested operation.
Definition ra8_err.h:161
@ k_ra8_err_validation_failed
Validation rule failed (caller-supplied invariant not satisfied).
Definition ra8_err.h:459
@ k_ra8_err_range_check_failed
Value outside range enforced by RA8_CHECK_RANGE / RA8_CHECK_RANGE_TAG.
Definition ra8_err.h:471
@ k_ra8_ok
Success – operation completed with all postconditions satisfied.
Definition ra8_err.h:119
@ k_ra8_err_hw_error
Generic hardware fault detected (error flag set, fault interrupt).
Definition ra8_err.h:310
@ k_ra8_err_not_found
Requested item not found (lookup / search missed).
Definition ra8_err.h:173
@ k_ra8_err_invalid_size
Invalid size parameter (too large, too small, or misaligned).
Definition ra8_err.h:167
ra8_err_codes_t ra8_err_t
Canonical error-return type used by every ra8-firmware API.
Definition ra8_err.h:546
Lightweight Logging Interface for ra8-firmware.
#define ra8_log_warn(tag, message)
RA8 log warn.
Definition ra8_log.h:347
#define ra8_log_error(tag, message)
RA8 log error.
Definition ra8_log.h:335
Everything ra8_epd_cal_resolve needs.
ra8_epd_cal_store_t store
Per-device record store seam.
bool has_provisioned
Whether the field above is set.
uint16_t provisioned_mv
Operator value for this boot.
ra8_epd_cal_panel_ops_t panel
Controller access seam.
ra8_epd_cal_limits_mv_t limits
Panel's documented VCOM window.
The panel's documented VCOM window, in millivolt magnitudes.
uint16_t min_mv
Lowest accepted VCOM magnitude, millivolts.
uint16_t max_mv
Highest accepted VCOM magnitude, millivolts.
ra8_epd_cal_panel_get_fn_t get
VCOM read seam; may be NULL.
void * ctx
Opaque context for both.
ra8_epd_cal_panel_set_fn_t set
VCOM write seam; may be NULL.
Decoded per-device calibration record.
uint8_t schema_version
Schema the record was written under.
uint16_t vcom_mv
VCOM magnitude in millivolts (e.g.
Outcome of ra8_epd_cal_resolve.
ra8_epd_cal_source_t source
Which authority supplied it.
uint16_t vcom_mv
Resolved VCOM magnitude, millivolts.
Injected non-volatile store for the calibration record.
ra8_epd_cal_nv_read_fn_t read
Record read seam; may be NULL.
void * ctx
Opaque context for both.
ra8_epd_cal_nv_write_fn_t write
Record write seam; may be NULL.