From a4e81c9852e1e173da867c6310b6f4ac99335dd1 Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Sat, 7 Feb 2026 11:42:03 -0800 Subject: [PATCH] SD card (SPI + CD), GPS PPS polarity/diagnostics, wiring docs - Add sd_card component: SPI mode for ESP32-C5, optional CD (GPIO -1 to disable) - Add sdcard console command; CD shift fix for CONFIG_SD_CD_GPIO=-1 - GPS: pps_active_low support, gps pps-test / pps-scan console commands - board_config: ESP32-C5 SD pins (7,8,9,10,26), GPS PPS polarity - doc/SD_CARD_WIRING.md, doc/GPS_WIRING.md: pin diagrams, Y-cable power note - sdkconfig.defaults.esp32c5: SD CD GPIO 26 - php/mcs_fiwi.php: Fi-Wi demo page Co-authored-by: Cursor --- components/app_console/CMakeLists.txt | 3 +- components/app_console/app_console.c | 1 + components/app_console/app_console.h | 1 + components/app_console/cmd_gps.c | 79 ++- components/app_console/cmd_sdcard.c | 168 ++++++ components/gps_sync/gps_sync.c | 109 +++- components/gps_sync/gps_sync.h | 7 + components/sd_card/CMakeLists.txt | 5 + components/sd_card/sd_card.c | 332 ++++++++++++ components/sd_card/sd_card.h | 129 +++++ doc/GPS_WIRING.md | 241 +++++++++ doc/SD_CARD_WIRING.md | 180 +++++++ main/CMakeLists.txt | 2 +- main/Kconfig.projbuild | 15 + main/board_config.h | 12 +- main/main.c | 15 + php/mcs_fiwi.php | 713 ++++++++++++++++++++++++++ sdkconfig.defaults.esp32c5 | 3 + 18 files changed, 2009 insertions(+), 6 deletions(-) create mode 100644 components/app_console/cmd_sdcard.c create mode 100644 components/sd_card/CMakeLists.txt create mode 100644 components/sd_card/sd_card.c create mode 100644 components/sd_card/sd_card.h create mode 100644 doc/GPS_WIRING.md create mode 100644 doc/SD_CARD_WIRING.md create mode 100644 php/mcs_fiwi.php diff --git a/components/app_console/CMakeLists.txt b/components/app_console/CMakeLists.txt index f8aa771..4cabb9e 100644 --- a/components/app_console/CMakeLists.txt +++ b/components/app_console/CMakeLists.txt @@ -8,8 +8,9 @@ idf_component_register( "cmd_gps.c" "cmd_ping.c" "cmd_ip.c" + "cmd_sdcard.c" INCLUDE_DIRS "." - REQUIRES console wifi_cfg + REQUIRES console wifi_cfg sd_card wifi_controller iperf status_led gps_sync esp_wifi esp_netif nvs_flash spi_flash ) diff --git a/components/app_console/app_console.c b/components/app_console/app_console.c index bcf786f..a7bb03d 100644 --- a/components/app_console/app_console.c +++ b/components/app_console/app_console.c @@ -45,4 +45,5 @@ void app_console_register_commands(void) { register_ping_cmd(); register_monitor_cmd(); register_ip_cmd(); + register_sdcard_cmd(); } diff --git a/components/app_console/app_console.h b/components/app_console/app_console.h index 8130ab0..cfa4466 100644 --- a/components/app_console/app_console.h +++ b/components/app_console/app_console.h @@ -51,6 +51,7 @@ void register_gps_cmd(void); void register_ping_cmd(void); void register_monitor_cmd(void); void register_ip_cmd(void); +void register_sdcard_cmd(void); #ifdef __cplusplus } diff --git a/components/app_console/cmd_gps.c b/components/app_console/cmd_gps.c index cbe057a..44ccfab 100644 --- a/components/app_console/cmd_gps.c +++ b/components/app_console/cmd_gps.c @@ -43,6 +43,8 @@ // --- Forward Declarations --- static int gps_do_status(int argc, char **argv); +static int gps_do_pps_test(int argc, char **argv); +static int gps_do_pps_scan(int argc, char **argv); // ============================================================================ // COMMAND: gps (Dispatcher) @@ -52,6 +54,8 @@ static void print_gps_usage(void) { printf("Usage: gps [args]\n"); printf("Subcommands:\n"); printf(" status Show GPS lock status, time, and last NMEA message\n"); + printf(" pps-test Poll PPS GPIO to verify signal (default 5 sec)\n"); + printf(" pps-scan Scan GPIO 1, 6, 25 to find which has PPS signal\n"); printf("\nType 'gps --help' for details.\n"); } @@ -63,6 +67,8 @@ static int cmd_gps(int argc, char **argv) { if (strcmp(argv[1], "status") == 0) return gps_do_status(argc - 1, &argv[1]); if (strcmp(argv[1], "info") == 0) return gps_do_status(argc - 1, &argv[1]); // Alias + if (strcmp(argv[1], "pps-test") == 0) return gps_do_pps_test(argc - 1, &argv[1]); + if (strcmp(argv[1], "pps-scan") == 0) return gps_do_pps_scan(argc - 1, &argv[1]); printf("Unknown subcommand '%s'.\n", argv[1]); print_gps_usage(); @@ -129,6 +135,77 @@ static int gps_do_status(int argc, char **argv) { return 0; } +// ---------------------------------------------------------------------------- +// Sub-command: pps-test +// ---------------------------------------------------------------------------- +static struct { + struct arg_int *duration; + struct arg_lit *help; + struct arg_end *end; +} pps_test_args; + +static int gps_do_pps_test(int argc, char **argv) { + pps_test_args.duration = arg_int0("d", "duration", "", "Seconds to poll (1-30, default 5)"); + pps_test_args.help = arg_lit0("h", "help", "Help"); + pps_test_args.end = arg_end(1); + + int nerrors = arg_parse(argc, argv, (void **)&pps_test_args); + if (nerrors > 0) { + arg_print_errors(stderr, pps_test_args.end, argv[0]); + return 1; + } + + if (pps_test_args.help->count > 0) { + printf("Usage: gps pps-test [-d ]\n"); + printf("Polls the PPS GPIO to verify the signal is reaching the ESP32.\n"); + return 0; + } + + int duration = 5; + if (pps_test_args.duration->count > 0) { + duration = pps_test_args.duration->ival[0]; + } + + gps_pps_diagnostic(duration); + return 0; +} + +// ---------------------------------------------------------------------------- +// Sub-command: pps-scan +// ---------------------------------------------------------------------------- +static struct { + struct arg_int *duration; + struct arg_lit *help; + struct arg_end *end; +} pps_scan_args; + +static int gps_do_pps_scan(int argc, char **argv) { + pps_scan_args.duration = arg_int0("d", "duration", "", "Seconds per pin (1-10, default 3)"); + pps_scan_args.help = arg_lit0("h", "help", "Help"); + pps_scan_args.end = arg_end(1); + + int nerrors = arg_parse(argc, argv, (void **)&pps_scan_args); + if (nerrors > 0) { + arg_print_errors(stderr, pps_scan_args.end, argv[0]); + return 1; + } + + if (pps_scan_args.help->count > 0) { + printf("Usage: gps pps-scan [-d ]\n"); + printf("Scans GPIO 1, 6, 25 to find which pin has the PPS signal (~1 Hz).\n"); + printf("Use this if pps-test shows no edges on the configured pin.\n"); + return 0; + } + + int duration = 3; + if (pps_scan_args.duration->count > 0) { + duration = pps_scan_args.duration->ival[0]; + } + + gps_pps_scan(duration); + return 0; +} + // ---------------------------------------------------------------------------- // Registration // ---------------------------------------------------------------------------- @@ -136,7 +213,7 @@ static int gps_do_status(int argc, char **argv) { void register_gps_cmd(void) { const esp_console_cmd_t cmd = { .command = "gps", - .help = "GPS Tool: status", + .help = "GPS Tool: status, pps-test, pps-scan", .hint = "", .func = &cmd_gps, .argtable = NULL diff --git a/components/app_console/cmd_sdcard.c b/components/app_console/cmd_sdcard.c new file mode 100644 index 0000000..635bb12 --- /dev/null +++ b/components/app_console/cmd_sdcard.c @@ -0,0 +1,168 @@ +/* + * cmd_sdcard.c + * + * Copyright (c) 2025 Umber Networks & Robert McMahon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include "esp_console.h" +#include "argtable3/argtable3.h" +#include "app_console.h" +#include "sd_card.h" + +#define SDCARD_READ_BUF_SIZE 4096 + +static int do_sdcard_status(int argc, char **argv) { + (void)argc; + (void)argv; + + printf("SD Card Status:\n"); + printf(" CD (Card Detect): "); + if (sd_card_cd_available()) { + int level = sd_card_cd_get_level(); + bool inserted = sd_card_cd_is_inserted(); + printf("%s (GPIO=%s)\n", inserted ? "INSERTED" : "REMOVED", + level >= 0 ? (level ? "HIGH" : "LOW") : "?"); + if (!inserted && sd_card_is_ready()) { + printf(" (Card works but CD says REMOVED: wire CD to J1 Pin 12, or menuconfig -> SD Card -> uncheck 'LOW = inserted')\n"); + } + } else { + printf("N/A (pin not configured)\n"); + } + printf(" Mounted: %s\n", sd_card_is_ready() ? "yes" : "no"); + + if (sd_card_is_ready()) { + uint64_t total = 0, free_bytes = 0; + if (sd_card_get_info(&total, &free_bytes) == 0) { + printf(" Total: %.2f MB\n", total / (1024.0 * 1024.0)); + printf(" Free: %.2f MB\n", free_bytes / (1024.0 * 1024.0)); + } + } + return 0; +} + +static int do_sdcard_write(int argc, char **argv) { + if (argc < 3) { + printf("Usage: sdcard write \n"); + return 1; + } + const char *filename = argv[1]; + + if (!sd_card_is_ready()) { + printf("Error: SD card not mounted\n"); + return 1; + } + + /* Join argv[2]..argv[argc-1] with spaces for multi-word text */ + static char text_buf[512]; + size_t off = 0; + for (int i = 2; i < argc && off < sizeof(text_buf) - 2; i++) { + if (i > 2) { + text_buf[off++] = ' '; + } + size_t len = strlen(argv[i]); + if (off + len >= sizeof(text_buf)) { + len = sizeof(text_buf) - off - 1; + } + memcpy(text_buf + off, argv[i], len); + off += len; + } + text_buf[off] = '\0'; + + esp_err_t ret = sd_card_write_file(filename, text_buf, off, false); + if (ret != 0) { + printf("Write failed: %s\n", esp_err_to_name(ret)); + return 1; + } + printf("Wrote %zu bytes to %s\n", off, filename); + return 0; +} + +static int do_sdcard_read(int argc, char **argv) { + if (argc < 2) { + printf("Usage: sdcard read \n"); + return 1; + } + const char *filename = argv[1]; + + if (!sd_card_is_ready()) { + printf("Error: SD card not mounted\n"); + return 1; + } + + if (!sd_card_file_exists(filename)) { + printf("Error: File not found: %s\n", filename); + return 1; + } + + static uint8_t buf[SDCARD_READ_BUF_SIZE]; + size_t bytes_read = 0; + esp_err_t ret = sd_card_read_file(filename, buf, sizeof(buf) - 1, &bytes_read); + if (ret != 0) { + printf("Read failed: %s\n", esp_err_to_name(ret)); + return 1; + } + buf[bytes_read] = '\0'; + printf("Read %zu bytes from %s:\n", bytes_read, filename); + printf("---\n%s\n---\n", (char *)buf); + return 0; +} + +static int cmd_sdcard(int argc, char **argv) { + if (argc < 2) { + printf("Usage: sdcard [args]\n"); + printf(" status - Show CD, mounted, capacity\n"); + printf(" write - Write text to file\n"); + printf(" read - Read and print file\n"); + return 0; + } + if (strcmp(argv[1], "status") == 0) { + return do_sdcard_status(argc - 1, &argv[1]); + } + if (strcmp(argv[1], "write") == 0) { + return do_sdcard_write(argc - 1, &argv[1]); + } + if (strcmp(argv[1], "read") == 0) { + return do_sdcard_read(argc - 1, &argv[1]); + } + printf("Unknown subcommand '%s'\n", argv[1]); + return 1; +} + +void register_sdcard_cmd(void) { + const esp_console_cmd_t cmd = { + .command = "sdcard", + .help = "SD card: status (CD, capacity), write , read ", + .hint = "", + .func = &cmd_sdcard, + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); +} diff --git a/components/gps_sync/gps_sync.c b/components/gps_sync/gps_sync.c index 7ee0ad7..298125a 100644 --- a/components/gps_sync/gps_sync.c +++ b/components/gps_sync/gps_sync.c @@ -226,7 +226,7 @@ void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) { } gpio_config_t io_conf = {}; - io_conf.intr_type = GPIO_INTR_POSEDGE; + io_conf.intr_type = s_cfg.pps_active_low ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; io_conf.pin_bit_mask = (1ULL << s_cfg.pps_pin); io_conf.mode = GPIO_MODE_INPUT; io_conf.pull_up_en = 1; @@ -251,7 +251,8 @@ void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) { xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL); - ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d)", s_cfg.uart_port, s_cfg.pps_pin); + ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d %s)", s_cfg.uart_port, s_cfg.pps_pin, + s_cfg.pps_active_low ? "falling-edge" : "rising-edge"); } gps_timestamp_t gps_get_timestamp(void) { @@ -280,3 +281,107 @@ void gps_get_last_nmea(char *buf, size_t buf_len) { strlcpy(buf, s_last_nmea_msg, buf_len); } } + +void gps_pps_diagnostic(int duration_sec) { + if (duration_sec < 1) duration_sec = 1; + if (duration_sec > 30) duration_sec = 30; + + const int interval_ms = 50; + int samples = (duration_sec * 1000) / interval_ms; + int prev_level = -1; + int rising = 0, falling = 0; + + printf("PPS diagnostic: polling GPIO%d for %d sec (interval %d ms)...\n", + s_cfg.pps_pin, duration_sec, interval_ms); + + for (int i = 0; i < samples; i++) { + int level = gpio_get_level(s_cfg.pps_pin); + if (prev_level >= 0) { + if (level > prev_level) rising++; + else if (level < prev_level) falling++; + } + prev_level = level; + vTaskDelay(pdMS_TO_TICKS(interval_ms)); + } + + int total = rising + falling; + printf(" Samples: %d, Rising edges: %d, Falling edges: %d\n", samples, rising, falling); + /* 1 Hz pulse gives ~2 edges/sec (rising+falling) */ + int lo = 2 * (duration_sec - 1); + int hi = 2 * (duration_sec + 1); + if (total >= lo && total <= hi) { + printf(" Signal: DETECTED (~1 Hz)\n"); + printf(" Try opposite polarity: set GPS_PPS_ACTIVE_LOW=%d in board_config.h\n", + falling > rising ? 0 : 1); + } else if (total == 0) { + printf(" Signal: NOT DETECTED (no edges)\n"); + printf(" Check: PPS wire, GPS PPS pin, UBX-CFG-TP to enable timepulse\n"); + } else { + printf(" Signal: UNCLEAR (%d edges in %ds, expected ~%d)\n", total, duration_sec, 2 * duration_sec); + } +} + +// Candidate PPS pins for ESP32-C5 (J1: 6=GPIO1, 7=GPIO6, 13=GPIO25) +static const int s_pps_scan_pins[] = { 1, 6, 25 }; +#define PPS_SCAN_PIN_COUNT (sizeof(s_pps_scan_pins) / sizeof(s_pps_scan_pins[0])) + +static void poll_pin_edges(int pin, int duration_sec, int interval_ms, + int *rising, int *falling) { + gpio_config_t io = { + .pin_bit_mask = (1ULL << pin), + .mode = GPIO_MODE_INPUT, + .pull_up_en = 1, + .pull_down_en = 0, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&io); + + int samples = (duration_sec * 1000) / interval_ms; + int prev = -1; + *rising = *falling = 0; + for (int i = 0; i < samples; i++) { + int lvl = gpio_get_level(pin); + if (prev >= 0) { + if (lvl > prev) (*rising)++; + else if (lvl < prev) (*falling)++; + } + prev = lvl; + vTaskDelay(pdMS_TO_TICKS(interval_ms)); + } +} + +void gps_pps_scan(int duration_sec) { + if (duration_sec < 1) duration_sec = 1; + if (duration_sec > 10) duration_sec = 10; + const int interval_ms = 50; + + printf("PPS scan: polling GPIO 1, 6, 25 for %d sec each...\n", duration_sec); + printf("(J1 Pin 6=GPIO1, Pin 7=GPIO6, Pin 13=GPIO25)\n\n"); + + for (size_t i = 0; i < PPS_SCAN_PIN_COUNT; i++) { + int pin = s_pps_scan_pins[i]; + int rising, falling; + poll_pin_edges(pin, duration_sec, interval_ms, &rising, &falling); + int total = rising + falling; + /* 1 Hz pulse gives ~2 edges/sec (rising+falling) */ + int lo = 2 * (duration_sec - 1); + int hi = 2 * (duration_sec + 1); + + const char *result; + if (total >= lo && total <= hi) { + result = "DETECTED (~1 Hz)"; + } else if (total == 0) { + result = "no signal"; + } else { + result = "unclear"; + } + + printf(" GPIO%-2d (J1 P%2d): rising=%d falling=%d -> %s\n", + pin, (pin == 1) ? 6 : (pin == 6) ? 7 : 13, rising, falling, result); + + if (total >= lo && total <= hi) { + printf(" Set GPS_PPS_PIN to GPIO_NUM_%d in board_config.h\n", pin); + printf(" Polarity: set GPS_PPS_ACTIVE_LOW=%d\n", falling > rising ? 1 : 0); + } + } +} diff --git a/components/gps_sync/gps_sync.h b/components/gps_sync/gps_sync.h index 972cd01..75da623 100644 --- a/components/gps_sync/gps_sync.h +++ b/components/gps_sync/gps_sync.h @@ -49,6 +49,7 @@ typedef struct { gpio_num_t tx_pin; gpio_num_t rx_pin; gpio_num_t pps_pin; + bool pps_active_low; // true = trigger on falling edge (common for u-blox GT-U7/NEO-6M) } gps_sync_config_t; // --- Timestamp Struct --- @@ -71,6 +72,12 @@ int64_t gps_get_pps_age_ms(void); // Copies the last received NMEA line into buffer (Diagnostic) void gps_get_last_nmea(char *buf, size_t buf_len); +// PPS diagnostic: poll GPIO for duration_sec, count edges (call from console) +void gps_pps_diagnostic(int duration_sec); + +// PPS scan: poll multiple candidate pins to find which has ~1 Hz signal +void gps_pps_scan(int duration_sec); + #ifdef __cplusplus } #endif diff --git a/components/sd_card/CMakeLists.txt b/components/sd_card/CMakeLists.txt new file mode 100644 index 0000000..fd5c373 --- /dev/null +++ b/components/sd_card/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "sd_card.c" + INCLUDE_DIRS "." + REQUIRES driver fatfs esp_driver_sdspi sdmmc +) diff --git a/components/sd_card/sd_card.c b/components/sd_card/sd_card.c new file mode 100644 index 0000000..2d3d1e1 --- /dev/null +++ b/components/sd_card/sd_card.c @@ -0,0 +1,332 @@ +/* + * sd_card.c + * + * Copyright (c) 2025 Umber Networks & Robert McMahon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "sd_card.h" +#include "esp_log.h" +#include "driver/gpio.h" +#include "sdkconfig.h" +#include "esp_vfs_fat.h" +#include "ff.h" +#include "sdmmc_cmd.h" +#include +#include +#include +#include +#include + +// Pin definitions for SparkFun microSD Transflash Breakout +// ESP32-C5: no SDMMC host, use SD SPI mode +// SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional) +// CONFIG_SD_CD_GPIO / CONFIG_SD_CD_ACTIVE_LOW from Kconfig (main/Kconfig.projbuild) +// CONFIG_SD_CD_ACTIVE_LOW is only defined when y; when n it is omitted from sdkconfig.h +#ifndef CONFIG_SD_CD_GPIO +#define CONFIG_SD_CD_GPIO (-1) +#endif +#if defined(CONFIG_IDF_TARGET_ESP32C5) + #define SDSPI_CLK_PIN GPIO_NUM_9 + #define SDSPI_MOSI_PIN GPIO_NUM_10 + #define SDSPI_MISO_PIN GPIO_NUM_8 + #define SDSPI_CS_PIN GPIO_NUM_7 + #define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1)) + #define SDSPI_HOST_ID SPI2_HOST +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + #define SDSPI_CLK_PIN GPIO_NUM_14 + #define SDSPI_MOSI_PIN GPIO_NUM_15 + #define SDSPI_MISO_PIN GPIO_NUM_2 + #define SDSPI_CS_PIN GPIO_NUM_13 + #define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1)) + #define SDSPI_HOST_ID SPI2_HOST +#else + #define SDSPI_CLK_PIN GPIO_NUM_14 + #define SDSPI_MOSI_PIN GPIO_NUM_15 + #define SDSPI_MISO_PIN GPIO_NUM_2 + #define SDSPI_CS_PIN GPIO_NUM_13 + #define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1)) + #define SDSPI_HOST_ID SPI2_HOST +#endif + +#include "driver/sdspi_host.h" +#include "driver/spi_common.h" + +static const char *TAG = "sd_card"; +static bool s_sd_card_mounted = false; +static bool s_cd_configured = false; +static bool s_spi_bus_inited = false; +static sdmmc_card_t *s_card = NULL; +static const char *s_mount_point = "/sdcard"; + +static void sd_card_cd_ensure_configured(void) { + if (s_cd_configured || SD_CD_PIN < 0) { + return; + } + /* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */ + const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu; + gpio_config_t io = { + .pin_bit_mask = (1ULL << cd_pin), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + if (gpio_config(&io) == ESP_OK) { + s_cd_configured = true; + } +} + +esp_err_t sd_card_init(void) { + if (s_sd_card_mounted) { + ESP_LOGW(TAG, "SD card already initialized"); + return ESP_OK; + } + + ESP_LOGI(TAG, "Initializing SD card via SPI..."); + + // Initialize SPI bus (required before sdspi mount) + spi_bus_config_t bus_cfg = { + .mosi_io_num = SDSPI_MOSI_PIN, + .miso_io_num = SDSPI_MISO_PIN, + .sclk_io_num = SDSPI_CLK_PIN, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 4000, + }; + esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err)); + return err; + } + s_spi_bus_inited = true; + + sdmmc_host_t host = SDSPI_HOST_DEFAULT(); + host.slot = SDSPI_HOST_ID; + + sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + slot_config.gpio_cs = SDSPI_CS_PIN; + slot_config.host_id = SDSPI_HOST_ID; + // Do not pass gpio_cd to driver: ESP-IDF expects LOW=inserted and blocks init if CD says no card. + // We use CD only for status (sd_card_cd_is_inserted) with configurable polarity. + slot_config.gpio_cd = SDSPI_SLOT_NO_CD; + slot_config.gpio_wp = SDSPI_SLOT_NO_WP; + + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024 + }; + + err = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card); + + if (err != ESP_OK) { + if (s_spi_bus_inited) { + spi_bus_free(SDSPI_HOST_ID); + s_spi_bus_inited = false; + } + if (err == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount filesystem. " + "If you want the card to be formatted, set format_if_mount_failed = true."); + } else { + ESP_LOGE(TAG, "Failed to initialize the card (%s). " + "Make sure SD card is inserted and wiring is correct.", esp_err_to_name(err)); + } + return err; + } + + sdmmc_card_print_info(stdout, s_card); + s_sd_card_mounted = true; + ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point); + return ESP_OK; +} + +esp_err_t sd_card_deinit(void) { + if (!s_sd_card_mounted) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Unmounting SD card..."); + esp_err_t ret = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card); + if (ret == ESP_OK) { + s_sd_card_mounted = false; + s_card = NULL; + if (s_spi_bus_inited) { + spi_bus_free(SDSPI_HOST_ID); + s_spi_bus_inited = false; + } + ESP_LOGI(TAG, "SD card unmounted successfully"); + } else { + ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(ret)); + } + return ret; +} + +bool sd_card_is_ready(void) { + return s_sd_card_mounted; +} + +bool sd_card_cd_available(void) { + return (SD_CD_PIN >= 0); +} + +bool sd_card_cd_is_inserted(void) { + if (SD_CD_PIN < 0) { + return false; + } + sd_card_cd_ensure_configured(); + int level = gpio_get_level(SD_CD_PIN); +#if defined(CONFIG_SD_CD_ACTIVE_LOW) && !(CONFIG_SD_CD_ACTIVE_LOW) + return (level == 1); /* HIGH = inserted (inverted breakout) */ +#else + return (level == 0); /* LOW = inserted (SparkFun default) */ +#endif +} + +int sd_card_cd_get_level(void) { + if (SD_CD_PIN < 0) { + return -1; + } + sd_card_cd_ensure_configured(); + return gpio_get_level(SD_CD_PIN); +} + +esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) { + if (!s_sd_card_mounted) { + return ESP_ERR_INVALID_STATE; + } + + FATFS *fs; + DWORD fre_clust, fre_sect, tot_sect; + char path[32]; + snprintf(path, sizeof(path), "%s", s_mount_point); + + FRESULT res = f_getfree(path, &fre_clust, &fs); + if (res != FR_OK) { + ESP_LOGE(TAG, "Failed to get free space: %d", res); + return ESP_FAIL; + } + + tot_sect = (fs->n_fatent - 2) * fs->csize; + fre_sect = fre_clust * fs->csize; + + if (total_bytes) { + *total_bytes = (uint64_t)tot_sect * 512; + } + if (free_bytes) { + *free_bytes = (uint64_t)fre_sect * 512; + } + + return ESP_OK; +} + +esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) { + if (!s_sd_card_mounted) { + return ESP_ERR_INVALID_STATE; + } + + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + const char *mode = append ? "a" : "w"; + FILE *f = fopen(full_path, mode); + if (f == NULL) { + ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path); + return ESP_FAIL; + } + + size_t written = fwrite(data, 1, len, f); + fclose(f); + + if (written != len) { + ESP_LOGE(TAG, "Failed to write all data: wrote %zu of %zu bytes", written, len); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Wrote %zu bytes to %s", written, full_path); + return ESP_OK; +} + +esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) { + if (!s_sd_card_mounted) { + return ESP_ERR_INVALID_STATE; + } + + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + FILE *f = fopen(full_path, "r"); + if (f == NULL) { + ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path); + return ESP_FAIL; + } + + size_t read = fread(data, 1, len, f); + fclose(f); + + if (bytes_read) { + *bytes_read = read; + } + + ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path); + return ESP_OK; +} + +bool sd_card_file_exists(const char *filename) { + if (!s_sd_card_mounted) { + return false; + } + + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + struct stat st; + return (stat(full_path, &st) == 0); +} + +esp_err_t sd_card_delete_file(const char *filename) { + if (!s_sd_card_mounted) { + return ESP_ERR_INVALID_STATE; + } + + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + if (unlink(full_path) != 0) { + ESP_LOGE(TAG, "Failed to delete file: %s", full_path); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Deleted file: %s", full_path); + return ESP_OK; +} diff --git a/components/sd_card/sd_card.h b/components/sd_card/sd_card.h new file mode 100644 index 0000000..195b426 --- /dev/null +++ b/components/sd_card/sd_card.h @@ -0,0 +1,129 @@ +/* + * sd_card.h + * + * Copyright (c) 2025 Umber Networks & Robert McMahon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef SD_CARD_H +#define SD_CARD_H + +#include "esp_err.h" +#include +#include + +/** + * @brief Initialize SD card using SDIO interface + * + * @return ESP_OK on success, error code otherwise + */ +esp_err_t sd_card_init(void); + +/** + * @brief Deinitialize SD card + * + * @return ESP_OK on success + */ +esp_err_t sd_card_deinit(void); + +/** + * @brief Check if SD card is mounted and ready + * + * @return true if SD card is ready, false otherwise + */ +bool sd_card_is_ready(void); + +/** + * @brief Check if Card Detect (CD) pin is configured and wired + * + * @return true if CD pin is available, false otherwise + */ +bool sd_card_cd_available(void); + +/** + * @brief Check if a card is physically inserted (CD pin, polarity from Kconfig) + * Only meaningful when sd_card_cd_available() is true. + * + * @return true if CD indicates card inserted, false if removed or N/A + */ +bool sd_card_cd_is_inserted(void); + +/** + * @brief Get raw CD pin level for diagnostics (0=LOW, 1=HIGH, -1=not configured) + */ +int sd_card_cd_get_level(void); + +/** + * @brief Get SD card capacity information + * + * @param total_bytes Output parameter for total capacity in bytes + * @param free_bytes Output parameter for free space in bytes + * @return ESP_OK on success + */ +esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes); + +/** + * @brief Write data to a file on the SD card + * + * @param filename File path (e.g., "/sdcard/telemetry.json") + * @param data Data to write + * @param len Length of data in bytes + * @param append If true, append to file; if false, overwrite + * @return ESP_OK on success + */ +esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append); + +/** + * @brief Read data from a file on the SD card + * + * @param filename File path + * @param data Buffer to read into + * @param len Maximum length to read + * @param bytes_read Output parameter for actual bytes read + * @return ESP_OK on success + */ +esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read); + +/** + * @brief Check if a file exists on the SD card + * + * @param filename File path + * @return true if file exists, false otherwise + */ +bool sd_card_file_exists(const char *filename); + +/** + * @brief Delete a file from the SD card + * + * @param filename File path + * @return ESP_OK on success + */ +esp_err_t sd_card_delete_file(const char *filename); + +#endif // SD_CARD_H diff --git a/doc/GPS_WIRING.md b/doc/GPS_WIRING.md new file mode 100644 index 0000000..e1cf1c2 --- /dev/null +++ b/doc/GPS_WIRING.md @@ -0,0 +1,241 @@ +# GPS Module Wiring Guide + +## Overview + +This guide shows how to connect a GPS module (e.g., MakerFocus GT-U7, NEO-6M) to the ESP32 boards supported by this firmware. The GPS provides NMEA sentences for position/time and an optional PPS (pulse-per-second) signal for precise timing. + +## Supported GPS Modules + +- **MakerFocus GT-U7** (recommended) — 3.3V compatible, NMEA output, PPS, 9600 baud default +- **NEO-6M** — Same pinout and NMEA format + +## Connections Required + +| GPS Pin | ESP32 Side | Function | +|---------|------------|----------| +| VCC | 3.3V | Power (3.3V only) | +| GND | GND | Ground | +| TXD | ESP32 RX | NMEA data from GPS → ESP32 | +| RXD | ESP32 TX | Optional: config/commands to GPS | +| PPS | GPIO input | Pulse-per-second (optional but recommended) | + +**Note:** TX and RX are crossed: GPS TXD → ESP32 RX, GPS RXD → ESP32 TX. + +**Power with SD card:** If you also use the SparkFun microSD breakout, **J1 Pin 1 (3.3V)** and **J1 Pin 15 (GND)** must use **Y-cables**: one branch to the GPS, one to the SD breakout, so both devices are powered from the same 3.3V and GND. See [SD_CARD_WIRING.md](SD_CARD_WIRING.md) for the combined pin diagram. + +--- + +## ESP32-C5 (DevKitC-1) + +### Wiring Table + +| GPS Module Pin | ESP32-C5 GPIO | Header Location | Function | +|----------------|---------------|-----------------|----------| +| **VCC** | 3V3 | J1 Pin 1 (Left) | Power | +| **GND** | GND | J1 Pin 15 (Left) | Ground | +| **TXD** (NMEA out) | GPIO23 (RX) | J3 Pin 5 (Right) | GPS → ESP32 | +| **RXD** (config in) | GPIO24 (TX) | J3 Pin 4 (Right) | ESP32 → GPS (optional) | +| **PPS** | GPIO1 | J1 Pin 6 (Left) | Pulse per second | + +**Note:** GPIO25 (J1 Pin 13) is a strapping pin and can interfere with PPS input; the firmware uses GPIO1 (J1 Pin 6) instead. + +### Quick Wiring (5 wires) + +``` +GT-U7 VCC → J1 Pin 1 (3V3) [use Y-cable if also powering SD breakout] +GT-U7 GND → J1 Pin 15 (GND) [use Y-cable if also powering SD breakout] +GT-U7 TXD → J3 Pin 5 (GPIO23) — ESP32 RX +GT-U7 RXD → J3 Pin 4 (GPIO24) — ESP32 TX +GT-U7 PPS → J1 Pin 6 (GPIO1) +``` + +### Minimal Wiring (4 wires, no config) + +``` +GT-U7 VCC → J1 Pin 1 (3V3) +GT-U7 GND → J1 Pin 15 (GND) +GT-U7 TXD → J3 Pin 5 (GPIO23) +GT-U7 PPS → J1 Pin 6 (GPIO1) +``` + +### Pin Locations (J1 / J3) + +| J1 (Left) | Pin | J3 (Right) | Pin | +|-----------|---|------------|-----| +| 3V3 | 1 | GND | 1 | +| RST | 2 | TX (UART0) | 2 | +| ... | ... | RX (UART0) | 3 | +| ... | ... | GPIO24 | 4 ← GPS RXD | +| ... | ... | GPIO23 | 5 ← GPS TXD | +| GPIO1 | 6 ← PPS | ... | ... | +| ... | ... | ... | ... | +| GPIO25 | 13 | ... | ... (strapping, avoid for PPS) | +| 5V | 14 (do not use) | ... | ... | +| GND | 15 | ... | ... | + +--- + +## ESP32-S3 (DevKitC-1) + +| GPS Module Pin | ESP32-S3 GPIO | Function | +|----------------|---------------|----------| +| VCC | 3V3 | Power | +| GND | GND | Ground | +| TXD | GPIO4 (RX) | GPS → ESP32 | +| RXD | GPIO5 (TX) | ESP32 → GPS (optional) | +| PPS | GPIO6 | Pulse per second | + +### Quick Wiring + +``` +GT-U7 VCC → 3V3 +GT-U7 GND → GND +GT-U7 TXD → GPIO4 (RX) +GT-U7 RXD → GPIO5 (TX) +GT-U7 PPS → GPIO6 +``` + +--- + +## ESP32 (Original / Standard) + +| GPS Module Pin | ESP32 GPIO | Function | +|----------------|------------|----------| +| VCC | 3V3 | Power | +| GND | GND | Ground | +| TXD | GPIO16 (RX) | GPS → ESP32 | +| RXD | GPIO17 (TX) | ESP32 → GPS (optional) | +| PPS | GPIO4 | Pulse per second | + +### Quick Wiring + +``` +GT-U7 VCC → 3V3 +GT-U7 GND → GND +GT-U7 TXD → GPIO16 (RX) +GT-U7 RXD → GPIO17 (TX) +GT-U7 PPS → GPIO4 +``` + +--- + +## PPS Polarity + +The firmware supports both **rising-edge** and **falling-edge** (active-low) PPS. Many u-blox GT-U7/NEO-6M modules use **active-low** PPS (line pulls low for ~100 ms at each second). The default is `GPS_PPS_ACTIVE_LOW=1` in `board_config.h`. If PPS Lock shows NO despite correct wiring, try flipping this (set to 0 for rising-edge). + +## Important Notes + +⚠️ **Warnings:** +- **Use 3.3V only** — Do not connect GPS VCC to 5V; many modules are 3.3V logic. +- **TX ↔ RX crossover** — GPS TXD connects to ESP32 RX; GPS RXD connects to ESP32 TX. +- Avoid strapping pins (e.g., GPIO2, GPIO3 on ESP32-C5) for general I/O. + +## Testing + +After wiring, use the `gps` console command to check NMEA output and fix status. Example: + +``` +gps status +gps nmea +``` + +## Troubleshooting + +### NMEA Valid: NO + +- **TX/RX swapped** — GPS TXD must go to ESP32 RX (GPIO23 on C5). If you swapped them, NMEA won't parse. +- **Wrong pins** — Double-check GPIO numbers. Older guides (e.g. `esp32-c5-gps-sync-guide.html`) use GPIO4/5 for UART; the firmware expects GPIO23/24. +- **No satellite fix** — Move the antenna near a window or outdoors. Cold start can take 1–2 minutes. +- **Power** — Ensure VCC is 3.3V. Some modules draw more current during acquisition. + +### PPS Locked: NO (but NMEA Valid: YES) + +- **Verify signal** — Run `gps pps-test` (or `gps pps-test -d 5`) to poll the PPS GPIO. If no edges are detected, the signal isn't reaching the ESP32. +- **Wrong PPS pin** — The firmware expects PPS on GPIO1 (J1 Pin 6). GPIO25 (Pin 13) is a strapping pin and can interfere; use Pin 6 instead. +- **PPS polarity** — Many u-blox modules use active-low PPS. The default `GPS_PPS_ACTIVE_LOW=1` in `board_config.h` triggers on falling edge. If it still fails, try setting `GPS_PPS_ACTIVE_LOW=0` (rising-edge). +- **PPS not enabled** — Some modules need UBX configuration to output PPS. GT-U7 usually outputs it by default once a fix is acquired. +- **Loose connection** — PPS is a 1 Hz signal; a bad connection will cause missed edges. + +### No Time / No Fix + +- **Antenna** — Active antennas need power; ensure the module’s antenna connector matches. Outdoor or window placement helps. +- **Startup delay** — Allow 1–2 minutes for cold start. Check `gps nmea` for $GPGGA with "A" (valid) vs "V" (invalid). + +### Documentation Mismatch + +If you followed `doc/esp32-c5-gps-sync-guide.html`, note its pinout differs from the current firmware: + +| Signal | Old guide | Current firmware | +|--------|-----------|------------------| +| RX (GPS TXD) | GPIO4, J3 Pin 8 | GPIO23, J3 Pin 5 | +| TX (GPS RXD) | GPIO5, J3 Pin 9 | GPIO24, J3 Pin 4 | +| PPS | GPIO1, J1 Pin 6 | GPIO1, J1 Pin 6 ✓ (same) | + +Use the pinout in this document for compatibility with the firmware. + +## Configuration Source + +Pin assignments are defined in `main/board_config.h`. To change pins for a custom board, edit the appropriate `#elif defined (CONFIG_IDF_TARGET_...)` block. + +--- + +## Complete Pin Reference (ESP32-C5) + +The following table shows all pins for both the SparkFun microSD breakout and the GPS module (MakerFocus GT-U7 / NEO-6M), and how they map to the ESP32-C5 DevKit. + +### SparkFun microSD Transflash Breakout — All Pins + +| SparkFun Pin | Label | ESP32-C5 | J1 Pin | Function | +|--------------|-------|----------|--------|----------| +| 1 | VCC | 3V3 | 1 | Power (3.3V only) | +| 2 | GND | GND | 15 | Ground | +| 3 | SCK | GPIO9 | 10 | SPI clock | +| 4 | MOSI / DI | GPIO10 | 11 | Data in to card | +| 5 | MISO / DO | GPIO8 | 9 | Data out from card | +| 6 | CS | GPIO7 | 8 | Chip select | +| 7 | CD | GPIO26 | 12 | Card detect (LOW = inserted) | + +**Note:** Pin numbering may vary by SparkFun breakout revision. Some boards use DI/DO labels; DI = MOSI, DO = MISO. + +### GPS Module (GT-U7 / NEO-6M) — All Pins + +| GPS Pin | Label | ESP32-C5 | J1/J3 | Function | +|---------|-------|----------|-------|----------| +| 1 | VCC | 3V3 | J1 Pin 1 | Power (3.3V only) | +| 2 | GND | GND | J1 Pin 15 | Ground | +| 3 | TXD | GPIO23 (RX) | J3 Pin 5 | NMEA data out → ESP32 RX | +| 4 | RXD | GPIO24 (TX) | J3 Pin 4 | Config in ← ESP32 TX (optional) | +| 5 | PPS | GPIO1 | J1 Pin 6 | Pulse per second | + +**Note:** GPS and ESP32 may share VCC/GND with the SD card if powered from the same 3V3 rail. + +### Combined ESP32-C5 J1 Header (Left Side) + +| Pin | Signal | SparkFun | GPS | Notes | +|-----|--------|----------|-----|-------| +| 1 | 3V3 | VCC | VCC | Shared power | +| 2 | RST | — | — | Do not connect | +| 3 | GPIO2 | — | — | Strapping pin | +| 4 | GPIO3 | — | — | Strapping pin | +| 5 | GPIO0 | — | — | Boot mode | +| 6 | GPIO1 | — | PPS | GPS pulse | +| 7 | GPIO6 | — | — | — | +| 8 | GPIO7 | CS | — | SD chip select | +| 9 | GPIO8 | MISO (DO) | — | SD data out | +| 10 | GPIO9 | CLK (SCK) | — | SD clock | +| 11 | GPIO10 | MOSI (DI) | — | SD data in | +| 12 | GPIO26 | CD | — | SD card detect | +| 13 | GPIO25 | — | — | Strapping (avoid for PPS) | +| 14 | 5V | — | — | Do not use | +| 15 | GND | GND | GND | Shared ground | +| 16 | NC | — | — | No connection | + +### Combined ESP32-C5 J3 Header (Right Side) + +| Pin | Signal | SparkFun | GPS | Notes | +|-----|--------|----------|-----|-------| +| 1 | GND | — | — | — | +| 2 | TX (UART0) | — | — | USB bridge | +| 3 | RX (UART0) | — | — | USB bridge | +| 4 | GPIO24 | — | RXD | ESP32 TX → GPS RXD | +| 5 | GPIO23 | — | TXD | GPS TXD → ESP32 RX | diff --git a/doc/SD_CARD_WIRING.md b/doc/SD_CARD_WIRING.md new file mode 100644 index 0000000..1acb0a8 --- /dev/null +++ b/doc/SD_CARD_WIRING.md @@ -0,0 +1,180 @@ +# SparkFun microSD Transflash Breakout - ESP32-C5 Wiring Guide + +## Overview +This guide shows how to connect the SparkFun microSD Transflash Breakout board to the ESP32-C5 DevKit. **ESP32-C5 does not support SDMMC/SDIO host**, so the firmware uses **SPI mode** for SD card access. + +**If you also have a GPS module (e.g. GT-U7):** J1 Pin 1 (3.3V) and Pin 15 (GND) must each use a **Y-cable** (or splice) so one leg goes to the SD breakout and one to the GPS — both devices share the same 3.3V and GND from the ESP32-C5. + +## Required Connections (SPI mode) + +- VCC (3.3V) +- GND (Ground) +- CLK (SPI SCLK) +- DI (SPI MOSI - Data In to card) +- DO (SPI MISO - Data Out from card) +- CS (Chip Select) +- CD (Card Detect, optional) + +## Wiring Table (SPI Mode) + +| SparkFun Label | ESP32-C5 Pin | Header Location | GPIO Number | Function | +|----------------|--------------|-----------------|-------------|----------| +| **VCC** | Pin 1 | J1 (Left Side) | 3V3 | Power | +| **GND** | Pin 15 | J1 (Left Side) | GND | Ground | +| **SCK** | Pin 10 | J1 (Left Side) | GPIO9 | SPI_CLK | +| **DI** | Pin 11 | J1 (Left Side) | GPIO10 | SPI_MOSI | +| **DO** | Pin 9 | J1 (Left Side) | GPIO8 | SPI_MISO | +| **CS** | Pin 8 | J1 (Left Side) | GPIO7 | SPI_CS | +| **CD** | Pin 12 | J1 (Left Side) | GPIO26 | Card Detect (Input Pull-up) | + +Note: If your SparkFun breakout does not expose CS, some breakouts tie CS to GND (always selected). The firmware expects CS on GPIO7. + +## Wiring Diagram (SD only) + +``` + ┌─────────────────────────────┐ ┌──────────────────────────────┐ + │ SparkFun microSD Breakout │ │ ESP32-C5 DevKit (J1) │ + │ │ │ │ + │ VCC ──────────────────────┼─────────┼─► Pin 1 (3V3) │ + │ GND ──────────────────────┼─────────┼─► Pin 15 (GND) │ + │ SCK ──────────────────────┼─────────┼─► Pin 10 (GPIO9) CLK │ + │ DI ──────────────────────┼─────────┼─► Pin 11 (GPIO10) MOSI │ + │ DO ──────────────────────┼─────────┼─► Pin 9 (GPIO8) MISO │ + │ CS ──────────────────────┼─────────┼─► Pin 8 (GPIO7) Chip Select│ + │ CD ──────────────────────┼─────────┼─► Pin 12 (GPIO26) Card Detect│ + │ │ │ (optional) │ + └─────────────────────────────┘ └──────────────────────────────┘ +``` + +## Combined Pin Diagram (SD Card + GPS) + +When using **both** the SparkFun microSD breakout and a GPS module (e.g. GT-U7 / NEO-6M), power and ground are shared: + +- **J1 Pin 1 (3.3V)** and **J1 Pin 15 (GND)** each need a **Y-cable** (or one 3.3V and one GND split): one branch to the SD breakout, one branch to the GPS. Do **not** rely on a single wire to the first device and then daisy‑chain to the second; use a proper Y (or star) so both get a direct connection to 3.3V and GND. + +``` + ESP32-C5 DevKit J1 (left) + J3 (right) + ┌─────────────────────────────────────────────────────────────────────────┐ + │ Pin 1: 3V3 ◄────── Y-cable ──────┬────► SD Breakout VCC │ + │ └────► GPS VCC │ + │ Pin 2: RST (do not connect) │ + │ Pin 3: GPIO2 (strapping – do not use) │ + │ Pin 4: GPIO3 (strapping – do not use) │ + │ Pin 5: GPIO0 │ + │ Pin 6: GPIO1 ◄──────────────────────── GPS PPS (optional) │ + │ Pin 7: GPIO6 │ + │ Pin 8: GPIO7 ◄──────────────────────── SD CS │ + │ Pin 9: GPIO8 ◄──────────────────────── SD MISO (DO) │ + │ Pin 10: GPIO9 ◄──────────────────────── SD CLK (SCK) │ + │ Pin 11: GPIO10 ◄──────────────────────── SD MOSI (DI) │ + │ Pin 12: GPIO26 ◄──────────────────────── SD CD (optional) │ + │ Pin 13: GPIO25 ◄──────────────────────── GPS PPS (alt; if not using 6) │ + │ Pin 14: 5V (do NOT use – would damage SD/GPS) │ + │ Pin 15: GND ◄────── Y-cable ──────┬────► SD Breakout GND │ + │ └────► GPS GND │ + └─────────────────────────────────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────────────────────────┐ + │ J3 (right): Pin 4 = GPIO24 (TX) ◄────── GPS RXD (optional) │ + │ Pin 5 = GPIO23 (RX) ◄────── GPS TXD (NMEA from GPS) │ + └─────────────────────────────────────────────────────────────────────────┘ +``` + +**Summary:** Use Y-cables (or equivalent) on **3.3V (Pin 1)** and **GND (Pin 15)** so the SD card and GPS each have their own lead from the ESP32-C5. Same 3.3V and GND rail, two physical wires per net. + +## Quick Wiring Reference + +``` +SparkFun microSD Breakout → ESP32-C5 DevKit + +1. VCC → J1 Pin 1 (3V3) [Left side, top; use Y-cable if also powering GPS] +2. GND → J1 Pin 15 (GND) [Left side, bottom; use Y-cable if also powering GPS] +3. SCK → J1 Pin 10 (GPIO9) SPI_CLK +4. DI → J1 Pin 11 (GPIO10) SPI_MOSI +5. DO → J1 Pin 9 (GPIO8) SPI_MISO +6. CS → J1 Pin 8 (GPIO7) SPI_CS +7. CD → J1 Pin 12 (GPIO26) Card Detect (optional) +``` + +## Card Detect (CD) Logic + +- **LOW (0V):** Card is **Inserted** (default: `CONFIG_SD_CD_ACTIVE_LOW=y`) +- **HIGH (3.3V):** Card is **Removed** (pin floats high with pull-up) + +If your breakout uses inverted logic (HIGH = inserted), run `idf.py menuconfig` → **ESP32 iperf Configuration** → **SD Card** → uncheck **Card Detect: LOW = inserted**. + +**Breakout doesn’t support CD:** Some SparkFun-style breakouts have a CD pad or through-hole that is **not** connected to the slot’s card-detect switch (the slot may not have a CD pin). If wiring is correct but status always shows REMOVED (or CD never changes), disable CD: `idf.py menuconfig` → **ESP32 iperf Configuration** → **SD Card** → set **Card Detect GPIO** to **-1**. The card will still mount and work; only insert/remove detection is disabled. + +## Pin Locations on ESP32-C5 DevKit + +### J1 (LEFT SIDE - Top to Bottom): +``` +Pin 1: 3V3 ← VCC +Pin 2: RST (Reset - do not connect) +Pin 3: GPIO2 (Strapping - do not use) +Pin 4: GPIO3 (Strapping - do not use) +Pin 5: GPIO0 +Pin 6: GPIO1 (GPS PPS - if used) +Pin 7: GPIO6 +Pin 8: GPIO7 ← CS +Pin 9: GPIO8 ← MISO (DO) +Pin 10: GPIO9 ← CLK (SCK) +Pin 11: GPIO10 ← MOSI (DI) +Pin 12: GPIO26 ← CD (optional) +Pin 13: GPIO25 (GPS PPS - if used) +Pin 14: 5V (do not use - too high voltage) +Pin 15: GND ← GND +Pin 16: NC (No connection) +``` + +## Important Notes + +⚠️ **Warnings:** +- **Use 3.3V ONLY** - Do NOT connect to 5V (Pin 14) - this will damage the microSD card! +- ESP32-C5 does not have SDMMC host; SPI mode is required +- CD is optional: LOW = inserted, HIGH = removed (with pull-up) + +## Current Configuration + +The firmware is configured for **SPI mode** using: +- CLK: GPIO9 +- MOSI: GPIO10 +- MISO: GPIO8 +- CS: GPIO7 +- CD: GPIO26 (optional) + +## Testing + +After wiring, the firmware will automatically detect the SD card on boot. You should see: +``` +I (xxx) sd_card: Initializing SD card via SPI... +I (xxx) sd_card: SD card mounted successfully at /sdcard +I (xxx) sd_card: SD card ready: XX.XX MB total, XX.XX MB free +``` + +If the card is not detected, check: +1. All connections are secure +2. Card is properly inserted in the breakout board +3. Card is formatted (FAT32) +4. Power supply is stable (3.3V) + +## Troubleshooting + +**Card not detected:** +- Verify VCC is connected to 3V3 (Pin 1), NOT 5V +- Check GND connection +- Ensure card is properly seated in the breakout board +- Try a different microSD card + +**Initialization fails:** +- Card may need formatting (FAT32) +- Check for loose connections +- Verify GPIO pins are correct +- Some cards may not work in SPI mode - try a different brand + +**File system errors:** +- Card may be corrupted - reformat as FAT32 +- Check card capacity (very large cards may have issues) +- Ensure proper power supply + +**CD always shows REMOVED but card works:** +- Your breakout or slot may not support Card Detect (CD pad may be unconnected). Disable CD: menuconfig → ESP32 iperf Configuration → SD Card → Card Detect GPIO = **-1**. diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b69510f..68e1a79 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console) + PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console sd_card) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 4b6ca60..0e192d2 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -45,4 +45,19 @@ menu "ESP32 iperf Configuration" help Netmask for the network. + menu "SD Card" + config SD_CD_GPIO + int "Card Detect GPIO (-1 to disable)" + default 26 if IDF_TARGET_ESP32C5 + default -1 + help + GPIO for SD card detect. -1 to disable. SparkFun breakout on ESP32-C5: GPIO26. + + config SD_CD_ACTIVE_LOW + bool "Card Detect: LOW = inserted" + default y + help + When set (default), LOW on CD pin means card inserted (SparkFun). When unset, HIGH means inserted. + endmenu + endmenu diff --git a/main/board_config.h b/main/board_config.h index 05d7726..77511d8 100644 --- a/main/board_config.h +++ b/main/board_config.h @@ -46,7 +46,14 @@ #define HAS_RGB_LED 1 #define GPS_TX_PIN GPIO_NUM_24 #define GPS_RX_PIN GPIO_NUM_23 - #define GPS_PPS_PIN GPIO_NUM_25 + #define GPS_PPS_PIN GPIO_NUM_25 // J1 Pin 13; PPS works here with SD on 7,8,9,10,26 + #define GPS_PPS_ACTIVE_LOW 0 // rising-edge PPS (use 1 if pps-scan suggests falling > rising) + // SD SPI pins for SparkFun microSD Transflash Breakout (ESP32-C5 has no SDMMC host) + #define SDSPI_CLK_PIN GPIO_NUM_9 + #define SDSPI_MOSI_PIN GPIO_NUM_10 + #define SDSPI_MISO_PIN GPIO_NUM_8 + #define SDSPI_CS_PIN GPIO_NUM_7 + #define SD_CD_PIN GPIO_NUM_26 // Card Detect: LOW=inserted #elif defined (CONFIG_IDF_TARGET_ESP32S3) // ============================================================================ // ESP32-S3 (DevKitC-1) @@ -58,6 +65,7 @@ #define GPS_TX_PIN GPIO_NUM_5 #define GPS_RX_PIN GPIO_NUM_4 #define GPS_PPS_PIN GPIO_NUM_6 + #define GPS_PPS_ACTIVE_LOW 1 #elif defined (CONFIG_IDF_TARGET_ESP32) // ============================================================================ // ESP32 (Original / Standard) @@ -69,6 +77,7 @@ #define GPS_TX_PIN GPIO_NUM_17 #define GPS_RX_PIN GPIO_NUM_16 #define GPS_PPS_PIN GPIO_NUM_4 + #define GPS_PPS_ACTIVE_LOW 1 #else // Fallback #define RGB_LED_GPIO 8 @@ -76,6 +85,7 @@ #define GPS_TX_PIN GPIO_NUM_1 #define GPS_RX_PIN GPIO_NUM_3 #define GPS_PPS_PIN GPIO_NUM_5 + #define GPS_PPS_ACTIVE_LOW 1 #endif #endif // BOARD_CONFIG_H diff --git a/main/main.c b/main/main.c index 8ed5ac6..fc0d81e 100644 --- a/main/main.c +++ b/main/main.c @@ -58,6 +58,7 @@ #include "app_console.h" #include "iperf.h" +#include "sd_card.h" #ifdef CONFIG_ESP_WIFI_CSI_ENABLED #include "csi_log.h" #include "csi_manager.h" @@ -168,6 +169,7 @@ void app_main(void) { .tx_pin = GPS_TX_PIN, .rx_pin = GPS_RX_PIN, .pps_pin = GPS_PPS_PIN, + .pps_active_low = GPS_PPS_ACTIVE_LOW, }; gps_sync_init(&gps_cfg, true); } else { @@ -177,6 +179,19 @@ void app_main(void) { // Hardware Init status_led_init(RGB_LED_GPIO, HAS_RGB_LED); status_led_set_state(LED_STATE_FAILED); // Force Red Blink + + // Initialize SD card (optional - will fail gracefully if not present) + esp_err_t sd_ret = sd_card_init(); + if (sd_ret == ESP_OK) { + uint64_t total_bytes = 0, free_bytes = 0; + if (sd_card_get_info(&total_bytes, &free_bytes) == ESP_OK) { + ESP_LOGI(TAG, "SD card ready: %.2f MB total, %.2f MB free", + total_bytes / (1024.0 * 1024.0), free_bytes / (1024.0 * 1024.0)); + } + } else { + ESP_LOGW(TAG, "SD card initialization failed (card may not be present): %s", esp_err_to_name(sd_ret)); + } + #ifdef CONFIG_ESP_WIFI_CSI_ENABLED ESP_ERROR_CHECK(csi_log_init()); csi_mgr_init(); diff --git a/php/mcs_fiwi.php b/php/mcs_fiwi.php new file mode 100644 index 0000000..7c723f9 --- /dev/null +++ b/php/mcs_fiwi.php @@ -0,0 +1,713 @@ + time()]); + + // Keep only last 1000 samples per device + if (count($telemetry[$deviceId]) > 1000) { + $telemetry[$deviceId] = array_slice($telemetry[$deviceId], -1000); + } + + file_put_contents($telemetryFile, json_encode($telemetry, JSON_PRETTY_PRINT)); + echo json_encode(['status' => 'success']); + exit; + } + + echo json_encode(['status' => 'error', 'message' => 'Invalid data']); + exit; +} +?> + + + + + + Umber Fi-Wi: Deterministic Scaling & Mu-MIMO + + + + +

Umber Fi-Wi: Deterministic Scaling & Mu-MIMO

+ +
+ + + + +
+ +
+
+ Throughput + -- + -- +
+
+ P99 Latency + -- +
+
+ Packet Error Rate + -- +
+
+ Collision Prob + -- +
+
+ +
+
+
+
Active Rooms4
+ +
+
+
+
+
Total Clients12
+ +
+
+
+
+
Packet Pressure5000 PPS
+ +
+
+
+
+ Greedy + + L4S +
+
Rate Control
+
+
+
+
Aggregation (MPDU)12
+ +
+
+
+
+ + + +
+
+
+ +
+ ... +
+ +
+ + + + + + diff --git a/sdkconfig.defaults.esp32c5 b/sdkconfig.defaults.esp32c5 index 32d6077..501bf87 100644 --- a/sdkconfig.defaults.esp32c5 +++ b/sdkconfig.defaults.esp32c5 @@ -7,3 +7,6 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 CONFIG_ESP_WIFI_RX_BA_WIN=16 CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +# SD Card Detect: GPIO26, LOW=inserted (SparkFun breakout) +CONFIG_SD_CD_GPIO=26 +CONFIG_SD_CD_ACTIVE_LOW=y