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
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
|
//! ESP-Hosted's `g_h.funcs` port table, in Zig.
//!
//! This is the seam. Above it sit ~13,000 lines of ESP-Hosted C - the SDIO transport state machine,
//! the RPC protocol, the protobuf codec - which are already correct and which this project has no
//! intention of rewriting. Below it sit `std.Io`, `std.mem.Allocator` and `src/hal`. Everything
//! ESP-Hosted asks of an operating system passes through the 71 function pointers defined here, so
//! this file is the entire dependency of that C on FreeRTOS and ESP-IDF, and replacing it replaces
//! both.
//!
//! # The struct, and why its layout is the dangerous part
//!
//! `hosted_osi_funcs_t` is declared at `host/esp_hosted_os_abstraction.h:13-117`. Every member is a
//! function pointer, so on rv32 the struct is 71 words and **there is nothing in the type system,
//! on either side, that notices a field in the wrong place**. A mis-ordered pointer is a call to
//! the wrong function with the wrong arguments, which on this board is a hang with no console
//! output.
//!
//! Worse, the C struct is not one layout. Four mempool members are guarded by
//! `#ifdef H_USE_MEMPOOL` (`:64-69`), and `H_USE_MEMPOOL` is *always defined* - to 1 or to 0 - by
//! `host/port/esp/freertos/include/port_esp_hosted_host_config.h:127-131`, which `#ifdef` does not
//! care about. A translation unit that reaches the struct without having seen that header first
//! gets a struct 16 bytes shorter, with everything from `_h_config_gpio` onward displaced by four
//! pointers. That is reachable in the real tree: `host/esp_hosted.h:14` and
//! `host/drivers/transport/transport_util.h:10` both include the abstraction header as their first
//! include. Measured with our own flags:
//!
//! without -include port_esp_hosted_host_config.h: sizeof=268 _h_config_gpio=132 _h_event_post=264
//! with -include port_esp_hosted_host_config.h: sizeof=284 _h_config_gpio=148 _h_event_post=280
//!
//! This file targets the long layout, and `layout_check` below asserts the three numbers on the
//! right. The build force-includes that header into every ESP-Hosted translation unit and compares
//! C's `offsetof` against these assertions, so an include-order change fails the build instead of
//! the board.
//!
//! # What is real, what is a loud stub
//!
//! Real: memory, sync, threads, timers, time, GPIO, SDIO, events, mempool locks. That is every
//! entry the SDIO transport and the RPC layer touch, established by grepping the tree for each
//! `_h_` name rather than by guessing.
//!
//! Loud stubs: the SPI, SPI-HD and UART transports (a different bus), power-save (needs
//! `esp_sleep`), `_h_do_bus_transfer` (SPI-only; ESP-IDF leaves it null under SDIO), and
//! `_h_printf`. Each prints its own name through `ets_printf` and returns a failure code, so an
//! unimplemented path announces itself on the console instead of jumping through a null pointer.
//! `stub_calls` counts them.
//!
//! # Where ESP-Hosted's assumptions do not fit a cooperative single-core runtime
//!
//! Four places, all documented at the point of impact:
//!
//! * `_h_post_semaphore_from_isr` - FreeRTOS manipulates the semaphore inside a critical section
//! and requests a context switch on return. See `hosted_os.Semaphore.postFromIsr`.
//! * `_h_thread_cancel` - `vTaskDelete` kills a task where it stands; `std.Io`'s cancel asks and
//! waits, and ESP-Hosted's task bodies never return. See `hosted_os.Thread.cancel`.
//! * `_h_blocking_delay` - a deliberate busy-wait, which on a cooperative scheduler starves
//! every other task for its duration. Unused in the tree; kept honest.
//! * bounded waits - `std.Io` has no timed acquire for a mutex, semaphore or queue, so those
//! poll. See `hosted_os.poll_interval_ms`. Unbounded waits, which is what every hot path uses,
//! block properly.
const std = @import("std");
const assert = std.debug.assert;
const Io = std.Io;
const Allocator = std.mem.Allocator;
const hal = @import("hal");
const hheap = @import("heap.zig");
const os = @import("hosted_os.zig");
const ret = os.ret;
/// `ets_printf` from the mask ROM. Declared here rather than imported from `soc` so this file's
/// only module dependency is `hal`; the symbol comes from
/// `components/esp_rom/esp32p4/ld/esp32p4.rom.ld`.
extern fn ets_printf(fmt: [*:0]const u8, ...) c_int;
fn note(comptime fmt: [*:0]const u8, args: anytype) void {
_ = @call(.auto, ets_printf, .{fmt} ++ args);
}
// ============================================================================ the struct
/// `void (*start_routine)(void const *)`, `esp_hosted_os_abstraction.h:25`.
pub const StartRoutine = *const fn (?*const anyopaque) callconv(.c) void;
/// `void (*timeout_handler)(void *)`, `:60`.
pub const TimerHandler = *const fn (?*anyopaque) callconv(.c) void;
/// `void (*gpio_isr_handler)(void* arg)`, `:73`.
pub const IsrHandler = *const fn (?*anyopaque) callconv(.c) void;
/// `esp_event_base_t`, which is `const char *`.
pub const EventBase = [*:0]const u8;
/// `hosted_osi_funcs_t`, `host/esp_hosted_os_abstraction.h:13-117`, in the layout that
/// `H_USE_MEMPOOL` being defined produces. Field order is the C declaration order exactly; the
/// line number beside each is its declaration in that header.
pub const HostedOsiFuncs = extern struct {
// ---- Memory, :15-22
/// :15 `void* (*)(void* dest, const void* src, uint32_t size)`
memcpy: *const fn (?*anyopaque, ?*const anyopaque, u32) callconv(.c) ?*anyopaque,
/// :16 `void* (*)(void* buf, int val, size_t len)`
memset: *const fn (?*anyopaque, c_int, usize) callconv(.c) ?*anyopaque,
/// :17 `void* (*)(size_t size)`
malloc: *const fn (usize) callconv(.c) ?*anyopaque,
/// :18 `void* (*)(size_t blk_no, size_t size)`
calloc: *const fn (usize, usize) callconv(.c) ?*anyopaque,
/// :19 `void (*)(void* ptr)`
free: *const fn (?*anyopaque) callconv(.c) void,
/// :20 `void* (*)(void *mem, size_t newsize)`
realloc: *const fn (?*anyopaque, usize) callconv(.c) ?*anyopaque,
/// :21 `void* (*)(size_t size, size_t align)`
malloc_align: *const fn (usize, usize) callconv(.c) ?*anyopaque,
/// :22 `void (*)(void* ptr)`
free_align: *const fn (?*anyopaque) callconv(.c) void,
// ---- Thread, :25-27
/// :25 `void* (*)(const char *tname, uint32_t tprio, uint32_t tstack_size, void (*start_routine)(void const *), void *sr_arg)`
thread_create: *const fn ([*:0]const u8, u32, u32, StartRoutine, ?*anyopaque) callconv(.c) ?*anyopaque,
/// :26 `int (*)(void *thread_handle)`
thread_cancel: *const fn (?*anyopaque) callconv(.c) c_int,
/// :27 `void (*)(void)`
thread_yield: *const fn () callconv(.c) void,
// ---- Sleeps, :30-32
/// :30 `unsigned int (*)(unsigned int mseconds)`
msleep: *const fn (c_uint) callconv(.c) c_uint,
/// :31 `unsigned int (*)(unsigned int useconds)`
usleep: *const fn (c_uint) callconv(.c) c_uint,
/// :32 `unsigned int (*)(unsigned int seconds)`
sleep: *const fn (c_uint) callconv(.c) c_uint,
// ---- Blocking non-sleepable delay, :35
/// :35 `unsigned int (*)(unsigned int number)`
blocking_delay: *const fn (c_uint) callconv(.c) c_uint,
// ---- Queue, :38-43
/// :38 `int (*)(void * queue_handle, void *item, int timeout)`
queue_item: *const fn (?*anyopaque, ?*const anyopaque, c_int) callconv(.c) c_int,
/// :39 `void* (*)(uint32_t qnum_elem, uint32_t qitem_size)`
create_queue: *const fn (u32, u32) callconv(.c) ?*anyopaque,
/// :40 `int (*)(void * queue_handle, void *item, int timeout)`
dequeue_item: *const fn (?*anyopaque, ?*anyopaque, c_int) callconv(.c) c_int,
/// :41 `int (*)(void * queue_handle)`
queue_msg_waiting: *const fn (?*anyopaque) callconv(.c) c_int,
/// :42 `int (*)(void * queue_handle)`
destroy_queue: *const fn (?*anyopaque) callconv(.c) c_int,
/// :43 `int (*)(void * queue_handle)`
reset_queue: *const fn (?*anyopaque) callconv(.c) c_int,
// ---- Mutex, :46-49. Note that unlock comes *first*.
/// :46 `int (*)(void * mutex_handle)`
unlock_mutex: *const fn (?*anyopaque) callconv(.c) c_int,
/// :47 `void* (*)(void)`
create_mutex: *const fn () callconv(.c) ?*anyopaque,
/// :48 `int (*)(void * mutex_handle, int timeout_ms)`
lock_mutex: *const fn (?*anyopaque, c_int) callconv(.c) c_int,
/// :49 `int (*)(void * mutex_handle)`
destroy_mutex: *const fn (?*anyopaque) callconv(.c) c_int,
// ---- Semaphore, :52-56. `post` precedes `create`, as with the mutex.
/// :52 `int (*)(void * semaphore_handle)`
post_semaphore: *const fn (?*anyopaque) callconv(.c) c_int,
/// :53 `int (*)(void * semaphore_handle)`
post_semaphore_from_isr: *const fn (?*anyopaque) callconv(.c) c_int,
/// :54 `void* (*)(int maxCount)`
create_semaphore: *const fn (c_int) callconv(.c) ?*anyopaque,
/// :55 `int (*)(void * semaphore_handle, int timeout_ms)`
get_semaphore: *const fn (?*anyopaque, c_int) callconv(.c) c_int,
/// :56 `int (*)(void * semaphore_handle)`
destroy_semaphore: *const fn (?*anyopaque) callconv(.c) c_int,
// ---- Timer, :59-61. `stop` precedes `start`.
/// :59 `int (*)(void *timer_handle)`
timer_stop: *const fn (?*anyopaque) callconv(.c) c_int,
/// :60 `void* (*)(const char *name, int duration_ms, int type, void (*timeout_handler)(void *), void *arg)`
timer_start: *const fn ([*:0]const u8, c_int, c_int, TimerHandler, ?*anyopaque) callconv(.c) ?*anyopaque,
/// :61 `uint64_t (*)(void)`
get_time_ms: *const fn () callconv(.c) u64,
// ---- Mempool, :65-68, present because H_USE_MEMPOOL is defined. See the file header.
/// :65 `void* (*)(void)`
create_lock_mempool: *const fn () callconv(.c) ?*anyopaque,
/// :66 `void (*)(void *lock_handle)`
lock_mempool: *const fn (?*anyopaque) callconv(.c) void,
/// :67 `void (*)(void *lock_handle)`
unlock_mempool: *const fn (?*anyopaque) callconv(.c) void,
/// :68 `void (*)(void *lock_handle)`
destroy_lock_mempool: *const fn (?*anyopaque) callconv(.c) void,
// ---- GPIO, :72-79
/// :72 `int (*)(void* gpio_port, uint32_t gpio_num, uint32_t mode)`
config_gpio: *const fn (?*anyopaque, u32, u32) callconv(.c) c_int,
/// :73 `int (*)(void* gpio_port, uint32_t gpio_num, uint32_t intr_type, void (*gpio_isr_handler)(void* arg), void *arg)`
config_gpio_as_interrupt: *const fn (?*anyopaque, u32, u32, IsrHandler, ?*anyopaque) callconv(.c) c_int,
/// :74 `int (*)(void* gpio_port, uint32_t gpio_num)`
teardown_gpio_interrupt: *const fn (?*anyopaque, u32) callconv(.c) c_int,
/// :75 `int (*)(void* gpio_port, uint32_t gpio_num)`
read_gpio: *const fn (?*anyopaque, u32) callconv(.c) c_int,
/// :76 `int (*)(void* gpio_port, uint32_t gpio_num, uint32_t value)`
write_gpio: *const fn (?*anyopaque, u32, u32) callconv(.c) c_int,
/// :77 `int (*)(void* gpio_port, uint32_t gpio_num, uint32_t pull_value, uint32_t enable)`
pull_gpio: *const fn (?*anyopaque, u32, u32, u32) callconv(.c) c_int,
/// :78 `int (*)(void* gpio_port, uint32_t gpio_num, uint32_t hold_value)`
hold_gpio: *const fn (?*anyopaque, u32, u32) callconv(.c) c_int,
/// :79 `int (*)(void)`
get_host_wakeup_or_reboot_reason: *const fn () callconv(.c) c_int,
// ---- All transports, :81-82
/// :81 `void * (*)(void)`
bus_init: *const fn () callconv(.c) ?*anyopaque,
/// :82 `int (*)(void*)`
bus_deinit: *const fn (?*anyopaque) callconv(.c) c_int,
// ---- :84-88
/// :84 `int (*)(void *transfer_context)` - SPI only; ESP-IDF leaves this null under SDIO.
do_bus_transfer: *const fn (?*anyopaque) callconv(.c) c_int,
/// :85 `int (*)(int32_t event_id, void* event_data, size_t event_data_size, uint32_t ticks_to_wait)`
event_wifi_post: *const fn (i32, ?*anyopaque, usize, u32) callconv(.c) c_int,
/// :87 `void (*)(int level, const char *tag, const char *format, ...)`
printf: *const fn (c_int, [*:0]const u8, [*:0]const u8, ...) callconv(.c) void,
/// :88 `void (*)(void)`
hosted_init_hook: *const fn () callconv(.c) void,
// ---- Transport - SDIO, :91-97
/// :91 `int (*)(void *ctx, bool show_config)`
sdio_card_init: *const fn (?*anyopaque, bool) callconv(.c) c_int,
/// :92 `int (*)(void*ctx)`
sdio_card_deinit: *const fn (?*anyopaque) callconv(.c) c_int,
/// :93 `int (*)(void *ctx, uint32_t reg, uint8_t *data, uint16_t size, bool lock_required)`
sdio_read_reg: *const fn (?*anyopaque, u32, [*]u8, u16, bool) callconv(.c) c_int,
/// :94 same
sdio_write_reg: *const fn (?*anyopaque, u32, [*]u8, u16, bool) callconv(.c) c_int,
/// :95 same
sdio_read_block: *const fn (?*anyopaque, u32, [*]u8, u16, bool) callconv(.c) c_int,
/// :96 same
sdio_write_block: *const fn (?*anyopaque, u32, [*]u8, u16, bool) callconv(.c) c_int,
/// :97 `int (*)(void *ctx, uint32_t ticks_to_wait)`
sdio_wait_slave_intr: *const fn (?*anyopaque, u32) callconv(.c) c_int,
// ---- Transport - SPI HD, :100-105
/// :100 `int (*)(uint32_t reg, uint32_t *data, int poll, bool lock_required)`
spi_hd_read_reg: *const fn (u32, *u32, c_int, bool) callconv(.c) c_int,
/// :101 `int (*)(uint32_t reg, uint32_t *data, bool lock_required)`
spi_hd_write_reg: *const fn (u32, *u32, bool) callconv(.c) c_int,
/// :102 `int (*)(uint8_t *data, uint16_t size, bool lock_required)`
spi_hd_read_dma: *const fn ([*]u8, u16, bool) callconv(.c) c_int,
/// :103 same
spi_hd_write_dma: *const fn ([*]u8, u16, bool) callconv(.c) c_int,
/// :104 `int (*)(uint32_t data_lines)`
spi_hd_set_data_lines: *const fn (u32) callconv(.c) c_int,
/// :105 `int (*)(void)`
spi_hd_send_cmd9: *const fn () callconv(.c) c_int,
// ---- Transport - UART, :108-110
/// :108 `int (*)(void *ctx, uint8_t *data, uint16_t size)`
uart_read: *const fn (?*anyopaque, [*]u8, u16) callconv(.c) c_int,
/// :109 same
uart_write: *const fn (?*anyopaque, [*]u8, u16) callconv(.c) c_int,
/// :110 `int (*)(void *ctx)`
uart_flush_input: *const fn (?*anyopaque) callconv(.c) c_int,
/// :112 `int (*)(void)`
restart_host: *const fn () callconv(.c) c_int,
/// :114 `int (*)(uint32_t power_save_type, void* gpio_port, uint32_t gpio_num, int level)`
config_host_power_save_hal_impl: *const fn (u32, ?*anyopaque, u32, c_int) callconv(.c) c_int,
/// :115 `int (*)(uint32_t power_save_type)`
start_host_power_save_hal_impl: *const fn (u32) callconv(.c) c_int,
/// :116 `int (*)(esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, uint32_t ticks_to_wait)`
event_post: *const fn (EventBase, i32, ?*anyopaque, usize, u32) callconv(.c) c_int,
};
/// `struct hosted_config_t`, `esp_hosted_os_abstraction.h:119-121`.
pub const HostedConfig = extern struct {
funcs: *const HostedOsiFuncs,
};
/// The three numbers the C side must agree on. Measured from C with the force-include in place;
/// the build re-measures and compares, so this is a contract and not a comment.
pub const layout_check = struct {
pub const sizeof: usize = 284;
pub const offset_config_gpio: usize = 148;
pub const offset_event_post: usize = 280;
};
comptime {
if (@sizeOf(usize) != 4) @compileError(
"this layout is rv32-specific: 71 pointers at 4 bytes each. Re-measure offsetof on any other target.",
);
assert(@sizeOf(HostedOsiFuncs) == layout_check.sizeof);
assert(@offsetOf(HostedOsiFuncs, "config_gpio") == layout_check.offset_config_gpio);
assert(@offsetOf(HostedOsiFuncs, "event_post") == layout_check.offset_event_post);
// Every member is one pointer, so the count is derivable and worth asserting: a field
// accidentally deleted or duplicated changes this even when the size happens to survive.
assert(std.meta.fields(HostedOsiFuncs).len == 71);
assert(@sizeOf(HostedOsiFuncs) == 71 * @sizeOf(usize));
}
// ============================================================================ the exported table
/// The table itself. `HOSTED_CONFIG_INIT_DEFAULT` points `g_h.funcs` here
/// (`esp_hosted_os_abstraction.h:125-127`), and `port_esp_hosted_host_os.c:938` is the definition
/// this replaces.
pub export const g_hosted_osi_funcs: HostedOsiFuncs = .{
.memcpy = hostedMemcpy,
.memset = hostedMemset,
.malloc = hostedMalloc,
.calloc = hostedCalloc,
.free = hostedFree,
.realloc = hostedRealloc,
.malloc_align = hostedMallocAlign,
.free_align = hostedFreeAlign,
.thread_create = hostedThreadCreate,
.thread_cancel = hostedThreadCancel,
.thread_yield = hostedThreadYield,
.msleep = hostedMsleep,
.usleep = hostedUsleep,
.sleep = hostedSleep,
.blocking_delay = hostedBlockingDelay,
.queue_item = hostedQueueItem,
.create_queue = hostedCreateQueue,
.dequeue_item = hostedDequeueItem,
.queue_msg_waiting = hostedQueueMsgWaiting,
.destroy_queue = hostedDestroyQueue,
.reset_queue = hostedResetQueue,
.unlock_mutex = hostedUnlockMutex,
.create_mutex = hostedCreateMutex,
.lock_mutex = hostedLockMutex,
.destroy_mutex = hostedDestroyMutex,
.post_semaphore = hostedPostSemaphore,
.post_semaphore_from_isr = hostedPostSemaphoreFromIsr,
.create_semaphore = hostedCreateSemaphore,
.get_semaphore = hostedGetSemaphore,
.destroy_semaphore = hostedDestroySemaphore,
.timer_stop = hostedTimerStop,
.timer_start = hostedTimerStart,
.get_time_ms = hostedGetTimeMs,
.create_lock_mempool = hostedCreateLockMempool,
.lock_mempool = hostedLockMempool,
.unlock_mempool = hostedUnlockMempool,
.destroy_lock_mempool = hostedDestroyLockMempool,
.config_gpio = hostedConfigGpio,
.config_gpio_as_interrupt = hostedConfigGpioAsInterrupt,
.teardown_gpio_interrupt = hostedTeardownGpioInterrupt,
.read_gpio = hostedReadGpio,
.write_gpio = hostedWriteGpio,
.pull_gpio = hostedPullGpio,
.hold_gpio = hostedHoldGpio,
.get_host_wakeup_or_reboot_reason = hostedGetWakeupReason,
.bus_init = hostedBusInit,
.bus_deinit = hostedBusDeinit,
.do_bus_transfer = stubDoBusTransfer,
.event_wifi_post = hostedEventWifiPost,
.printf = stubPrintf,
.hosted_init_hook = hostedInitHook,
.sdio_card_init = hostedSdioCardInit,
.sdio_card_deinit = hostedSdioCardDeinit,
.sdio_read_reg = hostedSdioReadReg,
.sdio_write_reg = hostedSdioWriteReg,
.sdio_read_block = hostedSdioReadBlock,
.sdio_write_block = hostedSdioWriteBlock,
.sdio_wait_slave_intr = hostedSdioWaitSlaveIntr,
.spi_hd_read_reg = stubSpiHdReadReg,
.spi_hd_write_reg = stubSpiHdWriteReg,
.spi_hd_read_dma = stubSpiHdReadDma,
.spi_hd_write_dma = stubSpiHdWriteDma,
.spi_hd_set_data_lines = stubSpiHdSetDataLines,
.spi_hd_send_cmd9 = stubSpiHdSendCmd9,
.uart_read = stubUartRead,
.uart_write = stubUartWrite,
.uart_flush_input = stubUartFlushInput,
.restart_host = hostedRestartHost,
.config_host_power_save_hal_impl = stubConfigHostPowerSave,
.start_host_power_save_hal_impl = stubStartHostPowerSave,
.event_post = hostedEventPost,
};
/// `extern struct hosted_config_t g_h;` (`esp_hosted_os_abstraction.h:129`). Statically
/// initialised, because C reads `g_h.funcs->...` and nothing guarantees `install` ran first - it is
/// the *state* behind the functions that needs installing, not the pointer to them.
pub export var g_h: HostedConfig = .{ .funcs = &g_hosted_osi_funcs };
// ============================================================================ installed state
/// Board wiring and sizing. Compile-time so the static footprint is a build-time number.
pub const Config = struct {
/// The C6's reset/enable pin. GPIO54 on this board (`sdkconfig:4557`,
/// `CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE=54`).
///
/// It has an external pull-up, so the *released* state is the one the pull-up wins. ESP-Hosted
/// drives `H_RESET_VAL_ACTIVE` last (`sdio_drv.c:1651-1657`), and with
/// `CONFIG_ESP_HOSTED_RESET_GPIO_ACTIVE_LOW` unset - which is how the working IDF build on this
/// board was configured - `H_RESET_VAL_ACTIVE` is `H_GPIO_HIGH`
/// (`port_esp_hosted_host_config.h:445-451`). So the sequence is high, low, high: a reset pulse
/// that ends released. Invert that and the radio stays in reset for ever.
reset_pin: u8 = 54,
/// CLIC external line for the GPIO interrupt aggregate (`hal.intr.Source.gpio_intr0`).
gpio_clic_line: u5 = 20,
/// CLIC external line for the SDMMC host, which is where the C6's D1 slave interrupt arrives.
sdio_clic_line: u5 = 21,
/// Software timer slots. ESP-Hosted arms at most three at once: the slave-unresponsive timer
/// (`transport_drv.c:188`), a per-request asynchronous RPC timeout (`rpc_core.c:215`), and the
/// power-save timer. Four leaves one spare and costs 96 bytes.
timer_slots: usize = 4,
/// Pads whose interrupt can be registered at once. The SDIO transport registers none; SPI
/// registers two. Four is generous and costs 48 bytes.
gpio_isr_slots: usize = 4,
/// Wait for the C6's D1 slave interrupt through the CLIC, or poll for it.
///
/// `true`. The reason it was `false` is worth keeping written down, because it was a
/// misdiagnosis rather than a hardware limit.
///
/// Every attempt printed `MARK PORT_SDIO_LAPSE ... intmask=0x00000000`, and that was read as
/// "the unmask does not stick". It never said that: the print happens *after*
/// `disarmSdioLine()`, which had just written that zero on purpose, and the other witness -
/// `hal.sdmmc.interruptDiagnostics` - runs on the application task, which is never inside an
/// arming window. `hal.sdmmc.armSlaveInterrupt` now reads INTMASK back inside the same masked
/// region as the store, so the claim is finally testable: `stuck=` on `MARK PORT_SDIO_ARM`.
///
/// What was really missing is `takeInterruptControl`. Nothing in this build had ever called
/// `hal.intr.init()` - `examples/intrcheck.zig` and `examples/portcheck.zig` do,
/// `examples/http.zig` and `examples/radio.zig` do not, and nothing under `src/` did either -
/// so mtvec still belonged to the bootloader, the threshold was never opened, and mstatus.MIE
/// was never this image's decision. A CLIC line enabled in that state either cannot be
/// delivered at all, which is a LAPSE every window for ever, or is delivered *outside this
/// image*, which is the "board goes silent right after Open data path at slave" that was
/// blamed on a storm.
///
/// Not verified on hardware by the author of this change. Two nets remain under it: the
/// bounded re-look (`sdio_relook_ms`) carries the transport through any window the interrupt
/// misses, and `sdio_foreign_limit` consecutive unexplained handler entries abandon the line
/// for `sdioPoll` permanently. Set this to `false` to isolate a regression against the proven
/// polling path; nothing else has to change, and with it false the CLIC is not touched at all.
sdio_use_interrupt: bool = true,
};
pub const config: Config = .{};
const State = struct {
io: Io = undefined,
/// The allocator handed to `install`. Used directly for OS-object handles, and wrapped by
/// `cheap` for everything C allocates.
gpa: Allocator = undefined,
cheap: hheap.CHeap = undefined,
timers: os.TimerService(config.timer_slots) = .{},
installed: bool = false,
/// The bus context `_h_bus_init` hands to C and C hands back to every `_h_sdio_*` call. Its
/// *identity* is all that matters - ESP-IDF returns `&context`, a file-static - so this is a
/// single static object and a null `ctx` from C is a real error rather than a second bus.
bus: BusContext = .{},
/// Called with every event ESP-Hosted posts. Association and DHCP-relevant events arrive here.
on_event: ?*const fn (Event) void = null,
/// Deferred wake word for the SDIO slave interrupt. The ISR bumps it and wakes; the waiter
/// futex-waits on it.
sdio_intr_epoch: std.atomic.Value(u32) = .init(0),
/// RINTSTS and IDSTS as `sdioDispatch` saw them at entry. Both are sticky, so reading them
/// after the handler has disarmed loses nothing. INTMASK is *not* sticky and is deliberately
/// absent here: the disarm has just rewritten it, so a handler-entry read of it could only ever
/// return the disarmed value. What the mask really was is `sdio_armed_intmask`.
sdio_intr_rintsts: std.atomic.Value(u32) = .init(0),
sdio_intr_idsts: std.atomic.Value(u32) = .init(0),
/// INTMASK and MINTSTS as `hal.sdmmc.armSlaveInterrupt` read them back, inside the same masked
/// region as the store that armed them. Written and read only by the waiting task, so plain
/// words rather than atomics.
sdio_armed_intmask: u32 = 0,
sdio_armed_mintsts: u32 = 0,
/// Consecutive handler entries whose cause was not the card interrupt. Reset by any real one.
/// At `sdio_foreign_limit` the wait stops using the interrupt at all.
sdio_intr_foreign: u32 = 0,
/// Arming windows that lapsed with no handler entry. **At idle this is the normal state and
/// says nothing is wrong**: the C6 has nothing to report, so no interrupt arrives inside
/// `sdio_relook_ms`, the re-look finds nothing either, and the wait goes round again. It is
/// counted and printed because a *rising* count with frames flowing is how the re-look
/// carrying the transport announces itself.
sdio_intr_lapses: u32 = 0,
/// Consecutive lapsed windows in which the re-look then found the card *already calling* -
/// the pad low or the latch set. That is the failure that matters, and it is the only reading
/// that separates "the interrupt is not being delivered" from "the card is quiet": an idle
/// card lapses for free, a calling card whose interrupt did not arrive costs a real frame up
/// to `sdio_relook_ms` of latency.
///
/// Reset by any wake the handler really delivered. At `sdio_missed_limit` the wait gives the
/// line up for `sdioPoll` permanently, which is what keeps the interrupt path from being
/// strictly worse than the 1 ms poll it replaces.
sdio_intr_missed: u32 = 0,
/// MINTSTS and RINTSTS as `sdioDispatch` read them, *before* it disarmed. MINTSTS is
/// `RINTSTS & INTMASK` and the disarm zeroes it, so this is the only place its value at the
/// moment of delivery survives - and it is the direct answer to "does MINTSTS ever show this
/// slot's bit".
sdio_intr_mintsts: std.atomic.Value(u32) = .init(0),
/// Remaining diagnostic lines, one budget per failure mode. See `sdioMark`.
sdio_foreign_marks: u32 = 0,
sdio_lapse_marks: u32 = 0,
sdio_missed_marks: u32 = 0,
sdio_arm_marks: u32 = 0,
sdio_wake_marks: u32 = 0,
/// Arming windows completed, for the periodic tally. Every budgeted MARK above eventually goes
/// quiet; this one does not, because "is the interrupt or the re-look carrying the transport"
/// is a question that stays interesting for the whole run.
sdio_windows: u32 = 0,
/// GPIO ISR registrations, indexed arbitrarily.
gpio_isrs: [config.gpio_isr_slots]GpioIsr = @splat(.{}),
/// `_h_sleep` calls. In the file set build.zig compiles this counts exactly one thing: the
/// two `if (!is_rpc_lib_ready()) _h_sleep(1)` loops at the head of `rpc_rx_thread` and
/// `rpc_tx_thread` (rpc_core.c:482-485, :543-547). The tree's only other `_h_sleep` callers
/// are transport_drv.c:693, which is followed by `assert(0!=0)`, and stats.c:115 in
/// `raw_tp_tx_task`, which is never created with TEST_RAW_TP off.
///
/// So a count that keeps *growing* while a synchronous RPC request is outstanding means the
/// RPC lib state is not READY and the request will never be transmitted - the failure that
/// otherwise looks exactly like a coprocessor that does not answer. Two per second while
/// stuck, and it costs one add.
hosted_sleep_calls: u32 = 0,
/// Loud-stub call count. Nonzero after a run means a path nobody implemented was taken.
stub_calls: u32 = 0,
};
const GpioIsr = struct {
pin: u8 = 0xFF,
handler: ?IsrHandler = null,
arg: ?*anyopaque = null,
};
const BusContext = struct {
/// `hosted_sdio_init` creates this and every `SDIO_LOCK` takes it
/// (`port_esp_hosted_host_sdio.c:36-42, 395`).
lock: os.Mutex = .{},
up: bool = false,
};
var state: State = .{};
/// An event ESP-Hosted posted. `base` distinguishes `WIFI_EVENT` (via `_h_event_wifi_post`) from
/// `ESP_HOSTED_EVENT` and anything else (via `_h_event_post`).
pub const Event = struct {
pub const Base = union(enum) {
wifi,
/// The `esp_event_base_t` string C passed, which is a pointer to a string literal owned by
/// the C side and valid for the lifetime of the program.
named: EventBase,
};
base: Base,
id: i32,
/// Borrowed for the duration of the callback only. ESP-IDF's `esp_event_post` copies;
/// this does not, so a handler that needs the data past its return must copy it.
data: ?[]const u8,
};
/// Bring the table's state up. Idempotent.
///
/// After this returns, C may call anything in `g_h.funcs`. Note what it does *not* do: it does not
/// start a scheduler and it does not touch the radio. ESP-Hosted's own `esp_hosted_init` does that,
/// and the tasks it spawns through `_h_thread_create` first execute when the calling context next
/// blocks - `io.async` assigns a slot and marks it ready, it does not preempt. A caller that
/// installs, initialises ESP-Hosted and then never blocks will see nothing happen.
pub fn install(io: Io, gpa: Allocator) void {
state.io = io;
state.gpa = gpa;
state.cheap = .{ .gpa = gpa };
state.installed = true;
// The timer service owns one task; start it eagerly so `_h_timer_start` cannot fail for want
// of a scheduler.
if (!state.timers.start(io, gpa)) note("MARK PORT_TIMER_SERVICE_FAIL\r\n", .{});
}
/// Register the application's event sink. Association, disconnection and the slave's own lifecycle
/// events arrive here; this is not a reimplementation of `esp_event`, it is one callback.
pub fn setEventHandler(handler: ?*const fn (Event) void) void {
state.on_event = handler;
}
/// Diagnostics for a hardware self-test: heap use, whether any loud stub was reached, and whether
/// ESP-Hosted's RPC threads are stuck in their not-ready loop. See `State.hosted_sleep_calls`.
pub fn stats() struct {
bytes_live: usize,
bytes_reserved: usize,
peak_reserved: usize,
blocks_live: usize,
alloc_failures: usize,
stub_calls: u32,
hosted_sleep_calls: u32,
} {
return .{
.bytes_live = state.cheap.bytes_live,
.bytes_reserved = state.cheap.bytes_reserved,
.peak_reserved = state.cheap.peak_reserved,
.blocks_live = state.cheap.blocks_live,
.alloc_failures = state.cheap.failures,
.stub_calls = state.stub_calls,
.hosted_sleep_calls = state.hosted_sleep_calls,
};
}
/// The SDIO card-interrupt path's counters, for a heartbeat that wants to say whether the radio is
/// being woken or polled. Every field is a running total, none is reset by anything here.
///
/// `epoch` is handler entries. `foreign` is *consecutive* entries whose cause was not the card
/// interrupt - at `sdio_foreign_limit` the wait abandons the interrupt for `sdioPoll`, so a
/// non-zero `foreign` with a growing `epoch` means the line is being taken for the wrong reason.
/// `lapses` is arming windows that produced no entry at all; at idle that is the resting state and
/// costs nothing. `missed` is the subset of those whose re-look then found the card already
/// calling, which is the one that matters - at `sdio_missed_limit` the wait abandons the interrupt
/// too. `rintsts`/`idsts` are what the last handler entry saw; `armed_intmask` is what INTMASK read
/// back at the last arm, which is the only reading of that register that means anything.
pub fn sdioStats() struct {
epoch: u32,
foreign: u32,
lapses: u32,
missed: u32,
rintsts: u32,
idsts: u32,
armed_intmask: u32,
} {
return .{
.epoch = state.sdio_intr_epoch.load(.acquire),
.foreign = state.sdio_intr_foreign,
.lapses = state.sdio_intr_lapses,
.missed = state.sdio_intr_missed,
.rintsts = state.sdio_intr_rintsts.load(.acquire),
.idsts = state.sdio_intr_idsts.load(.acquire),
.armed_intmask = state.sdio_armed_intmask,
};
}
inline fn currentIo() Io {
assert(state.installed);
return state.io;
}
// ============================================================================ 1. memory
fn hostedMemcpy(dest: ?*anyopaque, src: ?*const anyopaque, size: u32) callconv(.c) ?*anyopaque {
// ESP-IDF asserts on a null pointer with a nonzero size (port_esp_hosted_host_os.c:67-76); the
// same condition, as a Zig assertion.
if (size == 0) return dest;
const d: [*]u8 = @ptrCast(dest.?);
const s: [*]const u8 = @ptrCast(src.?);
@memcpy(d[0..size], s[0..size]);
return dest;
}
fn hostedMemset(buf: ?*anyopaque, val: c_int, len: usize) callconv(.c) ?*anyopaque {
if (len == 0) return buf;
const b: [*]u8 = @ptrCast(buf.?);
@memset(b[0..len], @truncate(@as(c_uint, @bitCast(val))));
return buf;
}
fn hostedMalloc(size: usize) callconv(.c) ?*anyopaque {
assert(state.installed);
return @ptrCast(state.cheap.malloc(size));
}
fn hostedCalloc(blk_no: usize, size: usize) callconv(.c) ?*anyopaque {
assert(state.installed);
return @ptrCast(state.cheap.calloc(blk_no, size));
}
fn hostedFree(ptr: ?*anyopaque) callconv(.c) void {
assert(state.installed);
state.cheap.free(@ptrCast(ptr));
}
fn hostedRealloc(mem: ?*anyopaque, newsize: usize) callconv(.c) ?*anyopaque {
assert(state.installed);
return @ptrCast(state.cheap.realloc(@ptrCast(mem), newsize));
}
/// `_h_malloc_align(size, align)`. ESP-IDF routes this to `heap_caps_aligned_alloc` with
/// DMA-capable caps (`port_esp_hosted_host_os.c:128-143`) because IDF's SDMMC driver DMAs straight
/// out of the caller's buffer.
///
/// Ours does not: `hal.sdmmc` bounces every CMD53 through its own 64-byte-aligned buffer reached
/// through the non-cacheable alias, and memcpy's to and from the caller's slice. So the alignment
/// is honoured - it costs 64 bytes a buffer and callers may reasonably rely on it - but nothing
/// downstream needs it, and `_h_malloc` would do.
fn hostedMallocAlign(size: usize, alignment: usize) callconv(.c) ?*anyopaque {
assert(state.installed);
// ESP-Hosted only ever asks for 4, 32 or 64 (HOSTED_MEM_ALIGNMENT_*,
// port_esp_hosted_host_os.h:93-95). A non-power-of-two would silently corrupt the header
// arithmetic, so refuse it.
if (alignment == 0 or !std.math.isPowerOfTwo(alignment) or alignment > hheap.CHeap.max_alignment) {
note("MARK PORT_BAD_ALIGN %u\r\n", .{@as(u32, @intCast(alignment))});
return null;
}
return @ptrCast(state.cheap.mallocAligned(size, alignment));
}
/// One header format for both `_h_free` and `_h_free_align`, because ESP-IDF has one too: its
/// `hosted_free_align` is a plain `free` (`port_esp_hosted_host_os.c:145-148`), and mixing the two
/// is legal in the tree - `sdio_drv.c:353` frees with `_h_free_align` a buffer that
/// `transport_util.c:14` allocated with `_h_malloc_align`, while `HOSTED_FREE` uses `_h_free`
/// throughout.
fn hostedFreeAlign(ptr: ?*anyopaque) callconv(.c) void {
assert(state.installed);
state.cheap.free(@ptrCast(ptr));
}
// ============================================================================ 2. sync
fn hostedCreateMutex() callconv(.c) ?*anyopaque {
assert(state.installed);
const m = state.gpa.create(os.Mutex) catch return null;
m.* = .{};
return @ptrCast(m);
}
fn hostedLockMutex(handle: ?*anyopaque, timeout_ms: c_int) callconv(.c) c_int {
const m: *os.Mutex = @ptrCast(@alignCast(handle orelse return ret.invalid));
return m.lock(currentIo(), .fromMillis(timeout_ms));
}
fn hostedUnlockMutex(handle: ?*anyopaque) callconv(.c) c_int {
const m: *os.Mutex = @ptrCast(@alignCast(handle orelse return ret.invalid));
return m.unlock(currentIo());
}
fn hostedDestroyMutex(handle: ?*anyopaque) callconv(.c) c_int {
const m: *os.Mutex = @ptrCast(@alignCast(handle orelse return ret.invalid));
state.gpa.destroy(m);
return ret.ok;
}
fn hostedCreateSemaphore(max_count: c_int) callconv(.c) ?*anyopaque {
assert(state.installed);
const s = state.gpa.create(os.Semaphore) catch return null;
s.* = .init(if (max_count > 0) @intCast(max_count) else 1);
return @ptrCast(s);
}
fn hostedPostSemaphore(handle: ?*anyopaque) callconv(.c) c_int {
const s: *os.Semaphore = @ptrCast(@alignCast(handle orelse return ret.invalid));
return s.post(currentIo());
}
/// See `hosted_os.Semaphore.postFromIsr` for what "from ISR" can and cannot mean here.
fn hostedPostSemaphoreFromIsr(handle: ?*anyopaque) callconv(.c) c_int {
const s: *os.Semaphore = @ptrCast(@alignCast(handle orelse return ret.invalid));
return s.postFromIsr(state.io);
}
fn hostedGetSemaphore(handle: ?*anyopaque, timeout_ms: c_int) callconv(.c) c_int {
const s: *os.Semaphore = @ptrCast(@alignCast(handle orelse return ret.invalid));
return s.wait(currentIo(), .fromMillis(timeout_ms));
}
fn hostedDestroySemaphore(handle: ?*anyopaque) callconv(.c) c_int {
const s: *os.Semaphore = @ptrCast(@alignCast(handle orelse return ret.invalid));
state.gpa.destroy(s);
return ret.ok;
}
fn hostedCreateQueue(qnum_elem: u32, qitem_size: u32) callconv(.c) ?*anyopaque {
assert(state.installed);
if (qnum_elem == 0 or qitem_size == 0) return null;
return @ptrCast(os.Queue.create(state.gpa, qnum_elem, qitem_size));
}
fn hostedQueueItem(handle: ?*anyopaque, item: ?*const anyopaque, timeout: c_int) callconv(.c) c_int {
const q: *os.Queue = @ptrCast(@alignCast(handle orelse return ret.invalid));
const p: [*]const u8 = @ptrCast(item orelse return ret.invalid);
// `_h_queue_item`'s timeout reaches xQueueSendToBack unconverted, so its units are ticks; every
// caller passes HOSTED_BLOCK_MAX or 0, both of which mean the same thing in either dialect.
return q.send(currentIo(), p, .fromMillis(timeout));
}
fn hostedDequeueItem(handle: ?*anyopaque, item: ?*anyopaque, timeout: c_int) callconv(.c) c_int {
const q: *os.Queue = @ptrCast(@alignCast(handle orelse return ret.invalid));
const p: [*]u8 = @ptrCast(item orelse return ret.invalid);
// Seconds, not milliseconds, on the positive branch. See `hosted_os.Wait.fromQueueTimeout`.
return q.receive(currentIo(), p, .fromQueueTimeout(timeout));
}
fn hostedQueueMsgWaiting(handle: ?*anyopaque) callconv(.c) c_int {
const q: *os.Queue = @ptrCast(@alignCast(handle orelse return ret.invalid));
return q.waiting(currentIo());
}
fn hostedDestroyQueue(handle: ?*anyopaque) callconv(.c) c_int {
const q: *os.Queue = @ptrCast(@alignCast(handle orelse return ret.invalid));
q.destroy(currentIo(), state.gpa);
return ret.ok;
}
fn hostedResetQueue(handle: ?*anyopaque) callconv(.c) c_int {
const q: *os.Queue = @ptrCast(@alignCast(handle orelse return ret.invalid));
return q.reset(currentIo());
}
/// The mempool lock. `H_USE_MEMPOOL` is 1 in this board's configuration, so these four must not be
/// null even though the version of `common/mempool/mempool.c` in this tree does not call them.
///
/// ESP-IDF uses a `portMUX_TYPE` spinlock and `portENTER_CRITICAL`
/// (`port_esp_hosted_host_os.c:602-643`), which on a multi-core preemptive kernel means "take the
/// spinlock and disable interrupts". On one core with a cooperative scheduler the spinlock half is
/// vacuous - there is no other core to contend with - and the interrupt half is the whole content.
/// So the handle is `hal.intr`'s nesting mask guard, and the critical section is exactly as long as
/// interrupts are off.
const MempoolLock = struct {
guard: hal.clkrst.Guard = undefined,
held: bool = false,
};
fn hostedCreateLockMempool() callconv(.c) ?*anyopaque {
assert(state.installed);
const l = state.gpa.create(MempoolLock) catch return null;
l.* = .{};
return @ptrCast(l);
}
fn hostedLockMempool(handle: ?*anyopaque) callconv(.c) void {
const l: *MempoolLock = @ptrCast(@alignCast(handle orelse return));
l.guard = hal.intr.mask();
l.held = true;
}
fn hostedUnlockMempool(handle: ?*anyopaque) callconv(.c) void {
const l: *MempoolLock = @ptrCast(@alignCast(handle orelse return));
if (!l.held) return;
l.held = false;
l.guard.release();
}
fn hostedDestroyLockMempool(handle: ?*anyopaque) callconv(.c) void {
const l: *MempoolLock = @ptrCast(@alignCast(handle orelse return));
state.gpa.destroy(l);
}
// ============================================================================ 3. threads
/// ESP-Hosted spawns **seven** tasks on the SDIO transport, and their requested stacks are the
/// single largest memory claim in the whole port:
///
/// sdio_rx_buf RX_BUF_TASK_STACK_SIZE sdio_drv.c:1542 (= CONFIG_ESP_HOSTED_DFLT_TASK_STACK)
/// sdio_read DFLT_TASK_STACK_SIZE sdio_drv.c:1545
/// sdio_process_rx DFLT_TASK_STACK_SIZE sdio_drv.c:1548
/// sdio_write DFLT_TASK_STACK_SIZE sdio_drv.c:1551
/// rpc_rx RPC_TASK_STACK_SIZE rpc_core.c:578
/// rpc_tx RPC_TASK_STACK_SIZE rpc_core.c:580
/// rpc_supp_cb RPC_TASK_STACK_SIZE rpc_wrap.c:2398
///
/// `DFLT_TASK_STACK_SIZE` and `RPC_TASK_STACK_SIZE` are both `5*1024`
/// (`port_esp_hosted_host_os.h:64-67`), and ESP-IDF's `xTaskCreate` takes bytes, so the ask is
/// 35 KB. Plus this port's timer service task, plus the main context, that is nine slots.
///
/// The requested size is **ignored**, and that is not laziness: `std.Io.async` has no stack-size
/// parameter, and the runtime takes the first free slot from a pool whose slots are all declared at
/// one size. The number to declare is therefore the worst case over all seven, which is what the
/// caller of `install` decides when it builds its `Runtime`. 5 KB is FreeRTOS's number for tasks
/// that call `printf`; these bodies do not, and the honest way to size the pool is a painted-stack
/// watermark on the die, not this constant.
pub const thread_count = 7;
pub const requested_stack_bytes = 5 * 1024;
fn hostedThreadCreate(
tname: [*:0]const u8,
tprio: u32,
tstack_size: u32,
start_routine: StartRoutine,
sr_arg: ?*anyopaque,
) callconv(.c) ?*anyopaque {
assert(state.installed);
// Priority is meaningless on a cooperative scheduler: a task runs until it blocks, and
// ESP-Hosted gives all seven the same priority anyway (RPC_TASK_PRIO and DFLT_TASK_PRIO are
// both 23, port_esp_hosted_host_os.h:65-68).
_ = tprio;
_ = tstack_size;
return @ptrCast(os.Thread.create(currentIo(), state.gpa, tname, start_routine, sr_arg));
}
fn hostedThreadCancel(handle: ?*anyopaque) callconv(.c) c_int {
const t: *os.Thread = @ptrCast(@alignCast(handle orelse return ret.invalid));
return t.cancel(currentIo(), state.gpa);
}
fn hostedThreadYield() callconv(.c) void {
// A zero-duration sleep is the portable yield, and on this runtime it is a documented one
// trip round the run queue rather than a no-op. Cancelation is swallowed because the C caller
// (`spi_hd_drv.c:568`, the only one in the tree) has nowhere to report it.
currentIo().sleep(.zero, os.clock) catch {};
}
// ============================================================================ 4. time
fn hostedMsleep(mseconds: c_uint) callconv(.c) c_uint {
currentIo().sleep(.fromMilliseconds(mseconds), os.clock) catch {};
return 0;
}
fn hostedUsleep(useconds: c_uint) callconv(.c) c_uint {
currentIo().sleep(.fromMicroseconds(useconds), os.clock) catch {};
return 0;
}
/// Counted, because in this build every call is one turn of an ESP-Hosted RPC thread's not-ready
/// spin. See `State.hosted_sleep_calls`.
fn hostedSleep(seconds: c_uint) callconv(.c) c_uint {
state.hosted_sleep_calls += 1;
return hostedMsleep(seconds *| 1000);
}
/// `_h_blocking_delay` is documented in ESP-Hosted as a "non sleepable delay - BLOCKING dead wait"
/// and implemented as `for (idx = 0; idx < 100*number; idx++)` on a `volatile`
/// (`port_esp_hosted_host_os.c:261-267`). That is a loop count, not a duration, and its wall-clock
/// meaning depends on the compiler and the CPU clock.
///
/// It is reproduced as a real busy-wait rather than a sleep, because a caller reaching for this
/// specifically wants not to yield - and reproduced against `hal.systimer` rather than a loop
/// count, so the delay is at least defined. ESP-IDF's version at 360 MHz takes roughly 0.3 us per
/// unit; at this board's measured 90 MHz it would be about 1.1 us, and 1 us is the round number in
/// range. **Nothing in the tree calls this**, verified by grep, so no behaviour depends on the
/// choice.
///
/// On a cooperative scheduler this starves every other task for the duration. That is inherent to
/// what the entry means, not a defect of this implementation.
fn hostedBlockingDelay(number: c_uint) callconv(.c) c_uint {
hal.systimer.delayMicros(number);
return 0;
}
fn hostedGetTimeMs() callconv(.c) u64 {
return os.nowMs(currentIo());
}
// ============================================================================ timers
/// A timer handle as C sees it. ESP-IDF hands back a heap pointer
/// (`port_esp_hosted_host_os.c:697`); this hands back a pointer to one, so `_h_timer_stop` can find
/// the slot and free the handle exactly as ESP-IDF's does.
const TimerHandle = struct {
slot: usize,
};
fn hostedTimerStart(
name: [*:0]const u8,
duration_ms: c_int,
kind: c_int,
handler: TimerHandler,
arg: ?*anyopaque,
) callconv(.c) ?*anyopaque {
assert(state.installed);
if (duration_ms < 0) return null;
const k: os.TimerKind = switch (kind) {
0 => .oneshot,
1 => .periodic,
else => {
// ESP-IDF logs "Unsupported timer type" and returns NULL (:720-725).
note("MARK PORT_TIMER_BAD_TYPE %s %d\r\n", .{ name, kind });
return null;
},
};
const slot = state.timers.arm(currentIo(), @intCast(duration_ms), k, handler, arg) orelse {
note("MARK PORT_TIMER_SLOTS_FULL %s\r\n", .{name});
return null;
};
const h = state.gpa.create(TimerHandle) catch {
_ = state.timers.disarm(currentIo(), slot);
return null;
};
h.* = .{ .slot = slot };
return @ptrCast(h);
}
fn hostedTimerStop(handle: ?*anyopaque) callconv(.c) c_int {
const h: *TimerHandle = @ptrCast(@alignCast(handle orelse return ret.fail));
const r = state.timers.disarm(currentIo(), h.slot);
state.gpa.destroy(h);
return r;
}
// ============================================================================ 5. GPIO
/// `H_GPIO_MODE_DEF_*`, `port_esp_hosted_host_os.h:71-73`: bit 0 input, bit 1 output, bit 2
/// open-drain.
const gpio_mode_input: u32 = 1 << 0;
const gpio_mode_output: u32 = 1 << 1;
const gpio_mode_open_drain: u32 = 1 << 2;
/// `H_GPIO_PULL_UP` is 1 and `H_GPIO_PULL_DOWN` is 0 (`port_esp_hosted_host_os.h:83-84`) - note
/// that this is a *direction* selector and not a boolean, and the separate `enable` argument says
/// whether to turn that resistor on or off.
const gpio_pull_up: u32 = 1;
/// `_h_config_gpio`. The `gpio_port` argument is always `H_GPIO_PORT_DEFAULT` / NULL on this chip
/// (`port_esp_hosted_host_config.h:435`); ESP-IDF ignores it too.
///
/// ESP-IDF's version goes through `gpio_config`, which also clears both pulls
/// (`port_esp_hosted_host_os.c:746-758`). Reproduced, because the reset pin depends on it: GPIO54
/// has an external pull-up and an internal pull-down fighting it would be a weak, marginal high.
fn hostedConfigGpio(gpio_port: ?*anyopaque, gpio_num: u32, mode: u32) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
const pin: u8 = @intCast(gpio_num);
hal.gpio.setFunction(pin, .gpio);
hal.gpio.setPull(pin, .none);
hal.gpio.setOpenDrain(pin, mode & gpio_mode_open_drain != 0);
hal.gpio.setInputEnable(pin, mode & gpio_mode_input != 0);
if (mode & gpio_mode_output != 0) {
// Point the matrix at the GPIO peripheral before enabling the driver, so the pad never
// spends an instant driven by whatever signal the matrix happened to hold.
hal.gpio.matrixOut(pin, hal.gpio.matrix_gpio_signal);
hal.gpio.outputEnable(pin);
} else {
hal.gpio.outputDisable(pin);
}
return ret.ok;
}
fn hostedReadGpio(gpio_port: ?*anyopaque, gpio_num: u32) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
return hal.gpio.getLevel(@intCast(gpio_num));
}
fn hostedWriteGpio(gpio_port: ?*anyopaque, gpio_num: u32, value: u32) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
hal.gpio.setLevel(@intCast(gpio_num), if (value != 0) 1 else 0);
return ret.ok;
}
/// `_h_pull_gpio(port, pin, pull_value, enable)`.
///
/// The four-argument shape does not map onto one register field: the P4 has one pull-up bit and one
/// pull-down bit, and `hal.gpio.setPull` writes both in one store precisely so a pad can never end
/// up with two resistors fighting. Disabling one pull therefore means "leave the *other* alone",
/// which is read back rather than assumed.
fn hostedPullGpio(gpio_port: ?*anyopaque, gpio_num: u32, pull_value: u32, enable: u32) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
const pin: u8 = @intCast(gpio_num);
const up = pull_value == gpio_pull_up;
if (enable != 0) {
hal.gpio.setPull(pin, if (up) .up else .down);
} else {
// gpio_pullup_dis / gpio_pulldown_dis clear one bit only. If the other pull is not set
// either, the pad ends up floating, which is what ESP-IDF leaves behind too.
const current = hal.gpio.getPull(pin);
const target: hal.gpio.Pull = if (up)
(if (current == .down) .down else .none)
else
(if (current == .up) .up else .none);
hal.gpio.setPull(pin, target);
}
return ret.ok;
}
/// `_h_hold_gpio`. ESP-IDF calls `gpio_hold_en`, which latches a pad's output through a sleep or a
/// domain power-down so the slave is not reset by the host napping.
///
/// This image never sleeps and never powers a domain down: `_h_config_host_power_save_hal_impl` and
/// `_h_start_host_power_save_hal_impl` are both loud stubs, and the only callers of this entry are
/// in `power_save_drv.c:210,230`, which those stubs make unreachable. Holding a pad against a sleep
/// that cannot happen is not a no-op worth pretending to - the P4's hold bit lives in
/// `LP_AON`/`HP_SYS` registers the HAL does not model, and writing them blind is how a pad gets
/// stuck. So this reports failure loudly instead.
fn hostedHoldGpio(gpio_port: ?*anyopaque, gpio_num: u32, hold_value: u32) callconv(.c) c_int {
_ = gpio_port;
state.stub_calls += 1;
note("MARK PORT_STUB _h_hold_gpio pin=%u hold=%u (no sleep support; nothing should reach this)\r\n", .{ gpio_num, hold_value });
return ret.fail;
}
/// `H_GPIO_INTR_*`, `port_esp_hosted_host_config.h:56-62`. The values coincide exactly with the
/// P4's `GPIO_PINn_INT_TYPE` encoding (`gpio_reg.h:377-381`), which is not a coincidence: the
/// enum was written from it.
fn intrTypeFromHosted(intr_type: u32) ?hal.gpio.IntrType {
return switch (intr_type) {
0 => .disable,
1 => .posedge,
2 => .negedge,
3 => .anyedge,
4 => .low_level,
5 => .high_level,
else => null,
};
}
/// `_h_config_gpio_as_interrupt`.
///
/// ESP-IDF's version (`port_esp_hosted_host_os.c:760-797`) configures the pad as an input with a
/// pull that opposes the edge being detected, installs IDF's shared GPIO ISR service, adds a
/// per-pin handler, then sets the trigger type and enables. Same five steps here, with `hal.gpio`
/// and `hal.intr` in place of the driver:
///
/// 1. pad as input, pull opposing the edge - a floating pad on an edge-triggered interrupt is a
/// free-running interrupt source.
/// 2. record (pin, handler, arg) in `state.gpio_isrs`.
/// 3. arm the pad on GPIO interrupt line 0, which is the line ESP-IDF uses.
/// 4. route `gpio_intr0` to a CLIC line and give it `gpioDispatch`, once.
/// 5. enable.
///
/// The CLIC trigger is **level**, not edge: the GPIO peripheral holds its line asserted while any
/// status bit is set, and the handler clears the status. An edge-triggered CLIC line here would
/// lose a second pad's event that arrived while the first was being serviced.
///
/// Nothing in the SDIO transport calls this. Its callers are `spi_drv.c:625,628`,
/// `spi_hd_drv.c:548` and `power_save_drv.c:68`. It is implemented rather than stubbed because it
/// costs little and because a host-wakeup pin is the obvious next use.
fn hostedConfigGpioAsInterrupt(
gpio_port: ?*anyopaque,
gpio_num: u32,
intr_type: u32,
handler: IsrHandler,
arg: ?*anyopaque,
) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
const pin: u8 = @intCast(gpio_num);
const t = intrTypeFromHosted(intr_type) orelse {
note("MARK PORT_GPIO_BAD_INTR_TYPE %u\r\n", .{intr_type});
return ret.invalid;
};
// ESP-IDF pulls up for a falling edge and down for anything else (:771-775).
hal.gpio.configureInput(pin, .{ .pull = if (t == .negedge) .up else .down });
const slot = blk: {
for (&state.gpio_isrs) |*s| if (s.pin == pin) break :blk s;
for (&state.gpio_isrs) |*s| if (s.handler == null) break :blk s;
note("MARK PORT_GPIO_ISR_SLOTS_FULL pin=%u\r\n", .{gpio_num});
return ret.fail;
};
slot.* = .{ .pin = pin, .handler = handler, .arg = arg };
if (!gpio_line_attached) {
gpio_line_attached = true;
// mtvec, MTVT, the threshold and MIE, before a line that `configureLine` enables as its
// last act can be delivered anywhere. See `takeInterruptControl`.
takeInterruptControl();
hal.intr.routeId(@intFromEnum(hal.intr.Source.gpio_intr0), config.gpio_clic_line);
hal.intr.configureLine(config.gpio_clic_line, .{
.handler = gpioDispatch,
.trigger = .level,
});
}
hal.gpio.setInterrupt(pin, t, .line0);
return ret.ok;
}
fn hostedTeardownGpioInterrupt(gpio_port: ?*anyopaque, gpio_num: u32) callconv(.c) c_int {
_ = gpio_port;
if (gpio_num > hal.gpio.max_pin) return ret.invalid;
const pin: u8 = @intCast(gpio_num);
hal.gpio.disableInterrupt(pin);
hal.gpio.clearInterrupt(pin);
for (&state.gpio_isrs) |*s| {
if (s.pin == pin) s.* = .{};
}
return ret.ok;
}
var gpio_line_attached: bool = false;
/// The one CLIC handler behind every registered pad. Reads the whole pending mask once, clears it
/// once, then dispatches - so an event on a second pad arriving mid-dispatch is caught by the next
/// interrupt rather than lost.
///
/// The status is cleared *before* the handlers run. For an edge-triggered pad that is the correct
/// order: clearing after the handler would drop an edge that arrived during it.
fn gpioDispatch(line: u5) void {
_ = line;
const pending = hal.gpio.pendingMask(.line0);
hal.gpio.clearInterrupts(pending.low, pending.high);
for (&state.gpio_isrs) |*s| {
const h = s.handler orelse continue;
const bit: u32 = @as(u32, 1) << @intCast(if (s.pin < 32) s.pin else s.pin - 32);
const hit = if (s.pin < 32) pending.low & bit else pending.high & bit;
if (hit != 0) h(s.arg);
}
}
// ============================================================================ 6. SDIO
/// `ESP_ADDRESS_MASK`, `host/drivers/transport/sdio/sdio_reg.h:87`. Slave scratch registers live in
/// the low 10 bits of function 1's address space, and ESP-Hosted masks every register address with
/// this before the transfer (`port_esp_hosted_host_sdio.c:500,523`). Block transfers are *not*
/// masked, which is why `ESP_SLAVE_CMD53_END_ADDR - data_left` works.
const esp_address_mask: u32 = 0x3FF;
/// `ESP_BLOCK_SIZE`, `sdio_reg.h:39`.
const esp_block_size: u32 = 512;
/// The SDIO function ESP-Hosted talks to. `SDIO_FUNC_1`.
const sdio_func: u3 = 1;
/// `ESP_OK` / `ESP_FAIL` as `esp_err_t`, which is what the `_h_sdio_*` entries return and what
/// `sdio_drv.c` tests against zero.
const esp_ok: c_int = 0;
const esp_fail: c_int = -1;
fn busCtx(ctx: ?*anyopaque) ?*BusContext {
const p = ctx orelse return null;
const b: *BusContext = @ptrCast(@alignCast(p));
// ESP-IDF returns a pointer to one file-static context; anything else is a bug, and a wild
// pointer here would be a wild bus.
if (b != &state.bus) return null;
return b;
}
/// `_h_bus_init` = `hosted_sdio_init` (`port_esp_hosted_host_sdio.c:317-399`): bring the SDMMC host
/// and slot up, create the bus mutex, return the context. Guarded against a second call, as the
/// original is (`:322-326`).
///
/// The slot, width and clock are `hal.sdmmc`'s defaults, which are this board's measured working
/// configuration: slot 1, 4-bit, 40 MHz, CLK 18 / CMD 19 / D0-D3 14-17.
fn hostedBusInit() callconv(.c) ?*anyopaque {
assert(state.installed);
if (state.bus.up) {
note("MARK PORT_SDIO_ALREADY_UP\r\n", .{});
return @ptrCast(&state.bus);
}
hal.sdmmc.init(.{}) catch |e| {
note("MARK PORT_SDIO_INIT_FAIL %s\r\n", .{@errorName(e).ptr});
return null;
};
state.bus = .{ .lock = .{}, .up = true };
return @ptrCast(&state.bus);
}
fn hostedBusDeinit(ctx: ?*anyopaque) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
b.up = false;
return esp_ok;
}
/// `_h_sdio_card_init` = `hosted_sdio_card_init` + `hosted_sdio_card_fn_init`
/// (`port_esp_hosted_host_sdio.c:141-217, 401-471`).
///
/// `hal.sdmmc.cardInit` does the SD/SDIO card identification and programmes the host's block size.
/// What is left is the part that is ESP-Hosted's protocol rather than the bus's: enable function 1,
/// wait for it to report ready, enable its interrupt, and set the CCCR block size for functions 0
/// and 1. Those writes are idempotent and the read-back is the check; the sequence is reproduced
/// in ESP-IDF's order because that order is what this board was observed to come up with.
///
/// Failure returns `ESP_FAIL` rather than asserting, because the caller retries: `sdio_drv.c:1638`
/// loops up to `CARD_INIT_TIMEOUT_MS`, and the first register reads after a reset legitimately
/// fail while the C6 is still booting (`:150-153`).
fn hostedSdioCardInit(ctx: ?*anyopaque, show_config: bool) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
_ = b;
hal.sdmmc.cardInit() catch |e| {
note("MARK PORT_SDIO_CARD_INIT_FAIL %s\r\n", .{@errorName(e).ptr});
return esp_fail;
};
if (show_config) {
note("MARK PORT_SDIO slot=1 width=4 khz=40000 clk=18 cmd=19 d0-3=14,15,16,17 reset=%u\r\n", .{
@as(u32, config.reset_pin),
});
}
return sdioFunctionInit();
}
// CCCR and FBR offsets, `esp-idf/components/sdmmc/include/sd_protocol_defs.h:511-533`.
const cccr_fn_enable: u17 = 0x02;
const cccr_fn_ready: u17 = 0x03;
const cccr_int_enable: u17 = 0x04;
const cccr_bus_width: u17 = 0x07;
const cccr_blksize_l: u17 = 0x10;
const cccr_blksize_h: u17 = 0x11;
const fbr_start: u17 = 0x100;
/// `FUNC1_EN_MASK`, `port_esp_hosted_host_sdio.c:29`.
const func1_en_mask: u8 = 1 << 1;
/// `SDIO_INIT_MAX_RETRY`, `:30`.
const sdio_init_max_retry = 10;
fn sdioFunctionInit() c_int {
// Function 0 is the CCCR; every access here is CMD52 on function 0.
var ioe = cmd52(0, cccr_fn_enable) orelse return esp_fail;
cmd52w(0, cccr_fn_enable, ioe | func1_en_mask) orelse return esp_fail;
// Poll IOR until function 1 reports ready. 10 tries, 10 ms apart (:180-192).
var tries: u32 = 0;
while (tries < sdio_init_max_retry) : (tries += 1) {
const ior = cmd52(0, cccr_fn_ready) orelse return esp_fail;
if (ior & func1_en_mask != 0) break;
_ = hostedMsleep(10);
}
if (tries >= sdio_init_max_retry) {
note("MARK PORT_SDIO_FN1_NOT_READY\r\n", .{});
return esp_fail;
}
// Master interrupt enable (bit 0) plus function 1's own (:196-198).
const ie = cmd52(0, cccr_int_enable) orelse return esp_fail;
cmd52w(0, cccr_int_enable, ie | 1 | func1_en_mask) orelse return esp_fail;
const bus_width = cmd52(0, cccr_bus_width) orelse return esp_fail;
// CCCR block size for function 0, then function 1 through its FBR (:120-137, 208-214).
if (setBlockSize(0, esp_block_size) != esp_ok) return esp_fail;
if (setBlockSize(1, esp_block_size) != esp_ok) return esp_fail;
ioe = cmd52(0, cccr_fn_enable) orelse return esp_fail;
note("MARK PORT_SDIO_FN1 ioe=0x%02x ie=0x%02x bus_width=0x%02x\r\n", .{
@as(u32, ioe), @as(u32, ie | 1 | func1_en_mask), @as(u32, bus_width),
});
return esp_ok;
}
fn setBlockSize(func: u3, value: u16) c_int {
const offset: u17 = fbr_start * @as(u17, func);
const lo: u8 = @truncate(value);
const hi: u8 = @truncate(value >> 8);
cmd52w(0, offset + cccr_blksize_l, lo) orelse return esp_fail;
cmd52w(0, offset + cccr_blksize_h, hi) orelse return esp_fail;
const rb_lo = cmd52(0, offset + cccr_blksize_l) orelse return esp_fail;
const rb_hi = cmd52(0, offset + cccr_blksize_h) orelse return esp_fail;
const rb = @as(u16, rb_hi) << 8 | rb_lo;
return if (rb == value) esp_ok else esp_fail;
}
fn cmd52(func: u3, addr: u17) ?u8 {
return hal.sdmmc.cmd52Read(func, addr) catch null;
}
fn cmd52w(func: u3, addr: u17, value: u8) ?void {
hal.sdmmc.cmd52Write(func, addr, value) catch return null;
return {};
}
/// `_h_sdio_card_deinit` frees IDF's DMA bounce buffer (`port_esp_hosted_host_sdio.c:473-487`).
/// `hal.sdmmc` owns its bounce buffer statically, so there is nothing to free.
fn hostedSdioCardDeinit(ctx: ?*anyopaque) callconv(.c) c_int {
_ = busCtx(ctx) orelse return esp_fail;
return esp_ok;
}
/// `lock_required` exists because ESP-IDF's SDMMC driver is shared: `sdio_drv.c` reaches the bus
/// from four tasks, and a CMD53 that interleaves with another CMD53 is a corrupt transfer. Some
/// call sites already hold the bus lock (`SDIO_DRV_LOCK`) and pass false to avoid taking it twice;
/// the rest pass true.
///
/// **It is still required here**, and this is the one place where a cooperative scheduler does not
/// let a lock go. Cooperative means no task is preempted between two *instructions*; it does not
/// mean a task cannot yield in the middle of a transfer, and `hal.sdmmc`'s CMD53 path does exactly
/// that if it waits on the SDMMC host's interrupt. A second task entering `cmd53Read` while the
/// first is parked inside one would reprogramme the descriptor under it. The lock is what makes
/// "one transfer at a time" true, and it is cheap: `Io.Mutex.tryLock` is one compare-exchange when
/// uncontended, which is every call on the fast path.
fn sdioLock(b: *BusContext, required: bool) void {
if (required) _ = b.lock.lock(state.io, .forever);
}
fn sdioUnlock(b: *BusContext, required: bool) void {
if (required) _ = b.lock.unlock(state.io);
}
/// `_h_sdio_read_reg`: function 1, address masked, CMD52 for one byte and CMD53 byte mode with an
/// incrementing address for more (`port_esp_hosted_host_sdio.c:489-511`).
fn hostedSdioReadReg(ctx: ?*anyopaque, reg: u32, data: [*]u8, size: u16, lock_required: bool) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
const addr: u17 = @intCast(reg & esp_address_mask);
sdioLock(b, lock_required);
defer sdioUnlock(b, lock_required);
if (size <= 1) {
data[0] = hal.sdmmc.cmd52Read(sdio_func, addr) catch return esp_fail;
return esp_ok;
}
hal.sdmmc.cmd53Read(sdio_func, addr, data[0..size], true) catch return esp_fail;
return esp_ok;
}
fn hostedSdioWriteReg(ctx: ?*anyopaque, reg: u32, data: [*]u8, size: u16, lock_required: bool) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
const addr: u17 = @intCast(reg & esp_address_mask);
sdioLock(b, lock_required);
defer sdioUnlock(b, lock_required);
if (size <= 1) {
hal.sdmmc.cmd52Write(sdio_func, addr, data[0]) catch return esp_fail;
return esp_ok;
}
hal.sdmmc.cmd53Write(sdio_func, addr, data[0..size], true) catch return esp_fail;
return esp_ok;
}
/// `_h_sdio_read_block` / `_h_sdio_write_block`, `port_esp_hosted_host_sdio.c:536-576`, with the
/// splitting from `sdio_read_fromio`/`sdio_write_toio` (`:221-292`):
///
/// * the length is first rounded **up** to a multiple of four (`H_SDIO_TX_LEN_TO_TRANSFER`,
/// `port_esp_hosted_host_config.h:274-275`), because the slave's FIFO is word-wide;
/// * while 512 bytes or more remain, transfer whole 512-byte blocks;
/// * transfer the remainder in byte mode;
/// * the address advances by every chunk, and is **not** masked - block transfers address the
/// slave's data window, not its scratch registers.
///
/// Rounding up means reading or writing past `size`. That is ESP-Hosted's design, not an accident:
/// its buffers come from `_h_malloc_align(len, 64)`, so there are always at least 64 usable bytes
/// at the end - and this port's `_h_malloc_align` rounds the *allocation* up to the alignment for
/// exactly this reason. A caller that hands a tightly-sized buffer to a block transfer would have
/// the same bug under ESP-IDF.
fn hostedSdioReadBlock(ctx: ?*anyopaque, reg: u32, data: [*]u8, size: u16, lock_required: bool) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
sdioLock(b, lock_required);
defer sdioUnlock(b, lock_required);
if (size <= 1) {
// Unmasked, unlike the `_reg` entries: `hosted_sdio_read_block` has no
// `reg &= ESP_ADDRESS_MASK` (port_esp_hosted_host_sdio.c:536-555). Masking here would
// fold `ESP_SLAVE_CMD53_END_ADDR - data_left` (sdio_drv.c:756) onto a scratch register.
data[0] = hal.sdmmc.cmd52Read(sdio_func, @intCast(reg)) catch return esp_fail;
return esp_ok;
}
return blockTransfer(.read, reg, data, size);
}
fn hostedSdioWriteBlock(ctx: ?*anyopaque, reg: u32, data: [*]u8, size: u16, lock_required: bool) callconv(.c) c_int {
const b = busCtx(ctx) orelse return esp_fail;
sdioLock(b, lock_required);
defer sdioUnlock(b, lock_required);
if (size <= 1) {
// Unmasked; see `hostedSdioReadBlock`.
hal.sdmmc.cmd52Write(sdio_func, @intCast(reg), data[0]) catch return esp_fail;
return esp_ok;
}
return blockTransfer(.write, reg, data, size);
}
fn blockTransfer(comptime dir: enum { read, write }, reg: u32, data: [*]u8, size: u16) c_int {
// H_SDIO_{TX,RX}_LEN_TO_TRANSFER: (x + 3) & ~3.
const total: u32 = (@as(u32, size) + 3) & ~@as(u32, 3);
var remaining: u32 = total;
var addr: u32 = reg;
var at: u32 = 0;
while (remaining >= esp_block_size) {
// H_SDIO_{TX,RX}_BLOCKS_TO_TRANSFER: all whole blocks in one command unless the build
// forces one block at a time (port_esp_hosted_host_config.h:297-308).
const chunk = (remaining / esp_block_size) * esp_block_size;
const slice = data[at .. at + chunk];
switch (dir) {
.read => hal.sdmmc.cmd53Read(sdio_func, @intCast(addr), slice, true) catch return esp_fail,
.write => hal.sdmmc.cmd53Write(sdio_func, @intCast(addr), slice, true) catch return esp_fail,
}
remaining -= chunk;
at += chunk;
addr += chunk;
}
if (remaining > 0) {
const slice = data[at .. at + remaining];
switch (dir) {
.read => hal.sdmmc.cmd53Read(sdio_func, @intCast(addr), slice, true) catch return esp_fail,
.write => hal.sdmmc.cmd53Write(sdio_func, @intCast(addr), slice, true) catch return esp_fail,
}
}
return esp_ok;
}
/// `_h_sdio_wait_slave_intr`: block until the C6 asserts its SDIO interrupt on D1.
///
/// The arming order is IDF's, from `sd_host_sdmmc.c:396-426`: mask the card interrupt, drop the
/// previous wake's latch, look once at what is pending, and only then unmask and sleep. The look
/// is not optional - the capture is negedge-triggered, so an edge that arrived while this task was
/// awake is not going to arrive again.
///
/// ### The storm this function used to cause
///
/// Measured on the die: the first call here killed the machine. Every task starved, including one
/// that does nothing but sleep and print a heartbeat, from the instant `configureLine` set the
/// line's IE bit. On a cooperative scheduler nothing that *blocks* can do that. It was an
/// interrupt storm.
///
/// The controller drives a single line into the CLIC and asserts it whenever `RINTSTS & INTMASK`
/// (or the IDMAC's `IDSTS & IDINTEN`) is non-zero - not just for the card interrupt this function
/// waits on. Two separate causes were holding it high permanently: `INTMASK` carried
/// `Event.default`, whose card-detect bit no command path ever clears, and `initDma` had unmasked
/// the IDMAC's three completion interrupts with nothing ever clearing `IDSTS` after a transfer.
/// Either one is enough.
///
/// A level-triggered line whose source is still asserting re-enters the moment the handler
/// `mret`s. The old `sdioDispatch` tested `slaveInterruptPending()` *first* and took an early
/// return when the cause was not the card interrupt - without masking or clearing anything. So
/// the line stayed high, the core re-entered, and it never came back. `hal.intr`'s module comment
/// describes this precise failure for lines the ROM left armed (`intr.zig:512-518`); this was the
/// same bug, self-inflicted.
///
/// Three invariants fix it, none of which depends on guessing which bit was set:
///
/// * **the handler deasserts on every path**, before it reads anything at all;
/// * **only this function arms.** `hal.intr.configureLine` enables the line as its last act,
/// which is exactly what must not happen at configuration time, so the line is configured
/// with the individual setters and left disabled;
/// * **the controller is silent unless armed** - `hal.sdmmc`'s half of the fix, which reduces
/// the set of possible causes to one.
///
/// ### Level, not edge, and why the answer is not "either works"
///
/// Two different trigger behaviours meet on this path, and conflating them sends you tuning the
/// wrong knob. **Card to controller is an edge**: D1's negedge is captured once into RINTSTS,
/// which is why step 3 below reads D1's *pad* rather than the latch before sleeping.
/// **Controller to CLIC is a level**: RINTSTS is a sticky write-1-to-clear latch and MINTSTS is
/// `RINTSTS & INTMASK`, so the controller's single output stays asserted until software masks or
/// clears the bit that raised it. The CLIC trigger describes that second stage and only that one,
/// so it is `.level`.
///
/// `.edge` would be wrong three times over, and the third is the one that bites. It would need an
/// `edgeAck` this handler does not do. It would drop a re-assert that arrived while the line was
/// still high, because there is no second rising edge to capture. And it would *hide* a handler
/// that fails to deassert - the re-entry would stop, the storm would go away, and the bug would
/// still be there, waiting for the day something else holds MINTSTS non-zero. A level trigger
/// makes that failure loud and local, which is worth more than a trigger type that works by luck.
///
/// ### The precondition that was missing, and was read as a mask that would not stick
///
/// The line was configured, routed and armed - and nothing in this image had taken ownership of
/// the interrupt controller. `takeInterruptControl` is that step and its comment has the detail;
/// the short form is that `hal.intr.setHandler` files a handler in a table the core does not
/// consult until `hal.intr.init()` has written mtvec and MTVT, and that the threshold and
/// mstatus.MIE are equally this image's job and were nobody's. Neither diagnostic that reported
/// `intmask=0` could have shown anything else, because both read INTMASK after a deliberate
/// disarm; `MARK PORT_SDIO_ARM` carries the read-back that can.
///
/// `ticks_to_wait` is FreeRTOS ticks. The only caller (`sdio_drv.c:1191`) passes
/// `HOSTED_BLOCK_MAX`, so the bounded branch exists for completeness; at ESP-Hosted's recommended
/// tick rate one tick is one millisecond.
fn hostedSdioWaitSlaveIntr(ctx: ?*anyopaque, ticks_to_wait: u32) callconv(.c) c_int {
if (busCtx(ctx) == null) return esp_fail;
// One unconditional trip round the run queue, before anything else.
//
// Every other path out of this function can return without ever having slept: the pad read at
// step 3, the latch read after it, and `sdioPoll`'s fast path all answer "yes, now". That is
// correct - and it means a card holding D1 low that the C declines to drain (no NEW_PACKET
// bit, `sdio_drv.c:1247-1251`) turns `sdio_read_task`'s `for (;;)` into a loop with no
// yield in it anywhere, because the C has none of its own either. A blocking entry point that
// can return without blocking has to supply the scheduling point itself; the alternative is
// the same total starvation as the interrupt storm, reached by a different road.
state.io.sleep(.zero, os.clock) catch {};
// Configured off by default on this board: see `Config.sdio_use_interrupt`. Checked before the
// line is ever configured, so with polling selected the CLIC is not touched at all.
if (!config.sdio_use_interrupt) return sdioPoll(ticks_to_wait);
// Enough foreign handler entries, or enough calls the interrupt failed to deliver, and this
// line is not usable on this board whatever the mask says. Poll instead: slower per look, but
// bounded, proven, and faster than a 20 ms re-look that is carrying the transport on its own.
if (state.sdio_intr_foreign >= sdio_foreign_limit) return sdioPoll(ticks_to_wait);
if (state.sdio_intr_missed >= sdio_missed_limit) return sdioPoll(ticks_to_wait);
if (!sdio_line_configured) {
sdio_line_configured = true;
// First, and the step whose absence produced every LAPSE this board has reported: mtvec,
// MTVT, the threshold and mstatus.MIE.
takeInterruptControl();
hal.intr.route(hal.sdmmc.interrupt_source, config.sdio_clic_line);
// `hal.intr.configureLine` in its documented order, minus the `setEnabled(line, true)` it
// finishes with. See the storm note: enabling here is the bug.
hal.intr.setHandler(config.sdio_clic_line, sdioDispatch);
hal.intr.setTrigger(config.sdio_clic_line, .level);
hal.intr.setPriority(config.sdio_clic_line, sdio_clic_priority);
hal.intr.setVectored(config.sdio_clic_line, false);
hal.intr.setEnabled(config.sdio_clic_line, false);
// The whole delivery chain above the controller, once, before the first sleep. Each field
// is a distinct way for the line to exist and never arrive, and each has a different fix:
// `routed=99` is a matrix write that missed, `routed` unequal to `line` is two owners of
// one line, `thresh >= prio` masks it however armed it is (the comparison is inclusive),
// `mie=0` masks everything, and `mtvec` unequal to `want_mtvec` means the handler the core
// would reach is not this image's.
note("MARK PORT_SDIO_CLIC line=%u source=%u routed=%u prio=%u trig=%u thresh=%u mie=%u mtvec=0x%08x want_mtvec=0x%08x\r\n", .{
@as(u32, config.sdio_clic_line),
@as(u32, @intFromEnum(hal.sdmmc.interrupt_source)),
@as(u32, hal.intr.routedLine(hal.sdmmc.interrupt_source) orelse 99),
@as(u32, hal.intr.getPriority(config.sdio_clic_line)),
@as(u32, @intFromEnum(hal.intr.getTrigger(config.sdio_clic_line))),
@as(u32, hal.intr.getThreshold()),
@as(u32, @intFromBool(hal.intr.globalEnabled())),
hal.intr.readMtvec(),
hal.intr.trapEntryAddress() | hal.intr.mtvec_mode_clic,
});
}
// A bounded wait that loops, rather than the unbounded one the caller asked for.
//
// The lost-edge case that used to need this is now handled properly at step 3 of the arming
// sequence below, so this is no longer the mechanism - it is the net under it. It stays
// because an unbounded futex wait is precisely the shape of failure that cost an afternoon:
// silent, indistinguishable from a card that never called, and impossible to report on. A
// 20 ms re-look turns "the radio is dead" into `MARK PORT_SDIO_LAPSE` with the registers
// attached, and costs that latency only on beats where the interrupt did not arrive.
//
// `sdio_drv.c:1188` is right that a finite wait is unusable *for the caller*, so the loop, not
// the wait, is what honours `HOSTED_BLOCK_MAX`: this function still only returns when there is
// something to report. The property gained is that no path through it can be silent for ever.
const bounded = ticks_to_wait != std.math.maxInt(u32);
const deadline = os.nowMs(state.io) + ticks_to_wait;
while (true) {
// Read before arming, so an interrupt taken between here and the futex wait cannot be
// lost: `futexWaitTimeout` returns immediately on a value that no longer matches.
const seen = state.sdio_intr_epoch.load(.acquire);
// Steps 1-4 of `sd_host_sdmmc.c:404-426`, in that order, as written out on
// `hal.sdmmc.setSlaveInterruptEnabled`. Getting the order wrong loses wakeups; getting
// step 3 wrong loses them permanently.
hal.sdmmc.setSlaveInterruptEnabled(false);
hal.sdmmc.clearSlaveInterrupt();
// Step 3, and the one that cannot be done with the controller's registers alone. RINTSTS
// is a latch: it says "a negedge was captured", and step 2 has just thrown that away. D1's
// pad is a level: it says "the card is holding the line low *now*". A C6 that is still
// waiting to be drained is exactly the second without the first, and sleeping on it waits
// for an edge that has already happened. The latch is tested too, for the window between
// the clear above and this read.
//
// This is not a window that lapsed - nothing has been armed and nothing has slept - so it
// leaves `sdio_intr_missed` alone.
if (hal.sdmmc.slaveInterruptAsserted() or hal.sdmmc.slaveInterruptPending()) return esp_ok;
// Source first, CLIC last: the line must not be deliverable while the only cause it is
// allowed to have is still masked. The unmask reads INTMASK back inside its own masked
// region, which is the only reading of that register that can answer "did it stick".
const armed = hal.sdmmc.armSlaveInterrupt();
state.sdio_armed_intmask = armed.intmask;
state.sdio_armed_mintsts = armed.mintsts;
hal.intr.setEnabled(config.sdio_clic_line, true);
// `stuck=1` retires the "the unmask does not stick" hypothesis; `stuck=0` confirms it, with
// the word that was wanted printed beside the word the register returned. Budgeted,
// because it is a property of the configuration rather than of the beat.
sdioMark(&state.sdio_arm_marks, "MARK PORT_SDIO_ARM stuck=%u want=0x%08x intmask=0x%08x mintsts=0x%08x rintsts=0x%08x ie=%u\r\n", .{
@as(u32, @intFromBool(armed.stuck())),
armed.want,
armed.intmask,
armed.mintsts,
armed.rintsts,
@as(u32, @intFromBool(hal.intr.isEnabled(config.sdio_clic_line))),
});
// Timeout and cancelation are indistinguishable here and neither is a result; the epoch is
// the only thing that says whether the handler ran.
state.io.futexWaitTimeout(u32, &state.sdio_intr_epoch.raw, seen, .{
.duration = .{ .clock = os.clock, .raw = .fromMilliseconds(sdio_relook_ms) },
}) catch {};
// The CLIC's own pending bit, read *before* the disarm, because it is the discriminator a
// lapse otherwise has no way to report: `pend=1` with no handler entry means the CLIC
// latched this line and the core never took it, so the fault is mtvec, the threshold or
// MIE rather than the controller or the C6.
const clic_pending = hal.intr.isPending(config.sdio_clic_line);
// Idempotent: on a real wake the handler already did both. On a lapse it did not, and an
// armed line with nobody waiting is how a storm gets its second chance.
disarmSdioLine();
if (state.sdio_intr_epoch.load(.acquire) != seen) {
// The handler ran. It deliberately does not clear the latched SDIO bit - clearing it
// while D1 is still low would drop the next wakeup - so the bit still being set is
// what distinguishes "the C6 called" from "something else held the controller's line
// high and the handler is who noticed".
if (hal.sdmmc.slaveInterruptPending()) {
state.sdio_intr_foreign = 0;
state.sdio_intr_missed = 0;
// **The line that says the interrupt works.** Until now a successful delivery was
// the only outcome that printed nothing at all, so a console showing idle lapses
// and no wakes was indistinguishable from a console showing a dead CLIC - which is
// exactly the ambiguity that made the last flash inconclusive. `mintsts` is the
// word the controller's output follows, captured at handler entry before the
// disarm zeroed it; this slot's bit set in it is delivery proven end to end.
sdioMark(&state.sdio_wake_marks, "MARK PORT_SDIO_WAKE n=%u mintsts=0x%08x rintsts=0x%08x idsts=0x%08x\r\n", .{
state.sdio_intr_epoch.load(.acquire),
state.sdio_intr_mintsts.load(.acquire),
state.sdio_intr_rintsts.load(.acquire),
state.sdio_intr_idsts.load(.acquire),
});
return esp_ok;
}
state.sdio_intr_foreign += 1;
sdioMark(&state.sdio_foreign_marks, "MARK PORT_SDIO_FOREIGN n=%u mintsts=0x%08x rintsts=0x%08x idsts=0x%08x armed_intmask=0x%08x\r\n", .{
state.sdio_intr_foreign,
state.sdio_intr_mintsts.load(.acquire),
state.sdio_intr_rintsts.load(.acquire),
state.sdio_intr_idsts.load(.acquire),
state.sdio_armed_intmask,
});
if (state.sdio_intr_foreign >= sdio_foreign_limit) {
note("MARK PORT_SDIO_POLLING abandoning CLIC line %u\r\n", .{
@as(u32, config.sdio_clic_line),
});
return sdioPoll(ticks_to_wait);
}
} else {
// Nobody entered the handler. Ask both ends directly before calling it a lapse - the
// pad for a card asserting now, the latch for an edge captured while the CLIC was
// being taken down.
//
// This is the one reading that separates the two things a lapse can mean, and it is
// why `sdio_intr_lapses` alone is not a fault signal. **The card is calling and the
// interrupt did not deliver it**: a real frame has just paid up to `sdio_relook_ms` of
// latency, the re-look is doing the interrupt's job, and four of those in a row is a
// configuration that will not fix itself - so the line goes back to the poll, which is
// twenty times quicker at exactly this.
if (hal.sdmmc.slaveInterruptAsserted() or hal.sdmmc.slaveInterruptPending()) {
state.sdio_intr_missed += 1;
sdioMark(&state.sdio_missed_marks, "MARK PORT_SDIO_MISSED n=%u pend=%u armed_intmask=0x%08x armed_mintsts=0x%08x rintsts=0x%08x\r\n", .{
state.sdio_intr_missed,
@as(u32, @intFromBool(clic_pending)),
state.sdio_armed_intmask,
state.sdio_armed_mintsts,
hal.sdmmc.interruptStatusRaw(),
});
if (state.sdio_intr_missed >= sdio_missed_limit) {
note("MARK PORT_SDIO_POLLING abandoning CLIC line %u after %u undelivered calls\r\n", .{
@as(u32, config.sdio_clic_line),
state.sdio_intr_missed,
});
return sdioPoll(ticks_to_wait);
}
return esp_ok;
}
// The other meaning: the C6 had nothing to say. Free, and the resting state of an idle
// link - which is the whole point of waiting on an interrupt instead of polling.
state.sdio_intr_lapses +%= 1;
// `armed_*` is what the mask was during the window that lapsed; `now_*` is the
// disarmed state. Both are printed so the two can no longer be mistaken for each
// other: `now_intmask=0` is expected here, and always was.
sdioMark(&state.sdio_lapse_marks, "MARK PORT_SDIO_LAPSE n=%u armed_intmask=0x%08x armed_mintsts=0x%08x pend=%u now_rintsts=0x%08x now_intmask=0x%08x\r\n", .{
state.sdio_intr_lapses,
state.sdio_armed_intmask,
state.sdio_armed_mintsts,
@as(u32, @intFromBool(clic_pending)),
hal.sdmmc.interruptStatusRaw(),
hal.sdmmc.interruptMaskRaw(),
});
}
// The one diagnostic with no budget, because the ratio it reports is the whole question and
// it stays interesting after every other line has gone quiet. `wakes` is handler entries:
// rising with `lapses` means the interrupt is carrying the transport and the re-look is
// only covering the idle gaps, flat at zero means the CLIC is not delivering and the
// re-look is doing all of it.
state.sdio_windows +%= 1;
if (state.sdio_windows % sdio_tally_every == 0) {
note("MARK PORT_SDIO_TALLY windows=%u wakes=%u lapses=%u missed=%u foreign=%u\r\n", .{
state.sdio_windows,
state.sdio_intr_epoch.load(.acquire),
state.sdio_intr_lapses,
state.sdio_intr_missed,
state.sdio_intr_foreign,
});
}
if (bounded and os.nowMs(state.io) >= deadline) return esp_fail;
}
}
/// Consecutive foreign handler entries after which the interrupt is abandoned for polling. Four,
/// because one can be a race and four in a row is a configuration that will not fix itself.
const sdio_foreign_limit: u32 = 4;
/// Consecutive undelivered calls - lapsed windows whose re-look found the card already asserting -
/// after which the interrupt is abandoned for polling.
///
/// Four, for the same reason as `sdio_foreign_limit`: one can be a race against the CLIC being
/// taken down, four in a row is a configuration. At `sdio_relook_ms` each that is 80 ms of
/// degraded latency before the line is given up, well inside one of the transport's own 200 ms
/// retry turns (`transport_drv.c:233`).
///
/// This bound is what makes flipping `sdio_use_interrupt` to `true` an experiment rather than a
/// bet. Without it, a board where delivery is still broken would give every received frame 20 ms
/// instead of the poll's 1 ms, for ever, with eight budgeted MARK lines to say so. Note that it
/// counts *undelivered calls* and not lapses: an idle card lapses every window by construction,
/// and penalising that would trade the interrupt away 160 ms after boot on a link that was
/// working perfectly.
const sdio_missed_limit: u32 = 4;
/// Priority for the SDIO CLIC line. `hal.intr.init` leaves the threshold at 0 and the comparison
/// is inclusive, so 1 is the lowest value that can ever be taken. Nothing higher would win against
/// anything: `port.zig` is the only owner of a CLIC line in this image.
const sdio_clic_priority: u3 = 1;
/// Lines each distinct diagnostic may print. A wait that gives up has to be able to say why; it
/// does not have to say so ten thousand times.
const sdio_mark_budget: u32 = 8;
/// Arming windows between `MARK PORT_SDIO_TALLY` lines. 64 windows is at most 1.3 s of idle link
/// at `sdio_relook_ms`, and far less when frames are flowing, so the ratio is visible within a
/// couple of seconds of boot and costs one `ets_printf` per 64 windows.
const sdio_tally_every: u32 = 64;
/// Cadence of the polling fallback. Both the pad and the latch are single register reads, so this
/// is a latency budget rather than a cost.
const sdio_poll_ms: u32 = 1;
/// How long one arming window sleeps before looking at the pad and the latch itself.
///
/// The number is a latency budget, not a timeout: an interrupt that arrives is delivered at once,
/// and this only bounds how long a *lost* negedge can go unnoticed. 20 ms is two orders of
/// magnitude below anything the transport's own retries care about (`transport_drv.c:233` sleeps
/// 200 ms per turn) and two orders above the cost of the register reads it gates.
const sdio_relook_ms: u32 = 20;
fn sdioMark(budget: *u32, comptime fmt: [*:0]const u8, args: anytype) void {
if (budget.* >= sdio_mark_budget) return;
budget.* += 1;
note(fmt, args);
}
/// The interrupt-free path. `sdio_drv.c:1188` insists a finite wait is unusable here, so an
/// unbounded `ticks_to_wait` blocks until the card really does call - it just yields between
/// checks instead of sleeping on a futex.
///
/// The clear before returning is load-bearing. `sdio_clear_intr` writes the *slave's*
/// `ESP_SLAVE_INT_CLR_REG` (`sdio_drv.c:423-427`); nothing in the C touches this controller's
/// RINTSTS, so a latched bit left set here makes the next call return immediately, and
/// `sdio_read_task`'s loop contains no other yield. That is the same total starvation the
/// interrupt storm caused, reached the slow way - and it is why the interrupt path clears at the
/// top of every arm rather than on the way out.
fn sdioPoll(ticks_to_wait: u32) c_int {
_ = ticks_to_wait;
// Fast path: if either controller-side signal says the card is calling, say so at once. Both
// are real when they do fire, and they cost two register reads.
if (hal.sdmmc.slaveInterruptAsserted() or hal.sdmmc.slaveInterruptPending()) {
hal.sdmmc.clearSlaveInterrupt();
return esp_ok;
}
// Otherwise sleep briefly and report "look again" - deliberately, and this is the whole point of
// this function.
//
// Neither controller-side signal is a trustworthy answer to "does the slave have a packet":
//
// - `slaveInterruptPending` reads RINTSTS bit 16+slot, which LATCHES an edge. The card asserts
// once per packet; clear that latch while the card still has data queued and the edge is
// gone, with nothing to re-create it until the *next* packet arrives.
// - `slaveInterruptAsserted` reads D1's pad level, and D1 is a DATA line. The SDMMC controller
// owns that pad throughout every CMD53, and the SDIO interrupt is only meaningful in defined
// windows between blocks. ESP-IDF never reads it for this: `sdmmc_host_io_int_wait` consults
// the controller's own status word instead.
//
// Measured consequence of trusting them: the receive counter reached somewhere between 6 and 18
// frames and then froze for ever, while transmits kept working. The board took a real DHCP lease
// - the host speaks first there - and then answered no ARP and no ping.
//
// The authority on "is there a packet" is the slave's own ESP_SLAVE_INT_RAW_REG, and
// `sdio_read_task` already reads it on every pass and tests BIT(SDIO_INT_NEW_PACKET) itself
// (sdio_drv.c:1204, :1247). examples/sdiocheck.zig proved that register answers reliably over
// CMD53. So when the cheap signals say nothing, the right move is not to guess - it is to yield
// and let the caller ask the slave. `HOSTED_BLOCK_MAX` is honoured in the sense that matters:
// this returns only when the caller has something to do, and "read your registers again" always
// is.
//
// The cost is one register read per `sdio_poll_ms` while the link is idle. The benefit is that a
// lost edge can no longer strand a packet.
state.io.sleep(.fromMilliseconds(sdio_poll_ms), os.clock) catch return esp_fail;
return esp_ok;
}
/// Take ownership of the interrupt controller, once, before any line this file configures can be
/// delivered.
///
/// **This is the step whose absence made the interrupt path look like an INTMASK write that would
/// not stick.** `hal.intr.init()` is not decoration; it is what makes an interrupt reach *this
/// image* at all, and nothing in the `-Dapp=examples/http.zig` build had ever called it.
/// `examples/intrcheck.zig` and `examples/portcheck.zig` do; `examples/http.zig`,
/// `examples/radio.zig` and everything under `src/` did not. So when
/// `hal.intr.setEnabled(sdio_clic_line, true)` ran on this board, four separate preconditions were
/// missing:
///
/// * **mtvec still belonged to the bootloader.** `hal.intr.init` fills the vector table, writes
/// MTVT and writes `mtvec = trapEntry | 3` (`intr.zig:558-577`). Without it, the CLIC vectors
/// wherever the ROM left mtvec pointing, `hal.intr.setHandler` files `sdioDispatch` in a table
/// the core never consults, and the core leaves this image and does not come back. That is the
/// reported "whole board going silent right after Open data path at slave": not a storm, an
/// exit.
/// * **whatever the ROM armed was still armed** (`intr.zig:512-518`), so the first MIE could
/// also deliver somebody else's level-triggered source into the same nowhere.
/// * **the threshold was never opened.** The comparison is inclusive and this line runs at
/// priority 1, so a threshold the ROM left at 1 or above masks it for ever - which is a LAPSE
/// every window with no handler entry and nothing else wrong anywhere.
/// * **mstatus.MIE.** `hal.intr.init` deliberately leaves it clear and says that turning it on
/// is the caller's decision (`intr.zig:531`). Nothing in this build was that caller.
///
/// Enabling MIE here is safe *because* `init()` ran first: it has just detached all 128 sources
/// and cleared all 48 enables, so the only lines that can be delivered afterwards are the ones
/// this file enables itself.
///
/// Idempotent, and the test is the fact that matters rather than a flag of our own - if mtvec
/// already points at this image's trap entry then somebody has already done this, and re-running
/// `init()` would destroy `hal.intr.boot_state`, the only record of what the bootloader handed
/// over. Both call sites (here and `hostedConfigGpioAsInterrupt`) run it before they touch a line,
/// so whichever is first does the work and the other finds it done - which matters, because
/// `init()` detaches every source and would otherwise silence a line the other had just armed.
fn takeInterruptControl() void {
if (hal.intr.readMtvec() != (hal.intr.trapEntryAddress() | hal.intr.mtvec_mode_clic)) {
hal.intr.init();
// A fault is the one failure on this path that cannot report itself: `hal.intr` parks the
// core with the numbers recorded and no way to print them. Only installed if the
// application has not claimed the hook.
if (hal.intr.on_fault == null) hal.intr.on_fault = reportFault;
note("MARK PORT_INTR_OWN mtvec=0x%08x want=0x%08x mtvt=0x%08x thresh=%u boot_mie=%u rom_lines=0x%08x rom_sources=%u\r\n", .{
hal.intr.readMtvec(),
hal.intr.trapEntryAddress() | hal.intr.mtvec_mode_clic,
hal.intr.readMtvt(),
@as(u32, hal.intr.getThreshold()),
@as(u32, @intFromBool(hal.intr.boot_state.mie)),
hal.intr.boot_state.enabled_lines,
hal.intr.boot_state.routed_sources,
});
}
if (!hal.intr.globalEnabled()) hal.intr.globalEnable();
}
/// Last words. `hal.intr.intrFault` has already recorded the fault and will park the core after
/// this returns, so this is the only chance the numbers get to leave the board.
fn reportFault(f: hal.intr.Fault) void {
note("MARK PORT_INTR_FAULT mcause=0x%08x mepc=0x%08x mtval=0x%08x taken=%u last_id=%u spurious=%u\r\n", .{
f.mcause,
f.mepc,
f.mtval,
hal.intr.taken,
hal.intr.last_clic_id,
hal.intr.spurious,
});
}
/// Deassert and disable, in that order. The guarantee the handler needs: after this the line
/// cannot be taken again until somebody arms it.
fn disarmSdioLine() void {
hal.sdmmc.setSlaveInterruptEnabled(false);
hal.intr.setEnabled(config.sdio_clic_line, false);
}
var sdio_line_configured: bool = false;
/// The CLIC handler. Runs with `mstatus.MIE` clear on the interrupted stack
/// (`hal.intr.Handler`), so what follows cannot itself be interrupted - and after the first
/// statement it cannot be re-entered either.
fn sdioDispatch(line: u5) void {
_ = line;
// One load, before the disarm, and it is safe for a reason worth stating rather than assuming.
//
// The invariant is "no path returns from this handler with the line still asserted", because a
// level line re-enters the instant the handler `mret`s and that hangs the core. What breaks the
// invariant is a *branch* - any test that can return early. A read cannot return, so a load
// placed here costs the invariant nothing.
//
// It has to be here, though: MINTSTS is `RINTSTS & INTMASK`, so the disarm below zeroes it and
// reading it afterwards would report 0 on every entry - the same mistake the old INTMASK read
// made one line lower. This is the register the controller's output actually follows, so its
// value at the moment of delivery is the direct answer to "did the card interrupt reach the
// CLIC, or did something else".
const mintsts_at_entry = hal.sdmmc.interruptStatusMasked();
// Unconditional, and first among the *stores*. A level-triggered line does not deassert because
// the handler returned; masking the source and dropping the CLIC's enable are the only two
// things that stop it, and this handler does not know which status bit is holding the line up.
// Every test placed before this point is a chance to return with the line still asserted, which
// is not a missed interrupt - it is a hang of the whole core.
disarmSdioLine();
// The rest of what the line looked like at entry, and the reason
// `hal.sdmmc.interruptStatusRaw` and `hal.sdmmc.dmaStatusRaw` exist. Both of these registers
// are sticky, so reading them after the disarm loses nothing.
//
// INTMASK is deliberately *not* read here. It is not sticky, the disarm has just rewritten it,
// and a `MARK PORT_SDIO_FOREIGN` carrying that value only ever said that the disarm worked. The
// mask that was actually in force is `state.sdio_armed_intmask`, read back by the arm inside its
// own masked region.
state.sdio_intr_mintsts.store(mintsts_at_entry, .release);
state.sdio_intr_rintsts.store(hal.sdmmc.interruptStatusRaw(), .release);
state.sdio_intr_idsts.store(hal.sdmmc.dmaStatusRaw(), .release);
// Wake unconditionally too. The waiter can tell a real card interrupt from a foreign one, and
// a waiter that is told is a waiter that can report; returning silently is how the old handler
// turned a misconfigured mask into a wait that never ended.
_ = state.sdio_intr_epoch.fetchAdd(1, .release);
state.io.futexWake(u32, &state.sdio_intr_epoch.raw, 1);
}
// ============================================================================ 7. events
fn hostedEventWifiPost(event_id: i32, event_data: ?*anyopaque, event_data_size: usize, ticks_to_wait: u32) callconv(.c) c_int {
_ = ticks_to_wait;
deliver(.{
.base = .wifi,
.id = event_id,
.data = sliceOf(event_data, event_data_size),
});
return esp_ok;
}
fn hostedEventPost(event_base: EventBase, event_id: i32, event_data: ?*anyopaque, event_data_size: usize, ticks_to_wait: u32) callconv(.c) c_int {
_ = ticks_to_wait;
deliver(.{
.base = .{ .named = event_base },
.id = event_id,
.data = sliceOf(event_data, event_data_size),
});
return esp_ok;
}
fn sliceOf(p: ?*anyopaque, len: usize) ?[]const u8 {
const q = p orelse return null;
if (len == 0) return null;
const b: [*]const u8 = @ptrCast(q);
return b[0..len];
}
/// `ticks_to_wait` is dropped, and that is a real difference. `esp_event_post` copies the payload
/// into a queue and can block when that queue is full, which is what the argument is for. This
/// calls the application straight through, on the posting task, so there is no queue to fill and
/// nothing to wait for - but it also means a slow handler stalls the transport task that posted the
/// event. The application is expected to copy what it needs and return.
fn deliver(e: Event) void {
const h = state.on_event orelse {
// Silent by default would hide association and disconnection reasons, which is exactly
// what a bring-up needs to see.
switch (e.base) {
.wifi => note("MARK PORT_EVENT wifi id=%d len=%u (no handler)\r\n", .{ e.id, @as(u32, @intCast(if (e.data) |d| d.len else 0)) }),
.named => |n| note("MARK PORT_EVENT %s id=%d len=%u (no handler)\r\n", .{ n, e.id, @as(u32, @intCast(if (e.data) |d| d.len else 0)) }),
}
return;
};
h(e);
}
// ============================================================================ misc real entries
/// `hosted_init_hook` warns if `CONFIG_FREERTOS_HZ` is below ESP-Hosted's recommendation
/// (`port_esp_hosted_host_os.c:150-158`). There is no tick here at all - `std.Io`'s timebase is
/// `hal.systimer`'s 16 MHz counter and sleeps are absolute deadlines, not tick counts - so the
/// jitter that warning is about does not exist. Announce the port instead, which is the one line
/// that proves this table is the one being called.
fn hostedInitHook() callconv(.c) void {
note("MARK PORT_HOOK zig port installed=%u timers=%u\r\n", .{
@as(u32, @intFromBool(state.installed)),
@as(u32, config.timer_slots),
});
}
/// `_h_restart_host` reboots the host when the slave has stopped answering
/// (`transport_drv.c:70`, `sdio_drv.c:578`, and the init-timeout callback).
///
/// ESP-IDF calls `esp_restart`. There is no `esp_restart` here and, more to the point, a bring-up
/// that silently reboots is a bring-up you cannot debug: the interesting state is the state at the
/// moment the slave went quiet. So this reports and parks, with interrupts left on so the console
/// still works and a debugger can still attach.
fn hostedRestartHost() callconv(.c) c_int {
const s = stats();
note("MARK PORT_RESTART_HOST requested; parking. heap live=%u reserved=%u peak=%u blocks=%u fail=%u stubs=%u\r\n", .{
@as(u32, @intCast(s.bytes_live)),
@as(u32, @intCast(s.bytes_reserved)),
@as(u32, @intCast(s.peak_reserved)),
@as(u32, @intCast(s.blocks_live)),
@as(u32, @intCast(s.alloc_failures)),
s.stub_calls,
});
while (true) {}
}
/// `_h_get_host_wakeup_or_reboot_reason`. `HOSTED_WAKEUP_NORMAL_REBOOT` is what ESP-IDF returns
/// when power-save is not compiled in (`port_esp_hosted_host_os.c:932-934`), and it is the truth
/// here: this image has no sleep support, so every boot is a normal one.
fn hostedGetWakeupReason() callconv(.c) c_int {
return 0; // HOSTED_WAKEUP_NORMAL_REBOOT
}
// ============================================================================ 8. loud stubs
/// Every stub prints its own name and returns a failure code. The two properties that matter: a
/// path nobody implemented is *visible* on the console rather than a hang, and the pointer is never
/// null, so a call through it cannot be a jump to address zero.
fn stub(comptime name: []const u8) void {
state.stub_calls += 1;
note("MARK PORT_STUB " ++ name ++ "\r\n", .{});
}
/// SPI only. ESP-IDF assigns this just once, under `H_TRANSPORT_IN_USE == H_TRANSPORT_SPI`
/// (`port_esp_hosted_host_os.c:991`), leaving it **null** for SDIO - so under IDF, reaching this on
/// an SDIO build is a jump to zero. Here it is a message.
fn stubDoBusTransfer(_: ?*anyopaque) callconv(.c) c_int {
stub("_h_do_bus_transfer (SPI transport)");
return esp_fail;
}
/// `_h_printf` routes ESP-Hosted's logging through the port table. Nothing in the tree calls it -
/// every `ESP_LOG*` goes to `esp_log_writev` directly, which is the parent's symbol - so this is
/// unreachable in practice, and implementing it would mean either a printf formatter in Zig or a
/// `va_list` handed across an ABI boundary that has not been validated on rv32. The tag and the
/// unexpanded format string are printed, which is enough to identify the call site if it ever
/// happens.
fn stubPrintf(level: c_int, tag: [*:0]const u8, format: [*:0]const u8, ...) callconv(.c) void {
state.stub_calls += 1;
note("MARK PORT_STUB _h_printf level=%d tag=%s fmt=%s (varargs not expanded)\r\n", .{ level, tag, format });
}
fn stubSpiHdReadReg(_: u32, _: *u32, _: c_int, _: bool) callconv(.c) c_int {
stub("_h_spi_hd_read_reg");
return esp_fail;
}
fn stubSpiHdWriteReg(_: u32, _: *u32, _: bool) callconv(.c) c_int {
stub("_h_spi_hd_write_reg");
return esp_fail;
}
fn stubSpiHdReadDma(_: [*]u8, _: u16, _: bool) callconv(.c) c_int {
stub("_h_spi_hd_read_dma");
return esp_fail;
}
fn stubSpiHdWriteDma(_: [*]u8, _: u16, _: bool) callconv(.c) c_int {
stub("_h_spi_hd_write_dma");
return esp_fail;
}
fn stubSpiHdSetDataLines(_: u32) callconv(.c) c_int {
stub("_h_spi_hd_set_data_lines");
return esp_fail;
}
fn stubSpiHdSendCmd9() callconv(.c) c_int {
stub("_h_spi_hd_send_cmd9");
return esp_fail;
}
fn stubUartRead(_: ?*anyopaque, _: [*]u8, _: u16) callconv(.c) c_int {
stub("_h_uart_read");
return esp_fail;
}
fn stubUartWrite(_: ?*anyopaque, _: [*]u8, _: u16) callconv(.c) c_int {
stub("_h_uart_write");
return esp_fail;
}
fn stubUartFlushInput(_: ?*anyopaque) callconv(.c) c_int {
stub("_h_uart_flush_input");
return esp_fail;
}
/// Power save needs `esp_sleep`, a wakeup GPIO in the LP domain, and a hold latch this HAL does not
/// model. ESP-IDF's own version returns -1 unless `H_HOST_PS_ALLOWED`
/// (`port_esp_hosted_host_os.c:876-891`), so -1 is also the configured-off answer.
fn stubConfigHostPowerSave(_: u32, _: ?*anyopaque, _: u32, _: c_int) callconv(.c) c_int {
stub("_h_config_host_power_save_hal_impl");
return -1;
}
fn stubStartHostPowerSave(_: u32) callconv(.c) c_int {
stub("_h_start_host_power_save_hal_impl");
return -1;
}
// ============================================================================ compile-time census
/// A compile-time list of which entries are real and which are loud stubs, so the census in the
/// module header cannot drift from the table. `port.stubbed` is what a self-test prints.
pub const stubbed = [_][]const u8{
"_h_do_bus_transfer",
"_h_printf",
"_h_hold_gpio",
"_h_spi_hd_read_reg",
"_h_spi_hd_write_reg",
"_h_spi_hd_read_dma",
"_h_spi_hd_write_dma",
"_h_spi_hd_set_data_lines",
"_h_spi_hd_send_cmd9",
"_h_uart_read",
"_h_uart_write",
"_h_uart_flush_input",
"_h_config_host_power_save_hal_impl",
"_h_start_host_power_save_hal_impl",
};
comptime {
// 71 entries, 14 stubbed, 57 real.
assert(stubbed.len == 14);
assert(std.meta.fields(HostedOsiFuncs).len - stubbed.len == 57);
}
|