Перейти до змісту

microSD та файли

Щоби прочитати текстовий файл, не треба писати свій файловий менеджер. У embedded-sdmmc вже є звичайні open_file_in_dir, read та close. Почнемо саме з них.

Що підготувати

Вставте карту до запуску Lilka. Потрібен перший розділ MBR з FAT16 або FAT32; exFAT не підтримується. У корені створіть HELLO.TXT у UTF-8, до 256 байтів, наприклад:

Привіт з microSD!

Прошивка тільки читає файл і виводить текст у лог. Вона нічого не створює та не перезаписує.

Саме читання

Після монтування тому й відкриття кореневої теки файл відкривається так:

let file = root.open_file_in_dir("HELLO.TXT", Mode::ReadOnly)
    .expect("Cannot open HELLO.TXT");

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.

Запуск

cd examples/sd-files
cargo +esp build --release --locked
cargo +esp run --release --locked

У лог має прийти 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.