strat9_kernel/arch/x86_64/speaker.rs
1//! PC Speaker driver : beep codes + music playback.
2//!
3//! Hardware: PIT channel 2 (port 0x42) generates a square wave,
4//! port 0x61 bits 0-1 gate the speaker on/off.
5//!
6//! Two modes:
7//! 1. **Blocking** (boot): `beep()` busy-waits : used before scheduler.
8//! 2. **Music** (runtime): note queue drained by PIT timer interrupt.
9
10use super::io::{inb, outb};
11use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering};
12
13const PIT_FREQ: u32 = 1_193_182;
14const PIT_CHANNEL2_CMD: u8 = 0xB2; // channel 2, lobyte/hibyte, mode 3 (square wave), binary
15const SPEAKER_GATE_BIT: u8 = 1 << 0;
16const SPEAKER_DATA_BIT: u8 = 1 << 1;
17
18static SPEAKER_ACTIVE: AtomicBool = AtomicBool::new(false);
19static MUSIC_PLAYING: AtomicBool = AtomicBool::new(false);
20
21/// Current note frequency (Hz). Updated by the music player on timer tick.
22static NOTE_FREQ: AtomicU32 = AtomicU32::new(0);
23
24/// Remaining ticks for the current note.
25static NOTE_TICKS_LEFT: AtomicU32 = AtomicU32::new(0);
26
27/// Index into the current note sequence.
28static NOTE_INDEX: AtomicU32 = AtomicU32::new(0);
29
30/// Total number of notes in the current sequence.
31static NOTE_TOTAL: AtomicU32 = AtomicU32::new(0);
32
33/// Pointer to the note sequence array (freq in Hz).
34static mut NOTE_SEQ_PTR: *const u32 = core::ptr::null();
35
36/// Pointer to the duration array (in milliseconds).
37static mut NOTE_DUR_PTR: *const u32 = core::ptr::null();
38
39/// Tempo multiplier (100 = normal, 200 = double speed, 50 = half speed).
40static TEMPO: AtomicU16 = AtomicU16::new(100);
41
42// =========================================================================
43// Low-level speaker control
44// =========================================================================
45
46/// Set the PIT channel 2 frequency. `hz` = 0 disables the tone.
47#[inline]
48unsafe fn pit_set_freq(hz: u32) {
49 if hz == 0 || hz > 100_000 {
50 outb(0x43, PIT_CHANNEL2_CMD);
51 outb(0x42, 0);
52 outb(0x42, 0);
53 return;
54 }
55 let count = PIT_FREQ / hz;
56 outb(0x43, PIT_CHANNEL2_CMD);
57 outb(0x42, (count & 0xFF) as u8);
58 outb(0x42, ((count >> 8) & 0xFF) as u8);
59}
60
61/// Enable or disable the speaker output.
62#[inline]
63unsafe fn speaker_enable(on: bool) {
64 let val = inb(0x61);
65 if on {
66 outb(0x61, val | SPEAKER_GATE_BIT | SPEAKER_DATA_BIT);
67 } else {
68 outb(0x61, val & !(SPEAKER_GATE_BIT | SPEAKER_DATA_BIT));
69 }
70}
71
72// =========================================================================
73// Blocking API (pre-scheduler, boot-time diagnostics)
74// =========================================================================
75
76/// Play a tone at `freq` Hz for `ms` milliseconds. Blocks via PIT busy-wait.
77pub fn beep(freq: u32, ms: u32) {
78 if freq == 0 || ms == 0 {
79 return;
80 }
81 unsafe {
82 pit_set_freq(freq);
83 speaker_enable(true);
84 }
85 pit_busy_wait(ms);
86 unsafe {
87 speaker_enable(false);
88 }
89}
90
91/// Silence the speaker immediately.
92pub fn beep_off() {
93 unsafe {
94 speaker_enable(false);
95 }
96 SPEAKER_ACTIVE.store(false, Ordering::Relaxed);
97}
98
99/// Busy-wait for `ms` milliseconds using PIT channel 2 (mode 0, one-shot).
100/// Works before the APIC timer / scheduler are running.
101fn pit_busy_wait(ms: u32) {
102 if ms == 0 {
103 return;
104 }
105 // PIT one-shot: count = microseconds * PIT_FREQ / 1_000_000
106 // For large waits, split into chunks of ~50ms (PIT 16-bit max count ~54ms)
107 let chunk_ms = core::cmp::min(ms, 50);
108 let chunks = (ms + chunk_ms - 1) / chunk_ms;
109
110 for _ in 0..chunks {
111 let wait_ms = core::cmp::min(chunk_ms, ms);
112 let count = (PIT_FREQ * wait_ms) / 1000;
113 let count = count.min(0xFFFF) as u16;
114
115 unsafe {
116 // Gate LOW → disable counting
117 let val = inb(0x61);
118 outb(0x61, val & !SPEAKER_GATE_BIT);
119
120 // Program PIT channel 2: mode 0 (one-shot)
121 outb(0x43, 0xB0);
122 outb(0x42, (count & 0xFF) as u8);
123 outb(0x42, ((count >> 8) & 0xFF) as u8);
124
125 // Gate HIGH → start counting
126 outb(0x61, val | SPEAKER_GATE_BIT);
127
128 // Poll bit 5 of port 0x61 (T2 output) until it goes HIGH
129 loop {
130 if inb(0x61) & (1 << 5) != 0 {
131 break;
132 }
133 core::hint::spin_loop();
134 }
135 }
136 }
137}
138
139// =========================================================================
140// Boot diagnostic beep codes
141// =========================================================================
142
143/// Short beep : milestone reached.
144pub fn beep_ok() {
145 beep(880, 80); // A5, 80ms
146}
147
148/// Long beep : critical failure.
149pub fn beep_fail() {
150 beep(220, 300); // A3, 300ms
151}
152
153/// Double beep : warning.
154pub fn beep_warn() {
155 beep(660, 60); // E5, 60ms
156 pit_busy_wait(60);
157 beep(660, 60);
158}
159
160// =========================================================================
161// Boot crescendo : each phase plays a higher pitch
162// =========================================================================
163
164/// Play a crescendo beep for boot phase N.
165/// Each successive phase uses a higher frequency so you can identify
166/// the last completed phase by pitch alone.
167pub fn beep_phase(phase: u8) {
168 const PHASE_FREQS: [u32; 24] = [
169 // Boot fundamentals (1-6)
170 262, // 1: Kernel entry (C4)
171 294, // 2: Memory manager (D4)
172 330, // 3: Paging (E4)
173 349, // 4: APIC + SMP (F4)
174 392, // 5: Scheduler (G4)
175 440, // 6: Hardware drivers (A4)
176 // hardware::init() sub-drivers (7-14)
177 494, // 7: EC (B4)
178 523, // 8: Thermal (C5)
179 587, // 9: NIC (D5)
180 659, // 10: Storage (E5)
181 698, // 11: Timer (F5)
182 784, // 12: USB (G5)
183 880, // 13: VirtIO GPU (A5)
184 988, // 14: Framebuffer (B5)
185 // Individual drivers (15+)
186 1047, // 15: Hardware milestone (C6)
187 1175, // 16: Timers detailed (D6)
188 1319, // 17: USB detailed (E6)
189 1480, // 18: VirtIO block (F6)
190 1661, // 19: AHCI (G6)
191 1865, // 20: ATA (A6)
192 2093, // 21: NVMe (C7)
193 2349, // 22: VirtIO net (D7)
194 2637, // 23: VirtIO RNG (E7)
195 2794, // 24: VirtIO Console (F7)
196 ];
197 let idx = (phase as usize).min(PHASE_FREQS.len() - 1);
198 beep(PHASE_FREQS[idx], 60);
199}
200
201/// Panic melody : descending notes.
202pub fn beep_panic() {
203 let notes: [(u32, u32); 6] = [
204 (880, 150),
205 (740, 150),
206 (622, 150),
207 (523, 150),
208 (440, 150),
209 (330, 400),
210 ];
211 for (freq, dur) in notes {
212 beep(freq, dur);
213 pit_busy_wait(20);
214 }
215}
216
217/// Startup jingle : ascending notes (happy boot).
218pub fn beep_startup() {
219 let notes: [(u32, u32); 5] = [
220 (523, 80), // C5
221 (659, 80), // E5
222 (784, 80), // G5
223 (1047, 80), // C6
224 (1319, 150), // E6
225 ];
226 for (freq, dur) in notes {
227 beep(freq, dur);
228 pit_busy_wait(30);
229 }
230}
231
232// =========================================================================
233// Non-blocking music player (runtime, uses PIT tick)
234// =========================================================================
235
236/// A musical note: frequency in Hz and duration code.
237/// Duration codes map to note lengths at a given tempo.
238///
239/// | Code | Note value | Example at 120 BPM |
240/// |------|-----------|-------------------|
241/// | 1 | Whole | 2000ms |
242/// | 2 | Half | 1000ms |
243/// | 4 | Quarter | 500ms |
244/// | 8 | Eighth | 250ms |
245/// | 16 | Sixteenth | 125ms |
246pub struct Note {
247 pub freq: u32,
248 pub dur: u32, // duration code (1, 2, 4, 8, 16)
249}
250
251/// Special frequency values.
252pub const REST: u32 = 0; // silence
253pub const END: u32 = 0xFFFF; // end of sequence
254
255/// Start playing a note sequence. Non-blocking : drained by `speaker_tick()`.
256///
257/// # Safety
258/// `notes` and `durs` must remain valid until playback completes.
259pub unsafe fn music_start(notes: &[u32], durs: &[u32], tempo_bpm: u32) {
260 if notes.is_empty() || durs.is_empty() {
261 return;
262 }
263 // Tempo: quarter note duration in ms = 60000 / bpm
264 // Multiply by 100 for precision: 6_000_000 / bpm
265 let quarter_ms = 6_000_000u32 / tempo_bpm;
266 TEMPO.store((quarter_ms / 100) as u16, Ordering::Relaxed);
267
268 NOTE_SEQ_PTR = notes.as_ptr();
269 NOTE_DUR_PTR = durs.as_ptr();
270 NOTE_TOTAL.store(notes.len() as u32, Ordering::Relaxed);
271 NOTE_INDEX.store(0, Ordering::Relaxed);
272 MUSIC_PLAYING.store(true, Ordering::SeqCst);
273
274 // Start first note immediately
275 speaker_tick();
276}
277
278/// Stop music playback.
279pub fn music_stop() {
280 MUSIC_PLAYING.store(false, Ordering::SeqCst);
281 beep_off();
282}
283
284/// Returns whether music is currently playing.
285pub fn music_playing() -> bool {
286 MUSIC_PLAYING.load(Ordering::SeqCst)
287}
288
289/// Call this from the PIT timer interrupt (~10ms tick) to advance music playback.
290///
291/// The caller (timer ISR) must hold no locks that `speaker_tick` could contend.
292pub fn speaker_tick() {
293 if !MUSIC_PLAYING.load(Ordering::SeqCst) {
294 return;
295 }
296
297 let ticks_left = NOTE_TICKS_LEFT.load(Ordering::Relaxed);
298 if ticks_left > 1 {
299 NOTE_TICKS_LEFT.store(ticks_left - 1, Ordering::Relaxed);
300 return;
301 }
302
303 // Advance to next note
304 let idx = NOTE_INDEX.load(Ordering::Relaxed);
305 let total = NOTE_TOTAL.load(Ordering::Relaxed);
306
307 if idx >= total || unsafe { *NOTE_SEQ_PTR.add(idx as usize) } == END {
308 music_stop();
309 return;
310 }
311
312 let freq = unsafe { *NOTE_SEQ_PTR.add(idx as usize) };
313 let dur_code = unsafe { *NOTE_DUR_PTR.add(idx as usize) };
314
315 NOTE_INDEX.store(idx + 1, Ordering::Relaxed);
316
317 // Calculate duration in ticks (10ms per tick)
318 // base_ms = (quarter_ms * 4) / dur_code
319 let tempo = TEMPO.load(Ordering::Relaxed) as u32;
320 let quarter_ms = tempo * 100;
321 let base_ms = (quarter_ms * 4) / dur_code.max(1);
322 let ticks = (base_ms / 10).max(1);
323
324 NOTE_TICKS_LEFT.store(ticks, Ordering::Relaxed);
325 NOTE_FREQ.store(freq, Ordering::Relaxed);
326
327 unsafe {
328 if freq == REST || freq == 0 {
329 speaker_enable(false);
330 } else {
331 pit_set_freq(freq);
332 speaker_enable(true);
333 }
334 }
335}
336
337// =========================================================================
338// Built-in melodies
339// =========================================================================
340
341/// The Imperial March (Star Wars) : for panic screens.
342/// Notes: (freq Hz, duration code)
343pub const IMPERIAL_MARCH_NOTES: [u32; 16] = [
344 392, 392, 392, 311, 466, 392, 311, 466, 392, 0, 587, 587, 587, 622, 466, 0,
345];
346pub const IMPERIAL_MARCH_DURS: [u32; 16] = [4, 4, 4, 8, 2, 4, 8, 2, 4, 4, 4, 4, 4, 4, 2, 4];
347
348/// Startup jingle (ascending arpeggio).
349pub const STARTUP_NOTES: [u32; 7] = [523, 0, 659, 0, 784, 0, 1047];
350pub const STARTUP_DURS: [u32; 7] = [8, 16, 8, 16, 8, 16, 4];
351
352/// Error / failure tone.
353pub const ERROR_NOTES: [u32; 4] = [440, 415, 370, 330];
354pub const ERROR_DURS: [u32; 4] = [4, 4, 4, 2];