1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! `defmt` logging over shared memory: the `CDBG` ring, its firmware-side
//! [`global_logger`], its host-side reader/decoder, and an optional panic handler.
//!
//! [`global_logger`]: https://docs.rs/defmt/latest/defmt/attr.global_logger.html
//!
//! # The problem
//!
//! A coprocessor on a Consortium SoC usually has no UART of its own to log
//! through, or has one already spoken for. But it does share memory with a
//! Linux-capable core that has a filesystem, a clock, and somewhere to put logs.
//! So the debug path runs the same way as everything else here: firmware writes
//! into a shared window, Linux reads it.
//!
//! `defmt` is the right encoder for the firmware end. Format strings and type
//! metadata stay in the ELF and never reach the wire, so a log line costs a few
//! bytes and no formatting code — which is what makes logging affordable on a
//! Cortex-M at all. The cost is that the bytes are meaningless without the ELF
//! they came from, so the host side needs a decoder.
//!
//! # Architecture
//!
//! ```text
//! firmware (no_std) Linux / host
//! ──────────────────── ─────────────────────
//! defmt::info!(..)
//! └─ logger::ConsortiumLogger host::HostRing
//! (global_logger; rzcobs-encodes (validate CDBG, drain frames)
//! one frame, wraps it in a └─ host::DefmtSession
//! packet, appends to the ring) (decode against the ELF)
//! │ │
//! └──────── CDBG shared window ────────┘
//! descriptor + circular buffer
//! panic::(handler) ─ latches Flags::Panic in the descriptor
//! ```
//!
//! | Module | Role |
//! | --------------- | --------------------------------------------------------------- |
//! | `config` | The ring base/length `ConsortiumLogger::init` records. |
//! | [`descriptor`] | The `CDBG` descriptor and single-writer/single-reader ring rules. |
//! | [`packet`] | Per-frame envelope inside the ring. |
//! | [`ring`] | [`RingWriter`](ring::RingWriter) / [`RingReader`](ring::RingReader): the ring as a non-waiting transport. |
//! | `logger` | Firmware `#[global_logger]`; encodes and appends. |
//! | `host` | Host-side validation, draining, and `defmt` decoding. |
//! | `panic` | Optional `#[panic_handler]` that records a crash in the ring. |
//!
//! # The ring as a transport
//!
//! The ring is a memory-backed link with no doorbell, so it implements
//! `consortium-core`'s non-waiting pair, the same base the IPC shared-memory
//! transport and the SCMI agent build on. [`ring::RingWriter`] is a
//! `TrySendTransport`: appending a frame never waits, and a full ring is
//! `TryError::WouldBlock` with `Overflow` latched, which is what lets the
//! `#[global_logger]` and the panic handler use it. [`ring::RingReader`] is a
//! `TryRecvTransport` that copies into a caller buffer without allocating; the
//! host's `HostRing` is built on it. Both are generic over any coherent
//! `consortium_core::Region` and refuse a cached one: the `CDBG` layout shares
//! cache lines between writer- and reader-owned fields, so flushes would clobber
//! each other. The logger maps its ring as an `UncachedRegion`, as it always has.
//!
//! # Descriptor discipline
//!
//! The ring has exactly one writer (firmware) and one reader (host), and there is
//! no lock. What keeps it correct is that **each side only mutates the fields it
//! owns**: the writer owns `write_ptr`, `capacity`, and `flags`; the reader owns
//! `read_ptr`; `write_ptr == read_ptr` means empty. Preserve that split when
//! changing anything here — it is the whole synchronization story.
//!
//! # Configuration
//!
//! Layout comes from the manifest, not from the firmware source. `csti build`
//! lowers `[dbg.<core>]` in `Consortium.toml` into the generated `init()`, which
//! calls `ConsortiumLogger::init(base, len)` first thing. Until that call, the
//! logger and the panic handler are **no-ops**, so a firmware crate built without
//! generated code still links and runs; it just logs nothing.
//!
//! # Features
//!
//! Defaults are `host` and `logger`.
//!
//! | Feature | Effect |
//! | --------------- | -------------------------------------------------------------- |
//! | `logger` | Firmware `#[global_logger]` writing into the ring. |
//! | `host` | Host-side `HostRing` reader; implies `std`. |
//! | `decode` | Adds `defmt-decoder`/`defmt-parser` for `host::DefmtSession`; implies `host`. |
//! | `panic-handler` | Installs `#[panic_handler]`; implies `logger`. |
//! | `arch-cortex-m` | Interrupt-disable and halt for Cortex-M. Required by `panic-handler`. |
//! | `arch-riscv` | Interrupt-disable and halt for RISC-V. Required by `panic-handler`. |
//!
//! `panic-handler` needs **exactly one** arch feature, and the two arch features
//! are mutually exclusive; both conditions are `compile_error!`s below rather than
//! link-time surprises.
//!
//! # Testing
//!
//! ```bash
//! cargo test -p consortium-dbg # descriptor, ring, packet
//! cargo test -p consortium-dbg --features decode
//! just test dbg host
//! ```
//!
//! Host-side tests cover descriptor initialization, ring read/write behaviour,
//! decoding fixtures, and the overflow/error flags — no target hardware needed.
/// Re-export of the defmt stream decoder, so host consumers (e.g.
/// `consortium-runtime-app`) can name `host::DefmtSession` callback types
/// (`Frame`) without a separate `defmt-decoder` dependency.
pub use defmt_decoder;
/// Re-export of `defmt-parser`, whose [`Level`](defmt_parser::Level) is the type
/// returned by `Frame::level()` (defmt-decoder does not re-export it publicly).
pub use defmt_parser;
compile_error!;
compile_error!;