esp_alloc/
lib.rs

1//! A `no_std` heap allocator for RISC-V and Xtensa processors from
2//! Espressif. Supports all currently available ESP32 devices.
3//!
4//! **NOTE:** using this as your global allocator requires using Rust 1.68 or
5//! greater, or the `nightly` release channel.
6//!
7//! # Using this as your Global Allocator
8//!
9//! ```rust
10//! use esp_alloc as _;
11//!
12//! fn init_heap() {
13//!     const HEAP_SIZE: usize = 32 * 1024;
14//!     static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit();
15//!
16//!     unsafe {
17//!         esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new(
18//!             HEAP.as_mut_ptr() as *mut u8,
19//!             HEAP_SIZE,
20//!             esp_alloc::MemoryCapability::Internal.into(),
21//!         ));
22//!     }
23//! }
24//! ```
25//!
26//! Alternatively, you can use the `heap_allocator!` macro to configure the
27//! global allocator with a given size:
28//!
29//! ```rust
30//! esp_alloc::heap_allocator!(size: 32 * 1024);
31//! ```
32//!
33//! # Using this with the nightly `allocator_api`-feature
34//!
35//! Sometimes you want to have more control over allocations.
36//!
37//! For that, it's convenient to use the nightly `allocator_api`-feature,
38//! which allows you to specify an allocator for single allocations.
39//!
40//! **NOTE:** To use this, you have to enable the crate's `nightly` feature
41//! flag.
42//!
43//! Create and initialize an allocator to use in single allocations:
44//!
45//! ```rust
46//! static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();
47//!
48//! fn init_psram_heap() {
49//!     unsafe {
50//!         PSRAM_ALLOCATOR.add_region(esp_alloc::HeapRegion::new(
51//!             psram::psram_vaddr_start() as *mut u8,
52//!             psram::PSRAM_BYTES,
53//!             esp_alloc::MemoryCapability::External.into(),
54//!         ));
55//!     }
56//! }
57//! ```
58//!
59//! And then use it in an allocation:
60//!
61//! ```rust
62//! let large_buffer: Vec<u8, _> = Vec::with_capacity_in(1048576, &PSRAM_ALLOCATOR);
63//! ```
64//!
65//! Alternatively, you can use the `psram_allocator!` macro to configure the
66//! global allocator to use PSRAM:
67//!
68//! ```rust
69//! let p = esp_hal::init(esp_hal::Config::default());
70//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
71//! ```
72//!
73//! You can also use the `ExternalMemory` allocator to allocate PSRAM memory
74//! with the global allocator:
75//!
76//! ```rust
77//! let p = esp_hal::init(esp_hal::Config::default());
78//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
79//!
80//! let mut vec = Vec::<u32>::new_in(esp_alloc::ExternalMemory);
81//! ```
82//!
83//! ## `allocator_api` feature on stable Rust
84//!
85//! `esp-alloc` implements the allocator trait from [`allocator_api2`], which
86//! provides the nightly-only `allocator_api` features in stable Rust. The crate
87//! contains implementations for `Box` and `Vec`.
88//!
89//! To use the `allocator_api2` features, you need to add the crate to your
90//! `Cargo.toml`. Note that we do not enable the `alloc` feature by default, but
91//! you will need it for the `Box` and `Vec` types.
92//!
93//! ```toml
94//! allocator-api2 = { version = "0.3", default-features = false, features = ["alloc"] }
95//! ```
96//!
97//! With this, you can use the `Box` and `Vec` types from `allocator_api2`, with
98//! `esp-alloc` allocators:
99//!
100//! ```rust
101//! let p = esp_hal::init(esp_hal::Config::default());
102//! esp_alloc::heap_allocator!(size: 64000);
103//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
104//!
105//! let mut vec: Vec<u32, _> = Vec::new_in(esp_alloc::InternalMemory);
106//!
107//! vec.push(0xabcd1234);
108//! assert_eq!(vec[0], 0xabcd1234);
109//! ```
110//!
111//! Note that if you use the nightly `allocator_api` feature, you can use the
112//! `Box` and `Vec` types from `alloc`. `allocator_api2` is still available as
113//! an option, but types from `allocator_api2` are not compatible with the
114//! standard library types.
115//!
116//! # Heap stats
117//!
118//! You can also get stats about the heap usage at anytime with:
119//!
120//! ```rust
121//! let stats: HeapStats = esp_alloc::HEAP.stats();
122//! // HeapStats implements the Display and defmt::Format traits, so you can
123//! // pretty-print the heap stats.
124//! println!("{}", stats);
125//! ```
126//!
127//! Example output:
128//!
129//! ```txt
130//! HEAP INFO
131//! Size: 131068
132//! Current usage: 46148
133//! Max usage: 46148
134//! Total freed: 0
135//! Total allocated: 46148
136//! Memory Layout:
137//! Internal | ████████████░░░░░░░░░░░░░░░░░░░░░░░ | Used: 35% (Used 46148 of 131068, free: 84920)
138//! ```
139//! ## Feature Flags
140#![doc = document_features::document_features!()]
141#![no_std]
142#![cfg_attr(feature = "nightly", feature(allocator_api))]
143#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
144
145mod allocators;
146mod macros;
147
148use core::{
149    alloc::{GlobalAlloc, Layout},
150    cell::RefCell,
151    fmt::Display,
152    ptr::{self, NonNull},
153};
154
155pub use allocators::*;
156use critical_section::Mutex;
157use enumset::{EnumSet, EnumSetType};
158use linked_list_allocator::Heap;
159
160/// The global allocator instance
161#[global_allocator]
162pub static HEAP: EspHeap = EspHeap::empty();
163
164const NON_REGION: Option<HeapRegion> = None;
165
166const BAR_WIDTH: usize = 35;
167
168fn write_bar(f: &mut core::fmt::Formatter<'_>, usage_percent: usize) -> core::fmt::Result {
169    let used_blocks = BAR_WIDTH * usage_percent / 100;
170    (0..used_blocks).try_for_each(|_| write!(f, "█"))?;
171    (used_blocks..BAR_WIDTH).try_for_each(|_| write!(f, "░"))
172}
173
174#[cfg(feature = "defmt")]
175fn write_bar_defmt(fmt: defmt::Formatter, usage_percent: usize) {
176    let used_blocks = BAR_WIDTH * usage_percent / 100;
177    (0..used_blocks).for_each(|_| defmt::write!(fmt, "█"));
178    (used_blocks..BAR_WIDTH).for_each(|_| defmt::write!(fmt, "░"));
179}
180
181#[derive(EnumSetType, Debug)]
182/// Describes the properties of a memory region
183pub enum MemoryCapability {
184    /// Memory must be internal; specifically it should not disappear when
185    /// flash/spiram cache is switched off
186    Internal,
187    /// Memory must be in SPI RAM
188    External,
189}
190
191/// Stats for a heap region
192#[derive(Debug)]
193pub struct RegionStats {
194    /// Total usable size of the heap region in bytes.
195    size: usize,
196
197    /// Currently used size of the heap region in bytes.
198    used: usize,
199
200    /// Free size of the heap region in bytes.
201    free: usize,
202
203    /// Capabilities of the memory region.
204    capabilities: EnumSet<MemoryCapability>,
205}
206
207impl Display for RegionStats {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        let usage_percent = self.used * 100 / self.size;
210
211        // Display Memory type
212        if self.capabilities.contains(MemoryCapability::Internal) {
213            write!(f, "Internal")?;
214        } else if self.capabilities.contains(MemoryCapability::External) {
215            write!(f, "External")?;
216        } else {
217            write!(f, "Unknown")?;
218        }
219
220        write!(f, " | ")?;
221
222        write_bar(f, usage_percent)?;
223
224        write!(
225            f,
226            " | Used: {}% (Used {} of {}, free: {})",
227            usage_percent, self.used, self.size, self.free
228        )
229    }
230}
231
232#[cfg(feature = "defmt")]
233impl defmt::Format for RegionStats {
234    fn format(&self, fmt: defmt::Formatter<'_>) {
235        let usage_percent = self.used * 100 / self.size;
236
237        if self.capabilities.contains(MemoryCapability::Internal) {
238            defmt::write!(fmt, "Internal");
239        } else if self.capabilities.contains(MemoryCapability::External) {
240            defmt::write!(fmt, "External");
241        } else {
242            defmt::write!(fmt, "Unknown");
243        }
244
245        defmt::write!(fmt, " | ");
246
247        write_bar_defmt(fmt, usage_percent);
248
249        defmt::write!(
250            fmt,
251            " | Used: {}% (Used {} of {}, free: {})",
252            usage_percent,
253            self.used,
254            self.size,
255            self.free
256        );
257    }
258}
259
260/// A memory region to be used as heap memory
261pub struct HeapRegion {
262    heap: Heap,
263    capabilities: EnumSet<MemoryCapability>,
264}
265
266impl HeapRegion {
267    /// Create a new [HeapRegion] with the given capabilities
268    ///
269    /// # Safety
270    ///
271    /// - The supplied memory region must be available for the entire program
272    ///   (`'static`).
273    /// - The supplied memory region must be exclusively available to the heap
274    ///   only, no aliasing.
275    /// - `size > 0`.
276    pub unsafe fn new(
277        heap_bottom: *mut u8,
278        size: usize,
279        capabilities: EnumSet<MemoryCapability>,
280    ) -> Self {
281        unsafe {
282            let mut heap = Heap::empty();
283            heap.init(heap_bottom, size);
284
285            Self { heap, capabilities }
286        }
287    }
288
289    /// Return stats for the current memory region
290    pub fn stats(&self) -> RegionStats {
291        RegionStats {
292            size: self.heap.size(),
293            used: self.heap.used(),
294            free: self.heap.free(),
295            capabilities: self.capabilities,
296        }
297    }
298}
299
300/// Stats for a heap allocator
301///
302/// Enable the "internal-heap-stats" feature if you want collect additional heap
303/// informations at the cost of extra cpu time during every alloc/dealloc.
304#[derive(Debug)]
305pub struct HeapStats {
306    /// Granular stats for all the configured memory regions.
307    region_stats: [Option<RegionStats>; 3],
308
309    /// Total size of all combined heap regions in bytes.
310    size: usize,
311
312    /// Current usage of the heap across all configured regions in bytes.
313    current_usage: usize,
314
315    /// Estimation of the max used heap in bytes.
316    #[cfg(feature = "internal-heap-stats")]
317    max_usage: usize,
318
319    /// Estimation of the total allocated bytes since initialization.
320    #[cfg(feature = "internal-heap-stats")]
321    total_allocated: usize,
322
323    /// Estimation of the total freed bytes since initialization.
324    #[cfg(feature = "internal-heap-stats")]
325    total_freed: usize,
326}
327
328impl Display for HeapStats {
329    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
330        writeln!(f, "HEAP INFO")?;
331        writeln!(f, "Size: {}", self.size)?;
332        writeln!(f, "Current usage: {}", self.current_usage)?;
333        #[cfg(feature = "internal-heap-stats")]
334        {
335            writeln!(f, "Max usage: {}", self.max_usage)?;
336            writeln!(f, "Total freed: {}", self.total_freed)?;
337            writeln!(f, "Total allocated: {}", self.total_allocated)?;
338        }
339        writeln!(f, "Memory Layout: ")?;
340        for region in self.region_stats.iter() {
341            if let Some(region) = region.as_ref() {
342                region.fmt(f)?;
343                writeln!(f)?;
344            }
345        }
346        Ok(())
347    }
348}
349
350#[cfg(feature = "defmt")]
351impl defmt::Format for HeapStats {
352    fn format(&self, fmt: defmt::Formatter<'_>) {
353        defmt::write!(fmt, "HEAP INFO\n");
354        defmt::write!(fmt, "Size: {}\n", self.size);
355        defmt::write!(fmt, "Current usage: {}\n", self.current_usage);
356        #[cfg(feature = "internal-heap-stats")]
357        {
358            defmt::write!(fmt, "Max usage: {}\n", self.max_usage);
359            defmt::write!(fmt, "Total freed: {}\n", self.total_freed);
360            defmt::write!(fmt, "Total allocated: {}\n", self.total_allocated);
361        }
362        defmt::write!(fmt, "Memory Layout:\n");
363        for region in self.region_stats.iter() {
364            if let Some(region) = region.as_ref() {
365                defmt::write!(fmt, "{}\n", region);
366            }
367        }
368    }
369}
370
371/// Internal stats to keep track across multiple regions.
372#[cfg(feature = "internal-heap-stats")]
373struct InternalHeapStats {
374    max_usage: usize,
375    total_allocated: usize,
376    total_freed: usize,
377}
378
379/// A memory allocator
380///
381/// In addition to what Rust's memory allocator can do it allows to allocate
382/// memory in regions satisfying specific needs.
383pub struct EspHeap {
384    heap: Mutex<RefCell<[Option<HeapRegion>; 3]>>,
385    #[cfg(feature = "internal-heap-stats")]
386    internal_heap_stats: Mutex<RefCell<InternalHeapStats>>,
387}
388
389impl EspHeap {
390    /// Crate a new UNINITIALIZED heap allocator
391    pub const fn empty() -> Self {
392        EspHeap {
393            heap: Mutex::new(RefCell::new([NON_REGION; 3])),
394            #[cfg(feature = "internal-heap-stats")]
395            internal_heap_stats: Mutex::new(RefCell::new(InternalHeapStats {
396                max_usage: 0,
397                total_allocated: 0,
398                total_freed: 0,
399            })),
400        }
401    }
402
403    /// Add a memory region to the heap
404    ///
405    /// `heap_bottom` is a pointer to the location of the bottom of the heap.
406    ///
407    /// `size` is the size of the heap in bytes.
408    ///
409    /// You can add up to three regions per allocator.
410    ///
411    /// Note that:
412    ///
413    /// - Memory is allocated from the first suitable memory region first
414    ///
415    /// - The heap grows "upwards", towards larger addresses. Thus `end_addr`
416    ///   must be larger than `start_addr`
417    ///
418    /// - The size of the heap is `(end_addr as usize) - (start_addr as usize)`.
419    ///   The allocator won't use the byte at `end_addr`.
420    ///
421    /// # Safety
422    ///
423    /// - The supplied memory region must be available for the entire program (a
424    ///   `'static` lifetime).
425    /// - The supplied memory region must be exclusively available to the heap
426    ///   only, no aliasing.
427    /// - `size > 0`.
428    pub unsafe fn add_region(&self, region: HeapRegion) {
429        critical_section::with(|cs| {
430            let mut regions = self.heap.borrow_ref_mut(cs);
431            let free = regions
432                .iter()
433                .enumerate()
434                .find(|v| v.1.is_none())
435                .map(|v| v.0);
436
437            if let Some(free) = free {
438                regions[free] = Some(region);
439            } else {
440                panic!(
441                    "Exceeded the maximum of {} heap memory regions",
442                    regions.len()
443                );
444            }
445        });
446    }
447
448    /// Returns an estimate of the amount of bytes in use in all memory regions.
449    pub fn used(&self) -> usize {
450        critical_section::with(|cs| {
451            let regions = self.heap.borrow_ref(cs);
452            let mut used = 0;
453            for region in regions.iter() {
454                if let Some(region) = region.as_ref() {
455                    used += region.heap.used();
456                }
457            }
458            used
459        })
460    }
461
462    /// Return usage stats for the [Heap].
463    ///
464    /// Note:
465    /// [HeapStats] directly implements [Display], so this function can be
466    /// called from within `println!()` to pretty-print the usage of the
467    /// heap.
468    pub fn stats(&self) -> HeapStats {
469        const EMPTY_REGION_STAT: Option<RegionStats> = None;
470        let mut region_stats: [Option<RegionStats>; 3] = [EMPTY_REGION_STAT; 3];
471
472        critical_section::with(|cs| {
473            let mut used = 0;
474            let mut free = 0;
475            let regions = self.heap.borrow_ref(cs);
476            for (id, region) in regions.iter().enumerate() {
477                if let Some(region) = region.as_ref() {
478                    let stats = region.stats();
479                    free += stats.free;
480                    used += stats.used;
481                    region_stats[id] = Some(region.stats());
482                }
483            }
484
485            cfg_if::cfg_if! {
486                if #[cfg(feature = "internal-heap-stats")] {
487                    let internal_heap_stats = self.internal_heap_stats.borrow_ref(cs);
488                    HeapStats {
489                        region_stats,
490                        size: free + used,
491                        current_usage: used,
492                        max_usage: internal_heap_stats.max_usage,
493                        total_allocated: internal_heap_stats.total_allocated,
494                        total_freed: internal_heap_stats.total_freed,
495                    }
496                } else {
497                    HeapStats {
498                        region_stats,
499                        size: free + used,
500                        current_usage: used,
501                    }
502                }
503            }
504        })
505    }
506
507    /// Returns an estimate of the amount of bytes available.
508    pub fn free(&self) -> usize {
509        self.free_caps(EnumSet::empty())
510    }
511
512    /// The free heap satisfying the given requirements
513    pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
514        critical_section::with(|cs| {
515            let regions = self.heap.borrow_ref(cs);
516            let mut free = 0;
517            for region in regions.iter().filter(|region| {
518                if region.is_some() {
519                    region
520                        .as_ref()
521                        .unwrap()
522                        .capabilities
523                        .is_superset(capabilities)
524                } else {
525                    false
526                }
527            }) {
528                if let Some(region) = region.as_ref() {
529                    free += region.heap.free();
530                }
531            }
532            free
533        })
534    }
535
536    /// Allocate memory in a region satisfying the given requirements.
537    ///
538    /// # Safety
539    ///
540    /// This function is unsafe because undefined behavior can result
541    /// if the caller does not ensure that `layout` has non-zero size.
542    ///
543    /// The allocated block of memory may or may not be initialized.
544    pub unsafe fn alloc_caps(
545        &self,
546        capabilities: EnumSet<MemoryCapability>,
547        layout: Layout,
548    ) -> *mut u8 {
549        critical_section::with(|cs| {
550            #[cfg(feature = "internal-heap-stats")]
551            let before = self.used();
552            let mut regions = self.heap.borrow_ref_mut(cs);
553            let mut iter = (*regions).iter_mut().filter(|region| {
554                if region.is_some() {
555                    region
556                        .as_ref()
557                        .unwrap()
558                        .capabilities
559                        .is_superset(capabilities)
560                } else {
561                    false
562                }
563            });
564
565            let res = loop {
566                if let Some(Some(region)) = iter.next() {
567                    let res = region.heap.allocate_first_fit(layout);
568                    if let Ok(res) = res {
569                        break Some(res);
570                    }
571                } else {
572                    break None;
573                }
574            };
575
576            res.map_or(ptr::null_mut(), |allocation| {
577                #[cfg(feature = "internal-heap-stats")]
578                {
579                    let mut internal_heap_stats = self.internal_heap_stats.borrow_ref_mut(cs);
580                    drop(regions);
581                    // We need to call used because [linked_list_allocator::Heap] does internal size
582                    // alignment so we cannot use the size provided by the layout.
583                    let used = self.used();
584
585                    internal_heap_stats.total_allocated += used - before;
586                    internal_heap_stats.max_usage =
587                        core::cmp::max(internal_heap_stats.max_usage, used);
588                }
589
590                allocation.as_ptr()
591            })
592        })
593    }
594}
595
596unsafe impl GlobalAlloc for EspHeap {
597    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
598        unsafe { self.alloc_caps(EnumSet::empty(), layout) }
599    }
600
601    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
602        unsafe {
603            if ptr.is_null() {
604                return;
605            }
606
607            critical_section::with(|cs| {
608                #[cfg(feature = "internal-heap-stats")]
609                let before = self.used();
610                let mut regions = self.heap.borrow_ref_mut(cs);
611                let mut iter = (*regions).iter_mut();
612
613                while let Some(Some(region)) = iter.next() {
614                    if region.heap.bottom() <= ptr && region.heap.top() >= ptr {
615                        region.heap.deallocate(NonNull::new_unchecked(ptr), layout);
616                    }
617                }
618
619                #[cfg(feature = "internal-heap-stats")]
620                {
621                    let mut internal_heap_stats = self.internal_heap_stats.borrow_ref_mut(cs);
622                    drop(regions);
623                    // We need to call `used()` because [linked_list_allocator::Heap] does internal
624                    // size alignment so we cannot use the size provided by the
625                    // layout.
626                    internal_heap_stats.total_freed += before - self.used();
627                }
628            })
629        }
630    }
631}