Safety, testing Exercise 21 README internal linking on Forgejo
This commit is contained in:
parent
e5d30469bc
commit
1d0a29f2a3
18 changed files with 2098 additions and 1 deletions
|
|
@ -1,5 +1,7 @@
|
|||
## Exercise 06: RTC Check (PCF8563)
|
||||
|
||||
Time is always Greenwich Mean Time ("Zulu")
|
||||
|
||||
This exercise validates RTC read/write and power-off persistence on the T-Beam Supreme.
|
||||
|
||||
It:
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ void StartupSdManager::permissionsDemo(const char* path) {
|
|||
|
||||
void StartupSdManager::setStateMounted() {
|
||||
if (watchState_ != SdWatchState::MOUNTED) {
|
||||
dumpSdPins("mounted");
|
||||
logf("EVENT: card inserted/mounted");
|
||||
mountedEventPending_ = true;
|
||||
notify(SdEvent::CARD_MOUNTED, "SD card mounted");
|
||||
|
|
@ -348,10 +349,12 @@ void StartupSdManager::setStateMounted() {
|
|||
|
||||
void StartupSdManager::setStateAbsent() {
|
||||
if (watchState_ == SdWatchState::MOUNTED) {
|
||||
dumpSdPins("removed");
|
||||
logf("EVENT: card removed/unavailable");
|
||||
removedEventPending_ = true;
|
||||
notify(SdEvent::CARD_REMOVED, "SD card removed");
|
||||
} else if (watchState_ != SdWatchState::ABSENT) {
|
||||
dumpSdPins("absent");
|
||||
logf("EVENT: no card detected");
|
||||
notify(SdEvent::NO_CARD, "Missing SD card or invalid FAT16/FAT32 format");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@
|
|||
|
||||
static const uint32_t kSerialDelayMs = 5000;
|
||||
static const uint32_t kLoopMsDiscipline = 60000;
|
||||
static const uint32_t kNoTimeDelayMs = 30000;
|
||||
static const uint32_t kNoTimeDelayMs = 12000;
|
||||
static const uint32_t kGpsStartupProbeMs = 20000;
|
||||
static const uint32_t kPpsWaitTimeoutMs = 1500;
|
||||
static const bool kTemporaryAllowSdLogWithoutGps = true;
|
||||
static const uint32_t kGpsDisciplineGraceMs = 12000;
|
||||
|
||||
static XPowersLibInterface* g_pmu = nullptr;
|
||||
static StartupSdManager g_sd(Serial);
|
||||
|
|
@ -55,6 +57,7 @@ static HardwareSerial g_gpsSerial(1);
|
|||
|
||||
static uint32_t g_logSeq = 0;
|
||||
static uint32_t g_nextDisciplineMs = 0;
|
||||
static uint32_t g_bootMs = 0;
|
||||
static bool g_gpsPathReady = false;
|
||||
|
||||
static char g_gpsLine[128];
|
||||
|
|
@ -646,6 +649,37 @@ static void waitWithUpdates(uint32_t delayMs) {
|
|||
}
|
||||
}
|
||||
|
||||
static bool appendFallbackLogNoGps(const char* reason) {
|
||||
logf("Attempting write to SD Card");
|
||||
if (!ensureGpsLogPathReady()) {
|
||||
logf("SD not mounted, skipping fallback append to gps/discipline_rtc.log");
|
||||
return false;
|
||||
}
|
||||
|
||||
File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND);
|
||||
if (!f) {
|
||||
logf("Could not open /gps/discipline_rtc.log for fallback append");
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
snprintf(line,
|
||||
sizeof(line),
|
||||
"NO_GPS_UTC\t millis=%lu\t reason=%s\t this is a test write\t sd_probe_only=1\t fw_epoch=%lu; fw_build_utc=%s",
|
||||
(unsigned long)millis(),
|
||||
reason ? reason : "unknown",
|
||||
(unsigned long)FW_BUILD_EPOCH,
|
||||
FW_BUILD_UTC);
|
||||
size_t wrote = f.println(line);
|
||||
f.close();
|
||||
if (wrote == 0) {
|
||||
logf("Fallback append failed: /gps/discipline_rtc.log");
|
||||
return false;
|
||||
}
|
||||
logf("Write to SD Card successful");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void showNoTimeAndDelay() {
|
||||
uint8_t sats = bestSatelliteCount();
|
||||
char l3[24];
|
||||
|
|
@ -657,6 +691,14 @@ static void showNoTimeAndDelay() {
|
|||
|
||||
static bool disciplineRtcToGps() {
|
||||
if (!gpsUtcIsFresh()) {
|
||||
if (kTemporaryAllowSdLogWithoutGps &&
|
||||
(uint32_t)(millis() - g_bootMs) >= kGpsDisciplineGraceMs) {
|
||||
const bool ok = appendFallbackLogNoGps("gps_utc_unavailable_after_grace");
|
||||
oledShowLines("GPS time unavailable", "SD fallback log only", ok ? "Fallback write ok" : "Fallback write fail");
|
||||
logf("Temporary bypass: GPS UTC unavailable after grace; fallback log %s", ok ? "ok" : "failed");
|
||||
waitWithUpdates(kNoTimeDelayMs);
|
||||
return false;
|
||||
}
|
||||
showNoTimeAndDelay();
|
||||
return false;
|
||||
}
|
||||
|
|
@ -670,6 +712,14 @@ static bool disciplineRtcToGps() {
|
|||
|
||||
DateTime gpsSnap = g_gps.utc;
|
||||
if (!waitForNextPps(kPpsWaitTimeoutMs)) {
|
||||
if (kTemporaryAllowSdLogWithoutGps &&
|
||||
(uint32_t)(millis() - g_bootMs) >= kGpsDisciplineGraceMs) {
|
||||
const bool ok = appendFallbackLogNoGps("pps_missing_after_grace");
|
||||
oledShowLines("GPS 1PPS missing", "SD fallback log only", ok ? "Fallback write ok" : "Fallback write fail");
|
||||
logf("Temporary bypass: 1PPS missing after grace; fallback log %s", ok ? "ok" : "failed");
|
||||
waitWithUpdates(kNoTimeDelayMs);
|
||||
return false;
|
||||
}
|
||||
oledShowLines("GPS 1PPS missing", "RTC NOT disciplined", "Retry in 30 seconds");
|
||||
logf("No 1PPS edge observed within timeout. Waiting 30 seconds.");
|
||||
waitWithUpdates(kNoTimeDelayMs);
|
||||
|
|
@ -726,6 +776,7 @@ static bool disciplineRtcToGps() {
|
|||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(kSerialDelayMs);
|
||||
g_bootMs = millis();
|
||||
|
||||
Serial.println("\r\n==================================================");
|
||||
Serial.println("Exercise 11: Set RTC to GPS with 1PPS discipline");
|
||||
|
|
@ -770,6 +821,7 @@ void loop() {
|
|||
|
||||
if (g_sd.consumeMountedEvent()) {
|
||||
g_gpsPathReady = false;
|
||||
g_sd.printCardInfo();
|
||||
(void)ensureGpsLogPathReady();
|
||||
}
|
||||
if (g_sd.consumeRemovedEvent()) {
|
||||
|
|
|
|||
136
exercises/19_SD_Card_diag/README.md
Normal file
136
exercises/19_SD_Card_diag/README.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Exercise 19: SD Card Diagnostics
|
||||
|
||||
This exercise exists to develop and test a reusable SD card diagnostics library, `sdcard_diag`, under a simple sketch that prints the current card status to the serial console.
|
||||
|
||||
The immediate goal is operational: when a board in the field has an SD-related problem, the serial output from this exercise should help identify which card was installed and what the board could read from it at the time of the problem.
|
||||
|
||||
## What It Reports
|
||||
|
||||
The sketch prints:
|
||||
|
||||
- PMU and SD rail status
|
||||
- SD pin map and pin levels
|
||||
- SPI idle probe results for `HSPI` and `FSPI`
|
||||
- Mount result and working SPI bus/frequency
|
||||
- Card type and capacity
|
||||
- Filesystem total, used, and free space
|
||||
- Root directory summary and a few sample entries
|
||||
- CID-derived identity fields:
|
||||
- `mid`
|
||||
- `manufacturer_guess`
|
||||
- `oem`
|
||||
- `product`
|
||||
- `revision`
|
||||
- `serial`
|
||||
- `date`
|
||||
- `CID raw`
|
||||
|
||||
## Important Certainty Boundary
|
||||
|
||||
The following fields are read from the card and should be treated as authoritative:
|
||||
|
||||
- `mid`
|
||||
- `oem`
|
||||
- `product`
|
||||
- `revision`
|
||||
- `serial`
|
||||
- `date`
|
||||
- `CID raw`
|
||||
|
||||
The `manufacturer_guess` field is not authoritative. It is a best-effort interpretation of `mid` using community-observed mappings. This matters because:
|
||||
|
||||
- the public SD documentation describes the `MID` field, but does not provide a public authoritative `MID -> vendor name` table
|
||||
- counterfeit or reprogrammed cards may present misleading identity data
|
||||
- some real cards may use manufacturer IDs not covered by the current lookup table
|
||||
|
||||
Also note the difference between a retail brand and a CID-reported card identity:
|
||||
|
||||
- the label printed on the card or the Amazon listing may identify a reseller, storefront brand, or private-label marketer
|
||||
- the CID fields identify what the card itself reports electronically
|
||||
- those two may match, but they do not have to match
|
||||
- for troubleshooting, trust the printed `CID:` line and `CID raw:` more than marketplace branding
|
||||
|
||||
For issue work, always keep the full `CID:` line and the `CID raw:` line.
|
||||
|
||||
## Typical Output
|
||||
|
||||
```text
|
||||
CID: mid=0x03 manufacturer_guess=SanDisk oem=SD product=SD32G revision=8.5 serial=0x2CC7ADB4 date=2025-12
|
||||
CID raw: 03 53 44 53 44 33 32 47 85 2C C7 AD B4 01 9C 6B
|
||||
```
|
||||
|
||||
That output usually tells you much more than a retail package label. Even if `manufacturer_guess` is `Unknown`, the remaining fields are still valuable for correlation.
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
pio run -d exercises/19_SD_Card_diag/ -e cy
|
||||
```
|
||||
|
||||
## Upload
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
pio run -d exercises/19_SD_Card_diag/ -e cy -t upload --upload-port /dev/ttytCY
|
||||
```
|
||||
|
||||
## Monitor
|
||||
|
||||
```bash
|
||||
date
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
pio device monitor -b 115200 --port /dev/ttytCY
|
||||
```
|
||||
|
||||
## If `manufacturer_guess` Is `Unknown`
|
||||
|
||||
Use the following process:
|
||||
|
||||
1. Capture the full `CID:` line and `CID raw:` line from the serial log.
|
||||
2. Use `mid`, `oem`, and `product` together when searching. `mid` alone is often not enough.
|
||||
3. Compare the card against the references below.
|
||||
4. If the card still cannot be identified, record the raw CID in the issue so the lookup table in `sdcard_diag` can be extended later.
|
||||
|
||||
Suggested search terms:
|
||||
|
||||
```text
|
||||
SD CID MID 0xNN OID XX product YYYYY
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```text
|
||||
microSD CID 03 53 44 53 44 33 32 47 ...
|
||||
```
|
||||
|
||||
## Reference URLs
|
||||
|
||||
These pages are useful when investigating an unknown or suspicious card.
|
||||
|
||||
Official SD/SD-3C references:
|
||||
|
||||
- SD Association Physical Layer Simplified Specification downloads page:
|
||||
`https://www.sdcard.org/downloads/pls/`
|
||||
- SD Association simplified specification PDF referenced during development:
|
||||
`https://www.sdcard.org/cms/wp-content/themes/sdcard-org/dl.php?f=Part1_Physical_Layer_Simplified_Specification_Ver6.00.pdf`
|
||||
- SD-3C home page:
|
||||
`https://www.sd-3c.com/Default.aspx`
|
||||
|
||||
Community and field-reference pages:
|
||||
|
||||
- IT-SD secure digital card registers:
|
||||
`https://www.it-sd.com/articles/secure-digital-card-registers/`
|
||||
- Camera Memory Speed CID notes:
|
||||
`https://www.cameramemoryspeed.com/sd-memory-card-faq/reading-sd-card-cid-serial-psn-internal-numbers/`
|
||||
- Zero Alpha SD technology explainer:
|
||||
`https://zeroalpha.com.au/services/data-recovery-blog/sd/secure-digital-sd-memory-card-technology-explained`
|
||||
|
||||
## Notes For Issue Assignees
|
||||
|
||||
- Treat `manufacturer_guess` as a clue, not proof.
|
||||
- Treat `CID raw` as evidence.
|
||||
- If the board mounts the card but CID parsing fails, retain the mount and filesystem details anyway.
|
||||
- If a new legitimate `MID` is identified, update the lookup table in `lib/sdcard_diag/SdCardDiag.cpp` and add provenance for the new mapping.
|
||||
531
exercises/19_SD_Card_diag/lib/sdcard_diag/SdCardDiag.cpp
Normal file
531
exercises/19_SD_Card_diag/lib/sdcard_diag/SdCardDiag.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
#include "SdCardDiag.h"
|
||||
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "tbeam_supreme_adapter.h"
|
||||
|
||||
SdCardDiag::SdCardDiag(Print& out) : out_(out) {}
|
||||
|
||||
bool SdCardDiag::begin() {
|
||||
printDivider();
|
||||
logf("sdcard_diag begin");
|
||||
printPinMap();
|
||||
|
||||
pmuReady_ = initPmu();
|
||||
printRailStatus();
|
||||
return pmuReady_;
|
||||
}
|
||||
|
||||
void SdCardDiag::printReport() {
|
||||
printDivider();
|
||||
logf("starting SD card diagnostic cycle");
|
||||
|
||||
forceSpiDeselected();
|
||||
printRailStatus();
|
||||
printPinLevels();
|
||||
|
||||
cycleSdRail();
|
||||
delay(1200);
|
||||
printRailStatus();
|
||||
|
||||
idleProbe(hspi_, "HSPI");
|
||||
idleProbe(fspi_, "FSPI");
|
||||
|
||||
if (!mountCard()) {
|
||||
logf("result: no mountable SD card found");
|
||||
return;
|
||||
}
|
||||
|
||||
printCardDetails();
|
||||
}
|
||||
|
||||
void SdCardDiag::logf(const char* fmt, ...) {
|
||||
char msg[224];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
va_end(args);
|
||||
out_.printf("[%10lu][%06lu] %s\r\n",
|
||||
(unsigned long)millis(),
|
||||
(unsigned long)logSeq_++,
|
||||
msg);
|
||||
}
|
||||
|
||||
void SdCardDiag::printDivider() {
|
||||
out_.println("------------------------------------------------------------");
|
||||
}
|
||||
|
||||
void SdCardDiag::forceSpiDeselected() {
|
||||
pinMode(tbeam_supreme::sdCs(), OUTPUT);
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
pinMode(tbeam_supreme::imuCs(), OUTPUT);
|
||||
digitalWrite(tbeam_supreme::imuCs(), HIGH);
|
||||
}
|
||||
|
||||
bool SdCardDiag::initPmu() {
|
||||
if (!tbeam_supreme::initPmuForPeripherals(pmu_, &out_)) {
|
||||
logf("PMU init failed; SD rail power control unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
logf("PMU init ok");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardDiag::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
|
||||
if (!pmu_) {
|
||||
logf("rail cycle skipped: pmu unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
forceSpiDeselected();
|
||||
SD.end();
|
||||
hspi_.end();
|
||||
fspi_.end();
|
||||
|
||||
pmu_->disablePowerOutput(XPOWERS_BLDO1);
|
||||
delay(offMs);
|
||||
pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
|
||||
pmu_->enablePowerOutput(XPOWERS_BLDO1);
|
||||
delay(onSettleMs);
|
||||
|
||||
logf("cycled SD rail off=%lums settle=%lums",
|
||||
(unsigned long)offMs,
|
||||
(unsigned long)onSettleMs);
|
||||
}
|
||||
|
||||
void SdCardDiag::printRailStatus() {
|
||||
if (!pmu_) {
|
||||
logf("PMU: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const float vbusV = pmu_->getVbusVoltage() / 1000.0f;
|
||||
const float battV = pmu_->getBattVoltage() / 1000.0f;
|
||||
const bool battPresent = pmu_->isBatteryConnect();
|
||||
const bool sdRailOn = pmu_->isPowerChannelEnable(XPOWERS_BLDO1);
|
||||
|
||||
logf("PMU: SD_BLDO1=%s VBUS=%.3fV BATT=%.3fV battery=%s",
|
||||
boolToOnOff(sdRailOn),
|
||||
vbusV,
|
||||
battV,
|
||||
battPresent ? "present" : "absent");
|
||||
}
|
||||
|
||||
void SdCardDiag::printPinMap() {
|
||||
logf("Pins: SD_CS=%d SD_SCK=%d SD_MISO=%d SD_MOSI=%d IMU_CS=%d",
|
||||
tbeam_supreme::sdCs(),
|
||||
tbeam_supreme::sdSck(),
|
||||
tbeam_supreme::sdMiso(),
|
||||
tbeam_supreme::sdMosi(),
|
||||
tbeam_supreme::imuCs());
|
||||
}
|
||||
|
||||
void SdCardDiag::printPinLevels() {
|
||||
logf("Levels: CS=%d SCK=%d MISO=%d MOSI=%d",
|
||||
gpio_get_level((gpio_num_t)tbeam_supreme::sdCs()),
|
||||
gpio_get_level((gpio_num_t)tbeam_supreme::sdSck()),
|
||||
gpio_get_level((gpio_num_t)tbeam_supreme::sdMiso()),
|
||||
gpio_get_level((gpio_num_t)tbeam_supreme::sdMosi()));
|
||||
}
|
||||
|
||||
SdCardDiag::ProbeBytes SdCardDiag::idleProbe(SPIClass& bus, const char* busName) {
|
||||
ProbeBytes out;
|
||||
|
||||
SD.end();
|
||||
bus.end();
|
||||
delay(5);
|
||||
forceSpiDeselected();
|
||||
|
||||
bus.begin(tbeam_supreme::sdSck(),
|
||||
tbeam_supreme::sdMiso(),
|
||||
tbeam_supreme::sdMosi(),
|
||||
tbeam_supreme::sdCs());
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
delay(1);
|
||||
|
||||
for (uint8_t i = 0; i < 8; ++i) {
|
||||
const uint8_t value = bus.transfer(0xFF);
|
||||
out.bytes[i] = value;
|
||||
if (value == 0xFF) out.ffCount++;
|
||||
else if (value == 0x00) out.zeroCount++;
|
||||
else out.otherCount++;
|
||||
}
|
||||
|
||||
logf("probe %s: ff=%u zero=%u other=%u bytes=%02X %02X %02X %02X %02X %02X %02X %02X",
|
||||
busName,
|
||||
(unsigned)out.ffCount,
|
||||
(unsigned)out.zeroCount,
|
||||
(unsigned)out.otherCount,
|
||||
out.bytes[0],
|
||||
out.bytes[1],
|
||||
out.bytes[2],
|
||||
out.bytes[3],
|
||||
out.bytes[4],
|
||||
out.bytes[5],
|
||||
out.bytes[6],
|
||||
out.bytes[7]);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
bool SdCardDiag::tryMount(SPIClass& bus, const char* busName, uint32_t hz) {
|
||||
SD.end();
|
||||
bus.end();
|
||||
delay(5);
|
||||
forceSpiDeselected();
|
||||
|
||||
bus.begin(tbeam_supreme::sdSck(),
|
||||
tbeam_supreme::sdMiso(),
|
||||
tbeam_supreme::sdMosi(),
|
||||
tbeam_supreme::sdCs());
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
delay(1);
|
||||
for (uint8_t i = 0; i < 10; ++i) {
|
||||
bus.transfer(0xFF);
|
||||
}
|
||||
|
||||
const uint32_t startMs = millis();
|
||||
const bool ok = SD.begin(tbeam_supreme::sdCs(), bus, hz);
|
||||
const uint32_t elapsedMs = millis() - startMs;
|
||||
|
||||
if (!ok) {
|
||||
logf("mount fail bus=%s hz=%lu dt=%lums",
|
||||
busName,
|
||||
(unsigned long)hz,
|
||||
(unsigned long)elapsedMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t type = SD.cardType();
|
||||
if (type == CARD_NONE) {
|
||||
SD.end();
|
||||
logf("mount fail bus=%s hz=%lu dt=%lums cardType=NONE",
|
||||
busName,
|
||||
(unsigned long)hz,
|
||||
(unsigned long)elapsedMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
activeSpi_ = &bus;
|
||||
activeBusName_ = busName;
|
||||
activeHz_ = hz;
|
||||
|
||||
logf("mount ok bus=%s hz=%lu dt=%lums type=%s",
|
||||
busName,
|
||||
(unsigned long)hz,
|
||||
(unsigned long)elapsedMs,
|
||||
cardTypeToString(type));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SdCardDiag::mountCard() {
|
||||
activeSpi_ = nullptr;
|
||||
activeBusName_ = "none";
|
||||
activeHz_ = 0;
|
||||
|
||||
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
|
||||
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
|
||||
if (tryMount(hspi_, "HSPI", freqs[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
|
||||
if (tryMount(fspi_, "FSPI", freqs[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SdCardDiag::printCardDetails() {
|
||||
const uint8_t type = SD.cardType();
|
||||
const uint64_t cardBytes = SD.cardSize();
|
||||
const uint64_t totalBytes = SD.totalBytes();
|
||||
const uint64_t usedBytes = SD.usedBytes();
|
||||
|
||||
logf("card: type=%s size=%llu MB fs_total=%llu MB fs_used=%llu MB fs_free=%llu MB",
|
||||
cardTypeToString(type),
|
||||
(unsigned long long)(cardBytes / (1024ULL * 1024ULL)),
|
||||
(unsigned long long)(totalBytes / (1024ULL * 1024ULL)),
|
||||
(unsigned long long)(usedBytes / (1024ULL * 1024ULL)),
|
||||
(unsigned long long)((totalBytes >= usedBytes ? (totalBytes - usedBytes) : 0) / (1024ULL * 1024ULL)));
|
||||
logf("mount: bus=%s hz=%lu",
|
||||
activeBusName_,
|
||||
(unsigned long)activeHz_);
|
||||
printCardIdentity();
|
||||
|
||||
File root = SD.open("/");
|
||||
if (!root || !root.isDirectory()) {
|
||||
logf("root open failed after successful mount");
|
||||
if (root) {
|
||||
root.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DirSummary summary;
|
||||
summarizeDirectory(root, summary, 0);
|
||||
root.close();
|
||||
|
||||
logf("root summary: files=%lu dirs=%lu bytes=%llu max_depth=%u",
|
||||
(unsigned long)summary.fileCount,
|
||||
(unsigned long)summary.dirCount,
|
||||
(unsigned long long)summary.fileBytes,
|
||||
(unsigned)summary.maxDepth);
|
||||
|
||||
root = SD.open("/");
|
||||
if (!root || !root.isDirectory()) {
|
||||
logf("root reopen failed for sample listing");
|
||||
if (root) {
|
||||
root.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
printSampleEntries(root, 8);
|
||||
root.close();
|
||||
}
|
||||
|
||||
bool SdCardDiag::readCardIdentity(CardIdentity& identity) {
|
||||
identity = CardIdentity{};
|
||||
|
||||
if (!activeSpi_) {
|
||||
logf("CID read skipped: no active SPI bus");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t spiHz = (activeHz_ > 0 && activeHz_ < 4000000UL) ? activeHz_ : 4000000UL;
|
||||
uint8_t cmd[6] = {0x40 | 10, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
cmd[5] = (uint8_t)((crc7(cmd, 5) << 1) | 0x01);
|
||||
|
||||
activeSpi_->beginTransaction(SPISettings(spiHz, MSBFIRST, SPI_MODE0));
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
activeSpi_->transfer(0xFF);
|
||||
digitalWrite(tbeam_supreme::sdCs(), LOW);
|
||||
|
||||
if (!waitForSpiByte(0xFF, 250)) {
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
activeSpi_->transfer(0xFF);
|
||||
activeSpi_->endTransaction();
|
||||
logf("CID read failed: card never became ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(cmd); ++i) {
|
||||
activeSpi_->transfer(cmd[i]);
|
||||
}
|
||||
|
||||
uint8_t r1 = 0xFF;
|
||||
uint32_t startMs = millis();
|
||||
while ((millis() - startMs) < 250) {
|
||||
r1 = activeSpi_->transfer(0xFF);
|
||||
if ((r1 & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r1 != 0x00) {
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
activeSpi_->transfer(0xFF);
|
||||
activeSpi_->endTransaction();
|
||||
logf("CID read failed: CMD10 R1=0x%02X", r1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!waitForSpiByte(0xFE, 250)) {
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
activeSpi_->transfer(0xFF);
|
||||
activeSpi_->endTransaction();
|
||||
logf("CID read failed: no data token");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(identity.raw); ++i) {
|
||||
identity.raw[i] = activeSpi_->transfer(0xFF);
|
||||
}
|
||||
activeSpi_->transfer(0xFF);
|
||||
activeSpi_->transfer(0xFF);
|
||||
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
activeSpi_->transfer(0xFF);
|
||||
activeSpi_->endTransaction();
|
||||
|
||||
identity.mid = identity.raw[0];
|
||||
identity.oid[0] = (char)identity.raw[1];
|
||||
identity.oid[1] = (char)identity.raw[2];
|
||||
identity.oid[2] = '\0';
|
||||
memcpy(identity.pnm, &identity.raw[3], 5);
|
||||
identity.pnm[5] = '\0';
|
||||
identity.prvMajor = (identity.raw[8] >> 4) & 0x0F;
|
||||
identity.prvMinor = identity.raw[8] & 0x0F;
|
||||
identity.psn = ((uint32_t)identity.raw[9] << 24) |
|
||||
((uint32_t)identity.raw[10] << 16) |
|
||||
((uint32_t)identity.raw[11] << 8) |
|
||||
(uint32_t)identity.raw[12];
|
||||
const uint16_t mdt = (uint16_t)(((identity.raw[13] & 0x0F) << 8) | identity.raw[14]);
|
||||
identity.mdtYear = 2000 + ((mdt >> 4) & 0xFF);
|
||||
identity.mdtMonth = mdt & 0x0F;
|
||||
identity.valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardDiag::printCardIdentity() {
|
||||
CardIdentity identity;
|
||||
if (!readCardIdentity(identity)) {
|
||||
logf("CID: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
logf("CID: mid=0x%02X manufacturer_guess=%s oem=%s product=%s revision=%u.%u serial=0x%08llX date=%04u-%02u",
|
||||
identity.mid,
|
||||
manufacturerNameFromMid(identity.mid),
|
||||
identity.oid,
|
||||
identity.pnm,
|
||||
(unsigned)identity.prvMajor,
|
||||
(unsigned)identity.prvMinor,
|
||||
(unsigned long long)identity.psn,
|
||||
(unsigned)identity.mdtYear,
|
||||
(unsigned)identity.mdtMonth);
|
||||
logf("CID raw: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
|
||||
identity.raw[0],
|
||||
identity.raw[1],
|
||||
identity.raw[2],
|
||||
identity.raw[3],
|
||||
identity.raw[4],
|
||||
identity.raw[5],
|
||||
identity.raw[6],
|
||||
identity.raw[7],
|
||||
identity.raw[8],
|
||||
identity.raw[9],
|
||||
identity.raw[10],
|
||||
identity.raw[11],
|
||||
identity.raw[12],
|
||||
identity.raw[13],
|
||||
identity.raw[14],
|
||||
identity.raw[15]);
|
||||
}
|
||||
|
||||
void SdCardDiag::summarizeDirectory(File dir, DirSummary& summary, uint8_t depth) {
|
||||
File entry = dir.openNextFile();
|
||||
while (entry) {
|
||||
if (depth > summary.maxDepth) {
|
||||
summary.maxDepth = depth;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
summary.dirCount++;
|
||||
summarizeDirectory(entry, summary, depth + 1);
|
||||
} else {
|
||||
summary.fileCount++;
|
||||
summary.fileBytes += entry.size();
|
||||
if (depth > summary.maxDepth) {
|
||||
summary.maxDepth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
}
|
||||
|
||||
void SdCardDiag::printSampleEntries(File dir, uint8_t maxEntries) {
|
||||
uint8_t index = 0;
|
||||
File entry = dir.openNextFile();
|
||||
if (!entry) {
|
||||
logf("root entries: <empty>");
|
||||
return;
|
||||
}
|
||||
|
||||
while (entry && index < maxEntries) {
|
||||
logf("root[%u]: %s %s %llu bytes",
|
||||
(unsigned)index,
|
||||
entry.isDirectory() ? "DIR " : "FILE",
|
||||
entry.name(),
|
||||
(unsigned long long)entry.size());
|
||||
entry.close();
|
||||
++index;
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
|
||||
if (entry) {
|
||||
entry.close();
|
||||
logf("root entries: showing first %u only", (unsigned)maxEntries);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t SdCardDiag::crc7(const uint8_t* data, size_t len) const {
|
||||
uint8_t crc = 0;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
uint8_t value = data[i];
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
crc <<= 1;
|
||||
if (((value & 0x80) ^ (crc & 0x80)) != 0) {
|
||||
crc ^= 0x09;
|
||||
}
|
||||
value <<= 1;
|
||||
}
|
||||
}
|
||||
return crc & 0x7F;
|
||||
}
|
||||
|
||||
bool SdCardDiag::waitForSpiByte(uint8_t expected, uint32_t timeoutMs) {
|
||||
const uint32_t startMs = millis();
|
||||
while ((millis() - startMs) < timeoutMs) {
|
||||
if (activeSpi_->transfer(0xFF) == expected) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* SdCardDiag::manufacturerNameFromMid(uint8_t mid) const {
|
||||
// Provenance:
|
||||
// - Official SD CID field layout comes from the SD Association simplified spec:
|
||||
// "Part 1 Physical Layer Simplified Specification", CID table section.
|
||||
// MID is an 8-bit manufacturer ID assigned by SD-3C, LLC.
|
||||
// - SD-3C does not publish a public authoritative MID->vendor table.
|
||||
// - The mapping below is therefore best-effort and community-derived from
|
||||
// observed card CIDs, cross-checked 2026-04-13 against:
|
||||
// https://www.it-sd.com/articles/secure-digital-card-registers/
|
||||
// https://www.cameramemoryspeed.com/sd-memory-card-faq/reading-sd-card-cid-serial-psn-internal-numbers/
|
||||
// https://zeroalpha.com.au/services/data-recovery-blog/sd/secure-digital-sd-memory-card-technology-explained
|
||||
// Use the printed MID/OID/PNM/CID raw values as the authoritative log data.
|
||||
switch (mid) {
|
||||
case 0x01: return "Panasonic";
|
||||
case 0x02: return "Toshiba/Kioxia";
|
||||
case 0x03: return "SanDisk";
|
||||
case 0x1B: return "Samsung";
|
||||
case 0x1D: return "ADATA";
|
||||
case 0x1F: return "Kingston";
|
||||
case 0x27: return "Phison";
|
||||
case 0x28: return "Lexar";
|
||||
case 0x31: return "Silicon Power";
|
||||
case 0x41: return "Kingston/ATP";
|
||||
case 0x74: return "Transcend";
|
||||
case 0x76: return "Patriot";
|
||||
case 0x82: return "Sony";
|
||||
case 0x89: return "Delkin";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* SdCardDiag::cardTypeToString(uint8_t type) const {
|
||||
switch (type) {
|
||||
case CARD_MMC:
|
||||
return "MMC";
|
||||
case CARD_SD:
|
||||
return "SDSC";
|
||||
case CARD_SDHC:
|
||||
return "SDHC/SDXC";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char* SdCardDiag::boolToOnOff(bool value) const {
|
||||
return value ? "ON" : "OFF";
|
||||
}
|
||||
74
exercises/19_SD_Card_diag/lib/sdcard_diag/SdCardDiag.h
Normal file
74
exercises/19_SD_Card_diag/lib/sdcard_diag/SdCardDiag.h
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <XPowersLib.h>
|
||||
|
||||
class SdCardDiag {
|
||||
public:
|
||||
explicit SdCardDiag(Print& out = Serial);
|
||||
|
||||
bool begin();
|
||||
void printReport();
|
||||
|
||||
private:
|
||||
struct ProbeBytes {
|
||||
uint8_t ffCount = 0;
|
||||
uint8_t zeroCount = 0;
|
||||
uint8_t otherCount = 0;
|
||||
uint8_t bytes[8] = {0};
|
||||
};
|
||||
|
||||
struct DirSummary {
|
||||
uint32_t fileCount = 0;
|
||||
uint32_t dirCount = 0;
|
||||
uint64_t fileBytes = 0;
|
||||
uint8_t maxDepth = 0;
|
||||
};
|
||||
|
||||
struct CardIdentity {
|
||||
bool valid = false;
|
||||
uint8_t mid = 0;
|
||||
char oid[3] = {0};
|
||||
char pnm[6] = {0};
|
||||
uint8_t prvMajor = 0;
|
||||
uint8_t prvMinor = 0;
|
||||
uint32_t psn = 0;
|
||||
uint16_t mdtYear = 0;
|
||||
uint8_t mdtMonth = 0;
|
||||
uint8_t raw[16] = {0};
|
||||
};
|
||||
|
||||
void logf(const char* fmt, ...);
|
||||
void printDivider();
|
||||
void forceSpiDeselected();
|
||||
bool initPmu();
|
||||
void cycleSdRail(uint32_t offMs = 250, uint32_t onSettleMs = 700);
|
||||
void printRailStatus();
|
||||
void printPinMap();
|
||||
void printPinLevels();
|
||||
ProbeBytes idleProbe(SPIClass& bus, const char* busName);
|
||||
bool tryMount(SPIClass& bus, const char* busName, uint32_t hz);
|
||||
bool mountCard();
|
||||
void printCardDetails();
|
||||
bool readCardIdentity(CardIdentity& identity);
|
||||
void printCardIdentity();
|
||||
void summarizeDirectory(File dir, DirSummary& summary, uint8_t depth);
|
||||
void printSampleEntries(File dir, uint8_t maxEntries);
|
||||
uint8_t crc7(const uint8_t* data, size_t len) const;
|
||||
bool waitForSpiByte(uint8_t expected, uint32_t timeoutMs);
|
||||
const char* manufacturerNameFromMid(uint8_t mid) const;
|
||||
const char* cardTypeToString(uint8_t type) const;
|
||||
const char* boolToOnOff(bool value) const;
|
||||
|
||||
Print& out_;
|
||||
SPIClass hspi_{HSPI};
|
||||
SPIClass fspi_{FSPI};
|
||||
SPIClass* activeSpi_ = nullptr;
|
||||
const char* activeBusName_ = "none";
|
||||
uint32_t activeHz_ = 0;
|
||||
XPowersLibInterface* pmu_ = nullptr;
|
||||
uint32_t logSeq_ = 0;
|
||||
bool pmuReady_ = false;
|
||||
};
|
||||
12
exercises/19_SD_Card_diag/lib/sdcard_diag/library.json
Normal file
12
exercises/19_SD_Card_diag/lib/sdcard_diag/library.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "sdcard_diag",
|
||||
"version": "0.1.0",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "XPowersLib"
|
||||
},
|
||||
{
|
||||
"name": "Wire"
|
||||
}
|
||||
]
|
||||
}
|
||||
58
exercises/19_SD_Card_diag/platformio.ini
Normal file
58
exercises/19_SD_Card_diag/platformio.ini
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
; 20260413 ChatGPT
|
||||
; Exercise 19_SD_Card_diag
|
||||
|
||||
[platformio]
|
||||
default_envs = guy
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = default_8MB.csv
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
Wire
|
||||
lewisxhe/XPowersLib@0.3.3
|
||||
|
||||
build_flags =
|
||||
-I ../../shared/boards
|
||||
-I ../../external/microReticulum_Firmware
|
||||
-D BOARD_MODEL=BOARD_TBEAM_S_V1
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"AMY\"
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"BOB\"
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"CY\"
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"DAN\"
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"ED\"
|
||||
|
||||
[env:guy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"GUY\"
|
||||
41
exercises/19_SD_Card_diag/src/main.cpp
Normal file
41
exercises/19_SD_Card_diag/src/main.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// 20260413 ChatGPT
|
||||
// Exercise 19: SD Card Diagnostics
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "SdCardDiag.h"
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "SDDIAG"
|
||||
#endif
|
||||
|
||||
static const uint32_t kSerialDelayMs = 1500;
|
||||
static const uint32_t kLoopDelayMs = 25;
|
||||
static const uint32_t kReportEveryMs = 10000;
|
||||
|
||||
static SdCardDiag g_diag(Serial);
|
||||
static uint32_t g_lastReportMs = 0;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(kSerialDelayMs);
|
||||
|
||||
Serial.println();
|
||||
Serial.println("============================================================");
|
||||
Serial.printf("Exercise 19 SD Card Diagnostics [%s]\r\n", NODE_LABEL);
|
||||
Serial.println("============================================================");
|
||||
|
||||
g_diag.begin();
|
||||
g_diag.printReport();
|
||||
g_lastReportMs = millis();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const uint32_t now = millis();
|
||||
if ((uint32_t)(now - g_lastReportMs) >= kReportEveryMs) {
|
||||
g_lastReportMs = now;
|
||||
g_diag.printReport();
|
||||
}
|
||||
|
||||
delay(kLoopDelayMs);
|
||||
}
|
||||
3
exercises/20_microphone/README.md
Normal file
3
exercises/20_microphone/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
https://github.com/Xinyuan-LilyGO/LilyGo-LoRa-Series/issues/295#issuecomment-4241577603
|
||||
|
||||
tells us that the Specification as of April13, 2026, incorrectly includes a microphone. There is no microphone. The code here was ChatGPT's Codex trying to find the correct pins, a task that could never be accomplished. This exercise documents the attempts for posterity.
|
||||
453
exercises/20_microphone/lib/mic_recorder/MicrophoneRecorder.cpp
Normal file
453
exercises/20_microphone/lib/mic_recorder/MicrophoneRecorder.cpp
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
#include "MicrophoneRecorder.h"
|
||||
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <driver/i2s.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "tbeam_supreme_adapter.h"
|
||||
|
||||
#ifndef MIC_SAMPLE_RATE
|
||||
#define MIC_SAMPLE_RATE 16000
|
||||
#endif
|
||||
|
||||
#ifndef MIC_BITS_PER_SAMPLE
|
||||
#define MIC_BITS_PER_SAMPLE 16
|
||||
#endif
|
||||
|
||||
#ifndef MIC_DATA_PIN
|
||||
#define MIC_DATA_PIN 38
|
||||
#endif
|
||||
|
||||
#ifndef MIC_CLK_PIN
|
||||
#define MIC_CLK_PIN 39
|
||||
#endif
|
||||
|
||||
#ifndef MIC_SELECT_PIN
|
||||
#define MIC_SELECT_PIN 48
|
||||
#endif
|
||||
|
||||
#ifndef MIC_SELECT_LEFT
|
||||
#define MIC_SELECT_LEFT 1
|
||||
#endif
|
||||
|
||||
#ifndef FW_BUILD_EPOCH
|
||||
#define FW_BUILD_EPOCH 0
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr i2s_port_t kI2sPort = I2S_NUM_0;
|
||||
constexpr uint32_t kChunkBytes = 2048;
|
||||
constexpr uint8_t kWavChannels = 1;
|
||||
constexpr uint32_t kValidEpochFloor = 1700000000UL;
|
||||
|
||||
} // namespace
|
||||
|
||||
MicrophoneRecorder::MicrophoneRecorder(Print& out) : out_(out) {}
|
||||
|
||||
MicrophoneRecorder::~MicrophoneRecorder() {
|
||||
close();
|
||||
deactivateMicrophone();
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::begin() {
|
||||
micCfg_ = {
|
||||
.dataPin = MIC_DATA_PIN,
|
||||
.clkPin = MIC_CLK_PIN,
|
||||
.selectPin = MIC_SELECT_PIN,
|
||||
.selectLeft = (MIC_SELECT_LEFT != 0),
|
||||
};
|
||||
|
||||
forceSpiDeselected();
|
||||
pmuReady_ = initPmu();
|
||||
if (!pmuReady_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cycleSdRail();
|
||||
sdReady_ = mountSdCard();
|
||||
if (!sdReady_) {
|
||||
logf("SD mount failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensureDirectory("/recordings")) {
|
||||
logf("failed to create /recordings");
|
||||
return false;
|
||||
}
|
||||
|
||||
logf("ready: sample_rate=%u bits=%u data=%d clk=%d sel=%d",
|
||||
(unsigned)MIC_SAMPLE_RATE,
|
||||
(unsigned)MIC_BITS_PER_SAMPLE,
|
||||
micCfg_.dataPin,
|
||||
micCfg_.clkPin,
|
||||
micCfg_.selectPin);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::setRuntimeConfig(const MicRuntimeConfig& cfg) {
|
||||
deactivateMicrophone();
|
||||
micCfg_ = cfg;
|
||||
logf("config updated: data=%d clk=%d select=%d left=%d",
|
||||
micCfg_.dataPin,
|
||||
micCfg_.clkPin,
|
||||
micCfg_.selectPin,
|
||||
micCfg_.selectLeft ? 1 : 0);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::activateMicrophone() {
|
||||
if (micActive_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
pinMode(micCfg_.selectPin, OUTPUT);
|
||||
digitalWrite(micCfg_.selectPin, micCfg_.selectLeft ? LOW : HIGH);
|
||||
|
||||
const i2s_config_t config = {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
|
||||
.sample_rate = MIC_SAMPLE_RATE,
|
||||
.bits_per_sample = (i2s_bits_per_sample_t)MIC_BITS_PER_SAMPLE,
|
||||
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = 8,
|
||||
.dma_buf_len = 256,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = false,
|
||||
.fixed_mclk = 0,
|
||||
#if ESP_IDF_VERSION_MAJOR >= 4
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
|
||||
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
|
||||
#endif
|
||||
};
|
||||
|
||||
const i2s_pin_config_t pins = {
|
||||
.bck_io_num = I2S_PIN_NO_CHANGE,
|
||||
.ws_io_num = micCfg_.clkPin,
|
||||
.data_out_num = I2S_PIN_NO_CHANGE,
|
||||
.data_in_num = micCfg_.dataPin,
|
||||
};
|
||||
|
||||
i2s_driver_uninstall(kI2sPort);
|
||||
esp_err_t err = i2s_driver_install(kI2sPort, &config, 0, nullptr);
|
||||
if (err != ESP_OK) {
|
||||
logf("i2s_driver_install failed err=%d", (int)err);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = i2s_set_pin(kI2sPort, &pins);
|
||||
if (err != ESP_OK) {
|
||||
logf("i2s_set_pin failed err=%d", (int)err);
|
||||
i2s_driver_uninstall(kI2sPort);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = i2s_set_pdm_rx_down_sample(kI2sPort, I2S_PDM_DSR_8S);
|
||||
if (err != ESP_OK) {
|
||||
logf("i2s_set_pdm_rx_down_sample failed err=%d", (int)err);
|
||||
i2s_driver_uninstall(kI2sPort);
|
||||
return false;
|
||||
}
|
||||
|
||||
i2s_zero_dma_buffer(kI2sPort);
|
||||
micActive_ = true;
|
||||
logf("microphone active clk(ws)=%d data=%d select=%d left=%d",
|
||||
micCfg_.clkPin,
|
||||
micCfg_.dataPin,
|
||||
micCfg_.selectPin,
|
||||
micCfg_.selectLeft ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::deactivateMicrophone() {
|
||||
if (!micActive_) {
|
||||
return;
|
||||
}
|
||||
|
||||
i2s_driver_uninstall(kI2sPort);
|
||||
micActive_ = false;
|
||||
logf("microphone inactive");
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::open(const char* path) {
|
||||
if (!sdReady_) {
|
||||
logf("open refused: SD not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
close();
|
||||
|
||||
strncpy(currentPath_, path, sizeof(currentPath_) - 1);
|
||||
currentPath_[sizeof(currentPath_) - 1] = '\0';
|
||||
|
||||
file_ = SD.open(currentPath_, FILE_WRITE);
|
||||
if (!file_) {
|
||||
logf("failed to open %s", currentPath_);
|
||||
currentPath_[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
wavDataBytes_ = 0;
|
||||
fileOpen_ = writeWavHeader();
|
||||
if (!fileOpen_) {
|
||||
file_.close();
|
||||
currentPath_[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
logf("opened %s", currentPath_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::openTimestamped() {
|
||||
const uint32_t stamp = selectTimestamp();
|
||||
char path[96];
|
||||
snprintf(path, sizeof(path), "/recordings/mic_%lu.wav", (unsigned long)stamp);
|
||||
return open(path);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::captureForMs(uint32_t durationMs) {
|
||||
if (!fileOpen_) {
|
||||
logf("capture refused: no open file");
|
||||
return false;
|
||||
}
|
||||
if (!activateMicrophone()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t samples[kChunkBytes / sizeof(int16_t)];
|
||||
const uint32_t startMs = millis();
|
||||
uint32_t lastProgressMs = startMs;
|
||||
int16_t secondMin = INT16_MAX;
|
||||
int16_t secondMax = INT16_MIN;
|
||||
uint32_t secondSampleCount = 0;
|
||||
uint64_t secondAbsSum = 0;
|
||||
|
||||
while ((uint32_t)(millis() - startMs) < durationMs) {
|
||||
size_t bytesRead = 0;
|
||||
const esp_err_t err =
|
||||
i2s_read(kI2sPort, samples, sizeof(samples), &bytesRead, pdMS_TO_TICKS(250));
|
||||
if (err != ESP_OK) {
|
||||
logf("i2s_read failed err=%d", (int)err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesRead > 0) {
|
||||
const size_t sampleCount = bytesRead / sizeof(samples[0]);
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
const int16_t value = samples[i];
|
||||
if (value < secondMin) secondMin = value;
|
||||
if (value > secondMax) secondMax = value;
|
||||
secondAbsSum += (uint64_t)(value < 0 ? -(int32_t)value : value);
|
||||
}
|
||||
secondSampleCount += sampleCount;
|
||||
|
||||
const size_t wrote = file_.write((const uint8_t*)samples, bytesRead);
|
||||
if (wrote != bytesRead) {
|
||||
logf("file write failed wanted=%u wrote=%u", (unsigned)bytesRead, (unsigned)wrote);
|
||||
return false;
|
||||
}
|
||||
wavDataBytes_ += wrote;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
if ((uint32_t)(now - lastProgressMs) >= 1000) {
|
||||
lastProgressMs = now;
|
||||
const unsigned long avgAbs =
|
||||
secondSampleCount > 0 ? (unsigned long)(secondAbsSum / secondSampleCount) : 0UL;
|
||||
const char* status =
|
||||
(secondSampleCount == 0) ? "nosamples"
|
||||
: (secondMin == secondMax) ? "flat"
|
||||
: (avgAbs < 8UL) ? "nearzero"
|
||||
: "active";
|
||||
logf("recording %s elapsed=%lus bytes=%lu min=%d max=%d avgabs=%lu samples=%lu status=%s",
|
||||
currentPath_,
|
||||
(unsigned long)((now - startMs) / 1000),
|
||||
(unsigned long)wavDataBytes_,
|
||||
secondSampleCount > 0 ? secondMin : 0,
|
||||
secondSampleCount > 0 ? secondMax : 0,
|
||||
avgAbs,
|
||||
(unsigned long)secondSampleCount,
|
||||
status);
|
||||
secondMin = INT16_MAX;
|
||||
secondMax = INT16_MIN;
|
||||
secondSampleCount = 0;
|
||||
secondAbsSum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
file_.flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::close() {
|
||||
if (!fileOpen_) {
|
||||
return;
|
||||
}
|
||||
|
||||
finalizeWavHeader();
|
||||
file_.close();
|
||||
fileOpen_ = false;
|
||||
logf("closed %s bytes=%lu", currentPath_, (unsigned long)wavDataBytes_);
|
||||
currentPath_[0] = '\0';
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::logf(const char* fmt, ...) {
|
||||
char msg[200];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
out_.printf("[%10lu][%06lu] %s\r\n",
|
||||
(unsigned long)millis(),
|
||||
(unsigned long)logSeq_++,
|
||||
msg);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::initPmu() {
|
||||
if (!tbeam_supreme::initPmuForPeripherals(pmu_, &out_)) {
|
||||
logf("PMU init failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::forceSpiDeselected() {
|
||||
pinMode(tbeam_supreme::sdCs(), OUTPUT);
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
pinMode(tbeam_supreme::imuCs(), OUTPUT);
|
||||
digitalWrite(tbeam_supreme::imuCs(), HIGH);
|
||||
}
|
||||
|
||||
void MicrophoneRecorder::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
|
||||
if (!pmu_) {
|
||||
return;
|
||||
}
|
||||
|
||||
forceSpiDeselected();
|
||||
SD.end();
|
||||
sdSpiH_.end();
|
||||
sdSpiF_.end();
|
||||
|
||||
pmu_->disablePowerOutput(XPOWERS_BLDO1);
|
||||
delay(offMs);
|
||||
pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
|
||||
pmu_->enablePowerOutput(XPOWERS_BLDO1);
|
||||
delay(onSettleMs);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::mountSdCard() {
|
||||
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
|
||||
|
||||
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
|
||||
if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
|
||||
if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz) {
|
||||
SD.end();
|
||||
bus.end();
|
||||
delay(5);
|
||||
forceSpiDeselected();
|
||||
|
||||
bus.begin(tbeam_supreme::sdSck(),
|
||||
tbeam_supreme::sdMiso(),
|
||||
tbeam_supreme::sdMosi(),
|
||||
tbeam_supreme::sdCs());
|
||||
digitalWrite(tbeam_supreme::sdCs(), HIGH);
|
||||
delay(1);
|
||||
for (uint8_t i = 0; i < 10; ++i) {
|
||||
bus.transfer(0xFF);
|
||||
}
|
||||
|
||||
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
|
||||
return false;
|
||||
}
|
||||
if (SD.cardType() == CARD_NONE) {
|
||||
SD.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
activeSdSpi_ = &bus;
|
||||
logf("SD mounted via %s @ %lu Hz", busName, (unsigned long)hz);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::ensureDirectory(const char* path) {
|
||||
if (SD.exists(path)) {
|
||||
return true;
|
||||
}
|
||||
return SD.mkdir(path);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::writeWavHeader() {
|
||||
const WavHeader header = {
|
||||
.riff = {'R', 'I', 'F', 'F'},
|
||||
.chunkSize = 36,
|
||||
.wave = {'W', 'A', 'V', 'E'},
|
||||
.fmt = {'f', 'm', 't', ' '},
|
||||
.subchunk1Size = 16,
|
||||
.audioFormat = 1,
|
||||
.numChannels = kWavChannels,
|
||||
.sampleRate = MIC_SAMPLE_RATE,
|
||||
.byteRate = MIC_SAMPLE_RATE * kWavChannels * (MIC_BITS_PER_SAMPLE / 8),
|
||||
.blockAlign = (uint16_t)(kWavChannels * (MIC_BITS_PER_SAMPLE / 8)),
|
||||
.bitsPerSample = MIC_BITS_PER_SAMPLE,
|
||||
.data = {'d', 'a', 't', 'a'},
|
||||
.dataSize = 0,
|
||||
};
|
||||
|
||||
return file_.write((const uint8_t*)&header, sizeof(header)) == sizeof(header);
|
||||
}
|
||||
|
||||
bool MicrophoneRecorder::finalizeWavHeader() {
|
||||
if (!file_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader header = {
|
||||
.riff = {'R', 'I', 'F', 'F'},
|
||||
.chunkSize = 36 + wavDataBytes_,
|
||||
.wave = {'W', 'A', 'V', 'E'},
|
||||
.fmt = {'f', 'm', 't', ' '},
|
||||
.subchunk1Size = 16,
|
||||
.audioFormat = 1,
|
||||
.numChannels = kWavChannels,
|
||||
.sampleRate = MIC_SAMPLE_RATE,
|
||||
.byteRate = MIC_SAMPLE_RATE * kWavChannels * (MIC_BITS_PER_SAMPLE / 8),
|
||||
.blockAlign = (uint16_t)(kWavChannels * (MIC_BITS_PER_SAMPLE / 8)),
|
||||
.bitsPerSample = MIC_BITS_PER_SAMPLE,
|
||||
.data = {'d', 'a', 't', 'a'},
|
||||
.dataSize = wavDataBytes_,
|
||||
};
|
||||
|
||||
file_.seek(0);
|
||||
const bool ok = file_.write((const uint8_t*)&header, sizeof(header)) == sizeof(header);
|
||||
file_.seek(file_.size());
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint32_t MicrophoneRecorder::selectTimestamp() const {
|
||||
const time_t now = time(nullptr);
|
||||
if (now >= (time_t)kValidEpochFloor) {
|
||||
return (uint32_t)now;
|
||||
}
|
||||
if (FW_BUILD_EPOCH >= kValidEpochFloor) {
|
||||
return (uint32_t)FW_BUILD_EPOCH + (millis() / 1000UL);
|
||||
}
|
||||
return millis() / 1000UL;
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <XPowersLib.h>
|
||||
|
||||
class MicrophoneRecorder {
|
||||
public:
|
||||
struct MicRuntimeConfig {
|
||||
int dataPin;
|
||||
int clkPin;
|
||||
int selectPin;
|
||||
bool selectLeft;
|
||||
};
|
||||
|
||||
explicit MicrophoneRecorder(Print& out = Serial);
|
||||
~MicrophoneRecorder();
|
||||
|
||||
bool begin();
|
||||
void setRuntimeConfig(const MicRuntimeConfig& cfg);
|
||||
const MicRuntimeConfig& runtimeConfig() const { return micCfg_; }
|
||||
bool activateMicrophone();
|
||||
void deactivateMicrophone();
|
||||
|
||||
bool open(const char* path);
|
||||
bool openTimestamped();
|
||||
bool captureForMs(uint32_t durationMs);
|
||||
void close();
|
||||
|
||||
bool isSdReady() const { return sdReady_; }
|
||||
bool isMicActive() const { return micActive_; }
|
||||
bool isOpen() const { return fileOpen_; }
|
||||
const char* currentPath() const { return currentPath_; }
|
||||
|
||||
private:
|
||||
struct WavHeader {
|
||||
char riff[4];
|
||||
uint32_t chunkSize;
|
||||
char wave[4];
|
||||
char fmt[4];
|
||||
uint32_t subchunk1Size;
|
||||
uint16_t audioFormat;
|
||||
uint16_t numChannels;
|
||||
uint32_t sampleRate;
|
||||
uint32_t byteRate;
|
||||
uint16_t blockAlign;
|
||||
uint16_t bitsPerSample;
|
||||
char data[4];
|
||||
uint32_t dataSize;
|
||||
};
|
||||
|
||||
void logf(const char* fmt, ...);
|
||||
bool initPmu();
|
||||
void forceSpiDeselected();
|
||||
void cycleSdRail(uint32_t offMs = 250, uint32_t onSettleMs = 700);
|
||||
bool mountSdCard();
|
||||
bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz);
|
||||
bool ensureDirectory(const char* path);
|
||||
bool writeWavHeader();
|
||||
bool finalizeWavHeader();
|
||||
uint32_t selectTimestamp() const;
|
||||
|
||||
Print& out_;
|
||||
SPIClass sdSpiH_{HSPI};
|
||||
SPIClass sdSpiF_{FSPI};
|
||||
SPIClass* activeSdSpi_ = nullptr;
|
||||
XPowersLibInterface* pmu_ = nullptr;
|
||||
File file_;
|
||||
char currentPath_[96] = {0};
|
||||
uint32_t logSeq_ = 0;
|
||||
uint32_t wavDataBytes_ = 0;
|
||||
bool pmuReady_ = false;
|
||||
bool sdReady_ = false;
|
||||
bool micActive_ = false;
|
||||
bool fileOpen_ = false;
|
||||
MicRuntimeConfig micCfg_{};
|
||||
};
|
||||
7
exercises/20_microphone/lib/mic_recorder/library.json
Normal file
7
exercises/20_microphone/lib/mic_recorder/library.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "mic_recorder",
|
||||
"version": "0.1.0",
|
||||
"build": {
|
||||
"includeDir": "."
|
||||
}
|
||||
}
|
||||
81
exercises/20_microphone/platformio.ini
Normal file
81
exercises/20_microphone/platformio.ini
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
; 20260413 ChatGPT
|
||||
; Exercise 20_microphone
|
||||
|
||||
[platformio]
|
||||
default_envs = guy
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = default_8MB.csv
|
||||
monitor_speed = 115200
|
||||
extra_scripts = pre:scripts/set_build_epoch.py
|
||||
lib_deps =
|
||||
Wire
|
||||
olikraus/U8g2@^2.36.4
|
||||
lewisxhe/XPowersLib@0.3.3
|
||||
|
||||
build_flags =
|
||||
-I ../../shared/boards
|
||||
-I ../../external/microReticulum_Firmware
|
||||
-D BOARD_MODEL=BOARD_TBEAM_S_V1
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D MIC_SAMPLE_RATE=16000
|
||||
-D MIC_BITS_PER_SAMPLE=16
|
||||
-D MIC_RECORD_SECONDS=30
|
||||
-D MIC_IDLE_SECONDS=30
|
||||
-D MIC_DATA_PIN=39
|
||||
-D MIC_CLK_PIN=38
|
||||
-D MIC_SELECT_PIN=48
|
||||
-D MIC_SELECT_LEFT=1
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"AMY\"
|
||||
-D NODE_LABEL=\"Amy\"
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"BOB\"
|
||||
-D NODE_LABEL=\"Bob\"
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"CY\"
|
||||
-D NODE_LABEL=\"Cy\"
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"DAN\"
|
||||
-D NODE_LABEL=\"Dan\"
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"ED\"
|
||||
-D NODE_LABEL=\"Ed\"
|
||||
|
||||
[env:flo]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"FLO\"
|
||||
-D NODE_LABEL=\"Flo\"
|
||||
|
||||
[env:guy]
|
||||
extends = env
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D BOARD_ID=\"GUY\"
|
||||
-D NODE_LABEL=\"Guy\"
|
||||
13
exercises/20_microphone/scripts/set_build_epoch.py
Normal file
13
exercises/20_microphone/scripts/set_build_epoch.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import time
|
||||
Import("env")
|
||||
|
||||
epoch = int(time.time())
|
||||
utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch))
|
||||
|
||||
env.Append(
|
||||
CPPDEFINES=[
|
||||
("FW_BUILD_EPOCH", str(epoch)),
|
||||
("FW_BUILD_UTC", '\"%s\"' % utc_tag),
|
||||
]
|
||||
)
|
||||
|
||||
137
exercises/20_microphone/src/main.cpp
Normal file
137
exercises/20_microphone/src/main.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// 20260413 ChatGPT
|
||||
// Exercise 20: microphone recorder
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "MicrophoneRecorder.h"
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "MIC"
|
||||
#endif
|
||||
|
||||
#ifndef MIC_RECORD_SECONDS
|
||||
#define MIC_RECORD_SECONDS 30
|
||||
#endif
|
||||
|
||||
#ifndef MIC_IDLE_SECONDS
|
||||
#define MIC_IDLE_SECONDS 30
|
||||
#endif
|
||||
|
||||
#ifndef MIC_STARTUP_WAIT_SECONDS
|
||||
#define MIC_STARTUP_WAIT_SECONDS 10
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kSerialDelayMs = 1500;
|
||||
constexpr uint32_t kBetweenRecordingsMs = MIC_IDLE_SECONDS * 1000UL;
|
||||
|
||||
MicrophoneRecorder g_recorder(Serial);
|
||||
bool g_started = false;
|
||||
bool g_finished = false;
|
||||
size_t g_caseIndex = 0;
|
||||
uint32_t g_nextActionMs = 0;
|
||||
|
||||
struct SweepCase {
|
||||
const char* label;
|
||||
MicrophoneRecorder::MicRuntimeConfig cfg;
|
||||
};
|
||||
|
||||
SweepCase g_cases[] = {
|
||||
{"base", {.dataPin = MIC_DATA_PIN, .clkPin = MIC_CLK_PIN, .selectPin = MIC_SELECT_PIN, .selectLeft = (MIC_SELECT_LEFT != 0)}},
|
||||
{"flip_lr", {.dataPin = MIC_DATA_PIN, .clkPin = MIC_CLK_PIN, .selectPin = MIC_SELECT_PIN, .selectLeft = (MIC_SELECT_LEFT == 0)}},
|
||||
{"swap_pins", {.dataPin = MIC_CLK_PIN, .clkPin = MIC_DATA_PIN, .selectPin = MIC_SELECT_PIN, .selectLeft = (MIC_SELECT_LEFT != 0)}},
|
||||
{"swap_flip", {.dataPin = MIC_CLK_PIN, .clkPin = MIC_DATA_PIN, .selectPin = MIC_SELECT_PIN, .selectLeft = (MIC_SELECT_LEFT == 0)}},
|
||||
};
|
||||
|
||||
void printWhatToSay(const SweepCase& sweepCase) {
|
||||
Serial.println();
|
||||
Serial.println("Speak this sentence clearly 3 times during the test:");
|
||||
Serial.println("\"Testing T-Beam microphone case "
|
||||
+ String(sweepCase.label)
|
||||
+ ". One two three four five.\"");
|
||||
Serial.println("Look for console lines where:");
|
||||
Serial.println(" status=active");
|
||||
Serial.println(" min and max change over time and are not equal");
|
||||
Serial.println(" avgabs is not stuck at one repeated value");
|
||||
Serial.println("If status=flat with min=max every second, that case is bad.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(kSerialDelayMs);
|
||||
|
||||
Serial.println();
|
||||
Serial.println("============================================================");
|
||||
Serial.printf("Exercise 20 Microphone Recorder [%s]\r\n", NODE_LABEL);
|
||||
Serial.println("============================================================");
|
||||
|
||||
if (!g_recorder.begin()) {
|
||||
Serial.println("Recorder init failed; loop will retry.");
|
||||
}
|
||||
|
||||
Serial.printf("Waiting %u seconds before the first recording.\r\n",
|
||||
(unsigned)MIC_STARTUP_WAIT_SECONDS);
|
||||
g_nextActionMs = millis() + MIC_STARTUP_WAIT_SECONDS * 1000UL;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!g_recorder.isSdReady() || g_finished) {
|
||||
delay(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
if ((int32_t)(now - g_nextActionMs) < 0) {
|
||||
delay(100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_caseIndex >= (sizeof(g_cases) / sizeof(g_cases[0]))) {
|
||||
Serial.println("Microphone sweep complete. No more recordings will be started.");
|
||||
g_finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
SweepCase& sweepCase = g_cases[g_caseIndex];
|
||||
g_recorder.setRuntimeConfig(sweepCase.cfg);
|
||||
|
||||
char path[96];
|
||||
snprintf(path,
|
||||
sizeof(path),
|
||||
"/recordings/mic_%s_%lu.wav",
|
||||
sweepCase.label,
|
||||
(unsigned long)time(nullptr));
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("Starting case %u/%u: %s data=%d clk=%d select=%d left=%d\r\n",
|
||||
(unsigned)(g_caseIndex + 1),
|
||||
(unsigned)(sizeof(g_cases) / sizeof(g_cases[0])),
|
||||
sweepCase.label,
|
||||
sweepCase.cfg.dataPin,
|
||||
sweepCase.cfg.clkPin,
|
||||
sweepCase.cfg.selectPin,
|
||||
sweepCase.cfg.selectLeft ? 1 : 0);
|
||||
printWhatToSay(sweepCase);
|
||||
|
||||
if (!g_recorder.open(path)) {
|
||||
Serial.println("Failed to open output file; delaying and retrying.");
|
||||
g_nextActionMs = millis() + 1000;
|
||||
delay(100);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ok = g_recorder.captureForMs(MIC_RECORD_SECONDS * 1000UL);
|
||||
g_recorder.close();
|
||||
|
||||
Serial.printf("case %s status=%s next_case_in=%lus\r\n",
|
||||
sweepCase.label,
|
||||
ok ? "ok" : "failed",
|
||||
(unsigned long)(kBetweenRecordingsMs / 1000UL));
|
||||
|
||||
g_caseIndex++;
|
||||
g_nextActionMs = millis() + kBetweenRecordingsMs;
|
||||
delay(100);
|
||||
}
|
||||
172
exercises/20_microphone/trial_1_console.log
Normal file
172
exercises/20_microphone/trial_1_console.log
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
(rnsenv) jlpoole@jp ~/workstation $ date; pio device monitor -b 115200 --port /dev/ttytCY
|
||||
Mon Apr 13 20:32:15 PDT 2026
|
||||
/home/jlpoole/rnsenv/lib/python3.13/site-packages/requests/__init__.py:113: RequestsDependencyWarning: urllib3 (2.6.1) or chardet (6.0.0.post1)/charset_normalizer (3.4.4) doesn't match a supported version!
|
||||
warnings.warn(
|
||||
--- Terminal on /dev/ttytCY | 115200 8-N-1
|
||||
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
|
||||
--- More details at https://bit.ly/pio-monitor-filters
|
||||
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
|
||||
[ 13073][000002] config updated: data=39 clk=38 select=48 left=1
|
||||
|
||||
Starting case 1/4: base data=39 clk=38 select=48 left=1
|
||||
|
||||
Speak this sentence clearly 3 times during the test:
|
||||
"Testing T-Beam microphone case base. One two three four five."
|
||||
Look for console lines where:
|
||||
status=active
|
||||
min and max change over time and are not equal
|
||||
avgabs is not stuck at one repeated value
|
||||
If status=flat with min=max every second, that case is bad.
|
||||
[ 13152][000003] opened /recordings/mic_base_19.wav
|
||||
E (13037) I2S: i2s_driver_uninstall(2048): I2S port 0 has not installed
|
||||
[ 13153][000004] microphone active clk(ws)=38 data=39 select=48 left=1
|
||||
[ 14158][000005] recording /recordings/mic_base_19.wav elapsed=1s bytes=30720 min=-31424 max=41 avgabs=30930 samples=15360 status=active
|
||||
[ 15182][000006] recording /recordings/mic_base_19.wav elapsed=2s bytes=63488 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 16206][000007] recording /recordings/mic_base_19.wav elapsed=3s bytes=96256 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 17230][000008] recording /recordings/mic_base_19.wav elapsed=4s bytes=129024 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 18254][000009] recording /recordings/mic_base_19.wav elapsed=5s bytes=161792 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 19278][000010] recording /recordings/mic_base_19.wav elapsed=6s bytes=194560 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 20302][000011] recording /recordings/mic_base_19.wav elapsed=7s bytes=227328 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 21326][000012] recording /recordings/mic_base_19.wav elapsed=8s bytes=260096 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 22350][000013] recording /recordings/mic_base_19.wav elapsed=9s bytes=292864 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 23374][000014] recording /recordings/mic_base_19.wav elapsed=10s bytes=325632 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 24398][000015] recording /recordings/mic_base_19.wav elapsed=11s bytes=358400 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 25422][000016] recording /recordings/mic_base_19.wav elapsed=12s bytes=391168 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 26446][000017] recording /recordings/mic_base_19.wav elapsed=13s bytes=423936 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 27470][000018] recording /recordings/mic_base_19.wav elapsed=14s bytes=456704 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 28494][000019] recording /recordings/mic_base_19.wav elapsed=15s bytes=489472 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 29518][000020] recording /recordings/mic_base_19.wav elapsed=16s bytes=522240 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 30542][000021] recording /recordings/mic_base_19.wav elapsed=17s bytes=555008 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 31566][000022] recording /recordings/mic_base_19.wav elapsed=18s bytes=587776 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 32590][000023] recording /recordings/mic_base_19.wav elapsed=19s bytes=620544 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 33614][000024] recording /recordings/mic_base_19.wav elapsed=20s bytes=653312 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 34638][000025] recording /recordings/mic_base_19.wav elapsed=21s bytes=686080 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 35662][000026] recording /recordings/mic_base_19.wav elapsed=22s bytes=718848 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 36686][000027] recording /recordings/mic_base_19.wav elapsed=23s bytes=751616 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 37710][000028] recording /recordings/mic_base_19.wav elapsed=24s bytes=784384 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 38734][000029] recording /recordings/mic_base_19.wav elapsed=25s bytes=817152 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 39758][000030] recording /recordings/mic_base_19.wav elapsed=26s bytes=849920 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 40782][000031] recording /recordings/mic_base_19.wav elapsed=27s bytes=882688 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 41806][000032] recording /recordings/mic_base_19.wav elapsed=28s bytes=915456 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 42830][000033] recording /recordings/mic_base_19.wav elapsed=29s bytes=948224 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 43375][000034] closed /recordings/mic_base_19.wav bytes=960512
|
||||
case base status=ok next_case_in=30s
|
||||
[ 73375][000035] microphone inactive
|
||||
[ 73376][000036] config updated: data=39 clk=38 select=48 left=0
|
||||
|
||||
Starting case 2/4: flip_lr data=39 clk=38 select=48 left=0
|
||||
|
||||
Speak this sentence clearly 3 times during the test:
|
||||
"Testing T-Beam microphone case flip_lr. One two three four five."
|
||||
Look for console lines where:
|
||||
status=active
|
||||
min and max change over time and are not equal
|
||||
avgabs is not stuck at one repeated value
|
||||
If status=flat with min=max every second, that case is bad.
|
||||
[ 73465][000037] opened /recordings/mic_flip_lr_79.wav
|
||||
E (73350) I2S: i2s_driver_uninstall(2048): I2S port 0 has not installed
|
||||
[ 73466][000038] microphone active clk(ws)=38 data=39 select=48 left=0
|
||||
[ 74471][000039] recording /recordings/mic_flip_lr_79.wav elapsed=1s bytes=30720 min=-32768 max=25661 avgabs=30858 samples=15360 status=active
|
||||
[ 75495][000040] recording /recordings/mic_flip_lr_79.wav elapsed=2s bytes=63488 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 76519][000041] recording /recordings/mic_flip_lr_79.wav elapsed=3s bytes=96256 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 77543][000042] recording /recordings/mic_flip_lr_79.wav elapsed=4s bytes=129024 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 78567][000043] recording /recordings/mic_flip_lr_79.wav elapsed=5s bytes=161792 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 79591][000044] recording /recordings/mic_flip_lr_79.wav elapsed=6s bytes=194560 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 80615][000045] recording /recordings/mic_flip_lr_79.wav elapsed=7s bytes=227328 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 81639][000046] recording /recordings/mic_flip_lr_79.wav elapsed=8s bytes=260096 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 82663][000047] recording /recordings/mic_flip_lr_79.wav elapsed=9s bytes=292864 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 83687][000048] recording /recordings/mic_flip_lr_79.wav elapsed=10s bytes=325632 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 84711][000049] recording /recordings/mic_flip_lr_79.wav elapsed=11s bytes=358400 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 85735][000050] recording /recordings/mic_flip_lr_79.wav elapsed=12s bytes=391168 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 86759][000051] recording /recordings/mic_flip_lr_79.wav elapsed=13s bytes=423936 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 87783][000052] recording /recordings/mic_flip_lr_79.wav elapsed=14s bytes=456704 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 88807][000053] recording /recordings/mic_flip_lr_79.wav elapsed=15s bytes=489472 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 89831][000054] recording /recordings/mic_flip_lr_79.wav elapsed=16s bytes=522240 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 90855][000055] recording /recordings/mic_flip_lr_79.wav elapsed=17s bytes=555008 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 91879][000056] recording /recordings/mic_flip_lr_79.wav elapsed=18s bytes=587776 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 92903][000057] recording /recordings/mic_flip_lr_79.wav elapsed=19s bytes=620544 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 93927][000058] recording /recordings/mic_flip_lr_79.wav elapsed=20s bytes=653312 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 94951][000059] recording /recordings/mic_flip_lr_79.wav elapsed=21s bytes=686080 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 95975][000060] recording /recordings/mic_flip_lr_79.wav elapsed=22s bytes=718848 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 96999][000061] recording /recordings/mic_flip_lr_79.wav elapsed=23s bytes=751616 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 98023][000062] recording /recordings/mic_flip_lr_79.wav elapsed=24s bytes=784384 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 99047][000063] recording /recordings/mic_flip_lr_79.wav elapsed=25s bytes=817152 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 100071][000064] recording /recordings/mic_flip_lr_79.wav elapsed=26s bytes=849920 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 101095][000065] recording /recordings/mic_flip_lr_79.wav elapsed=27s bytes=882688 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 102119][000066] recording /recordings/mic_flip_lr_79.wav elapsed=28s bytes=915456 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 103143][000067] recording /recordings/mic_flip_lr_79.wav elapsed=29s bytes=948224 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 103689][000068] closed /recordings/mic_flip_lr_79.wav bytes=960512
|
||||
case flip_lr status=ok next_case_in=30s
|
||||
[ 133689][000069] microphone inactive
|
||||
[ 133690][000070] config updated: data=38 clk=39 select=48 left=1
|
||||
|
||||
Starting case 3/4: swap_pins data=38 clk=39 select=48 left=1
|
||||
|
||||
Speak this sentence clearly 3 times during the test:
|
||||
"Testing T-Beam microphone case swap_pins. One two three four five."
|
||||
Look for console lines where:
|
||||
status=active
|
||||
min and max change over time and are not equal
|
||||
avgabs is not stuck at one repeated value
|
||||
If status=flat with min=max every second, that case is bad.
|
||||
[ 133779][000071] opened /recordings/mic_swap_pins_139.wav
|
||||
E (133664) I2S: i2s_driver_uninstall(2048): I2S port 0 has not installed
|
||||
[ 133780][000072] microphone active clk(ws)=39 data=38 select=48 left=1
|
||||
[ 134785][000073] recording /recordings/mic_swap_pins_139.wav elapsed=1s bytes=30720 min=-32768 max=0 avgabs=30931 samples=15360 status=active
|
||||
[ 135809][000074] recording /recordings/mic_swap_pins_139.wav elapsed=2s bytes=63488 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 136833][000075] recording /recordings/mic_swap_pins_139.wav elapsed=3s bytes=96256 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 137857][000076] recording /recordings/mic_swap_pins_139.wav elapsed=4s bytes=129024 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 137922][000077] file write failed wanted=2048 wrote=468
|
||||
[ 137993][000078] closed /recordings/mic_swap_pins_139.wav bytes=129024
|
||||
case swap_pins status=failed next_case_in=30s
|
||||
[ 167993][000079] microphone inactive
|
||||
[ 167994][000080] config updated: data=38 clk=39 select=48 left=0
|
||||
|
||||
Starting case 4/4: swap_flip data=38 clk=39 select=48 left=0
|
||||
|
||||
Speak this sentence clearly 3 times during the test:
|
||||
"Testing T-Beam microphone case swap_flip. One two three four five."
|
||||
Look for console lines where:
|
||||
status=active
|
||||
min and max change over time and are not equal
|
||||
avgabs is not stuck at one repeated value
|
||||
If status=flat with min=max every second, that case is bad.
|
||||
[ 168083][000081] opened /recordings/mic_swap_flip_174.wav
|
||||
E (167968) I2S: i2s_driver_uninstall(2048): I2S port 0 has not installed
|
||||
[ 168084][000082] microphone active clk(ws)=39 data=38 select=48 left=0
|
||||
[ 169089][000083] recording /recordings/mic_swap_flip_174.wav elapsed=1s bytes=30720 min=-32768 max=0 avgabs=30931 samples=15360 status=active
|
||||
[ 170113][000084] recording /recordings/mic_swap_flip_174.wav elapsed=2s bytes=63488 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 171137][000085] recording /recordings/mic_swap_flip_174.wav elapsed=3s bytes=96256 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 172161][000086] recording /recordings/mic_swap_flip_174.wav elapsed=4s bytes=129024 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 173185][000087] recording /recordings/mic_swap_flip_174.wav elapsed=5s bytes=161792 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 174209][000088] recording /recordings/mic_swap_flip_174.wav elapsed=6s bytes=194560 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 175233][000089] recording /recordings/mic_swap_flip_174.wav elapsed=7s bytes=227328 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 176257][000090] recording /recordings/mic_swap_flip_174.wav elapsed=8s bytes=260096 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 177281][000091] recording /recordings/mic_swap_flip_174.wav elapsed=9s bytes=292864 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 178305][000092] recording /recordings/mic_swap_flip_174.wav elapsed=10s bytes=325632 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 179329][000093] recording /recordings/mic_swap_flip_174.wav elapsed=11s bytes=358400 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 180353][000094] recording /recordings/mic_swap_flip_174.wav elapsed=12s bytes=391168 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 181377][000095] recording /recordings/mic_swap_flip_174.wav elapsed=13s bytes=423936 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 182401][000096] recording /recordings/mic_swap_flip_174.wav elapsed=14s bytes=456704 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 183425][000097] recording /recordings/mic_swap_flip_174.wav elapsed=15s bytes=489472 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 184449][000098] recording /recordings/mic_swap_flip_174.wav elapsed=16s bytes=522240 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 185473][000099] recording /recordings/mic_swap_flip_174.wav elapsed=17s bytes=555008 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 186497][000100] recording /recordings/mic_swap_flip_174.wav elapsed=18s bytes=587776 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 187521][000101] recording /recordings/mic_swap_flip_174.wav elapsed=19s bytes=620544 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 188545][000102] recording /recordings/mic_swap_flip_174.wav elapsed=20s bytes=653312 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 189569][000103] recording /recordings/mic_swap_flip_174.wav elapsed=21s bytes=686080 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 190593][000104] recording /recordings/mic_swap_flip_174.wav elapsed=22s bytes=718848 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 191617][000105] recording /recordings/mic_swap_flip_174.wav elapsed=23s bytes=751616 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 192641][000106] recording /recordings/mic_swap_flip_174.wav elapsed=24s bytes=784384 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 193665][000107] recording /recordings/mic_swap_flip_174.wav elapsed=25s bytes=817152 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 194689][000108] recording /recordings/mic_swap_flip_174.wav elapsed=26s bytes=849920 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 195713][000109] recording /recordings/mic_swap_flip_174.wav elapsed=27s bytes=882688 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 196737][000110] recording /recordings/mic_swap_flip_174.wav elapsed=28s bytes=915456 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 197761][000111] recording /recordings/mic_swap_flip_174.wav elapsed=29s bytes=948224 min=-30935 max=-30935 avgabs=30935 samples=16384 status=flat
|
||||
[ 198306][000112] closed /recordings/mic_swap_flip_174.wav bytes=960512
|
||||
case swap_flip status=ok next_case_in=30s
|
||||
Disconnected ([Errno 5] Input/output error)
|
||||
Reconnecting to /dev/ttytCY ..^C
|
||||
(rnsenv) jlpoole@jp ~/workstation $ date
|
||||
Mon Apr 13 20:37:09 PDT 2026
|
||||
(rnsenv) jlpoole@jp ~/workstation $
|
||||
244
exercises/21_six_axis/README.md
Normal file
244
exercises/21_six_axis/README.md
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
This exercise addresses the QMI8658 six-axis inertial sensor.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The QMI8658 is a [6-DoF IMU](#six-degrees-of-freedom-6-dof-imu): a three-axis accelerometer plus a three-axis gyroscope.
|
||||
|
||||
The outputs from this sensor are:
|
||||
|
||||
- Acceleration on X, Y, and Z in [`g`](#g)
|
||||
- Angular rotation on X, Y, and Z in [`dps`](#dps-degrees-per-second)
|
||||
- Temperature in degrees C
|
||||
- Derived motion events such as wake-on-motion and pedometer counts
|
||||
- Derived orientation estimates such as [`roll`](#roll-pitch-yaw), [`pitch`](#roll-pitch-yaw), and [`yaw`](#roll-pitch-yaw) when paired with a fusion filter such as [Madgwick](#madgwick)
|
||||
|
||||
In practical terms, this sensor is useful for:
|
||||
|
||||
- Detecting tilt, orientation change, vibration, and shock
|
||||
- Measuring how fast the device is rotating
|
||||
- Detecting movement versus stillness for low-power wakeup
|
||||
- Building a step counter or simple activity detector
|
||||
- Driving a real-time attitude display, cursor, or gesture interface
|
||||
- Logging motion history with FIFO buffering when the CPU cannot sample every reading immediately
|
||||
|
||||
The manufacturer examples staged in `/usr/local/src/LilyGo-LoRa-Series/examples/Sensor` show three main usage patterns:
|
||||
|
||||
- `QMI8658_GetDataExample`: simple polling of raw accel, gyro, timestamp, and temperature
|
||||
- `QMI8658_InterruptExample`: use the data-ready interrupt instead of polling constantly
|
||||
- `QMI8658_ReadFromFifoExample`: burst-read buffered samples from the sensor FIFO
|
||||
- `QMI8658_MadgwickAHRS`: fuse accel + gyro into roll/pitch/yaw
|
||||
- `QMI8658_BlockExample` and `QMI8658_InterruptBlockExample`: turn orientation into a live visual block position
|
||||
- `QMI8658_WakeOnMotion`: low-power motion-triggered wakeup
|
||||
- `QMI8658_PedometerExample`: use the chip's built-in step-detection logic
|
||||
- `QMI8658_LockingMechanismExample`: callback-based handling for synchronized data reads
|
||||
|
||||
## What The Settings Mean
|
||||
|
||||
The examples expose four major tuning knobs.
|
||||
|
||||
### 1. Full-scale range
|
||||
|
||||
Accelerometer range:
|
||||
|
||||
- `ACC_RANGE_2G`
|
||||
- `ACC_RANGE_4G`
|
||||
- `ACC_RANGE_8G`
|
||||
- `ACC_RANGE_16G`
|
||||
|
||||
Gyroscope range:
|
||||
|
||||
- `GYR_RANGE_16DPS`
|
||||
- `GYR_RANGE_32DPS`
|
||||
- `GYR_RANGE_64DPS`
|
||||
- `GYR_RANGE_128DPS`
|
||||
- `GYR_RANGE_256DPS`
|
||||
- `GYR_RANGE_512DPS`
|
||||
- `GYR_RANGE_1024DPS`
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Lower range gives finer measurement resolution but clips sooner.
|
||||
- Higher range survives stronger motion but each ADC count represents a larger real-world change.
|
||||
|
||||
From the driver, the scale is computed as full-scale divided by `32768`, so approximate per-count resolution is:
|
||||
|
||||
- Accelerometer `2g`: `2 / 32768 = 0.000061 g/count`
|
||||
- Accelerometer `4g`: `4 / 32768 = 0.000122 g/count`
|
||||
- Accelerometer `8g`: `8 / 32768 = 0.000244 g/count`
|
||||
- Accelerometer `16g`: `16 / 32768 = 0.000488 g/count`
|
||||
- Gyro `64 dps`: `64 / 32768 = 0.00195 dps/count`
|
||||
- Gyro `256 dps`: `256 / 32768 = 0.00781 dps/count`
|
||||
- Gyro `1024 dps`: `1024 / 32768 = 0.03125 dps/count`
|
||||
|
||||
For this exercise, lower accel ranges such as `2g` or `4g` are usually best unless you expect impacts. A gyro range around `256 dps` is a good middle ground for hand motion.
|
||||
|
||||
### 2. Output data rate ([`Hz`](#hz-hertz))
|
||||
|
||||
Accelerometer ODR options seen in the examples:
|
||||
|
||||
- `1000`, `500`, `250`, `125`, `62.5`, `31.25 Hz`
|
||||
- Low-power accel-only modes: `128`, `21`, `11`, `3 Hz`
|
||||
|
||||
Gyroscope ODR options seen in the examples:
|
||||
|
||||
- `7174.4`, `3587.2`, `1793.6`, `896.8`, `448.4`, `224.2`, `112.1`, `56.05`, `28.025 Hz`
|
||||
|
||||
What this achieves:
|
||||
|
||||
- Higher `Hz` means more frequent samples and better tracking of fast motion.
|
||||
- Lower `Hz` reduces data volume, CPU work, and power usage.
|
||||
- If both accel and gyro are enabled in 6-DoF mode, the library notes that the effective accel timing is derived from the gyro timing.
|
||||
|
||||
Practical guidance:
|
||||
|
||||
- `25-100 Hz`: human motion, UI demos, tilt displays, step counting
|
||||
- `100-250 Hz`: responsive motion display with moderate load
|
||||
- `500-1000 Hz`: aggressive sampling for fast dynamics, usually more than needed for a serial console demo
|
||||
- Very high gyro ODR values are useful only if the rest of the system can keep up
|
||||
|
||||
### 3. Low-pass filter (`LPF_MODE_*`)
|
||||
|
||||
The examples show:
|
||||
|
||||
- `LPF_MODE_0`: `2.66%` of ODR
|
||||
- `LPF_MODE_1`: `3.63%` of ODR
|
||||
- `LPF_MODE_2`: `5.39%` of ODR
|
||||
- `LPF_MODE_3`: `13.37%` of ODR
|
||||
- `LPF_OFF`
|
||||
|
||||
This is a smoothing filter.
|
||||
|
||||
- Narrower bandwidth reduces noise but makes the signal slower and softer.
|
||||
- Wider bandwidth responds faster but passes more jitter.
|
||||
|
||||
Example:
|
||||
|
||||
- At `1000 Hz` accel ODR, `LPF_MODE_0` is about `26.6 Hz` bandwidth.
|
||||
- At `896.8 Hz` gyro ODR, `LPF_MODE_3` is about `120 Hz` bandwidth.
|
||||
|
||||
That explains why the examples often use:
|
||||
|
||||
- accel `LPF_MODE_0` for a steadier gravity vector
|
||||
- gyro `LPF_MODE_3` for quicker rotational response
|
||||
|
||||
### 4. FIFO depth and watermark
|
||||
|
||||
The FIFO example uses:
|
||||
|
||||
- FIFO sizes of `16`, `32`, `64`, or `128` samples
|
||||
- `FIFO_MODE_FIFO`: stop accepting new data when full
|
||||
- `FIFO_MODE_STREAM`: overwrite oldest data when full
|
||||
- A watermark interrupt level in samples
|
||||
|
||||
What this achieves:
|
||||
|
||||
- Lets the sensor accumulate data while the MCU is busy
|
||||
- Reduces interrupt rate by reading batches
|
||||
- Helps avoid losing data when serial output is slower than sensor sampling
|
||||
|
||||
Important detail from the driver:
|
||||
|
||||
- One enabled sensor stream uses `6 bytes/sample`
|
||||
- Accel + gyro together use `12 bytes/sample`
|
||||
|
||||
So a `16`-sample FIFO with both sensors enabled holds `192 bytes` of motion data.
|
||||
|
||||
## What Madgwick Does
|
||||
|
||||
[Madgwick](#madgwick) is a sensor-fusion algorithm. It combines the raw accelerometer and gyroscope readings into a more useful estimate of device orientation.
|
||||
|
||||
Instead of only showing:
|
||||
|
||||
- accel X/Y/Z
|
||||
- gyro X/Y/Z
|
||||
|
||||
it can estimate:
|
||||
|
||||
- roll
|
||||
- pitch
|
||||
- yaw
|
||||
|
||||
Why this helps:
|
||||
|
||||
- The gyroscope reacts quickly to motion, but it drifts over time.
|
||||
- The accelerometer provides a gravity reference, but it is noisy and is disturbed by real movement.
|
||||
- Madgwick blends the fast gyro response with the gravity reference from the accelerometer to produce a smoother and more stable attitude estimate.
|
||||
|
||||
In the staged examples:
|
||||
|
||||
- `QMI8658_MadgwickAHRS` prints orientation values derived from the raw IMU data
|
||||
- `QMI8658_BlockExample` and `QMI8658_InterruptBlockExample` use that orientation estimate to move a block on the display
|
||||
|
||||
Important limitation:
|
||||
|
||||
- With only accelerometer + gyroscope, roll and pitch can be stabilized reasonably well.
|
||||
- Yaw is relative and will drift over time because there is no magnetometer to provide an absolute heading reference.
|
||||
|
||||
For this exercise, Madgwick is useful because it converts the six raw motion channels into something easier to understand in real time while the board is tilted and rotated by hand.
|
||||
|
||||
## What The Sensor Is Best For In This Exercise
|
||||
|
||||
For a teaching exercise with live console output, the best first goal is not pedometer or wake-on-motion. It is a clean real-time observer that shows:
|
||||
|
||||
- raw accel X/Y/Z
|
||||
- raw gyro X/Y/Z
|
||||
- computed roll/pitch
|
||||
- sampling rate or inter-sample timing
|
||||
|
||||
That will let a student immediately see:
|
||||
|
||||
- gravity moving across axes as the device tilts
|
||||
- gyro spikes during rotation
|
||||
- the effect of changing range and ODR
|
||||
- the effect of LPF smoothing
|
||||
|
||||
## Recommended Direction For The Next Design Step
|
||||
|
||||
A strong Exercise 21 design would be:
|
||||
|
||||
- Start from `QMI8658_GetDataExample`
|
||||
- Add a fixed-rate output loop at `25-50 Hz` for readability over serial
|
||||
- Print accel, gyro, and temperature every cycle
|
||||
- Add an optional Madgwick roll/pitch output mode
|
||||
- Add one compile-time or menu-controlled profile for "slow and smooth" and one for "fast and responsive"
|
||||
|
||||
Suggested initial profiles:
|
||||
|
||||
- `Smooth console demo`: accel `4g @ 125 Hz`, gyro `256 dps @ 112.1 Hz`, moderate LPF
|
||||
- `Fast motion demo`: accel `4g @ 250 Hz`, gyro `256 dps @ 224.2 Hz`, lighter filtering
|
||||
|
||||
This exercise requires being connected via a console and real-time output is generated as the unit is moved in 3 dimensions so one can observe how quickly the sensors respond.
|
||||
|
||||
## Glossary
|
||||
|
||||
### Six Degrees Of Freedom (6-DoF) IMU
|
||||
|
||||
An inertial measurement unit with six motion channels: three accelerometer axes and three gyroscope axes.
|
||||
|
||||
### g
|
||||
|
||||
The accelerometer unit for acceleration. `1 g` is approximately Earth gravity.
|
||||
|
||||
### dps (Degrees Per Second)
|
||||
|
||||
The gyroscope unit for angular velocity. It reports how fast the device is rotating around an axis.
|
||||
|
||||
### mg
|
||||
|
||||
Milli-g. One thousandth of a `g`. This is commonly used for motion thresholds such as wake-on-motion and pedometer sensitivity.
|
||||
|
||||
### Hz (Hertz)
|
||||
|
||||
Samples per second. Higher `Hz` means the sensor reports data more frequently. Example: 1 Hz = 1 sample per second. 10 Hz = 1 sample per 100 milliseocnds.
|
||||
|
||||
### Roll, Pitch, Yaw
|
||||
|
||||
Common orientation angles.
|
||||
|
||||
- Roll: rotation around the front-to-back axis
|
||||
- Pitch: rotation around the side-to-side axis
|
||||
- Yaw: rotation around the vertical axis
|
||||
|
||||
### Madgwick
|
||||
|
||||
A sensor-fusion algorithm that combines accelerometer and gyroscope data to estimate orientation more cleanly than raw sensor values alone.
|
||||
Loading…
Add table
Add a link
Reference in a new issue