Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
sd_output.hpp
Go to the documentation of this file.
1/*
2 * sd_output.hpp — SD card log sink for SBL logger
3 *
4 * Per-boot sequential log files with size and count limits.
5 * Buffers writes to a 512-byte sector-aligned buffer and flushes
6 * to SD card via FatFs. Call flush() periodically from the main
7 * loop (never from ISR).
8 *
9 * Usage:
10 * // Mount FatFs first, then:
11 * SdLogConfig cfg;
12 * cfg.millis = sbl::driver::Timer::millis; // required
13 * SdLogOutput::init(cfg);
14 *
15 * // In main loop:
16 * SdLogOutput::write("hello\r\n");
17 * SdLogOutput::flush(); // time-gated, safe to call every iteration
18 */
19
20#pragma once
21
22#include "ff.h"
23#include <cstring>
24
25namespace sbl::log {
26
28 const char* prefix = "LOG"; // Filename prefix → LOG_000.TXT
29 size_t max_file_bytes = 4*1024*1024;// 4MB per file (~24h of debug output)
30 uint16_t max_files = 256; // 256 files × 4MB = 1GB max card usage
31 uint16_t flush_interval_ms = 100; // Min ms between flushes (0 = every call)
32 uint32_t (*millis)() = nullptr; // Millisecond timestamp function (required)
33};
34
36public:
37 /// Initialize: scan existing files, create next sequential log.
38 /// Returns true if log file was opened successfully.
39 static bool init(const SdLogConfig& cfg = {}) {
40 s_cfg = cfg;
41 s_pos = 0;
42 s_file_bytes = 0;
43 s_last_flush_ms = 0;
44 s_open = false;
45
46 // Find the next available file number
47 s_current_num = find_next_file_number();
48
49 // If we've hit max_files, delete the oldest to make room
50 if (s_current_num >= s_cfg.max_files) {
51 delete_oldest();
52 s_current_num = find_next_file_number();
53 }
54
55 return open_file(s_current_num);
56 }
57
58 /// Logger sink interface — buffers data, auto-flushes when buffer is full.
59 static void write(const char* str) {
60 if (!s_open) return;
61 size_t len = strlen(str);
62 const char* p = str;
63 while (len > 0) {
64 size_t space = BUF_SIZE - s_pos;
65 size_t chunk = len < space ? len : space;
66 memcpy(s_buf + s_pos, p, chunk);
67 s_pos += chunk;
68 p += chunk;
69 len -= chunk;
70 if (s_pos >= BUF_SIZE) {
71 flush_buffer();
72 }
73 }
74 }
75
76 /// Time-gated flush. Safe to call every main loop iteration.
77 /// Only writes to SD if flush_interval_ms has elapsed or buffer is >75% full.
78 static void flush() {
79 if (!s_open || s_pos == 0) return;
80
81 bool time_to_flush = true;
82 if (s_cfg.millis && s_cfg.flush_interval_ms > 0) {
83 uint32_t now = s_cfg.millis();
84 uint32_t elapsed = now - s_last_flush_ms;
85 time_to_flush = (elapsed >= s_cfg.flush_interval_ms) ||
86 (s_pos >= BUF_SIZE * 3 / 4);
87 }
88
89 if (time_to_flush) {
90 flush_buffer();
91 }
92 }
93
94 /// Flush immediately, bypassing time gate.
95 static void force_flush() {
96 if (!s_open || s_pos == 0) return;
97 flush_buffer();
98 }
99
100 /// Close file (flushes first). Call before removing card.
101 static void close() {
102 if (!s_open) return;
103 force_flush();
104 f_close(&s_fil);
105 s_open = false;
106 }
107
108 static bool is_open() { return s_open; }
109
110private:
111 static constexpr size_t BUF_SIZE = 512;
112 static constexpr size_t NAME_BUF = 16; // "LOG_000.TXT\0"
113
114 static void make_filename(char* buf, uint16_t num) {
115 // Build PREFIX_NNN.TXT (8.3 format)
116 size_t i = 0;
117 const char* p = s_cfg.prefix;
118 while (*p && i < 4) buf[i++] = *p++;
119 buf[i++] = '_';
120 buf[i++] = '0' + (num / 100) % 10;
121 buf[i++] = '0' + (num / 10) % 10;
122 buf[i++] = '0' + num % 10;
123 buf[i++] = '.';
124 buf[i++] = 'T';
125 buf[i++] = 'X';
126 buf[i++] = 'T';
127 buf[i] = '\0';
128 }
129
130 static uint16_t find_next_file_number() {
131 // Find the lowest unused file number
132 char name[NAME_BUF];
133 FILINFO fno;
134 for (uint16_t n = 0; n < s_cfg.max_files; n++) {
135 make_filename(name, n);
136 if (f_stat(name, &fno) != FR_OK) {
137 return n; // File doesn't exist — use this number
138 }
139 }
140 return s_cfg.max_files; // All slots full
141 }
142
143 static void delete_oldest() {
144 // Delete file 0 and shift all others down
145 // Simple approach: just delete the lowest-numbered file
146 char name[NAME_BUF];
147 FILINFO fno;
148 for (uint16_t n = 0; n < s_cfg.max_files; n++) {
149 make_filename(name, n);
150 if (f_stat(name, &fno) == FR_OK) {
151 f_unlink(name);
152 return; // Deleted oldest, now find_next will find the gap
153 }
154 }
155 }
156
157 static bool open_file(uint16_t num) {
158 char name[NAME_BUF];
159 make_filename(name, num);
160 FRESULT fr = f_open(&s_fil, name, FA_WRITE | FA_CREATE_NEW);
161 if (fr != FR_OK) return false;
162 s_open = true;
163 s_file_bytes = 0;
164 return true;
165 }
166
167 static bool rotate_file() {
168 // Close current, open next
169 f_close(&s_fil);
170 s_open = false;
171 s_current_num++;
172
173 if (s_current_num >= s_cfg.max_files) {
174 delete_oldest();
175 s_current_num = find_next_file_number();
176 }
177
178 return open_file(s_current_num);
179 }
180
181 static void flush_buffer() {
182 if (s_pos == 0) return;
183
184 // Check if we need to rotate before writing
185 if (s_cfg.max_file_bytes > 0 &&
186 s_file_bytes + s_pos > s_cfg.max_file_bytes) {
187 if (!rotate_file()) {
188 s_pos = 0;
189 return; // Rotation failed, drop data
190 }
191 }
192
193 UINT bw;
194 f_write(&s_fil, s_buf, s_pos, &bw);
195 f_sync(&s_fil);
196 s_file_bytes += bw;
197 s_pos = 0;
198
199 if (s_cfg.millis) {
200 s_last_flush_ms = s_cfg.millis();
201 }
202 }
203
204 static inline SdLogConfig s_cfg;
205 static inline FIL s_fil;
206 static inline char s_buf[BUF_SIZE];
207 static inline size_t s_pos = 0;
208 static inline size_t s_file_bytes = 0;
209 static inline uint16_t s_current_num = 0;
210 static inline uint32_t s_last_flush_ms = 0;
211 static inline bool s_open = false;
212};
213
214} // namespace sbl::log
static void close()
Close file (flushes first). Call before removing card.
static void flush()
Definition sd_output.hpp:78
static void force_flush()
Flush immediately, bypassing time gate.
Definition sd_output.hpp:95
static bool init(const SdLogConfig &cfg={})
Definition sd_output.hpp:39
static bool is_open()
static void write(const char *str)
Logger sink interface — buffers data, auto-flushes when buffer is full.
Definition sd_output.hpp:59
Lightweight logging system.
Definition banner.hpp:50
const char * prefix
Definition sd_output.hpp:28
uint32_t(* millis)()
Definition sd_output.hpp:32