summaryrefslogtreecommitdiff
path: root/quantum/wear_leveling/wear_leveling.c
blob: 0a519639eaedd3489814174a1ba29c294ef6479a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include <stdbool.h>
#include "fnv.h"
#include "wear_leveling.h"
#include "wear_leveling_internal.h"

/*
    This wear leveling algorithm is adapted from algorithms from previous
    implementations in QMK, namely:
        - Artur F. (http://engsta.com/stm32-flash-memory-eeprom-emulator/)
        - Yiancar -- QMK's base implementation for STM32F303
        - Ilya Zhuravlev -- initial wear leveling algorithm
        - Don Kjer -- increased flash density algorithm
        - Nick Brassel (@tzarc) -- decoupled for use on other peripherals

    At this layer, it is assumed that any reads/writes from the backing store
    have a "reset state" after erasure of zero.
    It is up to the backing store to perform translation of values, such as
    taking the complement in order to deal with flash memory's reset value.

    Terminology:

        - Backing store: this is the storage area used by the wear leveling
            algorithm.

        - Backing size: this is the amount of storage provided by the backing
            store for use by the wear leveling algorithm.

        - Backing write size: this is the minimum number of bytes the backing
            store can write in a single operation.

        - Logical data: this is the externally-visible "emulated EEPROM" that
            external subsystems "see" when performing reads/writes.

        - Logical size: this is the amount of storage available for use
            externally. Effectively, the "size of the EEPROM".

        - Write log: this is a section of the backing store used to keep track
            of modifications without overwriting existing data. This log is
            "played back" on startup such that any subsequent reads are capable
            of returning the latest data.

        - Consolidated data: this is a section of the backing store reserved for
            use for the latest copy of logical data. This is only ever written
            when the write log is full -- the latest values for the logical data
            are written here and the write log is cleared.

    Configurables:

        - BACKING_STORE_WRITE_SIZE: The number of bytes requires for a write
            operation. This is defined by the capabilities of the backing store.

        - WEAR_LEVELING_BACKING_SIZE: The number of bytes provided by the
            backing store for use by the wear leveling algorithm.  This is
            defined by the capabilities of the backing store. This value must
            also be at least twice the size of the logical size, as well as a
            multiple of the logical size.

        - WEAR_LEVELING_LOGICAL_SIZE: The number of bytes externally visible
            to other subsystems performing reads/writes. This must be a multiple
            of the write size.

    General algorithm:

        During initialization:
            * The contents of the consolidated data section are read into cache.
            * The contents of the write log are "played back" and update the
                cache accordingly.

        During reads:
            * Logical data is served from the cache.

        During writes:
            * The cache is updated with the new data.
            * A new write log entry is appended to the log.
            * If the log's full, data is consolidated and the write log cleared.

    Write log structure:

        The first 8 bytes of the write log are a FNV1a_64 hash of the contents
        of the consolidated data area, in an attempt to detect and guard against
        any data corruption.

        The write log follows the hash:

        Given that the algorithm needs to cater for 2-, 4-, and 8-byte writes,
        a variable-length write log entry is used such that the minimal amount
        of storage is used based off the backing store write size.

        Firstly, an empty log entry is expected to be all zeros. If the backing
        store uses 0xFF for cleared bytes, it should return the complement, such
        that this wear-leveling algorithm "receives" zeros.

        For multi-byte writes, up to 8 bytes will be used for each log entry,
        depending on the size of backing store writes:

        ╔ Multi-byte Log Entry (2, 4-byte) ═╗
        ║00XXXYYY║YYYYYYYY║YYYYYYYY║AAAAAAAA║
        ║  └┬┘└┬┘║└──┬───┘║└──┬───┘║└──┬───┘║
        ║  LenAdd║ Address║ Address║Value[0]║
        ╚════════╩════════╩════════╩════════╝
        ╔ Multi-byte Log Entry (2-byte) ══════════════════════╗
        ║00XXXYYY║YYYYYYYY║YYYYYYYY║AAAAAAAA║BBBBBBBB║CCCCCCCC║
        ║  └┬┘└┬┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║
        ║  LenAdd║ Address║ Address║Value[0]║Value[1]║Value[2]║
        ╚════════╩════════╩════════╩════════╩════════╩════════╝
        ╔ Multi-byte Log Entry (2, 4, 8-byte) ══════════════════════════════════╗
        ║00XXXYYY║YYYYYYYY║YYYYYYYY║AAAAAAAA║BBBBBBBB║CCCCCCCC║DDDDDDDD║EEEEEEEE║
        ║  └┬┘└┬┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║└──┬───┘║
        ║  LenAdd║ Address║ Address║Value[0]║Value[1]║Value[2]║Value[3]║Value[4]║
        ╚════════╩════════╩════════╩════════╩════════╩════════╩════════╩════════╝

        19 bits are used for the address, which allows for a max logical size of
        512kB. Up to 5 bytes can be included in a single log entry.

        For 2-byte backing store writes, the last two bytes are optional
            depending on the length of data to be written. Accordingly, either 3
            or 4 backing store write operations will occur.
        For 4-byte backing store writes, either one or two write operations
            occur, depending on the length.
        For 8-byte backing store writes, one write operation occur.

    2-byte backing store optimizations:

        For single byte writes, addresses between 0...63 are encoded in a single
        backing store write operation. 4- and 8-byte backing stores do not have
        this optimization as it does not minimize the number of bytes written.

        ╔ Byte-Entry ════╗
        ║01XXXXXXYYYYYYYY║
        ║  └─┬──┘└──┬───┘║
        ║ Address Value  ║
        ╚════════════════╝
        0 <= Address < 0x40 (64)

        A second optimization takes into account uint16_t writes of 0 or 1,
        specifically catering for KC_NO and KC_TRANSPARENT in the dynamic keymap
        subsystem. This is valid only for the first 16kB of logical data --
        addresses outside this range will use the multi-byte encoding above.

        ╔ U16-Encoded 0 ═╗
        ║100XXXXXXXXXXXXX║
        ║  │└─────┬─────┘║
        ║  │Address >> 1 ║
        ║  └── Value: 0  ║
        ╚════════════════╝
        0 <= Address <= 0x3FFE (16382)

        ╔ U16-Encoded 1 ═╗
        ║101XXXXXXXXXXXXX║
        ║  │└─────┬─────┘║
        ║  │Address >> 1 ║
        ║  └── Value: 1  ║
        ╚════════════════╝
        0 <= Address <= 0x3FFE (16382) */

/**
 * Storage area for the wear-leveling cache.
 */
static struct __attribute__((__aligned__(BACKING_STORE_WRITE_SIZE))) {
    __attribute__((__aligned__(BACKING_STORE_WRITE_SIZE))) uint8_t cache[(WEAR_LEVELING_LOGICAL_SIZE)];
    uint32_t                                                       write_address;
    bool                                                           unlocked;
} wear_leveling;

/**
 * Locking helper: status
 */
typedef enum backing_store_lock_status_t { STATUS_FAILURE = 0, STATUS_SUCCESS, STATUS_UNCHANGED } backing_store_lock_status_t;

/**
 * Locking helper: unlock
 */
static inline backing_store_lock_status_t wear_leveling_unlock(void) {
    if (wear_leveling.unlocked) {
        return STATUS_UNCHANGED;
    }
    if (!backing_store_unlock()) {
        return STATUS_FAILURE;
    }
    wear_leveling.unlocked = true;
    return STATUS_SUCCESS;
}

/**
 * Locking helper: lock
 */
static inline backing_store_lock_status_t wear_leveling_lock(void) {
    if (!wear_leveling.unlocked) {
        return STATUS_UNCHANGED;
    }
    if (!backing_store_lock()) {
        return STATUS_FAILURE;
    }
    wear_leveling.unlocked = false;
    return STATUS_SUCCESS;
}

/**
 * Resets the cache, ensuring the write address is correctly initialised.
 */
static void wear_leveling_clear_cache(void) {
    memset(wear_leveling.cache, 0, (WEAR_LEVELING_LOGICAL_SIZE));
    wear_leveling.write_address = (WEAR_LEVELING_LOGICAL_SIZE) + 8; // +8 is due to the FNV1a_64 of the consolidated buffer
}

/**
 * Reads the consolidated data from the backing store into the cache.
 * Does not consider the write log.
 */
static wear_leveling_status_t wear_leveling_read_consolidated(void) {
    wl_dprintf("Reading consolidated data\n");

    wear_leveling_status_t status = WEAR_LEVELING_SUCCESS;
    if (!backing_store_read_bulk(0, (backing_store_int_t *)wear_leveling.cache, sizeof(wear_leveling.cache) / sizeof(backing_store_int_t))) {
        wl_dprintf("Failed to read from backing store\n");
        status = WEAR_LEVELING_FAILED;
    }

    // Verify the FNV1a_64 result
    if (status != WEAR_LEVELING_FAILED) {
        uint64_t          expected = fnv_64a_buf(wear_leveling.cache, (WEAR_LEVELING_LOGICAL_SIZE), FNV1A_64_INIT);
        write_log_entry_t entry;
        wl_dprintf("Reading checksum\n");
#if BACKING_STORE_WRITE_SIZE == 2
        backing_store_read_bulk((WEAR_LEVELING_LOGICAL_SIZE), entry.raw16, 4);
#elif BACKING_STORE_WRITE_SIZE == 4
        backing_store_read_bulk((WEAR_LEVELING_LOGICAL_SIZE), entry.raw32, 2);
#elif BACKING_STORE_WRITE_SIZE == 8
        backing_store_read((WEAR_LEVELING_LOGICAL_SIZE) + 0, &entry.raw64);
#endif
        // If we have a mismatch, clear the cache but do not flag a failure,
        // which will cater for the completely clean MCU case.
        if (entry.raw64 == expected) {
            wl_dprintf("Checksum matches, consolidated data is correct\n");
        } else {
            wl_dprintf("Checksum mismatch, clearing cache\n");
            wear_leveling_clear_cache();
        }
    }

    // If we failed for any reason, then clear the cache
    if (status == WEAR_LEVELING_FAILED) {
        wear_leveling_clear_cache();
    }

    return status;
}

/**
 * Writes the current cache to consolidated data at the beginning of the backing store.
 * Does not clear the write log.
 * Pre-condition: this is just after an erase, so we can write directly without reading.
 */
static wear_leveling_status_t wear_leveling_write_consolidated(void) {
    wl_dprintf("Writing consolidated data\n");

    backing_store_lock_status_t lock_status = wear_leveling_unlock();
    wear_leveling_status_t      status      = WEAR_LEVELING_CONSOLIDATED;
    if (!backing_store_write_bulk(0, (backing_store_int_t *)wear_leveling.cache, sizeof(wear_leveling.cache) / sizeof(backing_store_int_t))) {
        wl_dprintf("Failed to write to backing store\n");
        status = WEAR_LEVELING_FAILED;
    }

    if (status != WEAR_LEVELING_FAILED) {
        // Write out the FNV1a_64 result of the consolidated data
        write_log_entry_t entry;
        entry.raw64 = fnv_64a_buf(wear_leveling.cache, (WEAR_LEVELING_LOGICAL_SIZE), FNV1A_64_INIT);
        wl_dprintf("Writing checksum\n");
        do {
#if BACKING_STORE_WRITE_SIZE == 2
            if (!backing_store_write_bulk((WEAR_LEVELING_LOGICAL_SIZE), entry.raw16, 4)) {
                status = WEAR_LEVELING_FAILED;
                break;
            }
#elif BACKING_STORE_WRITE_SIZE == 4
            if (!backing_store_write_bulk((WEAR_LEVELING_LOGICAL_SIZE), entry.raw32, 2)) {
                status = WEAR_LEVELING_FAILED;
                break;
            }
#elif BACKING_STORE_WRITE_SIZE == 8
            if (!backing_store_write((WEAR_LEVELING_LOGICAL_SIZE), entry.raw64)) {
                status = WEAR_LEVELING_FAILED;
                break;
            }
#endif
        } while (0);
    }

    if (lock_status == STATUS_SUCCESS) {
        wear_leveling_lock();
    }
    return status;
}

/**
 * Forces a write of the current cache.
 * Erases the backing store, including the write log.
 * During this operation, there is the potential for data loss if a power loss occurs.
 */
static wear_leveling_status_t wear_leveling_consolidate_force(void) {
    wl_dprintf("Erasing backing store\n");

    // Erase the backing store. Expectation is that any un-written values that are read back after this call come back as zero.
    bool ok = backing_store_erase();
    if (!ok) {
        wl_dprintf("Failed to erase backing store\n");
        return WEAR_LEVELING_FAILED;
    }

    // Write the cache to the first section of the backing store.
    wear_leveling_status_t status = wear_leveling_write_consolidated();
    if (status == WEAR_LEVELING_FAILED) {
        wl_dprintf("Failed to write consolidated data\n");
    }

    // Next write of the log occurs after the consolidated values at the start of the backing store.
    wear_leveling.write_address = (WEAR_LEVELING_LOGICAL_SIZE) + 8; // +8 due to the FNV1a_64 of the consolidated area

    return status;
}

/**
 * Potential write of the current cache to the backing store.
 * Skipped if the current write log position is not at the end of the backing store.
 * During this operation, there is the potential for data loss if a power loss occurs.
 *
 * @return true if consolidation occurred
 */
static wear_leveling_status_t wear_leveling_consolidate_if_needed(void) {
    if (wear_leveling.write_address >= (WEAR_LEVELING_BACKING_SIZE)) {
        return wear_leveling_consolidate_force();
    }

    return WEAR_LEVELING_SUCCESS;
}

/**
 * Appends the supplied fixed-width entry to the write log, optionally consolidating if the log is full.
 *
 * @return true if consolidation occurred
 */
static wear_leveling_status_t wear_leveling_append_raw(backing_store_int_t value) {
    bool ok = backing_store_write(wear_leveling.write_address, value);
    if (!ok) {
        wl_dprintf("Failed to write to backing store\n");
        return WEAR_LEVELING_FAILED;
    }
    wear_leveling.write_address += (BACKING_STORE_WRITE_SIZE);
    return wear_leveling_consolidate_if_needed();
}

/**
 * Handles writing multi_byte-encoded data to the backing store.
 *
 * @return true if consolidation occurred
 */
static wear_leveling_status_t wear_leveling_write_raw_multibyte(uint32_t address, const void *value, size_t length) {
    const uint8_t *   p   = value;
    write_log_entry_t log = LOG_ENTRY_MAKE_MULTIBYTE(address, length);
    for (size_t i = 0; i < length; ++i) {
        log.raw8[3 + i] = p[i];
    }

    // Write to the backing store. See the multi-byte log format in the documentation header at the top of the file.
    wear_leveling_status_t status;
#if BACKING_STORE_WRITE_SIZE == 2
    status = wear_leveling_append_raw(log.raw16[0]);
    if (status != WEAR_LEVELING_SUCCESS) {
        return status;
    }

    status = wear_leveling_append_raw(log.raw16[1]);
    if (status != WEAR_LEVELING_SUCCESS) {
        return status;
    }

    if (length > 1) {
        status = wear_leveling_append_raw(log.raw16[2]);
        if (status != WEAR_LEVELING_SUCCESS) {
            return status;
        }
    }

    if (length > 3) {
        status = wear_leveling_append_raw(log.raw16[3]);
        if (status != WEAR_LEVELING_SUCCESS) {
            return status;
        }
    }
#elif BACKING_STORE_WRITE_SIZE == 4
    status = wear_leveling_append_raw(log.raw32[0]);
    if (status != WEAR_LEVELING_SUCCESS) {
        return status;
    }

    if (length > 1) {
        status = wear_leveling_append_raw(log.raw32[1]);
        if (status != WEAR_LEVELING_SUCCESS) {
            return status;
        }
    }
#elif BACKING_STORE_WRITE_SIZE == 8
    status = wear_leveling_append_raw(log.raw64);
    if (status != WEAR_LEVELING_SUCCESS) {
        return status;
    }
#endif
    return status;
}

/**
 * Handles the actual writing of logical data into the write log section of the backing store.
 */
static wear_leveling_status_t wear_leveling_write_raw(uint32_t address, const void *value, size_t length) {
    const uint8_t *        p         = value;
    size_t                 remaining = length;
    wear_leveling_status_t status    = WEAR_LEVELING_SUCCESS;
    while (remaining > 0) {
#if BACKING_STORE_WRITE_SIZE == 2
        // Small-write optimizations - uint16_t, 0 or 1, address is even, address <16384:
        if (remaining >= 2 && address % 2 == 0 && address < 16384) {
            const uint16_t v = *(const uint16_t *)p;
            if (v == 0 || v == 1) {
                const write_log_entry_t log = LOG_ENTRY_MAKE_WORD_01(address, v);
                status                      = wear_leveling_append_raw(log.raw16[0]);
                if (status != WEAR_LEVELING_SUCCESS) {
                    // If consolidation occurred, then the cache has already been written to the consolidated area. No need to continue.
                    // If a failure occurred, pass it on.
                    return status;
                }

                remaining -= 2;
                address += 2;
                p += 2;
                continue;
            }
        }

        // Small-write optimizations - address<64:
        if (address < 64) {
            const write_log_entry_t log = LOG_ENTRY_MAKE_OPTIMIZED_64(address, *p);
            status                      = wear_leveling_append_raw(log.raw16[0]);
            if (status != WEAR_LEVELING_SUCCESS) {
                // If consolidation occurred, then the cache has already been written to the consolidated area. No need to continue.
                // If a failure occurred, pass it on.
                return status;
            }

            remaining--;
            address++;
            p++;
            continue;
        }
#endif // BACKING_STORE_WRITE_SIZE == 2
        const size_t this_length = remaining >= LOG_ENTRY_MULTIBYTE_MAX_BYTES ? LOG_ENTRY_MULTIBYTE_MAX_BYTES : remaining;
        status                   = wear_leveling_write_raw_multibyte(address, p, this_length);
        if (status != WEAR_LEVELING_SUCCESS) {
            // If consolidation occurred, then the cache has already been written to the consolidated area. No need to continue.
            // If a failure occurred, pass it on.
            return status;
        }
        remaining -= this_length;
        address += (uint32_t)this_length;
        p += this_length;
    }

    return status;
}

/**
 * "Replays" the write log from the backing store, updating the local cache with updated values.
 */
static wear_leveling_status_t wear_leveling_playback_log(void) {
    wl_dprintf("Playback write log\n");

    wear_leveling_status_t status          = WEAR_LEVELING_SUCCESS;
    bool                   cancel_playback = false;
    uint32_t               address         = (WEAR_LEVELING_LOGICAL_SIZE) + 8; // +8 due to the FNV1a_64 of the consolidated area
    while (!cancel_playback && address < (WEAR_LEVELING_BACKING_SIZE)) {
        backing_store_int_t value;
        bool                ok = backing_store_read(address, &value);
        if (!ok) {
            wl_dprintf("Failed to load from backing store, skipping playback of write log\n");
            cancel_playback = true;
            status          = WEAR_LEVELING_FAILED;
            break;
        }
        if (value == 0) {
            wl_dprintf("Found empty slot, no more log entries\n");
            cancel_playback = true;
            break;
        }

        // If we got a nonzero value, then we need to increment the address to ensure next write occurs at next location
        address += (BACKING_STORE_WRITE_SIZE);

        // Read from the write log
        write_log_entry_t log;
#if BACKING_STORE_WRITE_SIZE == 2
        log.raw16[0] = value;
#elif BACKING_STORE_WRITE_SIZE == 4
        log.raw32[0] = value;
#elif BACKING_STORE_WRITE_SIZE == 8
        log.raw64 = value;
#endif

        switch (LOG_ENTRY_GET_TYPE(log)) {
            case LOG_ENTRY_TYPE_MULTIBYTE: {
#if BACKING_STORE_WRITE_SIZE == 2
                ok = backing_store_read(address, &log.raw16[1]);
                if (!ok) {
                    wl_dprintf("Failed to load from backing store, skipping playback of write log\n");
                    cancel_playback = true;
                    status          = WEAR_LEVELING_FAILED;
                    break;
                }
                address += (BACKING_STORE_WRITE_SIZE);
#endif // BACKING_STORE_WRITE_SIZE == 2
                const uint32_t a = LOG_ENTRY_MULTIBYTE_GET_ADDRESS(log);
                const uint8_t  l = LOG_ENTRY_MULTIBYTE_GET_LENGTH(log);

                if (a + l > (WEAR_LEVELING_LOGICAL_SIZE)) {
                    cancel_playback = true;
                    status          = WEAR_LEVELING_FAILED;
                    break;
                }

#if BACKING_STORE_WRITE_SIZE == 2
                if (l > 1) {
                    ok = backing_store_read(address, &log.raw16[2]);
                    if (!ok) {
                        wl_dprintf("Failed to load from backing store, skipping playback of write log\n");
                        cancel_playback = true;
                        status          = WEAR_LEVELING_FAILED;
                        break;
                    }
                    address += (BACKING_STORE_WRITE_SIZE);
                }
                if (l > 3) {
                    ok = backing_store_read(address, &log.raw16[3]);
                    if (!ok) {
                        wl_dprintf("Failed to load from backing store, skipping playback of write log\n");
                        cancel_playback = true;
                        status          = WEAR_LEVELING_FAILED;
                        break;
                    }
                    address += (BACKING_STORE_WRITE_SIZE);
                }
#elif BACKING_STORE_WRITE_SIZE == 4
                if (l > 1) {
                    ok = backing_store_read(address, &log.raw32[1]);
                    if (!ok) {
                        wl_dprintf("Failed to load from backing store, skipping playback of write log\n");
                        cancel_playback = true;
                        status = WEAR_LEVELING_FAILED;
                        break;
                    }
                    address += (BACKING_STORE_WRITE_SIZE);
                }
#endif

                memcpy(&wear_leveling.cache[a], &log.raw8[3], l);
            } break;
#if BACKING_STORE_WRITE_SIZE == 2
            case LOG_ENTRY_TYPE_OPTIMIZED_64: {
                const uint32_t a = LOG_ENTRY_OPTIMIZED_64_GET_ADDRESS(log);
                const uint8_t  v = LOG_ENTRY_OPTIMIZED_64_GET_VALUE(log);

                if (a >= (WEAR_LEVELING_LOGICAL_SIZE)) {
                    cancel_playback = true;
                    status          = WEAR_LEVELING_FAILED;
                    break;
                }

                wear_leveling.cache[a] = v;
            } break;
            case LOG_ENTRY_TYPE_WORD_01: {
                const uint32_t a = LOG_ENTRY_WORD_01_GET_ADDRESS(log);
                const uint8_t  v = LOG_ENTRY_WORD_01_GET_VALUE(log);

                if (a + 1 >= (WEAR_LEVELING_LOGICAL_SIZE)) {
                    cancel_playback = true;
                    status          = WEAR_LEVELING_FAILED;
                    break;
                }

                wear_leveling.cache[a + 0] = v;
                wear_leveling.cache[a + 1] = 0;
            } break;
#endif // BACKING_STORE_WRITE_SIZE == 2
            default: {
                cancel_playback = true;
                status          = WEAR_LEVELING_FAILED;
            } break;
        }
    }

    // We've reached the end of the log, so we're at the new write location
    wear_leveling.write_address = address;

    if (status == WEAR_LEVELING_FAILED) {
        // If we had a failure during readback, assume we're corrupted -- force a consolidation with the data we already have
        status = wear_leveling_consolidate_force();
    } else {
        // Consolidate the cache + write log if required
        status = wear_leveling_consolidate_if_needed();
    }

    return status;
}

/**
 * Wear-leveling initialization
 */
wear_leveling_status_t wear_leveling_init(void) {
    wl_dprintf("Init\n");

    // Reset the cache
    wear_leveling_clear_cache();

    // Initialise the backing store
    if (!backing_store_init()) {
        // If it failed, clear the cache and return with failure
        wear_leveling_clear_cache();
        return WEAR_LEVELING_FAILED;
    }

    // Read the previous consolidated values, then replay the existing write log so that the cache has the "live" values
    wear_leveling_status_t status = wear_leveling_read_consolidated();
    if (status == WEAR_LEVELING_FAILED) {
        // If it failed, clear the cache and return with failure
        wear_leveling_clear_cache();
        return status;
    }

    status = wear_leveling_playback_log();
    if (status == WEAR_LEVELING_FAILED) {
        // If it failed, clear the cache and return with failure
        wear_leveling_clear_cache();
        return status;
    }

    return status;
}

/**
 * Wear-leveling erase.
 * Post-condition: any reads from the backing store directly after an erase operation must come back as zero.
 */
wear_leveling_status_t wear_leveling_erase(void) {
    wl_dprintf("Erase\n");

    // Unlock the backing store
    backing_store_lock_status_t lock_status = wear_leveling_unlock();
    if (lock_status == STATUS_FAILURE) {
        wear_leveling_lock();
        return WEAR_LEVELING_FAILED;
    }

    // Perform the erase
    bool ret = backing_store_erase();
    wear_leveling_clear_cache();

    // Lock the backing store if we acquired the lock successfully
    if (lock_status == STATUS_SUCCESS) {
        ret &= (wear_leveling_lock() != STATUS_FAILURE);
    }

    return ret ? WEAR_LEVELING_SUCCESS : WEAR_LEVELING_FAILED;
}

/**
 * Writes logical data into the backing store. Skips writes if there are no changes to values.
 */
wear_leveling_status_t wear_leveling_write(const uint32_t address, const void *value, size_t length) {
    wl_assert(address + length <= (WEAR_LEVELING_LOGICAL_SIZE));
    if (address + length > (WEAR_LEVELING_LOGICAL_SIZE)) {
        return WEAR_LEVELING_FAILED;
    }

    wl_dprintf("Write ");
    wl_dump(address, value, length);

    // Skip write if there's no change compared to the current cached value
    if (memcmp(value, &wear_leveling.cache[address], length) == 0) {
        return true;
    }

    // Update the cache before writing to the backing store -- if we hit the end of the backing store during writes to the log then we'll force a consolidation in-line
    memcpy(&wear_leveling.cache[address], value, length);

    // Unlock the backing store
    backing_store_lock_status_t lock_status = wear_leveling_unlock();
    if (lock_status == STATUS_FAILURE) {
        wear_leveling_lock();
        return WEAR_LEVELING_FAILED;
    }

    // Perform the actual write
    wear_leveling_status_t status = wear_leveling_write_raw(address, value, length);
    switch (status) {
        case WEAR_LEVELING_CONSOLIDATED:
        case WEAR_LEVELING_FAILED:
            // If the write triggered consolidation, or the write failed, then nothing else needs to occur.
            break;

        case WEAR_LEVELING_SUCCESS:
            // Consolidate the cache + write log if required
            status = wear_leveling_consolidate_if_needed();
            break;

        default:
            // Unsure how we'd get here...
            status = WEAR_LEVELING_FAILED;
            break;
    }

    if (lock_status == STATUS_SUCCESS) {
        if (wear_leveling_lock() == STATUS_FAILURE) {
            status = WEAR_LEVELING_FAILED;
        }
    }

    return status;
}

/**
 * Reads logical data from the cache.
 */
wear_leveling_status_t wear_leveling_read(const uint32_t address, void *value, size_t length) {
    wl_assert(address + length <= (WEAR_LEVELING_LOGICAL_SIZE));
    if (address + length > (WEAR_LEVELING_LOGICAL_SIZE)) {
        return WEAR_LEVELING_FAILED;
    }

    // Only need to copy from the cache
    memcpy(value, &wear_leveling.cache[address], length);

    wl_dprintf("Read  ");
    wl_dump(address, value, length);
    return WEAR_LEVELING_SUCCESS;
}

/**
 * Weak implementation of bulk read, drivers can implement more optimised implementations.
 */
__attribute__((weak)) bool backing_store_read_bulk(uint32_t address, backing_store_int_t *values, size_t item_count) {
    for (size_t i = 0; i < item_count; ++i) {
        if (!backing_store_read(address + (i * BACKING_STORE_WRITE_SIZE), &values[i])) {
            return false;
        }
    }
    return true;
}

/**
 * Weak implementation of bulk write, drivers can implement more optimised implementations.
 */
__attribute__((weak)) bool backing_store_write_bulk(uint32_t address, backing_store_int_t *values, size_t item_count) {
    for (size_t i = 0; i < item_count; ++i) {
        if (!backing_store_write(address + (i * BACKING_STORE_WRITE_SIZE), values[i])) {
            return false;
        }
    }
    return true;
}