diff --git a/README.md b/README.md
index 8f298f0..4ff53f7 100644
--- a/README.md
+++ b/README.md
@@ -13,3 +13,91 @@ Because blinkenwall v3 has `64 * 96 = 6144` pixels, driving it at 100 FPS requir
(typically SPI), so this would require coordinating several microcontrollers in parallel. A
much more integrated solution is to instantiate as many WS2812 drivers as desired in an FPGA,
then point the entire video firehose at the FPGA.
+
+## Ethernet communication
+
+The controller accepts UDP packets containing image data on port 61437 ("PIXEL"). Each packet
+contains color data for one strand of LEDs.
+
+The packet structure is as follows:
+
+
+
+ |
+ 00 |
+ 01 |
+ 02 |
+ 03 |
+ 04 |
+ 05 |
+ 06 |
+ 07 |
+ 08 |
+ 09 |
+ 0a |
+ 0b |
+ 0c |
+ 0d |
+ 0e |
+ 0f |
+
+
+ 00 |
+ magic (0x5049_584c ) |
+ strand number |
+ frame number |
+ 0 |
+ px1 R |
+ px1 G |
+ px1 B |
+
+
+ 10 |
+ 0 |
+ px2 R |
+ px2 G |
+ px2 B |
+ 0 |
+ px3 R |
+ px3 G |
+ px3 B |
+ 0 |
+ px4 R |
+ px4 G |
+ px4 B |
+ ... |
+
+
+
+Every packet begins with a magic number `0x5049584c` (ASCII for "PIXL"), followed by the strand
+number and the frame number. All numbers are big endian. After the header follows the pixel data,
+separated into one 4-byte word per LED.
+
+Once all strands for a given frame number have been received and the video page has been flipped,
+the controller answers with a confirmation packet:
+
+
+
+ |
+ 00 |
+ 01 |
+ 02 |
+ 03 |
+ 04 |
+ 05 |
+ 06 |
+ 07 |
+ 08 |
+ 09 |
+ 0a |
+ 0b |
+ 0c |
+ 0d |
+ 0e |
+ 0f |
+
+
+ 00 |
+ frame number |
+
+