1mod 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;
51const 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 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 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 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 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 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 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#[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 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 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 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 pub(crate) fn output_barrier(&self) -> RuntimeResult<SerialOutputBarrier> {
459 self.shared.ensure_started()?;
460 Ok(SerialOutputBarrier::new(self.shared.clone()))
461 }
462
463 pub(crate) fn begin_console_handoff(&self) -> RuntimeResult {
465 ax_hal::console::begin_runtime_handoff()?;
466 Ok(())
467 }
468
469 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 pub(crate) fn fail_console_closed(&self) {
487 self.shared.fail_closed();
488 }
489
490 pub(crate) fn commit_console_handoff(&self) -> RuntimeResult {
492 self.shared.ensure_started()?;
493 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#[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 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 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
605pub(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 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 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
642pub struct SerialRxSubscription {
644 consumer: Mutex<Option<SpscConsumer<RxItem>>>,
645 shared: Arc<RuntimeShared>,
646}
647
648pub(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#[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 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 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
1062struct 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 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
1096pub(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 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
1176pub(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}