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}