microSD та файли
Щоби прочитати текстовий файл, не треба писати свій файловий менеджер. У embedded-sdmmc вже є звичайні open_file_in_dir, read та close. Почнемо саме з них.
Що підготувати
Вставте карту до запуску Lilka. Потрібен перший розділ MBR з FAT16 або FAT32; exFAT не підтримується. У корені створіть HELLO.TXT у UTF-8, до 256 байтів, наприклад:
Прошивка тільки читає файл і виводить текст у лог. Вона нічого не створює та не перезаписує.
Саме читання
Після монтування тому й відкриття кореневої теки файл відкривається так:
file.read(&mut buffer) записує дані у ваш буфер і повертає кількість прочитаних байтів. Один виклик може прочитати тільки частину файла, тому у повній програмі є короткий цикл. Після читання — file.close().
Нижче вже є все необхідне для запуску, включно з ініціалізацією. Буфер фіксований, тому завеликий файл явно відхиляється. UTF-8 перевіряється після повного читання, щоб не розрізати багатобайтовий символ між порціями.
Повна програма
Тека прикладу — examples/sd-files/.
#![no_std]
#![no_main]
mod card;
use card::{Card, DemoTime};
use core::cell::RefCell;
use embedded_sdmmc::{Mode, VolumeIdx, VolumeManager};
use esp_println::{self as _, println};
use lilkaoxide::prelude::*;
esp_bootloader_esp_idf::esp_app_desc!();
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
println!("PANIC: {}", info);
loop {
core::hint::spin_loop();
}
}
#[esp_hal::main]
fn main() -> ! {
esp_alloc::heap_allocator!(size: 16 * 1024);
let mut config = Configuration::default();
config.buzzer_config.duty_pct = 0;
config.spi_frequency /= 3; // 20 MHz for the shared SPI bus.
let lilka = Lilka::init(config).expect("Lilka init failed");
let sd = lilka.sd.expect("Insert microSD before boot");
let manager = VolumeManager::new(Card(RefCell::new(sd)), DemoTime);
let volume = manager.open_volume(VolumeIdx(0)).expect("Use MBR FAT16/32");
let root = volume.open_root_dir().expect("Cannot open root directory");
let file = root
.open_file_in_dir("HELLO.TXT", Mode::ReadOnly)
.expect("Put HELLO.TXT in the card's root directory");
// A short UTF-8 file fits entirely in this fixed buffer.
let mut bytes = [0u8; 256];
assert!(
file.length() <= bytes.len() as u32,
"Use a file up to 256 bytes"
);
let mut used = 0;
while used < file.length() as usize {
let n = file.read(&mut bytes[used..]).expect("Read failed");
assert!(n != 0, "Unexpected end of file");
used += n;
}
let text = core::str::from_utf8(&bytes[..used]).expect("Use UTF-8 text");
println!("HELLO.TXT:\n{}", text);
file.close().expect("File close failed");
root.close().expect("Directory close failed");
volume.close().expect("Volume close failed");
loop {
esp_hal::delay::Delay::new().delay_millis(100);
}
}
VolumeIdx(0) — перший розділ карти, open_root_dir() — його коренева тека. Mode::ReadOnly відкриває вже існуючий файл для читання. Для першого прикладу використовуйте коротку назву 8.3, як HELLO.TXT.
expect тут навмисно залишений для простоти: при помилці panic-handler друкує причину. У своєму застосунку можна обробити Result і показати повідомлення користувачу.
Залежності
[package]
name = "sd-files"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
publish = false
[dependencies]
lilkaoxide = { path = "../../sdk-hal", default-features = false, features = ["blocking", "RefCellBus", "alloc", "display_buffer_1024"] }
esp-alloc = { version = "0.10.0", features = ["defmt"] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32s3", "defmt"] }
esp-println = { version = "0.16.1", features = ["esp32s3", "defmt-espflash"] }
embedded-sdmmc = "0.9.0"
build.rs, rust-toolchain.toml та .cargo/config.toml вже лежать у теці прикладу. Для окремого проєкту скопіюйте і їх, а шлях ../../sdk-hal замініть на свій.
Що за модуль card
Сам SDK повертає драйвер секторів у lilka.sd, а embedded-sdmmc надає файловий API. card.rs тільки з'єднує ці дві бібліотеки через стандартний BlockDevice. Створювати другий SPI чи знову ініціалізувати карту не потрібно.
Цей модуль уже є у прикладі; для читання іншого файла його змінювати не треба.
Код адаптера, якщо цікаво як це з'єднано
use core::cell::RefCell;
use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx, TimeSource, Timestamp};
use lilkaoxide::SdVolMgr;
pub struct Card(pub RefCell<SdVolMgr<'static>>);
#[derive(Debug)]
pub struct CardError;
impl BlockDevice for Card {
type Error = CardError;
fn read(&self, blocks: &mut [Block], start: BlockIdx) -> Result<(), CardError> {
self.0
.borrow_mut()
.read(start.0, blocks.iter_mut().map(|b| &mut b.contents))
.map_err(|_| CardError)
}
fn write(&self, blocks: &[Block], start: BlockIdx) -> Result<(), CardError> {
self.0
.borrow_mut()
.write(start.0, blocks.iter().map(|b| &b.contents))
.map_err(|_| CardError)
}
fn num_blocks(&self) -> Result<BlockCount, CardError> {
let count: u64 = self.0.borrow().num_blocks().into();
Ok(BlockCount(u32::try_from(count).map_err(|_| CardError)?))
}
}
// Demonstration timestamp only; this is NOT today's date.
pub struct DemoTime;
impl TimeSource for DemoTime {
fn get_timestamp(&self) -> Timestamp {
Timestamp::from_calendar(2000, 1, 1, 0, 0, 0).unwrap()
}
}
RefCell дає змінний доступ до драйвера через &self трейта BlockDevice. DemoTime потрібен для типу VolumeManager, але цей приклад відкриває файл тільки для читання й не змінює його дати. Повернена ним дата 01.01.2000 — демонстраційна. Для запису зі справжньою датою використовуйте джерело часу, наприклад RTC від Keira.
Запуск
У лог має прийти HELLO.TXT: і ваш текст. На дисплей цей приклад не малює: тут перевіряємо тільки файловий API. Для Keira можна створити application .bin, а лог читати окремим сумісним monitor через USB.
Якщо карта не визначилась, перевірте її підключення до старту. Якщо не монтується том — MBR та FAT16/FAT32. Якщо не відкрився файл — назву й розташування в корені. Частота спільної SPI-шини в прикладі знижена до 20 МГц.
Про alloc
FAT-бібліотека embedded-sdmmc 0.9.0 не потребує heap. Глобальний allocator зараз потрібен при компонуванні через extern crate alloc в SD-драйвері SDK. 16 КіБ у прикладі — простий варіант налаштування, а не вимога читання файла. Подробиці — пам'ять та SD.