Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"cfu-service",
"embedded-service",
"espi-service",
"uart-service",
"hid-service",
"partition-manager/generation",
"partition-manager/macros",
Expand Down
45 changes: 45 additions & 0 deletions uart-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
173 changes: 173 additions & 0 deletions uart-service/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<SmbusEspiMedium>),
/// 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<GlobalRawMutex, HostResponseMessage, HOST_TX_QUEUE_SIZE>,
assembly_buf_owned_ref: OwnedRef<'a, u8>,
}

impl Service<'_> {
pub fn new() -> Result<Self, Error> {
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<T: UartWrite>(&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<SmbusEspiMedium> = 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<T: UartRead>(&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::<SmbusEspiMedium>::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::<HostRequest>().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)
})
}
}
12 changes: 12 additions & 0 deletions uart-service/src/mctp.rs
Original file line number Diff line number Diff line change
@@ -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;
);
31 changes: 31 additions & 0 deletions uart-service/src/task.rs
Original file line number Diff line number Diff line change
@@ -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<T: UartRead + UartWrite>(
uart_service: &'static Service<'_>,
mut uart: T,
) -> Result<embedded_services::Never, Error> {
// 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)
}
}
}
}