Merge pull request 'rp2-sd-driver' (#9) from rp2-sd-driver into main
All checks were successful
All checks were successful
Reviewed-on: #9 Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
This commit was merged in pull request #9.
This commit is contained in:
@@ -15,3 +15,5 @@ set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
|
||||
|
||||
set(GEN_PINS_BOARD_CSV "${CMAKE_CURRENT_LIST_DIR}/pins.csv")
|
||||
set(GEN_PINS_CSV_ARG --board-csv "${GEN_PINS_BOARD_CSV}")
|
||||
|
||||
add_link_options("-Wl,--print-memory-usage")
|
||||
|
||||
@@ -8,7 +8,7 @@ set -eu
|
||||
make -C mpy-cross -j "$(nproc)"
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W clean
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W \
|
||||
USER_C_MODULES="$TOPDIR"/src/audiocore/micropython.cmake -j "$(nproc)"
|
||||
USER_C_MODULES="$TOPDIR"/src/micropython.cmake -j "$(nproc)"
|
||||
)
|
||||
|
||||
echo "Output in lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2"
|
||||
|
||||
2
software/src/micropython.cmake
Normal file
2
software/src/micropython.cmake
Normal file
@@ -0,0 +1,2 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/audiocore/micropython.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/rp2_sd/micropython.cmake)
|
||||
19
software/src/rp2_sd/micropython.cmake
Normal file
19
software/src/rp2_sd/micropython.cmake
Normal file
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
add_library(usermod_rp2_sd INTERFACE)
|
||||
|
||||
pico_generate_pio_header(usermod_rp2_sd ${CMAKE_CURRENT_LIST_DIR}/sd_spi_pio.pio)
|
||||
|
||||
target_sources(usermod_rp2_sd INTERFACE
|
||||
${CMAKE_CURRENT_LIST_DIR}/module.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/sd.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/sd_spi.c
|
||||
${CMAKE_CURRENT_BINARY_DIR}/sd_spi_pio.pio.h
|
||||
)
|
||||
|
||||
target_include_directories(usermod_rp2_sd INTERFACE
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(usermod INTERFACE usermod_rp2_sd)
|
||||
117
software/src/rp2_sd/module.c
Normal file
117
software/src/rp2_sd/module.c
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "py/obj.h"
|
||||
#include "sd.h"
|
||||
|
||||
// Include MicroPython API.
|
||||
#include "py/mperrno.h"
|
||||
#include "py/runtime.h"
|
||||
|
||||
// This module is RP2 specific
|
||||
#include "mphalport.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
const mp_obj_type_t sdcard_type;
|
||||
struct sdcard_obj {
|
||||
mp_obj_base_t base;
|
||||
struct sd_context sd_context;
|
||||
};
|
||||
|
||||
static void sdcard_init(struct sdcard_obj *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
|
||||
{
|
||||
enum { ARG_mosi, ARG_miso, ARG_sck, ARG_ss, ARG_baudrate };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{MP_QSTR_mosi, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
{MP_QSTR_miso, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
{MP_QSTR_sck, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
{MP_QSTR_ss, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
{MP_QSTR_baudrate, MP_ARG_INT, {.u_int = 15000000}},
|
||||
};
|
||||
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
const mp_hal_pin_obj_t pin_mosi = mp_hal_get_pin_obj(args[ARG_mosi].u_obj);
|
||||
const mp_hal_pin_obj_t pin_miso = mp_hal_get_pin_obj(args[ARG_miso].u_obj);
|
||||
const mp_hal_pin_obj_t pin_sck = mp_hal_get_pin_obj(args[ARG_sck].u_obj);
|
||||
const mp_hal_pin_obj_t pin_ss = mp_hal_get_pin_obj(args[ARG_ss].u_obj);
|
||||
const unsigned baudrate = args[ARG_baudrate].u_int;
|
||||
if (!sd_init(&obj->sd_context, pin_mosi, pin_miso, pin_sck, pin_ss, baudrate)) {
|
||||
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("sd_init() failed"));
|
||||
}
|
||||
}
|
||||
|
||||
static mp_obj_t sdcard_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args)
|
||||
{
|
||||
struct sdcard_obj *sdcard = mp_obj_malloc(struct sdcard_obj, &sdcard_type);
|
||||
mp_map_t kw_args;
|
||||
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
|
||||
sdcard_init(sdcard, n_args, args, &kw_args);
|
||||
return MP_OBJ_FROM_PTR(sdcard);
|
||||
}
|
||||
|
||||
static mp_obj_t sdcard_deinit(mp_obj_t self_obj)
|
||||
{
|
||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||
if (!sd_deinit(&self->sd_context))
|
||||
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("sd_deinit() failed"));
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_1(sdcard_deinit_obj, sdcard_deinit);
|
||||
|
||||
static mp_obj_t sdcard_readblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_t buf_obj)
|
||||
{
|
||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||
const int start_block = mp_obj_get_int(block_obj);
|
||||
mp_buffer_info_t bufinfo;
|
||||
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_WRITE))
|
||||
mp_raise_ValueError("Not a write buffer");
|
||||
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||
mp_raise_ValueError("Buffer length is invalid");
|
||||
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||
for (int block = 0; block < nblocks; block++) {
|
||||
// TODO: Implement CMD18 read multiple blocks
|
||||
if (!sd_readblock(&self->sd_context, start_block + block, bufinfo.buf + block * SD_SECTOR_SIZE))
|
||||
mp_raise_OSError(MP_EIO);
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_readblocks_obj, sdcard_readblocks);
|
||||
|
||||
static mp_obj_t sdcard_ioctl(mp_obj_t self_obj, mp_obj_t op_obj, mp_obj_t arg_obj)
|
||||
{
|
||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||
int op = mp_obj_get_int(op_obj);
|
||||
switch (op) {
|
||||
case 4:
|
||||
return mp_obj_new_int(self->sd_context.blocks);
|
||||
case 5:
|
||||
return mp_obj_new_int(SD_SECTOR_SIZE);
|
||||
default:
|
||||
return mp_const_none;
|
||||
}
|
||||
};
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_ioctl_obj, sdcard_ioctl);
|
||||
|
||||
static const mp_rom_map_elem_t sdcard_locals_dict_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&sdcard_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&sdcard_ioctl_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&sdcard_readblocks_obj)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(sdcard_locals_dict, sdcard_locals_dict_table);
|
||||
|
||||
MP_DEFINE_CONST_OBJ_TYPE(sdcard_type, MP_QSTR_SDCard, MP_TYPE_FLAG_NONE, locals_dict, &sdcard_locals_dict, make_new,
|
||||
&sdcard_make_new);
|
||||
|
||||
static const mp_rom_map_elem_t rp2_sd_module_globals_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_rp2_sd)},
|
||||
{MP_ROM_QSTR(MP_QSTR_SDCard), MP_ROM_PTR(&sdcard_type)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(rp2_sd_module_globals, rp2_sd_module_globals_table);
|
||||
|
||||
const mp_obj_module_t rp2_sd_cmodule = {
|
||||
.base = {&mp_type_module},
|
||||
.globals = (mp_obj_dict_t *)&rp2_sd_module_globals,
|
||||
};
|
||||
MP_REGISTER_MODULE(MP_QSTR_rp2_sd, rp2_sd_cmodule);
|
||||
267
software/src/rp2_sd/sd.c
Normal file
267
software/src/rp2_sd/sd.c
Normal file
@@ -0,0 +1,267 @@
|
||||
#include "sd.h"
|
||||
#include "sd_spi.h"
|
||||
#include "sd_util.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// #define SD_DEBUG
|
||||
|
||||
#define SD_R1_ILLEGAL_COMMAND (1 << 2)
|
||||
|
||||
static bool sd_acmd(const uint8_t cmd, const uint32_t arg, unsigned resplen, uint8_t res[static resplen])
|
||||
{
|
||||
if (!sd_cmd(55, 0, 1, res))
|
||||
return false;
|
||||
if ((res[0] & 0x7e) != 0x00)
|
||||
return false;
|
||||
|
||||
return sd_cmd(cmd, arg, resplen, res);
|
||||
}
|
||||
|
||||
static bool sd_early_init(void)
|
||||
{
|
||||
uint8_t buf;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (sd_cmd(0, 0, 1, &buf)) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("CMD0 resp %02hhx\n", buf);
|
||||
#endif
|
||||
if (buf == 0x01) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#ifdef SD_DEBUG
|
||||
printf("CMD0 timeout, try again...\n");
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool sd_check_interface_condition(void)
|
||||
{
|
||||
uint8_t buf[5];
|
||||
if (sd_cmd(8, 0x000001AA, 5, buf)) {
|
||||
if ((buf[3] & 0xf) != 0x1 || buf[4] != 0xAA) {
|
||||
printf("sd_init: check interface condition failed\n");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (buf[0] & SD_R1_ILLEGAL_COMMAND) {
|
||||
printf("sd_init: check interface condition returned illegal command - old card?\n");
|
||||
} else {
|
||||
printf("sd_init: check interface condition failed\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sd_send_op_cond(void)
|
||||
{
|
||||
uint8_t buf;
|
||||
bool use_acmd = true;
|
||||
for (int timeout = 0; timeout < 500; ++timeout) {
|
||||
bool result = false;
|
||||
if (use_acmd)
|
||||
result = sd_acmd(41, 0x40000000, 1, &buf);
|
||||
else
|
||||
result = sd_cmd(1, 0x40000000, 1, &buf);
|
||||
if (!result) {
|
||||
if (use_acmd && buf & SD_R1_ILLEGAL_COMMAND) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("sd_init: card does not understand ACMD41, try CMD1...\n");
|
||||
#endif
|
||||
continue;
|
||||
} else if (buf != 0x01) {
|
||||
printf("sd_init: send_op_cond failed\n");
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (buf == 0x00) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
printf("sd_init: send_op_cond: timeout waiting for !idle\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool sd_read_ocr(uint32_t *const ocr)
|
||||
{
|
||||
uint8_t buf[5];
|
||||
if (!sd_cmd(58, 0, 5, buf))
|
||||
return false;
|
||||
*ocr = buf[1] << 24 | buf[2] << 16 | buf[3] << 8 | buf[4];
|
||||
return true;
|
||||
}
|
||||
|
||||
static void sd_dump_cid [[maybe_unused]] (void)
|
||||
{
|
||||
uint8_t buf[16];
|
||||
if (sd_cmd_read(10, 0, 16, buf)) {
|
||||
const uint8_t crc = sd_crc7(15, buf);
|
||||
const uint8_t card_crc = buf[15] >> 1;
|
||||
if (card_crc != crc) {
|
||||
printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
|
||||
// Some cheap SD cards always report CRC=0, don't fail in that case
|
||||
if (card_crc != 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t mid = buf[0];
|
||||
char oid[2], pnm[5];
|
||||
memcpy(oid, buf + 1, 2);
|
||||
memcpy(pnm, buf + 3, 5);
|
||||
uint8_t prv = buf[8];
|
||||
uint32_t psn = buf[9] << 24 | buf[10] << 16 | buf[11] << 8 | buf[12];
|
||||
int mdt_year = 2000 + ((buf[13] & 0xf) << 4 | (buf[14] & 0xf0) >> 4);
|
||||
int mdt_month = buf[14] & 0x0f;
|
||||
printf("CID: mid: %02hhx, oid: %.2s, pnm: %.5s, prv: %02hhx, psn: %08" PRIx32 ", mdt_year: %d, mdt_month: %d\n",
|
||||
mid, oid, pnm, prv, psn, mdt_year, mdt_month);
|
||||
}
|
||||
}
|
||||
|
||||
static bool sd_read_csd(struct sd_context *sd_context)
|
||||
{
|
||||
uint8_t buf[16];
|
||||
if (sd_cmd_read(9, 0, 16, buf)) {
|
||||
const uint8_t crc = sd_crc7(15, buf);
|
||||
const uint8_t card_crc = buf[15] >> 1;
|
||||
if (card_crc != crc) {
|
||||
printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
|
||||
// Some cheap SD cards always report CRC=0, don't fail in that case
|
||||
if (card_crc != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const unsigned csd_ver = buf[0] >> 6;
|
||||
unsigned blocksize [[maybe_unused]] = 0;
|
||||
unsigned blocks = 0;
|
||||
unsigned version [[maybe_unused]] = 0;
|
||||
switch (csd_ver) {
|
||||
case 0: {
|
||||
if (sd_context->sdhc_sdxc) {
|
||||
printf("sd_init: Got CSD v1.0 but card is SDHC/SDXC?\n");
|
||||
return false;
|
||||
}
|
||||
const unsigned read_bl_len = buf[5] & 0xf;
|
||||
if (read_bl_len < 9 || read_bl_len > 11) {
|
||||
printf("Invalid read_bl_len in CSD 1.0\n");
|
||||
return false;
|
||||
}
|
||||
blocksize = 1 << (buf[5] & 0xf);
|
||||
const unsigned c_size_mult = (buf[9] & 0x1) << 2 | (buf[10] & 0xc0) >> 6;
|
||||
const unsigned c_size = (buf[6] & 0x3) << 10 | (buf[7] << 2) | (buf[8] & 0xc0) >> 6;
|
||||
blocks = (c_size + 1) * (1 << (c_size_mult + 2));
|
||||
version = 1;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
blocksize = SD_SECTOR_SIZE;
|
||||
const unsigned c_size = (buf[7] & 0x3f) << 16 | buf[8] << 8 | buf[9];
|
||||
blocks = (c_size + 1) * 1024;
|
||||
version = 2;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
printf("sd_init: Got CSD v3.0, but SDUC does not support SPI.\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
sd_context->blocks = blocks;
|
||||
#ifdef SD_DEBUG
|
||||
printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB\n", version, blocksize, blocks,
|
||||
((uint64_t)blocksize * blocks) / (1024 * 1024));
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss, int rate)
|
||||
{
|
||||
if (!sd_spi_init(mosi, miso, sck, ss)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sd_early_init()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sd_check_interface_condition()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t ocr;
|
||||
if (!sd_read_ocr(&ocr)) {
|
||||
printf("sd_init: read OCR failed\n");
|
||||
return false;
|
||||
}
|
||||
if ((ocr & 0x00380000) != 0x00380000) {
|
||||
printf("sd_init: unsupported card voltage range\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sd_send_op_cond())
|
||||
return false;
|
||||
|
||||
sd_spi_set_bitrate(rate);
|
||||
|
||||
if (!sd_read_ocr(&ocr)) {
|
||||
printf("sd_init: read OCR failed\n");
|
||||
return false;
|
||||
}
|
||||
if (!(ocr & (1 << 31))) {
|
||||
printf("sd_init: card not powered up but !idle?\n");
|
||||
return false;
|
||||
}
|
||||
sd_context->sdhc_sdxc = (ocr & (1 << 30));
|
||||
|
||||
if (!sd_read_csd(sd_context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef SD_DEBUG
|
||||
sd_dump_cid();
|
||||
#endif
|
||||
|
||||
sd_context->initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_deinit(struct sd_context *sd_context)
|
||||
{
|
||||
if (!sd_spi_deinit())
|
||||
return false;
|
||||
sd_context->initialized = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_readblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE])
|
||||
{
|
||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||
return false;
|
||||
|
||||
return sd_cmd_read(17, sector_num, SD_SECTOR_SIZE, buffer);
|
||||
}
|
||||
|
||||
bool sd_readblock_start(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE])
|
||||
{
|
||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||
return false;
|
||||
return sd_cmd_read_start(17, sector_num, SD_SECTOR_SIZE, buffer);
|
||||
}
|
||||
|
||||
bool sd_readblock_complete(struct sd_context *sd_context)
|
||||
{
|
||||
if (!sd_context->initialized)
|
||||
return false;
|
||||
sd_cmd_read_complete();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
|
||||
23
software/src/rp2_sd/sd.h
Normal file
23
software/src/rp2_sd/sd.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SD_SECTOR_SIZE 512
|
||||
|
||||
struct sd_context {
|
||||
size_t blocks;
|
||||
bool initialized;
|
||||
bool old_card;
|
||||
bool sdhc_sdxc;
|
||||
};
|
||||
|
||||
bool sd_init(struct sd_context *context, int mosi, int miso, int sck, int ss, int rate);
|
||||
bool sd_deinit(struct sd_context *sd_context);
|
||||
|
||||
bool sd_readblock(struct sd_context *context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE]);
|
||||
|
||||
bool sd_readblock_start(struct sd_context *context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE]);
|
||||
bool sd_readblock_complete(struct sd_context *context);
|
||||
bool sd_readblock_is_complete(struct sd_context *context);
|
||||
293
software/src/rp2_sd/sd_spi.c
Normal file
293
software/src/rp2_sd/sd_spi.c
Normal file
@@ -0,0 +1,293 @@
|
||||
#include "hardware/pio.h"
|
||||
|
||||
#include "sd_spi.h"
|
||||
#include "sd_util.h"
|
||||
|
||||
#include "sd_spi_pio.pio.h"
|
||||
|
||||
#include "hardware/dma.h"
|
||||
#include <hardware/gpio.h>
|
||||
#include <hardware/spi.h>
|
||||
#include <hardware/sync.h>
|
||||
#include <pico/time.h>
|
||||
#include <string.h>
|
||||
|
||||
struct sd_dma_context {
|
||||
uint8_t *read_buf;
|
||||
size_t len;
|
||||
uint8_t crc_buf[2];
|
||||
uint8_t read_token_buf;
|
||||
uint8_t wrdata;
|
||||
_Atomic enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } state;
|
||||
};
|
||||
|
||||
struct sd_spi_context {
|
||||
struct sd_dma_context sd_dma_context;
|
||||
int spi_sm;
|
||||
unsigned spi_offset;
|
||||
int spi_dma_rd, spi_dma_wr, spi_dma_rd_crc;
|
||||
dma_channel_config spi_dma_rd_cfg, spi_dma_wr_cfg, spi_dma_rd_crc_cfg;
|
||||
int mosi, miso, sck, ss;
|
||||
bool initialized;
|
||||
};
|
||||
|
||||
/* We only realistically need one context, so reduce overhead by statically allocating it here */
|
||||
static struct sd_spi_context sd_spi_context = {};
|
||||
|
||||
static void __time_critical_func(sd_spi_write_blocking)(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
pio_sm_put(SD_PIO, sd_spi_context.spi_sm, data[0] << 24);
|
||||
for (size_t i = 1; i < len; ++i) {
|
||||
pio_sm_put(SD_PIO, sd_spi_context.spi_sm, data[i] << 24);
|
||||
pio_sm_get_blocking(SD_PIO, sd_spi_context.spi_sm);
|
||||
}
|
||||
pio_sm_get_blocking(SD_PIO, sd_spi_context.spi_sm);
|
||||
|
||||
assert(pio_sm_is_tx_fifo_empty(SD_PIO, sd_spi_context.spi_sm));
|
||||
assert(pio_sm_is_rx_fifo_empty(SD_PIO, sd_spi_context.spi_sm));
|
||||
}
|
||||
|
||||
static void __time_critical_func(sd_spi_read_blocking)(uint8_t wrdata, uint8_t *data, size_t len)
|
||||
{
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
pio_sm_put(SD_PIO, sd_spi_context.spi_sm, wrdata << 24);
|
||||
for (size_t i = 0; i < len - 1; ++i) {
|
||||
pio_sm_put(SD_PIO, sd_spi_context.spi_sm, wrdata << 24);
|
||||
data[i] = pio_sm_get_blocking(SD_PIO, sd_spi_context.spi_sm);
|
||||
}
|
||||
data[len - 1] = pio_sm_get_blocking(SD_PIO, sd_spi_context.spi_sm);
|
||||
|
||||
assert(pio_sm_is_tx_fifo_empty(SD_PIO, sd_spi_context.spi_sm));
|
||||
assert(pio_sm_is_rx_fifo_empty(SD_PIO, sd_spi_context.spi_sm));
|
||||
}
|
||||
|
||||
static void __time_critical_func(sd_spi_dma_isr)(void)
|
||||
{
|
||||
if (dma_channel_get_irq0_status(sd_spi_context.spi_dma_rd)) {
|
||||
dma_channel_acknowledge_irq0(sd_spi_context.spi_dma_rd);
|
||||
if (sd_spi_context.sd_dma_context.state == DMA_READ_TOKEN) {
|
||||
if (sd_spi_context.sd_dma_context.read_token_buf != 0xff) {
|
||||
if (sd_spi_context.sd_dma_context.read_token_buf == 0xfe) {
|
||||
channel_config_set_chain_to(&sd_spi_context.spi_dma_rd_cfg, sd_spi_context.spi_dma_rd_crc);
|
||||
channel_config_set_irq_quiet(&sd_spi_context.spi_dma_rd_cfg, true);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_rd, &sd_spi_context.spi_dma_rd_cfg,
|
||||
sd_spi_context.sd_dma_context.read_buf, &SD_PIO->rxf[sd_spi_context.spi_sm],
|
||||
sd_spi_context.sd_dma_context.len, false);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_wr, &sd_spi_context.spi_dma_wr_cfg,
|
||||
&SD_PIO->txf[sd_spi_context.spi_sm], &sd_spi_context.sd_dma_context.wrdata,
|
||||
sd_spi_context.sd_dma_context.len + 2, false);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_rd_crc, &sd_spi_context.spi_dma_rd_crc_cfg,
|
||||
sd_spi_context.sd_dma_context.crc_buf, &SD_PIO->rxf[sd_spi_context.spi_sm], 2,
|
||||
false);
|
||||
dma_start_channel_mask((1 << sd_spi_context.spi_dma_rd) | (1 << sd_spi_context.spi_dma_wr));
|
||||
sd_spi_context.sd_dma_context.state = DMA_READ;
|
||||
} else {
|
||||
// Bad read token, abort transfer
|
||||
sd_spi_context.sd_dma_context.state = DMA_IDLE;
|
||||
}
|
||||
} else {
|
||||
// try again
|
||||
dma_channel_configure(sd_spi_context.spi_dma_rd, &sd_spi_context.spi_dma_rd_cfg,
|
||||
&sd_spi_context.sd_dma_context.read_token_buf,
|
||||
&SD_PIO->rxf[sd_spi_context.spi_sm], 1, false);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_wr, &sd_spi_context.spi_dma_wr_cfg,
|
||||
&SD_PIO->txf[sd_spi_context.spi_sm], &sd_spi_context.sd_dma_context.wrdata, 1,
|
||||
false);
|
||||
dma_start_channel_mask((1 << sd_spi_context.spi_dma_rd) | (1 << sd_spi_context.spi_dma_wr));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dma_channel_get_irq0_status(sd_spi_context.spi_dma_rd_crc)) {
|
||||
dma_channel_acknowledge_irq0(sd_spi_context.spi_dma_rd_crc);
|
||||
assert(sd_spi_context.sd_dma_context.state == DMA_READ);
|
||||
sd_spi_context.sd_dma_context.state = DMA_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void sd_spi_wait_complete(void)
|
||||
{
|
||||
while (sd_spi_context.sd_dma_context.state != DMA_IDLE)
|
||||
__wfi();
|
||||
}
|
||||
|
||||
bool sd_cmd_read_is_complete(void) { return sd_spi_context.sd_dma_context.state == DMA_IDLE; }
|
||||
|
||||
static bool sd_spi_read_dma(uint8_t wrdata, uint8_t *data, size_t len)
|
||||
{
|
||||
if (sd_spi_context.sd_dma_context.state != DMA_IDLE)
|
||||
return false;
|
||||
channel_config_set_chain_to(&sd_spi_context.spi_dma_rd_cfg, sd_spi_context.spi_dma_rd);
|
||||
channel_config_set_irq_quiet(&sd_spi_context.spi_dma_rd_cfg, false);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_rd, &sd_spi_context.spi_dma_rd_cfg,
|
||||
&sd_spi_context.sd_dma_context.read_token_buf, &SD_PIO->rxf[sd_spi_context.spi_sm], 1, false);
|
||||
dma_channel_configure(sd_spi_context.spi_dma_wr, &sd_spi_context.spi_dma_wr_cfg,
|
||||
&SD_PIO->txf[sd_spi_context.spi_sm], &sd_spi_context.sd_dma_context.wrdata, 1, false);
|
||||
sd_spi_context.sd_dma_context.state = DMA_READ_TOKEN;
|
||||
sd_spi_context.sd_dma_context.len = len;
|
||||
sd_spi_context.sd_dma_context.read_buf = data;
|
||||
sd_spi_context.sd_dma_context.wrdata = wrdata;
|
||||
dma_start_channel_mask((1 << sd_spi_context.spi_dma_rd) | (1 << sd_spi_context.spi_dma_wr));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void sd_spi_cmd_send(const uint8_t cmd, const uint32_t arg)
|
||||
{
|
||||
uint8_t buf[6] = {0x40 | cmd, arg >> 24, arg >> 16, arg >> 8, arg, 0};
|
||||
buf[5] = sd_crc7(5, buf) << 1 | 1;
|
||||
gpio_put(sd_spi_context.ss, false);
|
||||
// Write command, argument and CRC
|
||||
sd_spi_write_blocking(buf, 6);
|
||||
}
|
||||
|
||||
bool sd_cmd(const uint8_t cmd, const uint32_t arg, unsigned resplen, uint8_t resp[static resplen])
|
||||
{
|
||||
sd_spi_cmd_send(cmd, arg);
|
||||
resp[0] = 0;
|
||||
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
|
||||
bool got_r1 = false;
|
||||
for (int timeout = 0; timeout < 8; ++timeout) {
|
||||
sd_spi_read_blocking(0xff, resp, 1);
|
||||
if (!(resp[0] & 0x80)) {
|
||||
got_r1 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (got_r1 && (resp[0] & 0x7e) == 0) {
|
||||
// read rest of response if R1 does not indicate an error
|
||||
sd_spi_read_blocking(0xff, resp + 1, resplen - 1);
|
||||
}
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
const uint8_t buf = 0xff;
|
||||
// Ensure 8 SPI clock cycles after CS deasserted
|
||||
sd_spi_write_blocking(&buf, 1);
|
||||
|
||||
return got_r1 && (resp[0] & 0x7e) == 0;
|
||||
}
|
||||
|
||||
bool sd_cmd_read(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen])
|
||||
{
|
||||
if (!sd_cmd_read_start(cmd, arg, datalen, data))
|
||||
return false;
|
||||
|
||||
return sd_cmd_read_complete();
|
||||
}
|
||||
|
||||
bool sd_cmd_read_start(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen])
|
||||
{
|
||||
uint8_t buf[2];
|
||||
sd_spi_cmd_send(cmd, arg);
|
||||
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
|
||||
bool got_r1 = false;
|
||||
for (int timeout = 0; timeout < 8; ++timeout) {
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
if (!(buf[0] & 0x80)) {
|
||||
got_r1 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!got_r1 || buf[0] != 0x00)
|
||||
goto abort;
|
||||
if (!sd_spi_read_dma(0xff, data, datalen))
|
||||
goto abort;
|
||||
return true;
|
||||
|
||||
abort:
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sd_cmd_read_complete(void)
|
||||
{
|
||||
uint8_t buf;
|
||||
sd_spi_wait_complete();
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, &buf, 1);
|
||||
return (sd_spi_context.sd_dma_context.read_token_buf == 0xfe);
|
||||
}
|
||||
|
||||
bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
||||
{
|
||||
if (sd_spi_context.initialized)
|
||||
return false;
|
||||
if (!pio_can_add_program(SD_PIO, &sd_spi_pio_program))
|
||||
return false;
|
||||
sd_spi_context.spi_sm = pio_claim_unused_sm(SD_PIO, false);
|
||||
if (sd_spi_context.spi_sm == -1)
|
||||
return false;
|
||||
sd_spi_context.spi_offset = pio_add_program(SD_PIO, &sd_spi_pio_program);
|
||||
sd_spi_context.mosi = mosi;
|
||||
sd_spi_context.miso = miso;
|
||||
sd_spi_context.sck = sck;
|
||||
sd_spi_context.ss = ss;
|
||||
sd_spi_pio_program_init(SD_PIO, sd_spi_context.spi_sm, sd_spi_context.spi_offset, sd_spi_context.mosi,
|
||||
sd_spi_context.miso, sd_spi_context.sck, SD_INIT_BITRATE);
|
||||
pio_sm_set_enabled(SD_PIO, sd_spi_context.spi_sm, true);
|
||||
gpio_init(sd_spi_context.ss);
|
||||
gpio_set_dir(sd_spi_context.ss, true);
|
||||
|
||||
sd_spi_context.spi_dma_rd = dma_claim_unused_channel(false);
|
||||
sd_spi_context.spi_dma_rd_crc = dma_claim_unused_channel(false);
|
||||
sd_spi_context.spi_dma_wr = dma_claim_unused_channel(false);
|
||||
if (sd_spi_context.spi_dma_rd == -1 || sd_spi_context.spi_dma_rd_crc == -1 || sd_spi_context.spi_dma_wr == -1)
|
||||
return false;
|
||||
sd_spi_context.spi_dma_rd_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_rd);
|
||||
channel_config_set_read_increment(&sd_spi_context.spi_dma_rd_cfg, false);
|
||||
channel_config_set_write_increment(&sd_spi_context.spi_dma_rd_cfg, true);
|
||||
channel_config_set_dreq(&sd_spi_context.spi_dma_rd_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, false));
|
||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_cfg, DMA_SIZE_8);
|
||||
sd_spi_context.spi_dma_rd_crc_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_rd_crc);
|
||||
channel_config_set_read_increment(&sd_spi_context.spi_dma_rd_crc_cfg, false);
|
||||
channel_config_set_dreq(&sd_spi_context.spi_dma_rd_crc_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, false));
|
||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_crc_cfg, DMA_SIZE_8);
|
||||
sd_spi_context.spi_dma_wr_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_wr);
|
||||
channel_config_set_read_increment(&sd_spi_context.spi_dma_wr_cfg, false);
|
||||
channel_config_set_dreq(&sd_spi_context.spi_dma_wr_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, true));
|
||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_wr_cfg, DMA_SIZE_8);
|
||||
sd_spi_context.sd_dma_context.state = DMA_IDLE;
|
||||
irq_add_shared_handler(DMA_IRQ_0, &sd_spi_dma_isr, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
|
||||
dma_channel_set_irq0_enabled(sd_spi_context.spi_dma_rd, true);
|
||||
dma_channel_set_irq0_enabled(sd_spi_context.spi_dma_rd_crc, true);
|
||||
irq_set_enabled(DMA_IRQ_0, true);
|
||||
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
uint8_t buf[16];
|
||||
memset(buf, 0xff, 16);
|
||||
// Ensure at least 74 SPI clock cycles without CS asserted
|
||||
|
||||
sd_spi_write_blocking(buf, 10);
|
||||
sd_spi_context.initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_spi_deinit(void)
|
||||
{
|
||||
if (!sd_spi_context.initialized)
|
||||
return false;
|
||||
if (sd_spi_context.sd_dma_context.state != DMA_IDLE)
|
||||
return false;
|
||||
dma_channel_set_irq0_enabled(sd_spi_context.spi_dma_rd, false);
|
||||
dma_channel_set_irq0_enabled(sd_spi_context.spi_dma_rd_crc, false);
|
||||
irq_remove_handler(DMA_IRQ_0, &sd_spi_dma_isr);
|
||||
dma_channel_unclaim(sd_spi_context.spi_dma_rd);
|
||||
dma_channel_unclaim(sd_spi_context.spi_dma_rd_crc);
|
||||
dma_channel_unclaim(sd_spi_context.spi_dma_wr);
|
||||
pio_remove_program(SD_PIO, &sd_spi_pio_program, sd_spi_context.spi_offset);
|
||||
pio_sm_unclaim(SD_PIO, sd_spi_context.spi_sm);
|
||||
|
||||
sd_spi_context.initialized = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sd_spi_set_bitrate(const int rate)
|
||||
{
|
||||
pio_sm_set_enabled(SD_PIO, sd_spi_context.spi_sm, false);
|
||||
sd_spi_pio_program_init(SD_PIO, sd_spi_context.spi_sm, sd_spi_context.spi_offset, sd_spi_context.mosi,
|
||||
sd_spi_context.miso, sd_spi_context.sck, rate);
|
||||
pio_sm_set_enabled(SD_PIO, sd_spi_context.spi_sm, true);
|
||||
}
|
||||
27
software/src/rp2_sd/sd_spi.h
Normal file
27
software/src/rp2_sd/sd_spi.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SD_MISO 4
|
||||
#define SD_SCK 2
|
||||
#define SD_MOSI 3
|
||||
#define SD_CS 5
|
||||
|
||||
#define SD_PIO pio0
|
||||
|
||||
#define SD_INIT_BITRATE 400000
|
||||
#define SD_BITRATE 15000000
|
||||
|
||||
bool sd_cmd(const uint8_t cmd, const uint32_t arg, unsigned resplen, uint8_t resp[static resplen]);
|
||||
bool sd_cmd_read(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen]);
|
||||
|
||||
bool sd_spi_init(int mosi, int miso, int sck, int ss);
|
||||
bool sd_spi_deinit(void);
|
||||
void sd_spi_set_bitrate(const int rate);
|
||||
|
||||
void sd_spi_dbg_clk(const int div, const int frac);
|
||||
|
||||
bool sd_cmd_read_start(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen]);
|
||||
bool sd_cmd_read_complete(void);
|
||||
bool sd_cmd_read_is_complete(void);
|
||||
61
software/src/rp2_sd/sd_spi_pio.pio
Normal file
61
software/src/rp2_sd/sd_spi_pio.pio
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
.program sd_spi_pio
|
||||
.side_set 1
|
||||
|
||||
// data - MISO MOSI
|
||||
// sideset - SCK
|
||||
|
||||
// normal SPI
|
||||
normal:
|
||||
.wrap_target
|
||||
out pins, 1 side 0 [1]
|
||||
in pins, 1 side 1 [1]
|
||||
.wrap
|
||||
// Special "wait for token" repeated read SPI
|
||||
wait_loop:
|
||||
pull block side 0 [0]
|
||||
mov osr, x side 0 [0]
|
||||
set y, 8 side 0 [0]
|
||||
read_loop:
|
||||
out pins, 1 side 0 [1]
|
||||
in pins, 1 side 1 [0]
|
||||
jmp y--, read_loop side 1 [0]
|
||||
mov isr, y side 0 [0]
|
||||
jmp x != y, wait_done side 0 [0]
|
||||
set y, 0 side 0 [0]
|
||||
mov y, isr side 0 [0]
|
||||
jmp wait_loop side 0 [0]
|
||||
wait_done:
|
||||
push block side 0 [0]
|
||||
jmp normal side 0 [0]
|
||||
|
||||
% c-sdk {
|
||||
#include "hardware/clocks.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
|
||||
static inline void sd_spi_pio_program_init(PIO pio, uint sm, uint offset, uint mosi, uint miso, uint sck, uint bitrate) {
|
||||
pio_gpio_init(pio, mosi);
|
||||
pio_gpio_init(pio, miso);
|
||||
pio_gpio_init(pio, sck);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, mosi, 1, true);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, miso, 1, false);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, sck, 1, true);
|
||||
|
||||
pio_sm_config c = sd_spi_pio_program_get_default_config(offset);
|
||||
sm_config_set_out_pins(&c, mosi, 1);
|
||||
sm_config_set_in_pins(&c, miso);
|
||||
sm_config_set_sideset_pins(&c, sck);
|
||||
sm_config_set_out_shift(&c, false, true, 8);
|
||||
sm_config_set_in_shift(&c, false, true, 8);
|
||||
|
||||
const unsigned pio_freq = bitrate*4;
|
||||
const float div = clock_get_hz(clk_sys) / (float)pio_freq;
|
||||
// for some reason, small clkdiv values (even integer ones) cause issues
|
||||
sm_config_set_clkdiv(&c, div < 2.5f ? 2.5f : div);
|
||||
pio_sm_init(pio, sm, offset, &c);
|
||||
}
|
||||
%}
|
||||
30
software/src/rp2_sd/sd_util.h
Normal file
30
software/src/rp2_sd/sd_util.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
inline static uint8_t sd_crc7(size_t len, const uint8_t data[const static len])
|
||||
{
|
||||
const uint8_t poly = 0b1001;
|
||||
uint8_t crc = 0;
|
||||
for (size_t pos = 0; pos < len; ++pos) {
|
||||
crc ^= data[pos];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc << 1) ^ ((crc & 0x80) ? (poly << 1) : 0);
|
||||
}
|
||||
}
|
||||
return crc >> 1;
|
||||
}
|
||||
|
||||
/* inline static uint16_t sd_crc16(size_t len, const uint8_t data[const static len]) */
|
||||
/* { */
|
||||
/* const uint16_t poly = 0b1000000100001; */
|
||||
/* uint16_t crc = 0; */
|
||||
/* for (size_t pos = 0; pos < len; ++pos) { */
|
||||
/* crc ^= data[pos] << 8; */
|
||||
/* for (int bit = 0; bit < 8; ++bit) { */
|
||||
/* crc = (crc << 1) ^ ((crc & 0x8000) ? poly : 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
/* return crc; */
|
||||
/* } */
|
||||
@@ -9,11 +9,11 @@ import micropython
|
||||
import os
|
||||
import time
|
||||
from array import array
|
||||
from machine import Pin, SPI
|
||||
from machine import Pin
|
||||
from math import pi, sin, pow
|
||||
from micropython import const
|
||||
from rp2_neopixel import NeoPixel
|
||||
from sdcard import SDCard
|
||||
from rp2_sd import SDCard
|
||||
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
@@ -73,9 +73,8 @@ machine.mem32[0x4001c004 + 8*4] = 0x67
|
||||
|
||||
|
||||
def list_sd():
|
||||
sd_spi = SPI(0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
|
||||
try:
|
||||
sd = SDCard(sd_spi, Pin(5), 25000000)
|
||||
sd = SDCard(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000)
|
||||
except OSError:
|
||||
for i in range(leds):
|
||||
np[i] = (255, 0, 0)
|
||||
@@ -83,9 +82,9 @@ def list_sd():
|
||||
return
|
||||
try:
|
||||
os.mount(sd, '/sd')
|
||||
print(os.listdir('/sd'))
|
||||
except OSError:
|
||||
pass
|
||||
print(os.listdir(b'/sd'))
|
||||
except OSError as ex:
|
||||
print(f"{ex}")
|
||||
|
||||
|
||||
delay_sum = 0
|
||||
|
||||
Reference in New Issue
Block a user