Add derive macro for BuilderLite, add #[non_exhaustive] to some enums and structs (#2614)

* Add a derive procmacro to implement the Builder Lite pattern for a struct

* Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in I2C module

* Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in UART module

* Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in SPI module

* Update `CHANGELOG.md`

* Fix build errors in HIL tests

* Fix generated doc comments

* Return a `ParseError` rather than panicking

* Add a method to set the value to `None` for `Option` types
This commit is contained in:
Jesse Braham 2024-11-27 07:54:43 -08:00 committed by GitHub
parent aeda6ac00a
commit 1a2bee6f1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 235 additions and 132 deletions

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the `BuilderLite` derive macro which implements the Builder Lite pattern for a struct (#2614)
### Fixed
### Changed

View File

@ -52,7 +52,22 @@ use proc_macro::{Span, TokenStream};
use proc_macro2::Ident;
use proc_macro_crate::{crate_name, FoundCrate};
use proc_macro_error2::abort;
use syn::{parse, parse::Error as ParseError, spanned::Spanned, Item, ItemFn, ReturnType, Type};
use quote::{format_ident, quote};
use syn::{
parse,
parse::Error as ParseError,
spanned::Spanned,
Data,
DataStruct,
GenericArgument,
Item,
ItemFn,
Path,
PathArguments,
PathSegment,
ReturnType,
Type,
};
use self::interrupt::{check_attr_whitelist, WhiteListCaller};
@ -238,8 +253,8 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream {
/// esp_hal::interrupt::Priority::Priority2)]`.
///
/// If no priority is given, `Priority::min()` is assumed
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
#[proc_macro_error2::proc_macro_error]
pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream {
#[derive(Debug, FromMeta)]
struct MacroArgs {
@ -341,8 +356,8 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream {
/// Marks the entry function of a LP core / ULP program.
#[cfg(any(feature = "is-lp-core", feature = "is-ulp-core"))]
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
#[proc_macro_error2::proc_macro_error]
pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream {
lp_core::entry(args, input)
}
@ -381,3 +396,127 @@ pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
run(&args.meta, f, main()).unwrap_or_else(|x| x).into()
}
/// Automatically implement the [Builder Lite] pattern for a struct.
///
/// This will create an `impl` which contains methods for each field of a
/// struct, allowing users to easily set the values. The generated methods will
/// be the field name prefixed with `with_`, and calls to these methods can be
/// chained as needed.
///
/// ## Example
///
/// ```rust, no_run
/// #[derive(Default)]
/// enum MyEnum {
/// #[default]
/// A,
/// B,
/// }
///
/// #[derive(Default, BuilderLite)]
/// #[non_exhaustive]
/// struct MyStruct {
/// enum_field: MyEnum,
/// bool_field: bool,
/// option_field: Option<i32>,
/// }
///
/// MyStruct::default()
/// .with_enum_field(MyEnum::B)
/// .with_bool_field(true)
/// .with_option_field(-5);
/// ```
///
/// [Builder Lite]: https://matklad.github.io/2022/05/29/builder-lite.html
#[proc_macro_derive(BuilderLite)]
pub fn builder_lite_derive(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
let span = input.span();
let ident = input.ident;
let mut fns = Vec::new();
if let Data::Struct(DataStruct { fields, .. }) = &input.data {
for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let function_ident = format_ident!("with_{}", field_ident);
let maybe_path_type = extract_type_path(field_type)
.and_then(|path| extract_option_segment(path))
.and_then(|path_seg| match path_seg.arguments {
PathArguments::AngleBracketed(ref params) => params.args.first(),
_ => None,
})
.and_then(|generic_arg| match *generic_arg {
GenericArgument::Type(ref ty) => Some(ty),
_ => None,
});
let (field_type, field_assigns) = if let Some(inner_type) = maybe_path_type {
(inner_type, quote! { Some(#field_ident) })
} else {
(field_type, quote! { #field_ident })
};
fns.push(quote! {
#[doc = concat!(" Assign the given value to the `", stringify!(#field_ident) ,"` field.")]
pub fn #function_ident(mut self, #field_ident: #field_type) -> Self {
self.#field_ident = #field_assigns;
self
}
});
if maybe_path_type.is_some() {
let function_ident = format_ident!("with_{}_none", field_ident);
fns.push(quote! {
#[doc = concat!(" Set the value of `", stringify!(#field_ident), "` to `None`.")]
pub fn #function_ident(mut self) -> Self {
self.#field_ident = None;
self
}
});
}
}
} else {
return ParseError::new(
span,
"#[derive(Builder)] is only defined for structs, not for enums or unions!",
)
.to_compile_error()
.into();
}
let implementation = quote! {
#[automatically_derived]
impl #ident {
#(#fns)*
}
};
implementation.into()
}
// https://stackoverflow.com/a/56264023
fn extract_type_path(ty: &Type) -> Option<&Path> {
match *ty {
Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
_ => None,
}
}
// https://stackoverflow.com/a/56264023
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
acc.push_str(&v.ident.to_string());
acc.push('|');
acc
});
vec!["Option|", "std|option|Option|", "core|option|Option|"]
.into_iter()
.find(|s| idents_of_path == *s)
.and_then(|_| path.segments.last())
}

View File

@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `esp_hal::psram::psram_raw_parts` (#2546)
- The timer drivers `OneShotTimer` & `PeriodicTimer` have `into_async` and `new_typed` methods (#2586)
- `timer::Timer` trait has three new methods, `wait`, `async_interrupt_handler` and `peripheral_interrupt` (#2586)
- Configuration structs in the I2C, SPI, and UART drivers now implement the Builder Lite pattern (#2614)
### Changed
@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The timer drivers `OneShotTimer` & `PeriodicTimer` now have a `Mode` parameter and type erase the underlying driver by default (#2586)
- `timer::Timer` has new trait requirements of `Into<AnyTimer>`, `'static` and `InterruptConfigurable` (#2586)
- `systimer::etm::Event` no longer borrows the alarm indefinitely (#2586)
- A number of public enums and structs in the I2C, SPI, and UART drivers have been marked with `#[non_exhaustive]` (#2614)
### Fixed

View File

@ -28,11 +28,7 @@
//!
//! let mut spi = Spi::new_with_config(
//! peripherals.SPI2,
//! Config {
//! frequency: 100.kHz(),
//! mode: SpiMode::Mode0,
//! ..Config::default()
//! },
//! Config::default().with_frequency(100.kHz()).with_mode(SpiMode::Mode0)
//! )
//! .with_sck(sclk)
//! .with_mosi(mosi)

View File

@ -86,6 +86,7 @@ const MAX_ITERATIONS: u32 = 1_000_000;
/// I2C-specific transmission errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum Error {
/// The transmission exceeded the FIFO size.
ExceedingFifo,
@ -106,14 +107,15 @@ pub enum Error {
/// I2C-specific configuration errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum ConfigError {}
#[derive(PartialEq)]
// This enum is used to keep track of the last/next operation that was/will be
// performed in an embedded-hal(-async) I2c::transaction. It is used to
// determine whether a START condition should be issued at the start of the
// current operation and whether a read needs an ack or a nack for the final
// byte.
#[derive(PartialEq)]
enum OpKind {
Write,
Read,
@ -217,6 +219,7 @@ enum Ack {
Ack = 0,
Nack = 1,
}
impl From<u32> for Ack {
fn from(ack: u32) -> Self {
match ack {
@ -226,6 +229,7 @@ impl From<u32> for Ack {
}
}
}
impl From<Ack> for u32 {
fn from(ack: Ack) -> u32 {
ack as u32
@ -233,8 +237,9 @@ impl From<Ack> for u32 {
}
/// I2C driver configuration
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, procmacros::BuilderLite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub struct Config {
/// The I2C clock frequency.
pub frequency: HertzU32,

View File

@ -45,11 +45,7 @@
//!
//! let mut spi = Spi::new_with_config(
//! peripherals.SPI2,
//! Config {
//! frequency: 100.kHz(),
//! mode: SpiMode::Mode0,
//! ..Config::default()
//! },
//! Config::default().with_frequency(100.kHz()).with_mode(SpiMode::Mode0)
//! )
//! .with_sck(sclk)
//! .with_mosi(mosi)
@ -93,6 +89,7 @@ use crate::{
#[cfg(gdma)]
#[derive(Debug, EnumSetType)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum SpiInterrupt {
/// Indicates that the SPI transaction has completed successfully.
///
@ -423,8 +420,9 @@ impl Address {
}
/// SPI peripheral configuration
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, procmacros::BuilderLite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub struct Config {
/// SPI clock frequency
pub frequency: HertzU32,

View File

@ -17,6 +17,7 @@ pub mod slave;
/// SPI errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum Error {
/// Error occurred due to a DMA-related issue.
DmaError(DmaError),

View File

@ -244,6 +244,7 @@ const UART_FIFO_SIZE: u16 = 128;
/// UART Error
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum Error {
/// An invalid configuration argument was provided.
///
@ -289,21 +290,23 @@ impl embedded_io::Error for Error {
// (outside of `config` module in order not to "use" it an extra time)
/// UART clock source
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ClockSource {
/// APB_CLK clock source (default for UART on all the chips except of
/// esp32c6 and esp32h2)
#[cfg_attr(not(any(esp32c6, esp32h2, lp_uart)), default)]
Apb,
#[cfg(not(any(esp32, esp32s2)))]
/// RC_FAST_CLK clock source (17.5 MHz)
RcFast,
#[cfg(not(any(esp32, esp32s2)))]
RcFast,
/// XTAL_CLK clock source (default for UART on esp32c6 and esp32h2 and
/// LP_UART)
#[cfg(not(any(esp32, esp32s2)))]
#[cfg_attr(any(esp32c6, esp32h2, lp_uart), default)]
Xtal,
#[cfg(any(esp32, esp32s2))]
/// REF_TICK clock source (derived from XTAL or RC_FAST, 1MHz)
#[cfg(any(esp32, esp32s2))]
RefTick,
}
@ -317,7 +320,7 @@ const UART_TOUT_THRESH_DEFAULT: u8 = 10;
/// This enum represents the various configurations for the number of data
/// bits used in UART communication. The number of data bits defines the
/// length of each transmitted or received data frame.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DataBits {
/// 5 data bits per frame.
@ -327,6 +330,7 @@ pub enum DataBits {
/// 7 data bits per frame.
DataBits7 = 2,
/// 8 data bits per frame (most common).
#[default]
DataBits8 = 3,
}
@ -336,10 +340,11 @@ pub enum DataBits {
/// ensure that the data has not been corrupted during transmission. The
/// parity bit is added to the data bits to make the number of 1-bits
/// either even or odd.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Parity {
/// No parity bit is used (most common).
#[default]
ParityNone,
/// Even parity: the parity bit is set to make the total number of
/// 1-bits even.
@ -354,10 +359,11 @@ pub enum Parity {
/// The stop bit(s) signal the end of a data packet in UART communication.
/// This enum defines the possible configurations for the number of stop
/// bits.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum StopBits {
/// 1 stop bit.
#[default]
STOP1 = 1,
/// 1.5 stop bits.
STOP1P5 = 2,
@ -366,8 +372,9 @@ pub enum StopBits {
}
/// UART Configuration
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Clone, Copy, procmacros::BuilderLite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub struct Config {
/// The baud rate (speed) of the UART communication in bits per second
/// (bps).
@ -467,18 +474,10 @@ impl Default for Config {
fn default() -> Config {
Config {
baudrate: 115_200,
data_bits: DataBits::DataBits8,
parity: Parity::ParityNone,
stop_bits: StopBits::STOP1,
clock_source: {
cfg_if::cfg_if! {
if #[cfg(any(esp32c6, esp32h2, lp_uart))] {
ClockSource::Xtal
} else {
ClockSource::Apb
}
}
},
data_bits: Default::default(),
parity: Default::default(),
stop_bits: Default::default(),
clock_source: Default::default(),
rx_fifo_full_threshold: UART_FULL_THRESH_DEFAULT,
rx_timeout: Some(UART_TOUT_THRESH_DEFAULT),
}
@ -1158,6 +1157,7 @@ where
/// List of exposed UART events.
#[derive(Debug, EnumSetType)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum UartInterrupt {
/// Indicates that the received has detected the configured
/// [`Uart::set_at_cmd`] character.
@ -1589,12 +1589,13 @@ where
}
}
#[derive(EnumSetType, Debug)]
#[derive(Debug, EnumSetType)]
pub(crate) enum TxEvent {
TxDone,
TxFiFoEmpty,
}
#[derive(EnumSetType, Debug)]
#[derive(Debug, EnumSetType)]
pub(crate) enum RxEvent {
FifoFull,
CmdCharDetected,
@ -1705,6 +1706,7 @@ struct UartTxFuture {
state: &'static State,
registered: bool,
}
impl UartTxFuture {
fn new(uart: impl Peripheral<P = impl Instance>, events: impl Into<EnumSet<TxEvent>>) -> Self {
crate::into_ref!(uart);

View File

@ -59,11 +59,9 @@ async fn main(_spawner: Spawner) {
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_mosi(mosi)

View File

@ -39,11 +39,9 @@ fn main() -> ! {
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_miso(miso) // order matters

View File

@ -90,11 +90,9 @@ fn main() -> ! {
// output connection (because we are using the same pin to loop back)
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_miso(miso)

View File

@ -120,11 +120,9 @@ mod test {
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 10000.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(10000.kHz())
.with_mode(SpiMode::Mode0),
)
.with_miso(unsafe { mosi.clone_unchecked() })
.with_mosi(mosi)
@ -135,11 +133,9 @@ mod test {
#[cfg(any(esp32, esp32s2, esp32s3))]
let other_peripheral = Spi::new_with_config(
peripherals.SPI3,
Config {
frequency: 10000.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(10000.kHz())
.with_mode(SpiMode::Mode0),
)
.with_dma(dma_channel2)
.into_async();
@ -231,11 +227,9 @@ mod test {
let mut spi = Spi::new_with_config(
peripherals.spi,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_dma(peripherals.dma_channel)
.with_buffers(dma_rx_buf, dma_tx_buf)

View File

@ -202,11 +202,9 @@ mod tests {
let spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
);
Context {

View File

@ -73,16 +73,11 @@ mod tests {
let (mosi_loopback_pcnt, mosi) = mosi.split();
// Need to set miso first so that mosi can overwrite the
// output connection (because we are using the same pin to loop back)
let spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 10.MHz(),
..Config::default()
},
)
.with_sck(sclk)
.with_miso(unsafe { mosi.clone_unchecked() })
.with_mosi(mosi);
let spi =
Spi::new_with_config(peripherals.SPI2, Config::default().with_frequency(10.MHz()))
.with_sck(sclk)
.with_miso(unsafe { mosi.clone_unchecked() })
.with_mosi(mosi);
let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(32000);
@ -487,10 +482,7 @@ mod tests {
// This means that without working cancellation, the test case should
// fail.
ctx.spi
.apply_config(&Config {
frequency: 80.kHz(),
..Config::default()
})
.apply_config(&Config::default().with_frequency(80.kHz()))
.unwrap();
// Set up a large buffer that would trigger a timeout
@ -513,10 +505,7 @@ mod tests {
fn can_transmit_after_cancel(mut ctx: Context) {
// Slow down. At 80kHz, the transfer is supposed to take a bit over 3 seconds.
ctx.spi
.apply_config(&Config {
frequency: 80.kHz(),
..Config::default()
})
.apply_config(&Config::default().with_frequency(80.kHz()))
.unwrap();
// Set up a large buffer that would trigger a timeout
@ -533,11 +522,8 @@ mod tests {
transfer.cancel();
(spi, (dma_rx_buf, dma_tx_buf)) = transfer.wait();
spi.apply_config(&Config {
frequency: 10.MHz(),
..Config::default()
})
.unwrap();
spi.apply_config(&Config::default().with_frequency(10.MHz()))
.unwrap();
let transfer = spi
.transfer(dma_rx_buf, dma_tx_buf)

View File

@ -48,11 +48,9 @@ mod tests {
let spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_miso(miso)

View File

@ -52,11 +52,9 @@ mod tests {
let spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_mosi(mosi)

View File

@ -64,11 +64,9 @@ mod tests {
let spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_mosi(mosi)

View File

@ -90,11 +90,11 @@ mod tests {
let mut byte_to_write = 0xA5;
for (baudrate, clock_source) in configs {
ctx.uart
.apply_config(&uart::Config {
baudrate,
clock_source,
..Default::default()
})
.apply_config(
&uart::Config::default()
.with_baudrate(baudrate)
.with_clock_source(clock_source),
)
.unwrap();
ctx.uart.write(byte_to_write).ok();
let read = block!(ctx.uart.read());

View File

@ -36,8 +36,7 @@ use esp_hal::{
delay::Delay,
dma_loop_buffer,
gpio::{Level, Output},
i2c,
i2c::master::I2c,
i2c::{self, master::I2c},
lcd_cam::{
lcd::{
dpi::{Config, Dpi, Format, FrameTiming},
@ -61,10 +60,7 @@ fn main() -> ! {
let i2c = I2c::new(
peripherals.I2C0,
i2c::master::Config {
frequency: 400.kHz(),
..Default::default()
},
i2c::master::Config::default().with_frequency(400.kHz()),
)
.with_sda(peripherals.GPIO47)
.with_scl(peripherals.GPIO48);

View File

@ -77,11 +77,9 @@ fn main() -> ! {
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_mosi(mosi)

View File

@ -63,11 +63,9 @@ fn main() -> ! {
let mut spi = Spi::new_with_config(
peripherals.SPI2,
Config {
frequency: 100.kHz(),
mode: SpiMode::Mode0,
..Config::default()
},
Config::default()
.with_frequency(100.kHz())
.with_mode(SpiMode::Mode0),
)
.with_sck(sclk)
.with_mosi(mosi)