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