1mod 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;
49const 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 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 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 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 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 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#[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 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 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 pub(crate) fn output_barrier(&self) -> RuntimeResult<SerialOutputBarrier> {
415 self.shared.ensure_started()?;
416 Ok(SerialOutputBarrier::new(self.shared.clone()))
417 }
418
419 pub(crate) fn begin_console_handoff(&self) -> RuntimeResult {
421 ax_hal::console::begin_runtime_handoff()?;
422 Ok(())
423 }
424
425 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 pub(crate) fn fail_console_closed(&self) {
443 self.shared.fail_closed();
444 }
445
446 pub(crate) fn commit_console_handoff(&self) -> RuntimeResult {
448 self.shared.ensure_started()?;
449 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#[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 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 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
561pub(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 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 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
598pub struct SerialRxSubscription {
600 consumer: Mutex<Option<SpscConsumer<RxItem>>>,
601 shared: Arc<RuntimeShared>,
602}
603
604pub(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#[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 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 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
1029struct 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 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
1063pub(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 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
1146pub(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}