Avionics
Core avionics package for CURE flight computers
Loading...
Searching...
No Matches
DataSaverSPI.cpp
Go to the documentation of this file.
3
4#include <cstring>
5#include <limits>
6
7DataSaverSPI::DataSaverSPI(uint16_t timestampInterval_ms,
9 : timestampInterval_ms_(timestampInterval_ms),
10 lastTimestamp_ms_(0),
11 launchTimestamp_ms_(0),
12 flash_(flash),
13 nextWriteAddress_(kDataStartAddress),
14 launchWriteAddress_(0),
15 postLaunchMode_(false),
16 bufferIndex_(0),
17 isChipFullDueToPostLaunchProtection_(false) {
19}
20
21int DataSaverSPI::saveDataPoint(const DataPoint& dataPoint, uint8_t name) {
22 if (rebootedInPostLaunchMode_ || isChipFullDueToPostLaunchProtection_) {
23 return 1; // Do not save if writes are blocked by post-launch state.
24 }
25
26 // Write a timestamp automatically if enough time has passed since the last one
27 uint32_t const timestamp = dataPoint.timestamp_ms;
28 if (timestamp - lastTimestamp_ms_ > timestampInterval_ms_) {
29 int const timestampResult = saveTimestamp(timestamp);
30 if (timestampResult != 0) {
31 return timestampResult;
32 }
33 }
34
35 Record_t record = {name, dataPoint.data};
36 if (addRecordToBuffer(&record) != 0) {
37 if (isChipFullDueToPostLaunchProtection_) {
38 return 1;
39 }
40 return -1;
41 }
42
43 lastDataPoint_ = dataPoint;
44 return 0;
45}
46
47int DataSaverSPI::saveTimestamp(uint32_t timestamp_ms){
48 if (rebootedInPostLaunchMode_ || isChipFullDueToPostLaunchProtection_) {
49 return 1; // Do not save if writes are blocked by post-launch state.
50 }
51
52 TimestampRecord_t timeStampRecord = {TIMESTAMP, timestamp_ms};
53 if (addRecordToBuffer(&timeStampRecord) != 0) {
54 if (isChipFullDueToPostLaunchProtection_) {
55 return 1;
56 }
57 return -1;
58 }
59
60 lastTimestamp_ms_ = timestamp_ms;
61 return 0;
62}
63
64int DataSaverSPI::addDataToBuffer(const uint8_t* data, size_t length) {
65 if (bufferIndex_ + length > kBufferSize_bytes) {
66 // Flush the buffer
67 if (flushBuffer() < 0) {
68 return -1;
69 }
70 }
71
72 // Copy the data into the buffer
73 memcpy(buffer_ + bufferIndex_, data, length);
74 bufferIndex_ += length;
75 return 0;
76}
77
78uint32_t DataSaverSPI::normalizeDataAddress(uint32_t address) const {
79 if (address >= flash_->size()) {
80 return kDataStartAddress;
81 }
82 return address;
83}
84
85bool DataSaverSPI::isProtectedLaunchSector(uint32_t sectorNumber) const {
86 if (!postLaunchMode_) {
87 return false;
88 }
89 return sectorNumber == launchWriteAddress_ / SFLASH_SECTOR_SIZE;
90}
91
92DataSaverSPI::SectorEraseResult DataSaverSPI::eraseSectorForWriteAndLatchOnProtection(
93 uint32_t sectorNumber) {
94 if (isProtectedLaunchSector(sectorNumber)) {
95 isChipFullDueToPostLaunchProtection_ = true;
96 return SectorEraseResult::kProtectedSectorLatched;
97 }
98 if (!flash_->eraseSector(sectorNumber)) {
99 return SectorEraseResult::kFlashEraseFailed;
100 }
101 preparedSectorNumber_ = sectorNumber;
102 return SectorEraseResult::kErased;
103}
104
105bool DataSaverSPI::shouldStopForPostLaunchWindow() {
106 if (!postLaunchMode_) {
107 return false;
108 }
109 if (nextWriteAddress_ > launchWriteAddress_) {
110 return false;
111 }
112 if (nextWriteAddress_ + kBufferSize_bytes * 2 <= launchWriteAddress_) {
113 return false;
114 }
115 isChipFullDueToPostLaunchProtection_ = true;
116 return true;
117}
118
119// Write the entire buffer to flash.
120int DataSaverSPI::flushBuffer() {
121 if (bufferIndex_ == 0) {
122 return 1; // Nothing to flush
123 }
124
125 // If the next write would go past the end of the flash, wrap around to the beginning of the data section.
126 if (nextWriteAddress_ + kBufferSize_bytes > flash_->size()) {
127 nextWriteAddress_ = kDataStartAddress;
128 }
129
130 if (shouldStopForPostLaunchWindow()) {
131 return -1;
132 }
133
134 // Fallback erase for first entry into a sector. In steady-state this sector
135 // should already be prepared by the previous flush.
136 if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) {
137 uint32_t const currentSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE;
138 if (preparedSectorNumber_ != currentSectorNumber) {
139 SectorEraseResult const eraseResult =
140 eraseSectorForWriteAndLatchOnProtection(currentSectorNumber);
141 if (eraseResult != SectorEraseResult::kErased) {
142 return -1;
143 }
144 }
145 }
146
147 // Write 1 page of data.
148 if (!flash_->writeBuffer(nextWriteAddress_, buffer_, kBufferSize_bytes)) {
149 return -1;
150 }
151
152 nextWriteAddress_ = normalizeDataAddress(nextWriteAddress_ + kBufferSize_bytes);
153
154 // Pre-erase one step earlier: after writing the previous page, erase the
155 // sector that the next page will start in. This lets erase latency overlap
156 // with buffer fill time.
157 if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) {
158 uint32_t const nextSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE;
159 SectorEraseResult const preEraseResult =
160 eraseSectorForWriteAndLatchOnProtection(nextSectorNumber);
161 if (preEraseResult == SectorEraseResult::kFlashEraseFailed) {
162 return -1;
163 }
164 }
165
166 bufferIndex_ = 0; // Reset the buffer
167 bufferFlushes_++;
168 return 0;
169}
170
171
173 if (flash_ == nullptr) {
174 return false;
175 }
176 if (!flash_->begin()) {
177 return false;
178 }
179
180 this->postLaunchMode_ = isPostLaunchMode();
181 if (postLaunchMode_) {
182 // If we are already in post-launch mode, then don't write to flash at all.
183 rebootedInPostLaunchMode_ = true;
184 return false;
185 }
186
187 return true;
188}
189
191 uint8_t flag = 0;
192 flash_->readBuffer(kPostLaunchFlagAddress, &flag, sizeof(flag));
193 this->postLaunchMode_ = (flag == kPostLaunchFlagTrue);
194 return this->postLaunchMode_;
195}
196
198 flash_->eraseSector(kMetadataStartAddress / SFLASH_SECTOR_SIZE);
199
200 uint8_t flag = kPostLaunchFlagFalse;
201 flash_->writeBuffer(kPostLaunchFlagAddress, &flag, sizeof(flag));
202
203 postLaunchMode_ = false;
204}
205
206void DataSaverSPI::dumpData(Stream &serial, bool ignoreEmptyPages) { //NOLINT(readability-function-cognitive-complexity)
207 uint32_t readAddress = kDataStartAddress; //NOLINT(cppcoreguidelines-init-variables) //NOLINT(misc-const-correctness)
208 // For each page write 51 sets of 5 bytes to serial with a newline
209 std::array<uint8_t, SFLASH_PAGE_SIZE> buffer; //NOLINT(cppcoreguidelines-init-variables)
210 size_t recordSize = sizeof(Record_t); //NOLINT(cppcoreguidelines-init-variables)
211 size_t numRecordsPerPage = SFLASH_PAGE_SIZE / recordSize; //NOLINT(cppcoreguidelines-init-variables)
212
213 // If not in post-launch mode, erase the next sector after the next write address.
214 // This ensures that we don't accidentally dump old data from previous flights
215 // If ignoreEmptyPages is true, then we don't need to erase the next sector
216 if (!postLaunchMode_ && !ignoreEmptyPages) {
217 flash_->eraseSector(nextWriteAddress_ / SFLASH_SECTOR_SIZE + 1);
218 }
219
220 // To ensure it's lined-up let's set a '\n' , '\r' and a 's' at the start
221 serial.write('a');
222 serial.write('b');
223 serial.write('c');
224 serial.write('d');
225 serial.write('e');
226 serial.write('f');
227
228 bool done = false;
229 bool timedOut = false;
230 bool stoppedFromEmptyPage = false;
231 bool badRead = false;
232
233 while (readAddress < flash_->size()) {
234 if (!readFromFlash(readAddress, buffer.data(), SFLASH_PAGE_SIZE)) {
235 badRead = true;
236 return;
237 }
238
239 // If the first name of this page is 255 then break
240 if (buffer[0] == kEmptyPageValue) {
241 if (ignoreEmptyPages) {
242 continue;
243 }
244 done = true;
245 stoppedFromEmptyPage = true;
246 break;
247 }
248
249 // At the start of each page, write some alignment characters
250 std::array<uint8_t, 3> startLine = {'l', 's', 'h'};
251 serial.write(startLine.data(), 3);
252
253
254
255 for (size_t i = 0; i < numRecordsPerPage; i++) { //NOLINT(cppcoreguidelines-init-variables)
256
257 serial.write(buffer.data() + i * recordSize, recordSize);
258 // serial.write('\n');
259 }
260
261 // Wait for a 'n' character to be received before continuing (10 second timeout)
262 const uint32_t timeout = static_cast<uint32_t>(millis()) + 10000U;
263 while (serial.read() != 'n') {
264 if (static_cast<uint32_t>(millis()) > timeout) {
265 timedOut = true;
266 return;
267 }
268 }
269
270 }
271
272 for (size_t i = 0; i < kBufferSize_bytes; i++){
273 std::array<uint8_t, 3> doneLine = {'E', 'O', 'F'};
274 serial.write(doneLine.data(), doneLine.size());
275 if (done){
276 serial.write('D');
277 }
278 if (timedOut){
279 serial.write('T');
280 }
281 if (stoppedFromEmptyPage){
282 serial.write('P');
283 }
284 if (badRead){
285 serial.write('B');
286 }
287 if (readAddress >= flash_->size()){
288 serial.write('F');
289 }
290 }
291}
292
294 bufferIndex_ = 0;
295 memset(buffer_, 0, kBufferSize_bytes);
296 lastDataPoint_ = {0, 0};
297 nextWriteAddress_ = kDataStartAddress;
298 lastTimestamp_ms_ = 0;
299 postLaunchMode_ = false;
300 launchWriteAddress_ = 0;
301 bufferFlushes_ = 0;
302 isChipFullDueToPostLaunchProtection_ = false;
303 preparedSectorNumber_ = std::numeric_limits<uint32_t>::max();
304}
305
307 flash_->eraseChip();
309
311
312 // Clear the launch write address.
313 launchWriteAddress_ = 0;
314
315}
316
317void DataSaverSPI::launchDetected(uint32_t launchTimestamp_ms) {
318 this->launchTimestamp_ms_ = launchTimestamp_ms;
319
320 // 0) Stop if we are already in post-launch mode
321 if (postLaunchMode_) {
322 return;
323 }
324
325 // 0.5) Clear the metadata sector to avoid 0 --> 1 inabilities
326 flash_->eraseSector(kMetadataStartAddress / SFLASH_SECTOR_SIZE);
327
328 // 1) Set the post-launch flag in metadata so we don't overwrite post-launch data.
329 uint8_t flag = kPostLaunchFlagTrue;
330 flash_->writeBuffer(kPostLaunchFlagAddress, &flag, sizeof(flag));
331 postLaunchMode_ = true;
332
333 // 2) Compute how many bytes we want to roll back to capture ~1 minute of pre-launch data.
334 // If you always store timestamps + name + DataPoint, factor that in:
335 //
336 // struct DataRecord {
337 // uint8_t name;
338 // DataPoint data;
339 // // occasionally 4 more bytes for a timestamp if we cross the interval threshold
340 // };
341 //
342 // For simplicity, let's assume worst-case all data points have the 4-byte timestamp:
343 // size_t recordSize = sizeof(uint32_t) // timestamp
344 // + sizeof(uint8_t) // name
345 // + sizeof(DataPoint);
346 //
347 // If you only occasionally store the timestamp, you might want a more nuanced approach.
348 //
349 const size_t recordSize_bytes = sizeof(uint32_t) + sizeof(uint8_t) + sizeof(DataPoint);
350 uint32_t const oneMinuteInMs = 60000;
351 uint32_t const dataPointsPerMinute = oneMinuteInMs / timestampInterval_ms_;
352 uint64_t rollbackSize_bytes = static_cast<uint64_t>(dataPointsPerMinute) * static_cast<uint64_t>(recordSize_bytes);
353
354 // 3) Clamp rollbackSize_bytes to something reasonable. We must not exceed
355 // the usable flash region (from address=1 to address=flash size - 1).
356 const size_t flashSize_bytes = flash_->size();
357 const size_t maxUsable_bytes = (flashSize_bytes > static_cast<size_t>(kDataStartAddress))
358 ? (flashSize_bytes - static_cast<size_t>(kDataStartAddress))
359 : 0U;
360 const auto maxUsable = static_cast<uint64_t>(maxUsable_bytes);
361 if (rollbackSize_bytes > maxUsable) {
362 // If we can't keep an entire minute, just keep as much as we can
363 rollbackSize_bytes = maxUsable;
364 }
365
366 // 4) Next, we do ring-buffer math to find the new launch write address,
367 // which is "1 minute's worth of data behind the next write address" in a circular sense.
368
369 // Because the next write address can be anywhere in [1, flash size - 1],
370 // let’s do a safe modular subtraction:
371 //
372 // newAddr = (next write address + flash size - rollback size)
373 // % flash size
374 //
375 // Then we ensure it’s never 0 because 0 is used for metadata.
376
377 const auto sizeOfFlash = static_cast<uint32_t>(flashSize_bytes);
378
379 // Make sure we aren't in the metadata region
380 if (nextWriteAddress_ < kDataStartAddress) {
381 nextWriteAddress_ = kDataStartAddress;
382 }
383
384 // Use 64-bit to avoid any negative wrap during the subtraction.
385 int64_t potentialAddr = static_cast<int64_t>(nextWriteAddress_)
386 + static_cast<int64_t>(sizeOfFlash) // ensure positivity
387 - static_cast<int64_t>(rollbackSize_bytes);
388
389 // Modulo by sizeOfFlash to bring it back into [0, sizeOfFlash-1].
390 potentialAddr = potentialAddr % sizeOfFlash;
391
392 // If result lands in the metadata region, wrap back into the data region.
393 if (potentialAddr < static_cast<int64_t>(kDataStartAddress)) {
394 potentialAddr += sizeOfFlash;
395 }
396
397
398 launchWriteAddress_ = static_cast<uint32_t>(potentialAddr);
399
400 std::array<uint8_t, sizeof(launchWriteAddress_)> bytes;
401 std::memcpy(bytes.data(), &launchWriteAddress_, sizeof(launchWriteAddress_));
402 flash_->writeBuffer(kLaunchStartAddressAddress, bytes.data(), bytes.size());
403
404}
405
406bool DataSaverSPI::writeToFlash(const uint8_t* data, size_t length) {
407 if (!flash_->writeBuffer(nextWriteAddress_, data, length)) {
408 return false;
409 }
410 nextWriteAddress_ = static_cast<uint32_t>(nextWriteAddress_ + static_cast<uint32_t>(length));
411 return true;
412}
413
414bool DataSaverSPI::readFromFlash(uint32_t& readAddress, uint8_t* buffer, size_t length) {
415 if (!flash_->readBuffer(readAddress, buffer, length)) {
416 return false;
417 }
418 readAddress = static_cast<uint32_t>(readAddress + static_cast<uint32_t>(length));
419 return true;
420}
#define SFLASH_PAGE_SIZE
#define SFLASH_SECTOR_SIZE
#define TIMESTAMP
Definition DataNames.h:25
constexpr uint32_t kDataStartAddress
constexpr uint32_t kMetadataStartAddress
constexpr uint8_t kPostLaunchFlagTrue
constexpr uint32_t kPostLaunchFlagAddress
constexpr uint8_t kPostLaunchFlagFalse
constexpr uint32_t kLaunchStartAddressAddress
constexpr uint8_t kEmptyPageValue
bool writeBuffer(uint32_t address, const uint8_t *buffer, size_t length)
Timestamped float measurement container.
Definition DataPoint.h:11
float data
Definition DataPoint.h:14
uint32_t timestamp_ms
Definition DataPoint.h:13
DataSaverSPI(uint16_t timestampInterval_ms, Adafruit_SPIFlash *flash)
Construct a new DataSaverSPI object.
static constexpr size_t kBufferSize_bytes
int saveDataPoint(const DataPoint &dataPoint, uint8_t name) override
Saves a DataPoint to SPI flash.
void clearInternalState()
Reset in-memory pointers without erasing flash contents.
bool isPostLaunchMode()
Returns whether the metadata from the flash chip indicates that it contains post-launch data that has...
void clearPostLaunchMode()
Clears the post-launch mode flag on the flash chip **WARNING: This will allow the data to be overwrit...
void eraseAllData()
Erase the entire flash chip to start fresh.
int saveTimestamp(uint32_t timestamp_ms)
Persist a bare timestamp entry to flash.
void dumpData(Stream &serial, bool ignoreEmptyPages)
Stream all recorded data to a serial connection.
virtual bool begin() override
Initialize the flash chip and metadata.
void launchDetected(uint32_t launchTimestamp_ms)
Call this when launch is detected to set the post-launch flag and prevent any data from being overwri...
virtual size_t write(uint8_t byte)
Definition serial_mock.h:90
virtual int read()
Definition serial_mock.h:87