Ethernet MAC+DMA From Scratch
Seven Bugs Deep, No HAL, Down to a Real Packet Arriving on a PC
— Full Timeline —
From "does the PHY answer" to "50/50 packets received"
0x7 — a real, sane value. Cable in: "LINK UP — 100Mbps Full-Duplex." Cable out: "LINK DOWN."static arrays. This project's linker script places those in DTCM — which its own memory-map comment already flagged as "no DMA!" A bus-master peripheral simply cannot reach that memory.memcpy() generates.tcpdump, never showed up. The MAC's own address register had never been written — unicast filtering was silently dropping every packet addressed to it.— Part 1: Link Detection —
MDIO, RMII, and a real PHY ID — the part that just worked
Before any of the DMA saga, the first milestone was simpler: prove the MAC can talk to its PHY at all, independent of moving any actual frame data. Nine RMII pins (all AF11, cross-referenced against UM2407's board-specific solder bridge table), the SYSCFG interface-select field, and a two-register MDIO interface — all confirmed from the reference manual before writing code, the same discipline this project has used for every peripheral so far.
TamgaOS STM32H753ZI — Ethernet PHY Link Monitor [ETH] PHY ID (reg 2) = 0x7 (expect a real value, not 0x0000 or 0xFFFF) [ETH] LINK DOWN [ETH] LINK UP — 100Mbps Full-Duplex
The MDC clock divider needed tracing through the actual AHB1 frequency (240MHz, confirmed via RCC_D1CFGR.HPRE) rather than assumed from the 480MHz core clock — the same lesson this project's UART baud rate taught earlier, reappearing in a different peripheral. Everything else here worked on the first attempt. The real difficulty was still ahead.
— Part 2: Seven Bugs Deep —
ı used ether type: 0x88B5 because this value reserved by IEEE for "local/experimental use" The IEEE 802 standard specifically designates 0x88B5 and 0x88B6 as "Local Experimental EtherTypes" sooo ı usd this value n code and fillter because of that on wireshark....
Bug 1 — Fatal Bus Error, and a linker comment that already had the answer
The TX/RX descriptor arrays and their buffers were declared as ordinary static variables. This project's linker script places plain statics in DTCM (0x20000000) — and the linker script's own memory-map comment, written months earlier during a different session, already said: DTCM: 0x20000000 128KB — zero wait-state, CPU only (no DMA!). The Ethernet DMA engine is a bus-master peripheral; it cannot reach DTCM at all. Every descriptor and buffer address handed to it was fundamentally unreachable.
.axi_bss linker section — already defined, mapped to AXI SRAM (0x24000000), reachable by D1-domain DMA masters — via __attribute__((section(".axi_bss"))).Bug 2 — TX dies after exactly N sends, where N is the descriptor count
This exact-count pattern, reproduced identically at two different ring sizes, ruled out anything random and pointed at descriptor reuse specifically — the first use of any given descriptor always succeeded, the second attempt to reuse it never did. RM0433 states the tail pointer register "points to the location of the LAST VALID descriptor" — an early attempt had it pointing one descriptor past the ring instead, which was wrong, but fixing that alone didn't resolve the symptom, because the actual root cause was one layer deeper (Bug 3).
Bug 3 — the fault that revealed the real problem: D-Cache
Suspecting cache coherency (this project's startup code enables D-Cache globally, per PM0253 §4.8's mandatory invalidate-then-enable sequence), an MPU region was added over the AXI SRAM buffers to mark them non-cacheable. The very first access after enabling it produced an immediate UNALIGNED fault — even though the buffers were correctly 4-byte aligned.
The region had been configured as Strongly Ordered memory (TEX=000, C=0, B=0) — genuinely non-cacheable, but also far more restrictive than intended: Strongly Ordered memory forbids the multi-register load/store instructions a compiler generates for an ordinary memcpy(), regardless of alignment. The fix wasn't "make it aligned," it was "use a different, less restrictive non-cacheable memory type."
This is the bug that actually explains Bug 2. Once D-Cache could no longer hide the DMA's writes from the CPU, the tail pointer semantics from Bug 2's fix started working correctly — the two issues had been compounding each other the whole time.
Bug 4 — a linker script detail the MPU quietly requires
ARMv7-M's MPU requires a region's base address to be aligned to the region's own size — an 8KB region must start at an address that's a multiple of 8192. The .axi_bss section's linker placement only specified ALIGN(4), inherited from before any MPU region existed over it.
. = ALIGN(8192); immediately before _axi_bss_start in linker.ld — confirmed afterward via the .map file that the section genuinely starts at 0x24000000, a clean multiple of 8192.Bug 5 — RX stayed completely silent, not even an error
With TX fully working, RX produced nothing — no error flags, no activity, just a permanently unclear OWN bit on every descriptor. ETH_DMACRXCR's Receive Buffer Size field (RBSZ, bits 14:1) had been left at its reset value: zero. The DMA was being told every receive buffer was zero bytes long — there was nowhere for it to write anything.
Bug 6 — a queue-level setting one layer above the DMA
The MTL (MAC Transaction Layer) sits between the MAC and the DMA, with its own receive queue and its own operating mode register — never touched so far. ETH_MTLRXQOMR's Store-and-Forward bit (RSF) was still at its reset default, leaving the queue in threshold/cut-through mode, which didn't reliably forward the small test frames being used.
Bug 7 — real traffic worked, our own test frames didn't
With RBSZ and RSF both fixed, real network traffic started arriving — DHCP discovers, mDNS queries, router solicitations, all consistently received. But test frames sent specifically to the board's chosen MAC address (02:00:00:00:00:01), confirmed leaving the sending PC via tcpdump on the transmitting interface, never showed up on the board.
The board's own MAC address had never been written into ETH_MACA0HR/ETH_MACA0LR — the registers the hardware uses for unicast destination filtering. Broadcast frames bypass this filter by design (which is exactly why "other traffic" had been working all along); unicast frames were being silently dropped because the MAC didn't know its own address to compare against.
— Verification —
Seeing our own packet — TX proof
Once Bugs 1–4 were resolved, a standalone TX-only test binary sent a broadcast frame with a custom EtherType (0x88B5) and an 8-byte "TamgaOS!" payload every second, connected directly to a PC's Ethernet port.
Data: 54616d67614f53210100000000000000... T a m g a O S !
The exact bytes sent by the board, arriving intact on a real PC over a real cable — the first end-to-end proof that the DMA/MPU fixes actually worked, not just that the driver stopped reporting errors.
50/50 — real frames received, zero loss
With Bugs 5–7 resolved, a small Python script (scapy) sent 50 frames from a PC directly to the board's MAC address. Every single one arrived.
[ETH] *** OUR TEST FRAME *** #48 (rx total #51) — 64 bytes:
02 00 00 00 00 01 02 00 00 00 00 02 88 B5 54 61 6D 67 61 4F 53 21 ...
[ETH] payload ascii: TamgaOS!..........
[ETH] *** OUR TEST FRAME *** #49 (rx total #52) — 64 bytes: ...
[ETH] *** OUR TEST FRAME *** #50 (rx total #53) — 64 bytes: ...
50 sent, 50 received (frame #50 of 50, rx total #53 — the extra 3 being ordinary broadcast traffic mixed in on the same wire). No drops, no corruption, no reordering — the RX chain, from PHY through MTL through DMA through MPU-protected memory, working exactly as designed.
— Final State —
What works now
AXI SRAM and MPU bugs maybe will not come if someone will use standart linker.ld and startup file of board. Everything ı wanted to write custom And actually they are not bug all connected subject. I mean ethernet using DMA and without MPU (memory protection unit) will be weird. You want to touch memory without protction... sooo all in one logic actually
✓ PHY link detection — up/down, speed, duplex, confirmed on real hardware
✓ TX — confirmed in Wireshark, sustained continuous sends, zero failures
✓ RX — 50/50 real unicast test frames received with zero loss
✓ Both TX and RX use the same AXI SRAM + MPU Non-cacheable region, verified stable across a clean rebuild
| Bug | Root Cause | Status |
|---|---|---|
| 1 | Descriptors in DTCM (DMA-unreachable) | Fixed — moved to AXI SRAM |
| 2 | Tail pointer semantics | Fixed — points at last valid descriptor |
| 3 | D-Cache + Strongly Ordered memory | Fixed — MPU Normal Non-cacheable |
| 4 | Linker alignment for MPU region | Fixed — 8KB ALIGN added |
| 5 | RBSZ (RX buffer size) unset | Fixed — explicitly set to 1536 |
| 6 | MTL Store-and-Forward unset | Fixed — RSF bit set |
| 7 | MAC address never programmed | Fixed — MACA0HR/LR written |
Lessons worth writing down
A linker script comment can already contain the answer, months later
The DTCM memory-map comment ("no DMA!") had been written during an earlier, unrelated session — and turned out to be the exact answer to Bug 1, sitting unread in a file that had been open the whole time. Comments documenting hardware constraints are worth re-reading, not just writing.
An identical failure pattern at different scales is a strong clue, not noise
"Works exactly N times, then fails forever" at both N=1 and N=2 descriptors was the detail that ruled out randomness and pointed straight at descriptor reuse — a coincidence at one ring size might be ignored, but the same exact-count pattern repeating at a different size is a signal worth trusting.
Cache coherency bugs often look like something else entirely
The actual symptom of the D-Cache problem wasn't a cache-related error message — it was a tail pointer seemingly not working, then an unrelated-looking alignment fault once an MPU region was added to fix it. Cache issues frequently present as "this other thing I just changed is now broken," not as anything obviously cache-shaped.
Reset defaults are not always safe defaults
Three separate reset-default values (RBSZ=0, RSF=0, MAC address=0) each silently produced "receives nothing, reports no error" rather than any diagnosable failure. When a peripheral goes quiet without complaining, checking every register the driver never explicitly writes is often more productive than re-checking the ones it does.
Real network traffic is a better RX test than loopback
Internal MAC loopback (MACCR.LM=1) never worked reliably on this STM32H7/RMII combination even after every fix above — while the exact same descriptor/DMA/MPU code, tested against real network traffic instead, worked immediately. When a "simpler" self-test keeps failing while the real-world path works, it's worth questioning whether the self-test itself is reliable on this hardware, rather than continuing to debug against it.
References
Everything above can be verified against these primary sources.
When in doubt, go to the spec, datasheet, programm,ng manual... — not a blog post.
It is only what ı understand can be wrong !
STM32H7
- RM0433 — STM32H753 Reference Manual, Chapter 58 (Ethernet)
- PM0253 — Cortex-M7 Programming Manual (cache enabling sequence)