Skip to content

Commit 3210a82

Browse files
Merge pull request #1875 from rust-osdev/uefi-serial-improve-read-write
uefi: serial: add read_exact() and write_exact() + fix core::fmt::Write for Serial
2 parents 3cdf689 + 62f4691 commit 3210a82

File tree

3 files changed

+304
-7
lines changed

3 files changed

+304
-7
lines changed

uefi-test-runner/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ extern crate alloc;
1010

1111
use alloc::string::ToString;
1212
use alloc::vec::Vec;
13+
use core::fmt::Write;
1314
use uefi::mem::memory_map::MemoryMap;
1415
use uefi::prelude::*;
1516
use uefi::proto::console::serial::Serial;
@@ -115,7 +116,8 @@ fn send_request_helper(serial: &mut Serial, request: HostRequest) -> Result {
115116
serial.set_attributes(&io_mode)?;
116117

117118
// Send a screenshot request to the host.
118-
serial.write(request.as_bytes()).discard_errdata()?;
119+
// We transitively test Serial::write_exact() and Serial::write()
120+
write!(serial, "{}", request).expect("should write all bytes");
119121

120122
// Wait for the host's acknowledgement before moving forward.
121123
let mut reply = [0; 3];

uefi/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Added `Handle::component_name()` and `Handle::device_path()` to simplify the
2020
common use-case of querying more information about a handle.
2121
- Added `fs::path::Path::join()`.
22+
- Added `Serial::read_exact()` and `Serial::write_exact()`
2223

2324
## Changed
2425
- export all `text::{input, output}::*` types
@@ -42,6 +43,7 @@
4243
- **Breaking:** Renamed `DevicePathNode::to_string()` to `DevicePathNode::to_string16()`
4344
to better differentiate with the new `to_string()` coming from the new
4445
`Display`.
46+
- Fixed potential partial writes in `fmt::Write` impl for `Serial` protocol
4547

4648
# uefi - v0.36.1 (2025-11-05)
4749

uefi/src/proto/console/serial.rs

Lines changed: 299 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,80 @@
22

33
//! Abstraction over byte stream devices, also known as serial I/O devices.
44
5+
pub use uefi_raw::protocol::console::serial::{
6+
ControlBits, Parity, SerialIoMode as IoMode, StopBits,
7+
};
8+
59
use crate::proto::unsafe_protocol;
6-
use crate::{Error, Result, Status, StatusExt};
7-
use core::fmt;
10+
use crate::{Error, Result, ResultExt, Status, StatusExt, boot};
11+
use core::time::Duration;
12+
use core::{cmp, fmt};
13+
use log::error;
814
use uefi_raw::protocol::console::serial::{
915
SerialIoProtocol, SerialIoProtocol_1_1, SerialIoProtocolRevision,
1016
};
1117
use uguid::Guid;
1218

13-
pub use uefi_raw::protocol::console::serial::{
14-
ControlBits, Parity, SerialIoMode as IoMode, StopBits,
15-
};
19+
/// Returns the estimated time it takes to write a single byte.
20+
///
21+
/// This is conservative: it accounts for the actual serial mode settings
22+
/// (data bits, parity, stop bits) and rounds up to avoid underestimating.
23+
fn duration_per_byte_estimate(mode: &IoMode) -> Duration {
24+
if mode.baud_rate == 0 {
25+
// Baud rate unknown; assume a very slow link to be safe.
26+
return Duration::from_millis(100);
27+
}
28+
29+
// Count the number of bits per character conservatively:
30+
// - 1 start bit (always)
31+
// - data bits (use actual setting, fall back to maximum of 8)
32+
// - parity bit, if any
33+
// - stop bits (round up: ONE_FIVE and TWO both become 2)
34+
let data_bits = if mode.data_bits == 0 {
35+
8
36+
} else {
37+
mode.data_bits
38+
};
39+
40+
let parity_bits: u32 = if mode.parity == Parity::NONE || mode.parity == Parity::DEFAULT {
41+
0
42+
} else {
43+
1
44+
};
45+
46+
// Be conservative with stop bits: treat ONE_FIVE as 2.
47+
let stop_bits: u32 = match mode.stop_bits {
48+
StopBits::ONE => 1,
49+
StopBits::DEFAULT | StopBits::ONE_FIVE | StopBits::TWO => 2,
50+
// Unknown future variant: assume worst case.
51+
_ => 2,
52+
};
53+
54+
let bits_per_char = 1 + data_bits + parity_bits + stop_bits;
55+
56+
// Compute microseconds per bit, rounding up to avoid underestimating.
57+
let us_per_bit = 1_000_000_u64.div_ceil(mode.baud_rate);
58+
59+
// Total microseconds per byte/character.
60+
let us_per_byte = us_per_bit * (bits_per_char as u64);
61+
62+
Duration::from_micros(us_per_byte)
63+
}
64+
65+
/// Returns the estimated duration it takes to clear/transmit the UARTs internal
66+
/// FIFO.
67+
///
68+
/// This assumes the UART has each a transmit and a receive FIFO with the same
69+
/// size, which is the case for UART 16550 devices - the de-facto standard
70+
/// serial device.
71+
fn duration_fifo_estimate(mode: &IoMode, remaining: usize) -> Duration {
72+
let remaining = u32::try_from(remaining).unwrap_or(u32::MAX);
73+
74+
// default: depth = 1.
75+
let depth = mode.receive_fifo_depth.max(1);
76+
let remaining = cmp::min(depth, remaining);
77+
duration_per_byte_estimate(mode) * remaining
78+
}
1679

1780
/// Serial IO [`Protocol`]. Provides access to a serial I/O device.
1881
///
@@ -163,6 +226,67 @@ impl Serial {
163226
)
164227
}
165228

229+
/// Reads bytes into the provided buffer, blocking until it is full.
230+
///
231+
/// Retries automatically on [`Status::TIMEOUT`], stalling briefly between
232+
/// attempts based on the current baud rate. A maximum retry limit prevents
233+
/// spinning forever in the unlikely event of a hardware fault.
234+
///
235+
/// # Arguments
236+
///
237+
/// - `buffer`: buffer to fill
238+
///
239+
/// # Tips
240+
///
241+
/// Consider setting non-default properties via [`Self::set_attributes`]
242+
/// and [`Self::set_control_bits`] matching your use-case. For more info,
243+
/// please read the general [documentation](Self) of the protocol.
244+
///
245+
/// # Errors
246+
///
247+
/// - [`Status::DEVICE_ERROR`]: serial device reported an error
248+
/// - [`Status::TIMEOUT`]: This timeout happens if the underlying device
249+
/// seem to stopped its normal operation and is only reported to prevent
250+
/// an endless loop.
251+
pub fn read_exact(&mut self, buffer: &mut [u8]) -> Result<()> {
252+
// Chosen at will, tested on real hardware.
253+
const MAX_ZERO_PROGRESS: usize = 16;
254+
255+
let mut remaining_buffer = buffer;
256+
let mut zero_progress_count = 0;
257+
258+
// Retry until all bytes were written with endless loop protection.
259+
while !remaining_buffer.is_empty() {
260+
match self.read(remaining_buffer) {
261+
// All data read, buffer is full.
262+
Ok(_) => return Ok(()),
263+
Err(err) if err.status() == Status::TIMEOUT => {
264+
let n = *err.data();
265+
if n == 0 {
266+
zero_progress_count += 1;
267+
if zero_progress_count >= MAX_ZERO_PROGRESS {
268+
return Err(Error::from(Status::TIMEOUT));
269+
}
270+
} else {
271+
zero_progress_count = 0;
272+
}
273+
274+
remaining_buffer = &mut remaining_buffer[n..];
275+
276+
// Give FIFO time to fill up. Without that protection, we
277+
// might get TIMEOUT too often and return too early.
278+
let fifo_stall_duration =
279+
duration_fifo_estimate(self.io_mode(), remaining_buffer.len());
280+
boot::stall(fifo_stall_duration);
281+
}
282+
err => {
283+
return Err(Error::from(err.status()));
284+
}
285+
}
286+
}
287+
Ok(())
288+
}
289+
166290
/// Writes data to this device. This function has the raw semantics of the
167291
/// underlying UEFI protocol.
168292
///
@@ -195,6 +319,65 @@ impl Serial {
195319
)
196320
}
197321

322+
/// Writes all provided bytes, blocking until every byte has been sent.
323+
///
324+
/// Retries automatically on [`Status::TIMEOUT`], stalling briefly between
325+
/// attempts based on the current baud rate. A maximum retry limit prevents
326+
/// spinning forever in the unlikely event of a hardware fault.
327+
///
328+
/// # Arguments
329+
///
330+
/// - `data`: bytes to write
331+
///
332+
/// # Tips
333+
///
334+
/// Consider setting non-default properties via [`Self::set_attributes`]
335+
/// and [`Self::set_control_bits`] matching your use-case. For more info,
336+
/// please read the general [documentation](Self) of the protocol.
337+
///
338+
/// # Errors
339+
///
340+
/// - [`Status::DEVICE_ERROR`]: serial device reported an error
341+
/// - [`Status::TIMEOUT`]: This timeout happens if the underlying device
342+
/// seem to stopped its normal operation and is only reported to prevent
343+
/// an endless loop.
344+
pub fn write_exact(&mut self, data: &[u8]) -> Result<()> {
345+
// Chosen at will, tested on real hardware.
346+
const MAX_ZERO_PROGRESS: usize = 16;
347+
348+
let mut remaining_bytes = data;
349+
let mut zero_progress_count = 0;
350+
351+
// Retry until all bytes were written with endless loop protection.
352+
while !remaining_bytes.is_empty() {
353+
match self.write(remaining_bytes) {
354+
// All data written, no data left to send.
355+
Ok(_) => return Ok(()),
356+
Err(err) if err.status() == Status::TIMEOUT => {
357+
let n = *err.data();
358+
if n == 0 {
359+
zero_progress_count += 1;
360+
if zero_progress_count >= MAX_ZERO_PROGRESS {
361+
return Err(Error::from(Status::TIMEOUT));
362+
}
363+
} else {
364+
zero_progress_count = 0;
365+
}
366+
367+
remaining_bytes = &remaining_bytes[n..];
368+
369+
// Give FIFO time to drain. Without that protection, we
370+
// might get TIMEOUT too often and return too early.
371+
let fifo_stall_duration =
372+
duration_fifo_estimate(self.io_mode(), remaining_bytes.len());
373+
boot::stall(fifo_stall_duration);
374+
}
375+
Err(err) => return Err(Error::from(err.status())),
376+
}
377+
}
378+
Ok(())
379+
}
380+
198381
/// Pointer to a GUID identifying the device connected to the serial port.
199382
///
200383
/// This is either `Ok` if [`Self::revision`] is at least
@@ -238,6 +421,116 @@ impl Serial {
238421

239422
impl fmt::Write for Serial {
240423
fn write_str(&mut self, s: &str) -> fmt::Result {
241-
self.write(s.as_bytes()).map(|_| ()).map_err(|_| fmt::Error)
424+
// We retry on Status::TIMEOUT but propagate other errors
425+
self.write_exact(s.as_bytes()).map_err(|e| {
426+
let msg = "failed to write to serial device";
427+
// Simple check to prevent endless recursion if a logger
428+
// implementation uses the serial protocol
429+
if !s.contains(msg) {
430+
error!("{msg}: {e}");
431+
}
432+
fmt::Error
433+
})
434+
}
435+
}
436+
437+
#[cfg(test)]
438+
mod tests {
439+
use super::*;
440+
441+
fn make_mode(baud_rate: u64, data_bits: u32, parity: Parity, stop_bits: StopBits) -> IoMode {
442+
IoMode {
443+
control_mask: ControlBits::empty(),
444+
timeout: 0,
445+
baud_rate,
446+
receive_fifo_depth: 0,
447+
data_bits,
448+
parity,
449+
stop_bits,
450+
}
451+
}
452+
453+
#[test]
454+
fn unknown_baud_rate_returns_large_fallback() {
455+
let mode = make_mode(0, 8, Parity::NONE, StopBits::ONE);
456+
let duration = duration_per_byte_estimate(&mode);
457+
assert!(
458+
duration >= Duration::from_millis(50),
459+
"fallback should be at least 100ms, got {duration:?}"
460+
);
461+
}
462+
463+
#[test]
464+
fn higher_baud_rate_gives_shorter_duration() {
465+
let slow = make_mode(9_600, 8, Parity::NONE, StopBits::ONE);
466+
let fast = make_mode(115_200, 8, Parity::NONE, StopBits::ONE);
467+
assert!(
468+
duration_per_byte_estimate(&slow) > duration_per_byte_estimate(&fast),
469+
"9600 baud should take longer per byte than 115200 baud"
470+
);
471+
}
472+
473+
#[test]
474+
fn parity_bit_increases_duration() {
475+
let no_parity = make_mode(9_600, 8, Parity::NONE, StopBits::ONE);
476+
let with_parity = make_mode(9_600, 8, Parity::EVEN, StopBits::ONE);
477+
assert!(
478+
duration_per_byte_estimate(&with_parity) > duration_per_byte_estimate(&no_parity),
479+
"a parity bit should increase the estimated duration"
480+
);
481+
}
482+
483+
#[test]
484+
fn two_stop_bits_increases_duration() {
485+
let one_stop = make_mode(9_600, 8, Parity::NONE, StopBits::ONE);
486+
let two_stop = make_mode(9_600, 8, Parity::NONE, StopBits::TWO);
487+
assert!(
488+
duration_per_byte_estimate(&two_stop) > duration_per_byte_estimate(&one_stop),
489+
"two stop bits should increase the estimated duration"
490+
);
491+
}
492+
493+
#[test]
494+
fn one_five_stop_bits_same_as_two() {
495+
// ONE_FIVE is rounded up conservatively to 2, same as TWO.
496+
let one_five = make_mode(9_600, 8, Parity::NONE, StopBits::ONE_FIVE);
497+
let two = make_mode(9_600, 8, Parity::NONE, StopBits::TWO);
498+
assert_eq!(
499+
duration_per_byte_estimate(&one_five),
500+
duration_per_byte_estimate(&two),
501+
"ONE_FIVE should be treated as 2 stop bits"
502+
);
503+
}
504+
505+
#[test]
506+
fn more_data_bits_increases_duration() {
507+
let seven = make_mode(9_600, 7, Parity::NONE, StopBits::ONE);
508+
let eight = make_mode(9_600, 8, Parity::NONE, StopBits::ONE);
509+
assert!(
510+
duration_per_byte_estimate(&eight) > duration_per_byte_estimate(&seven),
511+
"more data bits should increase the estimated duration"
512+
);
513+
}
514+
515+
#[test]
516+
fn default_stop_bits_conservative() {
517+
// DEFAULT is unknown, so it should be treated at least as generously as TWO.
518+
let default_stop = make_mode(9_600, 8, Parity::NONE, StopBits::DEFAULT);
519+
let two_stop = make_mode(9_600, 8, Parity::NONE, StopBits::TWO);
520+
assert!(
521+
duration_per_byte_estimate(&default_stop) >= duration_per_byte_estimate(&two_stop),
522+
"DEFAULT stop bits should be at least as conservative as TWO"
523+
);
524+
}
525+
526+
#[test]
527+
fn default_parity_conservative() {
528+
// DEFAULT parity is unknown, so assume a parity bit may be present.
529+
let default_parity = make_mode(9_600, 8, Parity::DEFAULT, StopBits::ONE);
530+
let no_parity = make_mode(9_600, 8, Parity::NONE, StopBits::ONE);
531+
assert!(
532+
duration_per_byte_estimate(&default_parity) >= duration_per_byte_estimate(&no_parity),
533+
"DEFAULT parity should be at least as conservative as a known parity bit"
534+
);
242535
}
243536
}

0 commit comments

Comments
 (0)