The main bug was caused by using a bad example ethernet frame that had reversed bit-order in every byte. I confirmed this by capturing a real ethernet frame, and using that as the test vector in simulation. CRC check failed, so I tried reversing the bit order, and viola, I had valid CRC detection.
Of course, I could have left CRC calculation to software, but it is a rather boring thing to implement, and modern network cards all do this, so it seemed like a good idea to do. After all, if I can make it work once in the hardware, then all software can ignore it forever after.
This is one of the nice things about FPGA design, in that it is much more realistic to make the hardware nice to program, because the incremental cost of making an interface that bit nicer is quite low.
Another thing I have almost working for the ethernet is IRQ triggering on packet sending and receiving, so that you don't have to poll the ethernet adapter all the time to know if you there is a packet to receive, or if you can send another frame yet.
To give an idea of how simplified the interface to this ethernet adapter is, here are the register addresses that matter:
$DE800 - $DEFFF - 2KB received frame buffer. First two bytes are the length of the frame. If bit 15 is high, then the frame failed the CRC check. Frames with a bad CRC don't trigger IRQs.
$DE041 - bit 7 - Enable IRQ on frame RX
$DE041 - bit 6 - Enable IRQ on completion of frame TX
$DE041 - bit 5 - A frame has been received since $DE041 was last written.
$DE041 - bit 4 - A frame has been sent since $DE041 was last written.
$DE041 - bit 2 - Which RX buffer was last written to by the ethernet adapter.
$DE041 - bit 1 - Which RX buffer is mapped at $DE800 - $DEFFF for reading.
$DE041 - bit 0 - Set to 1 to force ethernet PHY into reset state. Reset to 0 to allow normal operation.
As described above, there are two receive buffers, so that you don't have to worry too much about losing frames while reading them out, or similarly having the part of the frame overwritten in the buffer while you are reading it out.
To clear any IRQs write anything to $DE041, although this should normally be whatever you read from it, so an LDA / STA pair or similar is the best bet for now. I might change the interface in time so that DEC can be used to do it a little quicker, but not just yet.
To send a frame, you write the bytes to $DE800 - $DEFFF, write the frame length to $DE043/$DE044, and then write $01 to $DE045. Note that the TX buffer is mapped to the same address range as the RX buffer. In other words, the TX buffer is write-only, while the RX buffers are read-only. When you transmit a frame, the ethernet adapter automatically calculates and appends the ethernet CRC to the end of the frame. This still has some bugs, so the CRC is incorrect, but you hopefully get an idea of how easy it is to send and receive ethernet frames with this ethernet adapter. I might add automatic IP checksum calculation later on, too.