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 heap;
147mod macros;
148#[cfg(feature = "compat")]
149mod malloc;
150
151use core::{
152 alloc::{GlobalAlloc, Layout},
153 fmt::Display,
154 ptr::{self, NonNull},
155};
156
157pub use allocators::*;
158use enumset::{EnumSet, EnumSetType};
159use esp_sync::NonReentrantMutex;
160
161use crate::heap::Heap;
162
163/// The global allocator instance
164#[global_allocator]
165pub static HEAP: EspHeap = EspHeap::empty();
166
167const BAR_WIDTH: usize = 35;
168
169fn write_bar(f: &mut core::fmt::Formatter<'_>, usage_percent: usize) -> core::fmt::Result {
170 let used_blocks = BAR_WIDTH * usage_percent / 100;
171 (0..used_blocks).try_for_each(|_| write!(f, "█"))?;
172 (used_blocks..BAR_WIDTH).try_for_each(|_| write!(f, "░"))
173}
174
175#[cfg(feature = "defmt")]
176fn write_bar_defmt(fmt: defmt::Formatter, usage_percent: usize) {
177 let used_blocks = BAR_WIDTH * usage_percent / 100;
178 (0..used_blocks).for_each(|_| defmt::write!(fmt, "█"));
179 (used_blocks..BAR_WIDTH).for_each(|_| defmt::write!(fmt, "░"));
180}
181
182#[derive(EnumSetType, Debug)]
183/// Describes the properties of a memory region
184pub enum MemoryCapability {
185 /// Memory must be internal; specifically it should not disappear when
186 /// flash/spiram cache is switched off
187 Internal,
188 /// Memory must be in SPI RAM
189 External,
190}
191
192/// Stats for a heap region
193#[derive(Debug)]
194pub struct RegionStats {
195 /// Total usable size of the heap region in bytes.
196 pub size: usize,
197
198 /// Currently used size of the heap region in bytes.
199 pub used: usize,
200
201 /// Free size of the heap region in bytes.
202 pub free: usize,
203
204 /// Capabilities of the memory region.
205 pub capabilities: EnumSet<MemoryCapability>,
206}
207
208impl Display for RegionStats {
209 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210 let usage_percent = self.used * 100 / self.size;
211
212 // Display Memory type
213 if self.capabilities.contains(MemoryCapability::Internal) {
214 write!(f, "Internal")?;
215 } else if self.capabilities.contains(MemoryCapability::External) {
216 write!(f, "External")?;
217 } else {
218 write!(f, "Unknown")?;
219 }
220
221 write!(f, " | ")?;
222
223 write_bar(f, usage_percent)?;
224
225 write!(
226 f,
227 " | Used: {}% (Used {} of {}, free: {})",
228 usage_percent, self.used, self.size, self.free
229 )
230 }
231}
232
233#[cfg(feature = "defmt")]
234impl defmt::Format for RegionStats {
235 fn format(&self, fmt: defmt::Formatter<'_>) {
236 let usage_percent = self.used * 100 / self.size;
237
238 if self.capabilities.contains(MemoryCapability::Internal) {
239 defmt::write!(fmt, "Internal");
240 } else if self.capabilities.contains(MemoryCapability::External) {
241 defmt::write!(fmt, "External");
242 } else {
243 defmt::write!(fmt, "Unknown");
244 }
245
246 defmt::write!(fmt, " | ");
247
248 write_bar_defmt(fmt, usage_percent);
249
250 defmt::write!(
251 fmt,
252 " | Used: {}% (Used {} of {}, free: {})",
253 usage_percent,
254 self.used,
255 self.size,
256 self.free
257 );
258 }
259}
260
261/// A memory region to be used as heap memory
262pub struct HeapRegion {
263 heap: Heap,
264 capabilities: EnumSet<MemoryCapability>,
265}
266
267impl HeapRegion {
268 /// Create a new [HeapRegion] with the given capabilities
269 ///
270 /// # Safety
271 ///
272 /// - The supplied memory region must be available for the entire program (`'static`).
273 /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
274 /// - `size > 0`.
275 pub unsafe fn new(
276 heap_bottom: *mut u8,
277 size: usize,
278 capabilities: EnumSet<MemoryCapability>,
279 ) -> Self {
280 Self {
281 heap: unsafe { Heap::new(heap_bottom, size) },
282 capabilities,
283 }
284 }
285
286 /// Return stats for the current memory region
287 pub fn stats(&self) -> RegionStats {
288 RegionStats {
289 size: self.size(),
290 used: self.used(),
291 free: self.free(),
292 capabilities: self.capabilities,
293 }
294 }
295
296 fn size(&self) -> usize {
297 self.heap.size()
298 }
299
300 fn used(&self) -> usize {
301 self.heap.used()
302 }
303
304 fn free(&self) -> usize {
305 self.heap.free()
306 }
307
308 fn allocate(&mut self, layout: Layout) -> Option<NonNull<u8>> {
309 self.heap.allocate(layout)
310 }
311
312 unsafe fn try_deallocate(&mut self, ptr: NonNull<u8>, layout: Layout) -> bool {
313 unsafe { self.heap.try_deallocate(ptr, layout) }
314 }
315}
316
317/// Stats for a heap allocator
318///
319/// Enable the "internal-heap-stats" feature if you want collect additional heap
320/// informations at the cost of extra cpu time during every alloc/dealloc.
321#[derive(Debug)]
322pub struct HeapStats {
323 /// Granular stats for all the configured memory regions.
324 pub region_stats: [Option<RegionStats>; 3],
325
326 /// Total size of all combined heap regions in bytes.
327 pub size: usize,
328
329 /// Current usage of the heap across all configured regions in bytes.
330 pub current_usage: usize,
331
332 /// Estimation of the max used heap in bytes.
333 #[cfg(feature = "internal-heap-stats")]
334 pub max_usage: usize,
335
336 /// Estimation of the total allocated bytes since initialization.
337 #[cfg(feature = "internal-heap-stats")]
338 pub total_allocated: usize,
339
340 /// Estimation of the total freed bytes since initialization.
341 #[cfg(feature = "internal-heap-stats")]
342 pub total_freed: usize,
343}
344
345impl Display for HeapStats {
346 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
347 writeln!(f, "HEAP INFO")?;
348 writeln!(f, "Size: {}", self.size)?;
349 writeln!(f, "Current usage: {}", self.current_usage)?;
350 #[cfg(feature = "internal-heap-stats")]
351 {
352 writeln!(f, "Max usage: {}", self.max_usage)?;
353 writeln!(f, "Total freed: {}", self.total_freed)?;
354 writeln!(f, "Total allocated: {}", self.total_allocated)?;
355 }
356 writeln!(f, "Memory Layout: ")?;
357 for region in self.region_stats.iter() {
358 if let Some(region) = region.as_ref() {
359 region.fmt(f)?;
360 writeln!(f)?;
361 }
362 }
363 Ok(())
364 }
365}
366
367#[cfg(feature = "defmt")]
368impl defmt::Format for HeapStats {
369 fn format(&self, fmt: defmt::Formatter<'_>) {
370 defmt::write!(fmt, "HEAP INFO\n");
371 defmt::write!(fmt, "Size: {}\n", self.size);
372 defmt::write!(fmt, "Current usage: {}\n", self.current_usage);
373 #[cfg(feature = "internal-heap-stats")]
374 {
375 defmt::write!(fmt, "Max usage: {}\n", self.max_usage);
376 defmt::write!(fmt, "Total freed: {}\n", self.total_freed);
377 defmt::write!(fmt, "Total allocated: {}\n", self.total_allocated);
378 }
379 defmt::write!(fmt, "Memory Layout:\n");
380 for region in self.region_stats.iter() {
381 if let Some(region) = region.as_ref() {
382 defmt::write!(fmt, "{}\n", region);
383 }
384 }
385 }
386}
387
388/// Internal stats to keep track across multiple regions.
389#[cfg(feature = "internal-heap-stats")]
390struct InternalHeapStats {
391 max_usage: usize,
392 total_allocated: usize,
393 total_freed: usize,
394}
395
396struct EspHeapInner {
397 heap: [Option<HeapRegion>; 3],
398 #[cfg(feature = "internal-heap-stats")]
399 internal_heap_stats: InternalHeapStats,
400}
401
402impl EspHeapInner {
403 /// Crate a new UNINITIALIZED heap allocator
404 pub const fn empty() -> Self {
405 EspHeapInner {
406 heap: [const { None }; 3],
407 #[cfg(feature = "internal-heap-stats")]
408 internal_heap_stats: InternalHeapStats {
409 max_usage: 0,
410 total_allocated: 0,
411 total_freed: 0,
412 },
413 }
414 }
415
416 pub unsafe fn add_region(&mut self, region: HeapRegion) {
417 let free = self
418 .heap
419 .iter()
420 .enumerate()
421 .find(|v| v.1.is_none())
422 .map(|v| v.0);
423
424 if let Some(free) = free {
425 self.heap[free] = Some(region);
426 } else {
427 panic!(
428 "Exceeded the maximum of {} heap memory regions",
429 self.heap.len()
430 );
431 }
432 }
433
434 /// Returns an estimate of the amount of bytes in use in all memory regions.
435 pub fn used(&self) -> usize {
436 let mut used = 0;
437 for region in self.heap.iter() {
438 if let Some(region) = region.as_ref() {
439 used += region.heap.used();
440 }
441 }
442 used
443 }
444
445 /// Return usage stats for the [EspHeap].
446 ///
447 /// Note:
448 /// [HeapStats] directly implements [Display], so this function can be
449 /// called from within `println!()` to pretty-print the usage of the
450 /// heap.
451 pub fn stats(&self) -> HeapStats {
452 let mut region_stats: [Option<RegionStats>; 3] = [const { None }; 3];
453
454 let mut used = 0;
455 let mut free = 0;
456 for (id, region) in self.heap.iter().enumerate() {
457 if let Some(region) = region.as_ref() {
458 let stats = region.stats();
459 free += stats.free;
460 used += stats.used;
461 region_stats[id] = Some(region.stats());
462 }
463 }
464
465 cfg_if::cfg_if! {
466 if #[cfg(feature = "internal-heap-stats")] {
467 HeapStats {
468 region_stats,
469 size: free + used,
470 current_usage: used,
471 max_usage: self.internal_heap_stats.max_usage,
472 total_allocated: self.internal_heap_stats.total_allocated,
473 total_freed: self.internal_heap_stats.total_freed,
474 }
475 } else {
476 HeapStats {
477 region_stats,
478 size: free + used,
479 current_usage: used,
480 }
481 }
482 }
483 }
484
485 /// Returns an estimate of the amount of bytes available.
486 pub fn free(&self) -> usize {
487 self.free_caps(EnumSet::empty())
488 }
489
490 /// The free heap satisfying the given requirements
491 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
492 let mut free = 0;
493 for region in self.heap.iter().filter(|region| {
494 if region.is_some() {
495 region
496 .as_ref()
497 .unwrap()
498 .capabilities
499 .is_superset(capabilities)
500 } else {
501 false
502 }
503 }) {
504 if let Some(region) = region.as_ref() {
505 free += region.heap.free();
506 }
507 }
508 free
509 }
510
511 /// Allocate memory in a region satisfying the given requirements.
512 ///
513 /// # Safety
514 ///
515 /// This function is unsafe because undefined behavior can result
516 /// if the caller does not ensure that `layout` has non-zero size.
517 ///
518 /// The allocated block of memory may or may not be initialized.
519 unsafe fn alloc_caps(
520 &mut self,
521 capabilities: EnumSet<MemoryCapability>,
522 layout: Layout,
523 ) -> *mut u8 {
524 #[cfg(feature = "internal-heap-stats")]
525 let before = self.used();
526 let mut iter = self
527 .heap
528 .iter_mut()
529 .filter_map(|region| region.as_mut())
530 .filter(|region| region.capabilities.is_superset(capabilities));
531
532 let allocation = loop {
533 let Some(region) = iter.next() else {
534 return ptr::null_mut();
535 };
536
537 if let Some(res) = region.allocate(layout) {
538 break res;
539 }
540 };
541
542 #[cfg(feature = "internal-heap-stats")]
543 {
544 // We need to call used because the heap impls have some internal overhead
545 // so we cannot use the size provided by the layout.
546 let used = self.used();
547
548 self.internal_heap_stats.total_allocated += used - before;
549 self.internal_heap_stats.max_usage =
550 core::cmp::max(self.internal_heap_stats.max_usage, used);
551 }
552
553 allocation.as_ptr()
554 }
555}
556
557/// A memory allocator
558///
559/// In addition to what Rust's memory allocator can do it allows to allocate
560/// memory in regions satisfying specific needs.
561pub struct EspHeap {
562 inner: NonReentrantMutex<EspHeapInner>,
563}
564
565impl EspHeap {
566 /// Crate a new UNINITIALIZED heap allocator
567 pub const fn empty() -> Self {
568 EspHeap {
569 inner: NonReentrantMutex::new(EspHeapInner::empty()),
570 }
571 }
572
573 /// Add a memory region to the heap
574 ///
575 /// `heap_bottom` is a pointer to the location of the bottom of the heap.
576 ///
577 /// `size` is the size of the heap in bytes.
578 ///
579 /// You can add up to three regions per allocator.
580 ///
581 /// Note that:
582 ///
583 /// - Memory is allocated from the first suitable memory region first
584 ///
585 /// - The heap grows "upwards", towards larger addresses. Thus `end_addr` must be larger than
586 /// `start_addr`
587 ///
588 /// - The size of the heap is `(end_addr as usize) - (start_addr as usize)`. The allocator won't
589 /// use the byte at `end_addr`.
590 ///
591 /// # Safety
592 ///
593 /// - The supplied memory region must be available for the entire program (a `'static`
594 /// lifetime).
595 /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
596 /// - `size > 0`.
597 pub unsafe fn add_region(&self, region: HeapRegion) {
598 self.inner.with(|heap| unsafe { heap.add_region(region) })
599 }
600
601 /// Returns an estimate of the amount of bytes in use in all memory regions.
602 pub fn used(&self) -> usize {
603 self.inner.with(|heap| heap.used())
604 }
605
606 /// Return usage stats for the [EspHeap].
607 ///
608 /// Note:
609 /// [HeapStats] directly implements [Display], so this function can be
610 /// called from within `println!()` to pretty-print the usage of the
611 /// heap.
612 pub fn stats(&self) -> HeapStats {
613 self.inner.with(|heap| heap.stats())
614 }
615
616 /// Returns an estimate of the amount of bytes available.
617 pub fn free(&self) -> usize {
618 self.inner.with(|heap| heap.free())
619 }
620
621 /// The free heap satisfying the given requirements
622 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
623 self.inner.with(|heap| heap.free_caps(capabilities))
624 }
625
626 /// Allocate memory in a region satisfying the given requirements.
627 ///
628 /// # Safety
629 ///
630 /// This function is unsafe because undefined behavior can result
631 /// if the caller does not ensure that `layout` has non-zero size.
632 ///
633 /// The allocated block of memory may or may not be initialized.
634 pub unsafe fn alloc_caps(
635 &self,
636 capabilities: EnumSet<MemoryCapability>,
637 layout: Layout,
638 ) -> *mut u8 {
639 self.inner
640 .with(|heap| unsafe { heap.alloc_caps(capabilities, layout) })
641 }
642}
643
644unsafe impl GlobalAlloc for EspHeap {
645 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
646 unsafe { self.alloc_caps(EnumSet::empty(), layout) }
647 }
648
649 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
650 let Some(ptr) = NonNull::new(ptr) else {
651 return;
652 };
653
654 self.inner.with(|this| {
655 #[cfg(feature = "internal-heap-stats")]
656 let before = this.used();
657 let mut iter = this.heap.iter_mut();
658
659 while let Some(Some(region)) = iter.next() {
660 if unsafe { region.try_deallocate(ptr, layout) } {
661 break;
662 }
663 }
664
665 #[cfg(feature = "internal-heap-stats")]
666 {
667 // We need to call `used()` because [linked_list_allocator::Heap] does internal
668 // size alignment so we cannot use the size provided by the
669 // layout.
670 this.internal_heap_stats.total_freed += before - this.used();
671 }
672 })
673 }
674}