Skip to main content

ax_runtime/serial/
mod.rs

1//! UART runtime ownership and task-context data service.
2//!
3//! Each UART has one CPU-affine maintenance task. Sleepable TTY output and
4//! non-blocking per-CPU log records use separate bounded queues; only the IRQ
5//! endpoint, maintenance task, and emergency endpoint touch UART registers.
6
7mod control;
8mod ingress;
9mod log_mailbox;
10mod ordered_output;
11pub(crate) mod spsc;
12mod state;
13mod worker;
14
15use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
16use core::{
17    fmt::{self, Write},
18    sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
19};
20
21use ax_driver::serial::SerialDevice;
22pub use ax_driver::serial::SerialDeviceInfo;
23use ax_lazyinit::OnceLock;
24use axpoll::IoEvents;
25use axpoll_set::PollSet;
26use rdif_serial::UartRegisterGate;
27pub use rdif_serial::{Config, ConfigError, DataBits, Parity, RxFlag, StopBits};
28
29pub(crate) use self::log_mailbox::{LogRecord, LogRecordKind};
30use self::{
31    control::{ControlOp, ControlQueue},
32    ingress::TxIngress,
33    log_mailbox::{LogMailbox, LogRecordMeta},
34    ordered_output::OrderedOutput,
35    spsc::{Consumer as SpscConsumer, Producer as SpscProducer},
36    state::{SerialIrqLatch, SerialStatsAtomic},
37    worker::SerialWorker,
38};
39use crate::{
40    RuntimeError, RuntimeResult,
41    irq::FixedIrqWorkerSignal,
42    task::{
43        sched::{CpuId, CpuSet, FairMode, Nice, SchedulePolicy},
44        sync::{Mutex, RawSpinLock, WaitQueue},
45    },
46};
47
48const NO_ACTIVE_CONSOLE: usize = usize::MAX;
49const IRQ_RX_CAPACITY: usize = 16_384;
50const SUBSCRIPTION_RX_CAPACITY: usize = 4_096;
51// A subscriber can be unable to run while all secondary CPUs publish their
52// startup records. Keep enough whole-record slots for the bounded SMP burst so
53// activating a console owner does not immediately lose diagnostics.
54const LOG_SUBSCRIPTION_CAPACITY: usize = 128;
55const SERIAL_WORKER_NICE: Nice = match Nice::new(-20) {
56    Ok(nice) => nice,
57    Err(_) => panic!("Linux console worker priority must be valid"),
58};
59
60const fn serial_worker_policy() -> SchedulePolicy {
61    // Linux keeps threaded console printers in SCHED_NORMAL at nice -20 so
62    // they run promptly with a generous Fair budget without becoming RT work.
63    SchedulePolicy::fair(SERIAL_WORKER_NICE, FairMode::Normal)
64}
65
66static SERIAL_RUNTIMES: OnceLock<Box<[SerialRuntimeHandle]>> = OnceLock::new();
67static LOG_MAILBOX: OnceLock<Arc<LogMailbox>> = OnceLock::new();
68static ACTIVE_CONSOLE: AtomicUsize = AtomicUsize::new(NO_ACTIVE_CONSOLE);
69
70const RUNTIME_DORMANT: u8 = 0;
71const RUNTIME_STARTED: u8 = 1;
72const RUNTIME_FAILED_CLOSED: u8 = 2;
73
74struct RuntimeLifecycle(AtomicU8);
75
76impl RuntimeLifecycle {
77    const fn new() -> Self {
78        Self(AtomicU8::new(RUNTIME_DORMANT))
79    }
80
81    fn started(&self) -> bool {
82        self.0.load(Ordering::Acquire) == RUNTIME_STARTED
83    }
84
85    fn ensure_available(&self) -> RuntimeResult {
86        (self.0.load(Ordering::Acquire) != RUNTIME_FAILED_CLOSED)
87            .then_some(())
88            .ok_or(RuntimeError::ConsoleFailedClosed)
89    }
90
91    fn ensure_started(&self) -> RuntimeResult {
92        match self.0.load(Ordering::Acquire) {
93            RUNTIME_STARTED => Ok(()),
94            RUNTIME_FAILED_CLOSED => Err(RuntimeError::ConsoleFailedClosed),
95            RUNTIME_DORMANT => Err(RuntimeError::SerialNotStarted),
96            _ => unreachable!(),
97        }
98    }
99
100    fn set_started(&self, started: bool) {
101        let next = if started {
102            RUNTIME_STARTED
103        } else {
104            RUNTIME_DORMANT
105        };
106        let _ = self
107            .0
108            .try_update(Ordering::AcqRel, Ordering::Acquire, |state| {
109                (state != RUNTIME_FAILED_CLOSED).then_some(next)
110            });
111    }
112
113    fn fail_closed(&self) {
114        self.0.store(RUNTIME_FAILED_CLOSED, Ordering::Release);
115    }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum RxItem {
120    Byte { byte: u8, flag: RxFlag },
121    Overrun,
122}
123
124impl Default for RxItem {
125    fn default() -> Self {
126        Self::Byte {
127            byte: 0,
128            flag: RxFlag::Normal,
129        }
130    }
131}
132
133struct RuntimeIrqBridge {
134    latch: SerialIrqLatch,
135    rx_overflow: AtomicBool,
136    register_retry: AtomicBool,
137    worker_signal: FixedIrqWorkerSignal,
138}
139
140impl RuntimeIrqBridge {
141    const fn new() -> Self {
142        Self {
143            latch: SerialIrqLatch::new(),
144            rx_overflow: AtomicBool::new(false),
145            register_retry: AtomicBool::new(false),
146            worker_signal: FixedIrqWorkerSignal::new(),
147        }
148    }
149
150    fn notify(&self) {
151        self.worker_signal.notify();
152    }
153
154    fn take_register_retry(&self) -> bool {
155        self.register_retry.swap(false, Ordering::AcqRel)
156    }
157
158    fn wait(&self) {
159        self.worker_signal
160            .wait()
161            .unwrap_or_else(|error| panic!("serial IRQ waiter could not quiesce: {error}"));
162    }
163}
164
165struct PendingIrqRegistration {
166    handle: ax_hal::irq::IrqHandle,
167    device_name: String,
168    committed: bool,
169}
170
171impl PendingIrqRegistration {
172    fn new(handle: ax_hal::irq::IrqHandle, device_name: String) -> Self {
173        Self {
174            handle,
175            device_name,
176            committed: false,
177        }
178    }
179
180    fn commit(mut self) {
181        self.committed = true;
182    }
183}
184
185impl Drop for PendingIrqRegistration {
186    fn drop(&mut self) {
187        if self.committed {
188            return;
189        }
190        if let Err(error) = ax_hal::irq::free_irq(self.handle) {
191            warn!(
192                "failed to roll back serial IRQ registration for {}: {error:?}",
193                self.device_name
194            );
195        }
196    }
197}
198
199fn try_enter_irq_registers<'a, E: ?Sized>(
200    gate: &'a UartRegisterGate<E>,
201    bridge: &RuntimeIrqBridge,
202) -> Option<rdif_serial::UartRegisterGuard<'a, E>> {
203    let guard = gate.try_enter();
204    if guard.is_none() {
205        // Emergency TX masks every device source before touching the FIFO, so a
206        // level-triggered line cannot continuously reassert while the IRQ
207        // endpoint defers register access. Publish the retry before waking the
208        // fixed worker; it polls status and restores normal source ownership
209        // after the bounded emergency transaction releases the gate.
210        bridge.register_retry.store(true, Ordering::Release);
211        bridge.notify();
212    }
213    guard
214}
215
216struct RuntimeShared {
217    index: usize,
218    info: SerialDeviceInfo,
219    owner_cpu: usize,
220    polling: bool,
221    port: RawSpinLock<Box<dyn rdif_serial::UartPort>>,
222    register_gate: Arc<rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>>,
223    ingress: TxIngress,
224    log_mailbox: Arc<LogMailbox>,
225    rx_subscription: RawSpinLock<Option<SpscConsumer<RxItem>>>,
226    log_subscription_gate: RawSpinLock<OrderedOutput>,
227    log_subscription_active: AtomicBool,
228    log_subscription_dropped_records: AtomicUsize,
229    log_subscription_dropped_bytes: AtomicUsize,
230    control: ControlQueue,
231    bridge: Arc<RuntimeIrqBridge>,
232    stats: Arc<SerialStatsAtomic>,
233    rx_source: Arc<PollSet>,
234    tx_source: Arc<PollSet>,
235    rx_progress: WaitQueue,
236    console_progress: WaitQueue,
237    tx_progress: WaitQueue,
238    tty_output_lock: Mutex<()>,
239    log_barriers: AtomicUsize,
240    lifecycle: RuntimeLifecycle,
241    irq_handle: OnceLock<ax_hal::irq::IrqHandle>,
242}
243
244impl RuntimeShared {
245    fn record_subscription_drop(&self, bytes: usize) {
246        self.log_subscription_dropped_records
247            .fetch_add(1, Ordering::Relaxed);
248        self.log_subscription_dropped_bytes
249            .fetch_add(bytes, Ordering::Relaxed);
250    }
251
252    fn publish_log(
253        &self,
254        cpu: usize,
255        meta: LogRecordMeta,
256        args: fmt::Arguments<'_>,
257    ) -> log_mailbox::PublishOutcome {
258        // Format before acquiring the gate: formatting can itself log.
259        let Ok(record) = LogRecord::format(cpu, 0, meta, args) else {
260            return log_mailbox::PublishOutcome::dropped(0);
261        };
262        let mut route = self.log_subscription_gate.lock_irqsave();
263        if self.log_subscription_active.load(Ordering::Acquire) {
264            self.stats.observe_log_record(
265                record.cpu_id(),
266                record.timestamp_nanos(),
267                record.task_id().is_some(),
268                record.kind() == LogRecordKind::Log,
269                record.is_truncated(),
270            );
271            match route.push(record) {
272                Ok(()) => log_mailbox::PublishOutcome::queued(&record),
273                Err(bytes) => {
274                    self.record_subscription_drop(bytes);
275                    log_mailbox::PublishOutcome::dropped(bytes)
276                }
277            }
278        } else {
279            self.log_mailbox.publish_formatted(cpu, record)
280        }
281    }
282
283    /// Runs one task-context register transaction with local IRQ delivery
284    /// excluded and all cross-CPU aliases serialized by the UART gate.
285    fn with_port<R>(&self, access: impl FnOnce(&mut dyn rdif_serial::UartPort) -> R) -> Option<R> {
286        let mut port = self.port.lock_irqsave();
287        let _register_access = loop {
288            if self.register_gate.emergency_active() {
289                return None;
290            }
291            if let Some(access) = self.register_gate.try_enter() {
292                break access;
293            }
294            core::hint::spin_loop();
295        };
296        Some(access(&mut **port))
297    }
298
299    fn started(&self) -> bool {
300        self.lifecycle.started()
301    }
302
303    fn ensure_started(&self) -> RuntimeResult {
304        self.lifecycle.ensure_started()
305    }
306
307    fn set_started(&self, started: bool) {
308        self.lifecycle.set_started(started);
309        if !started {
310            self.rx_progress.notify_all();
311            self.console_progress.notify_all();
312            self.tx_progress.notify_all();
313        }
314    }
315
316    fn fail_closed(&self) {
317        self.lifecycle.fail_closed();
318        self.disable_irq();
319        // `FailedClosed` is not merely an API state. Terminally claim the
320        // register gate so a final in-flight worker or IRQ transaction cannot
321        // hand the UART back to a normal endpoint afterward. The lifecycle
322        // publication and disabled IRQ prevent new contenders; an existing
323        // bounded register transaction is allowed to finish.
324        while !self.register_gate.emergency_active() {
325            if let Some(access) = self.register_gate.try_begin_emergency() {
326                drop(access);
327                break;
328            }
329            core::hint::spin_loop();
330        }
331        self.ingress.stop_and_discard();
332        self.rx_progress.notify_all();
333        self.console_progress.notify_all();
334        self.tx_progress.notify_all();
335    }
336
337    fn publish_tx_space(&self) {
338        self.tx_progress.notify_all();
339        // SAFETY: the maintenance task publishes queue space before waking
340        // task-context poll waiters.
341        unsafe { self.tx_source.wake(IoEvents::OUT) };
342    }
343
344    fn enable_irq(&self) -> RuntimeResult {
345        let Some(handle) = self.irq_handle.get().copied() else {
346            return Ok(());
347        };
348        ax_hal::irq::enable_irq(handle).map_err(|error| {
349            warn!(
350                "failed to enable serial IRQ for {}: {error:?}",
351                self.info.name
352            );
353            RuntimeError::from(error)
354        })
355    }
356
357    fn disable_irq(&self) {
358        let Some(handle) = self.irq_handle.get().copied() else {
359            return;
360        };
361        if let Err(err) = ax_hal::irq::disable_irq(handle) {
362            warn!(
363                "failed to disable serial IRQ for {}: {err:?}",
364                self.info.name
365            );
366        }
367    }
368}
369
370/// Cloneable OS-facing façade for one UART runtime.
371#[derive(Clone)]
372pub struct SerialRuntimeHandle {
373    shared: Arc<RuntimeShared>,
374}
375
376impl SerialRuntimeHandle {
377    pub fn info(&self) -> &SerialDeviceInfo {
378        &self.shared.info
379    }
380
381    /// Leases the only RX subscription.
382    ///
383    /// Dropping the subscription returns the consumer to this runtime so a
384    /// failed owner initialization does not permanently consume the RX path.
385    pub fn take_rx_subscription(&self) -> Option<SerialRxSubscription> {
386        self.shared.lifecycle.ensure_available().ok()?;
387        let consumer = self.shared.rx_subscription.lock_irqsave().take()?;
388        Some(SerialRxSubscription {
389            consumer: Mutex::new(Some(consumer)),
390            shared: self.shared.clone(),
391        })
392    }
393
394    pub(crate) fn take_log_subscription(&self) -> Option<SerialLogSubscription> {
395        self.shared.lifecycle.ensure_available().ok()?;
396        let mut reader = self.shared.log_mailbox.reader();
397        let mut route = self.shared.log_subscription_gate.lock_irqsave();
398        if self.shared.log_subscription_active.load(Ordering::Acquire) {
399            return None;
400        }
401        route.clear();
402        self.shared
403            .log_subscription_dropped_records
404            .store(0, Ordering::Release);
405        self.shared
406            .log_subscription_dropped_bytes
407            .store(0, Ordering::Release);
408        // The publication gate excludes both ordinary log publishers and the
409        // UART worker. Transfer the old mailbox prefix before switching routes.
410        while let Some(record) = reader.take(self.shared.index) {
411            if let Err(bytes) = route.push(record.record) {
412                self.shared.record_subscription_drop(bytes);
413            }
414        }
415        self.shared
416            .log_subscription_active
417            .store(true, Ordering::Release);
418        Some(SerialLogSubscription {
419            shared: self.shared.clone(),
420        })
421    }
422
423    /// Returns a cloneable task-context output capability for this UART.
424    ///
425    /// This per-port API is used by operating systems that expose non-console
426    /// serial devices. Physical-console consumers should use
427    /// [`crate::console::output`] so raw-HAL fallback and failed-closed state
428    /// remain hidden behind the console boundary.
429    pub fn task_output(&self) -> SerialTaskOutput {
430        SerialTaskOutput {
431            shared: self.shared.clone(),
432        }
433    }
434
435    pub fn start(&self, config: Config) -> RuntimeResult {
436        self.shared.lifecycle.ensure_available()?;
437        self.shared
438            .control
439            .submit(ControlOp::Start(config), || self.shared.bridge.notify())
440    }
441
442    pub fn shutdown(&self) -> RuntimeResult {
443        let result = self
444            .shared
445            .control
446            .submit(ControlOp::Shutdown, || self.shared.bridge.notify());
447        if result.is_ok() {
448            deactivate_console(&self.shared);
449        }
450        result
451    }
452
453    pub fn set_config(&self, config: Config) -> RuntimeResult {
454        self.output_barrier()?.set_config(config)
455    }
456
457    /// Pauses extraction of new log records until the returned guard drops.
458    pub(crate) fn output_barrier(&self) -> RuntimeResult<SerialOutputBarrier> {
459        self.shared.ensure_started()?;
460        Ok(SerialOutputBarrier::new(self.shared.clone()))
461    }
462
463    /// Blocks new early-console register access before runtime configuration.
464    pub(crate) fn begin_console_handoff(&self) -> RuntimeResult {
465        ax_hal::console::begin_runtime_handoff()?;
466        Ok(())
467    }
468
469    /// Adopts the already-running firmware console while the platform path is
470    /// in `Preparing`.
471    ///
472    /// The worker preserves the firmware line/FIFO configuration and only
473    /// masks device-local sources before enabling its registered IRQ action.
474    /// The console coordinator owns the surrounding handoff transaction and
475    /// closes the early path if this operation fails.
476    pub(crate) fn adopt_prepared_console(&self) -> RuntimeResult {
477        self.shared
478            .control
479            .submit(ControlOp::AdoptFirmwareConsole, || {
480                self.shared.bridge.notify()
481            })
482    }
483
484    /// Permanently rejects task, IRQ-consumer, and per-port use after a
485    /// selected console handoff becomes untrustworthy.
486    pub(crate) fn fail_console_closed(&self) {
487        self.shared.fail_closed();
488    }
489
490    /// Publishes runtime log routing and completes the platform handoff.
491    pub(crate) fn commit_console_handoff(&self) -> RuntimeResult {
492        self.shared.ensure_started()?;
493        // Reserve log routing before publishing either console-owner state.
494        // Once the platform transition is committed there is no safe early
495        // owner to roll back to, so every remaining operation must be
496        // infallible.
497        if !self.shared.log_mailbox.claim(self.shared.index) {
498            let _ = self.shutdown();
499            return Err(RuntimeError::SerialConsoleBusy);
500        }
501        if ACTIVE_CONSOLE
502            .compare_exchange(
503                NO_ACTIVE_CONSOLE,
504                self.shared.index,
505                Ordering::AcqRel,
506                Ordering::Acquire,
507            )
508            .is_err()
509        {
510            self.shared.log_mailbox.release(self.shared.index);
511            let _ = self.shutdown();
512            return Err(RuntimeError::SerialConsoleBusy);
513        }
514        if let Err(error) = ax_hal::console::commit_runtime_handoff() {
515            let _ = ACTIVE_CONSOLE.compare_exchange(
516                self.shared.index,
517                NO_ACTIVE_CONSOLE,
518                Ordering::AcqRel,
519                Ordering::Acquire,
520            );
521            self.shared.log_mailbox.release(self.shared.index);
522            let _ = self.shutdown();
523            return Err(error.into());
524        }
525        self.shared.bridge.notify();
526        Ok(())
527    }
528}
529
530/// Cloneable, bounded MPSC submission façade. It never accesses UART registers.
531#[derive(Clone)]
532pub(crate) struct SerialTxSender {
533    shared: Arc<RuntimeShared>,
534}
535
536impl SerialTxSender {
537    pub fn try_write(&self, bytes: &[u8]) -> RuntimeResult<usize> {
538        if bytes.is_empty() {
539            return Ok(0);
540        }
541        self.shared.ensure_started()?;
542        let accepted = self
543            .shared
544            .ingress
545            .try_write(bytes, || self.shared.bridge.notify());
546        if accepted == 0 {
547            Err(RuntimeError::WouldBlock)
548        } else {
549            Ok(accepted)
550        }
551    }
552
553    pub fn wait_writable(&self) -> RuntimeResult {
554        self.shared.ensure_started()?;
555        self.shared
556            .tx_progress
557            .wait_until(|| self.shared.ingress.write_room() > 0 || !self.shared.started());
558        self.shared
559            .started()
560            .then_some(())
561            .ok_or(RuntimeError::SerialNotStarted)
562    }
563
564    /// Writes every raw byte, sleeping only when the bounded TX ring is full.
565    pub fn write_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
566        self.write_all_with(bytes, |shared, remaining| {
567            shared
568                .ingress
569                .try_write(remaining, || shared.bridge.notify())
570        })
571    }
572
573    /// Writes every text byte, sleeping when the bounded TX ring is full.
574    ///
575    /// This task-context operation expands line feeds to CRLF. Hard-IRQ,
576    /// logging, and panic paths must use their dedicated non-blocking
577    /// endpoints instead.
578    pub fn write_text_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
579        self.write_all_with(bytes, |shared, remaining| {
580            shared
581                .ingress
582                .try_write_text(remaining, || shared.bridge.notify())
583        })
584    }
585
586    fn write_all_with(
587        &self,
588        bytes: &[u8],
589        submit: impl Fn(&RuntimeShared, &[u8]) -> usize,
590    ) -> RuntimeResult<usize> {
591        let mut written = 0;
592        while written < bytes.len() {
593            self.shared.ensure_started()?;
594            let accepted = submit(&self.shared, &bytes[written..]);
595            if accepted == 0 {
596                self.wait_writable()?;
597            } else {
598                written += accepted;
599            }
600        }
601        Ok(written)
602    }
603}
604
605/// Sleepable TTY/configuration transaction which excludes new log extraction.
606pub(crate) struct SerialOutputBarrier {
607    shared: Arc<RuntimeShared>,
608}
609
610impl SerialOutputBarrier {
611    fn new(shared: Arc<RuntimeShared>) -> Self {
612        shared.log_barriers.fetch_add(1, Ordering::AcqRel);
613        shared.bridge.notify();
614        Self { shared }
615    }
616
617    /// Waits for queued TTY bytes, the current log record, and UART hardware
618    /// to become idle. New log records remain paused after this method returns.
619    pub fn wait_idle(&self) -> RuntimeResult {
620        self.shared.ensure_started()?;
621        self.shared
622            .control
623            .submit_drain(|| self.shared.bridge.notify())
624    }
625
626    /// Applies configuration before allowing worker log extraction to resume.
627    pub fn set_config(&self, config: Config) -> RuntimeResult {
628        self.shared.ensure_started()?;
629        self.shared
630            .control
631            .submit(ControlOp::SetConfig(config), || self.shared.bridge.notify())
632    }
633}
634
635impl Drop for SerialOutputBarrier {
636    fn drop(&mut self) {
637        self.shared.log_barriers.fetch_sub(1, Ordering::AcqRel);
638        self.shared.bridge.notify();
639    }
640}
641
642/// The unique RX consumer for one UART runtime.
643pub struct SerialRxSubscription {
644    consumer: Mutex<Option<SpscConsumer<RxItem>>>,
645    shared: Arc<RuntimeShared>,
646}
647
648/// Internal complete-record consumer re-exported through `ax_runtime::console`.
649pub(crate) struct SerialLogSubscription {
650    shared: Arc<RuntimeShared>,
651}
652
653impl SerialLogSubscription {
654    pub(crate) fn try_read(&self) -> Option<LogRecord> {
655        self.shared.log_subscription_gate.lock_irqsave().pop()
656    }
657
658    pub(crate) fn write_output(&self, tag: u128, bytes: &[u8]) -> RuntimeResult {
659        self.shared.ensure_started()?;
660        let result = self
661            .shared
662            .log_subscription_gate
663            .lock_irqsave()
664            .write(tag, bytes);
665        if let Err(bytes) = result {
666            self.shared.record_subscription_drop(bytes);
667        }
668        self.shared.bridge.notify();
669        result.map_err(|_| RuntimeError::WouldBlock)
670    }
671
672    pub(crate) fn dropped(&self) -> (usize, usize) {
673        (
674            self.shared
675                .log_subscription_dropped_records
676                .swap(0, Ordering::AcqRel),
677            self.shared
678                .log_subscription_dropped_bytes
679                .swap(0, Ordering::AcqRel),
680        )
681    }
682
683    pub(crate) fn wait_readable(&self) -> RuntimeResult {
684        self.shared.ensure_started()?;
685        self.shared.console_progress.wait_until(|| {
686            self.has_pending()
687                || !self.shared.log_subscription_active.load(Ordering::Acquire)
688                || !self.shared.started()
689        });
690        self.has_pending()
691            .then_some(())
692            .ok_or(RuntimeError::SerialNotStarted)
693    }
694
695    pub(crate) fn has_pending(&self) -> bool {
696        self.shared
697            .log_subscription_dropped_records
698            .load(Ordering::Acquire)
699            != 0
700            || !self.shared.log_subscription_gate.lock_irqsave().is_empty()
701    }
702}
703
704impl Drop for SerialLogSubscription {
705    fn drop(&mut self) {
706        let mut route = self.shared.log_subscription_gate.lock_irqsave();
707        self.shared
708            .log_subscription_active
709            .store(false, Ordering::Release);
710        route.clear();
711        drop(route);
712        self.shared.console_progress.notify_all();
713        self.shared.bridge.notify();
714    }
715}
716
717/// Cloneable task-context output capability for one runtime UART.
718#[derive(Clone)]
719pub struct SerialTaskOutput {
720    shared: Arc<RuntimeShared>,
721}
722
723impl SerialTaskOutput {
724    pub fn try_write(&self, bytes: &[u8]) -> RuntimeResult<usize> {
725        let Some(_output) = self.shared.tty_output_lock.try_lock() else {
726            return Err(RuntimeError::WouldBlock);
727        };
728        SerialTxSender {
729            shared: self.shared.clone(),
730        }
731        .try_write(bytes)
732    }
733
734    pub fn write_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
735        let _output = self.shared.tty_output_lock.lock();
736        SerialTxSender {
737            shared: self.shared.clone(),
738        }
739        .write_all(bytes)
740    }
741
742    pub fn write_text_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
743        let _output = self.shared.tty_output_lock.lock();
744        SerialTxSender {
745            shared: self.shared.clone(),
746        }
747        .write_text_all(bytes)
748    }
749
750    pub fn write_fmt(&self, args: fmt::Arguments<'_>) -> fmt::Result {
751        let _output = self.shared.tty_output_lock.lock();
752        let mut writer = ActiveConsoleWriter {
753            sender: SerialTxSender {
754                shared: self.shared.clone(),
755            },
756        };
757        writer.write_fmt(args)
758    }
759
760    pub fn wait_idle(&self) -> RuntimeResult {
761        let _output = self.shared.tty_output_lock.lock();
762        SerialOutputBarrier::new(self.shared.clone()).wait_idle()
763    }
764
765    pub fn discard_pending(&self) -> RuntimeResult {
766        let _output = self.shared.tty_output_lock.lock();
767        self.shared.ensure_started()?;
768        self.shared
769            .control
770            .submit(ControlOp::DiscardTx, || self.shared.bridge.notify())
771    }
772
773    pub fn reconfigure(
774        &self,
775        config: Option<Config>,
776        drain: bool,
777        publish: impl FnOnce(),
778    ) -> RuntimeResult {
779        let _output = self.shared.tty_output_lock.lock();
780        let barrier = SerialOutputBarrier::new(self.shared.clone());
781        if drain {
782            barrier.wait_idle()?;
783        }
784        if let Some(config) = config {
785            barrier.set_config(config)?;
786        }
787        publish();
788        Ok(())
789    }
790
791    pub fn poll_source(&self) -> Arc<PollSet> {
792        self.shared.tx_source.clone()
793    }
794}
795
796impl SerialRxSubscription {
797    pub fn drain(&self, out: &mut [RxItem]) -> usize {
798        let count = self
799            .consumer
800            .lock()
801            .as_mut()
802            .map_or(0, |consumer| consumer.drain(out));
803        notify_drained_space(count, || self.shared.bridge.notify());
804        count
805    }
806
807    /// Blocks until RX data is available or the runtime stops.
808    pub fn wait_readable(&self) -> RuntimeResult {
809        self.shared.ensure_started()?;
810        self.shared.rx_progress.wait_until(|| {
811            self.consumer
812                .lock()
813                .as_ref()
814                .is_some_and(|consumer| !consumer.is_empty())
815                || !self.shared.started()
816        });
817        self.consumer
818            .lock()
819            .as_ref()
820            .is_some_and(|consumer| !consumer.is_empty())
821            .then_some(())
822            .ok_or(RuntimeError::SerialNotStarted)
823    }
824
825    pub fn discard_pending(&self) -> RuntimeResult {
826        self.shared.ensure_started()?;
827        self.clear_pending();
828        let result = self
829            .shared
830            .control
831            .submit(ControlOp::DiscardRx, || self.shared.bridge.notify());
832        self.clear_pending();
833        result
834    }
835
836    pub fn poll_source(&self) -> Arc<PollSet> {
837        self.shared.rx_source.clone()
838    }
839
840    pub(crate) fn wait_console_event(&self, logs: &SerialLogSubscription) -> RuntimeResult {
841        if !Arc::ptr_eq(&self.shared, &logs.shared) {
842            return Err(RuntimeError::OperationNotSupported);
843        }
844        self.shared.ensure_started()?;
845        self.shared
846            .console_progress
847            .wait_until(|| self.has_pending() || logs.has_pending() || !self.shared.started());
848        (self.has_pending() || logs.has_pending())
849            .then_some(())
850            .ok_or(RuntimeError::SerialNotStarted)
851    }
852
853    fn has_pending(&self) -> bool {
854        self.consumer
855            .lock()
856            .as_ref()
857            .is_some_and(|consumer| !consumer.is_empty())
858    }
859
860    fn clear_pending(&self) {
861        if let Some(consumer) = self.consumer.lock().as_mut() {
862            consumer.clear();
863        }
864        self.shared.bridge.notify();
865    }
866}
867
868impl Drop for SerialRxSubscription {
869    fn drop(&mut self) {
870        let Some(consumer) = self.consumer.get_mut().take() else {
871            return;
872        };
873        let mut available = self.shared.rx_subscription.lock_irqsave();
874        debug_assert!(
875            available.is_none(),
876            "serial runtime cannot have two RX consumers"
877        );
878        if available.is_none() {
879            *available = Some(consumer);
880        }
881    }
882}
883
884fn notify_drained_space(count: usize, notify_space: impl FnOnce()) {
885    if count != 0 {
886        notify_space();
887    }
888}
889
890pub fn runtimes() -> &'static [SerialRuntimeHandle] {
891    SERIAL_RUNTIMES.get().map_or(&[], Box::as_ref)
892}
893
894pub(crate) fn active_console() -> Option<&'static SerialRuntimeHandle> {
895    runtimes().get(ACTIVE_CONSOLE.load(Ordering::Acquire))
896}
897
898pub(crate) fn init(primary_cpu: usize) {
899    let log_mailbox = LOG_MAILBOX
900        .call_once(|| Arc::new(LogMailbox::new(ax_hal::cpu_num().max(1))))
901        .clone();
902    // `rust_main` initializes the primary scheduler and IPI/IRQ framework
903    // before serial discovery, so task-context doorbells are safe on this CPU.
904    log_mailbox.mark_wake_ready(primary_cpu);
905    let mut handles = Vec::new();
906    for serial in ax_driver::serial::take_serial_devices() {
907        match build_runtime(handles.len(), primary_cpu, serial, log_mailbox.clone()) {
908            Ok(handle) => handles.push(handle),
909            Err(err) => warn!("failed to initialize serial runtime: {err:?}"),
910        }
911    }
912    SERIAL_RUNTIMES.call_once(|| handles.into_boxed_slice());
913}
914
915#[cfg(feature = "smp")]
916pub(crate) fn mark_log_wake_ready(cpu_id: usize) {
917    if let Some(log_mailbox) = LOG_MAILBOX.get() {
918        log_mailbox.mark_wake_ready(cpu_id);
919    }
920}
921
922fn build_runtime(
923    index: usize,
924    primary_cpu: usize,
925    serial: SerialDevice,
926    log_mailbox: Arc<LogMailbox>,
927) -> RuntimeResult<SerialRuntimeHandle> {
928    let SerialDevice {
929        info,
930        port,
931        mut irq,
932        register_gate,
933    } = serial;
934    let polling = info.irq.is_none();
935    let bridge = Arc::new(RuntimeIrqBridge::new());
936    let stats = Arc::new(SerialStatsAtomic::new());
937    let register_gate: Arc<rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>> =
938        Arc::from(register_gate);
939    let (irq_rx_producer, irq_rx_consumer) = spsc::channel(IRQ_RX_CAPACITY);
940    let (rx_output_producer, rx_output_consumer) = spsc::channel(SUBSCRIPTION_RX_CAPACITY);
941    let shared = Arc::new(RuntimeShared {
942        index,
943        info,
944        owner_cpu: primary_cpu,
945        polling,
946        port: RawSpinLock::new(port),
947        register_gate: register_gate.clone(),
948        ingress: TxIngress::new(),
949        log_mailbox,
950        rx_subscription: RawSpinLock::new(Some(rx_output_consumer)),
951        log_subscription_gate: RawSpinLock::new(OrderedOutput::new(LOG_SUBSCRIPTION_CAPACITY)),
952        log_subscription_active: AtomicBool::new(false),
953        log_subscription_dropped_records: AtomicUsize::new(0),
954        log_subscription_dropped_bytes: AtomicUsize::new(0),
955        control: ControlQueue::new(),
956        bridge: bridge.clone(),
957        stats: stats.clone(),
958        rx_source: Arc::new(PollSet::new()),
959        tx_source: Arc::new(PollSet::new()),
960        rx_progress: WaitQueue::new(),
961        console_progress: WaitQueue::new(),
962        tx_progress: WaitQueue::new(),
963        tty_output_lock: Mutex::new(()),
964        log_barriers: AtomicUsize::new(0),
965        lifecycle: RuntimeLifecycle::new(),
966        irq_handle: OnceLock::new(),
967    });
968
969    let worker = SerialWorker::new(shared.clone(), irq_rx_consumer, rx_output_producer);
970    let owner_cpu =
971        u32::try_from(primary_cpu).map_err(|_| RuntimeError::InvalidCpu { cpu: primary_cpu })?;
972    let mut affinity = CpuSet::empty(ax_hal::cpu_num());
973    if !affinity.insert(CpuId::new(owner_cpu)) {
974        return Err(RuntimeError::InvalidCpu { cpu: primary_cpu });
975    }
976
977    let mut pending_irq_registration = None;
978    if let Some(binding) = shared.info.irq.clone() {
979        let irq_id = crate::irq::resolve_binding_irq(binding).map_err(|error| {
980            warn!(
981                "failed to resolve serial IRQ for {}: {error:?}",
982                shared.info.name
983            );
984            RuntimeError::from(error)
985        })?;
986        let callback_bridge = bridge.clone();
987        let callback_stats = stats.clone();
988        let mut callback_rx = RuntimeIrqPublisher {
989            producer: irq_rx_producer,
990            bridge: bridge.clone(),
991            stats: stats.clone(),
992        };
993        let callback_gate = register_gate.clone();
994        let request = serial_irq_request(
995            ax_hal::irq::IrqRequest::new(move |_| {
996                let Some(_register_access) =
997                    try_enter_irq_registers(&callback_gate, &callback_bridge)
998                else {
999                    return ax_hal::irq::IrqReturn::Handled;
1000                };
1001                let Some(report) = irq.handle() else {
1002                    callback_stats.spurious_irq();
1003                    return ax_hal::irq::IrqReturn::Unhandled;
1004                };
1005                let event = callback_rx.publish(report);
1006                mask_deferred_irq_rx(&mut *irq, event);
1007                callback_stats.handled_irq(event);
1008                callback_bridge.latch.publish(event);
1009                callback_bridge.notify();
1010                ax_hal::irq::IrqReturn::Handled
1011            }),
1012            primary_cpu,
1013        );
1014        let handle = ax_hal::irq::request_irq(irq_id, request).map_err(|error| {
1015            warn!(
1016                "failed to register serial IRQ for {}: {error:?}",
1017                shared.info.name
1018            );
1019            RuntimeError::from(error)
1020        })?;
1021        shared.irq_handle.call_once(|| handle);
1022        pending_irq_registration = Some(PendingIrqRegistration::new(
1023            handle,
1024            shared.info.name.clone(),
1025        ));
1026    }
1027
1028    crate::thread::builder(alloc::format!("serial{index}-maint"))
1029        .stack_size(crate::thread::default_task_stack_size())
1030        .policy(serial_worker_policy())
1031        .affinity(affinity)
1032        .spawn(move || worker.run())
1033        .map_err(|error| {
1034            warn!(
1035                "failed to start serial maintenance worker for {}: {error}",
1036                shared.info.name
1037            );
1038            RuntimeError::from(error)
1039        })?;
1040    if let Some(registration) = pending_irq_registration {
1041        registration.commit();
1042    }
1043    info!(
1044        "serial runtime {} ready: cpu={}, irq={:?}, polling={}",
1045        shared.info.name, shared.owner_cpu, shared.info.irq, shared.polling
1046    );
1047    Ok(SerialRuntimeHandle { shared })
1048}
1049
1050fn serial_irq_request(
1051    request: ax_hal::irq::IrqRequest,
1052    primary_cpu: usize,
1053) -> ax_hal::irq::IrqRequest {
1054    request
1055        .share_mode(ax_hal::irq::ShareMode::Shared)
1056        .affinity(ax_hal::irq::IrqAffinity::Fixed(ax_hal::irq::CpuId(
1057            primary_cpu,
1058        )))
1059        .auto_enable(ax_hal::irq::AutoEnable::No)
1060}
1061
1062/// IRQ-safe publication boundary captured beside the IRQ-owned driver endpoint.
1063///
1064/// The registered callback cannot reach the serial worker, control queue, or
1065/// device manager. It can only execute a bounded register transaction and
1066/// publish value reports into preallocated state.
1067struct RuntimeIrqPublisher {
1068    producer: SpscProducer<rdif_serial::RxSample>,
1069    bridge: Arc<RuntimeIrqBridge>,
1070    stats: Arc<SerialStatsAtomic>,
1071}
1072
1073impl RuntimeIrqPublisher {
1074    fn publish(&mut self, mut report: rdif_serial::SerialIrqReport) -> rdif_serial::SerialIrqEvent {
1075        // Preserve the driver's bounded-IRQ decision. A fully drained UART
1076        // must remain armed while the owner transports its samples; masking a
1077        // small FIFO until task context runs can overflow at line rate.
1078        for &sample in report.rx.as_slice() {
1079            if self.producer.push(sample).is_err() {
1080                self.stats.add_rx_dropped(1);
1081                self.bridge.rx_overflow.store(true, Ordering::Release);
1082                report.event.rx_errors |= rdif_serial::RxErrorFlags::OVERRUN;
1083                report.event.rearm |= rdif_serial::SerialEventSet::RX;
1084            }
1085        }
1086        report.event
1087    }
1088}
1089
1090fn mask_deferred_irq_rx(irq: &mut dyn rdif_serial::UartIrq, event: rdif_serial::SerialIrqEvent) {
1091    if event.rearm.intersects(rdif_serial::SerialEventSet::RX) {
1092        irq.mask(rdif_serial::SerialEventSet::RX);
1093    }
1094}
1095
1096/// Publishes one complete ordinary record without waiting for UART progress.
1097pub(crate) fn try_publish_record(
1098    meta: ax_log::RecordMeta,
1099    args: fmt::Arguments<'_>,
1100) -> Option<ax_log::PublishStatus> {
1101    let index = ACTIVE_CONSOLE.load(Ordering::Acquire);
1102    let runtime = runtimes().get(index)?;
1103    let guard = ax_task::sync::PreemptIrqSaveGuard::new();
1104    // SAFETY: `guard` prevents task migration and local IRQ re-entry for the
1105    // whole callback; runtime CPU-local state is installed before handoff.
1106    let (outcome, log_wake_ready) = unsafe {
1107        ax_hal::percpu::with_cpu_pin(|pin| {
1108            let cpu_id = ax_hal::percpu::this_cpu_id_pinned(pin);
1109            let task_id = crate::task::thread::current::current_thread_id()
1110                .ok()
1111                .map(|thread| thread.as_u64());
1112            let timestamp_nanos = ax_hal::time::monotonic_time().as_nanos() as u64;
1113            let record_meta = match meta.kind() {
1114                ax_log::RecordKind::Print => LogRecordMeta::print(timestamp_nanos, task_id),
1115                ax_log::RecordKind::Log => LogRecordMeta::log(timestamp_nanos, task_id),
1116            };
1117            (
1118                runtime.shared.publish_log(cpu_id, record_meta, args),
1119                runtime.shared.log_mailbox.wake_ready(cpu_id),
1120            )
1121        })
1122    }
1123    .unwrap_or_else(|_| (log_mailbox::PublishOutcome::dropped(0), false));
1124    drop(guard);
1125    runtime
1126        .shared
1127        .stats
1128        .add_log_dropped(outcome.dropped_source_bytes());
1129    runtime
1130        .shared
1131        .stats
1132        .add_log_dropped_records(outcome.dropped_records());
1133    match record_wake_context(
1134        outcome.published(),
1135        ax_hal::irq::in_irq_context(),
1136        log_wake_ready,
1137    ) {
1138        RecordWakeContext::Interrupt => {
1139            runtime.shared.bridge.notify();
1140        }
1141        RecordWakeContext::Task => {
1142            runtime.shared.bridge.notify();
1143        }
1144        RecordWakeContext::None => {}
1145    }
1146    Some(if !outcome.published() {
1147        ax_log::PublishStatus::Dropped
1148    } else if outcome.truncated() {
1149        ax_log::PublishStatus::Truncated
1150    } else {
1151        ax_log::PublishStatus::Published
1152    })
1153}
1154
1155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1156enum RecordWakeContext {
1157    None,
1158    Interrupt,
1159    Task,
1160}
1161
1162const fn record_wake_context(
1163    published: bool,
1164    in_irq_context: bool,
1165    log_wake_ready: bool,
1166) -> RecordWakeContext {
1167    if !published || !log_wake_ready {
1168        RecordWakeContext::None
1169    } else if in_irq_context {
1170        RecordWakeContext::Interrupt
1171    } else {
1172        RecordWakeContext::Task
1173    }
1174}
1175
1176/// Synchronously streams one emergency record without the log mailbox.
1177pub(crate) fn emergency_write(args: fmt::Arguments<'_>) -> Option<usize> {
1178    let index = ACTIVE_CONSOLE.load(Ordering::Acquire);
1179    let runtime = runtimes().get(index)?;
1180    let Some(_formatting) = EmergencyFormatting::try_enter() else {
1181        runtime.shared.stats.add_log_dropped_records(1);
1182        return Some(0);
1183    };
1184    let Some(register_access) = claim_emergency_registers(&runtime.shared.register_gate) else {
1185        runtime.shared.stats.add_log_dropped_records(1);
1186        return Some(0);
1187    };
1188    let mut writer = EmergencyWriter::new(register_access);
1189    writer.begin_record();
1190    if writer.write_fmt(args).is_err() {
1191        runtime.shared.stats.add_log_dropped_records(1);
1192    }
1193    Some(writer.source_written)
1194}
1195
1196const EMERGENCY_CLAIM_ATTEMPTS: usize = 4096;
1197static EMERGENCY_FORMATTING: AtomicBool = AtomicBool::new(false);
1198
1199fn claim_emergency_registers(
1200    gate: &rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>,
1201) -> Option<rdif_serial::UartEmergencyAccess<'_, dyn rdif_serial::UartEmergencyTx>> {
1202    for _ in 0..EMERGENCY_CLAIM_ATTEMPTS {
1203        if let Some(access) = gate.try_begin_emergency() {
1204            return Some(access);
1205        }
1206        core::hint::spin_loop();
1207    }
1208    None
1209}
1210
1211struct EmergencyFormatting;
1212
1213impl EmergencyFormatting {
1214    fn try_enter() -> Option<Self> {
1215        EMERGENCY_FORMATTING
1216            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1217            .ok()
1218            .map(|_| Self)
1219    }
1220}
1221
1222impl Drop for EmergencyFormatting {
1223    fn drop(&mut self) {
1224        EMERGENCY_FORMATTING.store(false, Ordering::Release);
1225    }
1226}
1227
1228struct EmergencyWriter<'a, E: rdif_serial::UartEmergencyTx + ?Sized> {
1229    access: rdif_serial::UartEmergencyAccess<'a, E>,
1230    source_written: usize,
1231}
1232
1233const EMERGENCY_RECORD_BOUNDARY: &[u8] = b"\x1b[0m\r\n";
1234
1235impl<'a, E: rdif_serial::UartEmergencyTx + ?Sized> EmergencyWriter<'a, E> {
1236    const fn new(access: rdif_serial::UartEmergencyAccess<'a, E>) -> Self {
1237        Self {
1238            access,
1239            source_written: 0,
1240        }
1241    }
1242
1243    fn begin_record(&self) {
1244        self.write_all_blocking(EMERGENCY_RECORD_BOUNDARY);
1245    }
1246
1247    fn write_all_blocking(&self, mut bytes: &[u8]) {
1248        while !bytes.is_empty() {
1249            let written = self.access.try_write(bytes).min(bytes.len());
1250            if written == 0 {
1251                core::hint::spin_loop();
1252            } else {
1253                bytes = &bytes[written..];
1254            }
1255        }
1256    }
1257}
1258
1259impl<E: rdif_serial::UartEmergencyTx + ?Sized> Write for EmergencyWriter<'_, E> {
1260    fn write_str(&mut self, text: &str) -> fmt::Result {
1261        let mut remaining = text.as_bytes();
1262        while let Some(newline) = remaining.iter().position(|&byte| byte == b'\n') {
1263            self.write_all_blocking(&remaining[..newline]);
1264            self.write_all_blocking(b"\r\n");
1265            remaining = &remaining[newline + 1..];
1266        }
1267        self.write_all_blocking(remaining);
1268        self.source_written = self.source_written.saturating_add(text.len());
1269        Ok(())
1270    }
1271}
1272
1273fn deactivate_console(shared: &RuntimeShared) {
1274    if ACTIVE_CONSOLE
1275        .compare_exchange(
1276            shared.index,
1277            NO_ACTIVE_CONSOLE,
1278            Ordering::AcqRel,
1279            Ordering::Acquire,
1280        )
1281        .is_ok()
1282    {
1283        shared.log_mailbox.release(shared.index);
1284        shared.bridge.notify();
1285    }
1286}
1287
1288struct ActiveConsoleWriter {
1289    sender: SerialTxSender,
1290}
1291
1292impl Write for ActiveConsoleWriter {
1293    fn write_str(&mut self, text: &str) -> fmt::Result {
1294        self.sender
1295            .write_text_all(text.as_bytes())
1296            .map(|_| ())
1297            .map_err(|_| fmt::Error)
1298    }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304
1305    struct RecordingEmergencyTx(&'static std::sync::Mutex<Vec<u8>>);
1306
1307    impl rdif_serial::UartEmergencyTx for RecordingEmergencyTx {
1308        unsafe fn mask_interrupts_unlocked(&self) {}
1309
1310        unsafe fn try_write_unlocked(&self, bytes: &[u8]) -> usize {
1311            self.0.lock().unwrap().extend_from_slice(bytes);
1312            bytes.len()
1313        }
1314    }
1315
1316    struct ChunkedEmergencyTx(&'static AtomicUsize);
1317
1318    impl rdif_serial::UartEmergencyTx for ChunkedEmergencyTx {
1319        unsafe fn mask_interrupts_unlocked(&self) {}
1320
1321        unsafe fn try_write_unlocked(&self, bytes: &[u8]) -> usize {
1322            let written = bytes.len().min(7);
1323            self.0.fetch_add(written, Ordering::Relaxed);
1324            written
1325        }
1326    }
1327
1328    struct RecordingIrq {
1329        masked: rdif_serial::SerialEventSet,
1330    }
1331
1332    #[test]
1333    fn failed_closed_runtime_cannot_return_to_dormant_or_started() {
1334        let lifecycle = RuntimeLifecycle::new();
1335
1336        assert_eq!(
1337            lifecycle.ensure_started(),
1338            Err(RuntimeError::SerialNotStarted)
1339        );
1340        lifecycle.set_started(true);
1341        assert!(lifecycle.ensure_started().is_ok());
1342
1343        lifecycle.fail_closed();
1344        assert_eq!(
1345            lifecycle.ensure_available(),
1346            Err(RuntimeError::ConsoleFailedClosed)
1347        );
1348        assert_eq!(
1349            lifecycle.ensure_started(),
1350            Err(RuntimeError::ConsoleFailedClosed)
1351        );
1352
1353        lifecycle.set_started(false);
1354        lifecycle.set_started(true);
1355        assert_eq!(
1356            lifecycle.ensure_started(),
1357            Err(RuntimeError::ConsoleFailedClosed)
1358        );
1359    }
1360
1361    impl rdif_serial::UartIrq for RecordingIrq {
1362        fn mask(&mut self, sources: rdif_serial::SerialEventSet) {
1363            self.masked |= sources;
1364        }
1365
1366        fn handle(&mut self) -> Option<rdif_serial::SerialIrqReport> {
1367            None
1368        }
1369    }
1370
1371    #[test]
1372    fn emergency_writer_streams_a_record_larger_than_the_former_buffer() {
1373        static HARDWARE_BYTES: AtomicUsize = AtomicUsize::new(0);
1374
1375        HARDWARE_BYTES.store(0, Ordering::Relaxed);
1376        let gate = rdif_serial::UartRegisterGate::new(ChunkedEmergencyTx(&HARDWARE_BYTES));
1377        let access = gate.try_begin_emergency().expect("emergency takeover");
1378        let mut writer = EmergencyWriter::new(access);
1379        let payload = "x".repeat(2_048);
1380
1381        writer.write_str(&payload).unwrap();
1382        writer.write_str("\nBACKTRACE_END").unwrap();
1383
1384        assert_eq!(writer.source_written, payload.len() + 14);
1385        assert_eq!(HARDWARE_BYTES.load(Ordering::Relaxed), payload.len() + 15);
1386        assert!(gate.try_enter().is_none());
1387    }
1388
1389    #[test]
1390    fn emergency_writer_starts_a_terminal_safe_record() {
1391        let hardware: &'static std::sync::Mutex<Vec<u8>> =
1392            Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
1393        let gate = rdif_serial::UartRegisterGate::new(RecordingEmergencyTx(hardware));
1394        let access = gate.try_begin_emergency().expect("emergency takeover");
1395        let mut writer = EmergencyWriter::new(access);
1396        let payload = "ARCEOS_PANIC_EMERGENCY\n";
1397
1398        writer.begin_record();
1399        writer.write_str(payload).unwrap();
1400
1401        let bytes = hardware.lock().unwrap();
1402        assert_eq!(
1403            bytes.as_slice(),
1404            b"\x1b[0m\r\nARCEOS_PANIC_EMERGENCY\r\n",
1405            "the panic marker must not become the final byte of an interrupted ANSI sequence"
1406        );
1407        assert_eq!(writer.source_written, payload.len());
1408    }
1409
1410    #[test]
1411    fn serial_worker_uses_linux_console_worker_priority() {
1412        assert_eq!(
1413            serial_worker_policy(),
1414            SchedulePolicy::fair(Nice::new(-20).unwrap(), FairMode::Normal)
1415        );
1416    }
1417
1418    #[test]
1419    fn irq_report_drops_only_after_the_preallocated_ring_is_full() {
1420        let bridge = Arc::new(RuntimeIrqBridge::new());
1421        let stats = Arc::new(SerialStatsAtomic::new());
1422        let (producer, mut consumer) = spsc::channel(2);
1423        let mut publisher = RuntimeIrqPublisher {
1424            producer,
1425            bridge: bridge.clone(),
1426            stats: stats.clone(),
1427        };
1428        let samples = [
1429            rdif_serial::RxSample {
1430                byte: Some(1),
1431                ..rdif_serial::RxSample::default()
1432            },
1433            rdif_serial::RxSample {
1434                byte: Some(2),
1435                ..rdif_serial::RxSample::default()
1436            },
1437            rdif_serial::RxSample {
1438                byte: Some(3),
1439                ..rdif_serial::RxSample::default()
1440            },
1441        ];
1442        let mut batch = rdif_serial::IrqRxBatch::new();
1443        for sample in samples {
1444            batch.try_push(sample).unwrap();
1445        }
1446        let event = publisher.publish(rdif_serial::SerialIrqReport::new(
1447            rdif_serial::SerialIrqEvent::default(),
1448            batch,
1449        ));
1450
1451        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(1));
1452        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(2));
1453        assert!(consumer.pop().is_none());
1454        assert_eq!(stats.snapshot().rx_dropped, 1);
1455        assert!(bridge.rx_overflow.load(Ordering::Acquire));
1456        assert!(event.rx_errors.contains(rdif_serial::RxErrorFlags::OVERRUN));
1457        assert!(event.rearm.contains(rdif_serial::SerialEventSet::RX));
1458    }
1459
1460    #[test]
1461    fn fully_drained_rx_irq_keeps_hardware_source_armed() {
1462        let bridge = Arc::new(RuntimeIrqBridge::new());
1463        let stats = Arc::new(SerialStatsAtomic::new());
1464        let (producer, mut consumer) = spsc::channel(2);
1465        let mut publisher = RuntimeIrqPublisher {
1466            producer,
1467            bridge,
1468            stats,
1469        };
1470        let mut batch = rdif_serial::IrqRxBatch::new();
1471        batch
1472            .try_push(rdif_serial::RxSample {
1473                byte: Some(b'x'),
1474                ..rdif_serial::RxSample::default()
1475            })
1476            .unwrap();
1477
1478        let event = publisher.publish(rdif_serial::SerialIrqReport::new(
1479            rdif_serial::SerialIrqEvent {
1480                events: rdif_serial::SerialEventSet::RX_DATA,
1481                ..rdif_serial::SerialIrqEvent::default()
1482            },
1483            batch,
1484        ));
1485
1486        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(b'x'));
1487        assert!(
1488            !event.rearm.contains(rdif_serial::SerialEventSet::RX),
1489            "a drained IRQ must not leave a small UART FIFO masked until the owner task runs"
1490        );
1491    }
1492
1493    #[test]
1494    fn deferred_rx_masks_only_the_uart_source() {
1495        let mut irq = RecordingIrq {
1496            masked: rdif_serial::SerialEventSet::empty(),
1497        };
1498        mask_deferred_irq_rx(
1499            &mut irq,
1500            rdif_serial::SerialIrqEvent {
1501                rearm: rdif_serial::SerialEventSet::RX | rdif_serial::SerialEventSet::TX_SPACE,
1502                ..rdif_serial::SerialIrqEvent::default()
1503            },
1504        );
1505
1506        assert_eq!(irq.masked, rdif_serial::SerialEventSet::RX);
1507    }
1508
1509    #[test]
1510    fn subscription_drain_notifies_a_worker_waiting_for_output_space() {
1511        let (mut producer, consumer) = spsc::channel(1);
1512        producer.push(RxItem::Overrun).unwrap();
1513        let mut consumer = consumer;
1514        let mut item = [RxItem::default()];
1515        let mut notify_count = 0;
1516
1517        let count = consumer.drain(&mut item);
1518        notify_drained_space(count, || notify_count += 1);
1519        assert_eq!(count, 1);
1520        assert_eq!(item, [RxItem::Overrun]);
1521        assert_eq!(notify_count, 1);
1522    }
1523
1524    #[test]
1525    fn serial_irq_stays_disabled_until_the_worker_starts_the_port() {
1526        let request = serial_irq_request(
1527            ax_hal::irq::IrqRequest::new(|_| ax_hal::irq::IrqReturn::Handled),
1528            0,
1529        );
1530
1531        assert_eq!(
1532            request.auto_enable_mode(),
1533            ax_hal::irq::AutoEnable::No,
1534            "the IRQ action must not run before the worker has configured the UART"
1535        );
1536    }
1537
1538    #[test]
1539    fn serial_work_is_coalesced_by_the_irq_doorbell() {
1540        let bridge = RuntimeIrqBridge::new();
1541
1542        bridge.notify();
1543
1544        assert!(bridge.worker_signal.is_pending());
1545    }
1546
1547    #[test]
1548    fn irq_gate_conflict_is_published_for_task_context_retry() {
1549        let bridge = RuntimeIrqBridge::new();
1550        let gate = UartRegisterGate::new(());
1551        let _owner = gate.try_enter().expect("first register owner");
1552
1553        assert!(try_enter_irq_registers(&gate, &bridge).is_none());
1554        assert!(
1555            bridge.take_register_retry(),
1556            "the hard-IRQ path must not silently discard an event while emergency TX owns \
1557             registers"
1558        );
1559        assert!(bridge.worker_signal.is_pending());
1560    }
1561
1562    #[test]
1563    fn absent_runtime_console_preserves_early_publication_fallback() {
1564        ACTIVE_CONSOLE.store(NO_ACTIVE_CONSOLE, Ordering::Release);
1565        assert_eq!(
1566            try_publish_record(ax_log::RecordMeta::print(), format_args!("fallback")),
1567            None
1568        );
1569    }
1570
1571    #[test]
1572    fn early_secondary_log_does_not_wake_before_log_wake_ready() {
1573        assert_eq!(
1574            record_wake_context(true, false, false),
1575            RecordWakeContext::None
1576        );
1577        assert_eq!(
1578            record_wake_context(true, false, true),
1579            RecordWakeContext::Task
1580        );
1581        assert_eq!(
1582            record_wake_context(true, true, false),
1583            RecordWakeContext::None
1584        );
1585        assert_eq!(
1586            record_wake_context(true, true, true),
1587            RecordWakeContext::Interrupt
1588        );
1589    }
1590
1591    #[test]
1592    fn wake_ready_transition_preserves_early_secondary_records() {
1593        const OWNER: usize = 7;
1594        let mailbox = Arc::new(LogMailbox::new(2));
1595        assert!(mailbox.claim(OWNER));
1596
1597        let early = mailbox.try_publish(
1598            1,
1599            LogRecordMeta::log(1, None),
1600            format_args!("secondary started\n"),
1601        );
1602        assert!(early.published());
1603        assert_eq!(
1604            record_wake_context(early.published(), false, mailbox.wake_ready(1)),
1605            RecordWakeContext::None
1606        );
1607
1608        mailbox.mark_wake_ready(1);
1609        let ready = mailbox.try_publish(
1610            1,
1611            LogRecordMeta::log(2, Some(8)),
1612            format_args!("secondary init OK\n"),
1613        );
1614        assert!(ready.published());
1615        assert_eq!(
1616            record_wake_context(ready.published(), false, mailbox.wake_ready(1)),
1617            RecordWakeContext::Task
1618        );
1619
1620        let mut reader = mailbox.reader();
1621        assert!(
1622            reader
1623                .take(OWNER)
1624                .is_some_and(|record| record.record.bytes().ends_with(b"secondary started\r\n"))
1625        );
1626        assert!(
1627            reader
1628                .take(OWNER)
1629                .is_some_and(|record| record.record.bytes().ends_with(b"secondary init OK\r\n"))
1630        );
1631        assert!(reader.take(OWNER).is_none());
1632    }
1633}