rpi-hal 0.7.0

Hardware abstraction layer for bare-metal Raspberry Pi: BCM2836/BCM2837 (Pi 2, Pi 3), with preliminary BCM2711 (Pi 4) and BCM2835 (Pi 1, Pi Zero) support
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
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
//! Blocking driver for the VideoCore mailbox property interface — the
//! RPC channel to the GPU firmware for anything the ARM core can't
//! learn or do on its own: real clock rates, board identification, the
//! ARM/VideoCore memory split, and (see
//! [`allocate_framebuffer`](crate::mailbox::Mailbox::allocate_framebuffer))
//! a display framebuffer. Display
//! output itself (HDMI or the MIPI DSI touchscreen) is entirely
//! mediated by the VideoCore the same way: there's no ARM-side HDMI or
//! DSI PHY to program directly, only this mailbox request for a buffer
//! to write pixels into. Which physical output that buffer appears on
//! is chosen with
//! [`set_display_num`](crate::mailbox::Mailbox::set_display_num), out
//! of the displays the firmware enumerated. How many there are to
//! choose between is still firmware/`config.txt` configuration — a
//! board whose `max_framebuffers` leaves room for one display offers a
//! choice of one, whatever is plugged into it — but which of them the
//! framebuffer lands on is this driver's to decide.
//!
//! ## Wire format
//!
//! A call writes one 32-bit "message" to mailbox 1: the request
//! buffer's address (which must therefore be 16-byte aligned — its
//! low 4 bits double as the channel number) with the channel (8, for
//! property tags) or-ed into those low bits. The firmware processes
//! the buffer in place and writes the same message (echoing the
//! address) back to mailbox 0 once done.
//!
//! The buffer itself is `[size, request/response code, tag id, tag
//! value size, tag request/response code, ...tag value words...,
//! 0]` — this driver only ever builds single-tag requests, which is
//! all any of the queries below need.
//!
//! ## Cache coherency
//!
//! This crate's MMU setup (`mmu.rs`) maps RAM Cacheable, but the
//! VideoCore reads/writes the request buffer over the bus, entirely
//! outside the ARM core's cache — so without explicit maintenance,
//! the firmware could read a stale, not-yet-written-back request, or
//! this driver could read back the stale pre-call contents of the
//! response instead of what the firmware wrote. `clean_range` runs
//! before handing the buffer to the firmware, `invalidate_range`
//! after, so this is handled explicitly rather than assumed away.
//!
//! Separately, the buffer's address is passed to the firmware
//! translated to the `0xC000_0000` "direct, uncached" VideoCore bus
//! alias (see `to_vc_bus_address`) rather than its plain physical
//! address — that's what keeps the *VideoCore's own* L2 cache out of
//! the picture, a second cache this driver has no maintenance
//! operations for at all. Both pieces are needed: one for the ARM
//! core's cache, one for the VideoCore's.
//!
//! ## Sources
//!
//! The raw register protocol in `Mailbox::call_raw` (channel folded
//! into the address's low 4 bits, FULL/EMPTY status polling, and the
//! property-tags channel's exception to the VC-bus-address rule) is
//! documented at
//! <https://github.com/raspberrypi/firmware/wiki/Accessing-mailboxes>.
//!
//! Most of the property tags this module uses — firmware/board
//! revision, serial, MAC address, the ARM/VC memory split, clock
//! rate get/set, temperature, the GPIO-expander state, display size,
//! overscan, and EDID — are listed at
//! <https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface>.
//! A handful of newer or less common tags this driver also uses —
//! [`throttled`](crate::mailbox::Mailbox::throttled) (`0x0003_0046`),
//! [`set_power_domain`](crate::mailbox::Mailbox::set_power_domain)
//! (`0x0003_8030`), the paged-framebuffer flip/vsync tags
//! (`0x0004_8009`/`0x0004_800e`),
//! [`vchiq_init`](crate::mailbox::Mailbox::vchiq_init) (`0x0004_8010`),
//! and
//! [`set_touch_buffer_address`](crate::mailbox::Mailbox::set_touch_buffer_address)
//! (`0x0004_801f`), and the display enumeration tags
//! [`num_displays`](crate::mailbox::Mailbox::num_displays)
//! (`0x0004_0013`),
//! [`set_display_num`](crate::mailbox::Mailbox::set_display_num)
//! (`0x0004_8013`) and
//! [`display_id`](crate::mailbox::Mailbox::display_id)
//! (`0x0004_0016`) — aren't on that wiki page at all; those are only
//! confirmed, name and value both, against the Linux kernel's own tag
//! enum at
//! <https://github.com/torvalds/linux/blob/master/include/soc/bcm2835/raspberrypi-firmware.h>.

use crate::cache::{clean_range, invalidate_range};
use crate::pac::VCMAILBOX;

/// Mailbox channel used for every call in this module — the
/// VideoCore "property tags" RPC channel. Other channels exist
/// (framebuffer alloc/release on older firmware, power management),
/// but every query this driver makes goes through this one.
const CHANNEL_PROPERTY_TAGS: u32 = 8;

/// Largest tag value area (in words) any query in this module needs —
/// currently 4, for "Get Overscan"'s (top, bottom, left, right)
/// response; most tags need 2 (a 64-bit board serial, a memory base+size
/// pair, a clock id and its rate). Bump this if a wider tag is ever added.
///
/// The value area is sized at this maximum for *every* call, not at the
/// individual tag's length. That costs a few words of stack and is what
/// the firmware expects — the tag's response code reports how much it
/// actually wrote — but it means an over-long call would silently overrun
/// this buffer, so [`Mailbox::property_call`] checks each call against it.
const MAX_VALUE_WORDS: usize = 4;

/// Total property buffer size in words: the 2-word message header
/// (buffer size, request/response code), one 3-word tag header (tag
/// id, value size, tag request/response code), the value area, and
/// the 1-word end tag.
const BUFFER_WORDS: usize = 2 + 3 + MAX_VALUE_WORDS + 1;

/// Set on the message header's/tag's response code once the firmware
/// has actually answered — the low 31 bits then carry (message
/// header) nothing meaningful, or (tag) the real response length in
/// bytes.
const RESPONSE_BIT: u32 = 1 << 31;

/// Errors from a [`Mailbox`] property-interface call.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
    /// The response's echoed buffer address didn't match the request
    /// the buffer was actually built for — would mean memory
    /// corruption, since this driver only ever has one call in flight
    /// at a time.
    AddressMismatch,
    /// The message header's response code didn't have its high bit
    /// set — the firmware rejected the whole request.
    RequestFailed,
    /// The tag's own response code didn't have its high bit set — the
    /// firmware didn't recognize or didn't answer this tag.
    TagNotAnswered,
    /// A tag asked for more request or response words than the shared
    /// value area holds ([`MAX_VALUE_WORDS`](self) — a private constant,
    /// since it is this module's own buffer, not the caller's). A bug in
    /// this driver rather than anything the hardware did: the fix is to
    /// widen that buffer for the tag that needs it.
    ValueTooLarge,
    /// The firmware answered with a zero-width or zero-height display
    /// mode, which means it has no mode configured — nothing plugged in,
    /// or an output it could not bring up.
    ///
    /// Unlike the variants above this is the firmware answering, not
    /// failing to: only [`Mailbox::display_mode`] turns it into an error,
    /// because a size to allocate is what that function exists to
    /// produce. [`Mailbox::display_size`] passes the zeros through
    /// unjudged.
    NoDisplayMode,
}

/// Identifies which SoC clock a [`Mailbox::clock_rate_hz`] query is
/// about.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum ClockId {
    /// EMMC/SD card controller clock.
    Emmc = 1,
    /// UART reference clock — already a fixed, firmware-guaranteed
    /// value this crate doesn't need to query (see `uart.rs`'s
    /// `init`); listed here for completeness against the firmware's
    /// own clock id list, not because any driver needs it.
    Uart = 2,
    /// ARM core (CPU) clock.
    Arm = 3,
    /// SoC "core" clock — what feeds the BSC and SPI0 controllers, and
    /// so what their `clock_divider` is measured against. Query it here
    /// and hand it to [`i2c::divider_for`](crate::i2c::divider_for) or
    /// [`spi::divider_for`](crate::spi::divider_for) to turn a target
    /// bus frequency into the divider those drivers take.
    Core = 4,
    /// 3D block (VideoCore GPU) clock.
    V3d = 5,
    /// H264 hardware video decoder clock.
    H264 = 6,
    /// Image Sensor Pipeline (camera) clock.
    Isp = 7,
    /// SDRAM clock.
    Sdram = 8,
    /// HDMI pixel clock.
    Pixel = 9,
    /// PWM clock.
    Pwm = 10,
    /// BCM2711 (Pi 4) EMMC2 controller clock — distinct from
    /// [`ClockId::Emmc`], which on that chip feeds a different controller (see
    /// `sd.rs`'s BCM2711 doc notes). Confirmed against the firmware's
    /// own mailbox property interface documentation, not assumed. Not
    /// meaningful on BCM2836/2837, which has no EMMC2 block at all.
    #[cfg(feature = "bcm2711")]
    Emmc2 = 12,
}

// Bits of [`Mailbox::throttled`]'s status word. Bits 0-3 report the
// board's state right now; the same conditions repeat at bits 16-19 as
// sticky "this has happened since boot" flags, which is where a dip that
// has already passed shows up.
/// Supply voltage is below the firmware's threshold *now*.
pub const THROTTLED_UNDER_VOLTAGE: u32 = 1 << 0;
/// The ARM clock is currently capped below its maximum.
pub const THROTTLED_ARM_FREQ_CAPPED: u32 = 1 << 1;
/// The board is currently being throttled.
pub const THROTTLED_NOW: u32 = 1 << 2;
/// The soft temperature limit is currently active.
pub const THROTTLED_SOFT_TEMP_LIMIT: u32 = 1 << 3;
/// Under-voltage has occurred since boot.
pub const THROTTLED_UNDER_VOLTAGE_EVER: u32 = 1 << 16;
/// The ARM clock has been capped since boot.
pub const THROTTLED_ARM_FREQ_CAPPED_EVER: u32 = 1 << 17;
/// Throttling has occurred since boot.
pub const THROTTLED_EVER: u32 = 1 << 18;
/// The soft temperature limit has been hit since boot.
pub const THROTTLED_SOFT_TEMP_LIMIT_EVER: u32 = 1 << 19;

/// The VideoCore GPIO-expander line carrying the on-board wireless
/// chip's `WL_ON` enable, for [`Mailbox::set_expander_gpio`]: expander
/// line 1, plus the firmware's expander base of 128. Driving it high
/// powers the radio up on a Pi 3 (both `B` and `B+`), which the SoC has
/// no direct ARM GPIO to reach.
pub const EXPANDER_WL_ON: u32 = 129;

/// The VideoCore GPIO-expander line carrying the on-board Bluetooth
/// controller's `BT_ON` enable, for [`Mailbox::set_expander_gpio`]:
/// expander line 0, plus the firmware's expander base of 128. Driving it
/// high releases the BCM43438's Bluetooth core from reset on a Pi 3
/// (device tree `bluetooth { shutdown-gpios = <&expgpio 0 …> }`), the
/// sibling of [`EXPANDER_WL_ON`]'s Wi-Fi enable; as with `WL_ON`, the SoC
/// has no direct ARM GPIO to reach it.
pub const EXPANDER_BT_ON: u32 = 128;

/// The VideoCore GPIO-expander line enabling the CSI camera connector's
/// power/regulator (`CAM_GPIO0`), for [`Mailbox::set_expander_gpio`]:
/// expander line 5, plus the firmware's expander base of 128. On a Pi 3
/// the camera module's supply is gated by this firmware line (device tree
/// `cam1_reg { gpio = <&expgpio 5 …> }`); it must be driven high before an
/// OV5647/IMX219 will respond on its I2C control bus. The SoC has no direct
/// ARM GPIO to it — this firmware call is the only way to power it.
pub const EXPANDER_CAM_GPIO0: u32 = 133;

/// The VideoCore GPIO-expander line driving the CSI camera connector's LED
/// (`CAM_GPIO1`), for [`Mailbox::set_expander_gpio`]: expander line 6, plus
/// the firmware's expander base of 128. Cosmetic — the activity LED on the
/// camera board — and independent of [`EXPANDER_CAM_GPIO0`]'s power line.
pub const EXPANDER_CAM_GPIO1: u32 = 134;

/// The VideoCore firmware power-domain id for the Unicam1 CSI-2 receiver,
/// for [`Mailbox::set_power_domain`]. This is the analog/PHY power island
/// the receiver needs to recover high-speed data — separate from register
/// (MMIO) access, which stays alive regardless — so it must be powered on
/// before [`crate::unicam`] can receive frames.
pub const POWER_DOMAIN_UNICAM1: u32 = 13;

/// A hardware block the firmware can power on or off via
/// [`Mailbox::set_power_state`] (tag `0x0002_8001`). Values are the
/// firmware's own device ids.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum PowerDeviceId {
    /// SD card controller.
    SdCard = 0,
    /// UART0 (PL011).
    Uart0 = 1,
    /// UART1 (mini-UART).
    Uart1 = 2,
    /// USB HCD — the DWC2 host controller. On this board the firmware
    /// may hand USB off only partially powered (enough for register
    /// access, not full operation), so a bare-metal host driver should
    /// power this on explicitly before bringing the controller up.
    UsbHcd = 3,
    /// I2C0 (BSC0).
    I2c0 = 4,
    /// I2C1 (BSC1).
    I2c1 = 5,
    /// I2C2 (BSC2).
    I2c2 = 6,
    /// SPI0.
    Spi = 7,
    /// CCP2TX.
    Ccp2tx = 8,
}

/// A contiguous memory range as reported by [`Mailbox::arm_memory`] /
/// [`Mailbox::vc_memory`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MemoryRegion {
    /// Physical base address of the region.
    pub base_address: u32,
    /// Size of the region, in bytes.
    pub size_bytes: u32,
}

/// The display resolution the firmware is currently driving, as
/// reported by [`Mailbox::display_size`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DisplaySize {
    /// Width of the display, in pixels.
    pub width: u32,
    /// Height of the display, in pixels.
    pub height: u32,
}

/// The blank border the firmware keeps around the displayed image, in
/// pixels per edge — see [`Mailbox::overscan`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Overscan {
    /// Blank rows above the image.
    pub top: u32,
    /// Blank rows below the image.
    pub bottom: u32,
    /// Blank columns left of the image.
    pub left: u32,
    /// Blank columns right of the image.
    pub right: u32,
}

impl Overscan {
    /// Whether every edge is zero — the displayed image is free to
    /// cover the display's whole raster, with none of it held back as a
    /// blank border.
    pub fn is_zero(&self) -> bool {
        self.top == 0 && self.bottom == 0 && self.left == 0 && self.right == 0
    }
}

/// Length of one EDID block, in bytes — the unit
/// [`Mailbox::edid_block`] reads in, fixed by the EDID standard rather
/// than by anything the firmware chooses.
pub const EDID_BLOCK_LEN: usize = 128;

/// Pixel channel order for a framebuffer, passed to
/// [`Mailbox::allocate_framebuffer`] (tag `0x0004_8006`, "Set Pixel
/// Order").
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum PixelOrder {
    /// Blue, green, red byte order.
    Bgr = 0,
    /// Red, green, blue byte order.
    Rgb = 1,
}

/// Which physical output one of the firmware's displays drives, as
/// answered by [`Mailbox::display_id`] (tag `0x0004_0016`, "Get Display
/// ID").
///
/// These are the VideoCore's own display identifiers, and they are not
/// the same numbering as the *display number* that
/// [`Mailbox::set_display_num`] takes: the id says what a display is,
/// the number says where it sits in the firmware's enumeration of the
/// displays actually present. A board with only HDMI attached has one
/// display, whose number is 0 and whose id is [`DisplayId::Hdmi0`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DisplayId {
    /// The MIPI DSI panel — the official touchscreen and the displays
    /// wired like it.
    MainLcd,
    /// A second LCD, on boards that have one.
    AuxLcd,
    /// The first (or only) HDMI connector.
    Hdmi0,
    /// Composite video out.
    Sdtv,
    /// The second HDMI connector, on boards that have two.
    Hdmi1,
    /// Something this crate has no name for, carrying the raw value the
    /// firmware answered with. Forward compatibility rather than an
    /// error: a caller looking for a display it does recognize can pass
    /// over this one, and a caller logging what it found can still say
    /// what the firmware said.
    Other(u32),
}

impl DisplayId {
    /// Decodes the firmware's raw display id.
    fn from_raw(raw: u32) -> Self {
        match raw {
            0 => Self::MainLcd,
            1 => Self::AuxLcd,
            2 => Self::Hdmi0,
            3 => Self::Sdtv,
            7 => Self::Hdmi1,
            other => Self::Other(other),
        }
    }
}

/// How many displays [`Mailbox::select_display`] will enumerate.
///
/// More than any Pi has, so a preference list runs out of displays to
/// name before it runs out of room. A firmware reporting more than this
/// has its surplus ignored rather than being refused: the ones that
/// matter are attached, and the fallback is display 0 either way.
pub const MAX_DISPLAYS: usize = 5;

/// What [`Mailbox::select_display`] did, and what it had to choose from.
///
/// Returned rather than logged because this crate has no console to log
/// to. The caller does — and needs to: with two displays attached, a
/// picture on the wrong one is otherwise a mystery, and which of the
/// outcomes below happened is the whole explanation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Selection {
    /// The displays the firmware reported, indexed by its own numbering.
    /// An entry is `None` where the query failed.
    attached: [Option<DisplayId>; MAX_DISPLAYS],
    /// How many of [`Self::attached`] the firmware actually reported.
    count: usize,
    /// The display the framebuffer tags now act on, as far as this can
    /// tell. `None` only if the firmware would not say what display 0 is.
    chosen: Option<DisplayId>,
    /// How [`Self::chosen`] came to be the one.
    outcome: Outcome,
    /// What `num_displays` answered, or `None` where it would not answer
    /// at all.
    reported: Option<u32>,
}

impl Selection {
    /// The displays the firmware reported, paired with its own number for
    /// each — which is what [`Mailbox::set_display_num`] takes, and what
    /// a log line should name alongside the id.
    pub fn attached(&self) -> impl Iterator<Item = (u32, Option<DisplayId>)> + '_ {
        self.attached[..self.count]
            .iter()
            .enumerate()
            .map(|(number, id)| (number as u32, *id))
    }

    /// How many displays the firmware reported.
    pub fn count(&self) -> usize {
        self.count
    }

    /// The display the framebuffer tags act on after the call.
    pub fn chosen(&self) -> Option<DisplayId> {
        self.chosen
    }

    /// How [`Self::chosen`] came to be the one.
    pub fn outcome(&self) -> Outcome {
        self.outcome
    }

    /// How many displays the firmware said it had, or `None` where it did
    /// not answer the question at all.
    ///
    /// The distinction [`Outcome::Sole`] cannot make, and the one worth
    /// having when two displays are plugged in and only one is reported.
    /// `None` is a firmware too old to know the tag. `Some(1)` is a
    /// firmware that knows it and means it — on a Pi that usually means
    /// `max_framebuffers` has been left at its default of 1 in
    /// `config.txt`, which is what decides how many displays the firmware
    /// will expose regardless of how many are attached.
    ///
    /// `Some(0)` happens too, and is read here the same way as `Some(1)`:
    /// whatever the picture is going to land on is already selected.
    pub fn reported_count(&self) -> Option<u32> {
        self.reported
    }
}

/// How [`Mailbox::select_display`] arrived at the display it did.
///
/// Worth distinguishing at the call site rather than collapsing to a
/// bool: three of these four are fine and one is a preference that could
/// not be met, and only the caller knows whether that is worth saying
/// loudly.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
    /// One display, so there was nothing to choose and nothing was
    /// changed. Also what a firmware too old to know these tags gets: it
    /// names nothing and counts nothing, and has one display as far as it
    /// is concerned.
    ///
    /// [`Selection::reported_count`] tells those two apart, which matters
    /// when more than one display is plugged in and this is still the
    /// outcome.
    Sole,
    /// The preference list named a display that was attached, and the
    /// firmware moved the framebuffer tags to it.
    Preferred,
    /// The preference list was empty, so the firmware's own selection
    /// stands. With more than one display attached that selection is
    /// whichever it enumerated first this boot, which is not stable from
    /// boot to boot.
    Firmwares,
    /// Nothing in the preference list was attached, or the firmware
    /// refused to select it, so display 0 was taken instead.
    FellBack,
}

/// The size to allocate a framebuffer so the firmware does not scale it —
/// see [`Mailbox::display_mode`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DisplayMode {
    /// Width to allocate, in pixels.
    pub width: u32,
    /// Height to allocate, in pixels.
    pub height: u32,
    /// The overscan border as it was found, before any attempt to clear
    /// it. Zero where there was none.
    pub overscan: Overscan,
    /// Whether the border was cleared.
    ///
    /// `false` alongside a non-zero [`Self::overscan`] means the firmware
    /// would not give it up — `config.txt` pinning the values does that —
    /// so the image genuinely has to stay inside the border, and
    /// [`Self::width`]/[`Self::height`] are the area within it rather
    /// than the whole mode.
    pub overscan_cleared: bool,
}

/// A framebuffer allocated by [`Mailbox::allocate_framebuffer`]: where
/// it lives in memory and how its pixels are laid out.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Framebuffer {
    /// ARM-side physical address of the first pixel. Identity-mapped by
    /// this crate's MMU setup (`mmu.rs`), so this doubles as a virtual
    /// address a caller can cast straight to a pointer and write
    /// through.
    pub address: u32,
    /// Total size of the buffer, in bytes — the whole virtual buffer,
    /// so with [`Mailbox::allocate_framebuffer_paged`] this covers
    /// every page, not just the one on screen.
    pub size_bytes: u32,
    /// Bytes from the start of one row to the start of the next. Not
    /// necessarily `width * bytes_per_pixel` — the firmware may pad
    /// each row wider than the requested width — so callers must
    /// stride writes by this, not a value they compute themselves.
    pub pitch_bytes: u32,
    /// Width, in pixels, as actually allocated (matches the request).
    pub width: u32,
    /// Height of the *displayed* area, in pixels, as actually
    /// allocated (matches the request).
    pub height: u32,
    /// Height of the whole allocated buffer, in pixels. Equal to
    /// [`height`](Self::height) for a plain
    /// [`Mailbox::allocate_framebuffer`]; `pages * height` for
    /// [`Mailbox::allocate_framebuffer_paged`], where the rows past the
    /// first `height` are off-screen until
    /// [`Mailbox::set_virtual_offset`] scrolls to them.
    pub virtual_height: u32,
    /// Bits per pixel, as actually allocated (matches the request).
    pub depth_bits: u32,
}

impl Framebuffer {
    /// How many [`height`](Self::height)-tall pages this buffer holds —
    /// what [`Mailbox::allocate_framebuffer_paged`] was asked for, and
    /// 1 for a plain [`Mailbox::allocate_framebuffer`].
    pub fn pages(&self) -> u32 {
        self.virtual_height / self.height
    }

    /// Byte offset from [`address`](Self::address) of page `page`'s
    /// first pixel — where to start writing the frame that
    /// [`Mailbox::set_virtual_offset`] will later bring on screen.
    pub fn page_offset_bytes(&self, page: u32) -> u32 {
        page * self.height * self.pitch_bytes
    }

    /// Writes back every cache line covering this framebuffer, making
    /// pixels the ARM core just wrote actually visible to the display
    /// hardware. The VideoCore's scanout reads this memory over the
    /// bus, entirely outside the ARM core's cache — the same reasoning
    /// as this module's mailbox call buffer (see the module doc
    /// comment) — so call this after writing a frame and before
    /// expecting it on screen.
    ///
    /// This covers the whole allocation. A paged buffer draws one page
    /// at a time, and cleaning the pages nothing was written to is
    /// wasted memory traffic — [`flush_page`](Self::flush_page) is the
    /// one to call there.
    pub fn flush(&self) {
        clean_range(self.address, self.size_bytes as usize);
    }

    /// Writes back only the cache lines covering page `page` — the
    /// per-frame flush for a buffer from
    /// [`Mailbox::allocate_framebuffer_paged`], and equivalent to
    /// [`flush`](Self::flush) for a single-page one.
    ///
    /// A `page` past the end of the allocation flushes nothing rather
    /// than cleaning memory that isn't the framebuffer's.
    pub fn flush_page(&self, page: u32) {
        if page >= self.pages() {
            return;
        }
        clean_range(
            self.address + self.page_offset_bytes(page),
            (self.height * self.pitch_bytes) as usize,
        );
    }
}

/// Blocking driver for the VideoCore mailbox property interface.
pub struct Mailbox {
    vcmailbox: VCMAILBOX,
}

impl Mailbox {
    /// Wraps the mailbox peripheral. Needs no setup — unlike every
    /// other peripheral this crate drives, there's no GPIO routing or
    /// clock divider to configure; the mailbox is always live.
    pub fn new(vcmailbox: VCMAILBOX) -> Self {
        Self { vcmailbox }
    }

    /// The running firmware's revision number, straight from tag
    /// `0x0000_0001` with no further interpretation.
    pub fn firmware_revision(&mut self) -> Result<u32, Error> {
        let response = self.property_call(0x0000_0001, &[], 1)?;
        Ok(response[0])
    }

    /// This board's revision code (tag `0x0001_0002`) — the same
    /// packed value `/proc/cpuinfo`'s `Revision` field reports under
    /// Linux, still undecoded here (board type/RAM size/manufacturer
    /// live in different bit ranges of it, and nothing in this crate
    /// needs them broken out yet).
    pub fn board_revision(&mut self) -> Result<u32, Error> {
        let response = self.property_call(0x0001_0002, &[], 1)?;
        Ok(response[0])
    }

    /// This board's 64-bit serial number (tag `0x0001_0004`), low word
    /// first as the firmware returns it.
    pub fn board_serial(&mut self) -> Result<u64, Error> {
        let response = self.property_call(0x0001_0004, &[], 2)?;
        Ok((response[0] as u64) | ((response[1] as u64) << 32))
    }

    /// This board's Ethernet MAC address (tag `0x0001_0003`), in
    /// transmission byte order. On this board the MAC lives only in the
    /// VideoCore firmware (the on-board LAN9514 USB-Ethernet controller
    /// has no EEPROM of its own), so this tag is the authoritative source
    /// — a driver bringing the LAN9514 up must read it here and program
    /// it into the chip's address registers, since the chip powers up
    /// without one.
    pub fn mac_address(&mut self) -> Result<[u8; 6], Error> {
        // Six bytes across two response words, low word first: word 0 is
        // MAC bytes 0..4, word 1's low half is bytes 4..6.
        let response = self.property_call(0x0001_0003, &[], 2)?;
        let low = response[0].to_le_bytes();
        let high = response[1].to_le_bytes();
        Ok([low[0], low[1], low[2], low[3], high[0], high[1]])
    }

    /// The RAM range reserved for the ARM core (tag `0x0001_0005`) —
    /// the complement of [`Self::vc_memory`]; together they're the
    /// "memory split" set at boot (`config.txt`'s `gpu_mem`).
    pub fn arm_memory(&mut self) -> Result<MemoryRegion, Error> {
        self.memory_region(0x0001_0005)
    }

    /// The RAM range reserved for the VideoCore GPU (tag
    /// `0x0001_0006`) — see [`Self::arm_memory`].
    pub fn vc_memory(&mut self) -> Result<MemoryRegion, Error> {
        self.memory_region(0x0001_0006)
    }

    /// The real, current rate of `clock`, in Hz (tag `0x0003_0002`,
    /// "Get Clock Rate") — what a caller needing an exact frequency
    /// computes a clock divider against (see
    /// [`ClockId::Core`] for the two that do), instead of assuming a
    /// nominal value that may not match this specific board's
    /// configuration.
    pub fn clock_rate_hz(&mut self, clock: ClockId) -> Result<u32, Error> {
        let response = self.property_call(0x0003_0002, &[clock as u32], 2)?;
        Ok(response[1])
    }

    /// The highest rate the firmware will run `clock` at, in Hz (tag
    /// `0x0003_0004`, "Get Max Clock Rate") — the board's configured
    /// maximum (`arm_freq` in `config.txt`), not necessarily what it is
    /// running now. Compare with [`Self::clock_rate_hz`].
    pub fn max_clock_rate_hz(&mut self, clock: ClockId) -> Result<u32, Error> {
        let response = self.property_call(0x0003_0004, &[clock as u32], 2)?;
        Ok(response[1])
    }

    /// Asks the firmware to run `clock` at `rate_hz` (tag `0x0003_8002`,
    /// "Set Clock Rate"), returning the rate it actually set — which is
    /// clamped to the firmware's own minimum and maximum, so passing
    /// [`Self::max_clock_rate_hz`] is the way to ask for "as fast as this
    /// board goes".
    ///
    /// **A bare-metal program that wants full speed has to do this.** The
    /// firmware brings the ARM core up at its *minimum* rate — 600 MHz on a
    /// Pi 3 whose maximum is 1200 — and something has to ask for more.
    /// Under Linux that something is the cpufreq governor, reacting to
    /// load through this same tag; with no OS there is no governor, so the
    /// clock simply stays at the floor and every measurement comes out half
    /// what the hardware can do, with nothing reported as wrong (see
    /// [`Self::throttled`], which stays clear — the board isn't being held
    /// back, it was never asked to go faster).
    pub fn set_clock_rate_hz(&mut self, clock: ClockId, rate_hz: u32) -> Result<u32, Error> {
        // Third word is `skip_setting_turbo`: 0 lets the firmware move the
        // other turbo-mode clocks (core, SDRAM) along with the ARM clock,
        // which is what makes the higher ARM rate worth having.
        let response = self.property_call(0x0003_8002, &[clock as u32, rate_hz, 0], 3)?;
        Ok(response[1])
    }

    /// Turns `clock` on (`on = true`) or off (tag `0x0003_8001`, "Set
    /// Clock State"), returning whether the firmware reports the clock
    /// exists afterwards.
    ///
    /// Distinct from [`Self::set_clock_rate_hz`]: rate selects *how
    /// fast* an already-running clock goes, this selects whether it
    /// runs at all. Every clock this crate has driven so far comes up
    /// already enabled by the boot firmware, since something upstream
    /// (the firmware itself, or Linux on another boot) already needed
    /// it — `ClockId::Emmc2` (BCM2711/Pi 4) is the first one this
    /// crate reaches that firmware may never have turned on for its own
    /// purposes.
    pub fn set_clock_state(&mut self, clock: ClockId, on: bool) -> Result<bool, Error> {
        let response = self.property_call(0x0003_8001, &[clock as u32, on as u32], 2)?;
        // Response bit 1: 0 = exists, 1 = does not exist.
        Ok(response[1] & 0b10 == 0)
    }

    /// The SoC's temperature, in thousandths of a degree Celsius (tag
    /// `0x0003_0006`, "Get Temperature") — `58_000` is 58°C.
    ///
    /// Read alongside [`Self::throttled`] when a workload that was keeping
    /// up stops doing so: the firmware caps the ARM clock as the die heats
    /// up, and a program that only watches its own frame times sees that as
    /// its code inexplicably getting slower.
    pub fn temperature_millicelsius(&mut self) -> Result<u32, Error> {
        // Sensor id 0 -- the only one this SoC has.
        let response = self.property_call(0x0003_0006, &[0], 2)?;
        Ok(response[1])
    }

    /// The firmware's throttling status word (tag `0x0003_0046`, "Get
    /// Throttled"), reporting both what is happening *now* and what has
    /// happened since boot. Test it with the `THROTTLED_*` constants.
    ///
    /// The two halves matter for different questions: the low bits say
    /// whether the board is being held back at this moment, and the sticky
    /// bits (16 and up) say whether it ever was — which is the only way to
    /// tell an under-voltage event that has since passed from one that
    /// never happened.
    pub fn throttled(&mut self) -> Result<u32, Error> {
        let response = self.property_call(0x0003_0046, &[0], 2)?;
        Ok(response[1])
    }

    /// Powers a firmware *power domain* on (`on = true`) or off (tag
    /// `0x0003_8030`, "Set Domain State"), returning the resulting state.
    ///
    /// Power domains are coarse analog/logic power islands the firmware
    /// gates — distinct from [`Self::set_power_state`]'s device list, and
    /// from register (MMIO) access, which stays alive independently. The
    /// camera receiver's analog D-PHY lives in one such domain
    /// ([`POWER_DOMAIN_UNICAM1`]) that must be powered before it can
    /// recover high-speed data. A firmware too old to implement the tag
    /// surfaces [`Error::TagNotAnswered`] from the underlying call.
    pub fn set_power_domain(&mut self, domain: u32, on: bool) -> Result<bool, Error> {
        let response = self.property_call(0x0003_8030, &[domain, on as u32], 2)?;
        Ok(response[1] != 0)
    }

    /// Enables (`enable = true`) or disables the V3D 3D pipeline (tag
    /// `0x0003_0012`, "Set Enable QPU") — the block containing the QPU
    /// shader cores (`crate::v3d`, behind this crate's `v3d` feature)
    /// is power/clock-gated off by default, and this is the only way
    /// to bring it up before its registers do anything. Not linked:
    /// this method is compiled unconditionally, but `v3d` isn't — a
    /// build without that feature would have a broken intra-doc link
    /// otherwise. The tag's response carries no documented
    /// meaning beyond acknowledging the request, so — like
    /// [`Self::set_expander_gpio`] — this only checks that the firmware
    /// answered at all rather than interpreting the response value.
    pub fn set_enable_qpu(&mut self, enable: bool) -> Result<(), Error> {
        self.property_call(0x0003_0012, &[enable as u32], 1)?;
        Ok(())
    }

    /// Powers `device` on (`on = true`) or off, waiting for its power
    /// state to stabilize before returning (tag `0x0002_8001`, "Set
    /// Power State", with the "wait for stable" bit set). Returns
    /// `true` if the device is powered and present afterwards, `false`
    /// if the firmware reports it does not exist. Needed for USB on
    /// this board — see [`PowerDeviceId::UsbHcd`].
    pub fn set_power_state(&mut self, device: PowerDeviceId, on: bool) -> Result<bool, Error> {
        // state bit 0 = power on/off, bit 1 = wait for the change to
        // stabilize before the firmware answers.
        let state = if on { 0b11 } else { 0b10 };
        let response = self.property_call(0x0002_8001, &[device as u32, state], 2)?;
        // response bit 0 = now powered, bit 1 = device does not exist.
        Ok(response[1] & 0b10 == 0 && response[1] & 0b1 == if on { 1 } else { 0 })
    }

    /// Drives one of the VideoCore firmware's GPIO-expander lines high
    /// (`on = true`) or low (tag `0x0003_8041`, "Set GPIO State").
    ///
    /// These are *not* the SoC's own ARM GPIO pins (those go through
    /// [`crate::gpio`]) — they're extra lines behind a small expander
    /// the VideoCore owns, addressed at a base of 128. On a Pi 3 the
    /// on-board wireless chip's enable line, `WL_ON`, is expander line
    /// 1, i.e. [`EXPANDER_WL_ON`] — the SoC has no direct GPIO to it, so
    /// this firmware call is the only way to power the radio up. The
    /// firmware echoes the request back; a mismatch surfaces as
    /// [`Error::TagNotAnswered`] from the underlying property call
    /// rather than being checked here.
    pub fn set_expander_gpio(&mut self, pin: u32, on: bool) -> Result<(), Error> {
        self.property_call(0x0003_8041, &[pin, on as u32], 2)?;
        Ok(())
    }

    /// Reads the current state of GPIO-expander line `pin` (tag
    /// `0x0003_0041`, "Get GPIO State") — the read counterpart to
    /// [`Self::set_expander_gpio`], returning `true` if the line reads
    /// high. Also useful purely to probe whether this firmware
    /// implements the expander mailbox interface at all: a firmware too
    /// old to know the tag answers with [`Error::TagNotAnswered`].
    pub fn get_expander_gpio(&mut self, pin: u32) -> Result<bool, Error> {
        let response = self.property_call(0x0003_0041, &[pin, 0], 2)?;
        Ok(response[1] != 0)
    }

    /// The resolution the firmware is currently driving the display at
    /// (tag `0x0004_0003`, "Get Physical Width/Height").
    ///
    /// Worth asking *before* [`Self::allocate_framebuffer`], because
    /// that call takes the size it is given: request 800×480 on a
    /// display running at 1920×1080 and the firmware allocates the
    /// small buffer and scales it up to the mode it negotiated, so
    /// every pixel written lands on screen as a blurry block. Sizing
    /// the request from this instead gives a framebuffer that maps 1:1
    /// to the display.
    ///
    /// What "currently" means depends on the output. HDMI has a
    /// negotiated mode — whatever the firmware picked from the sink's
    /// EDID at boot, or was forced to by `config.txt`'s
    /// `hdmi_group`/`hdmi_mode` — and this reports that choice, not
    /// what the display is capable of. A MIPI DSI panel has one fixed
    /// physical resolution and reports it.
    ///
    /// "Physical" here means the *displayed image*, not the display's
    /// raster: what comes back is the mode with [`Self::overscan`]'s
    /// blank border already subtracted. A 1920×1080 HDMI display with
    /// the firmware's stock 48-pixel border on every edge reports
    /// 1824×984, and a framebuffer that size is what maps 1:1 to
    /// pixels inside the border — the missing 96 rows and columns are
    /// raster the firmware is deliberately leaving blank, not an error
    /// and not something scaling would recover.
    ///
    /// Recover the full mode by adding [`Self::overscan`]'s border back
    /// on (`width + left + right`, `height + top + bottom`), *not* by
    /// clearing the border and asking again. This tag reads back the
    /// physical size the firmware's own boot-time framebuffer was
    /// created with, and [`Self::set_overscan`] doesn't resize an
    /// existing framebuffer: on a 1080p display, clearing the border to
    /// zero and re-querying still answers 1824×984. The way to get a
    /// full-raster framebuffer is to clear the border *and* pass the
    /// recovered size to [`Self::allocate_framebuffer`], which sets the
    /// physical size (tag `0x0004_8003`) as part of its request rather
    /// than being limited by what this reports.
    ///
    /// Check the answer for a zero width or height before sizing an
    /// allocation from it: the tag reports the mode firmware has
    /// configured, and firmware with nothing plugged in has no mode to
    /// report. A firmware too old to know the tag surfaces
    /// [`Error::TagNotAnswered`]; either way the caller needs a
    /// fallback resolution of its own, since this driver has no way to
    /// learn one.
    pub fn display_size(&mut self) -> Result<DisplaySize, Error> {
        let response = self.property_call(0x0004_0003, &[], 2)?;
        Ok(DisplaySize {
            width: response[0],
            height: response[1],
        })
    }

    /// The blank border the firmware is keeping around the displayed
    /// image (tag `0x0004_000A`, "Get Overscan").
    ///
    /// Overscan exists for televisions that crop the edges of their
    /// input, and the firmware applies a border by default rather than
    /// risk a picture whose edges are cut off on such a set. It is not
    /// scaling: the border is raster the firmware leaves blank, and
    /// [`Self::display_size`] reports the smaller image inside it, so a
    /// framebuffer sized from that call is already pixel-exact — just
    /// short of the display's full mode by the border on each edge.
    ///
    /// Query this to tell the two apart when a reported size doesn't
    /// match the display's nameplate resolution: a border of 48 on
    /// every edge accounts for exactly the 96 rows and columns missing
    /// from a 1080p panel reporting 1824×984.
    pub fn overscan(&mut self) -> Result<Overscan, Error> {
        let response = self.property_call(0x0004_000A, &[], 4)?;
        Ok(Overscan {
            top: response[0],
            bottom: response[1],
            left: response[2],
            right: response[3],
        })
    }

    /// Sets the blank border around the displayed image (tag
    /// `0x0004_800A`, "Set Overscan"), returning the border the
    /// firmware actually applied.
    ///
    /// Passing an all-zero [`Overscan`] gives up the border and hands
    /// the whole raster to the image, which is what a program wanting
    /// every pixel of an HDMI display wants — the equivalent of
    /// `disable_overscan=1` in `config.txt`, but decided by the program
    /// rather than by whoever wrote the SD card.
    ///
    /// This changes the border only, not the size of any framebuffer
    /// already allocated — see [`Self::display_size`], which keeps
    /// answering with the pre-clear size. Clearing the border is
    /// therefore half the job of going full-screen; the other half is
    /// allocating at the size that border was hiding.
    ///
    /// The firmware clamps what it is given, so the returned value is
    /// what to believe rather than the request. It also has the last
    /// word on whether the border can go at all: a `config.txt` that
    /// pins the overscan values can leave a non-zero border in place
    /// here, which is why this returns the result instead of `()`. Even
    /// the returned value is only the firmware's answer to this one
    /// call — read it back with [`Self::overscan`] for independent
    /// confirmation that the change took.
    pub fn set_overscan(&mut self, overscan: Overscan) -> Result<Overscan, Error> {
        let response = self.property_call(
            0x0004_800A,
            &[overscan.top, overscan.bottom, overscan.left, overscan.right],
            4,
        )?;
        Ok(Overscan {
            top: response[0],
            bottom: response[1],
            left: response[2],
            right: response[3],
        })
    }

    /// Reads one 128-byte block of the attached display's EDID (tag
    /// `0x0003_0020`, "Get EDID Block"), or `Ok(None)` if the firmware
    /// answered the tag but has no such block.
    ///
    /// EDID is the display's own description of itself, read by the
    /// firmware over the HDMI connector's I2C side-channel: which modes
    /// it supports, which one it prefers, who made it. That is strictly
    /// more than [`Self::display_size`] reports — that call gives the
    /// one mode the firmware settled on, this gives the menu it chose
    /// from.
    ///
    /// Block 0 is the base block every EDID has; its last-but-one byte
    /// is a count of *extension* blocks, which are then blocks 1 up to
    /// that count. `Ok(None)` is the normal answer for a display with
    /// no EDID at all — a DSI panel has a fixed resolution and nothing
    /// to describe, and nothing plugged in has nobody to answer — and
    /// equally for a block number past the end, so iterating until it
    /// appears is a valid way to read the lot.
    ///
    /// The bytes come back unvalidated and unparsed. EDID's structure
    /// (the header magic, the per-block checksum, timing descriptors,
    /// the CEA-861 extension blocks' mode lists) is display-standard
    /// wire format with no Pi in it, so it is left to the caller the
    /// same way TCP/IP is left to smoltcp and FAT to embedded-sdmmc —
    /// see `examples/display_edid.rs`, which parses what it needs.
    pub fn edid_block(&mut self, block: u32) -> Result<Option<[u8; EDID_BLOCK_LEN]>, Error> {
        /// Value area, in words: the block number echoed back, the
        /// status word, and [`EDID_BLOCK_LEN`] bytes of EDID.
        const VALUE_WORDS: usize = 2 + EDID_BLOCK_LEN / 4;

        /// Word count of the whole request/response buffer: the 2-word
        /// message header, the 3-word tag header, the value area, and
        /// the 1-word end tag.
        ///
        /// This tag gets its own buffer rather than the shared one
        /// [`Mailbox::property_call`] builds: at 34 value words it is an
        /// order of magnitude wider than any other tag here, and
        /// widening `MAX_VALUE_WORDS` to fit would grow the stack
        /// buffer of every unrelated two-word query alongside it.
        const WORDS: usize = 2 + 3 + VALUE_WORDS + 1;

        // 16-byte aligned for the same reason as `property_call`'s
        // buffer: the low 4 bits of its address double as the mailbox
        // channel number.
        #[repr(C, align(16))]
        struct Buffer {
            words: [u32; WORDS],
        }

        let mut buffer = Buffer { words: [0; WORDS] };
        buffer.words[0] = (WORDS * 4) as u32;
        buffer.words[1] = 0;
        buffer.words[2] = 0x0003_0020;
        buffer.words[3] = (VALUE_WORDS * 4) as u32;
        buffer.words[4] = 0;
        buffer.words[5] = block;
        // buffer.words[6..] (the status word, the EDID bytes, and the
        // end tag) are already zero from the initializer above.

        let address = &buffer as *const Buffer as u32;
        clean_range(address, WORDS * 4);

        let echoed = self.call_raw(CHANNEL_PROPERTY_TAGS, address);
        if echoed != address {
            return Err(Error::AddressMismatch);
        }

        invalidate_range(address, WORDS * 4);

        if buffer.words[1] & RESPONSE_BIT == 0 {
            return Err(Error::RequestFailed);
        }
        if buffer.words[4] & RESPONSE_BIT == 0 {
            return Err(Error::TagNotAnswered);
        }
        // Status word: zero means this block came back, anything else
        // that it didn't. Distinct from the tag going unanswered above —
        // the firmware understood the request and is reporting that
        // there is no such block to hand over.
        if buffer.words[6] != 0 {
            return Ok(None);
        }

        // The firmware wrote the block as 32 words; unpacked
        // little-endian to match this core's byte order, the same way
        // `mac_address` unpacks its two.
        let mut bytes = [0u8; EDID_BLOCK_LEN];
        for (word, chunk) in buffer.words[7..7 + EDID_BLOCK_LEN / 4]
            .iter()
            .zip(bytes.as_chunks_mut::<4>().0)
        {
            *chunk = word.to_le_bytes();
        }
        Ok(Some(bytes))
    }

    /// How many displays the firmware has enumerated (tag
    /// `0x0004_0013`, "Get Number of Displays") — the count the display
    /// numbers [`set_display_num`](Self::set_display_num) takes run
    /// below.
    ///
    /// Firmware old enough not to know this tag answers with an error
    /// or with zero; both mean the one display that firmware can
    /// describe, so a caller that only wants to know whether there is a
    /// choice to make should treat anything it can't get as `1`.
    pub fn num_displays(&mut self) -> Result<u32, Error> {
        let response = self.property_call(0x0004_0013, &[], 1)?;
        Ok(response[0])
    }

    /// Selects which display the framebuffer tags act on (tag
    /// `0x0004_8013`, "Set Display Num"), where `number` is below
    /// [`num_displays`](Self::num_displays).
    ///
    /// The selection is firmware state, not state of this handle: it
    /// persists across calls and across mailbox users until something
    /// sets it again, and it starts at 0. Everything framebuffer-related
    /// follows it — [`allocate_framebuffer`](Self::allocate_framebuffer),
    /// [`allocate_framebuffer_paged`](Self::allocate_framebuffer_paged),
    /// [`set_virtual_offset`](Self::set_virtual_offset) — so a program
    /// that flips pages on one display must not leave another selected
    /// in between.
    ///
    /// Selecting a display is only meaningful with more than one
    /// attached, and which one is number 0 is the firmware's decision,
    /// taken at boot from what it found: a board with both a DSI panel
    /// and HDMI connected can enumerate them either way round. A caller
    /// that wants a *particular* output has to look the numbers up with
    /// [`display_id`](Self::display_id) rather than assume one.
    pub fn set_display_num(&mut self, number: u32) -> Result<(), Error> {
        self.property_call(0x0004_8013, &[number], 1)?;
        Ok(())
    }

    /// What display `number` actually is (tag `0x0004_0016`, "Get
    /// Display ID") — where `number` is below
    /// [`num_displays`](Self::num_displays).
    ///
    /// Walking those numbers and calling this for each is how a caller
    /// finds the display it wants, since the numbering itself carries no
    /// meaning.
    ///
    /// The number goes in the request word and the id comes back in its
    /// place. This tag therefore answers about the display it is *asked*
    /// about rather than the one
    /// [`set_display_num`](Self::set_display_num) selected, which is
    /// what lets a caller enumerate every display without disturbing
    /// where the framebuffer tags point. Passing a fixed word here
    /// instead of the number — reading it as a no-argument "what is
    /// selected?" tag — gets display 0's id every time, and an
    /// enumeration in which every display looks like the first one.
    pub fn display_id(&mut self, number: u32) -> Result<DisplayId, Error> {
        let response = self.property_call(0x0004_0016, &[number], 1)?;
        Ok(DisplayId::from_raw(response[0]))
    }

    /// Allocates a `width`×`height` framebuffer at `depth_bits` bits per
    /// pixel (`8`/`16`/`24`/`32` are the depths the firmware actually
    /// supports; `32` is the common choice) in `pixel_order`, and
    /// returns where to write pixels.
    ///
    /// The buffer is exactly the size of the display, so pixels are
    /// Points the framebuffer tags at the first display in `order` that
    /// is actually attached.
    ///
    /// With one display there is nothing to choose and nothing is
    /// changed: the tags already act on it. With two — a DSI panel and
    /// HDMI both plugged in — which one they act on is whichever the
    /// firmware enumerated as number 0 that boot, and that is *not stable
    /// from boot to boot*. A board that wants a particular screen has to
    /// say so, which is what `order` is: most preferred first.
    ///
    /// An empty `order` enumerates and reports without selecting
    /// anything, leaving the firmware's own choice in force.
    ///
    /// Call this **before** [`display_mode`](Self::display_mode) and
    /// before allocating: both act on the selected display, so asking
    /// first sizes the buffer from whichever screen the firmware happened
    /// to start on.
    ///
    /// # What it reports, and why it does not log
    ///
    /// Everything worth saying comes back in the [`Selection`] rather
    /// than going to a console this crate does not have. A caller that
    /// wants the usual bring-up lines has all of it: which displays were
    /// found and under what numbers, which one was taken, and by
    /// [`Outcome`] whether that was the preference, the firmware's own
    /// choice, or a fallback.
    ///
    /// # Errors
    ///
    /// None. A firmware that answers neither `num_displays` nor
    /// `display_id` has one display as far as it is concerned, already
    /// selected — so a failed query is not an error here, it is
    /// [`Outcome::Sole`], and moving the selection could only make things
    /// worse.
    pub fn select_display(&mut self, order: &[DisplayId]) -> Selection {
        let reported = self.num_displays().ok();
        let count = (reported.unwrap_or(0) as usize).min(MAX_DISPLAYS);
        let mut attached = [None; MAX_DISPLAYS];

        if count < 2 {
            attached[0] = self.display_id(0).ok();
            return Selection {
                attached,
                // A firmware answering zero still has the one display the
                // picture is going to land on, and a caller listing what
                // it found should see it.
                count: count.max(1),
                chosen: attached[0],
                outcome: Outcome::Sole,
                reported,
            };
        }

        // Asking what each display is does not select it — the number is
        // an argument of the query, not the firmware's selection — so the
        // enumeration runs first and touches nothing, and the single
        // `set_display_num` below is the only thing that decides where
        // the framebuffer lands.
        for (number, id) in attached.iter_mut().enumerate().take(count) {
            *id = self.display_id(number as u32).ok();
        }

        if order.is_empty() {
            return Selection {
                attached,
                count,
                chosen: attached[0],
                outcome: Outcome::Firmwares,
                reported,
            };
        }

        for wanted in order {
            let Some(number) = attached[..count].iter().position(|id| id == &Some(*wanted)) else {
                continue;
            };
            if self.set_display_num(number as u32).is_ok() {
                return Selection {
                    attached,
                    count,
                    chosen: Some(*wanted),
                    outcome: Outcome::Preferred,
                    reported,
                };
            }
        }

        // Nothing named is attached, or the firmware refused. Display 0 is
        // where the picture would have landed with no preference expressed
        // at all, so that is what an unsatisfiable one falls back to —
        // stated rather than assumed, since a `set_display_num` that
        // failed in the loop above is the one case where the selection is
        // not known to still be where it started.
        let _ = self.set_display_num(0);
        Selection {
            attached,
            count,
            chosen: attached[0],
            outcome: Outcome::FellBack,
            reported,
        }
    }

    /// The size to allocate so the firmware does not scale the picture.
    ///
    /// **The firmware scales a framebuffer to the display mode.** A buffer
    /// smaller than the mode is not letterboxed into a corner, it is
    /// stretched to fill the screen — and by a different factor in each
    /// axis if the aspect ratios differ, which is what a logo drawn round
    /// and shown oval means. So this is not "a reasonable size to draw
    /// in", it is the mode exactly, and getting it wrong is visible
    /// rather than merely wasteful.
    ///
    /// # Why this is more than one query
    ///
    /// The firmware keeps a blank overscan border, for televisions that
    /// crop their input, and "Get Physical Width/Height" reports the image
    /// *inside* it — so a 720×576 mode with a 32-pixel border answers
    /// 656×512. Clearing the border does not resize a framebuffer the
    /// firmware has already made, and re-querying after the clear still
    /// answers the inner size, so the border has to be read **first** and
    /// added back arithmetically to recover the mode.
    ///
    /// # This clears the border
    ///
    /// Unavoidably, and that is why this takes `&mut self`: the full mode
    /// is not otherwise obtainable. A firmware that will not give it up —
    /// `config.txt` pinning the values — means the image genuinely has to
    /// stay inside it, and [`DisplayMode::overscan_cleared`] is `false`
    /// with the inner size reported, which is then the right size to
    /// allocate. A caller that wants its border kept should not call
    /// this; [`overscan`](Self::overscan) and
    /// [`display_size`](Self::display_size) are the parts it composes.
    ///
    /// # Errors
    ///
    /// [`Error::NoDisplayMode`] where the firmware reports a zero
    /// width or height, which means it has no mode configured and is no
    /// more usable than no answer at all. What to do about that is the
    /// caller's — a fallback size is a policy, not a fact about the
    /// hardware.
    ///
    /// What this cannot fix is a firmware driving the *wrong mode*: if a
    /// 1024×600 panel is being sent 720×576, this answers 720×576,
    /// correctly, and the panel does the stretching. That is a
    /// `config.txt` matter (`hdmi_cvt`/`hdmi_group`/`hdmi_mode`).
    pub fn display_mode(&mut self) -> Result<DisplayMode, Error> {
        // Read the border *before* clearing it: once it is zero, the size
        // it was hiding cannot be recovered from the firmware any more.
        let overscan = self.overscan().unwrap_or(Overscan {
            top: 0,
            bottom: 0,
            left: 0,
            right: 0,
        });

        let inner = self.display_size()?;
        if inner.width == 0 || inner.height == 0 {
            return Err(Error::NoDisplayMode);
        }

        if overscan.is_zero() {
            return Ok(DisplayMode {
                width: inner.width,
                height: inner.height,
                overscan,
                overscan_cleared: true,
            });
        }

        // Read the border back rather than trusting what `set_overscan`
        // echoed: the echo is that one call's answer, while a fresh query
        // is independent evidence that the change took.
        let cleared = self
            .set_overscan(Overscan {
                top: 0,
                bottom: 0,
                left: 0,
                right: 0,
            })
            .is_ok()
            && self.overscan().is_ok_and(|border| border.is_zero());

        Ok(if cleared {
            DisplayMode {
                width: inner.width + overscan.left + overscan.right,
                height: inner.height + overscan.top + overscan.bottom,
                overscan,
                overscan_cleared: true,
            }
        } else {
            DisplayMode {
                width: inner.width,
                height: inner.height,
                overscan,
                overscan_cleared: false,
            }
        })
    }

    /// written to the memory the VideoCore is scanning out at that very
    /// moment. Anything that redraws a whole frame will show a partly
    /// updated picture for as long as the redraw takes — see
    /// [`allocate_framebuffer_paged`](Self::allocate_framebuffer_paged)
    /// for the tear-free alternative.
    pub fn allocate_framebuffer(
        &mut self,
        width: u32,
        height: u32,
        depth_bits: u32,
        pixel_order: PixelOrder,
    ) -> Result<Framebuffer, Error> {
        self.allocate_framebuffer_paged(width, height, 1, depth_bits, pixel_order)
    }

    /// Allocates a framebuffer `pages` screens tall — a `width`×`height`
    /// display area backed by `pages` full-size pages, only one of which
    /// is on screen at a time.
    ///
    /// This is what makes tear-free output possible: draw the next frame
    /// into a page the display isn't showing, then bring it on screen in
    /// one step with [`set_virtual_offset`](Self::set_virtual_offset).
    /// Nothing is ever written to the page being scanned out, so a
    /// half-finished frame is never visible.
    ///
    /// Two pages is the minimum for that, but the page just retired
    /// stays on screen until the display hardware picks the new one up
    /// at its next vertical blank, so with two pages the next frame's
    /// drawing can start before that happens. Three pages puts two
    /// flips between a page leaving the screen and being drawn into
    /// again, which is enough margin to never wait.
    ///
    /// Requests physical size, virtual size, depth, pixel order, the
    /// buffer itself, and its pitch as one combined multi-tag call (tags
    /// `0x0004_8003`, `0x0004_8004`, `0x0004_8005`, `0x0004_8006`,
    /// `0x0004_0001`, `0x0004_0008`) rather than as separate calls —
    /// older firmware only processes a framebuffer request correctly
    /// when every tag arrives together in one buffer.
    ///
    /// Appears on whichever display
    /// [`set_display_num`](Self::set_display_num) last selected, which
    /// is display 0 until something selects another. That only matters
    /// with more than one display attached, and there it matters a great
    /// deal: with a DSI panel and HDMI both connected, display 0 is
    /// whichever of them the firmware happened to enumerate first, so a
    /// caller that has an output in mind must select it rather than let
    /// this land wherever.
    pub fn allocate_framebuffer_paged(
        &mut self,
        width: u32,
        height: u32,
        pages: u32,
        depth_bits: u32,
        pixel_order: PixelOrder,
    ) -> Result<Framebuffer, Error> {
        /// Alignment (in bytes) requested for the allocated buffer —
        /// the firmware's own recommendation for a framebuffer.
        const ALLOCATE_ALIGNMENT: u32 = 4096;

        /// Word count of the whole request/response buffer: the 2-word
        /// message header, then each tag's (3-word header + value
        /// words) — (5 + 5 + 4 + 4 + 5 + 4) — plus the 1-word end tag.
        const WORDS: usize = 2 + (5 + 5 + 4 + 4 + 5 + 4) + 1;

        // 16-byte aligned for the same reason as `property_call`'s
        // buffer: the low 4 bits of its address double as the mailbox
        // channel number.
        #[repr(C, align(16))]
        struct Buffer {
            words: [u32; WORDS],
        }

        // Zero pages would ask the firmware for a zero-height buffer;
        // one page is the un-paged case, which is what the caller of
        // `allocate_framebuffer` gets.
        let virtual_height = height * pages.max(1);

        let mut buffer = Buffer { words: [0; WORDS] };
        buffer.words[0] = (WORDS * 4) as u32;
        buffer.words[1] = 0;

        // Set physical (display) width/height.
        buffer.words[2] = 0x0004_8003;
        buffer.words[3] = 8;
        buffer.words[4] = 0;
        buffer.words[5] = width;
        buffer.words[6] = height;

        // Set virtual (buffer) width/height. Taller than physical is
        // what gives the pages: the firmware allocates the lot, and the
        // display shows a `height`-tall window into it that
        // `set_virtual_offset` moves.
        buffer.words[7] = 0x0004_8004;
        buffer.words[8] = 8;
        buffer.words[9] = 0;
        buffer.words[10] = width;
        buffer.words[11] = virtual_height;

        // Set depth.
        buffer.words[12] = 0x0004_8005;
        buffer.words[13] = 4;
        buffer.words[14] = 0;
        buffer.words[15] = depth_bits;

        // Set pixel order.
        buffer.words[16] = 0x0004_8006;
        buffer.words[17] = 4;
        buffer.words[18] = 0;
        buffer.words[19] = pixel_order as u32;

        // Allocate buffer: the request value is the desired alignment;
        // the firmware overwrites it with the buffer's VideoCore bus
        // address, and the word after with the buffer's size.
        buffer.words[20] = 0x0004_0001;
        buffer.words[21] = 8;
        buffer.words[22] = 0;
        buffer.words[23] = ALLOCATE_ALIGNMENT;
        buffer.words[24] = 0;

        // Get pitch.
        buffer.words[25] = 0x0004_0008;
        buffer.words[26] = 4;
        buffer.words[27] = 0;
        buffer.words[28] = 0;

        // buffer.words[29] (the end tag) is already zero from the
        // initializer above.

        let address = &buffer as *const Buffer as u32;
        clean_range(address, WORDS * 4);

        let echoed = self.call_raw(CHANNEL_PROPERTY_TAGS, address);
        if echoed != address {
            return Err(Error::AddressMismatch);
        }

        invalidate_range(address, WORDS * 4);

        if buffer.words[1] & RESPONSE_BIT == 0 {
            return Err(Error::RequestFailed);
        }
        // Every tag's response code word sits 2 words after its id.
        for &code_index in &[4, 9, 14, 18, 22, 27] {
            if buffer.words[code_index] & RESPONSE_BIT == 0 {
                return Err(Error::TagNotAnswered);
            }
        }

        Ok(Framebuffer {
            address: from_vc_bus_address(buffer.words[23]),
            size_bytes: buffer.words[24],
            pitch_bytes: buffer.words[28],
            width: buffer.words[5],
            height: buffer.words[6],
            // The firmware's own answer, not `virtual_height` as asked
            // for: a request it clamped would otherwise have callers
            // computing page offsets past the end of the allocation.
            virtual_height: buffer.words[11],
            depth_bits: buffer.words[15],
        })
    }

    /// Scrolls the display window to `(x, y)` within the virtual buffer
    /// (tag `0x0004_8009`, "Set Virtual Offset") — the page flip for a
    /// framebuffer from
    /// [`allocate_framebuffer_paged`](Self::allocate_framebuffer_paged),
    /// where `y` is `page * height`.
    ///
    /// The call returns as soon as the firmware has taken the new
    /// offset, which is not the same moment the picture changes: the
    /// display hardware picks the new window up at its next vertical
    /// blank, so the previous page stays on screen for up to one refresh
    /// after this returns. That delay is why a page must not be drawn
    /// into again immediately after being flipped away from — see that
    /// method's note on page count.
    pub fn set_virtual_offset(&mut self, x: u32, y: u32) -> Result<(), Error> {
        self.property_call(0x0004_8009, &[x, y], 2)?;
        Ok(())
    }

    /// Blocks until the display's next vertical blank (tag
    /// `0x0004_800e`, "Set VSync").
    ///
    /// The firmware answers this tag when the display next finishes a
    /// frame, so a caller that draws straight into the scanned-out
    /// buffer of [`allocate_framebuffer`](Self::allocate_framebuffer)
    /// can at least start each redraw at the top of a refresh. That
    /// only narrows the window for tearing rather than closing it — a
    /// redraw slower than a refresh still gets caught up with — so
    /// prefer pages and [`set_virtual_offset`](Self::set_virtual_offset)
    /// where the memory is available.
    ///
    /// Note that this occupies the mailbox for up to a full refresh
    /// period, which on a shared mailbox blocks every other caller for
    /// that long.
    pub fn wait_for_vsync(&mut self) -> Result<(), Error> {
        self.property_call(0x0004_800e, &[0], 1)?;
        Ok(())
    }

    /// Hands the firmware the base address of the VCHIQ shared slot memory
    /// (tag `0x0004_8010`, "VCHIQ init"), returning the word it answers
    /// with — zero on success, and whatever it made of the request
    /// otherwise. Not interpreted here: the caller (`crate::vchiq`) is
    /// the only thing that knows what a nonzero answer means for its own
    /// bring-up. Not linked, for the same reason as
    /// [`Self::set_enable_qpu`]'s note: this method is compiled
    /// unconditionally but that module isn't, so a build without the
    /// `vchiq` feature would have a broken intra-doc link.
    ///
    /// `slot_bus_address` must be a VideoCore *bus* address (see this
    /// module's doc comment), and the memory it names must already hold a
    /// fully initialized slot-zero structure: this call is the point at
    /// which the firmware starts reading it.
    ///
    /// This is the one property tag that hands the firmware a permanent
    /// reference to ARM memory rather than answering a question, so —
    /// like [`Self::set_touch_buffer_address`] — that memory must stay
    /// valid forever after.
    pub fn vchiq_init(&mut self, slot_bus_address: u32) -> Result<u32, Error> {
        let response = self.property_call(0x0004_8010, &[slot_bus_address], 1)?;
        Ok(response[0])
    }

    /// Tells the firmware to keep the DSI touchscreen's current touch
    /// points written into `address` from now on (tag `0x0004_801f`,
    /// "Set Touchbuffer") — `address` must stay valid and
    /// firmware-writable forever after this call, since the firmware
    /// won't be told again.
    ///
    /// The complementary tag, `0x0004_000f` ("Get Touchbuffer" — ask the
    /// firmware for a buffer *it* allocated, rather than handing it
    /// one), isn't exposed here: confirmed on real hardware to return
    /// an address inside the peripheral MMIO aperture rather than RAM,
    /// which then hangs the core on the first read rather than merely
    /// reading back zero. The Linux `rpi-ft5406` driver's own history
    /// shows the same shift — it now tries this push model first,
    /// falling back to `GET_TOUCHBUF` only for firmware old enough to
    /// not support this tag at all.
    pub fn set_touch_buffer_address(&mut self, address: u32) -> Result<(), Error> {
        self.property_call(0x0004_801f, &[to_vc_bus_address(address)], 1)?;
        Ok(())
    }

    /// Shared implementation of [`Self::arm_memory`]/
    /// [`Self::vc_memory`] — both are a no-argument tag whose response
    /// is a `(base_address, size_bytes)` pair, differing only in tag
    /// id.
    fn memory_region(&mut self, tag: u32) -> Result<MemoryRegion, Error> {
        let response = self.property_call(tag, &[], 2)?;
        Ok(MemoryRegion {
            base_address: response[0],
            size_bytes: response[1],
        })
    }

    /// Runs one single-tag property-interface request/response
    /// round-trip: builds the buffer, hands it to the firmware over
    /// the mailbox, and returns however many response words the
    /// caller declared it expects (`response_words`; the rest of the
    /// returned array is zero and meaningless).
    ///
    /// Neither `request` nor `response_words` may exceed
    /// [`MAX_VALUE_WORDS`], the shared value area's size; a call that does
    /// gets [`Error::ValueTooLarge`] rather than overrunning the buffer.
    /// Checked at runtime, not with a `debug_assert`: every call site is a
    /// fixed literal in this module, so getting it wrong is a build-time
    /// mistake — but release builds are the ones that run on hardware, and
    /// one that skipped the check turned a too-long tag into a panic on a
    /// board instead of an error at the call.
    fn property_call(
        &mut self,
        tag: u32,
        request: &[u32],
        response_words: usize,
    ) -> Result<[u32; MAX_VALUE_WORDS], Error> {
        if request.len() > MAX_VALUE_WORDS || response_words > MAX_VALUE_WORDS {
            return Err(Error::ValueTooLarge);
        }

        // 16-byte aligned: the low 4 bits of the buffer's address
        // double as the mailbox channel number in the message word,
        // so a misaligned buffer would silently corrupt its own
        // address.
        #[repr(C, align(16))]
        struct Buffer {
            words: [u32; BUFFER_WORDS],
        }

        let mut buffer = Buffer {
            words: [0; BUFFER_WORDS],
        };
        buffer.words[0] = (BUFFER_WORDS * 4) as u32;
        buffer.words[1] = 0;
        buffer.words[2] = tag;
        buffer.words[3] = (MAX_VALUE_WORDS * 4) as u32;
        buffer.words[4] = 0;
        buffer.words[5..5 + request.len()].copy_from_slice(request);
        // buffer.words[5 + MAX_VALUE_WORDS] (the end tag) is already
        // zero from the initializer above.

        let address = &buffer as *const Buffer as u32;
        clean_range(address, BUFFER_WORDS * 4);

        let echoed = self.call_raw(CHANNEL_PROPERTY_TAGS, address);
        if echoed != address {
            return Err(Error::AddressMismatch);
        }

        invalidate_range(address, BUFFER_WORDS * 4);

        if buffer.words[1] & RESPONSE_BIT == 0 {
            return Err(Error::RequestFailed);
        }
        if buffer.words[4] & RESPONSE_BIT == 0 {
            return Err(Error::TagNotAnswered);
        }

        let mut response = [0u32; MAX_VALUE_WORDS];
        response[..response_words].copy_from_slice(&buffer.words[5..5 + response_words]);
        Ok(response)
    }

    /// One raw mailbox round-trip: waits for room in mailbox 1 and
    /// writes `message` (`buffer_address`'s [`to_vc_bus_address`]
    /// translation, with `channel` or-ed into its low 4 bits), then
    /// waits for and returns mailbox 0's response message with that
    /// same channel — masked back to a plain address, still in VC bus
    /// address space (the caller already has the ARM-side address to
    /// compare against, from before the call).
    ///
    /// Other channels' responses (there are none in flight here, since
    /// this driver only ever issues one call at a time and waits for
    /// its reply before returning) are skipped rather than treated as
    /// this call's answer — matching how every mailbox client is
    /// documented to multiplex the same physical FIFO.
    fn call_raw(&mut self, channel: u32, buffer_address: u32) -> u32 {
        let bus_address = to_vc_bus_address(buffer_address);
        let message = (bus_address & !0xF) | (channel & 0xF);

        while self.vcmailbox.status1().read().bits() & (1 << 31) != 0 {}
        unsafe {
            self.vcmailbox.write().write_with_zero(|w| w.bits(message));
        }

        loop {
            while self.vcmailbox.status0().read().empty().bit_is_set() {}
            let response = self.vcmailbox.read().read().bits();
            if response & 0xF == channel {
                return (response & !0xF) - 0xC000_0000;
            }
        }
    }
}

/// Translates a plain ARM physical address to the VideoCore's
/// `0xC000_0000`-based "direct, uncached" bus alias — the window that
/// bypasses the VideoCore's own L2 cache entirely, so this driver only
/// has to reason about the ARM core's cache (see this module's doc
/// comment), not a second one it has no maintenance operations for.
fn to_vc_bus_address(physical_address: u32) -> u32 {
    (physical_address & 0x3FFF_FFFF) | 0xC000_0000
}

/// Translates a VideoCore bus address (as returned by, e.g.,
/// [`Mailbox::allocate_framebuffer`]'s buffer allocation) back to a
/// plain ARM physical address — the inverse of [`to_vc_bus_address`].
fn from_vc_bus_address(bus_address: u32) -> u32 {
    bus_address & 0x3FFF_FFFF
}