diff --git a/Cargo.toml b/Cargo.toml index 01ab174468b..2189d542872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["esp-idf-macros"] + [package] name = "esp-idf-hal" version = "0.45.2" @@ -25,7 +28,7 @@ harness = false default = ["std", "binstart"] std = ["alloc", "esp-idf-sys/std"] alloc = [] -nightly = [] +nightly = ["esp-idf-macros/doc-cfg"] experimental = [] wake-from-isr = [] # Only enable if you plan to use the `edge-executor` crate embassy-sync = [] # For now, the dependecy on the `embassy-sync` crate is non-optional, but this might change in future @@ -66,6 +69,7 @@ enumset = { version = "1.1.4", default-features = false } log = { version = "0.4", default-features = false } atomic-waker = { version = "1.1.1", default-features = false } embassy-sync = "0.7" +esp-idf-macros = { path = "esp-idf-macros" } [build-dependencies] embuild = "0.33" diff --git a/esp-idf-macros/Cargo.toml b/esp-idf-macros/Cargo.toml new file mode 100644 index 00000000000..9428a7bb8aa --- /dev/null +++ b/esp-idf-macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "esp-idf-macros" +version = "0.1.0" +edition = "2021" +rust-version = "1.79" + +[lib] +proc-macro = true + +[features] +default = [] +doc-cfg = [] + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["extra-traits", "full", "fold"] } diff --git a/esp-idf-macros/src/dcfg.rs b/esp-idf-macros/src/dcfg.rs new file mode 100644 index 00000000000..abb723c186e --- /dev/null +++ b/esp-idf-macros/src/dcfg.rs @@ -0,0 +1,24 @@ +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; + +pub fn dcfg(args: TokenStream, input: TokenStream) -> syn::Result { + // This will either emit #[cfg(...)] or #[cfg(...)] #[doc(cfg(...))] depending on whether + // the "doc-cfg" feature is enabled. + + let attrs = { + #[cfg(feature = "doc-cfg")] + { + quote_spanned!(args.span() => #[cfg(#args)] #[doc(cfg(#args))]) + } + #[cfg(not(feature = "doc-cfg"))] + { + quote_spanned!(args.span() => #[cfg(#args)]) + } + }; + + Ok(quote! { + #attrs + #input + }) +} diff --git a/esp-idf-macros/src/lib.rs b/esp-idf-macros/src/lib.rs new file mode 100644 index 00000000000..75ec7572f87 --- /dev/null +++ b/esp-idf-macros/src/lib.rs @@ -0,0 +1,150 @@ +use proc_macro::TokenStream; + +mod dcfg; +mod ram; + +/// This attribute places the annotated function or static variable into the internal RAM +/// of the ESP32 chip. +/// +/// This macro is not a magic bullet, there are several caveats that have to be considered +/// when using it. +/// +/// If a function is placed into RAM, the literals are not automatically placed into RAM as well: +/// +/// ``` +/// #[ram] +/// fn gpio_isr_handler() -> usize { +/// let s = "I am string still stored in flash"; +/// } +/// ``` +/// +/// To store the literal in flash, one could do +/// ``` +/// #[ram] +/// fn gpio_isr_handler() -> usize { +/// #[ram] +/// static _S: &str = "I am string still stored in flash"; +/// let s = _S; +/// } +/// ``` +/// The macro does not can not place called functions into RAM automatically. This **must** +/// be done manually by annotationing the called functions with `#[ram]` where they are declared. +/// The same applies to any functions that are transitively called. +/// +/// If the attribute is applied to a static variable that references some data, for example +/// `#[ram] static DATA: &[u8] = &OTHER_VARIABLE;` the referenced data might not be placed into RAM. +/// +/// For byte string (`b"..."`) and string literals (`"..."`), the attribute will ensure that +/// the referenced data is placed in RAM, but for other expressions the caller has to ensure that. +/// +/// If the expressions is referencing a slice, and the value is [`Copy`], +/// the variable can be annotated with `#[ram(copy)]` like this: +/// ``` +/// #[ram(copy)] +/// static BUFFER: [u8; 3] = &[0, 1, 2]; +/// ``` +/// +/// This will instruct to generate a constant expression that will ensure that the data is placed into RAM. +/// +/// If that is not an option, one can also write: +/// ``` +/// #[ram] +/// static BUFFER_DATA: [u8; 3] = [0, 1, 2]; +/// #[ram] +/// static BUFFER: &[u8] = &BUFFER_DATA; +/// ``` +/// +/// or like this: +/// +/// ``` +/// #[ram] +/// static BUFFER: &[u8] = { +/// #[ram] +/// static DATA: [u8; 3] = [0, 1, 2]; +/// DATA.as_slice() +/// }; +/// ``` +/// +/// +///
+/// +/// If the code is supposed to run while the flash is disabled, it is **strongly** recommended to +/// inspect the generated binary to ensure that the entire code and data is placed into RAM. +/// It might be necessary to implement that code in C or manually call the esp-idf functions +/// through `esp-idf-sys` to ensure that no flash functions are called. +/// +/// For more information, refer to +/// +///
+#[proc_macro_attribute] +pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { + ram::link_to_section(".iram1", ".dram1", args.into(), input.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// This attribute forwards its arguments to `cfg`, and if the `doc-cfg` feature of this +/// crate is enabled, it will emit a `doc(cfg(...))` attribute as well. +/// +/// # Example +/// +/// For rustdoc to document that a piece of code is only available when a specific condition is met, +/// one would have to write (because it is nightly-only): +/// +/// ``` +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// #[cfg_attr(feature = "nightly", doc(cfg(esp_idf_version_at_least_5_4_0)))] +/// Invalid, +/// } +/// ``` +/// +/// with this attribute, one can shorten it to: +/// +/// ``` +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[dcfg(esp_idf_version_at_least_5_4_0)] +/// Invalid, +/// } +/// ``` +/// +/// The macro will then expand to the following if the `doc-cfg` feature is enabled: +/// +/// ``` +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// #[doc(cfg(esp_idf_version_at_least_5_4_0))] +/// Invalid, +/// } +/// ``` +/// +/// and to the following if the `doc-cfg` feature is not enabled: +/// +/// ``` +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// Invalid, +/// } +/// ``` +#[proc_macro_attribute] +pub fn dcfg(args: TokenStream, input: TokenStream) -> TokenStream { + dcfg::dcfg(args.into(), input.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/esp-idf-macros/src/ram.rs b/esp-idf-macros/src/ram.rs new file mode 100644 index 00000000000..9d9bbae8b37 --- /dev/null +++ b/esp-idf-macros/src/ram.rs @@ -0,0 +1,207 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::spanned::Spanned; +use syn::{punctuated::Punctuated, Item, Token}; + +fn quote_link_section(name: &str, subsection: Option) -> TokenStream { + // NOTE: With rustc 1.82 the link_section attribute should be marked unsafe like this: + // #[unsafe(link_section = ::core::concat!(#name, #subsection))] + // With the 2024 edition this is mandatory. + // + // As of now, esp-idf-hal is targeting rustc 1.79 + // See https://doc.rust-lang.org/nightly/edition-guide/rust-2024/unsafe-attributes.html + if let Some(subsection) = subsection { + quote! { + #[link_section = ::core::concat!(#name, #subsection)] + } + } else { + quote! { + #[link_section = #name] + } + } +} + +/// In the following code: +/// +/// ``` +/// #[link_section = ".dram1"] +/// static VARIABLE: &str = "Hello"; +/// ``` +/// +/// only a pointer to the data + length will be stored in `.dram1`. The actual bytes +/// of the string will be stored in .flash.rodata +/// +/// To do anything useful with the string, it will still have to access the flash... +/// The same problem applies to any static slices. +/// +/// This function will rewrite the expression to explicitly define an owned buffer +/// that will be linked to .dram as well and the original expression will be a pointer +/// to that buffer. +/// +/// To build the buffer, the type stored in the buffer must be [`Copy`], if not, +/// the macro will not be able to create the buffer. +/// +/// There is no way to check whether a type is [`Copy`] in a proc-macro, which is why +/// this function has a `rewrite_any_expr` argument. If that is set to `true`, +/// the function will try to rewrite any expression, if not, it will only rewrite +/// string and byte string literals. +fn rewrite_static_expr( + section: &str, + syn::ItemStatic { + attrs, + vis, + ident, + ty, + mutability, + expr, + .. + }: syn::ItemStatic, + rewrite_any_expr: bool, +) -> syn::ItemStatic { + let mut expr = (*expr).clone(); + + let link_attr = quote_link_section(section, Some(unique_section([&ident]))); + + // Check if the expression is a string or byte string literal which are always `Copy` + if let syn::Expr::Lit(syn::ExprLit { attrs, lit }) = &expr { + let mut bytes: Option> = None; + let mut from_buffer_expr: Option = None; + + match lit { + syn::Lit::Str(lit_str) => { + bytes = Some(lit_str.value().into_bytes()); + from_buffer_expr = Some(syn::parse_quote! { + unsafe { ::core::str::from_utf8_unchecked(&_BUFFER) } + }); + } + syn::Lit::ByteStr(lit_byte_str) => { + bytes = Some(lit_byte_str.value()); + from_buffer_expr = Some(syn::parse_quote!(_BUFFER.as_slice())); + } + _ => {} + } + + if let (Some(bytes), Some(from_buffer_expr)) = (bytes, from_buffer_expr) { + let buffer_len = bytes.len(); + + expr = syn::parse_quote!(#(#attrs)* { + #link_attr + static _BUFFER: [u8; #buffer_len] = [#(#bytes),*]; + + #from_buffer_expr + }); + } + } + + if rewrite_any_expr { + // Check if it is an &[T] where T = inner_type + if let syn::Type::Reference(syn::TypeReference { elem, .. }) = &*ty { + if let syn::Type::Slice(syn::TypeSlice { elem, .. }) = &**elem { + expr = syn::parse_quote_spanned!(elem.span() => { + const VALUE: #ty = #expr; + const SLICE_EXPR: &[#elem] = #expr; + + #link_attr + static _BUFFER: [#elem; SLICE_EXPR.len()] = { + let mut buf: [#elem; SLICE_EXPR.len()] = [SLICE_EXPR[0]; SLICE_EXPR.len()]; + + let mut i = 0; + while i < buf.len() { + buf[i] = SLICE_EXPR[i]; + i += 1; + } + + buf + }; + + _BUFFER.as_slice() + }); + } + } + } + + syn::parse_quote!( + #(#attrs)* + #link_attr + #vis static #mutability #ident: #ty = #expr; + ) +} + +struct Arguments { + is_copy: bool, +} + +impl syn::parse::Parse for Arguments { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let items = Punctuated::::parse_terminated(input)?; + if items.is_empty() { + return Ok(Self { is_copy: false }); + } + + if items.len() == 1 { + if let syn::Meta::Path(path) = &items[0] { + if path.is_ident("copy") { + return Ok(Self { is_copy: true }); + } + } + } + + Err(syn::Error::new(input.span(), "Unknown argument")) + } +} + +pub fn link_to_section( + function_section: &str, + data_section: &str, + args: TokenStream, + input: TokenStream, +) -> syn::Result { + let args_span = args.span(); + let attr_args = syn::parse2::(args)?; + + let input_span = input.span(); + let item = syn::parse2::(input)?; + match &item { + Item::Static(item_static) => { + Ok( + rewrite_static_expr(data_section, item_static.clone(), attr_args.is_copy) + .into_token_stream(), + ) + } + Item::Fn(syn::ItemFn { + sig: syn::Signature { ident, .. }, + .. + }) => { + // For now, only support empty attribute arguments + if attr_args.is_copy { + return Err(syn::Error::new(args_span, "Unknown argument")); + } + + let link_attr = quote_link_section(function_section, Some(unique_section([ident]))); + + Ok(quote! { + #link_attr + #[inline(never)] + #item + }) + } + _ => Err(syn::Error::new( + input_span, + "The attribute can not be applied to this item", + )), + } +} + +/// This function generates a likely unique subsection name based on the provided identifiers. +fn unique_section(idents: impl IntoIterator) -> syn::Expr { + let separator = quote!("_",); + let iter = idents.into_iter(); + syn::parse_quote!(::core::concat!( + ".", + #(::core::stringify!(#iter))(#separator)*, + "_", + ::core::line!(), + "_", + ::core::column!() + )) +} diff --git a/src/cpu.rs b/src/cpu.rs index 8fb6fdf2a68..279dd80ffd2 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,6 +5,8 @@ use esp_idf_sys::*; use enumset::EnumSetType; +use crate::ram; + /// Returns the number of cores supported by the esp32* chip pub const CORES: u32 = SOC_CPU_CORES_NUM; @@ -46,8 +48,7 @@ impl From for Core { /// On dual-core systems like esp32 and esp32s3 this function returns: /// 0 - when the active core is the PRO CPU /// 1 - when the active core is the APP CPU -#[inline(always)] -#[link_section = ".iram1.cpu_core"] +#[ram] pub fn core() -> Core { #[cfg(any(esp32c3, esp32s2, esp32c2, esp32h2, esp32c5, esp32c6))] let core = 0; diff --git a/src/interrupt.rs b/src/interrupt.rs index efdaba3d87f..c2ad22765ec 100644 --- a/src/interrupt.rs +++ b/src/interrupt.rs @@ -2,6 +2,8 @@ use enumset::{EnumSet, EnumSetType}; use esp_idf_sys::*; +use crate::ram; + /// For backwards compatibility pub type IntrFlags = InterruptType; @@ -83,8 +85,7 @@ impl From for u32 { pub(crate) static CS: IsrCriticalSection = IsrCriticalSection::new(); /// Returns true if the currently active core is executing an ISR request -#[inline(always)] -#[link_section = ".iram1.interrupt_active"] +#[ram] pub fn active() -> bool { unsafe { xPortInIsrContext() != 0 } } @@ -116,8 +117,7 @@ unsafe fn do_yield_signal(arg: *mut ()) { static mut ISR_YIELDER: Option<(unsafe fn(*mut ()), *mut ())> = None; #[allow(clippy::type_complexity)] -#[inline(always)] -#[link_section = ".iram1.interrupt_get_isr_yielder"] +#[ram] pub(crate) unsafe fn get_isr_yielder() -> Option<(unsafe fn(*mut ()), *mut ())> { if active() { free(|| { @@ -144,8 +144,7 @@ pub(crate) unsafe fn get_isr_yielder() -> Option<(unsafe fn(*mut ()), *mut ())> /// ISR handler so as to reastore the yield function which was valid before the /// ISR handler was invoked. #[allow(clippy::type_complexity)] -#[inline(always)] -#[link_section = ".iram1.interrupt_set_isr_yielder"] +#[ram] pub unsafe fn set_isr_yielder( yielder: Option<(unsafe fn(*mut ()), *mut ())>, ) -> Option<(unsafe fn(*mut ()), *mut ())> { @@ -170,8 +169,7 @@ pub struct IsrCriticalSection(core::cell::UnsafeCell); pub struct IsrCriticalSection(core::marker::PhantomData<*const ()>); #[cfg(not(any(esp32, esp32s2, esp32s3, esp32p4)))] -#[inline(always)] -#[link_section = ".iram1.interrupt_enter"] +#[ram] fn enter(_cs: &IsrCriticalSection) { unsafe { vPortEnterCritical(); @@ -179,8 +177,7 @@ fn enter(_cs: &IsrCriticalSection) { } #[cfg(any(esp32, esp32s2, esp32s3, esp32p4))] -#[inline(always)] -#[link_section = ".iram1.interrupt_enter"] +#[ram] fn enter(cs: &IsrCriticalSection) { unsafe { xPortEnterCriticalTimeout(cs.0.get(), portMUX_NO_TIMEOUT); @@ -188,8 +185,7 @@ fn enter(cs: &IsrCriticalSection) { } #[cfg(not(any(esp32, esp32s2, esp32s3, esp32p4)))] -#[inline(always)] -#[link_section = ".iram1.interrupt_exit"] +#[ram] fn exit(_cs: &IsrCriticalSection) { unsafe { vPortExitCritical(); @@ -197,8 +193,7 @@ fn exit(_cs: &IsrCriticalSection) { } #[cfg(any(esp32, esp32s2, esp32s3, esp32p4))] -#[inline(always)] -#[link_section = ".iram1.interrupt_exit"] +#[ram] fn exit(cs: &IsrCriticalSection) { unsafe { vPortExitCritical(cs.0.get()); @@ -274,8 +269,7 @@ impl Drop for IsrCriticalSectionGuard<'_> { } /// Executes closure f in an interrupt-free context -#[inline(always)] -#[link_section = ".iram1.interrupt_free"] +#[ram] pub fn free(f: impl FnOnce() -> R) -> R { let _guard = CS.enter(); diff --git a/src/lib.rs b/src/lib.rs index ca35b36e866..187a37bd085 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ extern crate std; #[macro_use] extern crate alloc; +pub use esp_idf_macros::dcfg; +pub use esp_idf_macros::ram; + pub mod adc; pub mod can; pub mod cpu; diff --git a/src/task.rs b/src/task.rs index fcf9be55f58..decd0395282 100644 --- a/src/task.rs +++ b/src/task.rs @@ -13,6 +13,7 @@ use esp_idf_sys::*; use crate::cpu::Core; use crate::interrupt; +use crate::ram; #[cfg(not(any( esp_idf_version_major = "4", @@ -83,8 +84,7 @@ pub unsafe fn destroy(task: TaskHandle_t) { vTaskDelete(task) } -#[inline(always)] -#[link_section = ".iram1.interrupt_task_do_yield"] +#[ram] pub fn do_yield() { if interrupt::active() { unsafe { @@ -114,8 +114,7 @@ pub fn do_yield() { } } -#[inline(always)] -#[link_section = ".iram1.interrupt_task_current"] +#[ram] pub fn current() -> Option { if interrupt::active() { None @@ -463,8 +462,7 @@ pub struct CriticalSection(Cell>>, AtomicBool); // Not available in the esp-idf-sys bindings const QUEUE_TYPE_RECURSIVE_MUTEX: u8 = 4; -#[inline(always)] -#[link_section = ".iram1.cs_enter"] +#[ram] fn enter(cs: &CriticalSection) { if !cs.1.load(Ordering::SeqCst) { interrupt::free(|| { @@ -484,8 +482,7 @@ fn enter(cs: &CriticalSection) { } } -#[inline(always)] -#[link_section = ".iram1.cs_exit"] +#[ram] fn exit(cs: &CriticalSection) { if !cs.1.load(Ordering::SeqCst) { panic!("Called exit() without matching enter()");