Re: [PATCH v2 7/7] greybus: Add Rust UART node driver
From: Markus Probst
Date: Thu Sep 03 2026 - 16:51:41 EST
On Thu, 2026-08-27 at 13:24 +0530, Ayush Singh wrote:
> Add a driver for Greybus nodes attached over a plain serial port. The
> node is registered with the software SVC (gb-softsvc), which handles the
> SVC protocol on behalf of the AP, so no dedicated coprocessor running
> SVC firmware is needed.
>
> Greybus messages are carried over HDLC framing on the wire. Each frame
> carries a one-byte address (0x01 for Greybus) and control byte, followed
> by the 16-bit CPort ID and the Greybus message itself.
>
> Port parameters are taken from the firmware node: "baudrate" if
> present, otherwise 115200, with flow control and parity disabled.
>
> Since gb-uart-node imports types from gb-softsvc, Rust to Rust calling
> setup from nova-core [0] is being used.
>
> [0]: https://lore.kernel.org/all/20260622-nova-exports-v5-0-6191773fc977@xxxxxxxxxx/
>
> Signed-off-by: Ayush Singh <ayush@xxxxxxxxxxxxxxx>
> ---
> MAINTAINERS | 1 +
> drivers/greybus/.gitignore | 1 +
> drivers/greybus/Kconfig | 15 +++
> drivers/greybus/Makefile | 48 ++++++++
> drivers/greybus/gb_uart_node.rs | 245 ++++++++++++++++++++++++++++++++++++++++
> 5 files changed, 310 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index d047090be5f4..49c6dac72748 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -11339,6 +11339,7 @@ M: Ayush Singh <ayush@xxxxxxxxxxxxxxx>
> L: greybus-dev@xxxxxxxxxxxxxxxx (moderated for non-subscribers)
> S: Maintained
> F: Documentation/devicetree/bindings/beagle/beagle,beagleconnect-freedom.yaml
> +F: drivers/greybus/gb_uart_node.rs
>
> GREYBUS SUBSYSTEM
> M: Johan Hovold <johan@xxxxxxxxxx>
> diff --git a/drivers/greybus/.gitignore b/drivers/greybus/.gitignore
> new file mode 100644
> index 000000000000..ff9c4a3539b4
> --- /dev/null
> +++ b/drivers/greybus/.gitignore
> @@ -0,0 +1 @@
> +exports_gb_softsvc_generated.h
> diff --git a/drivers/greybus/Kconfig b/drivers/greybus/Kconfig
> index 381d1a6ee135..34de913af287 100644
> --- a/drivers/greybus/Kconfig
> +++ b/drivers/greybus/Kconfig
> @@ -60,5 +60,20 @@ config GREYBUS_SOFTSVC
> To compile this code as a module, choose M here: the module
> will be called gb-softsvc.ko
>
> +config GREYBUS_UART_NODE
> + tristate "Greybus UART node transport"
> + depends on RUST
> + depends on GREYBUS_SOFTSVC
> + depends on RUST_SERIAL_DEV_BUS_ABSTRACTIONS
> + select RUST_CRC_CCITT_ABSTRACTIONS
> + help
> + Select this option if you have a Greybus node connected over a
> + serial port. The node is registered with the software SVC, which
> + handles the SVC protocol on behalf of the AP, so no dedicated
> + coprocessor running SVC firmware is required.
> +
> + To compile this code as a module, choose M here: the module
> + will be called gb-uart-node.ko
> +
> endif # GREYBUS
>
> diff --git a/drivers/greybus/Makefile b/drivers/greybus/Makefile
> index e6f594128802..81151963c01e 100644
> --- a/drivers/greybus/Makefile
> +++ b/drivers/greybus/Makefile
> @@ -28,3 +28,51 @@ obj-$(CONFIG_GREYBUS_ES2) += gb-es2.o
> obj-$(CONFIG_GREYBUS_SOFTSVC) += gb-softsvc.o
> gb-softsvc-y += gb_softsvc.o gb_softsvc_exports.o
>
> +obj-$(CONFIG_GREYBUS_UART_NODE) += gb-uart-node.o
> +gb-uart-node-y += gb_uart_node.o
> +
> +# Export Rust symbols from gb-softsvc only if gb-uart-node actually references them.
> +gb-softsvc-export-deps := $(if $(CONFIG_GREYBUS_UART_NODE),$(obj)/gb_uart_node.o)
> +
> +rust_needed_exports = \
> + { $(if $(strip $(2)),$(NM) -u $(2);,) echo "__DEFINED_RUST_SYMBOLS__"; \
> + $(NM) -p --defined-only $(1); } | \
> + awk -v fmt='$(3)' ' \
> + /^__DEFINED_RUST_SYMBOLS__$$/ { defs = 1; next } \
> + !defs { if ($$NF ~ /^_R/) needed[$$NF] = 1; next } \
> + defs && $$2 ~ /(T|R|D|B)/ && $$3 ~ /^_R/ && \
> + $$3 !~ /_(init|cleanup)_module$$/ && \
> + $$3 !~ /__(pfx|cfi|odr_asan)/ && \
> + $$3 in needed { printf fmt, $$3 } \
> + '
> +
> +quiet_cmd_exports = EXPORTS $@
> + cmd_exports = \
> + $(call rust_needed_exports,$<,$(gb-softsvc-export-deps),EXPORT_SYMBOL_RUST_GPL(%s);\n) > $@
> +
> +$(obj)/exports_gb_softsvc_generated.h: $(obj)/gb_softsvc.o $(gb-softsvc-export-deps) FORCE
> + $(call if_changed,exports)
> +
> +targets += exports_gb_softsvc_generated.h
> +
> +$(obj)/gb_softsvc_exports.o: $(obj)/exports_gb_softsvc_generated.h
> +CFLAGS_gb_softsvc_exports.o := -I $(objtree)/$(obj)
> +
> +ifdef CONFIG_MODVERSIONS
> +# The C export shim declares Rust symbols as `extern int`, so reuse its export
> +# list but generate symbol CRCs from the Rust object instead of the shim's DWARF.
> +$(obj)/gb_softsvc_exports.o: private cmd_gensymtypes_c = \
> + $(call getexportsymbols,\1) | \
> + $(objtree)/scripts/gendwarfksyms/gendwarfksyms \
> + $(if $(KBUILD_GENDWARFKSYMS_STABLE), --stable) \
> + $(if $(KBUILD_SYMTYPES), --symtypes $(@:.o=.symtypes),) \
> + $(obj)/gb_softsvc.o
> +endif
> +
> +# Output nova-core's crate metadata for use by nova-drm at compile time.
> +RUSTFLAGS_gb_softsvc.o += \
> + --emit=metadata=$(objtree)/$(obj)/libgb_softsvc.rmeta
> +
> +# Allow nova-drm to import nova-core's types.
> +$(obj)/gb_uart_node.o: $(obj)/gb_softsvc.o
> +RUSTFLAGS_gb_uart_node.o := -L $(objtree)/$(obj) --extern gb_softsvc
> diff --git a/drivers/greybus/gb_uart_node.rs b/drivers/greybus/gb_uart_node.rs
> new file mode 100644
> index 000000000000..3eb4f8ab3655
> --- /dev/null
> +++ b/drivers/greybus/gb_uart_node.rs
> @@ -0,0 +1,245 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +//! Greybus UART Node driver
> +
> +use kernel::{
> + alloc::Flags,
> + crc_ccitt::crc_ccitt,
> + device::{
> + AsBusDevice,
> + Bound,
> + Core, //
> + },
> + error::code,
> + new_spinlock, of,
> + prelude::*,
> + serdev,
> + sync::{
> + aref::ARef,
> + Arc,
> + SpinLock, //
> + },
> +};
> +
> +use zerocopy::little_endian;
> +use zerocopy_derive::{FromBytes, Immutable, KnownLayout};
> +
> +const HDLC_MAX_FRAME_LEN: usize = 256;
> +
> +const HDLC_FRAME: u8 = 0x7E;
> +const HDLC_ESC: u8 = 0x7D;
> +const HDLC_XOR: u8 = 0x20;
> +const HDLC_EXPECTED_CRC: u16 = 0xf0b8;
> +
> +const ADDRESS_GREYBUS: u8 = 0x01;
> +
> +#[repr(C, packed)]
> +#[derive(FromBytes, Immutable, KnownLayout)]
> +struct GreybusFrame {
> + cport: little_endian::U16,
> + msg: [u8],
> +}
> +
> +struct HdlcRx {
> + rx_buf: KVec<u8>,
> + rx_in_esc: bool,
> + sdev: ARef<serdev::Device>,
> + node: gb_softsvc::Module,
> +}
> +
> +impl HdlcRx {
> + fn new(sdev: ARef<serdev::Device>, node: gb_softsvc::Module) -> Result<Self> {
> + Ok(Self {
> + node,
> + sdev,
> + rx_buf: KVec::with_capacity(HDLC_MAX_FRAME_LEN, GFP_KERNEL)?,
> + rx_in_esc: false,
> + })
> + }
> +
> + fn frame_finish(&self) -> Result<()> {
> + if self.rx_buf.len() < 4 {
> + return Err(code::EFAULT);
> + }
> +
> + let crc = crc_ccitt(0xffff, &self.rx_buf);
> + if crc != HDLC_EXPECTED_CRC {
> + dev_warn!(self.sdev.as_ref(), "CRC failed {}", crc);
> + return Ok(());
> + }
> +
> + let addr = self.rx_buf[0];
> + let _ctrl = self.rx_buf[1];
> + let payload = &self.rx_buf[2..self.rx_buf.len() - size_of::<u16>()];
> +
> + match addr {
> + ADDRESS_GREYBUS => {
> + let frame = GreybusFrame::ref_from_bytes(payload).map_err(|_| code::EINVAL)?;
> + self.node.submit_message(0, frame.cport.into(), &frame.msg)
> + }
> + _ => Err(code::EINVAL),
> + }
> + }
> +
> + fn rx(&mut self, data: &[u8]) -> usize {
> + for i in data.iter() {
> + match *i {
> + HDLC_FRAME => {
> + if !self.rx_buf.is_empty() {
> + if let Err(e) = self.frame_finish() {
> + dev_warn!(self.sdev.as_ref(), "bad frame: {e:?}\n");
> + }
> + }
> +
> + self.rx_buf.clear();
> + self.rx_in_esc = false;
> + }
> + HDLC_ESC => self.rx_in_esc = true,
> + _ => {
> + let c = if self.rx_in_esc { *i ^ HDLC_XOR } else { *i };
> + self.rx_in_esc = false;
> +
> + if self.rx_buf.push_within_capacity(c).is_err() {
> + dev_warn!(self.sdev.as_ref(), "buffer overflow. Dropping frame");
> +
> + self.rx_buf.clear();
> + self.rx_in_esc = false;
> + }
> + }
> + }
> + }
> +
> + data.len()
> + }
> +}
> +
> +struct GbNode {
> + sdev: ARef<serdev::Device>,
> +}
> +
> +impl GbNode {
> + const fn new(sdev: ARef<serdev::Device>) -> Self {
> + Self { sdev }
> + }
> +
> + fn fill_buf(mut crc: u16, data: &[u8], buf: &mut KVec<u8>) -> Result<u16> {
> + for i in data {
> + crc = crc_ccitt(crc, &[*i]);
> + if *i == HDLC_ESC || *i == HDLC_FRAME {
> + buf.push_within_capacity(HDLC_ESC)?;
> + buf.push_within_capacity(i ^ HDLC_XOR)?;
> + } else {
> + buf.push_within_capacity(*i)?;
> + }
> + }
> +
> + Ok(crc)
> + }
> +}
> +
> +impl gb_softsvc::InterfaceOps for GbNode {
> + fn write(&self, data: &[u8], cport: u16, gfp_mask: Flags) -> Result<()> {
> + // SAFETY: `GbNode` only exists while its serdev driver is bound, so the device is in the
> + // `Bound` state for the duration of this call.
> + let bound: &serdev::Device<Bound> =
> + unsafe { serdev::Device::from_device(self.sdev.as_ref().as_bound()) };
> +
> + let mut buf = KVec::with_capacity(HDLC_MAX_FRAME_LEN, gfp_mask)?;
> +
> + let mut crc = 0xffff;
> +
> + buf.push_within_capacity(HDLC_FRAME)?;
> +
> + crc = Self::fill_buf(crc, &[ADDRESS_GREYBUS, 0x03], &mut buf)?;
> + crc = Self::fill_buf(crc, &cport.to_le_bytes(), &mut buf)?;
> + crc = Self::fill_buf(crc, data, &mut buf)?;
> +
> + crc ^= 0xffff;
> + Self::fill_buf(crc, &crc.to_le_bytes(), &mut buf)?;
> +
> + buf.push_within_capacity(HDLC_FRAME)?;
> +
> + bound.write_all(&buf, 0)?;
> +
> + Ok(())
> + }
> +}
> +
> +#[pin_data]
> +struct GbUartNode {
> + #[pin]
> + rx: SpinLock<Option<HdlcRx>>,
(add me to CC please)
Instead of using a lock here, it might be a better idea to
synchronize/stop the receive callback before unbind is called in the
serdev rust abstraction. This would allow the abstraction to provide
mutable references to the driver data in `receive` and `unbind`. It
would also remove the Sync requirement.
I will send a patch soon.
Thanks
- Markus Probst
> +}
> +
> +impl GbUartNode {
> + fn init(sdev: &serdev::Device<Core<'_>>) -> Result<HdlcRx> {
> + if sdev
> + .set_baudrate(
> + sdev.as_ref()
> + .fwnode()
> + .and_then(|fwnode| fwnode.property_read(c"baudrate").optional())
> + .unwrap_or(115200),
> + )
> + .is_err()
> + {
> + return Err(EINVAL);
> + }
> + sdev.set_flow_control(false);
> + sdev.set_parity(serdev::Parity::None)?;
> +
> + let node = gb_softsvc::Module::new(&[Arc::new(GbNode::new(sdev.into()), GFP_KERNEL)?])?;
> +
> + HdlcRx::new(sdev.into(), node)
> + }
> +}
> +
> +kernel::of_device_table!(
> + OF_TABLE,
> + <GbUartNode as serdev::Driver>::IdInfo,
> + [(of::DeviceId::new(c"beagle,beagleconnect-freedom"), ())]
> +);
> +
> +#[vtable]
> +impl serdev::Driver for GbUartNode {
> + type IdInfo = ();
> + type Data<'bound> = Self;
> + const OF_ID_TABLE: Option<of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
> +
> + fn probe<'bound>(
> + sdev: &'bound serdev::Device<Core<'_>>,
> + _info: Option<&'bound Self::IdInfo>,
> + ) -> impl PinInit<Self, Error> + 'bound {
> + dev_dbg!(sdev.as_ref(), "Probe gb_uart_node.\n");
> +
> + try_pin_init!(Self {
> + rx <- new_spinlock!(Some(Self::init(sdev)?), "gb_uart_node::rx"),
> + }? Error)
> + }
> +
> + fn receive<'bound>(
> + _sdev: &'bound serdev::Device<Bound>,
> + this: Pin<&Self>,
> + data: &[u8],
> + ) -> usize {
> + if let Some(mut guard) = this.rx.try_lock() {
> + if let Some(ref mut hdlc_rx) = *guard {
> + return hdlc_rx.rx(data);
> + }
> + }
> +
> + 0
> + }
> +
> + fn unbind<'bound>(_: &'bound serdev::Device<Core<'_>>, this: Pin<&Self::Data<'bound>>) {
> + // Getting a bound device is not possible after this point. So drop HdlcRx.
> + let _ = this.rx.lock().take();
> + }
> +}
> +
> +kernel::module_serdev_device_driver! {
> + type: GbUartNode,
> + name: "gb_uart_node",
> + authors: ["Ayush Singh <ayush@xxxxxxxxxxxxxxx>"],
> + description: "Greybus node connected over UART",
> + license: "GPL v2",
> +}
Attachment:
signature.asc
Description: This is a digitally signed message part