From b3bd57e7f7f3c35106d2820e0f816c05b590c8b1 Mon Sep 17 00:00:00 2001 From: Kurtis Dinelle Date: Mon, 12 Jan 2026 14:54:38 -0800 Subject: [PATCH] Add uart-service --- Cargo.lock | 19 +++++ Cargo.toml | 1 + uart-service/Cargo.toml | 45 ++++++++++ uart-service/src/lib.rs | 173 +++++++++++++++++++++++++++++++++++++++ uart-service/src/mctp.rs | 12 +++ uart-service/src/task.rs | 31 +++++++ 6 files changed, 281 insertions(+) create mode 100644 uart-service/Cargo.toml create mode 100644 uart-service/src/lib.rs create mode 100644 uart-service/src/mctp.rs create mode 100644 uart-service/src/task.rs diff --git a/Cargo.lock b/Cargo.lock index a48728f1..ee0606d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2240,6 +2240,25 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uart-service" +version = "0.1.0" +dependencies = [ + "battery-service-messages", + "bitfield 0.17.0", + "debug-service-messages", + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-io-async", + "embedded-services", + "log", + "mctp-rs", + "num_enum", + "thermal-service-messages", +] + [[package]] name = "ufmt-write" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3f7b0142..c7107009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "cfu-service", "embedded-service", "espi-service", + "uart-service", "hid-service", "partition-manager/generation", "partition-manager/macros", diff --git a/uart-service/Cargo.toml b/uart-service/Cargo.toml new file mode 100644 index 00000000..f90ff1e8 --- /dev/null +++ b/uart-service/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "uart-service" +version = "0.1.0" +edition = "2024" +description = "UART embedded service implementation" +repository = "https://github.com/OpenDevicePartnership/embedded-services" +rust-version = "1.88" +license = "MIT" + +[lints] +workspace = true + +[dependencies] +bitfield.workspace = true +embedded-services.workspace = true +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embassy-time.workspace = true +embassy-sync.workspace = true +embassy-futures.workspace = true +mctp-rs = { workspace = true } +embedded-io-async.workspace = true +num_enum.workspace = true + +# TODO Service message type crates are a temporary dependency until we can parameterize +# the supported messages types at UART service creation time. +battery-service-messages.workspace = true +debug-service-messages.workspace = true +thermal-service-messages.workspace = true + +[features] +default = [] +defmt = [ + "dep:defmt", + "embedded-services/defmt", + "embassy-time/defmt", + "embassy-time/defmt-timestamp-uptime", + "embassy-sync/defmt", + "mctp-rs/defmt", + "thermal-service-messages/defmt", + "battery-service-messages/defmt", + "debug-service-messages/defmt", +] + +log = ["dep:log", "embedded-services/log", "embassy-time/log"] diff --git a/uart-service/src/lib.rs b/uart-service/src/lib.rs new file mode 100644 index 00000000..afac706f --- /dev/null +++ b/uart-service/src/lib.rs @@ -0,0 +1,173 @@ +//! uart-service +//! +//! To keep things consistent with eSPI service, this also uses the `SmbusEspiMedium` (though not +//! strictly necessary, this helps minimize code changes on the host side when swicthing between +//! eSPI or UART). +//! +//! Revisit: Will also need to consider how to handle notifications (likely need to have user +//! provide GPIO pin we can use). +#![no_std] + +mod mctp; +pub mod task; + +use crate::mctp::{HostRequest, HostResult, OdpHeader, OdpMessageType, OdpService}; +use core::borrow::BorrowMut; +use embassy_sync::channel::Channel; +use embedded_io_async::Read as UartRead; +use embedded_io_async::Write as UartWrite; +use embedded_services::GlobalRawMutex; +use embedded_services::buffer::OwnedRef; +use embedded_services::comms::{self, Endpoint, EndpointID, External}; +use embedded_services::trace; +use mctp_rs::smbus_espi::SmbusEspiMedium; +use mctp_rs::smbus_espi::SmbusEspiReplyContext; + +// Should be as large as the largest possible MCTP packet and its metadata. +const BUF_SIZE: usize = 256; +const HOST_TX_QUEUE_SIZE: usize = 5; +const SMBUS_HEADER_SIZE: usize = 4; +const SMBUS_LEN_IDX: usize = 2; + +embedded_services::define_static_buffer!(assembly_buf, u8, [0u8; BUF_SIZE]); + +#[derive(Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) struct HostResponseMessage { + pub source_endpoint: EndpointID, + pub message: HostResult, +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Comms error. + Comms, + /// UART error. + Uart, + /// MCTP serialization error. + Mctp(mctp_rs::MctpPacketError), + /// Other serialization error. + Serialize(&'static str), + /// Index/slice error. + IndexSlice, + /// Buffer error. + Buffer(embedded_services::buffer::Error), +} + +pub struct Service<'a> { + endpoint: Endpoint, + host_tx_queue: Channel, + assembly_buf_owned_ref: OwnedRef<'a, u8>, +} + +impl Service<'_> { + pub fn new() -> Result { + Ok(Self { + endpoint: Endpoint::uninit(EndpointID::External(External::Host)), + host_tx_queue: Channel::new(), + assembly_buf_owned_ref: assembly_buf::get_mut() + .ok_or(Error::Buffer(embedded_services::buffer::Error::InvalidRange))?, + }) + } + + async fn process_response(&self, uart: &mut T, response: &HostResponseMessage) -> Result<(), Error> { + let mut assembly_buf_access = self.assembly_buf_owned_ref.borrow_mut().map_err(Error::Buffer)?; + let pkt_ctx_buf = assembly_buf_access.borrow_mut(); + let mut mctp_ctx = mctp_rs::MctpPacketContext::new(SmbusEspiMedium, pkt_ctx_buf); + + let source_service: OdpService = OdpService::try_from(response.source_endpoint).map_err(|_| Error::Comms)?; + + let reply_context: mctp_rs::MctpReplyContext = mctp_rs::MctpReplyContext { + source_endpoint_id: mctp_rs::EndpointId::Id(0x80), + destination_endpoint_id: mctp_rs::EndpointId::Id(source_service.into()), + packet_sequence_number: mctp_rs::MctpSequenceNumber::new(0), + message_tag: mctp_rs::MctpMessageTag::try_from(3).map_err(Error::Serialize)?, + medium_context: SmbusEspiReplyContext { + destination_slave_address: 1, + source_slave_address: 0, + }, // Medium-specific context + }; + + let header = OdpHeader { + message_type: OdpMessageType::Result { + is_error: !response.message.is_ok(), + }, + is_datagram: false, + service: source_service, + message_id: response.message.discriminant(), + }; + + let mut packet_state = mctp_ctx + .serialize_packet(reply_context, (header, response.message.clone())) + .map_err(Error::Mctp)?; + + while let Some(packet_result) = packet_state.next() { + let packet = packet_result.map_err(Error::Mctp)?; + // Last byte is PEC, ignore for now + let packet = packet.get(..packet.len() - 1).ok_or(Error::IndexSlice)?; + + // Then actually send the response packet (which includes 4-byte SMBUS header containing payload size) + uart.write_all(packet).await.map_err(|_| Error::Uart)?; + } + + Ok(()) + } + + async fn wait_for_request(&self, uart: &mut T) -> Result<(), Error> { + let mut assembly_access = self.assembly_buf_owned_ref.borrow_mut().map_err(Error::Buffer)?; + let mut mctp_ctx = + mctp_rs::MctpPacketContext::::new(SmbusEspiMedium, assembly_access.borrow_mut()); + + // First wait for SMBUS header, which tells us how big the incoming packet is + let mut buf = [0; BUF_SIZE]; + uart.read_exact(buf.get_mut(..SMBUS_HEADER_SIZE).ok_or(Error::IndexSlice)?) + .await + .map_err(|_| Error::Uart)?; + + // Then wait until we've received the full payload + let len = *buf.get(SMBUS_LEN_IDX).ok_or(Error::IndexSlice)? as usize; + uart.read_exact( + buf.get_mut(SMBUS_HEADER_SIZE..SMBUS_HEADER_SIZE + len) + .ok_or(Error::IndexSlice)?, + ) + .await + .map_err(|_| Error::Uart)?; + + let message = mctp_ctx + .deserialize_packet(&buf) + .map_err(Error::Mctp)? + .ok_or(Error::Serialize("Partial message not supported"))?; + + let (header, host_request) = message.parse_as::().map_err(Error::Mctp)?; + let target_endpoint: EndpointID = header.service.get_endpoint_id(); + trace!( + "Host Request: Service {:?}, Command {:?}", + target_endpoint, header.message_id, + ); + + host_request + .send_to_endpoint(&self.endpoint, target_endpoint) + .await + .map_err(|_| Error::Comms)?; + + Ok(()) + } + + async fn wait_for_response(&self) -> HostResponseMessage { + self.host_tx_queue.receive().await + } +} + +impl comms::MailboxDelegate for Service<'_> { + fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { + crate::mctp::send_to_comms(message, |source_endpoint, message| { + self.host_tx_queue + .try_send(HostResponseMessage { + source_endpoint, + message, + }) + .map_err(|_| comms::MailboxDelegateError::BufferFull) + }) + } +} diff --git a/uart-service/src/mctp.rs b/uart-service/src/mctp.rs new file mode 100644 index 00000000..3c9fe10c --- /dev/null +++ b/uart-service/src/mctp.rs @@ -0,0 +1,12 @@ +use embedded_services::{ + comms, + relay::{SerializableMessage, SerializableResult, mctp::impl_odp_mctp_relay_types}, +}; + +// TODO We'd ideally like these types to be passed in as a generic or something when the UART service is instantiated +// so the UART service can be extended to handle 3rd party message types without needing to fork the UART service +impl_odp_mctp_relay_types!( + Battery, 0x08, (comms::EndpointID::Internal(comms::Internal::Battery)), battery_service_messages::AcpiBatteryRequest, battery_service_messages::AcpiBatteryResult; + Thermal, 0x09, (comms::EndpointID::Internal(comms::Internal::Thermal)), thermal_service_messages::ThermalRequest, thermal_service_messages::ThermalResult; + Debug, 0x0A, (comms::EndpointID::Internal(comms::Internal::Debug) ), debug_service_messages::DebugRequest, debug_service_messages::DebugResult; +); diff --git a/uart-service/src/task.rs b/uart-service/src/task.rs new file mode 100644 index 00000000..d77e4fcc --- /dev/null +++ b/uart-service/src/task.rs @@ -0,0 +1,31 @@ +use crate::{Error, Service}; +use embedded_io_async::Read as UartRead; +use embedded_io_async::Write as UartWrite; +use embedded_services::comms; +use embedded_services::error; + +pub async fn uart_service( + uart_service: &'static Service<'_>, + mut uart: T, +) -> Result { + // Register uart-service as the host service + comms::register_endpoint(uart_service, &uart_service.endpoint) + .await + .map_err(|_| Error::Comms)?; + + // Note: eSPI service uses `select!` to seemingly allow asyncrhonous `responses` from services, + // but there are concerns around async cancellation here at least for UART service. + // + // Thus this assumes services will only send messages in response to requests from the host, + // so we handle this in order. + loop { + if let Err(e) = uart_service.wait_for_request(&mut uart).await { + error!("uart-service request error: {:?}", e); + } else { + let host_msg = uart_service.wait_for_response().await; + if let Err(e) = uart_service.process_response(&mut uart, &host_msg).await { + error!("uart-service response error: {:?}", e) + } + } + } +}