A WPA2 access point
you can take apart
From complex I/Q samples to a webpage on an ordinary Wi-Fi client—with the source, the arithmetic, and the state transitions exposed.
A Wi-Fi chip normally hides the most interesting part of the connection. You ask it to join a network, and eventually a socket becomes available. Here, the radio samples, immediate responses, association, keys, encrypted packets, addresses, and HTTP response are all part of one readable implementation.
I built a WPA2-PSK access point with a Windows C++ protocol engine and an E310 SDR frontend. A real ESP8266 station completed association, the four-way handshake, DHCP, ARP, and repeated HTTP page reads. An iPhone also retrieved the page earlier in the same implementation's development. The final compact FPGA revision described here was exercised with the ESP8266.
Draw the boundary around the deadline—not the vendor
The useful abstraction is a station exchanging complete 802.11 frames with a protocol engine. The choice of SDR, FPGA, operating system, or processor sits underneath that boundary. A hardware address, a USB API, or a Linux driver does not define DHCP or WPA2.
| Responsibility | Final implementation | Natural porting boundary |
|---|---|---|
| Association, station state, WPA2, packet encryption, IP, TCP, HTTP | Windows C++ | Complete received PSDU → zero or more outgoing PSDUs |
| Ordinary transmit PLCP, scrambling, differential encoding, Barker/sample pattern | Windows C++ | PSDU → lossless two-level waveform descriptor |
| RX byte assembly, diagnostic peak/counter interpretation | Windows C++ | Raw byte events and native counter words |
| UART framing, register/FIFO moves, transmit completion and health lease | E310 ARM C++ adapter | Transport and device I/O; no Wi-Fi keys or IP stack |
| Continuous DSSS RX, receive FCS/classification, ACK/CTS, buffering and waveform playback | FPGA | Sample stream plus frame-end age; deterministic sample output |
| RF tuning, conversion and antenna switching | SDR frontend and device adapter | Complex samples and RF enable |
The reference runs a 1 Mb/s, long-preamble, 11-chip Barker/DBPSK PHY at 20 million complex samples per second. This keeps the radio path understandable without sacrificing WPA2-PSK/AES-CCMP. A lower PHY rate is not a request to use WEP or an open network.
The full E310 image uses 14.5 block RAMs and 1,420 occupied slices. The accepted build's worst setup/hold slack was +2.305/+0.091 ns. Its radio domain is 40 MHz and its processor-bus domain is 100 MHz. These are the clocks to use when reproducing its timing—not the separate higher-frequency arithmetic benchmark discussed below.
There is no Windows TCP/80 listener for this page. The C++ program constructs TCP segments itself and sends them as encrypted Wi-Fi frames. Typing the address on an unrelated wired computer does not reach this endpoint.
Turn a frame into the exact transmitted sample sequence
Start with a PSDU: the 802.11 MAC header and body, with its four-byte FCS already appended. e310_host_waveform.hpp::encode() constructs the surrounding physical packet, or PPDU. Its input is bytes; its output describes every sample of this particular waveform.
- Prepend 128 SYNC bits, initially all ones, followed by the long start-frame delimiter. The source stores SFD bytes
A0 F3and transmits each byte least-significant bit first. - Append the six-byte PLCP header: SIGNAL
0Afor 1 Mb/s, SERVICE00, a little-endian LENGTH equal to8 × PSDU_bytesmicroseconds, and the two-byte header CRC. - Generate the PLCP CRC with initial state
FFFF, reflected polynomial8408, LSB-first input and final complement. This CRC protects the first four PLCP header bytes. It is not the MAC FCS. - Scramble the entire preamble/header/PSDU bitstream with the seven-bit feedback state. The implementation's update is
s = input_bit XOR state[3] XOR state[6]; shift ins. The transmit seed is5D. - Differentially encode:
phase ^= s. Store that phase bit. A phase bit selects the polarity of a complete spread symbol; it is not an unencoded payload bit. - Spread each symbol with
+ − + + − + + + − − −, the 11-chip Barker word. The host's 20-sample representation selects chipfloor(11 × sample / 20)for sample positions 0 through 19.
At 1 Mb/s, each information bit occupies 1 µs. At 20 MS/s that is exactly 20 samples. Eleven chips need not imply a 22 MS/s interface: this code defines the fractional 11:20 sample pattern explicitly. The pattern repeats once per symbol, so the player does not accumulate a rounding error from one symbol to the next.
For this rectangular waveform, each sample is one of two signed complex values: I = +8192 or −8192, Q = 0, carried as packed IQ16. The descriptor contains a 20-bit polarity pattern, the two IQ words, and one differential phase bit per symbol. It is a lossless description of this two-level waveform, not a general arbitrary-I/Q compression format.
| WF20 byte offset | Meaning |
|---|---|
| 0–2 | Little-endian 20-bit sample-polarity pattern; upper four bits zero |
| 3 | 20 samples per symbol |
| 4–7 | First packed IQ16 sample value |
| 8–11 | Second packed IQ16 sample value |
| 12 onward | Eight LSB-first differential phase selections per byte, including the complete PHY preamble and header |
gf_host_waveform_tx.sv knows none of the Wi-Fi equations. It loads the two sample values, rotates the 20-position polarity pattern, XORs its current polarity with the current phase bit, and chooses the sample. After 20 sample ticks it advances the phase bit; after eight symbols it advances the stored byte. Block RAM supplies those bytes. A memory-read delay is explicit, not hidden in an assumed combinational RAM.
Once a PPDU begins, the player cannot pause the air waveform because software is late. If the sample sink stops accepting data, the player aborts and reports an error. At the other boundary, a kill or disarm overrides playback immediately.
This representation is why ordinary transmit DSP could move to Windows over a 460800-baud link. Raw IQ16 at 20 MS/s would require 80 MB/s before transport overhead. Sending one phase bit per symbol plus a shared sample pattern avoids that stream. UART remains a throughput bottleneck; it does not carry the SIFS response.
Read: complete host waveform encoder · complete FPGA player.
Recover bytes without losing the time at which the frame ended
The receiver in gf_dsss_1mbps_rx.sv accepts signed I/Q samples with an explicit valid signal. Twenty complex samples represent one possible symbol window. Correlation against the sampled Barker word turns matching windows into a large complex result; nonmatching windows tend to cancel.
C[n] = Σ k=0..19 ( b[k] × r[n − (19 − k)] )
b[k] = the ±1 Barker waveform sampled onto 20 positions
D[n] = Re{ C[n] × conjugate(C[n − 20]) }
= I[n] × I[n − 20] + Q[n] × Q[n − 20]
scrambled_bit = sign(D[n])
The Barker coefficients are signs, so the correlator uses additions and inversions instead of general coefficient multipliers. Its reduction is pipelined. Correlation values use a 24-bit signed path; the established differential input stage shifts by three bits and supplies signed 18-bit operands. The final compacting change preserves those widths—it does not gain its resource reduction by further truncating the receiver.
A symbol boundary is not known when reception begins. The receiver maintains timing scores for all 20 possible sample phases. A leaky accumulation of correlation magnitude makes the strongest recurring phase distinguishable from neighboring partial windows. The scores live in memory; their reset validity is tracked without requiring an expensive clear of every RAM cell.
The final SINGLE_PHASE_RX mode still observes all 20 timing scores, but keeps one selected differential/descrambler context instead of 20 complete decoders. Eight consecutive decoded preamble ones hold the candidate phase. A sufficiently long preamble run opens an SFD search; the accepted SFD locks the phase through the frame. Resampling phase selection and differential arithmetic are distinct operations.
The descrambler uses the received scrambled bits as feedback. It advances through the long preamble, recognizes the SFD, then enters READ_PLCP. Only a valid 1 Mb/s PLCP with a supported SERVICE value, nonzero length, integral byte count and correct CRC advances to READ_PSDU. The declared length determines exactly how many bytes follow. Those bytes include the MAC FCS.
Each decoded bit also has an age. Pipeline registers and serial arithmetic delay a decision, but the over-air frame boundary has already happened. The receiver carries the selected phase and elapsed-cycle metadata alongside the value. On phase reselection, stale arithmetic results are flushed rather than allowed to acquire meaning in the new descrambler context.
The serial arithmetic actually used
gf_serial_differential.sv schedules one signed dot-product request every 40 clocks. At a 40 MHz radio clock, that matches the one-symbol-per-microsecond decision rate. The arithmetic graph continuously consumes bit streams; an invalid slot carries zero with invalid metadata rather than introducing start/busy/drain bubbles.
gf_serial_mul40.sv implements Brian Greenforest's LSB-first serial multiplication and serial addition construction. Two signed 18×18 products feed the dot product. Sign extension is handled algebraically; redundant sign rows and additions of zero were removed. The 40-clock framing supplies product/sign headroom and a fixed schedule, rather than pretending an 18-bit multiplication can be restarted on arbitrary cycles without a latency contract.
The last accepted change pruned unused leaves of registered fan-out trees while preserving every live path's depth. That removed 212 flip-flops and 72 occupied slices: 5,397 → 5,185 FF and 1,492 → 1,420 slices, with the same 2,774 LUTs and zero DSP blocks. Registered copies are local timing structure, not arithmetic precision.
The isolated compact dot-product graph also routed at a 333.333 MHz constraint, using 146 LUTs and 631 FF with +1.698 ns setup slack. That characterizes the arithmetic graph alone; the working radio uses the 40 MHz schedule above.
Read: receiver and timing-score memory · serial scheduler and metadata · serial multiply/add graph.
Ten microseconds belongs to the immediate reply—not to HTTP
The requirement is to begin the immediate response after the short interframe space. It is not a requirement to demodulate an entire packet, run WPA2, produce an HTML document, and transmit the complete reply within 10 µs. Most of the frame is already decoded while it arrives.
The receive FCS is accumulated bit by bit, alongside byte assembly. The low-MAC classifier extracts just what the immediate response needs: frame type, receiver address, transmitter address and reservation duration. It checks that the frame's FCS is valid and that the receiver address matches this AP. Broadcast traffic does not receive a unicast ACK.
SIFS_cycles = clock_Hz × 10 / 1,000,000 remaining = SIFS_cycles − decision_age − fixed_pipeline_offset at deadline: start response only if the path is ready otherwise: count the miss and drop the immediate response
At 40 MHz, SIFS is 400 clocks. gf_sifs_scheduler subtracts the decode age and its fixed downstream offset. The control response can be prepared before that countdown finishes. A long-preamble 1 Mb/s ACK or CTS occupies 192 µs of PHY preamble/header plus 112 µs for its 14-byte MAC frame: 304 µs total. For CTS, the reservation duration is max(RTS_duration − 314 µs, 0), accounting for CTS and SIFS.
This path contains its own short-frame formatter and DSSS transmitter because asking Windows for a new response waveform after reception would put the UART and OS scheduler inside the deadline. Ordinary beacons, handshake frames and data packets use the host-generated waveform player instead.
PacketApCore::append() explicitly excludes outputs marked sifs_deadline from host transmission. The portable protocol model can describe ACK/CTS, but the deployed adapter leaves them to the local response engine. That prevents a second, late software ACK from following the hardware ACK.
The deadline requires a deterministic receive/response path; it does not require Wi-Fi algorithms in FPGA or ASIC logic. There is a published CPU implementation to examine, not just a hypothetical alternative. The CPU-only section below connects that precedent to a new Cortex-A9 receiver and measured CPU costs. For our currently operating E310 AP, the FPGA remains essential: its continuous receiver and radio-timed response path have not yet been replaced by an end-to-end CPU path.
Why we moved the fast path into FPGA: the original Pluto experiment
This did not begin with a slow UART and a declaration that software was impossible. The first implementation streamed real I/Q through stock Pluto's libiio/USB path into concurrent Windows C++ receive and transmit workers. It decoded real frames, formatted actual transmitted waveforms, and ultimately delivered an ESP8266 HTTP 200 response and HTML through the air. The original host-only path worked far enough to expose its failure precisely.
There were two different transport problems. At a configured 20 MS/s, one retained run delivered 60.900 seconds' worth of samples over 232.167 wall-clock seconds: a host delivery ratio of 0.262, with no drops in the application's own queue. That is a delivery-throughput measurement, not a sample-by-sample hardware discontinuity trace. It did not establish gap-free real-time observation. Separately, receiving some complete packets did not give Windows a bounded, ten-microsecond path back to the DAC.
We removed real software overhead before changing the architecture: precomputed the rational resampler instead of rebuilding a 48-tap trigonometric interpolation for each transmission; reused the IIO TX buffer instead of creating and destroying it per frame; removed an extra millisecond sleep; made RX batches configurable below their earlier ten-millisecond floor; and tested concurrent RX/TX instead of allowing a shared USB mutex to hide the next receive opportunity behind a TX push. Lower RX rates around 6 MS/s were explored to reduce transport load. None of those changes made USB-buffer delivery equivalent to immediate visibility of the last RF sample.
The decisive stock-iPhone attempts were not a wrong-password problem. The final retained host-only run verified M2 twice and M4 twice, yet never reached DHCP and logged seven deauthentication events. The best cached ACK reached the USB push call 0.012 ms after being queued, with zero reported synthesis or bus-lock wait. The following push call took 2.383 ms to return for 36,784 bytes of samples. An earlier narrative had reported a roughly 40 µs minimum; the actual log's minimum is 12 µs. Even that narrower queue-to-push interval exceeds SIFS before the response has crossed USB.
Neither the 12 µs nor the 2.383 ms value is a scope measurement of the first transmitted RF sample. They are separately instrumented software stages: input buffering and decoding precede the queue, and a push completion is not an air-start timestamp. The code's reported 0.460 ms waveform airtime includes its experimental guards, rather than redefining the standard 304 µs ACK PPDU. The selected timing records and their interpretation preserve these distinctions.
The ESP's successful retry-dependent page fetch therefore did not prove that a stock phone could stay connected. Preformatted response trains and speculative timing did not create a reliable causal reply to an unknown future frame. A conforming immediate response must use the received transmitter address, frame type and valid final FCS, and then begin at the correct air-relative time. The receiver and response trigger had to share a deterministic local timeline.
That is why the first FPGA island tapped incoming I/Q, continuously decoded DSSS, checked FCS and addresses, and locally generated ACK/CTS. It arbitrated with the existing ordinary host transmit stream; it did not require moving the WPA2 or IP stack. The routed Pluto island was a fit/timing result, not a deployed Pluto success. The later E310 implementation supplied the physically exercised integrated shell, and the iPhone subsequently retrieved HTML on that path. This progression separates three accomplishments that must not be conflated: host-only ESP HTTP, an FPGA timing result, and an integrated radio serving a phone.
Why even the ARM-local E310 version retained FPGA processing
Putting the processor beside the converter removes USB from the loop; it does not remove the loop's deadline. More importantly, the implemented E310 shell did not stream continuous raw I/Q into a hard-real-time ARM receiver. It delivered already-decoded bytes through GP0 registers/FIFO to a Linux C++ service. Its raw-I/Q instrument is a triggered 16,384-sample buffer that freezes before ARM reads it by address. That is useful measurement access, not a continuous low-latency sample DMA engine. The Zynq's potential high-bandwidth interfaces and this shell's actual interface are different things.
The earlier ARM-local version ran association, WPA2 and networking on Cortex-A9. It already relied on the FPGA receiver and SIFS island. The new CPU-only receiver now decodes retained physical I/Q on the actual E310 ARM, with the results below. Continuous radio DMA and CPU-triggered over-the-air ACK timing are still separate integration steps. Moving application code beside the ADC did not perform those steps automatically.
Even with an improved local DMA path, three budgets must all close:
- Continuous receive throughput. A new complex sample arrives every 50 ns at 20 MS/s. Correlation, timing acquisition, differential decoding, descrambling and CRC must keep up over every frame. High memory bandwidth alone does not execute those operations.
- Visibility of the final sample. DMA commonly releases blocks rather than individual samples. For illustration, a 1,024-sample block spans 51.2 µs; waiting for the rest of that block can by itself exceed SIFS. A 64-sample block spans 3.2 µs, leaving less than 6.8 µs for everything else in the worst alignment. These are calculated examples, not measured settings of our E310.
- Bounded response launch. Finish the last decode/FCS decision, select the addressed ACK or CTS, publish or trigger its samples, cross the bus/clock boundary and switch the RF path before the first response sample is due. A normal Linux userspace loop has no demonstrated bound on all of that. Priority or proximity is not a timing certificate.
Our packet adapter explicitly sleeps for 50 µs between service iterations, and TX-completion polling also uses 50 µs intervals. These are acceptable because that software is outside SIFS. Removing the sleep is possible; it would not, by itself, supply a continuous raw-sample transport, a bounded receiver, or a sample-timed TX trigger. Calling this existing Linux service a tightly bounded ten-microsecond PHY would misdescribe the code.
The FPGA solves the actual deployed problem by processing the samples on arrival, accumulating FCS during reception, extracting reply fields before the end, carrying decision age through the DSP pipeline and launching from the same radio-clock timeline. It does not wait for a completed frame to be delivered to a task. Only the remaining pipeline tail and RF turnaround sit after the last incoming bit. This is why moving just a timer into FPGA, while leaving all receive decisions behind an ordinary buffered software interface, would not solve the problem.
For your own chip, a viable alternative is a dedicated real-time core with tightly coupled sample buffers, incremental processing, bounded memory access and a hardware-timed sample-output trigger. Prebuild response data where possible; keep final validity as the local launch veto. Demonstrate sustained reception and the worst-case physical frame-end-to-first-response timing under contention. If that closes, the receiver need not be FPGA RTL. We retained RTL because this local streaming/deadline engine was the implemented and physically proven path—not because an ARM near an ADC is inherently forbidden from implementing Wi-Fi.
The later Windows offload adds a different transport budget
The E310's later packet UART is a separate, additional constraint—not the historical reason for abandoning reactive stock-Pluto USB ACKs. Signed 16-bit I plus 16-bit Q at 20 MS/s is 80,000,000 bytes/s. The final 460800-baud, 8N1 UART carries at most 46,080 bytes/s in each direction before framing overhead—a factor of about 1,736 short. Even one UART byte occupies 21.7 µs on the wire, already longer than the immediate-response interval. These are interface arithmetic, not a measured Windows latency benchmark.
Second, an ACK decision depends on the end of the received frame. Its address can be extracted early, but its FCS cannot be accepted before the final bits arrive. Sending that decision to Windows and a response back would miss the deadline even with zero CPU processing time on this serial interface. Precomputing the possible ACK waveform on Windows would still leave a local trigger, receiver validation and sample player necessary.
| Block retained locally | Reason in this implementation | What lets you move or replace it |
|---|---|---|
| DSSS receive DSP | The sample stream cannot cross the UART. | A sustained sample transport and a receiver that meets throughput, plus a deadline-capable return path. |
| FCS and minimal classification | The local reply must know whether the addressed frame actually passed its CRC. | A tightly coupled real-time processor receiving decoded bytes as they arrive. |
| ACK/CTS scheduler and short-frame TX | The first response sample must meet the frame-end deadline. | A deterministic local timer, formatter/player and RF turnaround path; it need not be FPGA logic. |
| RX FIFO | Continuous radio time and bursty processor/bus service are different clocks and schedules. | Another bounded queue or DMA interface with an explicit overflow contract. |
| Ordinary waveform player | UART can supply compact descriptions, not a continuous 80 MB/s waveform. | Direct sample DMA or an equivalent two-level sample player in your chip. |
The ordinary PSDU-to-DSSS formatter did move out. Windows now makes its PLCP, CRC, scrambling, differential phase and Barker sample description. What remains in the ordinary FPGA TX path is playback. Likewise, full receive-frame assembly and diagnostic counter interpretation moved to Windows. Keeping a local receiver is not a claim that association, WPA2 or HTTP belongs in an ASIC.
The integration pressure is real; the deadline is not a security feature
SIFS gives the immediate responder access before stations waiting longer to contend for the channel. Historical committee records explicitly budget receive RF delay, PLCP delay, MAC processing and receive-to-transmit turnaround; the timing mechanism predates WPA2. See the May 1995 IEEE 802.11 timing minutes and the 1995 MAC draft's interframe-space rules.
The practical consequence is an integration barrier: a packet application on a general-purpose host cannot use an arbitrary slow peripheral as though it were a Wi-Fi adapter. The fast receive/response loop must live near the samples. Selling that loop only as a closed chip makes an engineer dependent on its vendor. Publishing its data paths, arithmetic and timing interfaces removes the secrecy from that dependency. This evidence establishes the engineering consequence, not a historical claim that the committee chose the interval to sell ASICs.
Nor does Wi-Fi require 5,185 flip-flops for a ten-microsecond timer. That number is this entire E310 image, including receiver state, serial DSP pipelines, sample interfaces, queues, clock crossings and bus registers. A custom chip can use different memory, arithmetic, clocks and processors. Preserve the contracts; do not copy an FPGA utilization number as a protocol requirement.
Read: scheduler · frame classifier · control-frame transmitter.
A CPU-only replacement: the mechanism, the code, and the measured ARM cost
Microsoft Research's Sora / SoftWiFi paper, sections 3, 6 and 7, describes CPU PHY/MAC processing with a generic FPGA I/Q transport board. Its fast path uses dedicated CPU cores and low-latency PCIe. It prepares ACK samples after decoding the sender address, caches them on the radio board, and issues the transmission command after software CRC validation. The authors report interoperability with commercial Wi-Fi cards. A first short frame can miss while its ACK cache entry is prepared; retries reuse that entry. This demonstrates a CPU-processing architecture, not that an arbitrary USB SDR or ordinary scheduled task has the same timing.
The distinction matters when building custom chips: sample transport and sample playback are not Wi-Fi demodulation. A transport may carry arbitrary signed I/Q, maintain a sample counter and play a CPU-selected buffer at a CPU-selected timestamp. The CPU must still derive the received frame, FCS result, destination, response type and response samples. Calling a hardware Barker detector or automatic SIFS ACK engine “plumbing” would not remove acceleration.
Incremental C++ running on the E310's Cortex-A9
I implemented that CPU-side receiver and response preparation separately from the operating AP. The C++ receiver carries history across arbitrary sample chunks, acquires timing, detects DBPSK, descrambles, checks PLCP CRC-16, assembles bytes and accumulates FCS. It decoded a retained 16,384-sample ESP radio capture into the same valid 30-byte frame. Tests also cover 20 sample alignments, six chunk sizes, energy gating enabled/disabled, damaged FCS, and a CPU-produced ACK decoded back through the receiver.
The acquisition kernel uses the exact first difference of the original 20-sample Barker FIR. Once the symbol phase is held, the code copies intervening samples in spans and computes only the selected correlation. During quiet periods, a CPU NEON comparison skips correlation only when every old and new sample is individually below the configured threshold; therefore no rolling-window mean can cross it. This changes execution cost, not the stored I/Q values or the Barker filter.
| Measured Cortex-A9 receiver variant | Input samples processed per second |
|---|---|
| Direct correlation; no per-block clock instrumentation | 10.9 million |
| Exact sparse Barker recurrence | 14.9 million |
| Locked-symbol span copies | 17.2 million |
| Configured idle-energy gate | 20.3 million |
| CPU NEON quiet-span fast path | 36.6 million |
These are receiver-only memory-replay measurements on the actual board, not an x86 projection or a live DMA measurement. Each row used 200 repetitions of the retained capture, 64-sample calls and no clock read per call. The gated rows used a mean absolute I/Q threshold of 128 in the captured IQ16 scale. DMA, RF turnaround, ACK preparation and host forwarding are excluded from these throughput figures. The working set is replayed from memory; fresh radio traffic introduces another memory-access workload.
Prepare the reply before the last incoming bit
AckPlanner::byte() learns the transmitter address after byte 16 and prepares all 6,080 complex samples of a 304 µs ACK on the CPU. finish() accepts only a valid addressed frame, checks its response policy and rejects a passed launch timestamp. The cache avoids rebuilding the same waveform for subsequent frames. This is full raw-I/Q output—not a descriptor that asks FPGA logic to spread Barker chips.
During reception: decode header → prepare/cache complete ACK I/Q While payload arrives: continue decoding and accumulating FCS At final FCS: validate → select cached samples → request timed playback
In the retained ARM run, cold ACK preparation took 17.696 µs; cached final selection took 0.941 µs, including timer overhead. Preparation occurred with 14 bytes—112 µs at 1 Mb/s—still to arrive in that recorded frame. These are individual software timings, not worst-case guarantees. The software-selected start index was 200 samples after its receive-end index; no radio playback was connected to this workbench.
Why the working AP still needs its FPGA fast path today
On this retained capture, CPU processing is faster than the input sample rate; continuous sample delivery and bounded launch remain the replacement work. The instrumented Cortex-A9 run saw a 32.311 µs block-service outlier under normal Linux scheduling, already longer than SIFS. CPU affinity and locked memory did not establish a worst-case deadline. A clock read itself measured about 0.692 µs on this kernel, so instrumented block durations and uninstrumented throughput are reported separately.
The next implementation must supply small committed raw-I/Q DMA blocks, coherent buffer ownership, radio-relative timestamps, enough queue capacity to survive acquisition bursts, and a generic timed sample player. Queue capacity need not become block-release latency. Late replies must be discarded, not sent after their opportunity. A dedicated execution context and the transport must then be exercised together with an ESP station, followed by WPA2, DHCP, ARP and a complete HTTP body read.
For the E310 AP published here, removing the FPGA receiver or response engine now would remove a required working subsystem. The measured CPU workbench supplies the replacement algorithms and a throughput result; it does not yet supply that live subsystem. Conversely, a failed E310 variant cannot turn the published Sora counterexample into a universal requirement for a Wi-Fi ASIC.
Download the CPU-only C++ workbenchRead its full source inline
Build and run the supplied Windows launcher, or cross-compile the same code with the E310 ARM SDK. The workbench instructions explain input format, threshold, runtime and measurements. This separate program reads I/Q files and never transmits. The main article download remains the complete working FPGA-assisted WPA2 AP.
Make a network visible, then admit a station
The AP must advertise exactly the PHY and security that it can receive. The final configuration advertises only basic 1 Mb/s DSSS, a long preamble, the selected channel and a WPA2 RSN element. Advertising faster receive modes would invite a client to transmit a format that this receiver does not decode.
ApProtocol::beacon() builds a management header, timestamp, beacon interval, ESS/privacy capabilities, SSID, rate and channel elements, a traffic-indication map, RSN and FCS. PacketApCore::tick() emits beacons according to monotonic time. The reference's 10 TU setting is 10.24 ms; the same configured interval goes in both beacons and probe responses.
- Discovery. A matching or wildcard probe request receives a probe response carrying the same SSID, rates, channel and security description.
- Open-system authentication. Transaction 1 with algorithm 0 receives transaction 2, status 0. “Open-system” names this management exchange; it does not mean the later data traffic is unencrypted.
- Association. A previously authenticated station requests the SSID and RSN capabilities. The code requires RSN version 1, CCMP pairwise/group suites and the PSK authentication suite. It assigns an association ID and responds with status and supported rates.
- Key establishment. A successful new association starts the four-way handshake. The station is associated before it has a verified data key.
State is per station, keyed by MAC address: authentication, association ID, nonces, PTK, replay state, packet numbers, lease, power-save queue and TCP connections. The default station limit is eight; the host accepts a configured limit from one to 64. One client's packet number or TCP port must never become another client's state.
Retries are necessary protocol behavior. Repeated authentication requests are answered. A recent duplicate association sequence receives the response again without resetting an in-progress key exchange. Repeated valid M2 requests M3 again. A new association starts a new key epoch; disassociation clears keys, TCP state and queued traffic.
The iPhone's “Legacy Access Point” label is consistent with the advertised legacy PHY. It is separate from Safari's “Not Secure” label for HTTP without TLS. This implementation uses WPA2-PSK/AES-CCMP on the air link and deliberately serves ordinary HTTP on port 80.
Ordinary channel access: the exact implemented boundary
The reference shell gives a pending or active SIFS response priority over starting ordinary TX. Specifically, tx_channel_available = !(response_pending || response_active || tx_override_valid). That signal means the local response engine is free; it is not a measurement that the air channel is idle. The accepted shell does not implement a complete carrier-sense/DIFS/random-backoff/NAV contention engine. Its successful chamber connection must not be mistaken for a crowded-channel interoperability or certification test.
For a shared-channel product, put physical carrier sense, virtual carrier sense, interframe deferral and randomized contention at this transmit-admission boundary, and integrate acknowledgment-based MAC retries. Preserve the higher-priority SIFS path. The current host has management, key-exchange and TCP recovery, but those are not a substitute for every lower-MAC access/retry rule. This article exposes the working local endpoint and the exact missing general-network machinery instead of calling an empty-channel trial a complete implementation of every 802.11 service.
Read: ApProtocol: beacon(), process_management(), Station and ProtocolConfig.
Establish the keys, byte for byte
The passphrase is never transmitted as the password string. Both peers derive the same pairwise master key:
PMK = PBKDF2-HMAC-SHA1(passphrase, SSID_bytes, 4096 iterations, 32 bytes) B = min(AP_MAC, STA_MAC) || max(AP_MAC, STA_MAC) || min(ANonce, SNonce) || max(ANonce, SNonce) T[i] = HMAC-SHA1(PMK, "Pairwise key expansion" || 00 || B || byte(i)) PTK = first 48 bytes of T[0] || T[1] || T[2] KCK = PTK[0..15] KEK = PTK[16..31] TK = PTK[32..47]
Comparisons in that context construction are lexicographic byte-array comparisons. The label's terminating zero and the one-byte counter are part of the PRF input. This implementation needs 48 PTK bytes for CCMP: a 16-byte EAPOL confirmation key, a 16-byte key-encryption key, and a 16-byte temporal data key.
| Message | What the code sends or checks | State consequence |
|---|---|---|
| M1: AP → station | Fresh random 32-byte ANonce; pairwise descriptor version 2; replay counter; key-info 008A | Wait for a MIC-bearing M2 with the matching counter. |
| M2: station → AP | SNonce, candidate PTK derivation, EAPOL MIC verification with KCK | Only a valid MIC installs the candidate PTK in AP state. |
| M3: AP → station | Same ANonce; next replay value; key-info 13CA; RSN and GTK key-data element wrapped using KEK; EAPOL MIC using KCK | Wait for M4; a retry preserves the key exchange rather than inventing new keys. |
| M4: station → AP | Secure flag, pairwise descriptor, expected replay counter and valid MIC | Mark handshake complete; stop handshake retries. |
For descriptor version 2, compute HMAC-SHA1 over the complete EAPOL packet with its MIC field zeroed, then use the first 16 bytes. Do not authenticate only the nonce or accidentally include the outer 802.11 header. wpa2_parse_eapol_key() validates the EAPOL structure and exposes the correctly bounded packet.
M3 distributes a random 16-byte GTK in a vendor-specific key-data element. wpa2_aes_key_wrap() implements the RFC 3394 wrap operation: start A with eight A6 bytes, process 64-bit R blocks for six rounds, and fold the round counter into A. RSN and GTK data are padded to the required multiple of eight before wrapping. This is different from CCM data encryption.
The final implementation also recognizes a valid, non-replayed CCMP packet under the freshly verified PTK as confirmation that the station installed its key if M4 was missed. This path requires a previously verified M2 and successful CCMP authentication. It does not accept a DHCP packet merely because its bytes look plausible.
Randomness is supplied by the platform cryptographic provider, not a fixed test seed. Windows uses BCrypt; the non-Windows crypto branch uses OpenSSL. A microcontroller port must replace that provider with a real cryptographic RNG and compatible AES/HMAC operations.
Read: begin_four_way_handshake(), process_eapol(), make_four_way_m3() · PMK/PTK derivation, MIC and key wrap.
Authenticate an encrypted Wi-Fi frame before opening its IP packet
A protected data frame contains a MAC header, an eight-byte CCMP header, encrypted LLC/payload bytes, an eight-byte authentication tag and the MAC FCS. The FCS catches transmission corruption. The CCMP tag authenticates the protected contents and selected MAC fields. Passing one is not passing the other.
CCMP carries a 48-bit packet number. Its header places PN0/PN1 in bytes 0/1 and PN2–PN5 in bytes 4–7; byte 3 contains the extended-IV/key-ID fields. The nonce is 13 bytes: priority, transmitter MAC, then PN5 through PN0. Non-QoS traffic uses priority zero; QoS uses the TID.
The additional authenticated data is deliberately not a raw copy of the full MAC header. ccmp_aad_nonce() masks mutable frame-control fields, includes addresses, retains the fragment-number portion of sequence control, and includes Address 4 or QoS priority when present. AAD construction is a frequent source of “correct password, failed connection” bugs.
- Parse the frame layout and locate the protected body using the real header length.
- Select this station's temporal key. For its pairwise traffic, require key ID zero.
- Build the nonce and AAD; verify and decrypt using AES-CCM with an eight-byte tag.
- Check that the authenticated PN exceeds the last accepted PN for this station and receive queue. There are separate replay counters for 16 QoS TIDs and the non-QoS queue.
- Only then update replay state and release plaintext to LLC/IP parsing.
A bad tag must not advance the replay counter. A replayed retry can receive the radio ACK required by the low MAC, but must not deliver the same plaintext to the application twice. On transmit, every newly encrypted frame gets a fresh PN; exhaustion is an error, not a wrap back to zero.
LLC/SNAP begins AA AA 03 00 00 00, followed by a big-endian EtherType. 888E selects EAPOL, 0806 ARP and 0800 IPv4. Before key establishment, unprotected EAPOL is expected. Ordinary unprotected IP data is not an alternative to successful CCMP.
Downlink frames set FromDS and carry the station as receiver, AP as transmitter and the appropriate source address. Uplink frames set ToDS with the AP as receiver and station as transmitter. Do not reverse those addresses while constructing the CCMP nonce.
Read: process_data() and make_data_frame() · parse_data_layout(), ccmp_aad_nonce() and AES-CCM helpers.
Give the station an address and answer its local network
The endpoint is 192.168.44.1 with a /24 subnet in the reference configuration. Per-station leases begin at 192.168.44.100. These addresses belong to the C++ protocol state, not a Windows Ethernet interface.
DHCP: DISCOVER → OFFER → REQUEST → ACK
After decoding IPv4 and UDP 68→67, parse_dhcp() checks the BOOTP request shape and DHCP magic cookie 63 82 53 63. It reads transaction ID, flags, message type, requested address and server identifier. Unknown options are skipped by their length; padding and END have their own handling.
dhcp_reply() echoes the transaction ID and client hardware address, supplies yiaddr, and emits the message type, server identifier, 3600-second lease, /24 mask, router, DNS, broadcast, renewal and rebinding options. DISCOVER produces OFFER; REQUEST or INFORM produces ACK in this reference implementation. Requests selecting another DHCP server are ignored.
The DHCP IPv4 destination is broadcast, but the enclosing Wi-Fi reply is pairwise-encrypted unicast to the requesting station. IP broadcast and 802.11 group addressing are different layers. This choice also avoids needing group delivery to complete the page-fetch path.
ARP: which MAC owns the server IP?
process_arp() checks Ethernet/IPv4 address sizes and an ARP request for the configured server IP. Its reply gives the AP's MAC as the server's hardware address and copies the requesting peer's address into the target fields. The whole ARP reply returns inside an encrypted 802.11 data frame.
DNS and captive discovery
The reference includes a compact local DNS responder which returns the AP IPv4 address, and an ICMP echo reply. A captive-browser request can therefore reach the same C++ page without an internet uplink. The user-entered direct address http://192.168.44.1/ is the simplest deterministic test. This stack is a local endpoint, not a NAT router.
Constructed IPv4, UDP and TCP packets use network byte order. IPv4 gets its header checksum; UDP/TCP checksums include the pseudoheader containing source IP, destination IP, protocol and transport length. A computed zero UDP checksum is encoded as FFFF. Keep these checksums separate from PLCP CRC, Wi-Fi FCS and the CCM authentication tag.
The parser is a compact endpoint, not a general IP forwarding stack: there is no IPv6, IP fragment reassembly, general DNS recursion or arbitrary-size HTTP streaming. DHCP INFORM follows the source's simplified ACK path rather than a full DHCP server implementation. Before making an internet-facing product, add strict receive-side IP/transport validation and test malformed, fragmented and out-of-window input. The delivered source makes those decisions inspectable.
Read: ipv4_packet(), udp_packet(), dhcp_reply(), process_dhcp(), process_arp(), process_dns().
Complete the TCP stream, not merely transmit something that looks like HTTP
process_tcp() accepts destination port 80 and keeps a connection record per station and client source port. It parses the TCP data offset before locating payload. TCP options make “always skip 20 bytes” an incorrect parser.
- SYN. Record the client sequence number plus one. Select the reference server ISN, return SYN+ACK, and advertise the source's TCP options.
- ACK. Advance connection state when the client acknowledges the expected server sequence.
- Request bytes. Append only in-order bytes. Acknowledge the cumulative received position. If HTTP headers span several TCP segments, wait for
\r\n\r\n; do not assume the first packet holds the request. - HTTP response. Build a status line, Content-Type, Content-Length and Connection: close. GET includes the configured HTML; HEAD omits the body. The current page is capped at 1,400 bytes so this implementation can deliver its response with a compact single-response path.
- Data and FIN. Return ACK+PSH+FIN. The next server sequence includes all response bytes plus one for FIN.
- Completion. A client ACK covering response bytes and FIN marks the response complete. A UART TX completion or a low-MAC ACK does not establish that.
The core retains the response for retransmission. An initial one-second response timeout backs off to at most 60 seconds; the implementation limits retransmission attempts. A partial ACK advances the saved offset, so only the remaining bytes and FIN are sent again. The TCP sequence stays tied to the same byte stream while the new encrypted Wi-Fi transmission uses a fresh CCMP PN and MAC sequence number.
Duplicate request data does not become a second HTTP request. An in-window RST cancels queued/deferred response state. The included tests exercise split headers, duplicate prefixes, partial acknowledgments, FIN, reset and timeout. The /retry-test endpoint deliberately withholds the first response to exercise recovery; ordinary / does not.
A sleeping client still needs its page
A station can set the Power Management bit and stop listening between beacon intervals. The AP tracks that state and buffers unicast data. The beacon TIM bitmap marks the station's association ID. A PS-Poll releases one queued frame; the More Data bit reports whether more remain. When the station announces it is awake, queued frames can be flushed.
The More Data bit is excluded from CCMP AAD, so changing that flag does not require inventing a new ciphertext for a buffered frame; the outer FCS must be updated. HTTP retry timing also tracks whether a response is actually queued for a sleeping client instead of repeatedly adding copies to that queue. These details matter to a phone even when a continuously awake ESP8266 seems happy.
The complete observed result
The final compact-fanout trial produced verified M2 and M4, authenticated CCMP traffic, a DHCP ACK, an ARP reply and three complete HTTP transfers. The ESP8266 reported HTTP 200 with all 491 of 491 expected body bytes on three successive attempts; it fetched the full page again after the continuous service restarted. That client-side UART result is the completion test for this implementation.
Read: process_tcp(), http_response(), emit_http_response(), maintenance() and power-save routing.
A transport that does not pretend to be the radio
The Windows/E310 connection uses e310_packet_wire.hpp. It is independent of UART: the same encoding can travel over another byte stream, or disappear entirely if both sides share memory. It carries protocol messages, not JSON radio samples.
| Raw header offset | Field |
|---|---|
| 0–3 | ASCII GFAP |
| 4 / 5 | Version 1 / message kind |
| 6–7 | Payload length, little-endian, maximum 4095 |
| 8–15 | 64-bit session ID |
| 16–19 | 32-bit sequence number |
| 20–23 | CRC32 over header bytes 0–19 and payload; excludes this CRC field |
| 24 onward | Payload |
COBS removes zero bytes from each encoded message; a zero delimiter ends it. The decoder can recover after corruption without treating arbitrary serial bytes as a new valid command. It checks size, signature, version, kind and CRC before delivery. Session and monotonic sequence checks keep stale traffic from an earlier run out of the current key epoch.
The adapter first sends GF_E310_PACKET_AGENT_V1 in HELLO. Windows responds with INITIALIZE containing AP MAC, channel and the one-megabit PHY contract. READY reports ABI, sample/clock rates, channel and supported capabilities. WF20 means the host supplies ordinary waveform descriptions; EV10 means it assembles raw receive events; CR11 means it interprets native counter snapshots.
In the final offload, ARM forwards up to 128 receive byte events per batch, with a one-millisecond flush interval. Each event carries byte value and first/last markers. RxEventAssembler reconstructs bounded PSDUs on Windows. A missing transport sequence breaks the partial frame; it must not splice the tail of one RF frame into the head of another.
Windows also interprets the eleven native counter words and computes diagnostics. The ARM retains only the comparisons needed for local health, completion and faults. PING/PONG maintains a two-second host lease. STOP kills RF before STOPPED. A stalled host must not leave the radio transmitting indefinitely; local shutdown cannot depend on a final Windows message arriving.
Read: wire format · PacketApCore · receive event assembly · ARM adapter.
Port the contracts to your processor and radio
A port is a replacement of concrete interfaces, not a translation of the words “E310” into a different board name. Work from the innermost portable protocol outward.
- Compile the protocol without RF.
ApProtocol::ingest(PSDU)producesOutboundframes.maintenance(now)advances retries and deferred work.beacon(timestamp)produces discovery traffic. Supply monotonic time, cryptographic randomness and crypto operations. A small bare-metal target can replace STL containers with bounded storage while preserving the same transitions. - Choose the sample contract. Preserve signed I/Q ordering, scale, valid strobes and continuous sample timing. If your converter does not support 20 MS/s, build an explicit sample-rate mapping and update the Barker representation, phase scores and timing metadata together. Do not change a clock constant and leave a 20-sample symbol in a 22-sample stream.
- Run receive processing while samples arrive. Correlate, choose timing, differentially decode, descramble, validate PLCP, accumulate FCS and classify. Carry the end-of-frame timestamp or decision age through every pipeline stage. CPU or FPGA placement is a throughput/deadline decision.
- Close the immediate-response loop locally. The response engine needs good FCS, receiver match, source MAC, response type, duration, frame-end age, path readiness and RF kill. Measure the first transmitted ACK sample relative to the physical frame end. Reject late work.
- Connect ordinary transmission. Play WF20 or generate the equivalent raw samples in software. The source descriptor defines exactly what to output. A richer pulse shape or waveform format is a separate change that must be exercised over air.
- Connect the packet engine. Forward complete received PSDUs, or EV10 events, to the protocol. Send ordinary outgoing frames to the player. Do not forward the protocol model's SIFS-marked frames to a slow host path.
- Exercise the real station sequence. Require probe/authentication/association, M2 MIC, M4 or authenticated key confirmation, DHCP, ARP, TCP response acknowledgment and a complete client-side page read. Then test lost frames, duplicate requests, reconnects and power-save behavior.
Full I/Q on a host CPU is possible only if the chosen frontend and transport supply the sustained sample throughput and bounded response path. A processor fast enough in arithmetic but connected through an unbounded-latency transport does not satisfy the latter. Conversely, an FPGA is not intrinsically required for DHCP, AES, TCP, HTTP or ordinary frame formatting.
The E310 shell is one reference adapter. It maps PS7 GP0 registers and asynchronous FIFOs, uses explicit I/O timing, and keeps the processor AXI clocks running independently of radio activity. All nine PS AXI clock inputs are connected to the running bus clock. The reference preserves PMU behavior and sets unused pins to Pullnone. Those are hardware-specific requirements; the portable protocol does not depend on them.
Its actual startup order is: arm the independent PS watchdog guard; load the stock radio image; prepare the RF device with legacy UHD while keeping that device owner alive; set the proven receive delay; load the custom image; verify the custom register map and PMU access; initialize the RFIC against the custom shell; arm capture; start the packet agent; hand the UART to Windows. On stop, kill RF, restore the stock image, release the keeper, and disarm the guard only after successful recovery. The source includes each C++ helper for engineers adapting this reference, without making that vendor-specific sequence the architecture of the article.
The E310 radio-setup programs use the installed legacy Linux/UHD environment. Newer MPM images expose a different loading/device interface. Match that adapter to your OS rather than overwriting system libraries or loading this register map into a mismatched shell. An alternate SDR substitutes its own RF initialization and I/O code.
Build the source and follow it while it runs
Download and unzip the source archive into a writable folder. With MSVC C++ Build Tools and CMake 3.24 or newer installed, a normal command prompt is sufficient:
build-windows.cmd
wifi_e310_link\build\windows-packets\gf_e310_windows_ap.exe --help
The public source copy was compiled independently of the lab directory. Its protocol, transport, receive-event, counter, transmit-wait and complete Windows-core tests passed. These exercise the code without opening a radio.
With a compatible radio packet adapter already running and its serial terminal closed, create a text file containing your chosen passphrase and run:
run-host.cmd COM10 C:\radio-config\ap.key
Replace the COM port. The supplied example HTML is deliberately short. Join PLUTO-2.4 with the same passphrase and open http://192.168.44.1/. Ctrl+C requests RF shutdown. The native executable also accepts SSID, BSSID, channel, server IP, station count, page, beacon interval, duration, receive-PCAP output and stop-file options. The key file is local configuration, not something to place in a source repository.
build-e310-fpga.ps1 fixes the exact accepted generics and runs Vivado synthesis and implementation in separate processes. build-arm.sh compiles the packet adapter and capture helper using an ARM Linux C++20 toolchain. Neither script installs software, changes the live board, or requires the original experiment's hashes. The accompanying README explains the platform prerequisites and output paths.
The archive includes the full protocol/crypto source, live receiver/serial arithmetic, response engine, waveform player, transport, host/ARM adapters, device constraints and build files. The inline source browser embeds the complete text, with a direct download for each file; it is not a collection of fragments behind a screenshot.
A compact debugging ladder
| Last working step | Inspect next |
|---|---|
| No SSID | Actual beacon samples, frequency, gain, sample timing, preamble, rate/channel IEs and FCS |
| SSID visible, no association | RX SFD/PLCP/FCS, receiver address, timely ACK, authentication and RSN/rate agreement |
| Association, password complaint | M1 reception, M2 MIC, exact SSID/key bytes, PTK byte ordering, M3 wrap/MIC, M4 and RF loss |
| Keys established, no address | CCMP AAD/nonce/tag/replay state; LLC EtherType; DHCP transaction and encrypted reply |
| Address assigned, no page | ARP reply, TCP SYN+ACK, segmented request, sequence arithmetic, response retransmission and client sleep |
| HTTP response logged | Client ACK and complete received body—not merely TX completion |
This ladder keeps the failure attached to a protocol transition. A phone's password dialog does not identify which handshake packet was lost. A clean transmit spectrum does not establish that the receive path or TCP state works.
Classic algorithms, explicit source rights
The selected radio path is Barker DSSS/DBPSK, not OFDM, CCK, Viterbi or Reed–Solomon. Its value is the compact, legible chain from sampled waveform to ordinary client software. The Greenforest implementation is MIT-licensed. The E310-specific source bundle separately preserves the LGPL notices on three Ettus FPGA helpers and GPL notices on the RFIC calibration driver; those are not relabeled MIT.
For cryptography, the precise documented rights statement is unusually direct: RFC 3610 §9 releases the CCM authors' intellectual-property rights to the public domain and records that they knew of no patent covering CCM. That is a public-domain declaration, not an expired-patent claim. NIST FIPS 197 specifies AES; RFC 3394 specifies the separate AES key-wrap operation.
Two concrete US records found in the receiver search are US 7,460,621 B2, “Detection” (record reports fee-related expiry; adjusted term date July 21, 2026) and US 7,224,714 B1, DSSS channel characterization (record reports lifetime expiry June 5, 2025). They describe particular receiver methods, not ownership of every Barker correlation or all of WPA2. The latter's multipath channel-characterization method is not implemented here. An expired neighboring patent cannot establish clearance for unrelated code.
The algorithm-by-algorithm research appendix identifies what this code actually does, relevant publications and patent records, and the distinctions between an expired claim, prior art, a public-domain declaration and a source license. It records US-focused findings as of September 9, 2026. No complete claim chart establishing worldwide freedom to operate was obtained; this publication does not label the whole stack “all patents expired” on the strength of its PHY's age. Product clearance needs the relevant jurisdictions, live patent-family records and the actual implementation's claim analysis.
openwifi takes a broader Linux/mac80211 SDR-stack approach. This implementation is an alternative route for an engineer who wants to follow a narrow complete connection in C++ and small RTL, then choose their own offload boundary. It does not import openwifi's protocol or baseband implementation. The source is organized around the actual association-to-HTML path.
Make the radio understandable enough to change
The accomplishment is more than a visible SSID. Samples became authenticated frames; a station obtained an IP address; TCP delivered all the HTML; and the arithmetic and protocol state are available to change. Windows holds the work that benefits from software. The local response path holds the work whose deadline demands it.
Use the source to build the same path, move a boundary, replace the frontend, or teach the system one layer at a time. If you are building a radio, embedded platform, or serial DSP architecture, talk with Brian Greenforest about the part you want to make your own.