MCS-51 Real-World Interfacing - Ethernet


“‘No’ means let’s do it anyway!”

Gene Frantz

I almost abandoned this project ten months ago. The board would not talk to the Ethernet chip, I found an erratum saying the chip needs an 8 MHz SPI clock, I could reach about 35 kHz bit-banged, and that seemed like the end of it. In 2026 I picked it up again and gave it a second shot.

The temptation to get this going anyways remained a bit of a personal challenge. With so many IP stacks dangling around for the tinyest MCUs I was wondering if we can pull of IP networking on this chip.

Ethernet on an STC89C52

None of the STC89 parts have an Ethernet MAC or PHY, so the protocol has to live in an external controller. Dallas Semiconductor saw this coming in the 90s and shipped the DS80C400, an MCS-51 derivative with the MAC built in, still in production. But I do not feel like acquiring another dev kit to catch dust. The low-cost option everyone reaches for is the Microchip ENC28J60: a stand-alone 10BASE-T controller with an SPI interface and 8 KB of buffer memory, around since the early 2000s and still everywhere in hobby and industrial gear. It handles framing, Manchester encoding, collision detection and CRC.

Protocol Background

Every device on the network carries a unique six-byte MAC address, plus two special forms: the broadcast address FF:FF:FF:FF:FF:FF, which reaches everything, and multicast addresses, which reach a group.

Diagram of Ethernet unicast, broadcast and multicast MAC address forms

For a product you register with the IEEE for your own block. Having built a number of chip-down embedded systems for vision tasks over the years, I have an example of what that looks like.

An IEEE MA-S registration certificate for a 4095-address MAC block

After laying down a significant amount of funds, you own a turf of 4095 addresses. On the ENC28J60 they live in six registers called MAADR0 through MAADR5, and those six registers are where this project fell over.

Yet another IP stack

So before writing anything, I went looking for a stack I could borrow. The 8051 has been around since 1980 and the ENC28J60 since 2004, so somebody must have done this.

I built the candidates rather than reading their README files. Each one was compiled with SDCC 4.5.0 for mcs51, linked against a stub main() and stubbed drivers, and the sizes read off the linker’s .mem output. That is the same measurement my own stack reports, so the numbers compare directly.

stackflashRAMMTU
this port, ARP through TCP10,2725011500
uIP 1.0 + DHCP + DNS19,3601,113~380
tuxgraphics 3rd gen7,451776~530
lwIP 2.2.0, minimal137,90013,503n/a
picoTCP, on ARM30,508~19,000n/a

The STC89C52 has 8 KB of flash and 256 bytes of XRAM. Every row except one is over budget on RAM alone, and RAM is the wall you hit first.

lwIP fails to compile, in six files, with SDCC error 92: functions called via pointers must be reentrant. SDCC overlays parameters and locals in statically allocated RAM, so an indirect call cannot carry more than a couple of bytes of arguments. lwIP is built on exactly those calls: netif->input, netif->output, pcb->recv. Making every function reentrant clears the errors and produces the 137 KB above, which then fails to link.

tuxgraphics was written for an ATmega88 with an ENC28J60, almost exactly this board’s budget, and it ships the NIC driver. 7.5 KB of flash is genuinely under the limit. Then its driver reads each frame into buf[] with BUFFER_SIZE 550, and the TCP code indexes that array everywhere.

The pattern across all of them is keeping a whole frame in MCU RAM, because every part they were written for has kilobytes of it to spare.

Searching for 8051 ports turned up two that published their build output rather than a claim.

A Keil C51 port of uIP 0.9 has MEMORY MODEL: LARGE, Program Size: data=30.1 xdata=1024 code=20310 sitting in its repository. Subtracting the 5,635 bytes of ROM web pages leaves about 14.7 KB of stack and driver, using the whole 1 KB of XDATA, at an MTU of 233, with UDP compiled out and no DHCP or DNS. Its SPI is bit-banged on P1.5, P1.6 and P1.7, within one pin of this board.

A hand-rolled SDCC stack reports 18,181 bytes of flash and 2,015 of XDATA for ARP plus one TCP connection plus HTTP, with no ICMP and no UDP at all.

Both needed large memory model. Both needed at least 1 KB of XDATA. Neither managed UDP and ICMP and DHCP and DNS together. No published 8051 IP stack I could find runs below about 14 KB of flash or 1 KB of XDATA.

Keeping IP Headers in SRAM

This idea comes from UIPEthernet. Headers land in a 42-byte scratch buffer, payloads stream from the NIC’s receive buffer straight to its transmit buffer through a 32-byte window, and the TCP retransmit copy lives in a slot in the ENC28J60’s own 8 KB of SRAM. An MTU of 1500 therefore costs no XRAM at all.

UIPEthernet reaches the by setting UIP_CONF_BUFFER_SIZE 98 so the RAM buffer holds headers only and carving the payload pool out of the ENC’s transmit SRAM. Microchip’s own TCP/IP Lite does the same thing. Both are unusable here, one being Arduino C++ and the other licensed to Microchip silicon, but both had the idea first.

Both halves of that conclusion were wrong, and the interesting part is how they were wrong. The erratum does not apply to any chip you can buy today. And the 8051 turns out to have a hardware SPI port that its datasheet never calls one.

Rolling our Own

So picking up the best design lessons from the other stacks, I rolled my own to make it work. It was quite a bit of work in many stages.

stagefeatureflashXRAM
1ARP reply1,82778
2IPv4, ICMP echo2,51078
3UDP echo2,63078
4DHCP client4,02597
5MTU 15004,02597
6lease, renew, rebind, expiry5,137126
7ARP client, DNS A records7,147209
8TCP echo, one connection10,272279

Stage 5 is the one worth staring at. Going from a 576-byte MTU to 1500 costs no flash and no RAM at all, because the MTU is a constant and a different buffer layout inside the NIC.

Stage 8 does not fit the STC89C52RC. Ten kilobytes of flash against an eight kilobyte part is not a near miss, so TCP needs the pin-compatible STC89C516RD+ with 61 KB. Every other stage up to 7 fits the small part with room left.

Why It failed the First Time

The document is DS80349C, and erratum #1 says exactly what I remembered:

When the SPI clock from the host microcontroller is run at frequencies of less than 8 MHz, reading or writing to the MAC registers may be unreliable.

So the dusty one I had in the box had to be replaced by a newer silicon revision. So the first thing the firmware does now, before configuring anything, is say what it is talking to:

ENC28J60 bring-up
EREVID: 0x06  B5/B7 -- errata #1 does NOT apply

And if it is an affected die, there is still a way through that does not need 8 MHz. Read what the erratum actually restricts: access is unreliable, not impossible, and it covers the MAC registers only, not the ETH registers and not the packet buffer, which is where all the throughput lives. The MAC registers are written about a dozen times and all of them during initialisation. So write them, read them back, and retry:

static void enc28j60WriteMacReg(uint8_t address, uint8_t data)
{
	uint8_t attempt;

	if (!Enc28j60MacVerify) {
		enc28j60WriteReg(address, data);
		return;
	}
	for (attempt = 0; attempt < 8; attempt++) {
		enc28j60WriteReg(address, data);
		if (enc28j60ReadReg(address) == data)
			return;
	}
}

Bit-banged SPI

The second failure of the previous attempts were board peripherals. The pins I initially picked were used for LED control and landed in a logic chip.

On the initial attempt. EREVID should read 0x06. It read 0x0C. Byte-identical across three cold boots, which is the shape of a wiring fault rather than noise: noise is not repeatable to the bit. 0x0C is 0x06 shifted left by one. Every register read came back doubled.

The SPI lines were on P0. On an 8051, P0 has no internal pull-up at all: it is open-drain, and on this board it is pulled up through a 10k resistor pack into a bus it shares with an always-enabled 74HC245, the LED matrix rows, and the LCD data lines. None of that is hidden. It is on page one of the board schematic, which I had not read closely enough to notice what else was hanging off the port I had chosen.

Moving the pins away from things that could cause side effects did the trick. This is the configuration that actually worked:

signal8051 pinENC28J60 pin
clockP1.7SCK
host out, device inP1.6SI
host in, device outP1.4SO
chip selectP3.3CS

Those four are the only pins on this board that are actively driven and not shared with something else.

One thing to get right before powering it on: the module needs 3.3 V, and its pins tolerate 5 V. The ENC28J60 core is a 3.3 V part, so feeding the module 5 V destroys it. Its digital inputs are 5 V tolerant.

All sixteen write-and-read-back patterns then came back exact. UDP echo is 50 of 50, byte-exact, from 1 to 100 bytes. Which is terrible by modern standards, but impressive for such a low-spec part.

Those numbers come from a freshly booted board answering a handful of packets. Run it for a few minutes and it stopped replying altogether, and stayed deaf until a reset. Twelve and then fourteen consecutive test runs came back 0 of 5.

Flaky Connections

The SPI link is electrically marginal. A read-only EREVID canary, which must return 0x06, misreads one to three times per run once traffic is flowing. Corrupted headers come back carrying recognisable frame payload, 0x4141 for two ASCII letter As, or pointers that are odd, or pointers outside the ring entirely. Widening the clock made it worse rather than better.

Look at the wiring and it stops being mysterious.

The ENC28J60 module with a mess of Jumper Wires

My former boss and Mentor from 17 years ago that taught me the ins and outs of embedded design advised that any wired connection off a PCB needs a checksum and recovery, well good luck doing this with this protocol. So this required some creativity, that a custom PCB probably does not need.

The driver now defends itself rather than trusting what it reads. spi_resync() pulses CS to get a slave stuck mid-opcode back in step. rd_stable() reads twice and believes a repeat. Every packet header is read twice and validated, with recovery if the ring pointer is nonsense. There is an RXEN watchdog, because on this link even control writes get corrupted, and one lost ECON1 write leaves the board deaf while every status register still reads healthy.

The board no longer goes deaf. It answers one to five of five pings under sustained traffic instead of none. That is an improvement and it is not a fix.

Summary

The chip was never the problem. What made this hard was reading an erratum without its affected-revisions table, debugging through a console that had quietly stopped telling the truth, and then putting the bus on the one port that cannot drive it.

Four things I would take to the next project of this shape:

  1. Read the part number out loud before believing a datasheet. One EREVID read would have saved ten months.
  2. Fix the instrument first. When the debug channel and the device under test break in the same week, everything you conclude afterwards is decoration.
  3. Audit the port before you audit the protocol. P0 is open-drain and shared, and no amount of correct SPI timing survives a line that three other devices can pull down. A repeatable wrong answer is a wiring fault. Only noise is random.

Useful References

Code is in the stc89c52-demos repository. The simulator work is not published yet.

The two 8051 ports that published their build output, which is what made the comparison decidable rather than arguable:

Board and part documentation:

To learn more about Ethernet itself, Microchip University has a good free course covering the physical layer, data link layer and higher-level protocols, and there is a solid application note on Ethernet as well.

The quote at the top is from Gene Frantz. Without some unconventional hacking there would have been no answering machines and no Speak and Spell. His retirement symposium talk is worth a watch:

YouTube video thumbnail


Published: 2025-11-29
Updated  : 2026-09-26
Not a spam bot? Want to leave comments or provide editorial guidance? Please click any of the social links below and make an effort to connect. I promise I read all messages and will respond at my choosing.
← Reviving a Parallel-Port Iomega ZIP Drive on Ubuntu 22.04 MCS-51 Real-World Interfacing - Infrared Data →