Skip to content

Commit 6615aed

Browse files
authored
Add uart-service (#674)
Adds a basic uart service which can act a host service when eSPI is not available. Still uses the `SmbusEspiMedium` even though the SMBUS header isn't necessary just to keep code changes on the host side minimal when switching between UART/ESPI. Did some testing on the IMXRT board and can send/receive MCTP packets from host over a COM port successfully. Might catch additional bugs as I work on getting the ratatui app working over UART and will fix them as they come. Resolves #605
1 parent 07d89d1 commit 6615aed

File tree

6 files changed

+281
-0
lines changed

6 files changed

+281
-0
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ members = [
88
"cfu-service",
99
"embedded-service",
1010
"espi-service",
11+
"uart-service",
1112
"hid-service",
1213
"partition-manager/generation",
1314
"partition-manager/macros",

uart-service/Cargo.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[package]
2+
name = "uart-service"
3+
version = "0.1.0"
4+
edition = "2024"
5+
description = "UART embedded service implementation"
6+
repository = "https://github.com/OpenDevicePartnership/embedded-services"
7+
rust-version = "1.88"
8+
license = "MIT"
9+
10+
[lints]
11+
workspace = true
12+
13+
[dependencies]
14+
bitfield.workspace = true
15+
embedded-services.workspace = true
16+
defmt = { workspace = true, optional = true }
17+
log = { workspace = true, optional = true }
18+
embassy-time.workspace = true
19+
embassy-sync.workspace = true
20+
embassy-futures.workspace = true
21+
mctp-rs = { workspace = true }
22+
embedded-io-async.workspace = true
23+
num_enum.workspace = true
24+
25+
# TODO Service message type crates are a temporary dependency until we can parameterize
26+
# the supported messages types at UART service creation time.
27+
battery-service-messages.workspace = true
28+
debug-service-messages.workspace = true
29+
thermal-service-messages.workspace = true
30+
31+
[features]
32+
default = []
33+
defmt = [
34+
"dep:defmt",
35+
"embedded-services/defmt",
36+
"embassy-time/defmt",
37+
"embassy-time/defmt-timestamp-uptime",
38+
"embassy-sync/defmt",
39+
"mctp-rs/defmt",
40+
"thermal-service-messages/defmt",
41+
"battery-service-messages/defmt",
42+
"debug-service-messages/defmt",
43+
]
44+
45+
log = ["dep:log", "embedded-services/log", "embassy-time/log"]

uart-service/src/lib.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! uart-service
2+
//!
3+
//! To keep things consistent with eSPI service, this also uses the `SmbusEspiMedium` (though not
4+
//! strictly necessary, this helps minimize code changes on the host side when swicthing between
5+
//! eSPI or UART).
6+
//!
7+
//! Revisit: Will also need to consider how to handle notifications (likely need to have user
8+
//! provide GPIO pin we can use).
9+
#![no_std]
10+
11+
mod mctp;
12+
pub mod task;
13+
14+
use crate::mctp::{HostRequest, HostResult, OdpHeader, OdpMessageType, OdpService};
15+
use core::borrow::BorrowMut;
16+
use embassy_sync::channel::Channel;
17+
use embedded_io_async::Read as UartRead;
18+
use embedded_io_async::Write as UartWrite;
19+
use embedded_services::GlobalRawMutex;
20+
use embedded_services::buffer::OwnedRef;
21+
use embedded_services::comms::{self, Endpoint, EndpointID, External};
22+
use embedded_services::trace;
23+
use mctp_rs::smbus_espi::SmbusEspiMedium;
24+
use mctp_rs::smbus_espi::SmbusEspiReplyContext;
25+
26+
// Should be as large as the largest possible MCTP packet and its metadata.
27+
const BUF_SIZE: usize = 256;
28+
const HOST_TX_QUEUE_SIZE: usize = 5;
29+
const SMBUS_HEADER_SIZE: usize = 4;
30+
const SMBUS_LEN_IDX: usize = 2;
31+
32+
embedded_services::define_static_buffer!(assembly_buf, u8, [0u8; BUF_SIZE]);
33+
34+
#[derive(Clone)]
35+
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
36+
pub(crate) struct HostResponseMessage {
37+
pub source_endpoint: EndpointID,
38+
pub message: HostResult,
39+
}
40+
41+
#[derive(Debug, Clone, Copy)]
42+
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
43+
pub enum Error {
44+
/// Comms error.
45+
Comms,
46+
/// UART error.
47+
Uart,
48+
/// MCTP serialization error.
49+
Mctp(mctp_rs::MctpPacketError<SmbusEspiMedium>),
50+
/// Other serialization error.
51+
Serialize(&'static str),
52+
/// Index/slice error.
53+
IndexSlice,
54+
/// Buffer error.
55+
Buffer(embedded_services::buffer::Error),
56+
}
57+
58+
pub struct Service<'a> {
59+
endpoint: Endpoint,
60+
host_tx_queue: Channel<GlobalRawMutex, HostResponseMessage, HOST_TX_QUEUE_SIZE>,
61+
assembly_buf_owned_ref: OwnedRef<'a, u8>,
62+
}
63+
64+
impl Service<'_> {
65+
pub fn new() -> Result<Self, Error> {
66+
Ok(Self {
67+
endpoint: Endpoint::uninit(EndpointID::External(External::Host)),
68+
host_tx_queue: Channel::new(),
69+
assembly_buf_owned_ref: assembly_buf::get_mut()
70+
.ok_or(Error::Buffer(embedded_services::buffer::Error::InvalidRange))?,
71+
})
72+
}
73+
74+
async fn process_response<T: UartWrite>(&self, uart: &mut T, response: &HostResponseMessage) -> Result<(), Error> {
75+
let mut assembly_buf_access = self.assembly_buf_owned_ref.borrow_mut().map_err(Error::Buffer)?;
76+
let pkt_ctx_buf = assembly_buf_access.borrow_mut();
77+
let mut mctp_ctx = mctp_rs::MctpPacketContext::new(SmbusEspiMedium, pkt_ctx_buf);
78+
79+
let source_service: OdpService = OdpService::try_from(response.source_endpoint).map_err(|_| Error::Comms)?;
80+
81+
let reply_context: mctp_rs::MctpReplyContext<SmbusEspiMedium> = mctp_rs::MctpReplyContext {
82+
source_endpoint_id: mctp_rs::EndpointId::Id(0x80),
83+
destination_endpoint_id: mctp_rs::EndpointId::Id(source_service.into()),
84+
packet_sequence_number: mctp_rs::MctpSequenceNumber::new(0),
85+
message_tag: mctp_rs::MctpMessageTag::try_from(3).map_err(Error::Serialize)?,
86+
medium_context: SmbusEspiReplyContext {
87+
destination_slave_address: 1,
88+
source_slave_address: 0,
89+
}, // Medium-specific context
90+
};
91+
92+
let header = OdpHeader {
93+
message_type: OdpMessageType::Result {
94+
is_error: !response.message.is_ok(),
95+
},
96+
is_datagram: false,
97+
service: source_service,
98+
message_id: response.message.discriminant(),
99+
};
100+
101+
let mut packet_state = mctp_ctx
102+
.serialize_packet(reply_context, (header, response.message.clone()))
103+
.map_err(Error::Mctp)?;
104+
105+
while let Some(packet_result) = packet_state.next() {
106+
let packet = packet_result.map_err(Error::Mctp)?;
107+
// Last byte is PEC, ignore for now
108+
let packet = packet.get(..packet.len() - 1).ok_or(Error::IndexSlice)?;
109+
110+
// Then actually send the response packet (which includes 4-byte SMBUS header containing payload size)
111+
uart.write_all(packet).await.map_err(|_| Error::Uart)?;
112+
}
113+
114+
Ok(())
115+
}
116+
117+
async fn wait_for_request<T: UartRead>(&self, uart: &mut T) -> Result<(), Error> {
118+
let mut assembly_access = self.assembly_buf_owned_ref.borrow_mut().map_err(Error::Buffer)?;
119+
let mut mctp_ctx =
120+
mctp_rs::MctpPacketContext::<SmbusEspiMedium>::new(SmbusEspiMedium, assembly_access.borrow_mut());
121+
122+
// First wait for SMBUS header, which tells us how big the incoming packet is
123+
let mut buf = [0; BUF_SIZE];
124+
uart.read_exact(buf.get_mut(..SMBUS_HEADER_SIZE).ok_or(Error::IndexSlice)?)
125+
.await
126+
.map_err(|_| Error::Uart)?;
127+
128+
// Then wait until we've received the full payload
129+
let len = *buf.get(SMBUS_LEN_IDX).ok_or(Error::IndexSlice)? as usize;
130+
uart.read_exact(
131+
buf.get_mut(SMBUS_HEADER_SIZE..SMBUS_HEADER_SIZE + len)
132+
.ok_or(Error::IndexSlice)?,
133+
)
134+
.await
135+
.map_err(|_| Error::Uart)?;
136+
137+
let message = mctp_ctx
138+
.deserialize_packet(&buf)
139+
.map_err(Error::Mctp)?
140+
.ok_or(Error::Serialize("Partial message not supported"))?;
141+
142+
let (header, host_request) = message.parse_as::<HostRequest>().map_err(Error::Mctp)?;
143+
let target_endpoint: EndpointID = header.service.get_endpoint_id();
144+
trace!(
145+
"Host Request: Service {:?}, Command {:?}",
146+
target_endpoint, header.message_id,
147+
);
148+
149+
host_request
150+
.send_to_endpoint(&self.endpoint, target_endpoint)
151+
.await
152+
.map_err(|_| Error::Comms)?;
153+
154+
Ok(())
155+
}
156+
157+
async fn wait_for_response(&self) -> HostResponseMessage {
158+
self.host_tx_queue.receive().await
159+
}
160+
}
161+
162+
impl comms::MailboxDelegate for Service<'_> {
163+
fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> {
164+
crate::mctp::send_to_comms(message, |source_endpoint, message| {
165+
self.host_tx_queue
166+
.try_send(HostResponseMessage {
167+
source_endpoint,
168+
message,
169+
})
170+
.map_err(|_| comms::MailboxDelegateError::BufferFull)
171+
})
172+
}
173+
}

uart-service/src/mctp.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use embedded_services::{
2+
comms,
3+
relay::{SerializableMessage, SerializableResult, mctp::impl_odp_mctp_relay_types},
4+
};
5+
6+
// TODO We'd ideally like these types to be passed in as a generic or something when the UART service is instantiated
7+
// so the UART service can be extended to handle 3rd party message types without needing to fork the UART service
8+
impl_odp_mctp_relay_types!(
9+
Battery, 0x08, (comms::EndpointID::Internal(comms::Internal::Battery)), battery_service_messages::AcpiBatteryRequest, battery_service_messages::AcpiBatteryResult;
10+
Thermal, 0x09, (comms::EndpointID::Internal(comms::Internal::Thermal)), thermal_service_messages::ThermalRequest, thermal_service_messages::ThermalResult;
11+
Debug, 0x0A, (comms::EndpointID::Internal(comms::Internal::Debug) ), debug_service_messages::DebugRequest, debug_service_messages::DebugResult;
12+
);

uart-service/src/task.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use crate::{Error, Service};
2+
use embedded_io_async::Read as UartRead;
3+
use embedded_io_async::Write as UartWrite;
4+
use embedded_services::comms;
5+
use embedded_services::error;
6+
7+
pub async fn uart_service<T: UartRead + UartWrite>(
8+
uart_service: &'static Service<'_>,
9+
mut uart: T,
10+
) -> Result<embedded_services::Never, Error> {
11+
// Register uart-service as the host service
12+
comms::register_endpoint(uart_service, &uart_service.endpoint)
13+
.await
14+
.map_err(|_| Error::Comms)?;
15+
16+
// Note: eSPI service uses `select!` to seemingly allow asyncrhonous `responses` from services,
17+
// but there are concerns around async cancellation here at least for UART service.
18+
//
19+
// Thus this assumes services will only send messages in response to requests from the host,
20+
// so we handle this in order.
21+
loop {
22+
if let Err(e) = uart_service.wait_for_request(&mut uart).await {
23+
error!("uart-service request error: {:?}", e);
24+
} else {
25+
let host_msg = uart_service.wait_for_response().await;
26+
if let Err(e) = uart_service.process_response(&mut uart, &host_msg).await {
27+
error!("uart-service response error: {:?}", e)
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)