rigless 0.3.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
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
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The embedded flash memory interface of RM0090 rev 22 chapter 3, written against the `flash` layout.
//!
//! Chapter 3 gives every register twice: sections 3.9.1, 3.9.5, 3.9.7 and 3.9.9 are the
//! STM32F405xx/07xx and STM32F415xx/17xx's, and 3.9.2, 3.9.6, 3.9.8, 3.9.10 and 3.9.11 are the
//! STM32F42xxx and STM32F43xxx's, which is this SoC, the same trap chapter 6 sets against chapter 7
//! for the clock controller and 5.5 against 5.4 for the regulator. Every citation here is the second
//! of each pair.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Reset state | Zero wait states, nothing enabled, `CR` locked, the option registers as the option bytes left them | 3.5.1, 3.9.2, 3.9.8, 3.9.10, 3.9.11 | `reset_values_match_the_manual` |
//! | `ACR` `LATENCY` | Four bits on this family, corrected from the vendor's three | 3.9.2 | `a_latency_above_seven_reads_back_whole`, `the_corrected_field_is_what_the_part_declares` |
//! | `ACR` `LATENCY` held against the clock and the supply | Told, not refused, on the transition into a value short of table 12, whenever `ACR` is written or `HCLK` changes: the write has already landed by the time a model sees it, and no gate spans two instances' fields | 3.5.1, table 12 | `too_few_wait_states_for_the_clock_is_told`, `the_supply_narrows_the_table_the_same_way`, `the_column_edges_match_table_twelve` |
//! | `HCLK` past table 12's last row | Told: no wait-state count in the table reaches it, whatever `LATENCY` holds | 3.5.1, table 12 | `a_clock_past_the_tables_last_row_is_told` |
//! | `PRFTEN` set below 2.1 V | Told: 3.5.1 requires the prefetch buffer disabled below that rail | 3.5.1 | `prefetch_on_below_2_1_v_is_told` |
//! | `ACR` `PRFTEN`, `ICEN`, `DCEN` | Stored | 3.5.2 | `prften_icen_dcen_are_stored` |
//! | `ACR` `ICRST` | A write-only bit reads back its reset value, which is zero, so this model forces it back to zero on every write to `ACR` | 3.9.2, 1.1 | `icrst_always_reads_zero` |
//! | `ACR` `DCRST` | Stored while `DCEN` is clear. Section 3.9.2: "can be written only when the D cache is disabled", so a write while `DCEN` is set is held at its prior value | 3.9.2 | `dcrst_is_stored`, `dcrst_is_held_while_dcen_is_set` |
//! | `KEYR` unlock sequence | `KEY1` then `KEY2` clears `CR` `LOCK`. Anything else locks `CR` until reset and tells the session, once, on the word that does it | 3.6.1 | `the_unlock_sequence_clears_lock`, `a_wrong_key_locks_the_control_register_until_reset`, `a_wrong_first_key_locks_the_control_register_until_reset` |
//! | `CR` `LOCK` | Write-one-only, cleared only by the unlock sequence | 3.6.1, 3.9.8 | `the_unlock_sequence_clears_lock` |
//! | `CR` frozen while locked | Every other field of `CR` keeps the value it had. Section 3.6.1: "write is not allowed" until unlocked | 3.6.1 | `the_control_register_is_frozen_while_locked` |
//! | `CR`, `OPTCR`, `OPTCR1` written while `BSY` stands | Held and landed the instant `BSY` clears, the same write this model answers rather than one it ignored | 3.6.1, 3.9.10, 3.9.11 | `a_cr_write_made_while_busy_lands_when_the_operation_ends`, `an_optcr_write_made_while_busy_lands_when_the_operation_ends` |
//! | `CR` `SER`, `SNB`, `STRT` | Sector erase: `BSY` for the sector's own erase time by `PSIZE`, `STRT` clearing with it | 3.6.3, DS9405 rev 13 table 48 | `a_sector_erase_sets_and_clears_bsy`, `erase_time_follows_table_48_by_sector_size_and_psize` |
//! | `CR` `SNB` range | Sectors 0 to 11 and 16 to 27 raw are this part's twenty-four. The four gaps either side of that split are a write-protection error | 3.9.8, 3.7.4 | `an_invalid_sector_number_is_a_write_protection_error`, `a_bank_two_sector_erases_like_bank_one` |
//! | `CR` `MER`, `MER1`, `STRT` | Mass erase, either bank or both, timed by `PSIZE` alone since `tME` and `tBE` give the same figures. `STRT` with none of `SER`, `MER`, `MER1` set clears itself and starts nothing | 3.6.3, DS9405 rev 13 table 48 | `a_mass_erase_sets_and_clears_bsy`, `strt_with_nothing_selected_clears_itself_and_starts_nothing` |
//! | `CR` `SER` and `MER`/`MER1` together | 3.6.3's note gives a mass erase. 3.7.4's first bullet gives `WRPERR` for the same combination. This model follows 3.6.3, the sentence that describes the mechanism rather than the blanket rule | 3.6.3, 3.7.4 | `ser_with_mer_performs_a_mass_erase` |
//! | `CR` `PG`, `PSIZE` | Stored. `PSIZE` past what table 13 allows the supply is told when an erase starts, once per transition, since programming's own trigger is out of this model's reach, second table | 3.6.2, table 13 | `pg_and_psize_are_stored`, `an_erase_outside_table_thirteens_parallelism_is_told` |
//! | `HCLK` below 1 MHz at an erase | Told from `start_erase`: 3.6 requires at least 1 MHz for a program or erase operation | 3.6 | `an_erase_below_1_mhz_hclk_is_told` |
//! | `CR` `EOPIE`, `ERRIE` | Gate `EOP` and `OPERR` themselves, not only the interrupt, exactly as each bit's own page states | 3.9.6 | `eop_stands_only_while_eopie_is_set`, `operr_stands_only_while_errie_is_set` |
//! | `SR` `BSY` | Set for the operation's own duration, cleared at its end. A stale event id changes nothing, there being no cancel | 3.6.1, 3.6.3, 3.7.2 | `a_sector_erase_sets_and_clears_bsy`, `optstrt_sets_and_clears_bsy`, `a_stale_event_is_ignored` |
//! | `SR` `EOP` | Set behind `EOPIE` when `BSY` clears, correctly or not, since 3.6.6 states the interrupt condition is the operation ending either way, and 3.9.6's "successfully" would leave a firmware waiting on the `EOP` interrupt alone, with `ERRIE` clear, stuck on an erroring erase. Write-one-to-clear | 3.6.6, 3.7.2, 3.9.6 | `eop_stands_only_while_eopie_is_set`, `an_option_commit_also_raises_eop`, `eop_follows_3_6_6_and_stands_on_an_error_too` |
//! | `SR` `WRPERR` | Set on a write-protected sector or bank, an invalid `SNB`, or an option commit 3.7.5 cancels, whether or not `ERRIE` is set | 3.7.4, 3.7.5 | `an_invalid_sector_number_is_a_write_protection_error`, `nwrp_gates_erase_through_wrperr`, `unprotecting_without_rdp_leaving_level_1_cancels_the_commit` |
//! | `SR` `OPERR` | The meta flag: stands with any error this model can raise, behind `ERRIE` | 3.9.6 | `operr_stands_only_while_errie_is_set` |
//! | `SR` `RDERR` | The vendor's table omits it entirely, declared here since this family's own page gives it bit 8 | 3.9.6 | `rderr_is_declared` |
//! | `SR` write-one-to-clear | Every flag but `BSY`. The register-level correction covers `RDERR` too, since nothing else protects a declared field | 3.9.6 | `flags_clear_on_a_written_one` |
//! | Flash interrupt | One line, raised while `EOP` or `OPERR` stands and lowered the instant neither does, refreshed from every write this model answers | 3.6.6, table 14 | `the_interrupt_follows_eop_and_operr`, `clearing_eopie_lowers_the_line_from_a_cr_write`, `operr_raises_the_interrupt_when_errie_is_set` |
//! | `OPTKEYR` unlock sequence | `OPTKEY1` then `OPTKEY2` clears `OPTCR` `OPTLOCK`. Anything else locks the option registers until reset and tells the session, once | 3.7.2, 3.9.10 | `the_option_unlock_sequence_clears_optlock`, `a_wrong_option_key_locks_the_option_registers_until_reset` |
//! | `OPTCR` `OPTLOCK` | Write-one-only, cleared by hardware, section 3.9.10 bit 0 | 3.7.2, 3.9.10 | `the_option_unlock_sequence_clears_optlock` |
//! | `OPTCR`, `OPTCR1` frozen while locked | Every other field of both keeps its value while `OPTLOCK` stands | 3.7.2 | `the_option_registers_are_frozen_while_locked` |
//! | `OPTCR` `OPTSTRT` | Commits the option registers: `BSY` for the model's own programming time, and the committed words recorded for a reset to carry forward, unless 3.7.3's Level 2 or 3.7.5's cancel applies | 3.7.2, 3.9.10, 3.7.3, 3.7.5 | `optstrt_sets_and_clears_bsy`, `option_registers_survive_a_system_reset`, `level_2_freezes_a_commit`, `unprotecting_without_rdp_leaving_level_1_cancels_the_commit` |
//! | `OPTCR`, `OPTCR1` reset value | The option bytes as the last commit left them, kept across a system reset by [`Flash::keep_options`] rather than by copying whatever the registers held at the instant of the reset | 3.9.10, 3.9.11, 6.1.1 | `option_registers_survive_a_system_reset`, `a_reset_mid_commit_keeps_the_last_completed_words` |
//! | `OPTCR` `RDP` from Level 1 to Level 0 | Mass-erases the backup SRAM at the commit that lands it, table 18's footnote. The array's own half is left out | 3.7.3, table 18 | `unlocking_to_level_zero_is_told_apart_from_other_option_commits` |
//! | `OPTCR` `RDP`, `BOR_LEV`, `WDG_SW`, `nRST_STOP`, `nRST_STDBY`, `DB1M`, `SPRMOD`, `OPTCR`/`OPTCR1` `nWRP` | Stored and read back live. `nWRP` and `SPRMOD` gate an erase through `WRPERR` | 3.9.10, 3.9.11, 3.7.1, 3.7.4, 3.7.5 | `nwrp_gates_erase_through_wrperr`, `sprmod_flips_which_nwrp_value_protects`, `rdp_wdg_sw_nrst_stdby_and_db1m_are_stored` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | Programming a word, and the true trigger of `PGAERR`, `PGPERR`, `PGSERR`, and `RDERR` | 3.6.4, 3.6.6, 3.7.5 | Every one of these is set by an access to the flash array itself, at `0x0800_0000`, which the core maps read-only. It drops a firmware's store there, so no model, this one included, is ever called for one, and a model's own write there through the host's guest memory is refused as the bus refuses one. Programming needs a way for a model to write the array, which is still to come |
//! | The array's own content, main memory and OTP alike: an erase does not set its sectors to all ones, a program does not deposit its bytes, and a locked OTP block does not refuse a second program with `WRPERR` | 3.6.3, 3.6.4, 3.8 | The same mapping, covering the OTP area (3.8, table 19) as well as `0x0800_0000`: both are the flash array the core maps read-only. `BSY`, `STRT`, `EOP`, and every error this model raises from register state alone are real. What a firmware reads back from the array or the OTP area afterward is what the run started with: the loaded image, and erased flash wherever no image fills it |
//! | `RDP`'s protection itself: JTAG, boot from RAM, and blocking erase or program under Level 1 or 2 | 3.7.3, table 18 | Debug gating is the core's and boot mode is the board's `BOOT`, neither a register this peripheral owns |
//! | `DB1M`'s effect on sector numbering | 3.4, table 7 | Table 7 is 1 Mbyte devices only. This part's memory map fixes a 2 Mbyte array, which table 6 always organizes the way `sector_of` reads it, so the bit is stored and changes nothing here |
//! | `BFB2` dual-bank boot | 3.7.1 | A machine's `boot` names what answers at the reset vector, and the system memory holds no bootloader here for `BFB2 1` to select, so a part committed with `BFB2 1` boots what its machine names regardless |
//! | Section 3.6.1's stall: a write to `CR`, `OPTCR` or `OPTCR1` while `BSY` stands "causes the AHB bus to stall" | 3.6.1, 3.9.10, 3.9.11 | Stalling the core mid-instruction is outside what the runtime can ask the engine to do. This model holds the write instead and lands it the instant `BSY` clears, which is the row above rather than a gap. The gap that remains is the stall itself, which no firmware sequence in the manual depends on observing |
//! | Section 3.6.1's bus error on a wrong unlock key | 3.6.1 | The write has already landed by the time a model is called, so there is nothing to refuse. The model tells the session instead of faulting the core. A local run prints it, and the link carries it to an engine in `Reached` |
//! | A write to `KEYR` or `OPTKEYR` once its register is already unlocked | 3.6.1, 3.9.3, 3.7.2, 3.9.4 | Neither page says what such a write does. This model treats it as nothing to do, which is the reading that cannot brick a sequence that already succeeded |
//! | 3.6 and 3.6.5: a read of one bank stalling while the other is erased or programmed, and the other bank answering meanwhile | 3.6, 3.6.5 | The same core-stall the row above already declines, and the array's own content besides |
//! | 3.6.2 and table 13's `VPP` column, x64 at 8 to 9 V for at most an hour | 3.6.2, table 13 | `VPP` is the `BOOT0` pad held at 8 to 9 V, DS9405 rev 13, which a programming fixture does and a running board doesn't. A machine has no input for it, so `PSIZE` x64 is told as past table 13 at any supply |
//! | 3.5.2 and 3.6.4: the ART's prefetch, its 64-line and 8-line caches, cache updates on a program, and stale lines after an erase until `ICRST`/`DCRST` | 3.5.2, 3.6.4 | `PRFTEN`, `ICEN` and `DCEN` qualify instruction and data timing, which the runtime doesn't model cycle by cycle |
//! | 3.6's undefined contents after a reset during an operation | 3.6 | A reset rebuilds the part fresh and drops whatever the old part had pending, which is one honest choice among the several the manual leaves open |
//! | 3.7.1, table 15: the option bytes readable as memory at `0x1FFF_C000` and `0x1FFE_C000` | 3.7.1, table 15 | `memory_map.rs`'s system region ends at `0x1FFF_8000` and never reaches `0x1FFE_C000` either, so both addresses are unmapped today. Answering them needs a memory region a model's live register state backs, which nothing in the runtime does yet |
//! | 3.9.10 `BOR_LEV` and 6.1.2: a supply below the selected level resets the part | 3.9.10, 6.1.2 | The supply is `Pwr`'s own input, and a reset it drives belongs beside `Pwr`'s existing brownout detector, not duplicated here |
//! | 3.7.1 and 6.1.1: `nRST_STOP`/`nRST_STDBY` resetting the part on Stop or Standby entry instead of entering it | 3.7.1, 6.1.1 | This model has no Stop or Standby to reset out of yet |
//! | `OPTCR` `WDG_SW` starting the independent watchdog automatically at reset when it selects the hardware option | 3.7.1, 3.9.10, 21.3.1 | Section 21.3.1: "If the 'Hardware watchdog' feature is enabled through the device option bits, the watchdog is automatically enabled at power-on." `WDG_SW` is stored here and kept across a system reset. Starting `IWDG` from it needs a model to act at the instant of a reset, which none can yet |
//! | RDP Level 1 to Level 0 mass-erasing the flash array half of table 18's footnote | 3.7.3, table 18 | The same array mapping. The backup SRAM half is modeled, below |
//! | ES0206 rev 24 2.2.3: unreachable instructions after `WFI`/`WFE` wake with the prefetch off, `LATENCY` above zero, a 4-byte-aligned `WFI`/`WFE`, and `DBG_SLEEP`/`DBG_STOP` set | ES0206 rev 24 | Applicable at every revision listed in table 3, `A` from rev A through rev B, including the 4/5/B this part reports. Its trigger is the core's instruction fetch pipeline racing the prefetch queue after a debug-held sleep, which needs `DBGMCU_CR` state this peripheral doesn't own and an effect on instruction fetch the runtime has no path to model |
//! | ES0206 rev 24 2.2.14: flash reads corrupt while `PA12` is a GPIO or alternate function, at 2.7 to 3.6 V, with bank 2 in use, on `STM32F42xxI` and `STM32F43xxI` (and `STM32F42xxG`/`STM32F43xxG` only with dual-bank enabled) | ES0206 rev 24 | Not applicable to the revisions this part reports (table 3 marks it `A` for revisions A, Y and 1 only, and the errata text says "fixed in silicon starting from revision 3"). Recorded rather than deleted, since the fact still holds for the revisions table 3 marks. `PA12`'s mode is `GPIOA`'s and reproducing the corruption needs the array's content regardless |
//! | ES0206 rev 24 2.2.15: the data cache may corrupt on a read from one bank while the other is written | ES0206 rev 24 | Applicable at every revision in table 3, `A` from rev A through rev B, including the 4/5/B this part reports. Its trigger is a read of the array during a write to it, the same unreachable access |
//!
//! **Where this model departs from the vendor's file.** `ACR` `LATENCY` is three bits in the SVD and
//! four in section 3.9.2. [`LATENCY`] and the `Builder::field` call in `memory_map.rs` carry the
//! correction. `SR` omits `RDERR` entirely, which section 3.9.6 gives bit 8. [`RDERR`] and a
//! `Builder::declare` call restore it. `CR` `LOCK` and `STRT`, and `OPTCR` `OPTLOCK` and `OPTSTRT`,
//! are plain read-write in the SVD where their own pages type them "write to 1 only, cleared by
//! hardware", so [`LOCK`], [`STRT`], [`OPTLOCK`] and [`OPTSTRT`] restate all four as write-one-to-set,
//! and the model clears them itself from inside its own effects. `SR`'s flags carry no write rule in
//! the SVD, where section 3.9.6 clears each but `BSY` on a written one, and a `Builder::write_modify`
//! call in `memory_map.rs` gives the register that rule.
//!
//! **Determinism.** `two_runs_of_one_script_are_the_same_run` drives two fresh instances through a
//! bricked `KEYR`, a `LATENCY` left short across a clock change, and a sector erase run to completion,
//! and checks `told`, the interrupt line and every register agree between the two.

use crate::device::{Device, DeviceContext, Level, RegisterFile};
use crate::soc::stm32f427::generated::flash::{Register, acr, cr, optcr, optcr1, sr};
use crate::soc::stm32f427::memory_map::SECTORS;
use crate::soc::{Access, FieldSpec, Layout, Width, WriteModify};
use crate::time::{EventId, VirtualNanos};
use std::num::NonZeroU32;

/// The flash interrupt, position zero of its instance's table. RM0090 rev 22 table 63 gives it line 4.
const IRQ: usize = 0;

/// How long the model takes to commit the option registers. RM0090 rev 22 section 3.7.2's note makes
/// a commit an erase of the user configuration sector then a program of every option byte, and
/// DS9405 rev 13 table 48 times neither for that sector, so this figure is the model's own with no
/// source behind it. It's far shorter than any erase the table does time: a firmware's own timeout
/// on `OPTSTRT` is never what ends the wait here.
const OPTION_PROGRAM_NANOS: u64 = 20_000;

/// RM0090 rev 22 section 3.6.1: `KEY1` then `KEY2` in `FLASH_KEYR` unlocks `FLASH_CR`.
const CR_KEY1: u32 = 0x4567_0123;
const CR_KEY2: u32 = 0xCDEF_89AB;
/// RM0090 rev 22 section 3.7.2: `OPTKEY1` then `OPTKEY2` in `FLASH_OPTKEYR` unlocks `FLASH_OPTCR`.
const OPT_KEY1: u32 = 0x0819_2A3B;
const OPT_KEY2: u32 = 0x4C5D_6E7F;

/// `LATENCY`, RM0090 rev 22 section 3.9.2.
pub static LATENCY: FieldSpec = FieldSpec::new("LATENCY", 0, 4, Access::ReadWrite);
/// `LOCK`, RM0090 rev 22 section 3.9.8: "Write to 1 only... cleared by hardware after detecting the
/// unlock sequence."
pub static LOCK: FieldSpec =
    FieldSpec::new("LOCK", 31, 1, Access::ReadWrite).with_write(WriteModify::OneToSet);
/// `STRT`, RM0090 rev 22 section 3.9.8: "set only by software and cleared when the BSY bit is cleared."
pub static STRT: FieldSpec =
    FieldSpec::new("STRT", 16, 1, Access::ReadWrite).with_write(WriteModify::OneToSet);
/// `OPTLOCK`, RM0090 rev 22 section 3.9.10, the same rule as [`LOCK`].
pub static OPTLOCK: FieldSpec =
    FieldSpec::new("OPTLOCK", 0, 1, Access::ReadWrite).with_write(WriteModify::OneToSet);
/// `OPTSTRT`, RM0090 rev 22 section 3.9.10, the same rule as [`STRT`].
pub static OPTSTRT: FieldSpec =
    FieldSpec::new("OPTSTRT", 1, 1, Access::ReadWrite).with_write(WriteModify::OneToSet);
/// `RDERR`, RM0090 rev 22 section 3.9.6 bit 8, which the vendor's table leaves out because it
/// transcribes the STM32F405's shorter `FLASH_SR`, section 3.9.5.
pub static RDERR: FieldSpec = FieldSpec::new("RDERR", 8, 1, Access::ReadWrite);

/// Every field this model declares because the vendor's file omits it. See
/// [`super::rcc::DECLARED`](super::rcc).
pub static DECLARED: &[&FieldSpec] = &[&RDERR];

/// Table 12, RM0090 rev 22 section 3.5.1: the greatest `HCLK` in hertz each wait-state count reaches,
/// one row per voltage range column, in ascending wait states.
const WAIT_STATE_CEILING_HZ: [[u64; 9]; 4] = [
    // 2.7 to 3.6 V.
    [
        30_000_000,
        60_000_000,
        90_000_000,
        120_000_000,
        150_000_000,
        180_000_000,
        180_000_000,
        180_000_000,
        180_000_000,
    ],
    // 2.4 to 2.7 V.
    [
        24_000_000,
        48_000_000,
        72_000_000,
        96_000_000,
        120_000_000,
        144_000_000,
        168_000_000,
        180_000_000,
        180_000_000,
    ],
    // 2.1 to 2.4 V.
    [
        22_000_000,
        44_000_000,
        66_000_000,
        88_000_000,
        110_000_000,
        132_000_000,
        154_000_000,
        176_000_000,
        180_000_000,
    ],
    // 1.8 to 2.1 V, prefetch off.
    [
        20_000_000,
        40_000_000,
        60_000_000,
        80_000_000,
        100_000_000,
        120_000_000,
        140_000_000,
        160_000_000,
        168_000_000,
    ],
];

/// The column of [`WAIT_STATE_CEILING_HZ`] a supply falls in. RM0090 rev 22 section 3.5.1's four
/// ranges, each named by its lower bound.
fn column(supply_mv: NonZeroU32) -> usize {
    match supply_mv.get() {
        2_700.. => 0,
        2_400..2_700 => 1,
        2_100..2_400 => 2,
        _ => 3,
    }
}

/// The fewest wait states table 12 allows at `hclk_hz` on `supply_mv`, or nothing where the supply is
/// unknown or `hclk_hz` is past the column's last row.
fn minimum_wait_states(hclk_hz: u64, supply_mv: Option<NonZeroU32>) -> Option<u32> {
    let ceilings = WAIT_STATE_CEILING_HZ[column(supply_mv?)];
    if hclk_hz > ceilings[8] {
        return None;
    }
    Some(u32::try_from(ceilings.partition_point(|&ceiling| ceiling < hclk_hz)).expect("nine rows"))
}

/// Table 13, RM0090 rev 22 section 3.6.2: the greatest `PSIZE` the supply allows, without the external
/// `VPP` column, DS9405 rev 13's name for the `BOOT0` pad, which only a programming fixture drives. `pdftotext
/// -layout` splits table 13's merged x16 cell across the 2.4-2.7 V and 2.1-2.4 V columns into two. DS9405
/// rev 13 table 18 ("16-bit erase and program operations" at both 2.1-2.4 V and 2.4-2.7 V) and table 48
/// (`Vprog` 16-bit, 2.1 to 3.6 V) agree it is one cell.
fn max_psize(supply_mv: NonZeroU32) -> u64 {
    match column(supply_mv) {
        0 => 0b10,
        1 | 2 => 0b01,
        _ => 0b00,
    }
}

/// The sector `SNB` names, RM0090 rev 22 section 3.9.8: 0 to 11 raw are sectors 0 to 11, 16 to 27 raw
/// are sectors 12 to 23, and the two four-value gaps either side are not allowed.
fn sector_of(raw: u32) -> Option<u32> {
    match raw {
        0..=11 => Some(raw),
        16..=27 => Some(raw - 4),
        _ => None,
    }
}

/// The size in bytes of `sector`, RM0090 rev 22 table 6, which the memory map's flash sectors are.
fn sector_bytes(sector: u32) -> u64 {
    let mut left = sector;
    for run in &SECTORS {
        if left < run.count.get() {
            return run.size.get();
        }
        left -= run.count.get();
    }
    unreachable!("sector {sector} is past table 6's twenty-four, which sector_of refuses")
}

/// DS9405 rev 13 table 48's typical sector erase time in milliseconds, by size (16, 64, 128 Kbytes)
/// and `PSIZE` (x8, x16, x32 in that order, `PSIZE`'s own encoding).
const SECTOR_ERASE_MS: [[u64; 3]; 3] = [[400, 300, 250], [1_200, 700, 550], [2_000, 1_300, 1_000]];

/// DS9405 rev 13 table 48's typical bank or mass erase time in milliseconds, by `PSIZE`: `tME` and
/// `tBE` give the same figures, so this model does not distinguish a mass erase from a single bank's.
const BANK_ERASE_MS: [u64; 3] = [16_000, 11_000, 8_000];

/// `PSIZE`'s own two bits as an index into a table's x8/x16/x32 columns, clamped past x32 since this
/// model has no `VPP` pin for x64.
fn psize_index(cr_value: u64) -> usize {
    usize::try_from(cr::PSIZE.get(cr_value))
        .expect("two bits")
        .min(2)
}

/// DS9405 rev 13 table 48's typical time for erasing `sector`, in nanoseconds, at `psize`. This model
/// is not kinder than the part: a firmware's own erase deadline or watchdog has to survive this wait
/// the way it would survive the part's.
fn sector_erase_nanos(sector: u32, cr_value: u64) -> u64 {
    let row = match sector_bytes(sector) {
        n if n <= 16 << 10 => 0,
        n if n <= 64 << 10 => 1,
        _ => 2,
    };
    SECTOR_ERASE_MS[row][psize_index(cr_value)] * 1_000_000
}

/// DS9405 rev 13 table 48's typical time for a bank or mass erase, in nanoseconds, at `psize`.
fn bank_erase_nanos(cr_value: u64) -> u64 {
    BANK_ERASE_MS[psize_index(cr_value)] * 1_000_000
}

/// Whether `sector` is protected against erase, RM0090 rev 22 sections 3.7.4 and 3.7.5: `nWRPi` clear
/// blocks it unless `SPRMOD` is set, in which case `nWRPi` set does instead.
fn protected(sector: u32, registers: &RegisterFile) -> bool {
    let sprmod = optcr::SPRMOD.is_set(registers.get(Register::Optcr));
    let bit_set = if sector <= 11 {
        (optcr::NWRP.get(registers.get(Register::Optcr)) >> sector) & 1 == 1
    } else {
        (optcr1::NWRP.get(registers.get(Register::Optcr1)) >> (sector - 12)) & 1 == 1
    };
    sprmod == bit_set
}

/// The `nWRP`/`nWRP1` bits across both registers, as one word, sector 0 at bit 0 through sector 23 at
/// bit 23: what 3.7.5's cancel rule compares before and after a commit.
fn all_nwrp(optcr: u64, optcr1: u64) -> u32 {
    let low = u32::try_from(optcr::NWRP.get(optcr)).expect("twelve bits");
    let high = u32::try_from(optcr1::NWRP.get(optcr1)).expect("twelve bits");
    low | (high << 12)
}

/// `RDP`'s protection level, RM0090 rev 22 section 3.7.3: `0xAA` is Level 0, `0xCC` is Level 2, and
/// every other value is Level 1.
fn rdp_level(optcr: u64) -> u8 {
    match optcr::RDP.get(optcr) {
        0xAA => 0,
        0xCC => 2,
        _ => 1,
    }
}

/// What the model has scheduled the completion of.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum Operation {
    Erase,
    Option,
}

/// Progress through a two-word unlock sequence, RM0090 rev 22 sections 3.6.1 and 3.7.2: `KEY1` then
/// `KEY2` in order, and anything else locks the sequence's register until the next reset.
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct KeyLock {
    expecting_second: bool,
    bricked: bool,
}

impl KeyLock {
    /// Whether `written` unlocks the sequence.
    fn take(&mut self, written: u64, key1: u32, key2: u32) -> bool {
        if self.bricked {
            return false;
        }
        if self.expecting_second {
            self.expecting_second = false;
            if written == u64::from(key2) {
                return true;
            }
        } else if written == u64::from(key1) {
            self.expecting_second = true;
            return false;
        }
        self.bricked = true;
        false
    }
}

/// What a write must not change unless it is the write doing the changing: a model sees the value
/// already landed and not the one before it, so freezing a register while locked, or holding a write
/// made while `BSY` stands, needs the instant before this write to restore to.
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
struct Shadow {
    acr: u64,
    cr: u64,
    optcr: u64,
    optcr1: u64,
}

impl Shadow {
    fn from_registers(registers: &RegisterFile) -> Self {
        Self {
            acr: registers.get(Register::Acr),
            cr: registers.get(Register::Cr),
            optcr: registers.get(Register::Optcr),
            optcr1: registers.get(Register::Optcr1),
        }
    }
}

/// The option words as the last completed commit left them, RM0090 rev 22 section 3.9.10's "loaded
/// with values from flash memory at reset release", carried across a reset rather than whatever
/// `OPTCR` and `OPTCR1` hold at the instant it happens, which may be an uncommitted edit or a commit
/// still in flight.
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
struct Committed {
    optcr: u64,
    optcr1: u64,
}

/// A write to `CR`, `OPTCR` or `OPTCR1` that arrived while `BSY` stood, held for landing when the
/// operation ends: RM0090 rev 22 sections 3.9.10 and 3.9.11 give both option registers the same "no
/// wait state when no flash memory operation is ongoing" access line as `CR`, and 3.6.1's note makes a
/// write during that wait a stall that lands once `BSY` clears, not one silently dropped.
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
struct Held {
    #[serde(with = "row")]
    register: Register,
    value: u64,
}

/// Serde for the register a held write names, as its row in the table, since the generated tables
/// carry no serde of their own.
mod row {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    use super::Register;
    use crate::soc::Layout;

    #[expect(
        clippy::trivially_copy_pass_by_ref,
        reason = "serde's `with` hands the field over by reference"
    )]
    pub(super) fn serialize<S: Serializer>(
        register: &Register,
        serializer: S,
    ) -> Result<S::Ok, S::Error> {
        register.index().serialize(serializer)
    }

    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
        deserializer: D,
    ) -> Result<Register, D::Error> {
        let index = usize::deserialize(deserializer)?;
        Register::from_index(index).ok_or_else(|| {
            serde::de::Error::custom(format_args!("the interface has no register at row {index}"))
        })
    }
}

/// The embedded flash memory interface of one STM32F42xxx, RM0090 rev 22 chapter 3.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[expect(
    clippy::struct_excessive_bools,
    reason = "five conditions each told on its own transition, not a state machine over one another"
)]
pub struct Flash {
    hclk_hz: u64,
    supply_mv: Option<NonZeroU32>,
    cr_key: KeyLock,
    opt_key: KeyLock,
    pending: Option<(EventId, Operation)>,
    held: Option<Held>,
    shadow: Shadow,
    committed: Committed,
    /// The level last driven, so the line is reported only on a change, never driven a second time
    /// for landing on the same level it already stood at.
    interrupt: Level,
    /// Whether the session was already told the latency is short of table 12, so a firmware that
    /// leaves it short is told once rather than on every access.
    told_latency: bool,
    /// Whether the session was already told `HCLK` is past table 12's last row.
    told_out_of_range: bool,
    /// Whether the session was already told the prefetch buffer is on below 2.1 V.
    told_prefetch: bool,
    /// Whether the session was already told an erase started with `PSIZE` past what the supply allows.
    told_parallelism: bool,
    /// Whether the session was already told an erase started with `HCLK` below 1 MHz.
    told_hclk_low: bool,
}

impl Flash {
    /// The interface fresh out of reset, on the clock the tree drives every bus at until a firmware
    /// touches it, with no supply until a board sets one.
    ///
    /// `shadow` and `committed` are built here from each register's own
    /// [`RegisterSpec::reset`](crate::soc::RegisterSpec::reset) rather than from a live register
    /// file, because none exists until after this call returns: a model is constructed first and
    /// wired into its instance's file second. By the time [`Device::write`] gives this model a
    /// [`RegisterFile`], that file already holds what the current write landed (its own doc says
    /// so), so there is no call inside a `Device` method that ever sees the state before a first
    /// write the way this constructor does. `Default` is not implemented for either: this is the
    /// one place either is built.
    #[must_use]
    pub fn with_clock(hclk_hz: u64) -> Self {
        Self {
            hclk_hz,
            supply_mv: None,
            cr_key: KeyLock::default(),
            opt_key: KeyLock::default(),
            pending: None,
            held: None,
            shadow: Shadow {
                acr: Register::Acr.spec().reset,
                cr: Register::Cr.spec().reset,
                optcr: Register::Optcr.spec().reset,
                optcr1: Register::Optcr1.spec().reset,
            },
            committed: Committed {
                optcr: Register::Optcr.spec().reset,
                optcr1: Register::Optcr1.spec().reset,
            },
            interrupt: Level::Low,
            told_latency: false,
            told_out_of_range: false,
            told_prefetch: false,
            told_parallelism: false,
            told_hclk_low: false,
        }
    }

    /// The board's supply rail, in millivolts: table 12's other axis, alongside `HCLK`, where nothing
    /// having set one yet is `column` not having an answer, rather than a violation.
    pub fn set_supply(&mut self, millivolts: NonZeroU32) {
        self.supply_mv = Some(millivolts);
    }

    /// The supply this instance was last told, for a system reset to carry to the part it rebuilds.
    #[must_use]
    pub const fn supply(&self) -> Option<NonZeroU32> {
        self.supply_mv
    }

    /// A system reset carrying the option words the last completed commit left, RM0090 rev 22 section
    /// 3.9.10, rather than whatever `OPTCR` and `OPTCR1` held at the instant of the reset: `OPTLOCK`
    /// comes back set and `OPTSTRT` comes back clear, since a reset is never mid-commit.
    pub fn keep_options(&mut self, optcr: u64, optcr1: u64, registers: &mut RegisterFile) {
        let optcr = OPTLOCK.set(OPTSTRT.set(optcr, 0), 1);
        registers.set(Register::Optcr, optcr);
        registers.set(Register::Optcr1, optcr1);
        self.committed = Committed { optcr, optcr1 };
        self.shadow = Shadow::from_registers(registers);
    }

    /// The option words the last completed commit left, for a system reset to carry to the part it
    /// rebuilds through [`Flash::keep_options`].
    #[must_use]
    pub const fn committed_options(&self) -> (u64, u64) {
        (self.committed.optcr, self.committed.optcr1)
    }
}

impl Device for Flash {
    type Register = Register;

    fn write(
        &mut self,
        register: Register,
        written: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        match register {
            Register::Acr => self.write_acr(registers, context),
            Register::Keyr => self.write_keyr(written, registers, context),
            Register::Optkeyr => self.write_optkeyr(written, registers, context),
            Register::Sr => self.refresh_interrupt(registers, context),
            Register::Cr => self.write_cr(registers, context),
            Register::Optcr | Register::Optcr1 => self.write_optcr(register, registers, context),
        }
        self.shadow = Shadow::from_registers(registers);
    }

    fn on_event(
        &mut self,
        event: EventId,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let Some((expected, operation)) = self.pending else {
            return;
        };
        if expected != event {
            return;
        }
        self.pending = None;
        let sr_value = sr::BSY.set(registers.get(Register::Sr), 0);
        let sr_value = if cr::EOPIE.is_set(registers.get(Register::Cr)) {
            sr::EOP.set(sr_value, 1)
        } else {
            sr_value
        };
        registers.set(Register::Sr, sr_value);
        match operation {
            Operation::Erase => {
                let cr_value = STRT.set(registers.get(Register::Cr), 0);
                registers.set(Register::Cr, cr_value);
            }
            Operation::Option => {
                let optcr_value = OPTSTRT.set(registers.get(Register::Optcr), 0);
                registers.set(Register::Optcr, optcr_value);
                self.committed = Committed {
                    optcr: registers.get(Register::Optcr),
                    optcr1: registers.get(Register::Optcr1),
                };
            }
        }
        self.refresh_interrupt(registers, context);
        self.shadow = Shadow::from_registers(registers);
        self.land_held(registers, context);
    }

    fn on_clock(&mut self, hz: u64, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
        self.hclk_hz = hz;
        self.check_latency(registers, context);
    }
}

impl Flash {
    /// A write to `CR`, `OPTCR` or `OPTCR1` held for `BSY`, applied as if it had just arrived: RM0090
    /// rev 22 sections 3.9.10, 3.9.11 and 3.6.1's stall note.
    fn land_held(&mut self, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
        let Some(held) = self.held.take() else {
            return;
        };
        registers.set(held.register, held.value);
        match held.register {
            Register::Cr => self.write_cr(registers, context),
            Register::Optcr | Register::Optcr1 => {
                self.write_optcr(held.register, registers, context);
            }
            _ => {}
        }
    }

    /// `ACR`: force `ICRST` back to zero, hold `DCRST` while `DCEN` stands, section 3.9.2, and hold
    /// `LATENCY` to what the clock and the supply need.
    fn write_acr(&mut self, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
        if acr::DCEN.is_set(self.shadow.acr) {
            let held = acr::DCRST.set(
                registers.get(Register::Acr),
                u64::from(acr::DCRST.is_set(self.shadow.acr)),
            );
            registers.set(Register::Acr, held);
        }
        let value = acr::ICRST.set(registers.get(Register::Acr), 0);
        registers.set(Register::Acr, value);
        self.check_latency(registers, context);
    }

    /// RM0090 rev 22 section 3.5.1 table 12: told, not refused, since the write has already landed
    /// and the supply is another instance's board input rather than a bit this model can gate on.
    /// Told once, on the transition into the condition, the way an interrupt line is driven once per
    /// edge.
    fn check_latency(&mut self, registers: &RegisterFile, context: &mut DeviceContext<'_>) {
        let minimum = minimum_wait_states(self.hclk_hz, self.supply_mv);
        let short = minimum.is_some_and(|minimum| {
            let programmed =
                u32::try_from(LATENCY.get(registers.get(Register::Acr))).expect("four bits");
            programmed < minimum
        });
        if short && !self.told_latency {
            context.tell(
                Register::Acr.spec().name,
                "RM0090 rev 22 section 3.5.1 table 12: LATENCY is short of what HCLK and the supply require",
            );
        }
        self.told_latency = short;

        let out_of_range = self.supply_mv.is_some() && minimum.is_none();
        if out_of_range && !self.told_out_of_range {
            context.tell(
                Register::Acr.spec().name,
                "RM0090 rev 22 section 3.5.1 table 12: HCLK is past this column's highest wait-state row",
            );
        }
        self.told_out_of_range = out_of_range;

        let prefetch_off_required = self.supply_mv.is_some_and(|mv| mv.get() < 2_100);
        let prefetch_on = acr::PRFTEN.is_set(registers.get(Register::Acr));
        let should_tell_prefetch = prefetch_off_required && prefetch_on;
        if should_tell_prefetch && !self.told_prefetch {
            context.tell(
                Register::Acr.spec().name,
                "RM0090 rev 22 section 3.5.1: the prefetch buffer must be disabled when the supply voltage is below 2.1 V",
            );
        }
        self.told_prefetch = should_tell_prefetch;
    }

    /// `KEYR`: RM0090 rev 22 section 3.6.1's sequence, only meaningful while `CR` is locked.
    fn write_keyr(
        &mut self,
        written: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        if !LOCK.is_set(registers.get(Register::Cr)) {
            return;
        }
        let was_bricked = self.cr_key.bricked;
        if self.cr_key.take(written, CR_KEY1, CR_KEY2) {
            let value = LOCK.set(registers.get(Register::Cr), 0);
            registers.set(Register::Cr, value);
        } else if self.cr_key.bricked && !was_bricked {
            context.tell(
                Register::Keyr.spec().name,
                "RM0090 rev 22 section 3.6.1: a wrong unlock sequence locks the control register until reset",
            );
        }
    }

    /// `OPTKEYR`: the same sequence, section 3.7.2, over `OPTCR`'s `OPTLOCK`.
    fn write_optkeyr(
        &mut self,
        written: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        if !OPTLOCK.is_set(registers.get(Register::Optcr)) {
            return;
        }
        let was_bricked = self.opt_key.bricked;
        if self.opt_key.take(written, OPT_KEY1, OPT_KEY2) {
            let value = OPTLOCK.set(registers.get(Register::Optcr), 0);
            registers.set(Register::Optcr, value);
        } else if self.opt_key.bricked && !was_bricked {
            context.tell(
                Register::Optkeyr.spec().name,
                "RM0090 rev 22 section 3.9.10: a wrong unlock sequence locks the option registers until reset",
            );
        }
    }

    /// `CR`: frozen while locked, section 3.6.1. A write made while `BSY` stands is held rather than
    /// dropped, sections 3.9.10, 3.9.11 and 3.6.1's stall note, and a rising `STRT` starts an erase.
    fn write_cr(&mut self, registers: &mut RegisterFile, context: &mut DeviceContext<'_>) {
        if LOCK.is_set(self.shadow.cr) {
            registers.restore(Register::Cr, !LOCK.mask(), self.shadow.cr);
        } else if sr::BSY.is_set(registers.get(Register::Sr)) {
            self.held = Some(Held {
                register: Register::Cr,
                value: registers.get(Register::Cr),
            });
            registers.restore(Register::Cr, !LOCK.mask(), self.shadow.cr);
        } else {
            let value = registers.get(Register::Cr);
            if STRT.is_set(value) && !STRT.is_set(self.shadow.cr) {
                self.start_erase(value, registers, context);
            }
        }
        self.refresh_interrupt(registers, context);
    }

    /// The erase section 3.6.3 describes: mass erase where either `MER` bit is set, sector erase
    /// otherwise, each refused with `WRPERR` under section 3.7.4's own conditions and timed by DS9405
    /// rev 13 table 48. `STRT` set with none of `SER`, `MER`, `MER1` clears itself, section 3.6.3's
    /// "unpredictable behavior... should be forbidden" left as starting nothing rather than choosing
    /// among the behaviors it forbids relying on.
    fn start_erase(
        &mut self,
        cr_value: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let nanos = if cr::MER.is_set(cr_value) || cr::MER1.is_set(cr_value) {
            let refused = (cr::MER.is_set(cr_value)
                && (0..=11).any(|sector| protected(sector, registers)))
                || (cr::MER1.is_set(cr_value)
                    && (12..=23).any(|sector| protected(sector, registers)));
            if refused {
                self.raise_error(sr::WRPERR, registers, context);
                return;
            }
            bank_erase_nanos(cr_value)
        } else if cr::SER.is_set(cr_value) {
            let raw = u32::try_from(cr::SNB.get(cr_value)).expect("five bits");
            let Some(sector) = sector_of(raw) else {
                self.raise_error(sr::WRPERR, registers, context);
                return;
            };
            if protected(sector, registers) {
                self.raise_error(sr::WRPERR, registers, context);
                return;
            }
            sector_erase_nanos(sector, cr_value)
        } else {
            let cleared = STRT.set(registers.get(Register::Cr), 0);
            registers.set(Register::Cr, cleared);
            return;
        };
        self.check_parallelism(cr_value, context);
        let hclk_low = self.hclk_hz < 1_000_000;
        if hclk_low && !self.told_hclk_low {
            context.tell(
                Register::Cr.spec().name,
                "RM0090 rev 22 section 3.6: HCLK is below the 1 MHz an erase or program operation requires",
            );
        }
        self.told_hclk_low = hclk_low;
        self.begin(Operation::Erase, nanos, registers, context);
    }

    /// RM0090 rev 22 section 3.6.2 and table 13: `PSIZE` must be set correctly before any program or
    /// erase operation, to what the supply allows. Told once per transition, the way `check_latency`
    /// tells rather than refuses.
    fn check_parallelism(&mut self, cr_value: u64, context: &mut DeviceContext<'_>) {
        let Some(supply_mv) = self.supply_mv else {
            self.told_parallelism = false;
            return;
        };
        let short = cr::PSIZE.get(cr_value) > max_psize(supply_mv);
        if short && !self.told_parallelism {
            context.tell(
                Register::Cr.spec().name,
                "RM0090 rev 22 section 3.6.2 table 13: PSIZE exceeds what the supply allows",
            );
        }
        self.told_parallelism = short;
    }

    /// `OPTCR`, `OPTCR1`: frozen while locked, section 3.7.2. A write made while `BSY` stands is held
    /// rather than dropped, sections 3.9.10, 3.9.11 and 3.6.1's stall note. A rising `OPTSTRT` commits
    /// them, unless section 3.7.3's Level 2 freezes the option bytes outright or section 3.7.5's cancel
    /// applies, each of which restores the last completed commit instead of starting one.
    fn write_optcr(
        &mut self,
        register: Register,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        if OPTLOCK.is_set(self.shadow.optcr) {
            registers.restore(Register::Optcr, !OPTLOCK.mask(), self.shadow.optcr);
            registers.restore(Register::Optcr1, u64::MAX, self.shadow.optcr1);
            return;
        }
        if sr::BSY.is_set(registers.get(Register::Sr)) {
            self.held = Some(Held {
                register,
                value: registers.get(register),
            });
            registers.restore(Register::Optcr, !OPTLOCK.mask(), self.shadow.optcr);
            registers.restore(Register::Optcr1, u64::MAX, self.shadow.optcr1);
            return;
        }
        let value = registers.get(Register::Optcr);
        if !OPTSTRT.is_set(value) || OPTSTRT.is_set(self.shadow.optcr) {
            return;
        }
        if rdp_level(self.committed.optcr) == 2 {
            registers.restore(Register::Optcr, u64::MAX, self.committed.optcr);
            registers.restore(Register::Optcr1, u64::MAX, self.committed.optcr1);
            return;
        }
        let optcr1 = registers.get(Register::Optcr1);
        let unlocks_to_zero = rdp_level(self.committed.optcr) == 1 && rdp_level(value) == 0;
        // 3.7.5: the restricted "unprotection" is PCROP's, not plain write-protection's. A bit
        // clearing under SPRMOD 0 is a sector gaining write protection, the ordinary case every
        // other test here exercises. It is only a sector losing PCROP's read-and-write protection,
        // under SPRMOD 1, that this cancel rule reaches.
        let was_pcrop = optcr::SPRMOD.is_set(self.committed.optcr);
        let was_wrp = all_nwrp(self.committed.optcr, self.committed.optcr1);
        let now_wrp = all_nwrp(value, optcr1);
        let unprotects = was_pcrop && ((!optcr::SPRMOD.is_set(value)) || (was_wrp & !now_wrp != 0));
        if unprotects && !unlocks_to_zero {
            registers.restore(Register::Optcr, u64::MAX, self.committed.optcr);
            registers.restore(Register::Optcr1, u64::MAX, self.committed.optcr1);
            self.raise_error(sr::WRPERR, registers, context);
            return;
        }
        self.begin(Operation::Option, OPTION_PROGRAM_NANOS, registers, context);
        if unlocks_to_zero {
            Self::clear_backup_sram(context);
        }
    }

    /// RM0090 rev 22 section 3.7.3 and table 18's footnote: the main flash memory and the backup SRAM
    /// are mass-erased only on the commit that moves `RDP` from Level 1 to Level 0. The array's own
    /// half is left out. This clears the backup SRAM at `0x4002_4000`, which the runtime does map.
    /// The manual does not state the erased value for the SRAM half the way 3.6.4's note implies
    /// all ones for flash, so this model clears it to zero.
    fn clear_backup_sram(context: &mut DeviceContext<'_>) {
        const BACKUP_SRAM: u64 = 0x4002_4000;
        const BACKUP_SRAM_WORDS: u64 = (4 << 10) / 4;
        for word in 0..BACKUP_SRAM_WORDS {
            context.write_to(BACKUP_SRAM + word * 4, Width::W32, 0, word);
        }
    }

    /// Raises `BSY` and schedules the operation's own end.
    fn begin(
        &mut self,
        operation: Operation,
        nanos: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let value = sr::BSY.set(registers.get(Register::Sr), 1);
        registers.set(Register::Sr, value);
        let at = VirtualNanos(context.now().0 + nanos);
        self.pending = Some((context.schedule(at), operation));
    }

    /// Sets `flag`, `OPERR` behind `ERRIE`, and `EOP` behind `EOPIE`, RM0090 rev 22 sections 3.7.4 and
    /// 3.9.6, then clears the `STRT` that asked for the operation this refuses: nothing was started, so
    /// nothing is left `BSY`. `EOP` follows 3.6.6's "operation completed, correctly or not" rather than
    /// 3.9.6's "successfully" alone, since a firmware using 3.6.5's "or use the EOP interrupt" with
    /// `ERRIE` clear would otherwise wait forever on an erase this model refuses.
    fn raise_error(
        &mut self,
        flag: &FieldSpec,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let mut value = flag.set(registers.get(Register::Sr), 1);
        if cr::ERRIE.is_set(registers.get(Register::Cr)) {
            value = sr::OPERR.set(value, 1);
        }
        if cr::EOPIE.is_set(registers.get(Register::Cr)) {
            value = sr::EOP.set(value, 1);
        }
        registers.set(Register::Sr, value);
        let cr_value = STRT.set(registers.get(Register::Cr), 0);
        registers.set(Register::Cr, cr_value);
        self.refresh_interrupt(registers, context);
    }

    /// RM0090 rev 22 table 14: the line stands while `EOP` or `OPERR` does and lowers the instant
    /// neither does, which is what the register file's own write-one-to-clear leaves to this model to
    /// notice.
    fn refresh_interrupt(&mut self, registers: &RegisterFile, context: &mut DeviceContext<'_>) {
        let sr_value = registers.get(Register::Sr);
        let cr_value = registers.get(Register::Cr);
        let active = (sr::EOP.is_set(sr_value) && cr::EOPIE.is_set(cr_value))
            || (sr::OPERR.is_set(sr_value) && cr::ERRIE.is_set(cr_value));
        let wanted = Level::from(active);
        if wanted != self.interrupt {
            self.interrupt = wanted;
            context.interrupt(IRQ, wanted);
        }
    }
}