Every source file, inline
108 files. All text below is embedded in this page: no source-viewer service, login or network fetch is needed to expand a file. Build outputs and private configuration are excluded.
Download the full source ZIP · Build and run instructions
.gitignore · 9 lines
Download this file · Permanent section link
**/build/
*.log
*.jou
*.str
*.pcap
*.key
private/
clockInfo.txt
LICENSE · 26 lines
Download this file · Permanent section link
MIT License
Copyright (c) 2026 Brian Greenforest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Scope: original Greenforest code and documentation. Third-party files retain
their own licenses, described in THIRD_PARTY.md. This notice does not relicense
Ettus code, system crypto libraries, development tools, or vendor primitives.
README.md · 107 lines
Download this file · Permanent section link
# Greenforest WPA2 AP: from I/Q to HTML
Read the full implementation walkthrough:
https://greenforest.io/e310-wpa2-access-point/
This is the source of the final ESP8266-tested Windows/E310 partition:
2,774 LUT, 5,185 flip-flops, 14.5 BRAM, zero DSP. It uses 1 Mb/s
long-preamble DSSS at 20 million complex samples/s, WPA2-PSK/AES-CCMP,
ARP, DHCP and a C++ TCP/80 endpoint. Wi-Fi clients retrieve HTML through
the radio, not a Windows network adapter. The example source key and HTML
are public replacements for the private lab configuration; algorithms are
unchanged. No lab passphrase, station MAC, device serial or raw RF capture
is included.
## Build and inspect on Windows
Install MSVC C++ Build Tools (Desktop development with C++) and CMake 3.24+.
Unzip to a normal writable directory on C:, open a command prompt there:
```
build-windows.cmd
wifi_e310_link\build\windows-packets\gf_e310_windows_ap.exe --help
```
The build runs protocol, wire framing, RX-event, counter snapshot, transmit
deadline and complete host-core tests. No radio is opened by these tests.
Create a UTF-8 text file containing your chosen 8-63 byte passphrase, without
a BOM. Both your Wi-Fi client and AP must use this same passphrase. Start a
compatible radio-side adapter FIRST, close any other serial terminal, then:
```
run-host.cmd COM10 C:\radio-config\ap.key
```
This public convenience wrapper only starts the native Windows protocol
process. It deliberately does not guess which SDR, firmware or root login
you have. It expects a running GFAP packet adapter; the article specifies
that contract byte for byte. Replace COM10 with your adapter's COM port.
Ctrl+C requests STOP; the radio adapter kills RF before reporting STOPPED.
For programmable stopping or configuration, use the native command:
```
wifi_e310_link\build\windows-packets\gf_e310_windows_ap.exe --run --port COM10 --baud 460800 --ssid PLUTO-2.4 --channel 6 --server-ip 192.168.44.1 --max-stations 8 --passphrase-file C:\radio-config\ap.key --page page.html --beacon-tu 10 --seconds 0 --stop-file C:\radio-config\ap.stop
```
The stop file must not exist when starting. Creating it stops the service.
The host does not bind a TCP/80 socket. Join the RF SSID and browse to
http://192.168.44.1/ with the phone or station. No NAT/internet uplink is
implemented in this core. Keep the page at or below 1,400 bytes.
## E310 reference compile
The retained E310 shell targets xc7z020clg484-3, with legacy Linux 3.14,
UHD 3.10.1.1 and /dev/xdevcfg. It is not an image for the newer MPM Linux
layout or other FPGA packages. The waveform/protocol is independent of
these device details. The shell sources show every relevant pin, PS7
connection, clock constraint, register and RF enable.
```
powershell -NoProfile -ExecutionPolicy Bypass -File build-e310-fpga.ps1 -Vivado C:\VitisVivado\2026.1\Vivado\bin\vivado.bat
```
Vivado runs synthesis and implementation in separate processes. Outputs:
`wifi_e310_link/build/release-compactmul/`, including bitstream, timing,
utilization, CDC and bus-skew reports. This does not load the board.
There is no IP Integrator/BSP generation or Vitis application project.
For the packet agent, use a C++20 ARM Linux compiler. The tested host used
the official Ettus E310 SDK 4.9 compiler; this is a compiler/runtime choice,
NOT an upgrade of the board's Linux. In WSL, source its environment then:
```
source /opt/greenforest/e310-sdk-4.9.0.0/environment-setup-cortexa9t2hf-neon-oe-linux-gnueabi
bash build-arm.sh
```
Run the result with its matching ARM loader and runtime in a private project
directory. Do not overwrite system libc to accommodate a new compiler.
The setup adapters (`e310_rf_preset.cpp`, `e310_legacy_radio_init.cpp`,
`e310_legacy_probe.cpp`, `e310_checked_loader.cpp`, `e310_recovery_guard.cpp`)
are retained as readable reference source. The first two use legacy UHD;
the AD9361 calibration driver retains GPL licensing, separately from MIT
Greenforest protocol code. The article gives their actual startup order.
Do not run the packet agent against stock UHD FPGA registers.
## Port by interfaces, not filenames
Start with `wifi_pluto_link/host/ap_realtime.cpp` (ApProtocol),
`tools/wifi_protocol.cpp` (crypto/frame helpers),
`wifi_e310_link/host/e310_packet_ap_core.hpp` (adapter boundary),
`wifi_e310_link/host/e310_host_waveform.hpp` (sample construction), and
`wifi_pluto_link/fpga_sifs/rtl/gf_dsss_1mbps_rx.sv` (sample receiver).
An alternate host must provide crypto/randomness, monotonic time, storage
and a packet transport. An alternate radio must implement continuous sample
RX, timestamped frame ends, receive FCS/classification, deadline-bound
ACK/CTS, ordinary TX playback and a fault/stop veto. The same state machines
can move to a real-time CPU when it meets their throughput and latency.
The article describes the scheduling equations, widths and interfaces.
Historical alternative branches remain in some source files because they
are part of the compiled source, but the FPGA script selects ONLY the final
accepted compact-fanout/host-waveform/host-counter/host-RX-event configuration.
Read the script's fixed generic list before changing a parameter. Source
tests do not replace an over-air association and complete client HTTP read.
THIRD_PARTY.md · 32 lines
Download this file · Permanent section link
# Source boundaries
The Greenforest AP protocol, host waveform generator, packet transport,
receiver, serial arithmetic and fast-response logic are original source
released under the accompanying MIT license. The source layout preserves
relative includes from the running implementation; `wifi_pluto_link` is an
historical directory name, not a Pluto hardware dependency of the Windows build.
The E310 reference adapter also includes:
* `vendor_uhd_4_9/fpga/usrp3/lib/control/synchronizer*.v` and
`vendor_uhd_4_9/fpga/usrp3/top/e31x/spi_slave.v`: Ettus UHD 4.9 FPGA source,
LGPL-3.0-or-later. The upstream FPGA license is retained next to these files.
* `wifi_e310_link/vendor/uhd_3_10_ad9361`: six unmodified Ettus AD9361 calibration
files from `release_003_010_001_001`, GPL-3.0-or-later. SOURCE.md gives the
upstream location. These compile into the separate radio-setup program,
not the Windows Wi-Fi protocol executable or the FPGA's waveform generator.
* Radio setup dynamically links the installed legacy UHD and Boost libraries.
Distributing a linked radio-setup executable must satisfy their licenses,
including the GPL requirements of the AD9361 driver. The source release does
not include those executables, SDK runtime libraries or an SD-card image.
* The Windows protocol executable uses the Windows BCrypt API. The alternative
non-Windows branch in `tools/wifi_protocol.cpp` uses OpenSSL; obtain OpenSSL
under its own license when porting that branch.
* The E310 shell instantiates Xilinx 7-series/PS7 device primitives. Vivado and
its simulation/device libraries are separate tools, not MIT source in this
archive. Other FPGA families require their own I/O/clock/processor adapters.
Full GPLv3 and LGPLv3 texts accompany this release in `licenses/`. Retain
upstream notices. The MIT core is independent of the vendor calibration
adapter; another SDR can use its own RF setup path with the same core.
build-arm.sh · 17 lines
Download this file · Permanent section link
#!/bin/bash
# Source an ARM Linux C++20 SDK environment before invoking this script.
# Packet agent and capture helper do not link UHD, OpenSSL or a Wi-Fi library.
set -euo pipefail
root=$(cd -- "$(dirname -- "$0")" && pwd)
out="$root/wifi_e310_link/build/release-arm"
mkdir -p "$out"
: "${CXX:?Source the ARM Linux SDK environment first}"
# The SDK intentionally supplies CXX as compiler plus architecture flags.
$CXX -std=c++20 -O2 -Wall -Wextra -Werror \
"$root/wifi_e310_link/host/e310_packet_agent.cpp" \
"$root/wifi_e310_link/host/e310_sifs_uio.cpp" -o "$out/gf_e310_packet_agent"
$CXX -std=c++20 -O2 -Wall -Wextra -Werror \
"$root/wifi_e310_link/host/e310_rx_capture.cpp" -o "$out/gf_e310_rx_capture"
printf 'Built ARM packet transport and RX capture helper in %s\n' "$out"
printf 'Use the matching SDK runtime locally; do not replace board system libraries.\n'
build-e310-fpga.ps1 · 19 lines
Download this file · Permanent section link
param([Parameter(Mandatory=$true)][string]$Vivado)
$ErrorActionPreference='Stop'
$root=$PSScriptRoot
$project=Join-Path $root 'wifi_e310_link'
$output=Join-Path $project 'build/release-compactmul'
if (-not (Test-Path -LiteralPath $Vivado -PathType Leaf)) { throw 'Supply the installed vivado.bat path.' }
New-Item -ItemType Directory -Force -Path $output | Out-Null
# Preserve the exact accepted generics. Separate Vivado invocations avoid
# carrying synthesis process state into implementation in Vivado 2026.1.
foreach ($phase in @('synth','route')) {
$resume=if ($phase -eq 'route') {'1'} else {'0'}
$args=@('-mode','batch','-nojournal','-log',"$output/$phase.log",'-source',"$project/tools/build_minimal_vivado.tcl",'-tclargs',
$project,'xc7z020clg484-3',$output,$phase,'1',$resume,'-','Pullnone',
'1','1','1','1','1','1','1','1','1','0','1','0','1')
& $Vivado @args
if ($LASTEXITCODE -ne 0) { throw "Vivado $phase failed: inspect $output/$phase.log" }
}
Write-Output "Build complete: $output/gf_e310_minimal.bit. Nothing was loaded onto hardware."
build-windows.cmd · 5 lines
Download this file · Permanent section link
@echo off
setlocal
call "%~dp0wifi_e310_link\build_windows_packet_host.cmd" %*
exit /b %errorlevel%
licenses/GPL-3.0.txt · 675 lines
Download this file · Permanent section link
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
licenses/LGPL-3.0.txt · 166 lines
Download this file · Permanent section link
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
page.html · 2 lines
Download this file · Permanent section link
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Greenforest RF AP</title><style>body{font:20px system-ui;max-width:36rem;margin:12vh auto;padding:1rem;background:#06131b;color:#dff}code{color:#7ff}</style><h1>A webpage through the radio</h1><p>The Greenforest C++ WPA2 access point served this HTML over its Wi-Fi air link. The HTTP endpoint is <code>192.168.44.1:80</code>.</p></html>
run-host.cmd · 14 lines
Download this file · Permanent section link
@echo off
setlocal
if "%~2"=="" (
echo Usage: run-host COM_PORT PASSPHRASE_FILE
echo Start the radio-side packet agent first; this command does not configure RF.
exit /b 2
)
set "GFPORT=%~1"
set "GFKEY=%~f2"
if not exist "%GFKEY%" exit /b 2
rem This is the real native executable, not a web server on the PC's network.
"%~dp0wifi_e310_link\build\windows-packets\gf_e310_windows_ap.exe" --run --port "%GFPORT%" --baud 460800 --passphrase-file "%GFKEY%" --page "%~dp0page.html" --beacon-tu 10 --seconds 0
exit /b %errorlevel%
tools/wifi_ap_portable_rt.hpp · 193 lines
Download this file · Permanent section link
#pragma once
#include "wifi_protocol.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace gf::rt {
using Clock = std::chrono::steady_clock;
using Mac = std::array<std::uint8_t, 6>;
constexpr Mac kBroadcast = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
inline std::string json_escape(std::string_view input) {
std::ostringstream output;
for (const unsigned char value : input) {
switch (value) {
case '\\': output << "\\\\"; break;
case '"': output << "\\\""; break;
case '\b': output << "\\b"; break;
case '\f': output << "\\f"; break;
case '\n': output << "\\n"; break;
case '\r': output << "\\r"; break;
case '\t': output << "\\t"; break;
default:
if (value < 0x20) {
output << "\\u" << std::hex << std::setfill('0')
<< std::setw(4) << static_cast<unsigned>(value)
<< std::dec;
} else {
output << static_cast<char>(value);
}
}
}
return output.str();
}
inline std::string quote(std::string_view value) {
return '"' + json_escape(value) + '"';
}
inline std::string mac_text(const Mac& mac) {
std::ostringstream output;
output << std::hex << std::setfill('0');
for (std::size_t index = 0; index < mac.size(); ++index) {
if (index != 0) output << ':';
output << std::setw(2) << static_cast<unsigned>(mac[index]);
}
return output.str();
}
inline std::uint16_t little_u16(const std::uint8_t* data) {
return static_cast<std::uint16_t>(data[0]) |
(static_cast<std::uint16_t>(data[1]) << 8);
}
inline void append_le16(std::vector<std::uint8_t>& output,
std::uint16_t value) {
output.push_back(static_cast<std::uint8_t>(value));
output.push_back(static_cast<std::uint8_t>(value >> 8));
}
inline void append_le32(std::vector<std::uint8_t>& output,
std::uint32_t value) {
for (int index = 0; index < 4; ++index)
output.push_back(static_cast<std::uint8_t>(value >> (8 * index)));
}
inline void append_be16(std::vector<std::uint8_t>& output,
std::uint16_t value) {
output.push_back(static_cast<std::uint8_t>(value >> 8));
output.push_back(static_cast<std::uint8_t>(value));
}
inline void append_mac(std::vector<std::uint8_t>& output, const Mac& mac) {
output.insert(output.end(), mac.begin(), mac.end());
}
inline std::uint32_t crc32_80211(const std::uint8_t* data,
std::size_t size) {
std::uint32_t crc = 0xffffffffu;
for (std::size_t index = 0; index < size; ++index) {
crc ^= data[index];
for (int bit = 0; bit < 8; ++bit) {
crc = (crc >> 1) ^
(0xedb88320u & static_cast<std::uint32_t>(-
static_cast<std::int32_t>(crc & 1u)));
}
}
return ~crc;
}
inline void append_fcs(std::vector<std::uint8_t>& frame) {
append_le32(frame, crc32_80211(frame.data(), frame.size()));
}
inline void append_management_header(std::vector<std::uint8_t>& frame,
std::uint16_t frame_control,
const Mac& destination,
const Mac& source,
const Mac& bssid,
std::uint16_t sequence) {
append_le16(frame, frame_control);
append_le16(frame, 0);
append_mac(frame, destination);
append_mac(frame, source);
append_mac(frame, bssid);
append_le16(frame,
static_cast<std::uint16_t>((sequence & 0x0fffu) << 4));
}
inline void append_common_ies(std::vector<std::uint8_t>& frame,
const std::string& ssid, int channel) {
frame.push_back(0);
frame.push_back(static_cast<std::uint8_t>(ssid.size()));
frame.insert(frame.end(), ssid.begin(), ssid.end());
frame.insert(frame.end(), {1, 4, 0x82, 0x84, 0x8b, 0x96});
frame.insert(frame.end(), {3, 1, static_cast<std::uint8_t>(channel)});
}
inline std::vector<std::uint8_t> make_association_request(
const std::string& ssid, const Mac& station, const Mac& bssid,
int channel, std::uint16_t sequence) {
std::vector<std::uint8_t> frame;
frame.reserve(96);
append_management_header(frame, 0x0000, bssid, station, bssid, sequence);
append_le16(frame, 0x0021);
append_le16(frame, 10);
append_common_ies(frame, ssid, channel);
append_fcs(frame);
return frame;
}
inline std::vector<std::uint8_t> make_authentication_request(
const Mac& station, const Mac& bssid, std::uint16_t sequence) {
std::vector<std::uint8_t> frame;
frame.reserve(34);
append_management_header(frame, 0x00b0, bssid, station, bssid, sequence);
append_le16(frame, 0);
append_le16(frame, 1);
append_le16(frame, 0);
append_fcs(frame);
return frame;
}
inline std::uint16_t internet_checksum(const std::uint8_t* data,
std::size_t size,
std::uint32_t sum = 0) {
std::size_t index = 0;
while (index + 1 < size) {
sum += (static_cast<std::uint16_t>(data[index]) << 8) |
data[index + 1];
index += 2;
}
if (index < size)
sum += static_cast<std::uint16_t>(data[index]) << 8;
while (sum >> 16) sum = (sum & 0xffffu) + (sum >> 16);
return static_cast<std::uint16_t>(~sum);
}
inline std::vector<std::uint8_t> make_data_frame(
const Mac& station, const Mac& bssid, const Mac& destination,
std::uint16_t ether_type, const std::vector<std::uint8_t>& payload,
std::uint16_t sequence) {
std::vector<std::uint8_t> frame;
frame.reserve(40 + payload.size());
append_le16(frame, 0x0108);
append_le16(frame, 0);
append_mac(frame, bssid);
append_mac(frame, station);
append_mac(frame, destination);
append_le16(frame,
static_cast<std::uint16_t>((sequence & 0x0fffu) << 4));
frame.insert(frame.end(), {0xaa, 0xaa, 0x03, 0x00, 0x00, 0x00});
append_be16(frame, ether_type);
frame.insert(frame.end(), payload.begin(), payload.end());
append_fcs(frame);
return frame;
}
} // namespace gf::rt
tools/wifi_dsss_tx.hpp · 268 lines
Download this file · Permanent section link
#pragma once
// Portable long-preamble IEEE 802.11 1 Mb/s DSSS waveform formatter.
// This is host-side packet formatting only: it consumes a complete PSDU,
// including FCS, and returns interleaved signed IQ16 at the requested rate.
#include <algorithm>
#include <array>
#include <cmath>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <numeric>
#include <stdexcept>
#include <vector>
namespace gf::dsss_tx {
using Complex = std::complex<double>;
inline constexpr double kPi = 3.1415926535897932384626433832795;
inline constexpr std::int64_t kDefaultSampleRate = 20'000'000;
inline constexpr double kChipRate = 11'000'000.0;
inline constexpr std::array<int, 11> kBarker = {
1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1};
inline constexpr std::array<std::uint8_t, 16> kLongSfd = {
0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1};
inline void append_le16(std::vector<std::uint8_t>& output,
std::uint16_t value) {
output.push_back(static_cast<std::uint8_t>(value));
output.push_back(static_cast<std::uint8_t>(value >> 8));
}
inline std::uint16_t crc16_plcp(const std::uint8_t* data,
std::size_t size) {
std::uint16_t crc = 0xffffu;
for (std::size_t index = 0; index < size; ++index) {
for (int bit = 0; bit < 8; ++bit) {
const bool mix = ((crc ^ (data[index] >> bit)) & 1u) != 0;
crc >>= 1;
if (mix) crc ^= 0x8408u;
}
}
return static_cast<std::uint16_t>(crc ^ 0xffffu);
}
inline std::vector<std::uint8_t> lsb_bits(
const std::vector<std::uint8_t>& bytes) {
std::vector<std::uint8_t> bits;
bits.reserve(bytes.size() * 8);
for (const auto value : bytes) {
for (int bit = 0; bit < 8; ++bit)
bits.push_back(static_cast<std::uint8_t>((value >> bit) & 1u));
}
return bits;
}
inline std::vector<std::uint8_t> scramble(
const std::vector<std::uint8_t>& bits, std::uint8_t state = 0x5d) {
std::vector<std::uint8_t> output;
output.reserve(bits.size());
for (const auto input : bits) {
const auto transmitted = static_cast<std::uint8_t>(
(input & 1u) ^ ((state >> 3) & 1u) ^ ((state >> 6) & 1u));
output.push_back(transmitted);
state = static_cast<std::uint8_t>(
((state << 1) | transmitted) & 0x7fu);
}
return output;
}
inline std::vector<Complex> make_long_1mbps_chips(
const std::vector<std::uint8_t>& psdu) {
if (psdu.empty() || psdu.size() > 4095)
throw std::runtime_error(
"DSSS PSDU length is outside 1..4095 bytes");
const auto duration_us = static_cast<std::uint16_t>(psdu.size() * 8);
std::vector<std::uint8_t> header = {
0x0a, 0x00, static_cast<std::uint8_t>(duration_us),
static_cast<std::uint8_t>(duration_us >> 8)};
append_le16(header, crc16_plcp(header.data(), header.size()));
std::vector<std::uint8_t> plain;
plain.insert(plain.end(), 128, 1);
plain.insert(plain.end(), kLongSfd.begin(), kLongSfd.end());
const auto header_bits = lsb_bits(header);
plain.insert(plain.end(), header_bits.begin(), header_bits.end());
const auto payload_bits = lsb_bits(psdu);
plain.insert(plain.end(), payload_bits.begin(), payload_bits.end());
const auto scrambled = scramble(plain);
std::vector<Complex> chips;
chips.reserve(scrambled.size() * kBarker.size());
Complex carrier{1.0, 0.0};
for (const auto bit : scrambled) {
if (bit != 0) carrier = -carrier;
for (const int chip : kBarker)
chips.push_back(carrier * static_cast<double>(chip));
}
return chips;
}
inline std::vector<Complex> sinc_resample(
const std::vector<Complex>& input, double input_rate,
double output_rate, int half_taps = 24) {
if (input_rate <= 0.0 || output_rate <= 0.0 || half_taps < 1)
throw std::runtime_error("invalid DSSS resampler configuration");
const auto input_hz = static_cast<std::int64_t>(std::llround(input_rate));
const auto output_hz = static_cast<std::int64_t>(std::llround(output_rate));
const bool integral_rates = input_hz > 0 && output_hz > 0 &&
std::abs(input_rate - static_cast<double>(input_hz)) < 0.5 &&
std::abs(output_rate - static_cast<double>(output_hz)) < 0.5;
const auto divisor = integral_rates ? std::gcd(input_hz, output_hz) : 1;
const auto phase_count = integral_rates ? output_hz / divisor : 0;
if (integral_rates && phase_count > 0 && phase_count <= 1024) {
struct RationalCache {
std::int64_t input_hz = 0;
std::int64_t output_hz = 0;
int half_taps = 0;
std::int64_t phase_count = 0;
std::int64_t advance = 0;
std::vector<double> weights;
};
static thread_local RationalCache cache;
const auto tap_count = static_cast<std::size_t>(half_taps * 2);
const int first_tap = -half_taps + 1;
if (cache.input_hz != input_hz ||
cache.output_hz != output_hz ||
cache.half_taps != half_taps) {
cache.input_hz = input_hz;
cache.output_hz = output_hz;
cache.half_taps = half_taps;
cache.phase_count = phase_count;
cache.advance = input_hz / divisor;
cache.weights.assign(
static_cast<std::size_t>(phase_count) * tap_count, 0.0);
const double cutoff =
0.94 * std::min(1.0, output_rate / input_rate);
for (std::int64_t phase = 0; phase < phase_count; ++phase) {
const double fraction = static_cast<double>(phase) /
static_cast<double>(phase_count);
for (std::size_t index = 0; index < tap_count; ++index) {
const int tap = first_tap + static_cast<int>(index);
const double distance =
fraction - static_cast<double>(tap);
const double window_position = distance / half_taps;
if (std::abs(window_position) >= 1.0) continue;
const double x = cutoff * distance;
const double sinc = std::abs(x) < 1e-12
? 1.0 : std::sin(kPi * x) / (kPi * x);
const double window =
0.42 + 0.5 * std::cos(kPi * window_position) +
0.08 * std::cos(2.0 * kPi * window_position);
cache.weights[
static_cast<std::size_t>(phase) * tap_count + index] =
cutoff * sinc * window;
}
}
}
const auto output_size = static_cast<std::size_t>(
static_cast<unsigned long long>(input.size()) *
static_cast<unsigned long long>(output_hz) /
static_cast<unsigned long long>(input_hz));
std::vector<Complex> output(output_size);
std::uint64_t position_numerator = 0;
for (std::size_t out = 0; out < output.size(); ++out) {
const auto center = static_cast<std::ptrdiff_t>(
position_numerator /
static_cast<std::uint64_t>(cache.phase_count));
const auto phase = static_cast<std::size_t>(
position_numerator %
static_cast<std::uint64_t>(cache.phase_count));
position_numerator += static_cast<std::uint64_t>(cache.advance);
Complex sum{};
double weight_sum = 0.0;
for (std::size_t index = 0; index < tap_count; ++index) {
const auto source = center + first_tap +
static_cast<std::ptrdiff_t>(index);
if (source < 0 ||
source >= static_cast<std::ptrdiff_t>(input.size()))
continue;
const double weight =
cache.weights[phase * tap_count + index];
sum += input[static_cast<std::size_t>(source)] * weight;
weight_sum += weight;
}
output[out] = weight_sum != 0.0 ? sum / weight_sum : Complex{};
}
return output;
}
const auto output_size = static_cast<std::size_t>(
std::floor(input.size() * output_rate / input_rate));
std::vector<Complex> output(output_size);
const double cutoff = 0.94 * std::min(1.0, output_rate / input_rate);
for (std::size_t out = 0; out < output.size(); ++out) {
const double position = out * input_rate / output_rate;
const auto center =
static_cast<std::ptrdiff_t>(std::floor(position));
Complex sum{};
double weight_sum = 0.0;
for (int tap = -half_taps + 1; tap <= half_taps; ++tap) {
const auto source = center + tap;
if (source < 0 ||
source >= static_cast<std::ptrdiff_t>(input.size()))
continue;
const double distance = position - static_cast<double>(source);
const double x = cutoff * distance;
const double sinc = std::abs(x) < 1e-12
? 1.0 : std::sin(kPi * x) / (kPi * x);
const double window_position = distance / half_taps;
if (std::abs(window_position) >= 1.0) continue;
const double window =
0.42 + 0.5 * std::cos(kPi * window_position) +
0.08 * std::cos(2.0 * kPi * window_position);
const double weight = cutoff * sinc * window;
sum += input[static_cast<std::size_t>(source)] * weight;
weight_sum += weight;
}
output[out] = weight_sum != 0.0 ? sum / weight_sum : Complex{};
}
return output;
}
inline std::vector<std::int16_t> make_waveform(
const std::vector<std::uint8_t>& psdu, double lead_ms = 1.0,
double tail_ms = 2.0, double amplitude = 0.25,
std::size_t guard_chips = 32,
std::int64_t output_sample_rate = kDefaultSampleRate) {
if (output_sample_rate <= 0)
throw std::runtime_error("waveform sample rate must be positive");
if (!(amplitude > 0.0 && amplitude < 1.0))
throw std::runtime_error("waveform amplitude must be between 0 and 1");
if (lead_ms < 0.0 || tail_ms < 0.0)
throw std::runtime_error("waveform guards must be nonnegative");
auto chips = make_long_1mbps_chips(psdu);
std::vector<Complex> padded(guard_chips, Complex{});
padded.insert(padded.end(), chips.begin(), chips.end());
padded.insert(padded.end(), guard_chips, Complex{});
const auto packet = sinc_resample(
padded, kChipRate, static_cast<double>(output_sample_rate));
const auto lead = static_cast<std::size_t>(
std::llround(lead_ms * output_sample_rate / 1000.0));
const auto tail = static_cast<std::size_t>(
std::llround(tail_ms * output_sample_rate / 1000.0));
std::vector<std::int16_t> output(
(lead + packet.size() + tail) * 2, 0);
const auto quantize = [amplitude](double sample) {
const auto scaled =
std::clamp(sample * amplitude, -0.999969, 0.999969);
return static_cast<std::int16_t>(
std::llround(scaled * 32767.0));
};
for (std::size_t index = 0; index < packet.size(); ++index) {
output[(lead + index) * 2] = quantize(packet[index].real());
output[(lead + index) * 2 + 1] = quantize(packet[index].imag());
}
return output;
}
} // namespace gf::dsss_tx
tools/wifi_protocol.cpp · 2668 lines
Download this file · Permanent section link
#define NOMINMAX
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#endif
#include "wifi_protocol.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cctype>
#include <cstdint>
#include <cstring>
#include <deque>
#include <fstream>
#include <iomanip>
#include <initializer_list>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
namespace gf::wifi {
namespace {
using Bytes = std::vector<std::uint8_t>;
using Mac = std::array<std::uint8_t,6>;
using Nonce = std::array<std::uint8_t,32>;
using Key16 = std::array<std::uint8_t,16>;
using Key32 = std::array<std::uint8_t,32>;
using Key48 = std::array<std::uint8_t,48>;
constexpr std::size_t kMaximumHandshakeFileBytes = 512 * 1024;
constexpr std::size_t kMaximumPairStates = 64;
constexpr std::size_t kMaximumCandidatesPerType = 8;
std::uint16_t be16(const std::uint8_t* data) {
return static_cast<std::uint16_t>(
(static_cast<std::uint16_t>(data[0]) << 8) | data[1]);
}
std::uint32_t be32(const std::uint8_t* data) {
return (static_cast<std::uint32_t>(data[0]) << 24) |
(static_cast<std::uint32_t>(data[1]) << 16) |
(static_cast<std::uint32_t>(data[2]) << 8) |
static_cast<std::uint32_t>(data[3]);
}
std::uint64_t be64(const std::uint8_t* data) {
std::uint64_t value = 0;
for (int index = 0; index < 8; ++index)
value = (value << 8) | data[index];
return value;
}
std::uint16_t le16(const std::uint8_t* data) {
return static_cast<std::uint16_t>(data[0]) |
(static_cast<std::uint16_t>(data[1]) << 8);
}
void put_be16(std::uint8_t* data, std::uint16_t value) {
data[0] = static_cast<std::uint8_t>(value >> 8);
data[1] = static_cast<std::uint8_t>(value);
}
std::string trim(std::string value) {
while (!value.empty() &&
std::isspace(static_cast<unsigned char>(value.back())))
value.pop_back();
std::size_t first = 0;
while (first < value.size() &&
std::isspace(static_cast<unsigned char>(value[first])))
++first;
return value.substr(first);
}
std::string hex(const std::uint8_t* data, std::size_t size) {
std::ostringstream out;
out << std::hex << std::setfill('0');
for (std::size_t index = 0; index < size; ++index)
out << std::setw(2) << static_cast<unsigned>(data[index]);
return out.str();
}
std::string hex_prefixed(std::uint64_t value, int width) {
std::ostringstream out;
out << "0x" << std::hex << std::setfill('0') << std::setw(width) << value;
return out.str();
}
std::string tcp_flag_names(std::uint16_t flags) {
static constexpr std::array<std::pair<std::uint16_t,const char*>,9> names = {{
{0x100, "NS"}, {0x080, "CWR"}, {0x040, "ECE"},
{0x020, "URG"}, {0x010, "ACK"}, {0x008, "PSH"},
{0x004, "RST"}, {0x002, "SYN"}, {0x001, "FIN"}}};
std::ostringstream out;
bool first = true;
for (const auto& [bit, name] : names) {
if ((flags & bit) == 0) continue;
if (!first) out << ',';
out << name;
first = false;
}
return first ? "none" : out.str();
}
template <std::size_t N>
std::string hex(const std::array<std::uint8_t,N>& data) {
return hex(data.data(), data.size());
}
int hex_nibble(char character) {
if (character >= '0' && character <= '9') return character - '0';
if (character >= 'a' && character <= 'f') return character - 'a' + 10;
if (character >= 'A' && character <= 'F') return character - 'A' + 10;
return -1;
}
Bytes unhex(std::string_view text) {
if ((text.size() & 1u) != 0)
throw std::runtime_error("odd-length hexadecimal field");
Bytes output(text.size() / 2);
for (std::size_t index = 0; index < output.size(); ++index) {
const int high = hex_nibble(text[index * 2]);
const int low = hex_nibble(text[index * 2 + 1]);
if (high < 0 || low < 0)
throw std::runtime_error("non-hexadecimal field");
output[index] = static_cast<std::uint8_t>((high << 4) | low);
}
return output;
}
template <std::size_t N>
std::array<std::uint8_t,N> unhex_array(std::string_view text) {
const auto decoded = unhex(text);
if (decoded.size() != N)
throw std::runtime_error("wrong hexadecimal field length");
std::array<std::uint8_t,N> output{};
std::copy(decoded.begin(), decoded.end(), output.begin());
return output;
}
std::string mac_string(const std::uint8_t* data) {
std::ostringstream out;
out << std::hex << std::setfill('0');
for (int index = 0; index < 6; ++index) {
if (index) out << ':';
out << std::setw(2) << static_cast<unsigned>(data[index]);
}
return out.str();
}
std::string mac_string(const Mac& mac) {
return mac_string(mac.data());
}
std::string canonical_mac(std::string value) {
value = trim(std::move(value));
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char character) {
if (character == '-') return ':';
return static_cast<char>(std::tolower(character));
});
if (value.size() != 17) return {};
for (std::size_t index = 0; index < value.size(); ++index) {
if (index % 3 == 2) {
if (value[index] != ':') return {};
} else if (!std::isxdigit(static_cast<unsigned char>(value[index]))) {
return {};
}
}
return value;
}
Mac parse_mac_string(const std::string& value) {
const auto canonical = canonical_mac(value);
if (canonical.empty()) throw std::runtime_error("invalid MAC address: " + value);
Mac output{};
for (std::size_t index = 0; index < output.size(); ++index) {
const int high = hex_nibble(canonical[index * 3]);
const int low = hex_nibble(canonical[index * 3 + 1]);
output[index] = static_cast<std::uint8_t>((high << 4) | low);
}
return output;
}
std::string ipv4(const std::uint8_t* data) {
std::ostringstream out;
out << static_cast<unsigned>(data[0]) << '.'
<< static_cast<unsigned>(data[1]) << '.'
<< static_cast<unsigned>(data[2]) << '.'
<< static_cast<unsigned>(data[3]);
return out.str();
}
std::string ipv6(const std::uint8_t* data) {
std::ostringstream out;
out << std::hex;
for (int index = 0; index < 8; ++index) {
if (index) out << ':';
out << be16(data + index * 2);
}
return out.str();
}
std::string safe_ascii(const std::uint8_t* data, std::size_t size,
std::size_t maximum = 128) {
std::string output;
const std::size_t count = std::min(size, maximum);
for (std::size_t index = 0; index < count; ++index) {
const std::uint8_t value = data[index];
if (value == 0) break;
output.push_back(value >= 0x20 && value <= 0x7e
? static_cast<char>(value) : '?');
}
return output;
}
void set_payload_preview(ProtocolInfo& result, const std::uint8_t* data,
std::size_t size) {
constexpr std::size_t maximum = 64;
const std::size_t shown = std::min(size, maximum);
result.payload_bytes = size;
result.payload_hex = hex(data, shown);
result.payload_ascii.clear();
result.payload_ascii.reserve(shown);
for (std::size_t index = 0; index < shown; ++index) {
const std::uint8_t value = data[index];
result.payload_ascii.push_back(
value >= 0x20 && value <= 0x7e ? static_cast<char>(value) : '.');
}
result.payload_truncated = size > maximum;
}
std::vector<std::string> split_tabs(const std::string& line) {
std::vector<std::string> output;
std::size_t first = 0;
for (;;) {
const auto tab = line.find('\t', first);
output.push_back(line.substr(first, tab == std::string::npos
? std::string::npos
: tab - first));
if (tab == std::string::npos) break;
first = tab + 1;
}
return output;
}
std::uint32_t checksum_sum(const std::uint8_t* data, std::size_t size,
std::uint32_t sum = 0) {
std::size_t index = 0;
while (index + 1 < size) {
sum += be16(data + index);
index += 2;
}
if (index < size) sum += static_cast<std::uint16_t>(data[index] << 8);
while (sum >> 16) sum = (sum & 0xffffu) + (sum >> 16);
return sum;
}
bool checksum_valid(const std::uint8_t* data, std::size_t size,
std::uint32_t initial = 0) {
return checksum_sum(data, size, initial) == 0xffffu;
}
std::uint16_t checksum_create(const std::uint8_t* data, std::size_t size,
std::uint32_t initial = 0) {
return static_cast<std::uint16_t>(~checksum_sum(data, size, initial));
}
std::uint64_t fnv1a64(const Bytes& bytes) {
std::uint64_t hash = 1469598103934665603ull;
for (const auto value : bytes) {
hash ^= value;
hash *= 1099511628211ull;
}
return hash;
}
std::string dhcp_message_name(int type) {
static const std::array<const char*,9> names = {
"UNKNOWN", "DISCOVER", "OFFER", "REQUEST", "DECLINE",
"ACK", "NAK", "RELEASE", "INFORM"};
if (type < 1 || type >= static_cast<int>(names.size())) return "UNKNOWN";
return names[static_cast<std::size_t>(type)];
}
void parse_dhcp_options(const std::uint8_t* data, std::size_t size,
DhcpObservation& observation, int& message_type,
int& overload) {
std::size_t offset = 0;
while (offset < size) {
const std::uint8_t code = data[offset++];
if (code == 0) continue;
if (code == 255) break;
if (offset >= size) break;
const std::size_t length = data[offset++];
if (offset + length > size) break;
const auto* value = data + offset;
if (code == 53 && length == 1) {
message_type = value[0];
} else if (code == 12) {
observation.host_name = safe_ascii(value, length);
} else if (code == 50 && length == 4) {
observation.requested_ipv4 = ipv4(value);
} else if (code == 54 && length == 4) {
observation.server_ipv4 = ipv4(value);
} else if (code == 60) {
observation.vendor_class = safe_ascii(value, length);
} else if (code == 52 && length == 1) {
overload = value[0];
} else if (code == 81 && length >= 3) {
observation.fqdn = safe_ascii(value + 3, length - 3);
}
offset += length;
}
}
std::optional<DhcpObservation> parse_dhcp(const std::uint8_t* data,
std::size_t size) {
if (size < 240 || std::memcmp(data + 236, "\x63\x82\x53\x63", 4) != 0)
return std::nullopt;
if (data[1] != 1 || data[2] != 6) return std::nullopt;
DhcpObservation result;
result.transaction_id = be32(data + 4);
result.client_mac = mac_string(data + 28);
if (std::any_of(data + 12, data + 16,
[](std::uint8_t value) { return value != 0; }))
result.client_ipv4 = ipv4(data + 12);
int message_type = 0;
int overload = 0;
parse_dhcp_options(data + 240, size - 240, result, message_type, overload);
if ((overload & 1) != 0)
parse_dhcp_options(data + 108, 128, result, message_type, overload);
if ((overload & 2) != 0)
parse_dhcp_options(data + 44, 64, result, message_type, overload);
result.message_type = dhcp_message_name(message_type);
if ((message_type == 2 || message_type == 5) &&
std::any_of(data + 16, data + 20,
[](std::uint8_t value) { return value != 0; }))
result.offered_ipv4 = ipv4(data + 16);
return result;
}
struct DnsNameResult {
std::string name;
std::size_t next = 0;
};
std::optional<DnsNameResult> dns_name(const std::uint8_t* data, std::size_t size,
std::size_t offset, int depth = 0) {
if (depth > 8 || offset >= size) return std::nullopt;
std::string output;
std::size_t cursor = offset;
std::size_t next = offset;
bool jumped = false;
std::size_t labels = 0;
while (cursor < size && labels++ < 64) {
const std::uint8_t length = data[cursor++];
if ((length & 0xc0u) == 0xc0u) {
if (cursor >= size) return std::nullopt;
const std::size_t pointer =
(static_cast<std::size_t>(length & 0x3fu) << 8) | data[cursor++];
if (!jumped) next = cursor;
jumped = true;
const auto suffix = dns_name(data, size, pointer, depth + 1);
if (!suffix) return std::nullopt;
if (!output.empty() && !suffix->name.empty()) output.push_back('.');
output += suffix->name;
break;
}
if (length == 0) {
if (!jumped) next = cursor;
return DnsNameResult{output, next};
}
if (length > 63 || cursor + length > size) return std::nullopt;
if (!output.empty()) output.push_back('.');
output += safe_ascii(data + cursor, length, 63);
cursor += length;
if (!jumped) next = cursor;
if (output.size() > 255) return std::nullopt;
}
if (jumped) return DnsNameResult{output, next};
return std::nullopt;
}
std::string parse_dns_summary(const std::uint8_t* data, std::size_t size,
bool mdns) {
std::ostringstream out;
out << (mdns ? "mDNS" : "DNS");
if (size < 12) return out.str() + " truncated";
const std::uint16_t flags = be16(data + 2);
const std::uint16_t questions = be16(data + 4);
const std::uint16_t answers = be16(data + 6);
out << ((flags & 0x8000u) ? " response" : " query")
<< " q=" << questions << " a=" << answers;
std::size_t offset = 12;
for (std::size_t index = 0; index < std::min<std::size_t>(questions, 4); ++index) {
const auto parsed = dns_name(data, size, offset);
if (!parsed || parsed->next + 4 > size) break;
if (!parsed->name.empty()) out << (index == 0 ? " name=" : ",") << parsed->name;
offset = parsed->next + 4;
}
return out.str();
}
void parse_transport_ipv4(ProtocolInfo& result, std::uint8_t protocol,
const std::uint8_t* payload, std::size_t size,
const std::uint8_t* source, const std::uint8_t* destination,
bool complete_datagram, std::ostringstream& summary) {
std::uint32_t pseudo_base = checksum_sum(source, 4);
pseudo_base = checksum_sum(destination, 4, pseudo_base);
if (protocol == 17) {
result.transport = "UDP";
if (size < 8) {
result.checksum_status += " UDP=truncated";
result.headers.push_back("UDP truncated captured_bytes=" +
std::to_string(size));
summary << " UDP truncated";
return;
}
const std::size_t udp_length = be16(payload + 4);
result.source_port = be16(payload);
result.destination_port = be16(payload + 2);
summary << " UDP " << result.source_port << "->" << result.destination_port;
std::string checksum_state;
if (udp_length < 8 || udp_length > size) {
result.checksum_status += " UDP=truncated";
checksum_state = "truncated";
std::ostringstream header;
header << "UDP src_port=" << result.source_port
<< " dst_port=" << result.destination_port
<< " length=" << udp_length
<< " checksum=" << hex_prefixed(be16(payload + 6), 4)
<< " checksum_status=" << checksum_state
<< " raw_header=" << hex(payload, 8);
result.headers.push_back(header.str());
return;
}
const std::array<std::uint8_t,4> pseudo_tail = {
0, protocol, static_cast<std::uint8_t>(udp_length >> 8),
static_cast<std::uint8_t>(udp_length)};
const std::uint32_t pseudo = checksum_sum(
pseudo_tail.data(), pseudo_tail.size(), pseudo_base);
const std::uint16_t received = be16(payload + 6);
if (received == 0) {
result.checksum_status += " UDP=not-present";
checksum_state = "not-present";
} else if (complete_datagram) {
const bool valid = checksum_valid(payload, udp_length, pseudo);
result.checksum_status += valid ? " UDP=valid" : " UDP=INVALID";
checksum_state = valid ? "valid" : "INVALID";
} else {
result.checksum_status += " UDP=unverified-fragment";
checksum_state = "unverified-fragment";
}
std::ostringstream header;
header << "UDP src_port=" << result.source_port
<< " dst_port=" << result.destination_port
<< " length=" << udp_length
<< " checksum=" << hex_prefixed(received, 4)
<< " checksum_status=" << checksum_state
<< " raw_header=" << hex(payload, 8);
result.headers.push_back(header.str());
const auto* body = payload + 8;
const std::size_t body_size = udp_length - 8;
set_payload_preview(result, body, body_size);
if ((result.source_port == 67 || result.source_port == 68 ||
result.destination_port == 67 || result.destination_port == 68)) {
const auto dhcp = parse_dhcp(body, body_size);
if (dhcp) {
result.application = "DHCP";
result.dhcp.push_back(*dhcp);
summary << " DHCP " << dhcp->message_type
<< " client=" << dhcp->client_mac;
if (!dhcp->host_name.empty()) summary << " host=" << dhcp->host_name;
if (!dhcp->fqdn.empty()) summary << " fqdn=" << dhcp->fqdn;
if (!dhcp->offered_ipv4.empty()) summary << " yiaddr=" << dhcp->offered_ipv4;
if (!dhcp->requested_ipv4.empty()) summary << " requested=" << dhcp->requested_ipv4;
}
} else if (result.source_port == 53 || result.destination_port == 53 ||
result.source_port == 5353 || result.destination_port == 5353) {
const bool mdns = result.source_port == 5353 || result.destination_port == 5353;
result.application = mdns ? "mDNS" : "DNS";
summary << ' ' << parse_dns_summary(body, body_size, mdns);
} else if (result.source_port == 1900 || result.destination_port == 1900) {
result.application = "SSDP";
const auto line_end = std::find(body, body + body_size,
static_cast<std::uint8_t>('\n'));
summary << " SSDP " << safe_ascii(body,
static_cast<std::size_t>(line_end - body), 120);
}
} else if (protocol == 6) {
result.transport = "TCP";
if (size < 20) {
result.checksum_status += " TCP=truncated";
result.headers.push_back("TCP truncated captured_bytes=" +
std::to_string(size));
summary << " TCP truncated";
return;
}
const std::array<std::uint8_t,4> pseudo_tail = {
0, protocol, static_cast<std::uint8_t>(size >> 8),
static_cast<std::uint8_t>(size)};
const std::uint32_t pseudo = checksum_sum(
pseudo_tail.data(), pseudo_tail.size(), pseudo_base);
result.source_port = be16(payload);
result.destination_port = be16(payload + 2);
const std::size_t header = static_cast<std::size_t>(payload[12] >> 4) * 4;
const std::uint16_t flags = static_cast<std::uint16_t>(
((payload[12] & 1u) << 8) | payload[13]);
summary << " TCP " << result.source_port << "->" << result.destination_port
<< " flags=" << tcp_flag_names(flags);
std::string checksum_state;
if (header < 20 || header > size) {
result.checksum_status += " TCP=truncated";
checksum_state = "invalid-data-offset";
} else if (complete_datagram) {
const bool valid = checksum_valid(payload, size, pseudo);
result.checksum_status += valid ? " TCP=valid" : " TCP=INVALID";
checksum_state = valid ? "valid" : "INVALID";
} else {
result.checksum_status += " TCP=unverified-fragment";
checksum_state = "unverified-fragment";
}
const std::size_t available_header = std::min(header, size);
std::ostringstream decoded;
decoded << "TCP src_port=" << result.source_port
<< " dst_port=" << result.destination_port
<< " sequence=" << be32(payload + 4)
<< " acknowledgment=" << be32(payload + 8)
<< " data_offset_bytes=" << header
<< " reserved=" << static_cast<unsigned>((payload[12] >> 1) & 7u)
<< " flags=" << hex_prefixed(flags, 3)
<< " flags_named=" << tcp_flag_names(flags)
<< " window=" << be16(payload + 14)
<< " checksum=" << hex_prefixed(be16(payload + 16), 4)
<< " checksum_status=" << checksum_state
<< " urgent_pointer=" << be16(payload + 18);
if (available_header > 20)
decoded << " options=" << hex(payload + 20, available_header - 20);
decoded << " raw_header=" << hex(payload, available_header);
result.headers.push_back(decoded.str());
if (header >= 20 && header <= size)
set_payload_preview(result, payload + header, size - header);
} else if (protocol == 1) {
result.transport = "ICMP";
if (size < 4) {
result.checksum_status += " ICMP=truncated";
result.headers.push_back("ICMP truncated captured_bytes=" +
std::to_string(size));
summary << " ICMP truncated";
return;
}
summary << " ICMP type=" << static_cast<unsigned>(payload[0])
<< " code=" << static_cast<unsigned>(payload[1]);
std::string checksum_state = "unverified-fragment";
if (complete_datagram) {
const bool valid = checksum_valid(payload, size);
result.checksum_status += valid ? " ICMP=valid" : " ICMP=INVALID";
checksum_state = valid ? "valid" : "INVALID";
} else {
result.checksum_status += " ICMP=unverified-fragment";
}
std::ostringstream header;
header << "ICMP type=" << static_cast<unsigned>(payload[0])
<< " code=" << static_cast<unsigned>(payload[1])
<< " checksum=" << hex_prefixed(be16(payload + 2), 4)
<< " checksum_status=" << checksum_state
<< " rest_of_header=" << (size >= 8 ? hex(payload + 4, 4) : "truncated")
<< " raw_header=" << hex(payload, std::min<std::size_t>(size, 8));
result.headers.push_back(header.str());
if (size > 8) set_payload_preview(result, payload + 8, size - 8);
} else {
result.transport = "IP protocol " + std::to_string(protocol);
summary << " proto=" << static_cast<unsigned>(protocol);
result.headers.push_back("IPv4 payload protocol=" +
std::to_string(protocol) +
" transport_header_not_decoded");
set_payload_preview(result, payload, size);
}
}
void parse_transport_ipv6(ProtocolInfo& result, std::uint8_t next_header,
const std::uint8_t* payload, std::size_t size,
const std::uint8_t* source, const std::uint8_t* destination,
std::ostringstream& summary) {
std::size_t offset = 0;
bool fragmented = false;
for (int depth = 0; depth < 8; ++depth) {
if (next_header == 0 || next_header == 43 || next_header == 60) {
const std::uint8_t extension_type = next_header;
if (offset + 2 > size) {
result.headers.push_back("IPv6 extension type=" +
std::to_string(extension_type) +
" truncated");
return;
}
const std::uint8_t following = payload[offset];
const std::size_t length = (static_cast<std::size_t>(payload[offset + 1]) + 1) * 8;
if (offset + length > size) {
result.headers.push_back("IPv6 extension type=" +
std::to_string(extension_type) +
" declared_bytes=" + std::to_string(length) +
" captured_bytes=" +
std::to_string(size - offset) + " truncated");
return;
}
const char* name = extension_type == 0 ? "hop-by-hop" :
extension_type == 43 ? "routing" : "destination";
std::ostringstream extension;
extension << "IPv6 extension name=" << name
<< " type=" << static_cast<unsigned>(extension_type)
<< " next_header=" << static_cast<unsigned>(following)
<< " length_bytes=" << length
<< " raw_header=" << hex(payload + offset, length);
result.headers.push_back(extension.str());
next_header = following;
offset += length;
} else if (next_header == 44) {
if (offset + 8 > size) {
result.headers.push_back("IPv6 fragment header truncated");
return;
}
const std::uint16_t fragment = be16(payload + offset + 2);
fragmented = (fragment & 0xfff9u) != 0;
std::ostringstream extension;
extension << "IPv6 extension name=fragment type=44"
<< " next_header=" << static_cast<unsigned>(payload[offset])
<< " reserved=" << static_cast<unsigned>(payload[offset + 1])
<< " fragment_offset_units=" << (fragment >> 3)
<< " fragment_offset_bytes=" << (fragment >> 3) * 8
<< " more_fragments=" << (fragment & 1u)
<< " identification=" << hex_prefixed(be32(payload + offset + 4), 8)
<< " raw_header=" << hex(payload + offset, 8);
result.headers.push_back(extension.str());
next_header = payload[offset];
offset += 8;
} else if (next_header == 51) {
if (offset + 2 > size) {
result.headers.push_back("IPv6 AH header truncated");
return;
}
const std::uint8_t following = payload[offset];
const std::size_t length = (static_cast<std::size_t>(payload[offset + 1]) + 2) * 4;
if (offset + length > size) {
result.headers.push_back("IPv6 AH declared_bytes=" +
std::to_string(length) +
" captured_bytes=" +
std::to_string(size - offset) + " truncated");
return;
}
std::ostringstream extension;
extension << "IPv6 extension name=AH type=51"
<< " next_header=" << static_cast<unsigned>(following)
<< " payload_len_units="
<< static_cast<unsigned>(payload[offset + 1])
<< " length_bytes=" << length;
if (length >= 12) {
extension << " spi=" << hex_prefixed(be32(payload + offset + 4), 8)
<< " sequence=" << be32(payload + offset + 8);
}
extension << " raw_header=" << hex(payload + offset, length);
result.headers.push_back(extension.str());
next_header = following;
offset += length;
} else {
break;
}
}
if (offset > size) return;
const auto* transport = payload + offset;
const std::size_t transport_size = size - offset;
if (next_header == 17 || next_header == 6 || next_header == 58) {
std::uint32_t pseudo_base = checksum_sum(source, 16);
pseudo_base = checksum_sum(destination, 16, pseudo_base);
const auto pseudo_for = [&](std::size_t length) {
const std::array<std::uint8_t,8> tail = {
static_cast<std::uint8_t>(length >> 24),
static_cast<std::uint8_t>(length >> 16),
static_cast<std::uint8_t>(length >> 8),
static_cast<std::uint8_t>(length), 0, 0, 0, next_header};
return checksum_sum(tail.data(), tail.size(), pseudo_base);
};
if (next_header == 17) {
result.transport = "UDP";
if (transport_size < 8) {
result.checksum_status += " UDP=truncated";
result.headers.push_back("UDP truncated captured_bytes=" +
std::to_string(transport_size));
summary << " UDP truncated";
return;
}
const std::size_t length = be16(transport + 4);
result.source_port = be16(transport);
result.destination_port = be16(transport + 2);
summary << " UDP " << result.source_port << "->" << result.destination_port;
std::string checksum_state;
if (length < 8 || length > transport_size) {
result.checksum_status += " UDP=truncated";
checksum_state = "truncated";
} else if (fragmented) {
result.checksum_status += " UDP=unverified-fragment";
checksum_state = "unverified-fragment";
} else {
const bool valid = checksum_valid(transport, length,
pseudo_for(length));
result.checksum_status += valid ? " UDP=valid" : " UDP=INVALID";
checksum_state = valid ? "valid" : "INVALID";
}
std::ostringstream header;
header << "UDP src_port=" << result.source_port
<< " dst_port=" << result.destination_port
<< " length=" << length
<< " checksum=" << hex_prefixed(be16(transport + 6), 4)
<< " checksum_status=" << checksum_state
<< " raw_header=" << hex(transport, 8);
result.headers.push_back(header.str());
if (length >= 8 && length <= transport_size)
set_payload_preview(result, transport + 8, length - 8);
if (length >= 8 && length <= transport_size &&
(result.source_port == 53 || result.destination_port == 53 ||
result.source_port == 5353 || result.destination_port == 5353)) {
const bool mdns = result.source_port == 5353 || result.destination_port == 5353;
result.application = mdns ? "mDNS" : "DNS";
summary << ' ' << parse_dns_summary(transport + 8, length - 8, mdns);
}
} else if (next_header == 6) {
result.transport = "TCP";
if (transport_size < 20) {
result.checksum_status += " TCP=truncated";
result.headers.push_back("TCP truncated captured_bytes=" +
std::to_string(transport_size));
summary << " TCP truncated";
return;
}
const std::uint32_t pseudo = pseudo_for(transport_size);
result.source_port = be16(transport);
result.destination_port = be16(transport + 2);
const std::size_t header_bytes =
static_cast<std::size_t>(transport[12] >> 4) * 4;
const std::uint16_t flags = static_cast<std::uint16_t>(
((transport[12] & 1u) << 8) | transport[13]);
summary << " TCP " << result.source_port << "->"
<< result.destination_port << " flags=" << tcp_flag_names(flags);
std::string checksum_state;
if (header_bytes < 20 || header_bytes > transport_size) {
result.checksum_status += " TCP=truncated";
checksum_state = "invalid-data-offset";
} else if (fragmented) {
result.checksum_status += " TCP=unverified-fragment";
checksum_state = "unverified-fragment";
} else {
const bool valid = checksum_valid(transport, transport_size, pseudo);
result.checksum_status += valid ? " TCP=valid" : " TCP=INVALID";
checksum_state = valid ? "valid" : "INVALID";
}
const std::size_t available_header =
std::min(header_bytes, transport_size);
std::ostringstream decoded;
decoded << "TCP src_port=" << result.source_port
<< " dst_port=" << result.destination_port
<< " sequence=" << be32(transport + 4)
<< " acknowledgment=" << be32(transport + 8)
<< " data_offset_bytes=" << header_bytes
<< " reserved=" << static_cast<unsigned>((transport[12] >> 1) & 7u)
<< " flags=" << hex_prefixed(flags, 3)
<< " flags_named=" << tcp_flag_names(flags)
<< " window=" << be16(transport + 14)
<< " checksum=" << hex_prefixed(be16(transport + 16), 4)
<< " checksum_status=" << checksum_state
<< " urgent_pointer=" << be16(transport + 18);
if (available_header > 20)
decoded << " options="
<< hex(transport + 20, available_header - 20);
decoded << " raw_header=" << hex(transport, available_header);
result.headers.push_back(decoded.str());
if (header_bytes >= 20 && header_bytes <= transport_size)
set_payload_preview(result, transport + header_bytes,
transport_size - header_bytes);
} else if (next_header == 58) {
result.transport = "ICMPv6";
if (transport_size < 4) {
result.checksum_status += " ICMPv6=truncated";
result.headers.push_back("ICMPv6 truncated captured_bytes=" +
std::to_string(transport_size));
summary << " ICMPv6 truncated";
return;
}
const std::uint32_t pseudo = pseudo_for(transport_size);
summary << " ICMPv6 type=" << static_cast<unsigned>(transport[0])
<< " code=" << static_cast<unsigned>(transport[1]);
std::string checksum_state = "unverified-fragment";
if (!fragmented) {
const bool valid = checksum_valid(transport, transport_size, pseudo);
result.checksum_status += valid ? " ICMPv6=valid" : " ICMPv6=INVALID";
checksum_state = valid ? "valid" : "INVALID";
} else {
result.checksum_status += " ICMPv6=unverified-fragment";
}
std::ostringstream header;
header << "ICMPv6 type=" << static_cast<unsigned>(transport[0])
<< " code=" << static_cast<unsigned>(transport[1])
<< " checksum=" << hex_prefixed(be16(transport + 2), 4)
<< " checksum_status=" << checksum_state
<< " rest_of_header="
<< (transport_size >= 8 ? hex(transport + 4, 4) : "truncated")
<< " raw_header="
<< hex(transport, std::min<std::size_t>(transport_size, 8));
result.headers.push_back(header.str());
if (transport_size > 8)
set_payload_preview(result, transport + 8, transport_size - 8);
}
} else if (next_header == 50) {
result.transport = "ESP";
summary << " ESP ciphertext";
std::ostringstream header;
header << "ESP";
if (transport_size >= 8)
header << " spi=" << hex_prefixed(be32(transport), 8)
<< " sequence=" << be32(transport + 4)
<< " raw_header=" << hex(transport, 8);
else
header << " truncated captured_bytes=" << transport_size;
result.headers.push_back(header.str());
} else {
result.transport = "IPv6 next-header " + std::to_string(next_header);
summary << " next=" << static_cast<unsigned>(next_header);
result.headers.push_back("IPv6 payload next_header=" +
std::to_string(next_header) +
" transport_header_not_decoded");
set_payload_preview(result, transport, transport_size);
}
}
ProtocolInfo parse_llc_payload(const std::uint8_t* payload, std::size_t size,
const std::string& trust) {
ProtocolInfo result;
result.trust = trust;
result.integrity_verified = trust == "CCMP-authenticated";
result.decrypted = result.integrity_verified;
std::ostringstream summary;
summary << trust << ' ';
if (size < 8 || payload[0] != 0xaa || payload[1] != 0xaa || payload[2] != 0x03) {
result.layer2 = "non-SNAP";
result.headers.push_back("LLC/SNAP absent captured_payload_bytes=" +
std::to_string(size));
summary << "non-SNAP data bytes=" << size;
result.summary = summary.str();
return result;
}
result.layer2 = "LLC/SNAP";
const std::uint16_t ether_type = be16(payload + 6);
{
std::ostringstream header;
header << "LLC/SNAP dsap=" << hex_prefixed(payload[0], 2)
<< " ssap=" << hex_prefixed(payload[1], 2)
<< " control=" << hex_prefixed(payload[2], 2)
<< " oui=" << hex(payload + 3, 3)
<< " ethertype=" << hex_prefixed(ether_type, 4)
<< " raw_header=" << hex(payload, 8);
result.headers.push_back(header.str());
}
const auto* body = payload + 8;
const std::size_t body_size = size - 8;
if (ether_type == 0x0800 && body_size >= 20 && (body[0] >> 4) == 4) {
result.network = "IPv4";
const std::size_t header = static_cast<std::size_t>(body[0] & 0x0f) * 4;
const std::size_t total = be16(body + 2);
if (header < 20 || header > body_size || total < header || total > body_size) {
summary << "IPv4 truncated/invalid-length captured=" << body_size
<< " declared=" << total << " IHL=" << header;
result.summary = summary.str();
return result;
}
result.source_ip = ipv4(body + 12);
result.destination_ip = ipv4(body + 16);
const bool ip_checksum = checksum_valid(body, header);
result.checksum_status = ip_checksum ? "IPv4=valid" : "IPv4=INVALID";
const std::uint16_t fragment = be16(body + 6);
const std::uint8_t dscp = body[1] >> 2;
const std::uint8_t ecn = body[1] & 3u;
const bool more = (fragment & 0x2000u) != 0;
const std::uint16_t fragment_offset = fragment & 0x1fffu;
{
std::ostringstream decoded;
decoded << "IPv4 version=" << static_cast<unsigned>(body[0] >> 4)
<< " ihl_bytes=" << header
<< " dscp=" << static_cast<unsigned>(dscp)
<< " ecn=" << static_cast<unsigned>(ecn)
<< " total_length=" << total
<< " identification=" << hex_prefixed(be16(body + 4), 4)
<< " reserved_flag=" << ((fragment & 0x8000u) != 0)
<< " dont_fragment=" << ((fragment & 0x4000u) != 0)
<< " more_fragments=" << more
<< " fragment_offset_units=" << fragment_offset
<< " fragment_offset_bytes=" << fragment_offset * 8
<< " ttl=" << static_cast<unsigned>(body[8])
<< " protocol=" << static_cast<unsigned>(body[9])
<< " header_checksum=" << hex_prefixed(be16(body + 10), 4)
<< " checksum_status=" << (ip_checksum ? "valid" : "INVALID")
<< " source=" << result.source_ip
<< " destination=" << result.destination_ip;
if (header > 20) decoded << " options=" << hex(body + 20, header - 20);
decoded << " raw_header=" << hex(body, header);
result.headers.push_back(decoded.str());
}
summary << "IPv4 " << result.source_ip << "->" << result.destination_ip
<< " ttl=" << static_cast<unsigned>(body[8])
<< " id=" << be16(body + 4);
if (fragment_offset != 0) {
summary << " fragment-offset=" << fragment_offset;
} else {
parse_transport_ipv4(result, body[9], body + header, total - header,
body + 12, body + 16, !more, summary);
}
} else if (ether_type == 0x0806 && body_size >= 8) {
result.network = "ARP";
const std::uint8_t hardware_length = body[4];
const std::uint8_t protocol_length = body[5];
const std::size_t needed = 8 + 2 * hardware_length + 2 * protocol_length;
if (be16(body) == 1 && be16(body + 2) == 0x0800 &&
hardware_length == 6 && protocol_length == 4 && needed <= body_size) {
const std::uint16_t operation = be16(body + 6);
const std::string sender_mac = mac_string(body + 8);
const std::string sender_ip = ipv4(body + 14);
const std::string target_mac = mac_string(body + 18);
const std::string target_ip = ipv4(body + 24);
result.source_ip = sender_ip;
result.destination_ip = target_ip;
std::ostringstream decoded;
decoded << "ARP hardware_type=" << be16(body)
<< " protocol_type=" << hex_prefixed(be16(body + 2), 4)
<< " hardware_length=" << static_cast<unsigned>(hardware_length)
<< " protocol_length=" << static_cast<unsigned>(protocol_length)
<< " operation=" << operation
<< " sender_mac=" << sender_mac
<< " sender_ip=" << sender_ip
<< " target_mac=" << target_mac
<< " target_ip=" << target_ip
<< " raw_header=" << hex(body, needed);
result.headers.push_back(decoded.str());
summary << "ARP " << (operation == 1 ? "request" : operation == 2 ? "reply" : "op")
<< " sender=" << sender_mac << '/' << sender_ip
<< " target=" << target_mac << '/' << target_ip;
} else {
result.headers.push_back("ARP unsupported_or_truncated captured_bytes=" +
std::to_string(body_size));
summary << "ARP unsupported/truncated";
}
} else if (ether_type == 0x86dd && body_size >= 40 && (body[0] >> 4) == 6) {
result.network = "IPv6";
const std::size_t payload_length = be16(body + 4);
if (payload_length + 40 > body_size) {
summary << "IPv6 truncated captured=" << body_size
<< " declared=" << payload_length + 40;
} else {
result.source_ip = ipv6(body + 8);
result.destination_ip = ipv6(body + 24);
const std::uint32_t first_word = be32(body);
std::ostringstream decoded;
decoded << "IPv6 version=" << (first_word >> 28)
<< " traffic_class=" << ((first_word >> 20) & 0xffu)
<< " flow_label=" << hex_prefixed(first_word & 0xfffffu, 5)
<< " payload_length=" << payload_length
<< " next_header=" << static_cast<unsigned>(body[6])
<< " hop_limit=" << static_cast<unsigned>(body[7])
<< " source=" << result.source_ip
<< " destination=" << result.destination_ip
<< " raw_header=" << hex(body, 40);
result.headers.push_back(decoded.str());
summary << "IPv6 " << result.source_ip << "->" << result.destination_ip
<< " hop=" << static_cast<unsigned>(body[7]);
parse_transport_ipv6(result, body[6], body + 40, payload_length,
body + 8, body + 24, summary);
}
} else if (ether_type == 0x888e) {
result.network = "EAPOL";
result.application = "EAPOL";
result.eapol = true;
if (body_size >= 4) {
const std::size_t declared = be16(body + 2) + 4;
std::ostringstream decoded;
decoded << "EAPOL version=" << static_cast<unsigned>(body[0])
<< " type=" << static_cast<unsigned>(body[1])
<< " body_length=" << be16(body + 2)
<< " captured_bytes=" << body_size
<< " raw_header=" << hex(body, 4);
result.headers.push_back(decoded.str());
summary << "EAPOL type=" << static_cast<unsigned>(body[1])
<< " bytes=" << declared;
if (declared > body_size) summary << " truncated";
} else {
result.headers.push_back("EAPOL truncated captured_bytes=" +
std::to_string(body_size));
summary << "EAPOL truncated";
}
} else {
std::ostringstream type;
type << "EtherType 0x" << std::hex << std::setw(4) << std::setfill('0')
<< ether_type;
result.network = type.str();
result.headers.push_back(result.network + " header_not_decoded bytes=" +
std::to_string(body_size));
summary << result.network << " bytes=" << body_size;
}
if (!result.checksum_status.empty()) summary << " [" << result.checksum_status << ']';
result.summary = summary.str();
return result;
}
ProtocolInfo parse_plain_payload(const std::uint8_t* payload, std::size_t size,
const DataLayout& layout,
const std::string& trust) {
if (!layout.amsdu) return parse_llc_payload(payload, size, trust);
ProtocolInfo result;
result.trust = trust;
result.layer2 = "A-MSDU";
result.decrypted = trust == "CCMP-authenticated";
result.integrity_verified = result.decrypted;
std::ostringstream summary;
summary << trust << " A-MSDU";
std::size_t offset = 0;
std::size_t subframes = 0;
while (offset + 14 <= size && subframes < 32) {
const std::size_t length = be16(payload + offset + 12);
if (offset + 14 + length > size) {
summary << " [truncated subframe]";
result.headers.push_back("A-MSDU subframe=" +
std::to_string(subframes) +
" declared_payload_bytes=" +
std::to_string(length) +
" captured_bytes=" +
std::to_string(size - offset) + " truncated");
break;
}
{
std::ostringstream header;
header << "A-MSDU subframe=" << subframes
<< " destination=" << mac_string(payload + offset)
<< " source=" << mac_string(payload + offset + 6)
<< " payload_length=" << length
<< " raw_header=" << hex(payload + offset, 14);
result.headers.push_back(header.str());
}
auto child = parse_llc_payload(payload + offset + 14, length, trust);
if (subframes == 0) {
result.network = child.network;
result.transport = child.transport;
result.application = child.application;
result.source_ip = child.source_ip;
result.destination_ip = child.destination_ip;
result.source_port = child.source_port;
result.destination_port = child.destination_port;
result.checksum_status = child.checksum_status;
result.payload_bytes = child.payload_bytes;
result.payload_hex = child.payload_hex;
result.payload_ascii = child.payload_ascii;
result.payload_truncated = child.payload_truncated;
}
for (const auto& header : child.headers)
result.headers.push_back("A-MSDU[" + std::to_string(subframes) +
"] " + header);
result.dhcp.insert(result.dhcp.end(), child.dhcp.begin(), child.dhcp.end());
summary << " {" << child.summary << '}';
++subframes;
const std::size_t occupied = 14 + length;
offset += occupied;
if (offset < size) offset = (offset + 3) & ~std::size_t(3);
}
summary << " subframes=" << subframes;
result.summary = summary.str();
return result;
}
const std::uint8_t* payload_begin(const Bytes& psdu, const DataLayout& layout,
std::size_t* size) {
if (psdu.size() < 4 || layout.header_bytes > psdu.size() - 4) {
*size = 0;
return nullptr;
}
*size = psdu.size() - 4 - layout.header_bytes;
return psdu.data() + layout.header_bytes;
}
} // namespace
std::optional<DataLayout> parse_data_layout(const Bytes& psdu) {
if (psdu.size() < 28) return std::nullopt;
const std::size_t frame_size = psdu.size() - 4;
const std::uint16_t control = le16(psdu.data());
const int type = (control >> 2) & 3;
const int subtype = (control >> 4) & 15;
if (type != 2 || frame_size < 24) return std::nullopt;
DataLayout result;
result.frame_control = control;
result.to_ds = (control & 0x0100u) != 0;
result.from_ds = (control & 0x0200u) != 0;
result.more_fragments = (control & 0x0400u) != 0;
result.protected_frame = (control & 0x4000u) != 0;
result.ordered = (control & 0x8000u) != 0;
result.has_address4 = result.to_ds && result.from_ds;
result.qos = (subtype & 8) != 0;
std::copy_n(psdu.data() + 4, 6, result.address1.begin());
std::copy_n(psdu.data() + 10, 6, result.address2.begin());
std::copy_n(psdu.data() + 16, 6, result.address3.begin());
result.fragment_number = static_cast<std::uint8_t>(psdu[22] & 0x0f);
result.header_bytes = 24;
if (result.has_address4) {
if (frame_size < 30) return std::nullopt;
std::copy_n(psdu.data() + 24, 6, result.address4.begin());
result.header_bytes += 6;
}
const std::size_t qos_offset = result.header_bytes;
if (result.qos) {
if (frame_size < result.header_bytes + 2) return std::nullopt;
const std::uint16_t qos_control = le16(psdu.data() + qos_offset);
result.tid = static_cast<std::uint8_t>(qos_control & 0x0f);
result.amsdu = (qos_control & 0x0080u) != 0;
result.header_bytes += 2;
if (result.ordered) {
if (frame_size < result.header_bytes + 4) return std::nullopt;
result.header_bytes += 4;
}
}
result.receiver = mac_string(result.address1);
result.transmitter = mac_string(result.address2);
if (!result.to_ds && !result.from_ds) {
result.destination = result.receiver;
result.source = result.transmitter;
result.bssid = mac_string(result.address3);
} else if (result.to_ds && !result.from_ds) {
result.destination = mac_string(result.address3);
result.source = result.transmitter;
result.bssid = result.receiver;
} else if (!result.to_ds && result.from_ds) {
result.destination = result.receiver;
result.source = mac_string(result.address3);
result.bssid = result.transmitter;
} else {
result.destination = mac_string(result.address3);
result.source = mac_string(result.address4);
}
return result;
}
ProtocolInfo inspect_unprotected(const Bytes& psdu, const DataLayout& layout) {
if (layout.protected_frame)
return inspect_ciphertext(psdu, layout, "protected bit set");
if (layout.more_fragments || layout.fragment_number != 0) {
ProtocolInfo result;
result.trust = "open";
result.layer2 = "802.11 fragment";
result.summary = "open fragmented MSDU; reassembly not yet available";
return result;
}
std::size_t size = 0;
const auto* payload = payload_begin(psdu, layout, &size);
if (!payload) return {};
return parse_plain_payload(payload, size, layout, "open");
}
ProtocolInfo inspect_ciphertext(const Bytes& psdu, const DataLayout& layout,
std::string reason) {
ProtocolInfo result;
result.trust = "ciphertext";
result.layer2 = "CCMP";
std::size_t size = 0;
const auto* payload = payload_begin(psdu, layout, &size);
if (!payload || size < 8) {
result.summary = "protected payload truncated before security header";
return result;
}
const auto* ccmp = payload;
result.ccmp_key_id = (ccmp[3] >> 6) & 3;
result.ccmp_packet_number =
static_cast<std::uint64_t>(ccmp[0]) |
(static_cast<std::uint64_t>(ccmp[1]) << 8) |
(static_cast<std::uint64_t>(ccmp[4]) << 16) |
(static_cast<std::uint64_t>(ccmp[5]) << 24) |
(static_cast<std::uint64_t>(ccmp[6]) << 32) |
(static_cast<std::uint64_t>(ccmp[7]) << 40);
std::ostringstream out;
out << "CCMP ciphertext key=" << result.ccmp_key_id
<< " PN=" << result.ccmp_packet_number << " (" << reason << ')';
result.summary = out.str();
return result;
}
} // namespace gf::wifi
namespace gf::wifi {
namespace {
enum class HashAlgorithm { Md5, Sha1 };
#ifdef _WIN32
ULONG narrow_size(std::size_t size, const char* label) {
if (size > std::numeric_limits<ULONG>::max())
throw std::runtime_error(std::string(label) + " is too large");
return static_cast<ULONG>(size);
}
void require_nt(NTSTATUS status, const char* operation) {
if (status >= 0) return;
std::ostringstream message;
message << operation << " failed with NTSTATUS 0x" << std::hex
<< static_cast<std::uint32_t>(status);
throw std::runtime_error(message.str());
}
class AlgorithmHandle {
public:
AlgorithmHandle(LPCWSTR algorithm, ULONG flags = 0) {
require_nt(BCryptOpenAlgorithmProvider(&handle_, algorithm, nullptr, flags),
"BCryptOpenAlgorithmProvider");
}
~AlgorithmHandle() {
if (handle_) BCryptCloseAlgorithmProvider(handle_, 0);
}
AlgorithmHandle(const AlgorithmHandle&) = delete;
AlgorithmHandle& operator=(const AlgorithmHandle&) = delete;
BCRYPT_ALG_HANDLE get() const { return handle_; }
private:
BCRYPT_ALG_HANDLE handle_ = nullptr;
};
class KeyHandle {
public:
KeyHandle() = default;
~KeyHandle() {
if (handle_) BCryptDestroyKey(handle_);
}
KeyHandle(const KeyHandle&) = delete;
KeyHandle& operator=(const KeyHandle&) = delete;
BCRYPT_KEY_HANDLE* put() { return &handle_; }
BCRYPT_KEY_HANDLE get() const { return handle_; }
private:
BCRYPT_KEY_HANDLE handle_ = nullptr;
};
Bytes hmac(HashAlgorithm algorithm, const std::uint8_t* key,
std::size_t key_size,
const std::uint8_t* data, std::size_t data_size) {
const LPCWSTR algorithm_name = algorithm == HashAlgorithm::Md5
? BCRYPT_MD5_ALGORITHM : BCRYPT_SHA1_ALGORITHM;
AlgorithmHandle provider(algorithm_name, BCRYPT_ALG_HANDLE_HMAC_FLAG);
ULONG object_size = 0;
ULONG hash_size = 0;
ULONG returned = 0;
require_nt(BCryptGetProperty(provider.get(), BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_size),
sizeof(object_size), &returned, 0),
"BCryptGetProperty(hash object)");
require_nt(BCryptGetProperty(provider.get(), BCRYPT_HASH_LENGTH,
reinterpret_cast<PUCHAR>(&hash_size),
sizeof(hash_size), &returned, 0),
"BCryptGetProperty(hash length)");
Bytes object(object_size);
Bytes digest(hash_size);
BCRYPT_HASH_HANDLE hash_handle = nullptr;
require_nt(BCryptCreateHash(provider.get(), &hash_handle,
object.data(), object_size,
const_cast<PUCHAR>(key), narrow_size(key_size, "HMAC key"),
0),
"BCryptCreateHash");
try {
require_nt(BCryptHashData(hash_handle, const_cast<PUCHAR>(data),
narrow_size(data_size, "HMAC input"), 0),
"BCryptHashData");
require_nt(BCryptFinishHash(hash_handle, digest.data(), hash_size, 0),
"BCryptFinishHash");
} catch (...) {
BCryptDestroyHash(hash_handle);
throw;
}
BCryptDestroyHash(hash_handle);
return digest;
}
Key32 derive_pmk(const std::string& passphrase, const std::string& ssid) {
AlgorithmHandle provider(BCRYPT_SHA1_ALGORITHM, BCRYPT_ALG_HANDLE_HMAC_FLAG);
Key32 output{};
require_nt(BCryptDeriveKeyPBKDF2(
provider.get(),
reinterpret_cast<PUCHAR>(const_cast<char*>(passphrase.data())),
narrow_size(passphrase.size(), "passphrase"),
reinterpret_cast<PUCHAR>(const_cast<char*>(ssid.data())),
narrow_size(ssid.size(), "SSID"), 4096,
output.data(), static_cast<ULONG>(output.size()), 0),
"BCryptDeriveKeyPBKDF2");
return output;
}
#else
int narrow_int(std::size_t size, const char* label) {
if (size > static_cast<std::size_t>(std::numeric_limits<int>::max()))
throw std::runtime_error(std::string(label) + " is too large");
return static_cast<int>(size);
}
class EvpCipherContext {
public:
EvpCipherContext() : value_(EVP_CIPHER_CTX_new()) {
if (!value_) throw std::runtime_error("EVP_CIPHER_CTX_new failed");
}
~EvpCipherContext() { EVP_CIPHER_CTX_free(value_); }
EvpCipherContext(const EvpCipherContext&) = delete;
EvpCipherContext& operator=(const EvpCipherContext&) = delete;
EVP_CIPHER_CTX* get() const { return value_; }
private:
EVP_CIPHER_CTX* value_ = nullptr;
};
const EVP_MD* digest_for(HashAlgorithm algorithm) {
return algorithm == HashAlgorithm::Md5 ? EVP_md5() : EVP_sha1();
}
const EVP_CIPHER* ecb_cipher_for(std::size_t key_size) {
if (key_size == 16) return EVP_aes_128_ecb();
if (key_size == 24) return EVP_aes_192_ecb();
if (key_size == 32) return EVP_aes_256_ecb();
throw std::runtime_error("AES key must contain 16, 24, or 32 bytes");
}
Bytes hmac(HashAlgorithm algorithm, const std::uint8_t* key,
std::size_t key_size,
const std::uint8_t* data, std::size_t data_size) {
const EVP_MD* digest_algorithm = digest_for(algorithm);
Bytes digest(static_cast<std::size_t>(EVP_MD_size(digest_algorithm)));
unsigned int written = 0;
if (!HMAC(digest_algorithm, key, narrow_int(key_size, "HMAC key"), data,
data_size, digest.data(), &written)) {
throw std::runtime_error("OpenSSL HMAC failed");
}
digest.resize(written);
return digest;
}
Key32 derive_pmk(const std::string& passphrase, const std::string& ssid) {
Key32 output{};
if (PKCS5_PBKDF2_HMAC_SHA1(
passphrase.data(), narrow_int(passphrase.size(), "passphrase"),
reinterpret_cast<const unsigned char*>(ssid.data()),
narrow_int(ssid.size(), "SSID"), 4096,
narrow_int(output.size(), "PMK"), output.data()) != 1) {
throw std::runtime_error("OpenSSL PBKDF2-HMAC-SHA1 failed");
}
return output;
}
#endif
bool constant_equal(const std::uint8_t* left, const std::uint8_t* right,
std::size_t size) {
std::uint8_t difference = 0;
for (std::size_t index = 0; index < size; ++index)
difference |= static_cast<std::uint8_t>(left[index] ^ right[index]);
return difference == 0;
}
Key48 derive_ptk(const Key32& pmk, const Mac& ap, const Mac& station,
const Nonce& anonce, const Nonce& snonce) {
static constexpr std::string_view label = "Pairwise key expansion";
Bytes context;
context.reserve(2 * 6 + 2 * 32);
const auto append_ordered = [&context](const auto& left, const auto& right) {
const auto& first = left < right ? left : right;
const auto& second = left < right ? right : left;
context.insert(context.end(), first.begin(), first.end());
context.insert(context.end(), second.begin(), second.end());
};
append_ordered(ap, station);
append_ordered(anonce, snonce);
Key48 output{};
std::size_t written = 0;
std::uint8_t counter = 0;
while (written < output.size()) {
Bytes input(label.begin(), label.end());
input.push_back(0);
input.insert(input.end(), context.begin(), context.end());
input.push_back(counter++);
const auto digest = hmac(HashAlgorithm::Sha1, pmk.data(), pmk.size(),
input.data(), input.size());
const std::size_t count = std::min(digest.size(), output.size() - written);
std::copy_n(digest.begin(), count,
output.begin() + static_cast<std::ptrdiff_t>(written));
written += count;
}
return output;
}
#ifdef _WIN32
class AesEcbDecryptor {
public:
AesEcbDecryptor(const std::uint8_t* key, std::size_t key_size)
: provider_(BCRYPT_AES_ALGORITHM) {
require_nt(BCryptSetProperty(
provider_.get(), BCRYPT_CHAINING_MODE,
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_ECB)),
sizeof(BCRYPT_CHAIN_MODE_ECB), 0),
"BCryptSetProperty(AES-ECB)");
ULONG returned = 0;
require_nt(BCryptGetProperty(provider_.get(), BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_size_),
sizeof(object_size_), &returned, 0),
"BCryptGetProperty(AES object)");
object_.resize(object_size_);
require_nt(BCryptGenerateSymmetricKey(
provider_.get(), key_.put(), object_.data(), object_size_,
const_cast<PUCHAR>(key), narrow_size(key_size, "AES key"), 0),
"BCryptGenerateSymmetricKey(AES-ECB)");
}
std::array<std::uint8_t,16> decrypt(
const std::array<std::uint8_t,16>& ciphertext) const {
std::array<std::uint8_t,16> output{};
ULONG written = 0;
require_nt(BCryptDecrypt(key_.get(),
const_cast<PUCHAR>(ciphertext.data()),
static_cast<ULONG>(ciphertext.size()), nullptr,
nullptr, 0, output.data(),
static_cast<ULONG>(output.size()), &written, 0),
"BCryptDecrypt(AES-ECB)");
if (written != output.size())
throw std::runtime_error("AES-ECB returned the wrong block length");
return output;
}
private:
AlgorithmHandle provider_;
mutable KeyHandle key_;
ULONG object_size_ = 0;
Bytes object_;
};
class AesEcbEncryptor {
public:
AesEcbEncryptor(const std::uint8_t* key, std::size_t key_size)
: provider_(BCRYPT_AES_ALGORITHM) {
require_nt(BCryptSetProperty(
provider_.get(), BCRYPT_CHAINING_MODE,
reinterpret_cast<PUCHAR>(
const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_ECB)),
sizeof(BCRYPT_CHAIN_MODE_ECB), 0),
"BCryptSetProperty(AES-ECB encrypt)");
ULONG returned = 0;
require_nt(BCryptGetProperty(provider_.get(), BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_size_),
sizeof(object_size_), &returned, 0),
"BCryptGetProperty(AES encrypt object)");
object_.resize(object_size_);
require_nt(BCryptGenerateSymmetricKey(
provider_.get(), key_.put(), object_.data(), object_size_,
const_cast<PUCHAR>(key),
narrow_size(key_size, "AES encrypt key"), 0),
"BCryptGenerateSymmetricKey(AES-ECB encrypt)");
}
std::array<std::uint8_t,16> encrypt(
const std::array<std::uint8_t,16>& plain) const {
std::array<std::uint8_t,16> output{};
ULONG written = 0;
require_nt(BCryptEncrypt(key_.get(),
const_cast<PUCHAR>(plain.data()),
static_cast<ULONG>(plain.size()), nullptr,
nullptr, 0, output.data(),
static_cast<ULONG>(output.size()), &written, 0),
"BCryptEncrypt(AES-ECB)");
if (written != output.size())
throw std::runtime_error(
"AES-ECB encrypt returned the wrong block length");
return output;
}
private:
AlgorithmHandle provider_;
mutable KeyHandle key_;
ULONG object_size_ = 0;
Bytes object_;
};
#else
class AesEcbDecryptor {
public:
AesEcbDecryptor(const std::uint8_t* key, std::size_t key_size)
: key_(key, key + key_size), cipher_(ecb_cipher_for(key_size)) {}
std::array<std::uint8_t,16> decrypt(
const std::array<std::uint8_t,16>& ciphertext) const {
EvpCipherContext context;
if (EVP_DecryptInit_ex(context.get(), cipher_, nullptr, key_.data(),
nullptr) != 1 ||
EVP_CIPHER_CTX_set_padding(context.get(), 0) != 1) {
throw std::runtime_error("OpenSSL AES-ECB decrypt init failed");
}
std::array<std::uint8_t,16> output{};
int first = 0;
int final = 0;
if (EVP_DecryptUpdate(context.get(), output.data(), &first,
ciphertext.data(),
static_cast<int>(ciphertext.size())) != 1 ||
EVP_DecryptFinal_ex(context.get(), output.data() + first,
&final) != 1 ||
first + final != static_cast<int>(output.size())) {
throw std::runtime_error("OpenSSL AES-ECB decrypt failed");
}
return output;
}
private:
Bytes key_;
const EVP_CIPHER* cipher_ = nullptr;
};
class AesEcbEncryptor {
public:
AesEcbEncryptor(const std::uint8_t* key, std::size_t key_size)
: key_(key, key + key_size), cipher_(ecb_cipher_for(key_size)) {}
std::array<std::uint8_t,16> encrypt(
const std::array<std::uint8_t,16>& plain) const {
EvpCipherContext context;
if (EVP_EncryptInit_ex(context.get(), cipher_, nullptr, key_.data(),
nullptr) != 1 ||
EVP_CIPHER_CTX_set_padding(context.get(), 0) != 1) {
throw std::runtime_error("OpenSSL AES-ECB encrypt init failed");
}
std::array<std::uint8_t,16> output{};
int first = 0;
int final = 0;
if (EVP_EncryptUpdate(context.get(), output.data(), &first,
plain.data(),
static_cast<int>(plain.size())) != 1 ||
EVP_EncryptFinal_ex(context.get(), output.data() + first,
&final) != 1 ||
first + final != static_cast<int>(output.size())) {
throw std::runtime_error("OpenSSL AES-ECB encrypt failed");
}
return output;
}
private:
Bytes key_;
const EVP_CIPHER* cipher_ = nullptr;
};
#endif
std::optional<Bytes> aes_key_wrap(const std::uint8_t* kek,
std::size_t kek_size,
const Bytes& plain) {
if (plain.size() < 16 || (plain.size() & 7u) != 0) return std::nullopt;
const std::size_t n = plain.size() / 8;
std::array<std::uint8_t,8> a = {
0xa6,0xa6,0xa6,0xa6,0xa6,0xa6,0xa6,0xa6};
Bytes output = plain;
AesEcbEncryptor aes(kek, kek_size);
for (int j = 0; j <= 5; ++j) {
for (std::size_t index = 1; index <= n; ++index) {
std::array<std::uint8_t,16> block{};
std::copy(a.begin(), a.end(), block.begin());
std::copy_n(output.begin() +
static_cast<std::ptrdiff_t>((index - 1) * 8),
8, block.begin() + 8);
const auto encrypted = aes.encrypt(block);
std::copy_n(encrypted.begin(), 8, a.begin());
const std::uint64_t t = static_cast<std::uint64_t>(n) *
static_cast<std::uint64_t>(j) + index;
for (int byte = 0; byte < 8; ++byte)
a[7 - byte] ^= static_cast<std::uint8_t>(t >> (byte * 8));
std::copy_n(encrypted.begin() + 8, 8,
output.begin() + static_cast<std::ptrdiff_t>(
(index - 1) * 8));
}
}
Bytes wrapped(a.begin(), a.end());
wrapped.insert(wrapped.end(), output.begin(), output.end());
return wrapped;
}
std::optional<Bytes> aes_key_unwrap(const std::uint8_t* kek,
std::size_t kek_size,
const Bytes& wrapped) {
if (wrapped.size() < 24 || (wrapped.size() & 7u) != 0) return std::nullopt;
const std::size_t n = wrapped.size() / 8 - 1;
std::array<std::uint8_t,8> a{};
std::copy_n(wrapped.begin(), 8, a.begin());
Bytes output(wrapped.begin() + 8, wrapped.end());
AesEcbDecryptor aes(kek, kek_size);
for (int j = 5; j >= 0; --j) {
for (std::size_t reverse = n; reverse > 0; --reverse) {
const std::uint64_t t = static_cast<std::uint64_t>(n) *
static_cast<std::uint64_t>(j) + reverse;
std::array<std::uint8_t,16> block{};
std::copy(a.begin(), a.end(), block.begin());
for (int byte = 0; byte < 8; ++byte)
block[7 - byte] ^= static_cast<std::uint8_t>(t >> (byte * 8));
std::copy_n(output.begin() + static_cast<std::ptrdiff_t>((reverse - 1) * 8),
8, block.begin() + 8);
const auto plain = aes.decrypt(block);
std::copy_n(plain.begin(), 8, a.begin());
std::copy_n(plain.begin() + 8, 8,
output.begin() + static_cast<std::ptrdiff_t>((reverse - 1) * 8));
}
}
static constexpr std::array<std::uint8_t,8> expected = {
0xa6,0xa6,0xa6,0xa6,0xa6,0xa6,0xa6,0xa6};
if (!constant_equal(a.data(), expected.data(), expected.size())) return std::nullopt;
return output;
}
std::optional<Bytes> aes_ccm(bool decrypt, const Key16& key,
const Bytes& nonce, const Bytes& aad,
const Bytes& input, Bytes& tag) {
#ifdef _WIN32
AlgorithmHandle provider(BCRYPT_AES_ALGORITHM);
require_nt(BCryptSetProperty(
provider.get(), BCRYPT_CHAINING_MODE,
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_CCM)),
sizeof(BCRYPT_CHAIN_MODE_CCM), 0),
"BCryptSetProperty(AES-CCM)");
ULONG object_size = 0;
ULONG returned = 0;
require_nt(BCryptGetProperty(provider.get(), BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_size),
sizeof(object_size), &returned, 0),
"BCryptGetProperty(AES-CCM object)");
Bytes object(object_size);
KeyHandle key_handle;
require_nt(BCryptGenerateSymmetricKey(
provider.get(), key_handle.put(), object.data(), object_size,
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0),
"BCryptGenerateSymmetricKey(AES-CCM)");
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO auth;
BCRYPT_INIT_AUTH_MODE_INFO(auth);
auth.pbNonce = const_cast<PUCHAR>(nonce.data());
auth.cbNonce = narrow_size(nonce.size(), "CCM nonce");
auth.pbAuthData = const_cast<PUCHAR>(aad.data());
auth.cbAuthData = narrow_size(aad.size(), "CCM AAD");
auth.pbTag = tag.data();
auth.cbTag = narrow_size(tag.size(), "CCM tag");
Bytes output(input.size());
ULONG output_size = 0;
const NTSTATUS status = decrypt
? BCryptDecrypt(key_handle.get(), const_cast<PUCHAR>(input.data()),
narrow_size(input.size(), "CCM ciphertext"), &auth,
nullptr, 0, output.data(), narrow_size(output.size(), "CCM output"),
&output_size, 0)
: BCryptEncrypt(key_handle.get(), const_cast<PUCHAR>(input.data()),
narrow_size(input.size(), "CCM plaintext"), &auth,
nullptr, 0, output.data(), narrow_size(output.size(), "CCM output"),
&output_size, 0);
if (status < 0) return std::nullopt;
output.resize(output_size);
return output;
#else
if (nonce.size() < 7 || nonce.size() > 13 || tag.size() < 4 ||
tag.size() > 16 || (tag.size() & 1u) != 0) {
return std::nullopt;
}
EvpCipherContext context;
Bytes output(input.size());
int count = 0;
int written = 0;
if (decrypt) {
if (EVP_DecryptInit_ex(context.get(), EVP_aes_128_ccm(), nullptr,
nullptr, nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_CCM_SET_IVLEN,
narrow_int(nonce.size(), "CCM nonce"),
nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_CCM_SET_TAG,
narrow_int(tag.size(), "CCM tag"),
tag.data()) != 1 ||
EVP_DecryptInit_ex(context.get(), nullptr, nullptr, key.data(),
nonce.data()) != 1 ||
EVP_DecryptUpdate(context.get(), nullptr, &count, nullptr,
narrow_int(input.size(), "CCM ciphertext")) != 1 ||
(!aad.empty() &&
EVP_DecryptUpdate(context.get(), nullptr, &count, aad.data(),
narrow_int(aad.size(), "CCM AAD")) != 1) ||
EVP_DecryptUpdate(context.get(), output.data(), &written,
input.data(),
narrow_int(input.size(), "CCM ciphertext")) != 1) {
return std::nullopt;
}
} else {
if (EVP_EncryptInit_ex(context.get(), EVP_aes_128_ccm(), nullptr,
nullptr, nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_CCM_SET_IVLEN,
narrow_int(nonce.size(), "CCM nonce"),
nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_CCM_SET_TAG,
narrow_int(tag.size(), "CCM tag"),
nullptr) != 1 ||
EVP_EncryptInit_ex(context.get(), nullptr, nullptr, key.data(),
nonce.data()) != 1 ||
EVP_EncryptUpdate(context.get(), nullptr, &count, nullptr,
narrow_int(input.size(), "CCM plaintext")) != 1 ||
(!aad.empty() &&
EVP_EncryptUpdate(context.get(), nullptr, &count, aad.data(),
narrow_int(aad.size(), "CCM AAD")) != 1) ||
EVP_EncryptUpdate(context.get(), output.data(), &written,
input.data(),
narrow_int(input.size(), "CCM plaintext")) != 1 ||
EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_CCM_GET_TAG,
narrow_int(tag.size(), "CCM tag"),
tag.data()) != 1) {
return std::nullopt;
}
}
output.resize(static_cast<std::size_t>(written));
return output;
#endif
}
struct EapolKey {
Bytes eapol;
std::uint16_t key_info = 0;
int descriptor_version = 0;
bool pairwise = false;
bool install = false;
bool ack = false;
bool mic = false;
bool secure = false;
bool encrypted_key_data = false;
std::uint64_t replay_counter = 0;
Nonce nonce{};
Key16 key_mic{};
Bytes key_data;
};
std::optional<EapolKey> parse_eapol_key(const std::uint8_t* eapol,
std::size_t size) {
if (size < 99 || eapol[1] != 3) return std::nullopt;
const std::size_t body_size = be16(eapol + 2);
if (body_size < 95 || body_size + 4 > size) return std::nullopt;
const auto* body = eapol + 4;
const std::size_t key_data_size = be16(body + 93);
if (95 + key_data_size > body_size) return std::nullopt;
EapolKey result;
result.eapol.assign(eapol, eapol + 4 + body_size);
result.key_info = be16(body + 1);
result.descriptor_version = result.key_info & 7;
result.pairwise = (result.key_info & (1u << 3)) != 0;
result.install = (result.key_info & (1u << 6)) != 0;
result.ack = (result.key_info & (1u << 7)) != 0;
result.mic = (result.key_info & (1u << 8)) != 0;
result.secure = (result.key_info & (1u << 9)) != 0;
result.encrypted_key_data = (result.key_info & (1u << 12)) != 0;
result.replay_counter = be64(body + 5);
std::copy_n(body + 13, result.nonce.size(), result.nonce.begin());
std::copy_n(body + 77, result.key_mic.size(), result.key_mic.begin());
result.key_data.assign(body + 95, body + 95 + key_data_size);
return result;
}
bool eapol_mic_valid(const EapolKey& key, const Key48& ptk) {
if (!key.mic || key.eapol.size() < 97) return false;
Bytes cleared = key.eapol;
std::fill(cleared.begin() + 81, cleared.begin() + 97, 0);
if (key.descriptor_version != 1 && key.descriptor_version != 2)
return false;
const auto algorithm = key.descriptor_version == 1
? HashAlgorithm::Md5 : HashAlgorithm::Sha1;
const auto digest = hmac(algorithm, ptk.data(), 16,
cleared.data(), cleared.size());
return digest.size() >= key.key_mic.size() &&
constant_equal(digest.data(), key.key_mic.data(), key.key_mic.size());
}
std::string eapol_message_name(const EapolKey& key) {
const bool nonce_present = std::any_of(key.nonce.begin(), key.nonce.end(),
[](std::uint8_t value) { return value != 0; });
if (key.pairwise && key.ack && !key.mic) return "M1";
if (key.pairwise && !key.ack && key.mic && nonce_present && !key.secure)
return "M2";
if (key.pairwise && key.ack && key.mic) return "M3";
if (key.pairwise && !key.ack && key.mic && key.secure) return "M4";
if (!key.pairwise && key.ack && key.mic) return "Group-M1";
if (!key.pairwise && !key.ack && key.mic) return "Group-M2";
return "Key-unknown";
}
std::optional<std::pair<const std::uint8_t*,std::size_t>> eapol_from_psdu(
const Bytes& psdu, const DataLayout& layout) {
if (layout.protected_frame || layout.amsdu) return std::nullopt;
std::size_t payload_size = 0;
const auto* payload = payload_begin(psdu, layout, &payload_size);
if (!payload || payload_size < 8 || payload[0] != 0xaa ||
payload[1] != 0xaa || payload[2] != 0x03 || be16(payload + 6) != 0x888e)
return std::nullopt;
return std::pair<const std::uint8_t*,std::size_t>{payload + 8, payload_size - 8};
}
std::pair<Bytes,Bytes> ccmp_aad_nonce(const Bytes& psdu,
const DataLayout& layout,
const std::uint8_t* ccmp) {
std::uint16_t control = layout.frame_control;
control &= static_cast<std::uint16_t>(~(0x0800u | 0x1000u | 0x2000u));
control &= static_cast<std::uint16_t>(~0x0070u);
control |= 0x4000u;
if (layout.qos) control &= static_cast<std::uint16_t>(~0x8000u);
Bytes aad;
aad.reserve(30);
aad.push_back(static_cast<std::uint8_t>(control));
aad.push_back(static_cast<std::uint8_t>(control >> 8));
aad.insert(aad.end(), psdu.begin() + 4, psdu.begin() + 22);
aad.push_back(static_cast<std::uint8_t>(psdu[22] & 0x0f));
aad.push_back(0);
if (layout.has_address4)
aad.insert(aad.end(), psdu.begin() + 24, psdu.begin() + 30);
if (layout.qos) {
aad.push_back(layout.tid);
aad.push_back(0);
}
Bytes nonce;
nonce.reserve(13);
nonce.push_back(layout.qos ? layout.tid : 0);
nonce.insert(nonce.end(), layout.address2.begin(), layout.address2.end());
nonce.push_back(ccmp[7]);
nonce.push_back(ccmp[6]);
nonce.push_back(ccmp[5]);
nonce.push_back(ccmp[4]);
nonce.push_back(ccmp[1]);
nonce.push_back(ccmp[0]);
return {std::move(aad), std::move(nonce)};
}
struct AuthorizedNetwork {
std::string bssid;
Mac bssid_bytes{};
std::string ssid;
Key32 pmk{};
};
std::map<std::string,AuthorizedNetwork> load_authorized_networks(
const std::filesystem::path& path) {
std::map<std::string,AuthorizedNetwork> output;
if (path.empty() || !std::filesystem::exists(path)) return output;
if (std::filesystem::file_size(path) > kMaximumHandshakeFileBytes)
throw std::runtime_error("authorized Wi-Fi key database exceeds 512 KiB");
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("cannot open authorized Wi-Fi key database");
std::string line;
std::size_t line_number = 0;
while (std::getline(input, line)) {
++line_number;
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty() || line.front() == '#') continue;
const auto fields = split_tabs(line);
if (fields.size() != 4 || fields[0] != "v1")
throw std::runtime_error("invalid authorized key row " +
std::to_string(line_number));
AuthorizedNetwork network;
network.bssid = canonical_mac(fields[1]);
if (network.bssid.empty())
throw std::runtime_error("invalid authorized BSSID on row " +
std::to_string(line_number));
network.bssid_bytes = parse_mac_string(network.bssid);
const auto ssid = unhex(fields[2]);
if (ssid.empty() || ssid.size() > 32)
throw std::runtime_error("invalid authorized SSID on row " +
std::to_string(line_number));
network.ssid.assign(ssid.begin(), ssid.end());
network.pmk = unhex_array<32>(fields[3]);
if (!output.emplace(network.bssid, std::move(network)).second)
throw std::runtime_error("duplicate authorized BSSID in key database");
}
return output;
}
void write_atomic(const std::filesystem::path& path, const std::string& contents) {
if (path.empty()) throw std::runtime_error("empty persistent-state path");
if (contents.size() > kMaximumHandshakeFileBytes)
throw std::runtime_error("refusing persistent state larger than 512 KiB");
if (!path.parent_path().empty())
std::filesystem::create_directories(path.parent_path());
auto temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
if (!output) throw std::runtime_error("cannot create persistent-state temporary file");
output.write(contents.data(), static_cast<std::streamsize>(contents.size()));
output.flush();
if (!output) throw std::runtime_error("cannot write persistent-state temporary file");
}
#ifdef _WIN32
HANDLE file = CreateFileW(temporary.c_str(), GENERIC_READ,
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (file != INVALID_HANDLE_VALUE) {
FlushFileBuffers(file);
CloseHandle(file);
}
if (!MoveFileExW(temporary.c_str(), path.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
const DWORD error = GetLastError();
DeleteFileW(temporary.c_str());
throw std::runtime_error("cannot atomically replace persistent state; Win32 error " +
std::to_string(error));
}
#else
const int file = ::open(temporary.c_str(), O_RDONLY | O_CLOEXEC);
if (file < 0) {
std::filesystem::remove(temporary);
throw std::runtime_error(
"cannot open persistent-state temporary file for fsync: " +
std::string(std::strerror(errno)));
}
if (::fsync(file) != 0) {
const int error = errno;
::close(file);
std::filesystem::remove(temporary);
throw std::runtime_error(
"cannot fsync persistent-state temporary file: " +
std::string(std::strerror(error)));
}
::close(file);
std::error_code rename_error;
std::filesystem::rename(temporary, path, rename_error);
if (rename_error) {
std::filesystem::remove(temporary);
throw std::runtime_error(
"cannot atomically replace persistent state: " +
rename_error.message());
}
const auto directory = path.parent_path().empty()
? std::filesystem::current_path() : path.parent_path();
const int directory_file =
::open(directory.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (directory_file >= 0) {
(void)::fsync(directory_file);
::close(directory_file);
}
#endif
}
struct NonceCandidate {
std::uint64_t replay = 0;
Nonce nonce{};
};
struct EapolCandidate {
std::uint64_t replay = 0;
Bytes eapol;
};
struct PairState {
Mac ap{};
Mac station{};
std::deque<NonceCandidate> anonces;
std::deque<EapolCandidate> m2;
std::deque<EapolCandidate> key_messages;
std::deque<Key48> verified_ptks;
};
template <typename T, typename Equal>
bool add_bounded(std::deque<T>& records, T record, Equal equal) {
if (std::any_of(records.begin(), records.end(),
[&](const T& current) { return equal(current, record); }))
return false;
records.push_back(std::move(record));
while (records.size() > kMaximumCandidatesPerType) records.pop_front();
return true;
}
std::string pair_key(const Mac& ap, const Mac& station) {
return mac_string(ap) + '/' + mac_string(station);
}
void remember_hash(std::unordered_set<std::uint64_t>& values,
std::deque<std::uint64_t>& order, std::uint64_t hash,
std::size_t maximum = 8192) {
if (!values.insert(hash).second) return;
order.push_back(hash);
while (order.size() > maximum) {
values.erase(order.front());
order.pop_front();
}
}
} // namespace
Wpa2Pmk wpa2_derive_pmk(const std::string& passphrase,
const std::string& ssid) {
return derive_pmk(passphrase, ssid);
}
Wpa2Ptk wpa2_derive_ptk(const Wpa2Pmk& pmk, const Mac& ap,
const Mac& station, const Wpa2Nonce& anonce,
const Wpa2Nonce& snonce) {
return derive_ptk(pmk, ap, station, anonce, snonce);
}
std::array<std::uint8_t,16> wpa2_eapol_mic(
const Wpa2Key& kck, const Bytes& eapol) {
if (eapol.size() < 97)
throw std::runtime_error("EAPOL-Key is too short for a MIC");
Bytes cleared = eapol;
std::fill(cleared.begin() + 81, cleared.begin() + 97, 0);
const auto digest = hmac(HashAlgorithm::Sha1, kck.data(), kck.size(),
cleared.data(), cleared.size());
std::array<std::uint8_t,16> result{};
if (digest.size() < result.size())
throw std::runtime_error("HMAC-SHA1 result is too short");
std::copy_n(digest.begin(), result.size(), result.begin());
return result;
}
std::optional<Wpa2EapolKey> wpa2_parse_eapol_key(
const std::uint8_t* eapol, std::size_t size) {
const auto parsed = parse_eapol_key(eapol, size);
if (!parsed) return std::nullopt;
Wpa2EapolKey result;
result.eapol = parsed->eapol;
result.key_info = parsed->key_info;
result.descriptor_version = parsed->descriptor_version;
result.pairwise = parsed->pairwise;
result.install = parsed->install;
result.ack = parsed->ack;
result.mic = parsed->mic;
result.secure = parsed->secure;
result.encrypted_key_data = parsed->encrypted_key_data;
result.replay_counter = parsed->replay_counter;
result.nonce = parsed->nonce;
result.key_mic = parsed->key_mic;
result.key_data = parsed->key_data;
return result;
}
bool wpa2_eapol_mic_valid(const Wpa2EapolKey& key,
const Wpa2Ptk& ptk) {
const auto parsed = parse_eapol_key(key.eapol.data(), key.eapol.size());
return parsed && eapol_mic_valid(*parsed, ptk);
}
std::optional<Bytes> wpa2_aes_key_wrap(const Wpa2Key& kek,
const Bytes& plain) {
return aes_key_wrap(kek.data(), kek.size(), plain);
}
std::optional<Bytes> wpa2_aes_key_unwrap(const Wpa2Key& kek,
const Bytes& wrapped) {
return aes_key_unwrap(kek.data(), kek.size(), wrapped);
}
std::optional<Bytes> wpa2_aes_ccm(bool decrypt, const Wpa2Key& key,
const Bytes& nonce, const Bytes& aad,
const Bytes& input, Bytes& tag) {
return aes_ccm(decrypt, key, nonce, aad, input, tag);
}
struct SecurityContext::Impl {
explicit Impl(std::filesystem::path key_database,
std::filesystem::path handshake_database)
: key_database_path(std::move(key_database)),
handshake_database_path(std::move(handshake_database)),
networks(load_authorized_networks(key_database_path)) {
load_handshakes();
for (auto& [key, state] : pairs) {
(void)key;
try_derive(state);
}
dirty = false;
}
PairState& pair(const Mac& ap, const Mac& station) {
const auto key = pair_key(ap, station);
const auto found = pairs.find(key);
if (found != pairs.end()) return found->second;
if (pairs.size() >= kMaximumPairStates) {
auto removable = std::find_if(pairs.begin(), pairs.end(),
[](const auto& item) { return item.second.verified_ptks.empty(); });
if (removable == pairs.end()) removable = pairs.begin();
pairs.erase(removable);
}
PairState state;
state.ap = ap;
state.station = station;
return pairs.emplace(key, std::move(state)).first->second;
}
const AuthorizedNetwork* network(const std::string& bssid) const {
const auto found = networks.find(bssid);
return found == networks.end() ? nullptr : &found->second;
}
void add_group_key(const std::string& bssid, int key_id, const Key16& key) {
auto& keys = group_keys[bssid + '/' + std::to_string(key_id)];
if (std::any_of(keys.begin(), keys.end(), [&](const Key16& existing) {
return constant_equal(existing.data(), key.data(), key.size());
})) return;
keys.push_back(key);
while (keys.size() > 4) keys.pop_front();
}
void extract_group_keys(PairState& state, const EapolKey& message,
const Key48& ptk) {
if (!message.mic || !message.encrypted_key_data ||
message.key_data.empty() || !eapol_mic_valid(message, ptk) ||
message.descriptor_version != 2)
return;
const auto plain = aes_key_unwrap(ptk.data() + 16, 16, message.key_data);
if (!plain) return;
std::size_t offset = 0;
while (offset + 2 <= plain->size()) {
const std::uint8_t element = (*plain)[offset++];
if (element == 0) continue;
if (offset >= plain->size()) break;
const std::size_t length = (*plain)[offset++];
if (offset + length > plain->size()) break;
const auto* value = plain->data() + offset;
if (element == 0xdd && length >= 22 &&
std::memcmp(value, "\x00\x0f\xac\x01", 4) == 0) {
const int key_id = value[4] & 3;
if (length - 6 >= 16) {
Key16 gtk{};
std::copy_n(value + 6, gtk.size(), gtk.begin());
add_group_key(mac_string(state.ap), key_id, gtk);
}
}
offset += length;
}
}
void try_derive(PairState& state) {
const auto* authorized = network(mac_string(state.ap));
if (!authorized) return;
for (const auto& m2_record : state.m2) {
const auto m2 = parse_eapol_key(m2_record.eapol.data(),
m2_record.eapol.size());
if (!m2 || !m2->pairwise || !m2->mic ||
(m2->descriptor_version != 1 && m2->descriptor_version != 2))
continue;
for (const auto& anonce : state.anonces) {
const auto ptk = derive_ptk(authorized->pmk, state.ap, state.station,
anonce.nonce, m2->nonce);
if (!eapol_mic_valid(*m2, ptk)) continue;
const bool known = std::any_of(
state.verified_ptks.begin(), state.verified_ptks.end(),
[&](const Key48& existing) {
return constant_equal(existing.data(), ptk.data(), ptk.size());
});
if (!known) {
state.verified_ptks.push_back(ptk);
while (state.verified_ptks.size() > 4)
state.verified_ptks.pop_front();
}
}
}
for (const auto& record : state.key_messages) {
const auto message = parse_eapol_key(record.eapol.data(),
record.eapol.size());
if (!message) continue;
for (const auto& ptk : state.verified_ptks)
extract_group_keys(state, *message, ptk);
}
}
void load_handshakes() {
if (handshake_database_path.empty() ||
!std::filesystem::exists(handshake_database_path)) return;
const auto bytes = std::filesystem::file_size(handshake_database_path);
if (bytes > kMaximumHandshakeFileBytes)
throw std::runtime_error("persistent handshake database exceeds 512 KiB");
persistent_bytes = bytes;
std::ifstream input(handshake_database_path, std::ios::binary);
if (!input) throw std::runtime_error("cannot open persistent handshake database");
std::string line;
std::size_t line_number = 0;
while (std::getline(input, line)) {
++line_number;
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty() || line.front() == '#') continue;
const auto fields = split_tabs(line);
if (fields.size() != 6 || fields[0] != "v1")
throw std::runtime_error("invalid persistent handshake row " +
std::to_string(line_number));
const std::string ap_text = canonical_mac(fields[2]);
const std::string station_text = canonical_mac(fields[3]);
if (ap_text.empty() || station_text.empty())
throw std::runtime_error("invalid persistent handshake MAC row " +
std::to_string(line_number));
if (!network(ap_text)) continue;
std::size_t consumed = 0;
const std::uint64_t replay = std::stoull(fields[4], &consumed);
if (consumed != fields[4].size())
throw std::runtime_error("invalid replay counter row " +
std::to_string(line_number));
auto& state = pair(parse_mac_string(ap_text),
parse_mac_string(station_text));
bool added = false;
if (fields[1] == "ANONCE") {
const auto nonce = unhex_array<32>(fields[5]);
added = add_bounded(state.anonces, NonceCandidate{replay, nonce},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.nonce == right.nonce;
});
} else if (fields[1] == "M2" || fields[1] == "KEY") {
auto record = EapolCandidate{replay, unhex(fields[5])};
auto& destination = fields[1] == "M2" ? state.m2 : state.key_messages;
added = add_bounded(destination, std::move(record),
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.eapol == right.eapol;
});
} else {
throw std::runtime_error("invalid persistent handshake type row " +
std::to_string(line_number));
}
if (added) ++persistent_records;
}
}
void save_handshakes() {
std::ostringstream output;
output << "# Bounded EAPOL evidence only. No I/Q, general packets, payload logs, or passphrase.\n"
<< "# v1<TAB>ANONCE|M2|KEY<TAB>AP<TAB>station<TAB>replay<TAB>hex-data\n";
std::size_t records = 0;
for (const auto& [key, state] : pairs) {
(void)key;
const std::string ap = mac_string(state.ap);
const std::string station = mac_string(state.station);
for (const auto& record : state.anonces) {
output << "v1\tANONCE\t" << ap << '\t' << station << '\t'
<< record.replay << '\t' << hex(record.nonce) << '\n';
++records;
}
for (const auto& record : state.m2) {
output << "v1\tM2\t" << ap << '\t' << station << '\t'
<< record.replay << '\t'
<< hex(record.eapol.data(), record.eapol.size()) << '\n';
++records;
}
for (const auto& record : state.key_messages) {
output << "v1\tKEY\t" << ap << '\t' << station << '\t'
<< record.replay << '\t'
<< hex(record.eapol.data(), record.eapol.size()) << '\n';
++records;
}
}
write_atomic(handshake_database_path, output.str());
persistent_records = records;
persistent_bytes = std::filesystem::file_size(handshake_database_path);
dirty = false;
}
ProtocolInfo observe(const Bytes& psdu, const DataLayout& layout) {
auto result = inspect_unprotected(psdu, layout);
const auto location = eapol_from_psdu(psdu, layout);
if (!location) return result;
const auto key = parse_eapol_key(location->first, location->second);
if (!key) return result;
result.eapol = true;
result.eapol_message = eapol_message_name(*key);
const std::uint64_t packet_hash = fnv1a64(psdu);
if (seen_eapol.insert(packet_hash).second) {
seen_eapol_order.push_back(packet_hash);
++unique_eapol_messages;
while (seen_eapol_order.size() > 8192) {
seen_eapol.erase(seen_eapol_order.front());
seen_eapol_order.pop_front();
}
}
Mac ap{};
Mac station{};
if (layout.to_ds && !layout.from_ds) {
ap = layout.address1;
station = layout.address2;
} else if (!layout.to_ds && layout.from_ds) {
ap = layout.address2;
station = layout.address1;
} else {
result.summary += " key=" + result.eapol_message + " unsupported DS mapping";
return result;
}
const std::string ap_text = mac_string(ap);
if (!network(ap_text)) {
result.summary += " key=" + result.eapol_message + " no configured credential";
return result;
}
result.authorized_network = true;
auto& state = pair(ap, station);
bool changed = false;
if (result.eapol_message == "M1") {
changed = add_bounded(state.anonces,
NonceCandidate{key->replay_counter, key->nonce},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.nonce == right.nonce;
});
} else if (result.eapol_message == "M2") {
changed = add_bounded(state.m2,
EapolCandidate{key->replay_counter, key->eapol},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.eapol == right.eapol;
});
} else if (result.eapol_message == "M3") {
changed |= add_bounded(state.anonces,
NonceCandidate{key->replay_counter, key->nonce},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.nonce == right.nonce;
});
changed |= add_bounded(state.key_messages,
EapolCandidate{key->replay_counter, key->eapol},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.eapol == right.eapol;
});
} else if (result.eapol_message == "Group-M1") {
changed = add_bounded(state.key_messages,
EapolCandidate{key->replay_counter, key->eapol},
[](const auto& left, const auto& right) {
return left.replay == right.replay && left.eapol == right.eapol;
});
}
if (key->descriptor_version != 1 && key->descriptor_version != 2)
++unsupported_key_descriptors;
if (changed) dirty = true;
try_derive(state);
result.summary += " key=" + result.eapol_message +
" credential=allow-listed PTK=" +
(state.verified_ptks.empty() ? "pending" : "MIC-verified");
return result;
}
ProtocolInfo decrypt(const Bytes& psdu, const DataLayout& layout) {
auto ciphertext = inspect_ciphertext(psdu, layout);
const auto* authorized = network(layout.bssid);
if (!authorized)
return inspect_ciphertext(psdu, layout,
"network not credential-allow-listed");
ciphertext.authorized_network = true;
std::size_t protected_size = 0;
const auto* protected_payload = payload_begin(psdu, layout, &protected_size);
if (!protected_payload || protected_size < 16 ||
(protected_payload[3] & 0x20u) == 0) {
ciphertext.summary = "protected payload is not a complete CCMP MPDU";
return ciphertext;
}
const int key_id = (protected_payload[3] >> 6) & 3;
const std::size_t encrypted_size = protected_size - 16;
Bytes encrypted(protected_payload + 8,
protected_payload + 8 + encrypted_size);
Bytes tag(protected_payload + 8 + encrypted_size,
protected_payload + 16 + encrypted_size);
const auto [aad, nonce] = ccmp_aad_nonce(psdu, layout, protected_payload);
std::vector<Key16> candidates;
if ((layout.address1[0] & 1u) != 0) {
const auto found = group_keys.find(layout.bssid + '/' + std::to_string(key_id));
if (found != group_keys.end())
candidates.assign(found->second.rbegin(), found->second.rend());
} else {
Mac station{};
bool have_station = false;
if (layout.address1 == authorized->bssid_bytes) {
station = layout.address2;
have_station = true;
} else if (layout.address2 == authorized->bssid_bytes) {
station = layout.address1;
have_station = true;
}
if (have_station) {
const auto found = pairs.find(pair_key(authorized->bssid_bytes, station));
if (found != pairs.end()) {
for (auto key = found->second.verified_ptks.rbegin();
key != found->second.verified_ptks.rend(); ++key) {
Key16 temporal{};
std::copy_n(key->begin() + 32, temporal.size(), temporal.begin());
candidates.push_back(temporal);
}
}
}
}
if (candidates.empty())
return inspect_ciphertext(psdu, layout, "no MIC-verified session key");
std::optional<Bytes> plain;
for (const auto& candidate : candidates) {
Bytes candidate_tag = tag;
plain = aes_ccm(true, candidate, nonce, aad, encrypted, candidate_tag);
if (plain) break;
}
const std::uint64_t packet_hash = fnv1a64(psdu);
if (!plain) {
if (seen_failures.find(packet_hash) == seen_failures.end()) {
remember_hash(seen_failures, seen_failure_order, packet_hash);
++authentication_failures;
}
return inspect_ciphertext(psdu, layout, "CCMP tag verification failed");
}
if (seen_decryptions.find(packet_hash) == seen_decryptions.end()) {
remember_hash(seen_decryptions, seen_decryption_order, packet_hash);
++authenticated_decryptions;
}
ProtocolInfo result;
if (layout.more_fragments || layout.fragment_number != 0) {
result.trust = "CCMP-authenticated";
result.layer2 = "802.11 fragment";
result.decrypted = true;
result.integrity_verified = true;
result.summary = "CCMP-authenticated fragmented MSDU; reassembly not yet available";
} else {
result = parse_plain_payload(plain->data(), plain->size(), layout,
"CCMP-authenticated");
}
result.authorized_network = true;
result.ccmp_key_id = ciphertext.ccmp_key_id;
result.ccmp_packet_number = ciphertext.ccmp_packet_number;
return result;
}
SecurityStats snapshot() const {
SecurityStats result;
result.authorized_networks = networks.size();
result.unique_eapol_messages = unique_eapol_messages;
for (const auto& [key, state] : pairs) {
(void)key;
result.verified_pairwise_keys += state.verified_ptks.size();
}
for (const auto& [key, values] : group_keys) {
(void)key;
result.verified_group_keys += values.size();
}
result.authenticated_decryptions = authenticated_decryptions;
result.authentication_failures = authentication_failures;
result.unsupported_key_descriptors = unsupported_key_descriptors;
result.persisted_handshake_records = persistent_records;
result.persistent_bytes = persistent_bytes;
return result;
}
std::filesystem::path key_database_path;
std::filesystem::path handshake_database_path;
std::map<std::string,AuthorizedNetwork> networks;
std::map<std::string,PairState> pairs;
std::map<std::string,std::deque<Key16>> group_keys;
bool dirty = false;
std::size_t persistent_records = 0;
std::uintmax_t persistent_bytes = 0;
std::uint64_t unique_eapol_messages = 0;
std::uint64_t authenticated_decryptions = 0;
std::uint64_t authentication_failures = 0;
std::uint64_t unsupported_key_descriptors = 0;
std::unordered_set<std::uint64_t> seen_eapol;
std::deque<std::uint64_t> seen_eapol_order;
std::unordered_set<std::uint64_t> seen_decryptions;
std::deque<std::uint64_t> seen_decryption_order;
std::unordered_set<std::uint64_t> seen_failures;
std::deque<std::uint64_t> seen_failure_order;
};
SecurityContext::SecurityContext(
const std::filesystem::path& authorized_key_database,
const std::filesystem::path& handshake_database)
: impl_(std::make_unique<Impl>(authorized_key_database, handshake_database)) {}
SecurityContext::~SecurityContext() = default;
SecurityContext::SecurityContext(SecurityContext&&) noexcept = default;
SecurityContext& SecurityContext::operator=(SecurityContext&&) noexcept = default;
ProtocolInfo SecurityContext::observe_unprotected(
const Bytes& psdu, const DataLayout& layout) {
return impl_->observe(psdu, layout);
}
ProtocolInfo SecurityContext::decrypt_protected(
const Bytes& psdu, const DataLayout& layout) {
return impl_->decrypt(psdu, layout);
}
void SecurityContext::persist_if_dirty() {
if (impl_->dirty) impl_->save_handshakes();
}
SecurityStats SecurityContext::stats() const {
return impl_->snapshot();
}
void provision_authorized_network(
const std::filesystem::path& authorized_key_database,
const std::string& ssid,
const std::string& bssid,
const std::string& passphrase) {
if (ssid.empty() || ssid.size() > 32)
throw std::runtime_error("SSID must contain 1 through 32 bytes");
if (passphrase.size() < 8 || passphrase.size() > 63)
throw std::runtime_error("WPA2 passphrase must contain 8 through 63 bytes");
const std::string normalized_bssid = canonical_mac(bssid);
if (normalized_bssid.empty())
throw std::runtime_error("BSSID must be a six-byte MAC address");
auto networks = load_authorized_networks(authorized_key_database);
AuthorizedNetwork network;
network.bssid = normalized_bssid;
network.bssid_bytes = parse_mac_string(normalized_bssid);
network.ssid = ssid;
network.pmk = derive_pmk(passphrase, ssid);
networks[normalized_bssid] = network;
std::ostringstream output;
output << "# Private authorized-network PMKs. Passphrases are never stored.\n"
<< "# v1<TAB>BSSID<TAB>SSID-hex<TAB>PMK-hex\n";
for (const auto& [key, value] : networks) {
(void)key;
output << "v1\t" << value.bssid << '\t'
<< hex(reinterpret_cast<const std::uint8_t*>(value.ssid.data()),
value.ssid.size())
<< '\t' << hex(value.pmk) << '\n';
}
write_atomic(authorized_key_database, output.str());
#ifdef _WIN32
SecureZeroMemory(network.pmk.data(), network.pmk.size());
#else
OPENSSL_cleanse(network.pmk.data(), network.pmk.size());
#endif
}
void self_test() {
const auto require = [](bool condition, const char* message) {
if (!condition) throw std::runtime_error(std::string("Wi-Fi self-test: ") + message);
};
const auto pmk = derive_pmk("password", "IEEE");
require(hex(pmk) ==
"f42c6fc52df0ebef9ebb4b90b38a5f90"
"2e83fe1b135a70e23aed762e9710a12e",
"PBKDF2-HMAC-SHA1 vector failed");
const Mac ap = unhex_array<6>("001122334455");
const Mac station = unhex_array<6>("66778899aabb");
const Nonce anonce = unhex_array<32>(
"000102030405060708090a0b0c0d0e0f"
"101112131415161718191a1b1c1d1e1f");
const Nonce snonce = unhex_array<32>(
"202122232425262728292a2b2c2d2e2f"
"303132333435363738393a3b3c3d3e3f");
const auto ptk = derive_ptk(pmk, ap, station, anonce, snonce);
require(hex(ptk) ==
"85c98eca56145629359ac8830bb66a59"
"c5562d473fddcb4eee9ce4de54e1cb1a"
"12cdd4448325c84079abcd76b1b89f8f",
"WPA PRF PTK vector failed");
const auto kek = unhex_array<16>("000102030405060708090a0b0c0d0e0f");
const auto unwrapped = aes_key_unwrap(
kek.data(), kek.size(),
unhex("1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"));
require(unwrapped && hex(unwrapped->data(), unwrapped->size()) ==
"00112233445566778899aabbccddeeff",
"RFC 3394 AES key-unwrap vector failed");
const Key16 ccm_key = unhex_array<16>("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf");
const Bytes ccm_nonce = unhex("00000003020100a0a1a2a3a4a5");
const Bytes ccm_aad = unhex("0001020304050607");
const Bytes ccm_plain = unhex(
"08090a0b0c0d0e0f101112131415161718191a1b1c1d1e");
Bytes ccm_tag(8, 0);
const auto ccm_cipher = aes_ccm(false, ccm_key, ccm_nonce, ccm_aad,
ccm_plain, ccm_tag);
require(ccm_cipher &&
hex(ccm_cipher->data(), ccm_cipher->size()) ==
"588c979a61c663d2f066d0c2c0f989806d5f6b61dac384" &&
hex(ccm_tag.data(), ccm_tag.size()) == "17e8d12cfdf926e0",
"RFC 3610 AES-CCM encryption vector failed");
const auto ccm_round_trip = aes_ccm(true, ccm_key, ccm_nonce, ccm_aad,
*ccm_cipher, ccm_tag);
require(ccm_round_trip && *ccm_round_trip == ccm_plain,
"AES-CCM authenticated decryption failed");
Bytes bad_tag = ccm_tag;
bad_tag[0] ^= 1;
require(!aes_ccm(true, ccm_key, ccm_nonce, ccm_aad, *ccm_cipher, bad_tag),
"AES-CCM accepted a modified authentication tag");
Bytes dhcp(240, 0);
dhcp[0] = 1;
dhcp[1] = 1;
dhcp[2] = 6;
dhcp[4] = 0x12;
dhcp[5] = 0x34;
dhcp[6] = 0x56;
dhcp[7] = 0x78;
std::copy(station.begin(), station.end(), dhcp.begin() + 28);
const std::array<std::uint8_t,4> cookie = {0x63,0x82,0x53,0x63};
std::copy(cookie.begin(), cookie.end(), dhcp.begin() + 236);
const auto append = [&dhcp](std::initializer_list<std::uint8_t> bytes) {
dhcp.insert(dhcp.end(), bytes.begin(), bytes.end());
};
append({53,1,1,12,8});
const std::string host = "lab-node";
dhcp.insert(dhcp.end(), host.begin(), host.end());
append({50,4,192,168,1,77,255});
Bytes udp(8 + dhcp.size(), 0);
put_be16(udp.data(), 68);
put_be16(udp.data() + 2, 67);
put_be16(udp.data() + 4, static_cast<std::uint16_t>(udp.size()));
std::copy(dhcp.begin(), dhcp.end(), udp.begin() + 8);
const std::array<std::uint8_t,4> ip_source = {0,0,0,0};
const std::array<std::uint8_t,4> ip_destination = {255,255,255,255};
std::uint32_t udp_pseudo = checksum_sum(ip_source.data(), ip_source.size());
udp_pseudo = checksum_sum(ip_destination.data(), ip_destination.size(), udp_pseudo);
const std::array<std::uint8_t,4> udp_tail = {
0,17,static_cast<std::uint8_t>(udp.size() >> 8),
static_cast<std::uint8_t>(udp.size())};
udp_pseudo = checksum_sum(udp_tail.data(), udp_tail.size(), udp_pseudo);
std::uint16_t udp_checksum = checksum_create(udp.data(), udp.size(), udp_pseudo);
if (udp_checksum == 0) udp_checksum = 0xffff;
put_be16(udp.data() + 6, udp_checksum);
Bytes ip(20 + udp.size(), 0);
ip[0] = 0x45;
put_be16(ip.data() + 2, static_cast<std::uint16_t>(ip.size()));
put_be16(ip.data() + 4, 0x2468);
ip[8] = 64;
ip[9] = 17;
std::copy(ip_source.begin(), ip_source.end(), ip.begin() + 12);
std::copy(ip_destination.begin(), ip_destination.end(), ip.begin() + 16);
put_be16(ip.data() + 10, checksum_create(ip.data(), 20));
std::copy(udp.begin(), udp.end(), ip.begin() + 20);
Bytes psdu(24, 0);
psdu[0] = 0x08;
psdu[1] = 0x01;
std::copy(ap.begin(), ap.end(), psdu.begin() + 4);
std::copy(station.begin(), station.end(), psdu.begin() + 10);
std::fill(psdu.begin() + 16, psdu.begin() + 22, 0xff);
const std::array<std::uint8_t,8> llc = {0xaa,0xaa,0x03,0,0,0,0x08,0x00};
psdu.insert(psdu.end(), llc.begin(), llc.end());
psdu.insert(psdu.end(), ip.begin(), ip.end());
psdu.insert(psdu.end(), 4, 0);
const auto layout = parse_data_layout(psdu);
require(layout && layout->source == "66:77:88:99:aa:bb" &&
layout->bssid == "00:11:22:33:44:55",
"802.11 data address mapping failed");
const auto decoded = inspect_unprotected(psdu, *layout);
require(decoded.application == "DHCP" && decoded.dhcp.size() == 1,
"synthetic DHCP frame was not decoded");
require(decoded.checksum_status.find("IPv4=valid") != std::string::npos &&
decoded.checksum_status.find("UDP=valid") != std::string::npos,
"synthetic IPv4/UDP checksum validation failed");
require(decoded.dhcp[0].message_type == "DISCOVER" &&
decoded.dhcp[0].host_name == "lab-node" &&
decoded.dhcp[0].requested_ipv4 == "192.168.1.77",
"synthetic DHCP fields were decoded incorrectly");
}
} // namespace gf::wifi
tools/wifi_protocol.hpp · 182 lines
Download this file · Permanent section link
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace gf::wifi {
struct DataLayout {
std::uint16_t frame_control = 0;
bool to_ds = false;
bool from_ds = false;
bool more_fragments = false;
bool protected_frame = false;
bool ordered = false;
bool has_address4 = false;
bool qos = false;
bool amsdu = false;
std::uint8_t tid = 0;
std::uint8_t fragment_number = 0;
std::size_t header_bytes = 0;
std::array<std::uint8_t,6> address1{};
std::array<std::uint8_t,6> address2{};
std::array<std::uint8_t,6> address3{};
std::array<std::uint8_t,6> address4{};
std::string receiver;
std::string transmitter;
std::string source;
std::string destination;
std::string bssid;
};
struct DhcpObservation {
std::string client_mac;
std::string message_type;
std::string host_name;
std::string fqdn;
std::string requested_ipv4;
std::string offered_ipv4;
std::string client_ipv4;
std::string server_ipv4;
std::string vendor_class;
std::uint32_t transaction_id = 0;
};
struct ProtocolInfo {
std::string trust = "none";
std::string layer2 = "-";
std::string network = "-";
std::string transport = "-";
std::string application = "-";
std::string source_ip;
std::string destination_ip;
int source_port = -1;
int destination_port = -1;
std::string checksum_status;
std::string summary;
std::size_t payload_bytes = 0;
std::string payload_hex;
std::string payload_ascii;
bool payload_truncated = false;
bool decrypted = false;
bool integrity_verified = false;
bool eapol = false;
std::string eapol_message;
bool authorized_network = false;
int ccmp_key_id = -1;
std::uint64_t ccmp_packet_number = 0;
// Complete, field-decoded headers for every network/transport packet
// recovered from this MPDU. A-MSDU subframes are retained separately.
std::vector<std::string> headers;
std::vector<DhcpObservation> dhcp;
};
struct SecurityStats {
std::size_t authorized_networks = 0;
std::uint64_t unique_eapol_messages = 0;
std::size_t verified_pairwise_keys = 0;
std::size_t verified_group_keys = 0;
std::uint64_t authenticated_decryptions = 0;
std::uint64_t authentication_failures = 0;
std::uint64_t unsupported_key_descriptors = 0;
std::size_t persisted_handshake_records = 0;
std::uintmax_t persistent_bytes = 0;
};
using Wpa2Nonce = std::array<std::uint8_t,32>;
using Wpa2Pmk = std::array<std::uint8_t,32>;
using Wpa2Ptk = std::array<std::uint8_t,48>;
using Wpa2Key = std::array<std::uint8_t,16>;
struct Wpa2EapolKey {
std::vector<std::uint8_t> eapol;
std::uint16_t key_info = 0;
int descriptor_version = 0;
bool pairwise = false;
bool install = false;
bool ack = false;
bool mic = false;
bool secure = false;
bool encrypted_key_data = false;
std::uint64_t replay_counter = 0;
Wpa2Nonce nonce{};
Wpa2Key key_mic{};
std::vector<std::uint8_t> key_data;
};
// Reusable WPA2-PSK/CCMP primitives for the AP and capture engines. Every
// authenticated operation returns an explicit success/failure result.
Wpa2Pmk wpa2_derive_pmk(const std::string& passphrase,
const std::string& ssid);
Wpa2Ptk wpa2_derive_ptk(const Wpa2Pmk& pmk,
const std::array<std::uint8_t,6>& ap,
const std::array<std::uint8_t,6>& station,
const Wpa2Nonce& anonce,
const Wpa2Nonce& snonce);
std::array<std::uint8_t,16> wpa2_eapol_mic(
const Wpa2Key& kck, const std::vector<std::uint8_t>& eapol);
std::optional<Wpa2EapolKey> wpa2_parse_eapol_key(
const std::uint8_t* eapol, std::size_t size);
bool wpa2_eapol_mic_valid(const Wpa2EapolKey& key,
const Wpa2Ptk& ptk);
std::optional<std::vector<std::uint8_t>> wpa2_aes_key_wrap(
const Wpa2Key& kek, const std::vector<std::uint8_t>& plain);
std::optional<std::vector<std::uint8_t>> wpa2_aes_key_unwrap(
const Wpa2Key& kek, const std::vector<std::uint8_t>& wrapped);
std::optional<std::vector<std::uint8_t>> wpa2_aes_ccm(
bool decrypt, const Wpa2Key& key,
const std::vector<std::uint8_t>& nonce,
const std::vector<std::uint8_t>& aad,
const std::vector<std::uint8_t>& input,
std::vector<std::uint8_t>& tag);
std::optional<DataLayout> parse_data_layout(
const std::vector<std::uint8_t>& psdu);
ProtocolInfo inspect_unprotected(
const std::vector<std::uint8_t>& psdu,
const DataLayout& layout);
ProtocolInfo inspect_ciphertext(
const std::vector<std::uint8_t>& psdu,
const DataLayout& layout,
std::string reason = "no verified session key");
class SecurityContext {
public:
SecurityContext(const std::filesystem::path& authorized_key_database,
const std::filesystem::path& handshake_database);
~SecurityContext();
SecurityContext(SecurityContext&&) noexcept;
SecurityContext& operator=(SecurityContext&&) noexcept;
SecurityContext(const SecurityContext&) = delete;
SecurityContext& operator=(const SecurityContext&) = delete;
ProtocolInfo observe_unprotected(const std::vector<std::uint8_t>& psdu,
const DataLayout& layout);
ProtocolInfo decrypt_protected(const std::vector<std::uint8_t>& psdu,
const DataLayout& layout);
void persist_if_dirty();
SecurityStats stats() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
void provision_authorized_network(
const std::filesystem::path& authorized_key_database,
const std::string& ssid,
const std::string& bssid,
const std::string& passphrase);
void self_test();
} // namespace gf::wifi
vendor_uhd_4_9/LICENSE.md · 643 lines
Download this file · Permanent section link
# Ettus Research FPGA Repository: Licenses
Please refer to the individual files to verify which license applies to them.
UHD and MPM themselves, are, by default, licensed under the GPLv3. If you wish
to obtain an alternative license, please contact info@ettus.com. Some of UHD's
dependencies are shipped as part of the repository
(see https://github.com/EttusResearch/uhd/tree/master/host/lib/deps for details).
The FPGA codebase (stored under fpga/) has its own
[licensing situation][fpga-license]. See the various subdirectories therein for
more details.
Individual source files in this repository may have different licenses than
discussed above. Please refer to individual files and directories to verify
which license applies to which file.
## GPLv3 License Text (applies to most of UHD and MPM)
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
[fpga-license]: https://github.com/EttusResearch/uhd/blob/master/fpga/usrp3/LICENSE.md
vendor_uhd_4_9/fpga/usrp3/LICENSE.md · 179 lines
Download this file · Permanent section link
# Ettus Research USRP3 FPGA Repository: Licenses
All code written by Ettus Research within this usrp3 subdirectory of the FPGA
repository is licensed as LGPLv3. This repository also contains code from other
sources, which is marked appropriately and may be licensed differently.
If you wish to obtain an alternative license on the components owned by Ettus
Research / National Instruments, please contact info@ettus.com.
## LGPL License Text
Source: https://www.gnu.org/licenses/lgpl-3.0.txt
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
vendor_uhd_4_9/fpga/usrp3/lib/control/synchronizer.v · 77 lines
Download this file · Permanent section link
//
// Copyright 2021 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: synchronizer.v
//
// Description:
//
// This is a double-synchronizer module. However, it can can implement a
// synchronizer of any depth (double, triple, etc.) and width.
//
// A double synchronizer is typically used to cross a single-bit glitch-free
// signal from one clock domain to another.
//
// *WARNING*: The input signal must be glitch-free. In other words, it should
// be driven by a register and NOT combinational logic. Otherwise
// you could capture a glitch instead of the intended signal.
//
// *WARNING*: When WIDTH is not 1, the multiple bits are not guaranteed to be
// coherent. In other words, they can arrive on the output at
// different times. This module should not usually be used to
// cross a multi-bit signal. Consider using the handshake module
// instead.
//
// When crossing between unrelated clock domains, we typically don't want the
// timing analyzer to consider the path between clock domains. To make
// writing this constraint easier, the FALSE_PATH_TO_IN parameter controls
// the name of the synchronizer_impl instance. The following XDC constraint
// is used to ignore all instances of this false path.
//
// set_false_path -to [get_pins -hierarchical -filter \
// {NAME =~ */synchronizer_false_path/stages[0].value_reg[0][*]/D}]
//
// Parameters:
//
// WIDTH : Width of the synchronizer (1 by default).
// STAGES : Number of synchronizer stages (2 by default, for a
// standard double-synchronizer).
// INITIAL_VAL : Initial value of the output register (0 by default).
// FALSE_PATH_TO_IN : Set to 1 if the input should be considered a false path
// and ignored by the timing analyzer. Set to 0 to let the
// tool analyze this path.
//
`default_nettype none
module synchronizer #(
parameter WIDTH = 1,
parameter STAGES = 2,
parameter INITIAL_VAL = 0,
parameter FALSE_PATH_TO_IN = 1
)(
input wire clk,
input wire rst,
input wire [WIDTH-1:0] in,
output wire [WIDTH-1:0] out
);
generate if (FALSE_PATH_TO_IN == 1) begin
synchronizer_impl #(
.WIDTH(WIDTH), .STAGES(STAGES), .INITIAL_VAL(INITIAL_VAL)
) synchronizer_false_path (
.clk(clk), .rst(rst), .in(in), .out(out)
);
end else begin
synchronizer_impl #(
.WIDTH(WIDTH), .STAGES(STAGES), .INITIAL_VAL(INITIAL_VAL)
) synchronizer_constrained (
.clk(clk), .rst(rst), .in(in), .out(out)
);
end endgenerate
endmodule //synchronizer
//
`default_nettype wire
vendor_uhd_4_9/fpga/usrp3/lib/control/synchronizer_impl.v · 52 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research LLC
// Copyright 2018 Ettus Research, a National Instruments Company
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
`default_nettype none
module synchronizer_impl #(
parameter WIDTH = 1,
parameter STAGES = 2,
parameter INITIAL_VAL = 0
)(
input wire clk,
input wire rst,
input wire [WIDTH-1:0] in,
output wire [WIDTH-1:0] out
);
(* ASYNC_REG = "TRUE" *) reg [WIDTH-1:0] value[0:STAGES-1];
integer k;
initial begin
for (k = 0; k < STAGES; k = k + 1) begin
value[k] = INITIAL_VAL;
end
end
genvar i;
generate
for (i=0; i<STAGES; i=i+1) begin: stages
always @(posedge clk) begin
if (rst) begin
value[i] <= INITIAL_VAL;
end else begin
if (i == 0) begin
value[i] <= in;
end else begin
value[i] <= value[i-1];
end
end
end
end
endgenerate
assign out = value[STAGES-1];
endmodule //synchronizer_impl
`default_nettype wire
vendor_uhd_4_9/fpga/usrp3/top/e31x/spi_slave.v · 108 lines
Download this file · Permanent section link
//
// Copyright 2015 Ettus Research LLC
//
`ifndef LOG2
`define LOG2(N) (\
N < 2 ? 0 : \
N < 4 ? 1 : \
N < 8 ? 2 : \
N < 16 ? 3 : \
N < 32 ? 4 : \
N < 64 ? 5 : \
N < 128 ? 6 : \
N < 256 ? 7 : \
N < 512 ? 8 : \
N < 1024 ? 9 : \
10)
`endif
module spi_slave
#(
parameter DEPTH = 64
)
(
// sys connect
input clk,
input rst,
// spi slave port
input ss,
input mosi,
output miso,
input sck,
// parallel data io port
output parallel_stb,
input [DEPTH-1:0] parallel_din,
output [DEPTH-1:0] parallel_dout
);
reg mosi_d, mosi_q;
reg ss_d, ss_q;
reg sck_d, sck_q;
reg sck_old_d, sck_old_q;
reg miso_d, miso_q;
reg [DEPTH-1:0] data_d, data_q;
reg parallel_stb_d, parallel_stb_q;
reg [`LOG2(DEPTH)-1:0] bit_ct_d, bit_ct_q;
reg [DEPTH-1:0] parallel_dout_d, parallel_dout_q;
assign miso = miso_q;
assign parallel_stb = parallel_stb_q;
assign parallel_dout = parallel_dout_q;
always @(*) begin
ss_d = ss;
mosi_d = mosi;
miso_d = miso_q;
sck_d = sck;
sck_old_d = sck_q;
data_d = data_q;
parallel_stb_d = 1'b0;
bit_ct_d = bit_ct_q;
parallel_dout_d = parallel_dout_q;
if (ss_q) begin
bit_ct_d = 'h0;
data_d = parallel_din;
miso_d = data_q[DEPTH-1];
end
else begin
if (!sck_old_q && sck_q) begin // rising edge
data_d = {data_q[DEPTH-1-1:0], mosi_q};
bit_ct_d = bit_ct_q + 1'b1;
if (bit_ct_q == (DEPTH - 1)) begin
parallel_dout_d = {data_q[DEPTH-1-1:0], mosi_q};
parallel_stb_d = 1'b1;
data_d = parallel_din;
end
end
else if (sck_old_q && !sck_q) begin // falling edge
miso_d = data_q[DEPTH-1];
end
end
end
always @(posedge clk) begin
if (rst) begin
parallel_stb_q <= 1'b0;
bit_ct_q <= 'h0;
parallel_dout_q <= 'h0;
miso_q <= 1'b1;
end else begin
parallel_stb_q <= parallel_stb_d;
bit_ct_q <= bit_ct_d;
parallel_dout_q <= parallel_dout_d;
miso_q <= miso_d;
end
sck_q <= sck_d;
mosi_q <= mosi_d;
ss_q <= ss_d;
data_q <= data_d;
sck_old_q <= sck_old_d;
end
endmodule
wifi_e310_link/build_windows_packet_host.cmd · 29 lines
Download this file · Permanent section link
@echo off
setlocal
rem An alternate directory builds a candidate without relinking the live EXE.
set "BUILD_DIR=%~dp0build\windows-packets"
if not "%~1"=="" set "BUILD_DIR=%~f1"
if not "%~2"=="" exit /b 2
set "VSWHERE=C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" exit /b 2
for /f "usebackq delims=" %%I in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSROOT=%%I"
if not defined VSROOT exit /b 2
call "%VSROOT%\VC\Auxiliary\Build\vcvars64.bat" >nul
if errorlevel 1 exit /b %errorlevel%
cmake.exe -S "%~dp0host\windows" -B "%BUILD_DIR%" -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
if errorlevel 1 exit /b %errorlevel%
cmake.exe --build "%BUILD_DIR%" --config Release
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_windows_protocol_selftest.exe"
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_packet_wire_selftest.exe"
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_rx_event_selftest.exe"
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_counter_snapshot_test.exe"
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_packet_tx_wait_selftest.exe"
if errorlevel 1 exit /b %errorlevel%
"%BUILD_DIR%\gf_e310_windows_ap.exe" --self-test
exit /b %errorlevel%
wifi_e310_link/fpga/open_e310/e310_open_shell.xdc · 201 lines
Download this file · Permanent section link
# Exact stock E310 package contract for the open Wi-Fi baseband shell.
# Pin names and I/O standards are copied from UHD's e31x_pins.xdc.
# AD9361 DATA_CLK is 40 MHz in the required 20 MS/s 2R2T mode. Constrain the
# BUFG output net used by the actual radio logic; nextpnr does not propagate a
# port-level clock declaration through BUFG automatically. FCLK0/GP0 uses the
# separate 100 MHz command-line default. TX data, frame, and forwarded clock
# are all launched by stock-compatible I/O-site ODDRs in this same domain.
create_clock -name radio_clk -period 25.000 [get_nets radio_clk]
set_property LOC AB5 [get_ports {CAT_CTRL_OUT[0]}]
set_property LOC AB6 [get_ports {CAT_CTRL_OUT[1]}]
set_property LOC AB7 [get_ports {CAT_CTRL_OUT[2]}]
set_property LOC AA4 [get_ports {CAT_CTRL_OUT[3]}]
set_property LOC K20 [get_ports {CAT_CTRL_OUT[4]}]
set_property LOC L19 [get_ports {CAT_CTRL_OUT[5]}]
set_property LOC V12 [get_ports {CAT_CTRL_OUT[6]}]
set_property LOC W12 [get_ports {CAT_CTRL_OUT[7]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[3]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[4]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[5]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[6]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_OUT[7]}]
set_property LOC V4 [get_ports {CAT_CTRL_IN[0]}]
set_property LOC V5 [get_ports {CAT_CTRL_IN[1]}]
set_property LOC U5 [get_ports {CAT_CTRL_IN[2]}]
set_property LOC U6 [get_ports {CAT_CTRL_IN[3]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_IN[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_IN[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_IN[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_CTRL_IN[3]}]
set_property LOC U11 [get_ports CAT_RESET]
set_property LOC W6 [get_ports CAT_CS]
set_property LOC W5 [get_ports CAT_SCLK]
set_property LOC V7 [get_ports CAT_MOSI]
set_property LOC W7 [get_ports CAT_MISO]
set_property LOC AB1 [get_ports CAT_TXNRX]
set_property LOC AB4 [get_ports CAT_ENABLE]
set_property LOC AB2 [get_ports CAT_ENAGC]
set_property LOC T16 [get_ports CAT_SYNC]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_RESET]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_CS]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_SCLK]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_MOSI]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_MISO]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_TXNRX]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_ENABLE]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_ENAGC]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_SYNC]
set_property LOC L18 [get_ports CAT_DATA_CLK]
set_property LOC N18 [get_ports CAT_RX_FRAME]
set_property LOC R21 [get_ports CAT_FB_CLK]
set_property LOC P22 [get_ports CAT_TX_FRAME]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_DATA_CLK]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_RX_FRAME]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_FB_CLK]
set_property IOSTANDARD LVCMOS18 [get_ports CAT_TX_FRAME]
set_property LOC L16 [get_ports {CAT_P0_D[0]}]
set_property LOC K18 [get_ports {CAT_P0_D[1]}]
set_property LOC L17 [get_ports {CAT_P0_D[2]}]
set_property LOC J17 [get_ports {CAT_P0_D[3]}]
set_property LOC M16 [get_ports {CAT_P0_D[4]}]
set_property LOC J15 [get_ports {CAT_P0_D[5]}]
set_property LOC K16 [get_ports {CAT_P0_D[6]}]
set_property LOC J16 [get_ports {CAT_P0_D[7]}]
set_property LOC N20 [get_ports {CAT_P0_D[8]}]
set_property LOC K15 [get_ports {CAT_P0_D[9]}]
set_property LOC N17 [get_ports {CAT_P0_D[10]}]
set_property LOC M17 [get_ports {CAT_P0_D[11]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[3]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[4]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[5]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[6]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[7]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[8]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[9]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[10]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P0_D[11]}]
set_property LOC N19 [get_ports {CAT_P1_D[0]}]
set_property LOC M21 [get_ports {CAT_P1_D[1]}]
set_property LOC P20 [get_ports {CAT_P1_D[2]}]
set_property LOC P15 [get_ports {CAT_P1_D[3]}]
set_property LOC P17 [get_ports {CAT_P1_D[4]}]
set_property LOC P18 [get_ports {CAT_P1_D[5]}]
set_property LOC R20 [get_ports {CAT_P1_D[6]}]
set_property LOC P21 [get_ports {CAT_P1_D[7]}]
set_property LOC M22 [get_ports {CAT_P1_D[8]}]
set_property LOC T17 [get_ports {CAT_P1_D[9]}]
set_property LOC N22 [get_ports {CAT_P1_D[10]}]
set_property LOC N15 [get_ports {CAT_P1_D[11]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[3]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[4]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[5]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[6]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[7]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[8]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[9]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[10]}]
set_property IOSTANDARD LVCMOS18 [get_ports {CAT_P1_D[11]}]
set_property LOC H19 [get_ports {TX_BANDSEL[2]}]
set_property LOC F21 [get_ports {TX_BANDSEL[1]}]
set_property LOC F22 [get_ports {TX_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {TX_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {TX_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {TX_BANDSEL[2]}]
set_property LOC G20 [get_ports {RX1_BANDSEL[2]}]
set_property LOC G21 [get_ports {RX1_BANDSEL[1]}]
set_property LOC E19 [get_ports {RX1_BANDSEL[0]}]
set_property LOC AA8 [get_ports {RX2_BANDSEL[2]}]
set_property LOC AA9 [get_ports {RX2_BANDSEL[1]}]
set_property LOC AB9 [get_ports {RX2_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1_BANDSEL[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2_BANDSEL[2]}]
set_property LOC G19 [get_ports {RX1B_BANDSEL[1]}]
set_property LOC F19 [get_ports {RX1B_BANDSEL[0]}]
set_property LOC B15 [get_ports {RX1C_BANDSEL[1]}]
set_property LOC A19 [get_ports {RX1C_BANDSEL[0]}]
set_property LOC U9 [get_ports {RX2B_BANDSEL[1]}]
set_property LOC Y4 [get_ports {RX2B_BANDSEL[0]}]
set_property LOC U10 [get_ports {RX2C_BANDSEL[1]}]
set_property LOC Y10 [get_ports {RX2C_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1B_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1B_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1C_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RX1C_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2B_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2B_BANDSEL[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2C_BANDSEL[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {RX2C_BANDSEL[1]}]
set_property LOC G22 [get_ports TX_ENABLE1A]
set_property LOC A17 [get_ports TX_ENABLE1B]
set_property LOC H22 [get_ports TX_ENABLE2A]
set_property LOC B16 [get_ports TX_ENABLE2B]
set_property IOSTANDARD LVCMOS33 [get_ports TX_ENABLE1A]
set_property IOSTANDARD LVCMOS33 [get_ports TX_ENABLE1B]
set_property IOSTANDARD LVCMOS33 [get_ports TX_ENABLE2A]
set_property IOSTANDARD LVCMOS33 [get_ports TX_ENABLE2B]
set_property LOC C15 [get_ports VCTXRX1_V1]
set_property LOC B17 [get_ports VCTXRX1_V2]
set_property LOC A16 [get_ports VCTXRX2_V1]
set_property LOC E20 [get_ports VCTXRX2_V2]
set_property LOC E18 [get_ports VCRX1_V1]
set_property LOC F18 [get_ports VCRX1_V2]
set_property LOC F17 [get_ports VCRX2_V1]
set_property LOC G17 [get_ports VCRX2_V2]
set_property IOSTANDARD LVCMOS33 [get_ports VCTXRX1_V1]
set_property IOSTANDARD LVCMOS33 [get_ports VCTXRX1_V2]
set_property IOSTANDARD LVCMOS33 [get_ports VCTXRX2_V1]
set_property IOSTANDARD LVCMOS33 [get_ports VCTXRX2_V2]
set_property IOSTANDARD LVCMOS33 [get_ports VCRX1_V1]
set_property IOSTANDARD LVCMOS33 [get_ports VCRX1_V2]
set_property IOSTANDARD LVCMOS33 [get_ports VCRX2_V1]
set_property IOSTANDARD LVCMOS33 [get_ports VCRX2_V2]
set_property LOC Y11 [get_ports LED_TXRX1_TX]
set_property LOC AB10 [get_ports LED_TXRX1_RX]
set_property LOC AA12 [get_ports LED_RX1_RX]
set_property LOC U12 [get_ports LED_TXRX2_TX]
set_property LOC AB11 [get_ports LED_TXRX2_RX]
set_property LOC AA11 [get_ports LED_RX2_RX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_TXRX1_TX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_TXRX1_RX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_RX1_RX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_TXRX2_TX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_TXRX2_RX]
set_property IOSTANDARD LVCMOS18 [get_ports LED_RX2_RX]
# E310 board power-management SPI, not the AD9361 programming SPI.
set_property PACKAGE_PIN A22 [get_ports AVR_CS_R]
set_property IOSTANDARD LVCMOS33 [get_ports AVR_CS_R]
set_property PACKAGE_PIN B22 [get_ports AVR_IRQ]
set_property IOSTANDARD LVCMOS33 [get_ports AVR_IRQ]
set_property PACKAGE_PIN C22 [get_ports AVR_MISO_R]
set_property IOSTANDARD LVCMOS33 [get_ports AVR_MISO_R]
set_property PACKAGE_PIN A21 [get_ports AVR_MOSI_R]
set_property IOSTANDARD LVCMOS33 [get_ports AVR_MOSI_R]
set_property PACKAGE_PIN D22 [get_ports AVR_SCK_R]
set_property IOSTANDARD LVCMOS33 [get_ports AVR_SCK_R]
wifi_e310_link/fpga/open_e310/gf_barker_fit.sv · 22 lines
Download this file · Permanent section link
// Fit-only registered boundary; not the hardware radio top.
module gf_barker_fit_top(
input wire clk,reset,sample_valid,
input wire [15:0] sample,
output wire [23:0] correlation,
output wire result_valid
);
wire valid_r,next_valid;
wire [15:0] sample_r;
wire [23:0] next_correlation;
gf_serial_reg v(.clk(clk),.reset(reset),.d(sample_valid),.q(valid_r));
generate for(genvar n=0;n<16;n=n+1)begin:g_input
gf_serial_reg r(.clk(clk),.reset(reset),.d(sample[n]),.q(sample_r[n]));
end endgenerate
gf_dsss_barker_recurrence graph(.clk(clk),.clear(reset),.sample_valid(valid_r),
.sample(sample_r),.correlation(next_correlation),.result_valid(next_valid));
generate for(genvar n=0;n<24;n=n+1)begin:g_output
gf_serial_reg r(.clk(clk),.reset(reset),.d(next_correlation[n]),.q(correlation[n]));
end endgenerate
gf_serial_reg out_v(.clk(clk),.reset(reset),.d(next_valid),.q(result_valid));
endmodule
wifi_e310_link/fpga/open_e310/gf_barker_radio_bridge.sv · 71 lines
Download this file · Permanent section link
// 40-MHz IQ16 sample clock <-> phase-related 320-MHz serial correlator.
// Input toggle synchronizer qualifies a bundled word held for 50 ns. Output
// capture uses the FALLING radio edge, away from the qualified fast-word edge.
// See constrain_barker_radio_bridge.tcl for the explicit settling budgets.
`timescale 1ns/1ps
module gf_barker_radio_bridge(
input wire radio_clk,fast_clk,resetn,enable,clock_locked,
input wire sample_valid,input wire signed [15:0] sample_i,sample_q,
output wire receiver_resetn,result_valid,fault,
output reg signed [23:0] correlation_i=0,correlation_q=0
);
wire clear=!resetn || !enable || !clock_locked;
reg [31:0] sample_hold=0;
reg sample_toggle=0;
always @(posedge radio_clk or posedge clear)begin
if(clear)begin sample_hold<=0;sample_toggle<=0;end
else if(sample_valid)begin sample_hold<={sample_q,sample_i};sample_toggle<=~sample_toggle;end
end
(* ASYNC_REG="TRUE" *) reg [1:0] fast_reset_pipe=2'b11;
always @(posedge fast_clk or posedge clear)begin
if(clear)fast_reset_pipe<=2'b11;else fast_reset_pipe<={fast_reset_pipe[0],1'b0};
end
wire fast_reset=fast_reset_pipe[1];
(* ASYNC_REG="TRUE" *) reg [1:0] sample_sync=0;
reg sample_seen=0,arrival_delayed=0,started=0;
wire arrival=sample_sync[1]^sample_seen;
always @(posedge fast_clk or posedge fast_reset)begin
if(fast_reset)begin sample_sync<=0;sample_seen<=0;arrival_delayed<=0;started<=0;end
else begin
sample_sync<={sample_sync[0],sample_toggle};sample_seen<=sample_sync[1];
arrival_delayed<=arrival;
if(arrival)started<=1;
end
end
// These registers may see a transitioning word early. Only the PISO load
// qualified by the synchronized toggle consumes it, after the settling
// interval. Each source bit has one crossing load; each fast bit has two
// PISO loads. No wide fast-domain sample-enable fanout is introduced.
wire [31:0] sample_fast;
generate for(genvar b=0;b<32;b=b+1)begin:g_sample
gf_serial_reg capture(fast_clk,fast_reset,sample_hold[b],sample_fast[b]);
end endgenerate
wire graph_reset=fast_reset || !started;
wire unused_slot,graph_fault,word_valid;
wire signed [31:0] ci,cq;
gf_barker_serial_iq graph(fast_clk,graph_reset,arrival_delayed,
$signed(sample_fast[15:0]),$signed(sample_fast[31:16]),unused_slot,graph_fault,word_valid,ci,cq);
wire word_toggle;
gf_serial_reg event_toggle(fast_clk,graph_reset,word_toggle^word_valid,word_toggle);
wire word_toggle_crossing;
gf_serial_reg event_copy(fast_clk,graph_reset,word_toggle,word_toggle_crossing);
reg word_seen=0,captured_valid=0;
always @(negedge radio_clk or posedge clear)begin
if(clear)begin word_seen<=0;captured_valid<=0;correlation_i<=0;correlation_q<=0;end
else begin
word_seen<=word_toggle_crossing;
captured_valid<=word_seen!=word_toggle_crossing;
if(word_seen!=word_toggle_crossing)begin correlation_i<=ci[23:0];correlation_q<=cq[23:0];end
end
end
// Shift the serial detector's reset/word epoch with the seven-radio-clock
// input path. Hold this reset on disable/clock loss, even if fast_clk stops.
reg [6:0] receiver_reset_pipe=0;
always @(posedge radio_clk or posedge clear)begin
if(clear)receiver_reset_pipe<=0;else receiver_reset_pipe<={receiver_reset_pipe[5:0],1'b1};
end
gf_serial_reg fault_copy(fast_clk,fast_reset,graph_fault,fault);
assign receiver_resetn=receiver_reset_pipe[6] && !clear && !graph_fault;
assign result_valid=captured_valid && receiver_resetn;
endmodule
wifi_e310_link/fpga/open_e310/gf_barker_serial.sv · 117 lines
Download this file · Permanent section link
// Full-precision scalar Barker correlation, one parity lane of an interleaved
// stream. Two sign-extended IQ16 words enter LSB-first every 32 clocks:
// current=x[n], previous=x[n-1]; successive words advance n by two.
// A pair of phase-offset lanes can cover all samples. This module alone is
// NOT a complete 20-MS/s radio or clock-domain crossing implementation.
`timescale 1ns/1ps
module gf_serial_delay32(input wire clk,d,output wire q);
// No runtime reset or masking. Every consumer must flush ten input words
// after reset before treating a correlation as valid. SRL retains stale
// data across reset; the arithmetic regression explicitly exercises this.
(* shreg_extract="yes", srl_style="srl" *) reg [31:0] storage=0;
always @(posedge clk) storage<={storage[30:0],d};
assign q=storage[31];
endmodule
// Brian Greenforest registered serial full-adder topology, expressed as its
// subtract/borrow dual. sum=x^y^borrow; borrow'=(~x&(y|borrow))|(y&borrow).
// Clear the borrow while consuming the MSB so the next LSB starts a new word.
module gf_serial_sub_retimed(
input wire clk,reset,clear_borrow,x,y,output wire difference
);
wire borrow,next_borrow,next_difference;
gf_serial_lut #(.INIT(16'h9696)) s(.a(x),.b(y),.c(borrow),.d(1'b0),.q(next_difference));
gf_serial_lut #(.INIT(16'h00d4)) b(.a(x),.b(y),.c(borrow),.d(clear_borrow),.q(next_borrow));
gf_serial_reg rs(.clk(clk),.reset(reset),.d(next_difference),.q(difference));
gf_serial_reg rb(.clk(clk),.reset(reset),.d(next_borrow),.q(borrow));
endmodule
module gf_barker_serial_lane #(parameter integer INITIAL_PHASE=0)(
input wire clk,reset,current_bit,previous_bit,
output wire correlation_bit,correlation_word_end
);
wire [31:0] phase;
gf_serial_reg #(.INIT(INITIAL_PHASE==0)) p0(.clk(clk),.reset(reset),.d(phase[31]),.q(phase[0]));
generate for(genvar p=1;p<32;p=p+1)begin:g_phase
gf_serial_reg #(.INIT(INITIAL_PHASE==p)) r(.clk(clk),.reset(reset),.d(phase[p-1]),.q(phase[p]));
end endgenerate
wire [9:0] current_history,previous_history;
assign current_history[0]=current_bit;
assign previous_history[0]=previous_bit;
generate for(genvar t=1;t<10;t=t+1)begin:g_history
gf_serial_delay32 a(.clk(clk),.d(current_history[t-1]),.q(current_history[t]));
gf_serial_delay32 b(.clk(clk),.d(previous_history[t-1]),.q(previous_history[t]));
end endgenerate
wire [19:0] taps;
generate for(genvar t=0;t<20;t=t+1)begin:g_tap
wire source=t%2 ? previous_history[t/2] : current_history[t/2];
// History successor plus registered tap are exactly two loads.
gf_serial_reg r(.clk(clk),.reset(reset),.d(source),.q(taps[t]));
end endgenerate
function automatic integer positive_index(input integer n);
case(n)
0:positive_index=5;1:positive_index=6;2:positive_index=7;
3:positive_index=8;4:positive_index=9;5:positive_index=12;
6:positive_index=13;7:positive_index=14;8:positive_index=15;
9:positive_index=18;default:positive_index=19;
endcase
endfunction
function automatic integer negative_index(input integer n);
case(n)
0:negative_index=0;1:negative_index=1;2:negative_index=2;
3:negative_index=3;4:negative_index=4;5:negative_index=10;
6:negative_index=11;7:negative_index=16;default:negative_index=17;
endcase
endfunction
wire [15:0] positive[0:4],negative[0:4];
generate for(genvar n=0;n<16;n=n+1)begin:g_groups
if(n<11)assign positive[0][n]=taps[positive_index(n)];
else assign positive[0][n]=1'b0;
if(n<9)assign negative[0][n]=taps[negative_index(n)];
else assign negative[0][n]=1'b0;
end
for(genvar level=0;level<4;level=level+1)begin:g_reduce
localparam integer N=8>>level,PC=(11+(1<<level)-1)>>level,NC=(9+(1<<level)-1)>>level;
wire [2*N-1:0] clear;
// First tap adds one clock. Every tree level adds one more. Advance
// the ring source to compensate the registered binary clear tree.
gf_serial_fanout #(.N(2*N),.D(4-level)) distribution(.clk(clk),.reset(reset),
.bit_in(phase[(27+2*level)%32]),.leaves(clear));
for(genvar n=0;n<N;n=n+1)begin:g_node
if(2*n+1<PC)begin:g_positive_pair
gf_serial_add_retimed a(.clk(clk),.reset(reset),.clear_carry(clear[2*n]),
.x(positive[level][2*n]),.y(positive[level][2*n+1]),.sum(positive[level+1][n]));
end else if(2*n<PC)begin:g_positive_single
gf_serial_reg r(.clk(clk),.reset(reset),.d(positive[level][2*n]),.q(positive[level+1][n]));
end else assign positive[level+1][n]=1'b0;
if(2*n+1<NC)begin:g_negative_pair
gf_serial_add_retimed a(.clk(clk),.reset(reset),.clear_carry(clear[2*n+1]),
.x(negative[level][2*n]),.y(negative[level][2*n+1]),.sum(negative[level+1][n]));
end else if(2*n<NC)begin:g_negative_single
gf_serial_reg r(.clk(clk),.reset(reset),.d(negative[level][2*n]),.q(negative[level+1][n]));
end else assign negative[level+1][n]=1'b0;
end
assign positive[level+1][15:N]=0;
assign negative[level+1][15:N]=0;
end endgenerate
wire clear_difference;
gf_serial_reg borrow_marker(.clk(clk),.reset(reset),.d(phase[3]),.q(clear_difference));
gf_serial_sub_retimed difference(.clk(clk),.reset(reset),.clear_borrow(clear_difference),
.x(positive[4][0]),.y(negative[4][0]),.difference(correlation_bit));
gf_serial_reg end_marker(.clk(clk),.reset(reset),.d(phase[4]),.q(correlation_word_end));
endmodule
// Fit-only registered serial boundary, not the ADC serializer or radio top.
module gf_barker_serial_fit_top(
input wire clk,reset,current_bit,previous_bit,
output wire correlation_bit,correlation_word_end
);
wire a,b,s,e;
gf_serial_reg ia(.clk(clk),.reset(reset),.d(current_bit),.q(a));
gf_serial_reg ib(.clk(clk),.reset(reset),.d(previous_bit),.q(b));
gf_barker_serial_lane core(.clk(clk),.reset(reset),.current_bit(a),.previous_bit(b),
.correlation_bit(s),.correlation_word_end(e));
gf_serial_reg os(.clk(clk),.reset(reset),.d(s),.q(correlation_bit));
gf_serial_reg oe(.clk(clk),.reset(reset),.d(e),.q(correlation_word_end));
endmodule
wifi_e310_link/fpga/open_e310/gf_barker_serial_iq.sv · 135 lines
Download this file · Permanent section link
// Full parallel IQ16 -> serial Barker graph -> full signed 32-bit correlation.
// SYNCHRONOUS fast-clock boundary, not an asynchronous ADC crossing or PLL.
// At 320 MHz, provide one sample in every sample_slot (16 clocks apart),
// beginning on the first edge after reset. Any missing/extra sample latches
// fault until reset and suppresses result_valid. Never compress a sample gap.
`timescale 1ns/1ps
// Seeded registered duplication of a periodic control. PHASE_MASK describes
// the required LEAF value before each edge of the common 32-clock schedule.
// bit_in must be advanced D+1 clocks. Initial values also populate this
// look-ahead pipeline, so the very first input word is not silently lost.
module gf_serial_periodic_fanout #(
parameter integer N=32,D=$clog2(N),parameter [31:0] PHASE_MASK=0
)(input wire clk,reset,bit_in,output wire [N-1:0] leaves);
wire [N-1:0] stage[0:D];
gf_serial_reg #(.INIT(PHASE_MASK[D])) root(clk,reset,bit_in,stage[0][0]);
generate for(genvar l=1;l<=D;l=l+1)begin:g_level
for(genvar n=0;n<(1<<l);n=n+1)begin:g_node
gf_serial_reg #(.INIT(PHASE_MASK[D-l])) r(clk,reset,stage[l-1][n/2],stage[l][n]);
end
end endgenerate
assign leaves=stage[D];
endmodule
module gf_serial_piso16(
input wire clk,reset,input wire [15:0] load,parallel_word,output wire serial_bit
);
wire [15:0] bits;
generate for(genvar n=0;n<16;n=n+1)begin:g_bit
wire next_bit;
// After sixteen shifts, repeat the sign bit for the rest of the
// 32-bit word. The sign register drives exactly two local muxes.
gf_serial_lut #(.INIT(16'hcaca)) mux(
.a(bits[n==15?15:n+1]),.b(parallel_word[n]),.c(load[n]),.d(1'b0),.q(next_bit));
gf_serial_reg r(clk,reset,next_bit,bits[n]);
end endgenerate
assign serial_bit=bits[0];
endmodule
module gf_serial_delay16(input wire clk,d,output wire q);
(* shreg_extract="yes",srl_style="srl" *) reg [15:0] storage=0;
always @(posedge clk)storage<={storage[14:0],d};
assign q=storage[15];
endmodule
module gf_barker_serial_iq(
input wire clk,reset,sample_valid,
input wire signed [15:0] sample_i,sample_q,
output wire sample_slot,fault,result_valid,
output wire signed [31:0] correlation_i,correlation_q
);
wire [31:0] phase,tap;
generate for(genvar p=0;p<32;p=p+1)begin:g_phase
gf_serial_reg #(.INIT(p==0)) ring(clk,reset,phase[(p+31)%32],phase[p]);
gf_serial_reg #(.INIT(p==31)) copy(clk,reset,phase[p],tap[p]);
end endgenerate
assign sample_slot=tap[31]|tap[15];
wire internal_fault;
wire bad_sample=(sample_valid^sample_slot)|internal_fault;
// Identical registered leaves, not a delayed fault report: the internal
// latch drives feedback+valid veto, the other drives the public status.
gf_serial_reg bad_cadence(clk,reset,bad_sample,internal_fault);
gf_serial_reg fault_report(clk,reset,bad_sample,fault);
wire [31:0] load[0:1];
gf_serial_periodic_fanout #(.PHASE_MASK(32'h00000001)) even_load(clk,reset,tap[25],load[0]);
gf_serial_periodic_fanout #(.PHASE_MASK(32'h00010000)) odd_load(clk,reset,tap[9],load[1]);
wire [1:0] current[0:1],previous_source[0:1],previous[0:1];
wire [31:0] words[0:1][0:1];
generate for(genvar parity=0;parity<2;parity=parity+1)begin:g_parity
for(genvar iq=0;iq<2;iq=iq+1)begin:g_iq
wire serial_input,correlation_bit,unused_end;
wire [15:0] parallel_input=iq==0?sample_i:sample_q;
gf_serial_piso16 serializer(clk,reset,load[parity][16*iq+:16],
parallel_input,serial_input);
// Separate registered copies: current drives history+tap, while
// the other copy feeds a half-word delay for the opposite parity.
gf_serial_reg cur(clk,reset,serial_input,current[parity][iq]);
gf_serial_reg old(clk,reset,serial_input,previous_source[parity][iq]);
gf_serial_delay16 half_word(clk,previous_source[1-parity][iq],previous[parity][iq]);
gf_barker_serial_lane #(.INITIAL_PHASE(parity==0?30:14)) core(
clk,reset,current[parity][iq],previous[parity][iq],correlation_bit,unused_end);
for(genvar b=0;b<32;b=b+1)begin:g_sipo
wire next_bit;
if(b==31)assign next_bit=correlation_bit;
else assign next_bit=words[parity][iq][b+1];
gf_serial_reg r(clk,reset,next_bit,words[parity][iq][b]);
end
end
end endgenerate
// Full words are stable at capture edges 40+16*n. Both the data-select
// and output-write dependencies use registered binary duplication trees.
wire [31:0] write_word,select_odd;
gf_serial_periodic_fanout #(.PHASE_MASK(32'h01000100)) capture(
clk,reset,tap[1]|tap[17],write_word);
gf_serial_periodic_fanout #(.PHASE_MASK(32'h01000000)) select_parity(
clk,reset,tap[17],select_odd);
wire [31:0] output_words[0:1];
generate for(genvar b=0;b<32;b=b+1)begin:g_output
for(genvar iq=0;iq<2;iq=iq+1)begin:g_iq
wire chosen,next_bit;
gf_serial_lut #(.INIT(16'hcaca)) mux_parity(
words[0][iq][b],words[1][iq][b],select_odd[b],1'b0,chosen);
gf_serial_lut #(.INIT(16'hcaca)) mux_write(
output_words[iq][b],chosen,write_word[b],1'b0,next_bit);
gf_serial_reg r(clk,reset,next_bit,output_words[iq][b]);
end
end endgenerate
assign correlation_i=output_words[0];assign correlation_q=output_words[1];
// Ignore pipeline fragments and twenty-sample history warmup. No reset
// of the SRLs is assumed; these validity histories ARE reset explicitly.
wire [1:0] ready;
generate for(genvar parity=0;parity<2;parity=parity+1)begin:g_warmup
localparam integer W=parity==0?11:10;
wire [7:0] advance;
wire [W-1:0] valid_history;
gf_serial_periodic_fanout #(.N(8),.D(3),.PHASE_MASK(parity==0?32'h100:32'h1000000)) marker(
clk,reset,tap[parity==0?3:19],advance);
for(genvar n=0;n<W;n=n+1)begin:g_valid
wire next_valid,prior;
if(n==0)assign prior=1'b1;
else assign prior=valid_history[n-1];
gf_serial_lut #(.INIT(16'hcaca)) mux(valid_history[n],prior,
advance[n/2],1'b0,next_valid);
gf_serial_reg r(clk,reset,next_valid,valid_history[n]);
end
assign ready[parity]=valid_history[W-1];
end endgenerate
wire qualified;
gf_serial_reg valid_word(clk,reset,(tap[7]&ready[0])|(tap[23]&ready[1]),qualified);
assign result_valid=qualified&~internal_fault;
endmodule
wifi_e310_link/fpga/open_e310/gf_control_crc_fit.sv · 15 lines
Download this file · Permanent section link
// Fit-only 333-MHz registered boundary, never used as the radio top.
module gf_control_crc_fit_top (
input wire clk,reset,bit_valid,data_bit,
output wire [31:0] fcs
);
wire valid_r,data_r;
wire [31:0] result;
gf_serial_reg input_valid(.clk(clk),.reset(reset),.d(bit_valid),.q(valid_r));
gf_serial_reg input_data(.clk(clk),.reset(reset),.d(data_bit),.q(data_r));
gf_control_crc_bitserial graph(.clk(clk),.clear(reset),.bit_valid(valid_r),.data_bit(data_r),.fcs(result));
generate for(genvar n=0;n<32;n=n+1)begin:g_output
gf_serial_reg boundary(.clk(clk),.reset(reset),.d(result[n]),.q(fcs[n]));
end endgenerate
endmodule
wifi_e310_link/fpga/open_e310/gf_dsss_1mbps_psdu_tx.sv · 333 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// General IEEE 802.11 long-preamble 1 Mb/s DSSS PSDU transmitter.
//
// C++ writes a complete PSDU, including its FCS, into the frame memory and
// commits the byte count. This block adds PLCP, scrambling, DBPSK, and the
// 11-chip Barker sequence. It emits one rectangular complex sample per
// 20 MS/s DAC tick and stores no I/Q waveform.
`timescale 1ns/1ps
module gf_dsss_1mbps_psdu_tx #(
parameter integer IQ_WIDTH = 32,
parameter integer AMPLITUDE = 8192,
parameter integer RF_LEAD_CYCLES = 80
) (
input wire clk,
input wire resetn,
input wire arm,
input wire kill,
input wire frame_commit,
input wire [11:0] frame_length_bytes,
output reg [11:0] frame_read_address,
input wire [7:0] frame_read_data,
input wire tx_channel_available,
input wire tx_sample_tick,
input wire tx_sink_ready,
output wire ready,
output wire busy,
output wire tx_rf_claim,
output wire tx_valid,
output wire [IQ_WIDTH-1:0] tx_iq,
output reg frame_done,
output reg frame_error
);
localparam [1:0] STATE_IDLE = 2'd0;
localparam [1:0] STATE_PRELOAD = 2'd1;
localparam [1:0] STATE_LEAD = 2'd2;
localparam [1:0] STATE_ACTIVE = 2'd3;
localparam integer LEAD_WIDTH =
RF_LEAD_CYCLES < 2 ? 1 : $clog2(RF_LEAD_CYCLES + 1);
localparam signed [15:0] AMP = AMPLITUDE;
initial begin
if (IQ_WIDTH != 32)
$error("gf_dsss_1mbps_psdu_tx requires IQ16 packed as 32 bits");
if (AMPLITUDE < 1 || AMPLITUDE > 32767)
$error("AMPLITUDE must fit positive signed IQ16");
if (RF_LEAD_CYCLES < 1)
$error("RF_LEAD_CYCLES must be positive");
end
reg [1:0] state = STATE_IDLE;
reg [11:0] length_bytes = 12'd0;
reg [15:0] plcp_duration_us = 16'd0;
reg [15:0] plcp_crc = 16'd0;
reg [19:0] samples_remaining = 20'd0;
reg [LEAD_WIDTH-1:0] lead_cycles = {LEAD_WIDTH{1'b0}};
reg [15:0] bit_index = 16'd0;
reg [3:0] barker_index = 4'd0;
reg [4:0] sample_phase = 5'd0;
reg [6:0] scrambler_state = 7'h3b;
reg carrier_negative = 1'b1;
reg [11:0] current_psdu_byte_index = 12'd0;
reg [7:0] current_psdu_byte = 8'd0;
function automatic [15:0] crc16_plcp_for_duration;
input [15:0] duration_us;
integer byte_number;
integer bit_number;
reg [7:0] value;
reg mix;
reg [15:0] crc;
begin
crc = 16'hffff;
for (byte_number = 0; byte_number < 4;
byte_number = byte_number + 1) begin
case (byte_number)
0: value = 8'h0a;
1: value = 8'h00;
2: value = duration_us[7:0];
default: value = duration_us[15:8];
endcase
for (bit_number = 0; bit_number < 8;
bit_number = bit_number + 1) begin
mix = crc[0] ^ value[bit_number];
crc = crc >> 1;
if (mix)
crc = crc ^ 16'h8408;
end
end
crc16_plcp_for_duration = crc ^ 16'hffff;
end
endfunction
function automatic barker_negative;
input [3:0] index;
begin
case (index)
1, 4, 8, 9, 10: barker_negative = 1'b1;
default: barker_negative = 1'b0;
endcase
end
endfunction
function automatic plain_bit_at;
input [15:0] requested_bit;
input [15:0] duration_us;
input [15:0] header_crc;
input [11:0] current_byte_index;
input [7:0] current_byte;
input [7:0] prefetched_byte;
integer offset;
integer byte_number;
integer bit_number;
reg [7:0] value;
reg [15:0] sfd;
begin
value = 8'h00;
sfd = 16'hf3a0;
if (requested_bit < 128) begin
plain_bit_at = 1'b1;
end else if (requested_bit < 144) begin
plain_bit_at = sfd[requested_bit - 128];
end else if (requested_bit < 192) begin
offset = requested_bit - 144;
byte_number = offset >> 3;
bit_number = offset & 7;
case (byte_number)
0: value = 8'h0a;
1: value = 8'h00;
2: value = duration_us[7:0];
3: value = duration_us[15:8];
4: value = header_crc[7:0];
5: value = header_crc[15:8];
default: value = 8'h00;
endcase
plain_bit_at = value[bit_number];
end else begin
offset = requested_bit - 192;
byte_number = offset >> 3;
bit_number = offset & 7;
if (byte_number == current_byte_index)
value = current_byte;
else if (byte_number == current_byte_index + 1)
value = prefetched_byte;
else
value = 8'h00;
plain_bit_at = value[bit_number];
end
end
endfunction
reg [15:0] next_bit_index;
reg [3:0] next_barker_index;
reg [4:0] next_sample_phase;
reg [6:0] next_scrambler_state;
reg next_carrier_negative;
reg following_plain_bit;
reg following_scrambled_bit;
reg [5:0] phase_sum;
reg advances_bit;
always @* begin
next_bit_index = bit_index;
next_barker_index = barker_index;
next_sample_phase = sample_phase + 5'd11;
next_scrambler_state = scrambler_state;
next_carrier_negative = carrier_negative;
following_plain_bit = 1'b0;
following_scrambled_bit = 1'b0;
phase_sum = sample_phase + 5'd11;
advances_bit = 1'b0;
if (phase_sum >= 20) begin
next_sample_phase = phase_sum - 20;
if (barker_index < 10) begin
next_barker_index = barker_index + 1'b1;
end else begin
next_barker_index = 4'd0;
next_bit_index = bit_index + 1'b1;
advances_bit = 1'b1;
following_plain_bit = plain_bit_at(
bit_index + 1'b1,
plcp_duration_us,
plcp_crc,
current_psdu_byte_index,
current_psdu_byte,
frame_read_data);
following_scrambled_bit = following_plain_bit ^
scrambler_state[3] ^ scrambler_state[6];
next_scrambler_state =
{scrambler_state[5:0], following_scrambled_bit};
if (following_scrambled_bit)
next_carrier_negative = ~carrier_negative;
end
end
end
wire chip_negative =
carrier_negative ^ barker_negative(barker_index);
wire signed [15:0] output_i = chip_negative ? -AMP : AMP;
assign ready = state == STATE_IDLE && arm && !kill;
assign busy = state != STATE_IDLE;
assign tx_rf_claim =
state == STATE_ACTIVE ||
(state == STATE_LEAD && tx_channel_available);
assign tx_valid = state == STATE_ACTIVE && arm && !kill;
assign tx_iq = {16'h0000, output_i};
wire [15:0] committed_duration = {frame_length_bytes, 3'b000};
wire [15:0] committed_bits =
16'd192 + {frame_length_bytes, 3'b000};
wire [19:0] committed_samples =
({4'd0, committed_bits} << 4) +
({4'd0, committed_bits} << 2);
always @(posedge clk) begin
if (!resetn) begin
state <= STATE_IDLE;
length_bytes <= 12'd0;
plcp_duration_us <= 16'd0;
plcp_crc <= 16'd0;
samples_remaining <= 20'd0;
lead_cycles <= {LEAD_WIDTH{1'b0}};
frame_read_address <= 12'd0;
bit_index <= 16'd0;
barker_index <= 4'd0;
sample_phase <= 5'd0;
scrambler_state <= 7'h3b;
carrier_negative <= 1'b1;
current_psdu_byte_index <= 12'd0;
current_psdu_byte <= 8'd0;
frame_done <= 1'b0;
frame_error <= 1'b0;
end else begin
frame_done <= 1'b0;
frame_error <= 1'b0;
if (kill || !arm) begin
if (state != STATE_IDLE)
frame_error <= 1'b1;
state <= STATE_IDLE;
end else begin
case (state)
STATE_IDLE: begin
if (frame_commit) begin
if (frame_length_bytes == 0) begin
frame_error <= 1'b1;
end else begin
length_bytes <= frame_length_bytes;
plcp_duration_us <= committed_duration;
plcp_crc <= crc16_plcp_for_duration(
committed_duration);
samples_remaining <= committed_samples;
frame_read_address <= 12'd0;
current_psdu_byte_index <= 12'd0;
state <= STATE_PRELOAD;
end
end
end
STATE_PRELOAD: begin
current_psdu_byte <= frame_read_data;
frame_read_address <=
length_bytes > 1 ? 12'd1 : 12'd0;
lead_cycles <= RF_LEAD_CYCLES[LEAD_WIDTH-1:0];
bit_index <= 16'd0;
barker_index <= 4'd0;
sample_phase <= 5'd0;
// The first plain sync bit produces transmitted one
// from seed 0x5d, so carrier and state begin after it.
scrambler_state <= 7'h3b;
carrier_negative <= 1'b1;
state <= STATE_LEAD;
end
STATE_LEAD: begin
if (!tx_channel_available) begin
lead_cycles <= RF_LEAD_CYCLES[LEAD_WIDTH-1:0];
end else if (lead_cycles != 0) begin
lead_cycles <= lead_cycles - 1'b1;
end else if (tx_sample_tick && tx_sink_ready) begin
state <= STATE_ACTIVE;
end
end
STATE_ACTIVE: begin
if (tx_sample_tick) begin
if (!tx_sink_ready) begin
state <= STATE_IDLE;
frame_error <= 1'b1;
end else if (samples_remaining <= 1) begin
state <= STATE_IDLE;
samples_remaining <= 20'd0;
frame_done <= 1'b1;
end else begin
samples_remaining <= samples_remaining - 1'b1;
bit_index <= next_bit_index;
barker_index <= next_barker_index;
sample_phase <= next_sample_phase;
scrambler_state <= next_scrambler_state;
carrier_negative <= next_carrier_negative;
if (advances_bit &&
next_bit_index >= 192 &&
((next_bit_index - 192) & 16'h0007) == 0 &&
next_bit_index != 192) begin
current_psdu_byte <= frame_read_data;
current_psdu_byte_index <=
(next_bit_index - 192) >> 3;
if (((next_bit_index - 192) >> 3) + 1 <
length_bytes)
frame_read_address <=
((next_bit_index - 192) >> 3) + 1;
end
end
end
end
default: begin
state <= STATE_IDLE;
frame_error <= 1'b1;
end
endcase
end
end
end
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_async_fifo.sv · 129 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// Small dual-clock FIFO used to carry decoded PSDU bytes to the E310 ARM.
//
// Optional synchronous look-ahead read permits block RAM inference without
// a generated vendor FIFO core. The legacy asynchronous path is preserved.
// Gray pointers alone convey occupancy between the two clock domains.
`timescale 1ns/1ps
module gf_e310_async_fifo #(
parameter integer WIDTH = 10,
parameter integer ADDRESS_WIDTH = 12,
parameter integer USE_BLOCK_RAM = 0
) (
input wire write_clk,
input wire write_reset,
input wire [WIDTH-1:0] write_data,
input wire write_enable,
output wire write_ready,
output wire write_overflow,
input wire read_clk,
input wire read_reset,
output wire [WIDTH-1:0] read_data,
output wire read_valid,
input wire read_pop
);
localparam integer POINTER_WIDTH = ADDRESS_WIDTH + 1;
initial begin
if (ADDRESS_WIDTH < 2)
$error("gf_e310_async_fifo ADDRESS_WIDTH must be at least two");
end
(* ram_style = USE_BLOCK_RAM ? "block" : "distributed" *)
reg [WIDTH-1:0] memory [0:(1 << ADDRESS_WIDTH)-1];
reg [POINTER_WIDTH-1:0] write_binary = {POINTER_WIDTH{1'b0}};
reg [POINTER_WIDTH-1:0] write_gray = {POINTER_WIDTH{1'b0}};
reg write_full = 1'b0;
reg [POINTER_WIDTH-1:0] read_binary = {POINTER_WIDTH{1'b0}};
reg [POINTER_WIDTH-1:0] read_gray = {POINTER_WIDTH{1'b0}};
(* ASYNC_REG = "TRUE" *) reg [POINTER_WIDTH-1:0]
read_gray_write_sync_1 = {POINTER_WIDTH{1'b0}};
(* ASYNC_REG = "TRUE" *) reg [POINTER_WIDTH-1:0]
read_gray_write_sync_2 = {POINTER_WIDTH{1'b0}};
(* ASYNC_REG = "TRUE" *) reg [POINTER_WIDTH-1:0]
write_gray_read_sync_1 = {POINTER_WIDTH{1'b0}};
(* ASYNC_REG = "TRUE" *) reg [POINTER_WIDTH-1:0]
write_gray_read_sync_2 = {POINTER_WIDTH{1'b0}};
wire write_accept = write_enable && !write_full;
wire read_accept = read_pop && read_valid;
wire [POINTER_WIDTH-1:0] write_binary_next =
write_binary + write_accept;
wire [POINTER_WIDTH-1:0] write_gray_next =
(write_binary_next >> 1) ^ write_binary_next;
wire [POINTER_WIDTH-1:0] read_binary_next =
read_binary + read_accept;
wire [POINTER_WIDTH-1:0] read_gray_next =
(read_binary_next >> 1) ^ read_binary_next;
// A full FIFO has reached the synchronized read pointer with both wrap
// bits inverted. Empty is an exact Gray-pointer match.
wire [POINTER_WIDTH-1:0] full_compare = {
~read_gray_write_sync_2[POINTER_WIDTH-1:POINTER_WIDTH-2],
read_gray_write_sync_2[POINTER_WIDTH-3:0]
};
wire write_full_next = write_gray_next == full_compare;
wire empty = read_gray == write_gray_read_sync_2;
assign write_ready = !write_full;
assign write_overflow = write_enable && !write_ready;
generate if(USE_BLOCK_RAM) begin: block_read
reg [WIDTH-1:0] next_word;
reg output_valid=1'b0;
// Keep the current word while stalled, or fetch its successor on a
// pop. The RAM read register has no reset, so it can map into BRAM.
// Occupancy retains the current word until consumption: full capacity
// is exactly 2**ADDRESS_WIDTH, not that amount plus a hidden slot.
always @(posedge read_clk)
next_word <= memory[read_binary_next[ADDRESS_WIDTH-1:0]];
always @(posedge read_clk)
if(read_reset) output_valid<=1'b0;
else output_valid <= read_gray_next != write_gray_read_sync_2;
assign read_data=next_word;
assign read_valid=output_valid;
end else begin: asynchronous_read
assign read_valid = !empty;
assign read_data = memory[read_binary[ADDRESS_WIDTH-1:0]];
end endgenerate
always @(posedge write_clk) begin
if (write_reset) begin
write_binary <= {POINTER_WIDTH{1'b0}};
write_gray <= {POINTER_WIDTH{1'b0}};
write_full <= 1'b0;
read_gray_write_sync_1 <= {POINTER_WIDTH{1'b0}};
read_gray_write_sync_2 <= {POINTER_WIDTH{1'b0}};
end else begin
read_gray_write_sync_1 <= read_gray;
read_gray_write_sync_2 <= read_gray_write_sync_1;
write_full <= write_full_next;
if (write_accept) begin
memory[write_binary[ADDRESS_WIDTH-1:0]] <= write_data;
write_binary <= write_binary_next;
write_gray <= write_gray_next;
end
end
end
always @(posedge read_clk) begin
if (read_reset) begin
read_binary <= {POINTER_WIDTH{1'b0}};
read_gray <= {POINTER_WIDTH{1'b0}};
write_gray_read_sync_1 <= {POINTER_WIDTH{1'b0}};
write_gray_read_sync_2 <= {POINTER_WIDTH{1'b0}};
end else begin
write_gray_read_sync_1 <= write_gray;
write_gray_read_sync_2 <= write_gray_read_sync_1;
if (read_accept) begin
read_binary <= read_binary_next;
read_gray <= read_gray_next;
end
end
end
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_frontend_2g4.sv · 92 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// Exact E310 external RF-switch settings for the 2.4 GHz Wi-Fi band.
//
// Values are derived from UHD's E31x switch tables. The E310 swaps logical
// channels: logical channel zero uses physical front end two. TX bias and the
// TX/RX switch are asserted only while an armed packet path owns the DAC.
`timescale 1ns/1ps
module gf_e310_frontend_2g4 (
input wire arm,
input wire kill,
input wire tx_pll_locked,
input wire rx_pll_locked,
input wire tx_claim,
input wire logical_tx_channel,
input wire [1:0] rx_use_txrx,
output wire [2:0] TX_BANDSEL,
output wire [2:0] RX1_BANDSEL,
output wire [2:0] RX2_BANDSEL,
output wire [1:0] RX1B_BANDSEL,
output wire [1:0] RX1C_BANDSEL,
output wire [1:0] RX2B_BANDSEL,
output wire [1:0] RX2C_BANDSEL,
output wire TX_ENABLE1A,
output wire TX_ENABLE1B,
output wire TX_ENABLE2A,
output wire TX_ENABLE2B,
output wire VCTXRX1_V1,
output wire VCTXRX1_V2,
output wire VCTXRX2_V1,
output wire VCTXRX2_V2,
output wire VCRX1_V1,
output wire VCRX1_V2,
output wire VCRX2_V1,
output wire VCRX2_V2,
output wire LED_TXRX1_TX,
output wire LED_TXRX1_RX,
output wire LED_RX1_RX,
output wire LED_TXRX2_TX,
output wire LED_TXRX2_RX,
output wire LED_RX2_RX,
output wire tx_rf_active
);
// 2.4 GHz is RX LB_B7 and TX LB_2750 in the E31x tables.
assign TX_BANDSEL = 3'b000;
assign RX1_BANDSEL = 3'b101;
assign RX2_BANDSEL = 3'b100;
assign RX1B_BANDSEL = 2'b01;
assign RX1C_BANDSEL = 2'b00;
assign RX2B_BANDSEL = 2'b10;
assign RX2C_BANDSEL = 2'b00;
wire rf_ready = arm && !kill && tx_pll_locked && rx_pll_locked;
assign tx_rf_active = rf_ready && tx_claim;
// logical channel 0 -> physical front end 2; logical channel 1 -> front
// end 1. Low-band TX uses bias code {B,A}=2'b10.
wire tx_frontend_1 = tx_rf_active && logical_tx_channel;
wire tx_frontend_2 = tx_rf_active && !logical_tx_channel;
assign {TX_ENABLE1B, TX_ENABLE1A} =
tx_frontend_1 ? 2'b10 : 2'b00;
assign {TX_ENABLE2B, TX_ENABLE2A} =
tx_frontend_2 ? 2'b10 : 2'b00;
// VCTXRX codes are asymmetric on the two front ends.
// Front end 1: TX=01, RX=10. Front end 2: TX=10, RX=01.
wire [1:0] vctxrx1 = tx_frontend_1 ? 2'b01 :
((rf_ready && !tx_rf_active && rx_use_txrx[1]) ? 2'b10 : 2'b00);
wire [1:0] vctxrx2 = tx_frontend_2 ? 2'b10 :
((rf_ready && !tx_rf_active && rx_use_txrx[0]) ? 2'b01 : 2'b00);
assign {VCTXRX1_V1, VCTXRX1_V2} = vctxrx1;
assign {VCTXRX2_V1, VCTXRX2_V2} = vctxrx2;
// Low-band receive selector: 01 routes the dedicated RX connector and 10
// routes TX/RX. Both are disconnected while transmitting or killed.
wire [1:0] vcrx1 = (rf_ready && !tx_rf_active)
? (rx_use_txrx[1] ? 2'b10 : 2'b01) : 2'b00;
wire [1:0] vcrx2 = (rf_ready && !tx_rf_active)
? (rx_use_txrx[0] ? 2'b10 : 2'b01) : 2'b00;
assign {VCRX1_V1, VCRX1_V2} = vcrx1;
assign {VCRX2_V1, VCRX2_V2} = vcrx2;
assign LED_TXRX1_TX = tx_frontend_1;
assign LED_TXRX1_RX = rf_ready && !tx_rf_active && rx_use_txrx[1];
assign LED_RX1_RX = rf_ready && !tx_rf_active && !rx_use_txrx[1];
assign LED_TXRX2_TX = tx_frontend_2;
assign LED_TXRX2_RX = rf_ready && !tx_rf_active && rx_use_txrx[0];
assign LED_RX2_RX = rf_ready && !tx_rf_active && !rx_use_txrx[0];
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_gp0_regs.sv · 661 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// E310 GP0 register plane, decoded-PSDU FIFO, and host-loaded TX frame RAM.
//
// The ARM writes protocol frames as bytes. Only a committed, complete frame
// can cross into radio_clk. Arming requires a separate key write, and reset,
// kill, PLL loss, or malformed TX commands leave the RF path disarmed.
`timescale 1ns/1ps
module gf_e310_gp0_regs #(
parameter [47:0] DEFAULT_AP_MAC = 48'h024746415031,
parameter integer USE_HOST_WAVEFORM = 0,
parameter integer USE_TX_BLOCK_RAM = 0,
parameter integer COUNTERS_TO_SOFTWARE = 0,
parameter integer RX_FIFO_BLOCK_RAM = 0,
parameter integer PEAKS_TO_SOFTWARE = 0
) (
input wire bus_clk,
input wire bus_reset,
input wire bus_write,
input wire [11:0] bus_write_address,
input wire [31:0] bus_write_data,
input wire [3:0] bus_write_strobe,
input wire bus_read,
input wire [11:0] bus_read_address,
output reg [31:0] bus_read_data,
input wire radio_clk,
input wire radio_reset,
input wire radio_path_ready,
output wire radio_arm,
output wire radio_kill,
output wire [47:0] radio_ap_mac,
output wire radio_logical_tx_channel,
output wire [1:0] radio_rx_use_txrx,
output reg radio_tx_commit,
output wire [11:0] radio_tx_length,
input wire [11:0] radio_tx_read_address,
output wire [7:0] radio_tx_read_data,
input wire radio_tx_busy,
input wire radio_tx_done,
input wire radio_tx_error,
input wire psdu_start,
input wire psdu_byte_valid,
input wire [7:0] psdu_byte,
input wire psdu_byte_last,
input wire response_pending,
input wire response_active,
input wire tx_override_valid,
input wire mode_fault,
input wire [31:0] rx_psdu_count,
input wire [31:0] response_count,
input wire [31:0] deadline_miss_count,
input wire [31:0] rejected_count,
input wire rx_sample_valid,
input wire [31:0] rx_sample_iq,
input wire rx_capture_veto,
input wire [31:0] rx_sfd_count,
input wire [31:0] rx_plcp_ok_count,
input wire [31:0] rx_plcp_error_count
);
localparam [11:0] REG_MAGIC = 12'h200;
localparam [11:0] REG_CONTROL = 12'h204;
localparam [11:0] REG_AP_MAC_LO = 12'h208;
localparam [11:0] REG_AP_MAC_HI = 12'h20c;
localparam [11:0] REG_STATUS = 12'h210;
localparam [11:0] REG_PSDU_EVENT = 12'h214;
localparam [11:0] REG_FIFO_OVERFLOW = 12'h218;
localparam [11:0] REG_RX_PSDU_COUNT = 12'h21c;
localparam [11:0] REG_RESPONSE_COUNT = 12'h220;
localparam [11:0] REG_DEADLINE_MISS = 12'h224;
localparam [11:0] REG_REJECTED_COUNT = 12'h228;
localparam [11:0] REG_VERSION = 12'h22c;
localparam [11:0] REG_TX_WRITE = 12'h230;
localparam [11:0] REG_TX_COMMIT = 12'h234;
localparam [11:0] REG_TX_STATUS = 12'h238;
localparam [11:0] REG_TX_DONE_COUNT = 12'h23c;
localparam [11:0] REG_ARM_KEY = 12'h240;
localparam [11:0] REG_RF_CONFIG = 12'h244;
localparam [11:0] REG_TX_REJECTED = 12'h248;
localparam [11:0] REG_TX_ERROR_COUNT = 12'h24c;
localparam [31:0] MAGIC = 32'h47464531; // "GFE1"
localparam [31:0] VERSION = 32'h00010003; // v1.3, SFD pretrigger capture
localparam [31:0] ARM_KEY = 32'h47324641; // "G2FA"
reg armed_bus = 1'b0;
reg arm_key_valid = 1'b0;
reg [47:0] ap_mac_bus = DEFAULT_AP_MAC;
// bit 0 selects logical TX channel; bits 2:1 select TX/RX rather than
// dedicated RX for logical channels 0 and 1.
reg [2:0] rf_config_bus = 3'b000;
(* ram_style = USE_TX_BLOCK_RAM ? "block" : "distributed" *)
reg [7:0] tx_memory [0:4095];
reg [12:0] tx_bytes_written = 13'd0;
reg [11:0] tx_length_bus = 12'd0;
reg tx_start_toggle_bus = 1'b0;
reg tx_inflight_bus = 1'b0;
reg [31:0] tx_rejected_bus = 32'd0;
reg config_fault_bus = 1'b0;
(* ASYNC_REG = "TRUE" *) reg [1:0] arm_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [47:0] ap_mac_sync_1 = DEFAULT_AP_MAC;
(* ASYNC_REG = "TRUE" *) reg [47:0] ap_mac_sync_2 = DEFAULT_AP_MAC;
(* ASYNC_REG = "TRUE" *) reg [2:0] rf_config_sync_1 = 3'b000;
(* ASYNC_REG = "TRUE" *) reg [2:0] rf_config_sync_2 = 3'b000;
(* ASYNC_REG = "TRUE" *) reg [1:0] tx_toggle_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [11:0] tx_length_sync_1 = 12'd0;
(* ASYNC_REG = "TRUE" *) reg [11:0] tx_length_sync_2 = 12'd0;
reg tx_toggle_seen = 1'b0;
reg [31:0] tx_done_count_radio = 32'd0;
reg [31:0] tx_error_count_radio = 32'd0;
reg tx_complete_toggle_radio = 1'b0;
assign radio_arm = arm_sync[1] && radio_path_ready;
assign radio_kill = !radio_arm;
assign radio_ap_mac = ap_mac_sync_2;
assign radio_logical_tx_channel = rf_config_sync_2[0];
assign radio_rx_use_txrx = rf_config_sync_2[2:1];
assign radio_tx_length = tx_length_sync_2;
generate if(USE_TX_BLOCK_RAM) begin: tx_block_ram_read
reg [7:0] read_data_q;
// One clock read latency; committed ownership excludes read/write races.
// No array/read-port reset: the complete committed frame is written first.
always @(posedge radio_clk) read_data_q <= tx_memory[radio_tx_read_address];
assign radio_tx_read_data = read_data_q;
end else begin: tx_distributed_read
assign radio_tx_read_data = tx_memory[radio_tx_read_address];
end endgenerate
always @(posedge radio_clk) begin
if (radio_reset) begin
arm_sync <= 2'b00;
ap_mac_sync_1 <= DEFAULT_AP_MAC;
ap_mac_sync_2 <= DEFAULT_AP_MAC;
rf_config_sync_1 <= 3'b000;
rf_config_sync_2 <= 3'b000;
tx_toggle_sync <= 2'b00;
tx_length_sync_1 <= 12'd0;
tx_length_sync_2 <= 12'd0;
tx_toggle_seen <= 1'b0;
radio_tx_commit <= 1'b0;
tx_done_count_radio <= 32'd0;
tx_error_count_radio <= 32'd0;
tx_complete_toggle_radio <= 1'b0;
end else begin
arm_sync <= {arm_sync[0], armed_bus};
ap_mac_sync_1 <= ap_mac_bus;
ap_mac_sync_2 <= ap_mac_sync_1;
rf_config_sync_1 <= rf_config_bus;
rf_config_sync_2 <= rf_config_sync_1;
tx_toggle_sync <= {tx_toggle_sync[0], tx_start_toggle_bus};
tx_length_sync_1 <= tx_length_bus;
tx_length_sync_2 <= tx_length_sync_1;
radio_tx_commit <= 1'b0;
if (tx_toggle_sync[1] != tx_toggle_seen) begin
tx_toggle_seen <= tx_toggle_sync[1];
if (radio_arm)
radio_tx_commit <= 1'b1;
end
if (radio_tx_done)
tx_done_count_radio <= tx_done_count_radio + 1'b1;
if (radio_tx_error)
tx_error_count_radio <= tx_error_count_radio + 1'b1;
if (radio_tx_done || radio_tx_error)
tx_complete_toggle_radio <= ~tx_complete_toggle_radio;
end
end
wire [9:0] fifo_write_data = {
psdu_byte_last, psdu_start, psdu_byte
};
wire fifo_write_ready;
wire fifo_write_overflow;
wire [9:0] fifo_read_data;
wire fifo_read_valid;
wire fifo_read_pop = bus_read &&
bus_read_address == REG_PSDU_EVENT && fifo_read_valid;
gf_e310_async_fifo #(
.WIDTH(10),
.ADDRESS_WIDTH(12),
.USE_BLOCK_RAM(RX_FIFO_BLOCK_RAM)
) decoded_psdu_fifo (
.write_clk(radio_clk),
.write_reset(radio_reset),
.write_data(fifo_write_data),
.write_enable(psdu_byte_valid),
.write_ready(fifo_write_ready),
.write_overflow(fifo_write_overflow),
.read_clk(bus_clk),
.read_reset(bus_reset),
.read_data(fifo_read_data),
.read_valid(fifo_read_valid),
.read_pop(fifo_read_pop)
);
reg [31:0] fifo_overflow_count_radio = 32'd0;
always @(posedge radio_clk) begin
if (radio_reset)
fifo_overflow_count_radio <= 32'd0;
else if (fifo_write_overflow)
fifo_overflow_count_radio <= fifo_overflow_count_radio + 1'b1;
end
(* ASYNC_REG = "TRUE" *) reg [1:0] radio_arm_bus_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [1:0] radio_kill_bus_sync = 2'b11;
(* ASYNC_REG = "TRUE" *) reg [1:0] radio_busy_bus_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [1:0] ready_bus_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [1:0] complete_bus_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [3:0] status_sync_1 = 4'd0, status_sync_2 = 4'd0;
reg previous_tx_complete = 1'b0;
// Register derived status in its source domain before synchronization.
// The immediate radio-side kill path remains combinational and unchanged.
reg [7:0] status_radio = 8'h20;
always @(posedge radio_clk) begin
if (radio_reset)
status_radio <= 8'h20;
else
status_radio <= {radio_path_ready, radio_arm, radio_kill,
radio_tx_busy, mode_fault, tx_override_valid,
response_active, response_pending};
end
wire [31:0] tx_done_sync_2, tx_error_sync_2, overflow_sync_2;
wire [31:0] rx_psdu_sync_2, response_sync_2, deadline_sync_2, rejected_sync_2;
// Diagnostic counters advance by at most one per radio clock. Registered
// Gray encoding avoids incoherent binary carries crossing clock domains.
// Packet RAM ownership uses the completion toggle, never a counter value.
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_done (radio_clk, radio_reset, tx_done_count_radio, bus_clk, bus_reset, tx_done_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_error (radio_clk, radio_reset, tx_error_count_radio, bus_clk, bus_reset, tx_error_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_overflow (radio_clk, radio_reset, fifo_overflow_count_radio, bus_clk, bus_reset, overflow_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_rx (radio_clk, radio_reset, rx_psdu_count, bus_clk, bus_reset, rx_psdu_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_response (radio_clk, radio_reset, response_count, bus_clk, bus_reset, response_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_deadline (radio_clk, radio_reset, deadline_miss_count, bus_clk, bus_reset, deadline_sync_2);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) cdc_rejected (radio_clk, radio_reset, rejected_count, bus_clk, bus_reset, rejected_sync_2);
integer tx_byte_index;
always @(posedge bus_clk) begin
if (bus_reset) begin
armed_bus <= 1'b0;
arm_key_valid <= 1'b0;
ap_mac_bus <= DEFAULT_AP_MAC;
rf_config_bus <= 3'b000;
tx_bytes_written <= 13'd0;
tx_length_bus <= 12'd0;
tx_start_toggle_bus <= 1'b0;
tx_inflight_bus <= 1'b0;
tx_rejected_bus <= 32'd0;
config_fault_bus <= 1'b0;
radio_arm_bus_sync <= 2'b00;
radio_kill_bus_sync <= 2'b11;
radio_busy_bus_sync <= 2'b00;
ready_bus_sync <= 2'b00;
complete_bus_sync <= 2'b00;
status_sync_1 <= 4'd0;
status_sync_2 <= 4'd0;
previous_tx_complete <= 1'b0;
end else begin
radio_arm_bus_sync <= {radio_arm_bus_sync[0], status_radio[6]};
radio_kill_bus_sync <= {radio_kill_bus_sync[0], status_radio[5]};
radio_busy_bus_sync <= {radio_busy_bus_sync[0], status_radio[4]};
ready_bus_sync <= {ready_bus_sync[0], status_radio[7]};
complete_bus_sync <= {complete_bus_sync[0], tx_complete_toggle_radio};
status_sync_1 <= status_radio[3:0];
status_sync_2 <= status_sync_1;
if (complete_bus_sync[1] != previous_tx_complete) begin
previous_tx_complete <= complete_bus_sync[1];
tx_inflight_bus <= 1'b0;
end
if (radio_kill_bus_sync[1])
tx_inflight_bus <= 1'b0;
if (bus_write) begin
case (bus_write_address)
REG_ARM_KEY: begin
arm_key_valid <=
bus_write_strobe == 4'hf &&
bus_write_data == ARM_KEY;
end
REG_CONTROL: begin
if (bus_write_data[1] || !bus_write_data[0]) begin
armed_bus <= 1'b0;
arm_key_valid <= 1'b0;
tx_bytes_written <= 13'd0;
tx_inflight_bus <= 1'b0;
config_fault_bus <= 1'b0;
end else if (bus_write_data[0] && arm_key_valid) begin
armed_bus <= 1'b1;
arm_key_valid <= 1'b0;
end else begin
armed_bus <= 1'b0;
config_fault_bus <= 1'b1;
tx_rejected_bus <= tx_rejected_bus + 1'b1;
end
end
REG_AP_MAC_LO: begin
if (!armed_bus) begin
for (tx_byte_index = 0; tx_byte_index < 4;
tx_byte_index = tx_byte_index + 1)
if (bus_write_strobe[tx_byte_index])
ap_mac_bus[tx_byte_index*8 +: 8] <=
bus_write_data[tx_byte_index*8 +: 8];
end else begin
config_fault_bus <= 1'b1;
end
end
REG_AP_MAC_HI: begin
if (!armed_bus) begin
if (bus_write_strobe[0])
ap_mac_bus[39:32] <= bus_write_data[7:0];
if (bus_write_strobe[1])
ap_mac_bus[47:40] <= bus_write_data[15:8];
end else begin
config_fault_bus <= 1'b1;
end
end
REG_RF_CONFIG: begin
if (!armed_bus)
rf_config_bus <= bus_write_data[2:0];
else
config_fault_bus <= 1'b1;
end
REG_TX_WRITE: begin
if (armed_bus && !tx_inflight_bus &&
!radio_busy_bus_sync[1] &&
bus_write_strobe[0] && bus_write_strobe[1] &&
bus_write_strobe[2] &&
bus_write_data[19:8] == tx_bytes_written[11:0] &&
tx_bytes_written < 4095) begin
tx_memory[bus_write_data[19:8]] <=
bus_write_data[7:0];
tx_bytes_written <= tx_bytes_written + 1'b1;
end else begin
tx_rejected_bus <= tx_rejected_bus + 1'b1;
end
end
REG_TX_COMMIT: begin
if (armed_bus && !tx_inflight_bus &&
!radio_busy_bus_sync[1] &&
bus_write_data[11:0] != 0 &&
{1'b0, bus_write_data[11:0]} ==
tx_bytes_written) begin
tx_length_bus <= bus_write_data[11:0];
tx_start_toggle_bus <= ~tx_start_toggle_bus;
tx_inflight_bus <= 1'b1;
tx_bytes_written <= 13'd0;
end else begin
tx_rejected_bus <= tx_rejected_bus + 1'b1;
config_fault_bus <= 1'b1;
end
end
default: begin
tx_rejected_bus <= tx_rejected_bus + 1'b1;
end
endcase
end
end
end
wire [31:0] rx_diagnostic_read_data;
gf_e310_rx_capture #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE),
.PEAKS_TO_SOFTWARE(PEAKS_TO_SOFTWARE)) rx_diagnostic (
.bus_clk(bus_clk), .bus_reset(bus_reset),
.bus_write(bus_write && bus_write_strobe == 4'hf),
.bus_address(bus_write_address), .bus_write_data(bus_write_data),
.bus_read_address(bus_read_address), .bus_read_data(rx_diagnostic_read_data),
.radio_clk(radio_clk), .radio_reset(radio_reset),
.sample_valid(rx_sample_valid), .sample_iq(rx_sample_iq),
.capture_veto(rx_capture_veto),
.sfd_count(rx_sfd_count), .plcp_ok_count(rx_plcp_ok_count),
.plcp_error_count(rx_plcp_error_count)
);
always @* begin
case (bus_read_address)
REG_MAGIC: bus_read_data = MAGIC;
REG_CONTROL: bus_read_data = {30'd0, !armed_bus, armed_bus};
REG_AP_MAC_LO: bus_read_data = ap_mac_bus[31:0];
REG_AP_MAC_HI: bus_read_data = {16'd0, ap_mac_bus[47:32]};
REG_STATUS: bus_read_data = {
18'd0,
ready_bus_sync[1],
config_fault_bus,
tx_error_sync_2 != 0,
tx_done_sync_2 != 0,
radio_busy_bus_sync[1],
tx_inflight_bus,
status_sync_2[3:0],
overflow_sync_2 != 0,
fifo_read_valid,
radio_kill_bus_sync[1],
radio_arm_bus_sync[1]
};
REG_PSDU_EVENT: bus_read_data = fifo_read_valid
? {1'b1, 21'd0, fifo_read_data}
: 32'd0;
REG_FIFO_OVERFLOW: bus_read_data = overflow_sync_2;
REG_RX_PSDU_COUNT: bus_read_data = rx_psdu_sync_2;
REG_RESPONSE_COUNT: bus_read_data = response_sync_2;
REG_DEADLINE_MISS: bus_read_data = deadline_sync_2;
REG_REJECTED_COUNT: bus_read_data = rejected_sync_2;
REG_VERSION: bus_read_data = VERSION;
12'h27c: bus_read_data = USE_HOST_WAVEFORM ? 32'h57463230 : 32'd0;
12'h280: bus_read_data = COUNTERS_TO_SOFTWARE ? 32'h47523332 : 32'd0; // GR32
12'h284: bus_read_data = PEAKS_TO_SOFTWARE ? 32'h504b5357 : 32'd0; // PKSW
REG_TX_STATUS: bus_read_data = {
16'd0, tx_bytes_written[12:0], config_fault_bus,
radio_busy_bus_sync[1], tx_inflight_bus
};
REG_TX_DONE_COUNT: bus_read_data = tx_done_sync_2;
REG_RF_CONFIG: bus_read_data = {29'd0, rf_config_bus};
REG_TX_REJECTED: bus_read_data = tx_rejected_bus;
REG_TX_ERROR_COUNT: bus_read_data = tx_error_sync_2;
12'h250, 12'h254, 12'h258, 12'h25c,
12'h260, 12'h264, 12'h268, 12'h26c, 12'h270, 12'h278:
bus_read_data = rx_diagnostic_read_data;
default: bus_read_data = 32'hdead0000 |
{20'd0, bus_read_address};
endcase
end
endmodule
// Observation only: cannot arm RF, touch TX data, or change decoder state.
// 16384 consecutive {Q16,I16} samples (819.2 us at 20 MS/s). Amplitude mode
// starts at a threshold crossing; SFD mode retains a quarter-buffer pretrigger.
// Neither mode accepts a trigger during/just after our TX. No resampling.
// The CPU reads memory only after DONE; an explicit new ARM releases it.
module gf_e310_rx_capture #(
parameter integer ADDRESS_BITS = 14,
parameter integer COUNTERS_TO_SOFTWARE = 0,
parameter integer PEAKS_TO_SOFTWARE = 0
) (
input wire bus_clk, bus_reset, bus_write,
input wire [11:0] bus_address, bus_read_address,
input wire [31:0] bus_write_data,
output reg [31:0] bus_read_data,
input wire radio_clk, radio_reset, sample_valid,
input wire [31:0] sample_iq,
input wire capture_veto,
input wire [31:0] sfd_count, plcp_ok_count, plcp_error_count
);
reg request_toggle = 0;
reg sfd_mode_bus = 0;
reg [15:0] threshold_bus = 16'd256;
reg [ADDRESS_BITS-1:0] read_index = 0;
(* ram_style = "block" *) reg [31:0] memory [0:(1<<ADDRESS_BITS)-1];
reg [31:0] memory_read;
localparam integer PRETRIGGER_SAMPLES = (1<<ADDRESS_BITS)/4;
(* ASYNC_REG = "TRUE" *) reg [1:0] request_sync = 0;
(* ASYNC_REG = "TRUE" *) reg [15:0] threshold_sync1 = 256, threshold_sync2 = 256;
reg request_seen = 0;
(* ASYNC_REG = "TRUE" *) reg [1:0] sfd_mode_sync = 0;
reg active_sfd_mode = 0;
reg [31:0] previous_sfd = 0;
reg [ADDRESS_BITS-1:0] start_index = 0;
reg [ADDRESS_BITS:0] filled_samples = 0;
reg [ADDRESS_BITS:0] post_remaining = 0;
reg armed = 0, capturing = 0, done = 0;
reg [15:0] active_threshold = 256;
reg [ADDRESS_BITS-1:0] write_index = 0;
reg [31:0] sample_count = 0;
reg [15:0] peak_i = 0, peak_q = 0;
reg [31:0] captured_peak = 0;
reg [9:0] tx_guard = 0;
wire [15:0] abs_i = sample_iq[15] ? (~sample_iq[15:0] + 16'd1) : sample_iq[15:0];
wire [15:0] abs_q = sample_iq[31] ? (~sample_iq[31:16] + 16'd1) : sample_iq[31:16];
wire trigger_now = sample_valid && armed && !capturing &&
!capture_veto && tx_guard == 0 &&
(active_sfd_mode ? (sfd_count != previous_sfd && filled_samples >= PRETRIGGER_SAMPLES)
: (abs_i >= active_threshold || abs_q >= active_threshold));
wire store_sample = sample_valid && armed;
(* ASYNC_REG = "TRUE" *) reg [2:0] status_sync1 = 0, status_sync2 = 0;
(* ASYNC_REG = "TRUE" *) reg [1:0] capture_mode_sync = 0;
// Multi-bit frozen payload is stable several bus clocks before DONE is used.
reg [31:0] peak_sync1 = 0, peak_sync2 = 0;
reg [ADDRESS_BITS-1:0] start_sync1 = 0, start_sync2 = 0;
wire [ADDRESS_BITS-1:0] memory_read_index = start_sync2 + read_index;
wire [31:0] sfd_bus, plcp_ok_bus, plcp_error_bus, sample_count_bus;
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) diag_sfd (radio_clk, radio_reset, sfd_count, bus_clk, bus_reset, sfd_bus);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) diag_plcp (radio_clk, radio_reset, plcp_ok_count, bus_clk, bus_reset, plcp_ok_bus);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) diag_error (radio_clk, radio_reset, plcp_error_count, bus_clk, bus_reset, plcp_error_bus);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE)) diag_samples (radio_clk, radio_reset, sample_count, bus_clk, bus_reset, sample_count_bus);
// True dual-clock RAM: no reset/clear loop, and read/write ports separate.
always @(posedge radio_clk)
if (!radio_reset && store_sample) memory[write_index] <= sample_iq;
always @(posedge bus_clk) memory_read <= memory[memory_read_index];
always @(posedge bus_clk) begin
if (bus_reset) begin
request_toggle <= 0;
sfd_mode_bus <= 0;
threshold_bus <= 256;
read_index <= 0;
status_sync1 <= 0;
status_sync2 <= 0;
capture_mode_sync <= 0;
peak_sync1 <= 0;
peak_sync2 <= 0;
start_sync1 <= 0;
start_sync2 <= 0;
end else begin
status_sync1 <= {done, capturing, armed};
status_sync2 <= status_sync1;
capture_mode_sync <= {capture_mode_sync[0], active_sfd_mode};
peak_sync1 <= captured_peak;
peak_sync2 <= peak_sync1;
start_sync1 <= start_index;
start_sync2 <= start_sync1;
if (bus_write) begin
case (bus_address)
12'h250: if (bus_write_data == 32'h52584341) request_toggle <= !request_toggle;
12'h254: threshold_bus <= bus_write_data[15:0];
12'h258: read_index <= bus_write_data[ADDRESS_BITS-1:0];
12'h278: sfd_mode_bus <= bus_write_data[0];
endcase
end
end
end
always @(posedge radio_clk) begin
if (radio_reset) begin
request_sync <= 0;
request_seen <= 0;
sfd_mode_sync <= 0;
active_sfd_mode <= 0;
previous_sfd <= 0;
start_index <= 0;
filled_samples <= 0;
post_remaining <= 0;
threshold_sync1 <= 256;
threshold_sync2 <= 256;
armed <= 0;
capturing <= 0;
done <= 0;
write_index <= 0;
active_threshold <= 256;
sample_count <= 0;
peak_i <= 0;
peak_q <= 0;
captured_peak <= 0;
tx_guard <= 0;
end else begin
request_sync <= {request_sync[0], request_toggle};
sfd_mode_sync <= {sfd_mode_sync[0], sfd_mode_bus};
threshold_sync1 <= threshold_bus;
threshold_sync2 <= threshold_sync1;
if (sample_valid) begin
sample_count <= sample_count + 1'b1;
previous_sfd <= sfd_count;
end
if (capture_veto) tx_guard <= 10'd800; // 20 us at 40 MHz after own TX.
else if (tx_guard != 0) tx_guard <= tx_guard - 1'b1;
if (request_sync[1] != request_seen) begin
request_seen <= request_sync[1];
armed <= 1;
capturing <= 0;
done <= 0;
write_index <= 0;
active_threshold <= threshold_sync2;
active_sfd_mode <= sfd_mode_sync[1];
filled_samples <= 0;
post_remaining <= 0;
peak_i <= 0;
peak_q <= 0;
captured_peak <= 0;
end else if (store_sample) begin
write_index <= write_index + 1'b1;
if (filled_samples < PRETRIGGER_SAMPLES) filled_samples <= filled_samples + 1'b1;
if (!PEAKS_TO_SOFTWARE && (capturing || trigger_now) && abs_i > peak_i) peak_i <= abs_i;
if (!PEAKS_TO_SOFTWARE && (capturing || trigger_now) && abs_q > peak_q) peak_q <= abs_q;
if (trigger_now) begin
capturing <= 1;
start_index <= active_sfd_mode ? write_index - PRETRIGGER_SAMPLES : write_index;
post_remaining <= (1<<ADDRESS_BITS) - 1 - (active_sfd_mode ? PRETRIGGER_SAMPLES : 0);
end else if (capturing && post_remaining == 1) begin
armed <= 0;
capturing <= 0;
done <= 1;
if (!PEAKS_TO_SOFTWARE)
captured_peak <= {abs_q > peak_q ? abs_q : peak_q,
abs_i > peak_i ? abs_i : peak_i};
end else if (capturing) post_remaining <= post_remaining - 1'b1;
end
end
end
always @* begin
case (bus_read_address)
12'h250: bus_read_data = {16'd0, 8'(ADDRESS_BITS), 4'd0,
(PEAKS_TO_SOFTWARE ? capture_mode_sync[1] : 1'b0), status_sync2};
12'h254: bus_read_data = {16'd0, threshold_bus};
12'h258: bus_read_data = {{(32-ADDRESS_BITS){1'b0}}, read_index};
12'h25c: bus_read_data = status_sync2[2] ? memory_read : 32'd0;
12'h260: bus_read_data = sfd_bus;
12'h264: bus_read_data = plcp_ok_bus;
12'h268: bus_read_data = plcp_error_bus;
12'h26c: bus_read_data = sample_count_bus;
12'h270: bus_read_data = status_sync2[2] ? peak_sync2 : 32'd0;
12'h278: bus_read_data = {31'd0, sfd_mode_bus};
default: bus_read_data = 32'hdead0000 | {20'd0, bus_read_address};
endcase
end
endmodule
// Coherent monotonic diagnostic counter transfer. Reset samples are not events.
module gf_e310_counter_cdc #(parameter integer COUNTERS_TO_SOFTWARE = 0) (
input wire source_clk,
input wire source_reset,
input wire [31:0] source_count,
input wire destination_clk,
input wire destination_reset,
output wire [31:0] destination_count
);
reg [31:0] gray_radio = 32'd0;
(* ASYNC_REG = "TRUE" *) reg [31:0] gray_sync_1 = 32'd0;
(* ASYNC_REG = "TRUE" *) reg [31:0] gray_sync_2 = 32'd0;
always @(posedge source_clk)
if (source_reset) gray_radio <= 32'd0;
else gray_radio <= source_count ^ (source_count >> 1);
always @(posedge destination_clk) begin
if (destination_reset) begin
gray_sync_1 <= 32'd0;
gray_sync_2 <= 32'd0;
end else begin
gray_sync_1 <= gray_radio;
gray_sync_2 <= gray_sync_1;
end
end
// The optional contract exposes the coherent Gray word unchanged. C++
// decodes diagnostics; RF control and packet-RAM ownership do not use it.
generate if(COUNTERS_TO_SOFTWARE) begin: software_decode
assign destination_count = gray_sync_2;
end else begin: hardware_decode
// Preserve the legacy registered prefix-tree register contract.
reg [31:0] decode_1=0, decode_2=0, decode_4=0, decode_8=0, decoded=0;
assign destination_count = decoded;
always @(posedge destination_clk) begin
if(destination_reset) begin
decode_1<=0; decode_2<=0; decode_4<=0; decode_8<=0; decoded<=0;
end else begin
decode_1 <= gray_sync_2 ^ (gray_sync_2 >> 1);
decode_2 <= decode_1 ^ (decode_1 >> 2);
decode_4 <= decode_2 ^ (decode_2 >> 4);
decode_8 <= decode_4 ^ (decode_4 >> 8);
decoded <= decode_8 ^ (decode_8 >> 16);
end
end
end endgenerate
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_open_shell_top.sv · 623 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// Minimal open-tool E310 Wi-Fi baseband shell.
//
// The already-booted Zynq PS keeps Linux, DDR, MIO, FCLK0, GP0, and SPI0.
// This PL image preserves the AD9361 SPI path, owns the exact E310 CMOS sample
// and RF-switch pins, receives 1 Mb/s DSSS, generates SIFS ACK/CTS locally,
// and transmits arbitrary C++-formatted PSDUs from a byte-addressed GP0 RAM.
`timescale 1ns/1ps
module gf_e310_open_shell_top #(
parameter integer USE_RX_BUFR = 0,
parameter integer SPI_CS_INDEX = 0,
parameter integer USE_HOST_WAVEFORM = 0,
parameter integer USE_TX_BLOCK_RAM = 0,
parameter integer COUNTERS_TO_SOFTWARE = 0,
parameter integer RX_FIFO_BLOCK_RAM = 0,
parameter integer SINGLE_PHASE_RX = 0,
parameter integer SERIAL_DIFFERENTIAL = 0,
parameter integer TIMING_SCORE_RAM = 0,
parameter integer SERIAL_CONTROL_CRC = 0,
parameter integer SERIAL_RX_CRC = 0,
parameter integer RECURSIVE_CORRELATOR = 0,
parameter integer PEAKS_TO_SOFTWARE = 0,
parameter integer SERIAL_BARKER = 0
) (
input wire [7:0] CAT_CTRL_OUT,
output wire [3:0] CAT_CTRL_IN,
output wire CAT_RESET,
output wire CAT_CS,
output wire CAT_SCLK,
output wire CAT_MOSI,
input wire CAT_MISO,
output wire CAT_SYNC,
output wire CAT_TXNRX,
output wire CAT_ENABLE,
output wire CAT_ENAGC,
input wire CAT_RX_FRAME,
input wire CAT_DATA_CLK,
output wire CAT_TX_FRAME,
output wire CAT_FB_CLK,
input wire [11:0] CAT_P0_D,
output wire [11:0] CAT_P1_D,
output wire [2:0] TX_BANDSEL,
output wire [2:0] RX1_BANDSEL,
output wire [2:0] RX2_BANDSEL,
output wire [1:0] RX1B_BANDSEL,
output wire [1:0] RX1C_BANDSEL,
output wire [1:0] RX2B_BANDSEL,
output wire [1:0] RX2C_BANDSEL,
output wire TX_ENABLE1A,
output wire TX_ENABLE1B,
output wire TX_ENABLE2A,
output wire TX_ENABLE2B,
output wire VCTXRX1_V1,
output wire VCTXRX1_V2,
output wire VCTXRX2_V1,
output wire VCTXRX2_V2,
output wire VCRX1_V1,
output wire VCRX1_V2,
output wire VCRX2_V1,
output wire VCRX2_V2,
output wire LED_TXRX1_TX,
output wire LED_TXRX1_RX,
output wire LED_RX1_RX,
output wire LED_TXRX2_TX,
output wire LED_TXRX2_RX,
output wire LED_RX2_RX,
input wire AVR_CS_R, AVR_MOSI_R, AVR_SCK_R,
output wire AVR_MISO_R, AVR_IRQ
);
wire [3:0] fclk_clk;
wire [3:0] fclk_resetn;
wire gp0_aresetn;
wire [31:0] gp0_awaddr;
wire [11:0] gp0_awid;
wire [3:0] gp0_awlen;
wire gp0_awvalid;
reg gp0_awready = 1'b0;
wire [31:0] gp0_wdata;
wire [11:0] gp0_wid;
wire gp0_wlast;
wire [3:0] gp0_wstrb;
wire gp0_wvalid;
reg gp0_wready = 1'b0;
wire gp0_bready;
reg [11:0] gp0_bid = 12'd0;
reg [1:0] gp0_bresp = 2'b00;
reg gp0_bvalid = 1'b0;
wire [31:0] gp0_araddr;
wire [11:0] gp0_arid;
wire [3:0] gp0_arlen;
wire gp0_arvalid;
reg gp0_arready = 1'b0;
wire gp0_rready;
reg [11:0] gp0_rid = 12'd0;
reg [31:0] gp0_rdata = 32'd0;
reg [1:0] gp0_rresp = 2'b00;
reg gp0_rlast = 1'b1;
reg gp0_rvalid = 1'b0;
wire spi0_mosi;
wire spi0_sclk;
wire [2:0] spi0_ss;
wire pmu_irq;
// Stock e31x.v places PMU on fabric bit 3: GIC ID 64 (DT SPI 32).
// Bit 2 is the stock power-button release interrupt, not the PMU.
wire [19:0] ps_irq_f2p = {16'd0, pmu_irq, 3'd0};
(* keep *) PS7 ps7_i (
.FCLKCLK(fclk_clk),
.FCLKRESETN(fclk_resetn),
// Match the installed processing_system7 IP's disabled-port default
// (component.xml FPGA_IDLE_N defaultValue=0). The old constant 1 was
// an unsupported change to PS central-interconnect power management.
// The idle0-only physical test did not resolve the observed stalls.
.FPGAIDLEN(1'b0),
.IRQF2P(ps_irq_f2p),
.EMIOSPI0MI(CAT_MISO),
.EMIOSPI0MO(spi0_mosi),
.EMIOSPI0SCLKO(spi0_sclk),
.EMIOSPI0SSON(spi0_ss),
.EMIOSPI0SI(1'b0),
.EMIOSPI0SCLKI(1'b0),
.EMIOSPI0SSIN(1'b1),
.MAXIGP0ACLK(fclk_clk[0]),
// UG585 "AXI Clocks and Resets": GPV accesses require ALL PS-PL
// AXI clocks, even for unused interfaces. Keep request inputs on
// unused slave ports inactive; providing a clock does not start DMA.
// This corrects a clock-contract omission, not a proven stall cause.
.MAXIGP1ACLK(fclk_clk[0]),
.SAXIGP0ACLK(fclk_clk[0]),
.SAXIGP1ACLK(fclk_clk[0]),
.SAXIHP0ACLK(fclk_clk[0]),
.SAXIHP1ACLK(fclk_clk[0]),
.SAXIHP2ACLK(fclk_clk[0]),
.SAXIHP3ACLK(fclk_clk[0]),
.SAXIACPACLK(fclk_clk[0]),
.MAXIGP0ARESETN(gp0_aresetn),
.MAXIGP0AWADDR(gp0_awaddr),
.MAXIGP0AWID(gp0_awid),
.MAXIGP0AWLEN(gp0_awlen),
.MAXIGP0AWVALID(gp0_awvalid),
.MAXIGP0AWREADY(gp0_awready),
.MAXIGP0WDATA(gp0_wdata),
.MAXIGP0WID(gp0_wid),
.MAXIGP0WLAST(gp0_wlast),
.MAXIGP0WSTRB(gp0_wstrb),
.MAXIGP0WVALID(gp0_wvalid),
.MAXIGP0WREADY(gp0_wready),
.MAXIGP0BID(gp0_bid),
.MAXIGP0BRESP(gp0_bresp),
.MAXIGP0BVALID(gp0_bvalid),
.MAXIGP0BREADY(gp0_bready),
.MAXIGP0ARADDR(gp0_araddr),
.MAXIGP0ARID(gp0_arid),
.MAXIGP0ARLEN(gp0_arlen),
.MAXIGP0ARVALID(gp0_arvalid),
.MAXIGP0ARREADY(gp0_arready),
.MAXIGP0RID(gp0_rid),
.MAXIGP0RDATA(gp0_rdata),
.MAXIGP0RRESP(gp0_rresp),
.MAXIGP0RLAST(gp0_rlast),
.MAXIGP0RVALID(gp0_rvalid),
.MAXIGP0RREADY(gp0_rready)
);
assign CAT_MOSI = spi0_mosi;
assign CAT_SCLK = spi0_sclk;
// UHD 3.10 E310 Linux uses spidev0.1 and stock SPI0_SS1; newer MPM
// images use SS0. Select the installed PS/Linux contract at build time.
assign CAT_CS = spi0_ss[SPI_CS_INDEX];
// Match the stock E310 control contract. Only CTRL_IN[0] is asserted.
assign CAT_CTRL_IN = 4'b0001;
assign CAT_RESET = 1'b1;
assign CAT_SYNC = 1'b0;
assign CAT_TXNRX = 1'b1;
assign CAT_ENABLE = 1'b1;
assign CAT_ENAGC = 1'b1;
wire fabric_resetn = gp0_aresetn & fclk_resetn[0];
wire bus_reset = !fabric_resetn;
reg [31:0] saved_write_address = 32'd0;
reg [11:0] saved_write_id = 12'd0;
reg [3:0] saved_write_length = 4'd0;
reg [3:0] write_beats_remaining = 4'd0;
reg write_error = 1'b0;
reg saved_write_address_valid = 1'b0;
reg [31:0] saved_write_data = 32'd0;
reg [3:0] saved_write_strobe = 4'd0;
reg [11:0] saved_write_data_id = 12'd0;
reg saved_write_last = 1'b0;
reg saved_write_data_valid = 1'b0;
reg [3:0] read_beats_remaining = 4'd0;
wire write_beat_consume = saved_write_address_valid &&
saved_write_data_valid && !gp0_bvalid;
wire write_final_beat = (write_beats_remaining == 4'd0);
wire write_beat_error = (saved_write_data_id != saved_write_id) ||
(saved_write_last != write_final_beat);
// One outstanding write address; write interleaving depth is one.
// Buffer W independently of AW, then drain exactly AWLEN+1 beats. Bursts
// are unsupported register operations, not permission to complete early
// or to execute the first beat against a radio-control register.
// Legacy 2017 Linux locates its real PMU syscon at 0x40300000; newer MPM
// uses 0x40000000. Neither may alias the custom Wi-Fi bank at 0x40010000.
localparam [31:0] PMU_BASE = SPI_CS_INDEX == 1 ? 32'h40300000 : 32'h40000000;
wire write_wifi = saved_write_address[31:12] == 20'h40010;
wire read_wifi = gp0_araddr[31:12] == 20'h40010;
wire write_pmu = saved_write_address[31:8] == PMU_BASE[31:8];
wire read_pmu = gp0_araddr[31:8] == PMU_BASE[31:8];
wire write_single = write_beat_consume && saved_write_length == 0 &&
!write_error && !write_beat_error;
wire register_write = write_single && write_wifi;
wire pmu_write_error;
wire [31:0] pmu_read_data;
gf_e310_pmu_regs pmu (
.clk(fclk_clk[0]), .reset(bus_reset),
.spi_ss(AVR_CS_R), .spi_mosi(AVR_MOSI_R), .spi_sck(AVR_SCK_R),
.spi_miso(AVR_MISO_R), .irq(pmu_irq),
.write_enable(write_single && write_pmu),
.write_address(saved_write_address[7:0]),
.write_data(saved_write_data), .write_strobe(saved_write_strobe),
.write_error(pmu_write_error), .read_address(gp0_araddr[7:0]),
.read_data(pmu_read_data)
);
assign AVR_IRQ = 1'b0; // Same inactive AVR interrupt output as stock E310.
wire read_address_accept = gp0_arvalid && gp0_arready;
// GP0 is AXI3, not AXI-Lite. An unsupported burst must still receive
// ARLEN+1 beats and exactly one final RLAST, even when returning DECERR.
// No burst is allowed to pop a receive FIFO or touch any register state.
wire register_read = read_address_accept && (gp0_arlen == 4'd0) && read_wifi;
wire [31:0] register_read_data;
always @(posedge fclk_clk[0]) begin
if (bus_reset) begin
gp0_awready <= 1'b0;
gp0_wready <= 1'b0;
gp0_bvalid <= 1'b0;
gp0_arready <= 1'b0;
gp0_rvalid <= 1'b0;
gp0_rlast <= 1'b1;
gp0_bresp <= 2'b00;
gp0_rresp <= 2'b00;
read_beats_remaining <= 4'd0;
saved_write_address_valid <= 1'b0;
saved_write_data_valid <= 1'b0;
write_beats_remaining <= 4'd0;
write_error <= 1'b0;
end else begin
// Registered READY must close its slot on the accepting edge.
// Looking only at the old pending flag leaves READY high for one
// extra cycle and can overwrite an unanswered transaction.
gp0_awready <= !saved_write_address_valid && !gp0_bvalid &&
!(gp0_awvalid && gp0_awready);
gp0_wready <= !saved_write_data_valid && !gp0_bvalid &&
!(gp0_wvalid && gp0_wready);
gp0_arready <= !gp0_rvalid && !read_address_accept;
if (gp0_awvalid && gp0_awready) begin
saved_write_address <= gp0_awaddr;
saved_write_id <= gp0_awid;
saved_write_length <= gp0_awlen;
write_beats_remaining <= gp0_awlen;
write_error <= 1'b0;
saved_write_address_valid <= 1'b1;
end
if (gp0_wvalid && gp0_wready) begin
saved_write_data <= gp0_wdata;
saved_write_strobe <= gp0_wstrb;
saved_write_data_id <= gp0_wid;
saved_write_last <= gp0_wlast;
saved_write_data_valid <= 1'b1;
end
if (write_beat_consume) begin
saved_write_data_valid <= 1'b0;
write_error <= write_error || write_beat_error;
if (write_final_beat) begin
gp0_bid <= saved_write_id;
gp0_bresp <= (write_error || write_beat_error ||
(write_pmu && pmu_write_error)) ? 2'b10 :
((saved_write_length == 0 && (write_wifi || write_pmu)) ? 2'b00 : 2'b11);
gp0_bvalid <= 1'b1;
saved_write_address_valid <= 1'b0;
end else begin
write_beats_remaining <= write_beats_remaining - 4'd1;
end
end else if (gp0_bvalid && gp0_bready) begin
gp0_bvalid <= 1'b0;
end
if (read_address_accept) begin
gp0_rid <= gp0_arid;
gp0_rdata <= gp0_arlen != 0 ? 32'd0 :
(read_wifi ? register_read_data : (read_pmu ? pmu_read_data : 32'd0));
gp0_rresp <= (gp0_arlen == 0 && (read_wifi || read_pmu)) ? 2'b00 : 2'b11;
gp0_rlast <= (gp0_arlen == 4'd0);
read_beats_remaining <= gp0_arlen;
gp0_rvalid <= 1'b1;
end else if (gp0_rvalid && gp0_rready) begin
if (read_beats_remaining != 4'd0) begin
read_beats_remaining <= read_beats_remaining - 4'd1;
gp0_rlast <= (read_beats_remaining == 4'd1);
end else begin
gp0_rvalid <= 1'b0;
end
end
end
end
wire radio_clk;
wire radio_rst;
wire [11:0] rx_i0;
wire [11:0] rx_q0;
wire [11:0] rx_i1;
wire [11:0] rx_q1;
wire rx_stb;
wire [11:0] tx_i0;
wire [11:0] tx_q0;
wire [11:0] tx_i1;
wire [11:0] tx_q1;
wire tx_stb;
// Mirror the channel swap in the stock E310 top level.
gf_e310_io_open #(.USE_RX_BUFR(USE_RX_BUFR)) ad9361_io (
.areset(bus_reset),
.mimo(1'b1),
.radio_clk(radio_clk),
.radio_rst(radio_rst),
.rx_i0(rx_i1),
.rx_q0(rx_q1),
.rx_i1(rx_i0),
.rx_q1(rx_q0),
.rx_stb(rx_stb),
.tx_i0(tx_i1),
.tx_q0(tx_q1),
.tx_i1(tx_i0),
.tx_q1(tx_q0),
.tx_stb(tx_stb),
.rx_clk(CAT_DATA_CLK),
.rx_frame(CAT_RX_FRAME),
.rx_data(CAT_P0_D),
.tx_clk(CAT_FB_CLK),
.tx_frame(CAT_TX_FRAME),
.tx_data(CAT_P1_D)
);
(* ASYNC_REG = "TRUE" *) reg [1:0] tx_lock_sync = 2'b00;
(* ASYNC_REG = "TRUE" *) reg [1:0] rx_lock_sync = 2'b00;
always @(posedge radio_clk) begin
if (radio_rst) begin
tx_lock_sync <= 2'b00;
rx_lock_sync <= 2'b00;
end else begin
tx_lock_sync <= {tx_lock_sync[0], CAT_CTRL_OUT[7]};
rx_lock_sync <= {rx_lock_sync[0], CAT_CTRL_OUT[6]};
end
end
wire serial_clk,serial_clock_locked;
generate if(SERIAL_BARKER)begin:g_serial_clock
gf_e310_serial_clock clock_generator(.radio_clk(radio_clk),.control_clk(fclk_clk[0]),
.reset_request(bus_reset || radio_rst),.fast_clk(serial_clk),.locked(serial_clock_locked));
end else begin:g_no_serial_clock
assign serial_clk=1'b0;assign serial_clock_locked=1'b1;
end endgenerate
wire radio_path_ready = tx_lock_sync[1] && rx_lock_sync[1] && serial_clock_locked;
wire radio_arm;
wire radio_kill;
wire [47:0] radio_ap_mac;
wire logical_tx_channel;
wire [1:0] rx_use_txrx;
wire radio_tx_commit;
wire [11:0] radio_tx_length;
wire [11:0] radio_tx_read_address;
wire [7:0] radio_tx_read_data;
wire ordinary_tx_busy;
wire ordinary_tx_done;
wire ordinary_tx_error;
wire host_psdu_start;
wire host_psdu_byte_valid;
wire [7:0] host_psdu_byte;
wire host_psdu_byte_last;
wire [15:0] host_psdu_end_age_cycles;
wire response_pending;
wire response_active;
wire response_start;
wire response_is_cts;
wire [47:0] response_mac;
wire [15:0] response_duration_us;
wire tx_override_valid;
wire mode_fault;
wire rf_kill=radio_kill || (SERIAL_BARKER && (!serial_clock_locked || mode_fault));
wire [31:0] rx_sfd_count;
wire [31:0] rx_plcp_ok_count;
wire [31:0] rx_plcp_error_count;
wire [31:0] rx_psdu_count;
wire [31:0] classified_frame_count;
wire [31:0] classified_fcs_ok_count;
wire [31:0] response_candidate_count;
wire [31:0] malformed_count;
wire [31:0] response_count;
wire [31:0] deadline_miss_count;
wire [31:0] rejected_count;
wire [31:0] stream_abort_count;
initial if(USE_TX_BLOCK_RAM && !USE_HOST_WAVEFORM)
$error("Block RAM TX requires the host waveform player");
gf_e310_gp0_regs #(.USE_HOST_WAVEFORM(USE_HOST_WAVEFORM),
.USE_TX_BLOCK_RAM(USE_TX_BLOCK_RAM),
.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE),
.RX_FIFO_BLOCK_RAM(RX_FIFO_BLOCK_RAM),
.PEAKS_TO_SOFTWARE(PEAKS_TO_SOFTWARE)) control_plane (
.bus_clk(fclk_clk[0]),
.bus_reset(bus_reset),
.bus_write(register_write),
.bus_write_address(saved_write_address[11:0]),
.bus_write_data(saved_write_data),
.bus_write_strobe(saved_write_strobe),
.bus_read(register_read),
.bus_read_address(gp0_araddr[11:0]),
.bus_read_data(register_read_data),
.radio_clk(radio_clk),
.radio_reset(radio_rst),
.radio_path_ready(radio_path_ready),
.radio_arm(radio_arm),
.radio_kill(radio_kill),
.radio_ap_mac(radio_ap_mac),
.radio_logical_tx_channel(logical_tx_channel),
.radio_rx_use_txrx(rx_use_txrx),
.radio_tx_commit(radio_tx_commit),
.radio_tx_length(radio_tx_length),
.radio_tx_read_address(radio_tx_read_address),
.radio_tx_read_data(radio_tx_read_data),
.radio_tx_busy(ordinary_tx_busy),
.radio_tx_done(ordinary_tx_done),
.radio_tx_error(ordinary_tx_error),
.psdu_start(host_psdu_start),
.psdu_byte_valid(host_psdu_byte_valid),
.psdu_byte(host_psdu_byte),
.psdu_byte_last(host_psdu_byte_last),
.response_pending(response_pending),
.response_active(response_active),
.tx_override_valid(tx_override_valid),
.mode_fault(mode_fault),
.rx_psdu_count(rx_psdu_count),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count),
.rx_sample_valid(rx_stb),
.rx_sample_iq({rx_q0, 4'd0, rx_i0, 4'd0}),
.rx_capture_veto(radio_kill || ordinary_tx_rf_claim || sifs_channel_claim),
.rx_sfd_count(rx_sfd_count),
.rx_plcp_ok_count(rx_plcp_ok_count),
.rx_plcp_error_count(rx_plcp_error_count)
);
wire ordinary_tx_ready;
wire ordinary_tx_rf_claim;
wire ordinary_tx_valid;
wire [31:0] ordinary_tx_iq;
wire sifs_channel_claim =
response_pending || response_active || tx_override_valid;
generate if(USE_HOST_WAVEFORM) begin: host_waveform_path
gf_host_waveform_tx #(.MEMORY_READ_LATENCY(USE_TX_BLOCK_RAM)) ordinary_tx (
.clk(radio_clk), .resetn(!radio_rst), .arm(radio_arm), .kill(rf_kill),
.frame_commit(radio_tx_commit), .frame_length_bytes(radio_tx_length),
.frame_read_address(radio_tx_read_address), .frame_read_data(radio_tx_read_data),
.tx_channel_available(!sifs_channel_claim), .tx_sample_tick(tx_stb),
.tx_sink_ready(1'b1), .ready(ordinary_tx_ready), .busy(ordinary_tx_busy),
.tx_rf_claim(ordinary_tx_rf_claim), .tx_valid(ordinary_tx_valid),
.tx_iq(ordinary_tx_iq), .frame_done(ordinary_tx_done), .frame_error(ordinary_tx_error)
);
end else begin: psdu_path
gf_dsss_1mbps_psdu_tx ordinary_tx (
.clk(radio_clk),
.resetn(!radio_rst),
.arm(radio_arm),
.kill(radio_kill),
.frame_commit(radio_tx_commit),
.frame_length_bytes(radio_tx_length),
.frame_read_address(radio_tx_read_address),
.frame_read_data(radio_tx_read_data),
.tx_channel_available(!sifs_channel_claim),
.tx_sample_tick(tx_stb),
.tx_sink_ready(1'b1),
.ready(ordinary_tx_ready),
.busy(ordinary_tx_busy),
.tx_rf_claim(ordinary_tx_rf_claim),
.tx_valid(ordinary_tx_valid),
.tx_iq(ordinary_tx_iq),
.frame_done(ordinary_tx_done),
.frame_error(ordinary_tx_error)
);
end endgenerate
wire [63:0] host_tx_flat = ordinary_tx_valid
? {32'd0, ordinary_tx_iq[15:0], ordinary_tx_iq[31:16]}
: 64'd0;
wire [63:0] rx_flat = {
rx_i1, 4'd0, rx_q1, 4'd0,
rx_i0, 4'd0, rx_q0, 4'd0
};
wire [63:0] air_tx_flat;
gf_e31x_sifs_inline #(
.CLOCK_HZ(40_000_000),
.SIFS_US(10),
.SINGLE_PHASE_RX(SINGLE_PHASE_RX),
.SERIAL_DIFFERENTIAL(SERIAL_DIFFERENTIAL),
.TIMING_SCORE_RAM(TIMING_SCORE_RAM),
.SERIAL_CONTROL_CRC(SERIAL_CONTROL_CRC),
.SERIAL_RX_CRC(SERIAL_RX_CRC),
.RECURSIVE_CORRELATOR(RECURSIVE_CORRELATOR),.SERIAL_BARKER(SERIAL_BARKER)
) sifs_path (
.clk(radio_clk),
.resetn(!radio_rst),
.mode_mimo(1'b1),
.arm(radio_arm),
.kill(radio_kill),
.ap_mac(radio_ap_mac),
.rx_sample_valid(rx_stb),
.rx_flat(rx_flat),
.tx_sample_tick(tx_stb),
.host_tx_flat(host_tx_flat),
.air_tx_flat(air_tx_flat),
.host_psdu_start(host_psdu_start),
.host_psdu_byte_valid(host_psdu_byte_valid),
.host_psdu_byte(host_psdu_byte),
.host_psdu_byte_last(host_psdu_byte_last),
.host_psdu_end_age_cycles(host_psdu_end_age_cycles),
.response_pending(response_pending),
.response_active(response_active),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_mac(response_mac),
.response_duration_us(response_duration_us),
.tx_override_valid(tx_override_valid),
.mode_fault(mode_fault),
.rx_sfd_count(rx_sfd_count),
.rx_plcp_ok_count(rx_plcp_ok_count),
.rx_plcp_error_count(rx_plcp_error_count),
.rx_psdu_count(rx_psdu_count),
.classified_frame_count(classified_frame_count),
.classified_fcs_ok_count(classified_fcs_ok_count),
.response_candidate_count(response_candidate_count),
.malformed_count(malformed_count),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count),
.stream_abort_count(stream_abort_count),.fast_clk(serial_clk),.serial_clock_locked(serial_clock_locked)
);
assign tx_i0 = air_tx_flat[31:20];
assign tx_q0 = air_tx_flat[15:4];
assign tx_i1 = air_tx_flat[63:52];
assign tx_q1 = air_tx_flat[47:36];
wire tx_rf_active;
gf_e310_frontend_2g4 frontend (
.arm(radio_arm),
.kill(rf_kill),
.tx_pll_locked(tx_lock_sync[1]),
.rx_pll_locked(rx_lock_sync[1]),
.tx_claim(ordinary_tx_rf_claim || sifs_channel_claim),
.logical_tx_channel(logical_tx_channel),
.rx_use_txrx(rx_use_txrx),
.TX_BANDSEL(TX_BANDSEL),
.RX1_BANDSEL(RX1_BANDSEL),
.RX2_BANDSEL(RX2_BANDSEL),
.RX1B_BANDSEL(RX1B_BANDSEL),
.RX1C_BANDSEL(RX1C_BANDSEL),
.RX2B_BANDSEL(RX2B_BANDSEL),
.RX2C_BANDSEL(RX2C_BANDSEL),
.TX_ENABLE1A(TX_ENABLE1A),
.TX_ENABLE1B(TX_ENABLE1B),
.TX_ENABLE2A(TX_ENABLE2A),
.TX_ENABLE2B(TX_ENABLE2B),
.VCTXRX1_V1(VCTXRX1_V1),
.VCTXRX1_V2(VCTXRX1_V2),
.VCTXRX2_V1(VCTXRX2_V1),
.VCTXRX2_V2(VCTXRX2_V2),
.VCRX1_V1(VCRX1_V1),
.VCRX1_V2(VCRX1_V2),
.VCRX2_V1(VCRX2_V1),
.VCRX2_V2(VCRX2_V2),
.LED_TXRX1_TX(LED_TXRX1_TX),
.LED_TXRX1_RX(LED_TXRX1_RX),
.LED_RX1_RX(LED_RX1_RX),
.LED_TXRX2_TX(LED_TXRX2_TX),
.LED_TXRX2_RX(LED_TXRX2_RX),
.LED_RX2_RX(LED_RX2_RX),
.tx_rf_active(tx_rf_active)
);
wire unused = &{
1'b0,
ordinary_tx_ready,
tx_rf_active,
host_psdu_end_age_cycles,
response_start,
response_is_cts,
response_mac,
response_duration_us,
rx_sfd_count,
rx_plcp_ok_count,
rx_plcp_error_count,
classified_frame_count,
classified_fcs_ok_count,
response_candidate_count,
malformed_count,
stream_abort_count,
CAT_CTRL_OUT[5:0]
};
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_pmu_regs.sv · 89 lines
Download this file · Permanent section link
// SPDX-License-Identifier: LGPL-3.0-or-later
// E310 board-management register/SPI contract, adapted from Ettus axi_pmu.v.
// No Wi-Fi protocol or RF sample processing lives here. The AVR supplies every
// reported power/battery value; there are no invented healthy-status values.
`timescale 1ns/1ps
module gf_e310_pmu_regs (
input wire clk, reset,
input wire spi_ss, spi_mosi, spi_sck,
output wire spi_miso,
input wire write_enable,
input wire [7:0] write_address,
input wire [31:0] write_data,
input wire [3:0] write_strobe,
output wire write_error,
input wire [7:0] read_address,
output reg [31:0] read_data,
output wire irq
);
wire spi_strobe;
wire [63:0] spi_received;
reg [63:0] spi_reply = 0;
spi_slave spi (
.clk(clk), .rst(reset), .ss(spi_ss), .mosi(spi_mosi),
.sck(spi_sck), .miso(spi_miso), .parallel_stb(spi_strobe),
.parallel_din(spi_reply), .parallel_dout(spi_received)
);
reg [63:0] status0 = 0, status1 = 0, status2 = 0;
reg [7:0] irq_enable = 0;
reg [31:0] shutdown = 0;
reg [31:0] commands [0:31];
reg [4:0] write_pointer = 0, read_pointer = 0;
reg [5:0] command_count = 0;
wire command_write = write_address == 0 || write_address > 4;
wire pop = spi_strobe && command_count != 0;
assign write_error = write_strobe != 4'hf ||
(command_write && command_count == 32 && !pop);
wire push = write_enable && !write_error && command_write;
wire [31:0] command_word = write_address == 0 ?
{write_data[23:0], 8'h00} :
{write_data[7:0], write_data[15:8], write_address, 8'h01};
always @(posedge clk) begin
if (reset) begin
status0 <= 0; status1 <= 0; status2 <= 0;
spi_reply <= 0; irq_enable <= 0; shutdown <= 0;
write_pointer <= 0; read_pointer <= 0; command_count <= 0;
end else begin
if (spi_strobe) begin
case (spi_received[7:0])
0: status0 <= spi_received;
1: status1 <= spi_received;
2: status2 <= spi_received;
endcase
// Stock AVR polls pipelined 64-bit messages. Only a real
// queued host write may set the valid bit in its reply.
spi_reply <= pop ? {1'b1, 31'd0, commands[read_pointer]} : 64'd0;
end
if (push) begin
commands[write_pointer] <= command_word;
write_pointer <= write_pointer + 1'b1;
end
if (pop) read_pointer <= read_pointer + 1'b1;
case ({push, pop})
2'b10: command_count <= command_count + 1'b1;
2'b01: command_count <= command_count - 1'b1;
endcase
if (write_enable && !write_error) begin
if (write_address == 0) shutdown <= write_data;
if (write_address == 4) irq_enable <= write_data[15:8];
end
end
end
assign irq = |(status1[63:56] & irq_enable);
always @* begin
read_data = 32'hdeadbeef;
case (read_address)
8'h00: read_data = shutdown;
8'h04: read_data = {16'd0, irq_enable, status0[15:8]};
8'h08: read_data = {8'd0, status0[55:48], status0[63:56], status0[47:40]};
8'h0c: read_data = {27'd0, status0[33:32], status0[35], status0[37:36]};
8'h10: read_data = {status1[31:24], status1[39:32], status1[15:8], status1[23:16]};
8'h14: read_data = {8'd0, status1[63:56], status1[47:40], status1[55:48]};
8'h18: read_data = {16'd0, status2[15:8], status2[23:16]};
8'h1c: read_data = {24'd0, status2[31:24]};
endcase
end
endmodule
wifi_e310_link/fpga/open_e310/gf_e310_serial_clock.sv · 34 lines
Download this file · Permanent section link
// Dedicated clock primitive only: 40 * 24 / 3 = 320 MHz, VCO 960 MHz.
// No IP integrator, PS clock change, Linux power-mode change or clock gating.
// Primitive reference: AMD UG953 MMCME2_BASE (2024.2).
`timescale 1ns/1ps
module gf_e310_serial_clock(
input wire radio_clk,control_clk,reset_request,
output wire fast_clk,locked
);
wire feedback,feedback_buffered,fast_unbuffered,raw_locked;
(* ASYNC_REG="TRUE" *) reg [1:0] locked_sync=0;
reg seen_lock=0;
reg [3:0] reset_hold=4'hf;
always @(posedge control_clk or posedge reset_request)begin
if(reset_request)begin locked_sync<=0;seen_lock<=0;reset_hold<=4'hf;end
else begin
locked_sync<={locked_sync[0],raw_locked};
reset_hold<={reset_hold[2:0],1'b0};
if(locked_sync[1])seen_lock<=1;
else if(seen_lock)begin reset_hold<=4'hf;seen_lock<=0;end
end
end
// UG953 requires reset after loss of LOCKED. This supervisor runs from the
// independent, existing 100-MHz bus clock, not the possibly stopped RF clock.
wire mmcm_reset=reset_request || reset_hold[3];
MMCME2_BASE #(.BANDWIDTH("OPTIMIZED"),.CLKFBOUT_MULT_F(24.0),.DIVCLK_DIVIDE(1),
.CLKIN1_PERIOD(25.0),.CLKOUT0_DIVIDE_F(3.0),.CLKOUT0_DUTY_CYCLE(0.5),
.CLKOUT0_PHASE(0.0),.REF_JITTER1(0.100),.STARTUP_WAIT("FALSE")) mmcm(
.CLKIN1(radio_clk),.CLKFBIN(feedback_buffered),.CLKFBOUT(feedback),
.CLKOUT0(fast_unbuffered),.LOCKED(raw_locked),.RST(mmcm_reset),.PWRDWN(1'b0));
BUFG feedback_buffer(.I(feedback),.O(feedback_buffered));
BUFG fast_buffer(.I(fast_unbuffered),.O(fast_clk));
assign locked=raw_locked && !mmcm_reset;
endmodule
wifi_e310_link/fpga/open_e310/gf_host_waveform_tx.sv · 151 lines
Download this file · Permanent section link
// Streaming serial carry is the constant +1 specialization of Brian
// Greenforest's LSB-first serial full adder: sum=x^carry, carry=x&carry.
// Continuous 12-clock words; no start/busy/drain bubble or parallel '+' cell.
// This is control arithmetic, not a claim that the receiver DSP is converted.
`timescale 1ns/1ps
module gf_serial_increment12 (
input wire clk, input wire resetn, input wire [11:0] value,
output reg [11:0] source, output reg [11:0] result,
output reg valid
);
reg [11:0] phase=12'b1, x=0, partial=0, snapshot=0;
reg carry=0;
wire bit_x = phase[0] ? value[0] : x[0];
wire bit_c = phase[0] ? 1'b1 : carry;
wire bit_sum = bit_x ^ bit_c;
always @(posedge clk) begin
if (!resetn) begin
phase<=12'b1; x<=0; partial<=0; snapshot<=0;
carry<=0; source<=0; result<=0; valid<=0;
end else begin
phase<={phase[10:0],phase[11]};
x<=phase[0] ? {1'b0,value[11:1]} : {1'b0,x[11:1]};
if (phase[0]) snapshot<=value;
carry<=bit_x & bit_c;
partial<={bit_sum,partial[11:1]};
if (phase[11]) begin
source<=snapshot; result<={bit_sum,partial[11:1]}; valid<=1;
end
end
end
endmodule
// Generic two-IQ-level pattern player. No knowledge of Wi-Fi framing, CRC,
// scrambler, differential encoding or Barker sequence exists in this module.
// Format: 20-bit polarity pattern LE, byte 3=20, two packed IQ16 words LE,
// followed by LSB-first phase selections. Every phase uses 20 sample ticks.
module gf_host_waveform_tx #(
parameter integer MEMORY_READ_LATENCY = 0
) (
input wire clk, input wire resetn, input wire arm, input wire kill,
input wire frame_commit, input wire [11:0] frame_length_bytes,
output reg [11:0] frame_read_address, input wire [7:0] frame_read_data,
input wire tx_channel_available, input wire tx_sample_tick,
input wire tx_sink_ready,
output wire ready, output wire busy, output wire tx_rf_claim,
output wire tx_valid, output wire [31:0] tx_iq,
output reg frame_done, output reg frame_error
);
localparam [2:0] IDLE=0, HEADER=1, PRELOAD=2, LEAD=3, ACTIVE=4, READ_WAIT=5;
initial if(MEMORY_READ_LATENCY<0 || MEMORY_READ_LATENCY>1)
$error("Supported memory read latency is zero or one clock");
reg [2:0] state=IDLE;
reg [11:0] length_bytes=0;
reg [19:0] pattern=0, sample_phase=20'b1;
reg [7:0] phase_byte=0, bit_phase=8'b1;
reg [31:0] level_a=0, level_b=0;
// A local one-hot delay replaces an inferred parallel decrementer.
reg [79:0] lead=80'b1;
wire [11:0] increment_source, increment_result;
wire increment_valid;
gf_serial_increment12 next_address (
.clk(clk), .resetn(resetn), .value(frame_read_address),
.source(increment_source), .result(increment_result), .valid(increment_valid)
);
wire increment_matches = increment_valid && increment_source==frame_read_address;
assign ready = state==IDLE && resetn && arm && !kill;
assign busy = state!=IDLE;
assign tx_rf_claim = resetn && arm && !kill &&
(state==ACTIVE || (state==LEAD && tx_channel_available));
assign tx_valid = state==ACTIVE && resetn && arm && !kill;
assign tx_iq = (pattern[0] ^ phase_byte[0]) ? level_b : level_a;
always @(posedge clk) begin
if (!resetn) begin
state<=IDLE; frame_read_address<=0; length_bytes<=0;
pattern<=0; sample_phase<=20'b1; phase_byte<=0; bit_phase<=8'b1;
level_a<=0; level_b<=0; lead<=80'b1;
frame_done<=0; frame_error<=0;
end else begin
frame_done<=0; frame_error<=0;
if(kill || !arm) begin
if(state!=IDLE) frame_error<=1;
state<=IDLE;
end else case(state)
IDLE: if(frame_commit) begin
if(frame_length_bytes<=12) frame_error<=1;
else begin
length_bytes<=frame_length_bytes; frame_read_address<=0;
state<=HEADER;
end
end
HEADER: if(increment_matches) begin
case(frame_read_address)
0: pattern[7:0]<=frame_read_data;
1: pattern[15:8]<=frame_read_data;
2: pattern[19:16]<=frame_read_data[3:0];
4: level_a[7:0]<=frame_read_data;
5: level_a[15:8]<=frame_read_data;
6: level_a[23:16]<=frame_read_data;
7: level_a[31:24]<=frame_read_data;
8: level_b[7:0]<=frame_read_data;
9: level_b[15:8]<=frame_read_data;
10: level_b[23:16]<=frame_read_data;
11: level_b[31:24]<=frame_read_data;
default: begin end
endcase
if((frame_read_address==2 && frame_read_data[7:4]!=0) ||
(frame_read_address==3 && frame_read_data!=20)) begin
frame_error<=1; state<=IDLE;
end else begin
frame_read_address<=increment_result;
if(frame_read_address==11)
state<=MEMORY_READ_LATENCY ? READ_WAIT : PRELOAD;
end
end
READ_WAIT: state<=PRELOAD;
PRELOAD: begin
phase_byte<=frame_read_data; frame_read_address<=13;
sample_phase<=20'b1; bit_phase<=8'b1; lead<=80'b1;
state<=LEAD;
end
LEAD: begin
if(!tx_channel_available) lead<=80'b1;
else if(!lead[79]) lead<={lead[78:0],1'b0};
else if(tx_sample_tick && tx_sink_ready) state<=ACTIVE;
end
ACTIVE: if(tx_sample_tick) begin
if(!tx_sink_ready) begin frame_error<=1; state<=IDLE; end
else begin
pattern<={pattern[0],pattern[19:1]};
sample_phase<={sample_phase[18:0],sample_phase[19]};
if(sample_phase[19]) begin
bit_phase<={bit_phase[6:0],bit_phase[7]};
if(bit_phase[7]) begin
if(frame_read_address==length_bytes) begin
frame_done<=1; state<=IDLE;
end else if(!increment_matches) begin
frame_error<=1; state<=IDLE;
end else begin
phase_byte<=frame_read_data;
frame_read_address<=increment_result;
end
end else phase_byte<={1'b0,phase_byte[7:1]};
end
end
end
default: begin frame_error<=1; state<=IDLE; end
endcase
end
end
endmodule
wifi_e310_link/fpga/open_e310/gf_rx_crc_fit.sv · 14 lines
Download this file · Permanent section link
// Fit-only boundary; the deployed graph consumes the RX descrambler bitstream.
module gf_rx_crc_fit_top (
input wire clk,reset,bit_valid,data_bit,
output wire good
);
wire valid_r,data_r,matched;
wire [31:0] fcs;
gf_serial_reg input_valid(.clk(clk),.reset(reset),.d(bit_valid),.q(valid_r));
gf_serial_reg input_data(.clk(clk),.reset(reset),.d(data_bit),.q(data_r));
gf_control_crc_bitserial crc(.clk(clk),.clear(reset),.bit_valid(valid_r),.data_bit(data_r),.fcs(fcs));
gf_rx_crc_residue residue(.clk(clk),.clear(reset),.fcs(fcs),.good(matched));
gf_serial_reg output_good(.clk(clk),.reset(reset),.d(matched),.q(good));
endmodule
wifi_e310_link/fpga/open_e310/gf_serial_differential.sv · 101 lines
Download this file · Permanent section link
// Scheduled adapter for the bubbles-free scalar differential graph.
// One request per 40 clocks, at any fixed phase. A phase change must flush
// pending metadata before starting the new phase. Arithmetic never stalls.
`timescale 1ns/1ps
module gf_serial_differential(
input wire clk,resetn,enable,flush,
input wire request_valid,
input wire signed [17:0] ai,bi,aq,bq,
input wire [4:0] request_phase,
input wire [7:0] request_age,
output wire request_ready,
output reg result_valid,result_sign,
output reg [4:0] result_phase,
output reg [15:0] result_age,
output reg overflow
);
reg [39:0] slot;
reg [7:0] tick;
reg pending,current_valid,previous_valid;
reg signed [17:0] pending_ai,pending_bi,pending_aq,pending_bq;
reg signed [17:0] shift_ai,shift_bi,shift_aq,shift_bq;
reg [4:0] pending_phase,current_phase,previous_phase;
reg [7:0] pending_tick,current_tick,previous_tick;
reg [7:0] pending_age,current_age,previous_age;
wire boundary=slot[39];
wire sum_bit,sum_end;
wire [7:0] elapsed=tick-previous_tick;
assign request_ready=!pending || boundary;
`ifdef GF_COMPACT_SERIAL_FANOUT
localparam integer COMPACT_FANOUT=1;
`else
localparam integer COMPACT_FANOUT=0;
`endif
gf_serial_dot18 #(.COMPACT_FANOUT(COMPACT_FANOUT)) graph(.clk(clk),.reset(!resetn || !enable),
.ai(shift_ai[0]),.bi(shift_bi[0]),.aq(shift_aq[0]),.bq(shift_bq[0]),
.sum_bit(sum_bit),.sum_word_end(sum_end));
always @(posedge clk) begin
if(!resetn || !enable) begin
slot<=40'd1;tick<=0;
pending<=0;current_valid<=0;previous_valid<=0;
pending_ai<=0;pending_bi<=0;pending_aq<=0;pending_bq<=0;
shift_ai<=0;shift_bi<=0;shift_aq<=0;shift_bq<=0;
pending_phase<=0;current_phase<=0;previous_phase<=0;
pending_tick<=0;current_tick<=0;previous_tick<=0;
pending_age<=0;current_age<=0;previous_age<=0;
result_valid<=0;result_sign<=0;result_phase<=0;result_age<=0;
overflow<=0;
end else begin
slot<={slot[38:0],slot[39]};
// Clock age must advance every clock; these are eight-bit timing
// counters, not sample arithmetic or a wide multiply/add datapath.
tick<=tick+1'b1;
result_valid<=0;
shift_ai<={shift_ai[17],shift_ai[17:1]};
shift_bi<={shift_bi[17],shift_bi[17:1]};
shift_aq<={shift_aq[17],shift_aq[17:1]};
shift_bq<={shift_bq[17],shift_bq[17:1]};
if(boundary) begin
// Next core word starts on the following clock. Its last
// sum bit is consumed 55 clocks after this boundary, after
// one further boundary has advanced its metadata.
shift_ai<=pending ? pending_ai : 18'sd0;
shift_bi<=pending ? pending_bi : 18'sd0;
shift_aq<=pending ? pending_aq : 18'sd0;
shift_bq<=pending ? pending_bq : 18'sd0;
previous_valid<=current_valid;
previous_phase<=current_phase;
previous_tick<=current_tick;
previous_age<=current_age;
current_valid<=pending;
current_phase<=pending_phase;
current_tick<=pending_tick;
current_age<=pending_age;
pending<=0;
end
if(sum_end && previous_valid && !overflow) begin
result_valid<=1;
result_sign<=sum_bit;
result_phase<=previous_phase;
result_age<={8'd0,previous_age}+{8'd0,elapsed};
end
if(request_valid && request_ready) begin
pending<=1;
pending_ai<=ai;pending_bi<=bi;pending_aq<=aq;pending_bq<=bq;
pending_phase<=request_phase;
pending_tick<=tick;
pending_age<=request_age;
end else if(request_valid) begin
overflow<=1;
result_valid<=0;
end
if(flush) begin
// Core words may keep flowing, but no old-phase result can
// acquire meaning in the next descrambler context.
pending<=0;current_valid<=0;previous_valid<=0;result_valid<=0;
end
end
end
endmodule
wifi_e310_link/fpga/open_e310/gf_serial_mul40.sv · 262 lines
Download this file · Permanent section link
// Brian Greenforest's fully retimed, bubbles-free low-word multiplier graph.
// Adapted from serial_multiplier_hitl/rtl/gf_logisim_mul64_low_full_retime.sv:
// 40 LSB-first bits/word, 40 active partials in the same six-level padded tree.
// The 40-bit result is exact modulo 2^40. Sign-extended 18x18 inputs therefore
// preserve the entire signed product, without a DSP or inferred wide multiply.
// First output bit follows first input bit by 13 clocks. No clock enable/gaps.
`timescale 1ns/1ps
`default_nettype none
module gf_serial_reg #(parameter INIT = 1'b0)(
input wire clk, reset, d, output wire q
);
`ifdef SYNTHESIS
generate if (INIT) begin : g_set
(* DONT_TOUCH = "true" *) FDPE #(.INIT(1'b1)) r(.C(clk),.CE(1'b1),.PRE(reset),.D(d),.Q(q));
end else begin : g_clear
(* DONT_TOUCH = "true" *) FDCE #(.INIT(1'b0)) r(.C(clk),.CE(1'b1),.CLR(reset),.D(d),.Q(q));
end endgenerate
`else
// Match the explicit FDCE/FDPE INIT above, including time-zero values
// observed by non-reset SRL histories before the first clock edge.
reg value=INIT;
always @(posedge clk or posedge reset)
if (reset) value <= INIT; else value <= d;
assign q = value;
`endif
endmodule
module gf_serial_lut #(parameter [15:0] INIT=16'd0)(
input wire a,b,c,d, output wire q
);
`ifdef SYNTHESIS
(* DONT_TOUCH = "true" *) LUT4 #(.INIT(INIT)) l(.I0(a),.I1(b),.I2(c),.I3(d),.O(q));
`else
assign q = INIT[{d,c,b,a}];
`endif
endmodule
module gf_serial_fanout #(parameter N=32, parameter D=$clog2(N), parameter LIVE_LEAVES=N)(
input wire clk,reset,bit_in, output wire [N-1:0] leaves
);
wire [N-1:0] stage [0:D];
initial if(LIVE_LEAVES<1 || LIVE_LEAVES>N || (1<<D)!=N)
$error("Fanout requires a nonempty prefix of a power-of-two tree");
gf_serial_reg root(.clk(clk),.reset(reset),.d(bit_in),.q(stage[0][0]));
generate for(genvar level=1;level<=D;level=level+1) begin : g_level
// Keep the original D+1 clocks on every live leaf. A prefix needs
// only its ancestors, not the unused half-trees held by DONT_TOUCH.
for(genvar node=0;node<((LIVE_LEAVES+(1<<(D-level))-1)>>(D-level));node=node+1) begin : g_node
gf_serial_reg branch(.clk(clk),.reset(reset),
.d(stage[level-1][node>>1]),.q(stage[level][node]));
end
end endgenerate
assign leaves=stage[D];
generate if(LIVE_LEAVES<N) begin:g_unused
assign stage[D][N-1:LIVE_LEAVES]=0;
end endgenerate
endmodule
module gf_serial_add_retimed(
input wire clk,reset,clear_carry,x,y, output wire sum
);
wire carry, sum_next, carry_next;
gf_serial_lut #(.INIT(16'h9696)) sum_lut(.a(x),.b(y),.c(carry),.d(1'b0),.q(sum_next));
gf_serial_lut #(.INIT(16'h00e8)) carry_lut(.a(x),.b(y),.c(carry),.d(clear_carry),.q(carry_next));
gf_serial_reg sum_reg(.clk(clk),.reset(reset),.d(sum_next),.q(sum));
gf_serial_reg carry_reg(.clk(clk),.reset(reset),.d(carry_next),.q(carry));
endmodule
module gf_serial_mul40(
input wire clk,reset,a_bit,b_bit,
output wire product_bit,product_word_end
);
wire [31:0] a_leaf;
gf_serial_fanout a_tree(.clk(clk),.reset(reset),.bit_in(a_bit),.leaves(a_leaf));
wire [39:0] phase,phase_tap;
wire [31:0] clear_leaf;
gf_serial_fanout word_clear_tree(.clk(clk),.reset(reset),.bit_in(phase_tap[39]),.leaves(clear_leaf));
gf_serial_reg #(.INIT(1'b1)) phase_zero(.clk(clk),.reset(reset),.d(phase[39]),.q(phase[0]));
generate for(genvar p=1;p<40;p=p+1) begin : g_phase
gf_serial_reg step(.clk(clk),.reset(reset),.d(phase[p-1]),.q(phase[p]));
end endgenerate
// A phase-ring node drives only its successor and one local tap. Advance
// the chosen tap by one phase to compensate this registered duplication.
generate for(genvar p=0;p<40;p=p+1) begin : g_phase_tap
gf_serial_reg tap(.clk(clk),.reset(reset),.d(phase[p]),.q(phase_tap[p]));
end endgenerate
wire [39:0] a_hold,b_delay,partial;
wire [5:0] b_input_delay;
generate for(genvar b=0;b<6;b=b+1) begin : g_b_input
if(b==0) begin
gf_serial_reg r(.clk(clk),.reset(reset),.d(b_bit),.q(b_input_delay[b]));
end else begin
gf_serial_reg r(.clk(clk),.reset(reset),.d(b_input_delay[b-1]),.q(b_input_delay[b]));
end
end endgenerate
generate for(genvar lane=0;lane<40;lane=lane+1) begin : g_lane
wire load,hold_next,active,active_next;
gf_serial_reg load_reg(.clk(clk),.reset(reset),.d(phase_tap[(lane+4)%40]),.q(load));
gf_serial_lut #(.INIT(16'hcaca)) hold_mux(.a(a_hold[lane]),.b(a_leaf[lane>>1]),
.c(load),.d(1'b0),.q(hold_next));
gf_serial_reg hold_reg(.clk(clk),.reset(reset),.d(hold_next),.q(a_hold[lane]));
// Retire the preceding word's high partials at bit zero. Each lane
// becomes valid again with its new A bit; no inter-word idle clocks.
// Registered clear fanout has the same six-edge delay as A.
assign active_next = load | (active & ~clear_leaf[lane>>1]);
gf_serial_reg active_reg(.clk(clk),.reset(reset),.d(active_next),.q(active));
if(lane==0) begin
gf_serial_reg b_reg(.clk(clk),.reset(reset),.d(b_input_delay[5]),.q(b_delay[lane]));
end else begin
gf_serial_reg b_reg(.clk(clk),.reset(reset),.d(b_delay[lane-1]),.q(b_delay[lane]));
end
gf_serial_reg partial_reg(.clk(clk),.reset(reset),.d(a_hold[lane]&b_delay[lane]&active),.q(partial[lane]));
end endgenerate
wire [63:0] tree [0:6];
assign tree[0] = {24'd0,partial};
generate for(genvar level=0;level<6;level=level+1) begin : g_reduce
localparam N=32>>level;
wire [N-1:0] clear;
gf_serial_fanout #(.N(N),.D(5-level)) clear_tree(.clk(clk),.reset(reset),
.bit_in(phase_tap[2*level]),.leaves(clear));
for(genvar node=0;node<N;node=node+1) begin : g_node
gf_serial_add_retimed add(.clk(clk),.reset(reset),.clear_carry(clear[node]),
.x(tree[level][2*node]),.y(tree[level][2*node+1]),.sum(tree[level+1][node]));
end
assign tree[level+1][63:N] = 0;
end endgenerate
assign product_bit = tree[6][0];
// A separate marker register avoids a third load on a phase tap when the
// downstream dot adder and its end-marker register both consume this bit.
gf_serial_reg marker_reg(.clk(clk),.reset(reset),.d(phase_tap[11]),.q(product_word_end));
endmodule
// Signed-18 specialization of the same bubbles-free graph. Input A must be
// sign-extended from 18 to 40 bits; B may be any 40-bit word. The output remains
// exact modulo 2^40. A = sum(A[i]*2^i, i=0..16) - A[17]*2^17: the 23 repeated
// sign rows collapse to ONE negative row, without discarding any input bits.
// A retimed sign-negation layer replaces one removed reduction-tree level,
// preserving the original 13-clock latency and 40-clock word initiation rate.
module gf_serial_mul18_to40 #(parameter integer PRUNE_ZERO_ROWS=1,
parameter integer COMPACT_FANOUT=0)(
input wire clk,reset,a_bit,b_bit,
output wire product_bit,product_word_end
);
wire [31:0] a_leaf,clear_leaf;
wire [39:0] phase,phase_tap;
gf_serial_fanout #(.LIVE_LEAVES(COMPACT_FANOUT ? 9 : 32)) a_tree(.clk(clk),.reset(reset),.bit_in(a_bit),.leaves(a_leaf));
gf_serial_fanout #(.LIVE_LEAVES(COMPACT_FANOUT ? 9 : 32)) word_clear_tree(.clk(clk),.reset(reset),.bit_in(phase_tap[39]),.leaves(clear_leaf));
gf_serial_reg #(.INIT(1'b1)) phase_zero(.clk(clk),.reset(reset),.d(phase[39]),.q(phase[0]));
generate for(genvar p=1;p<40;p=p+1)begin:g_phase
gf_serial_reg step(.clk(clk),.reset(reset),.d(phase[p-1]),.q(phase[p]));
end
for(genvar p=0;p<40;p=p+1)begin:g_phase_tap
gf_serial_reg tap(.clk(clk),.reset(reset),.d(phase[p]),.q(phase_tap[p]));
end endgenerate
wire [17:0] a_hold,b_delay,partial,prepared;
wire [5:0] b_input_delay;
generate for(genvar b=0;b<6;b=b+1)begin:g_b_input
if(b==0)begin
gf_serial_reg r(.clk(clk),.reset(reset),.d(b_bit),.q(b_input_delay[b]));
end else begin
gf_serial_reg r(.clk(clk),.reset(reset),.d(b_input_delay[b-1]),.q(b_input_delay[b]));
end
end
for(genvar lane=0;lane<18;lane=lane+1)begin:g_lane
wire load,hold_next,active,active_next;
gf_serial_reg load_reg(.clk(clk),.reset(reset),.d(phase_tap[(lane+4)%40]),.q(load));
gf_serial_lut #(.INIT(16'hcaca)) hold_mux(.a(a_hold[lane]),.b(a_leaf[lane>>1]),
.c(load),.d(1'b0),.q(hold_next));
gf_serial_reg hold_reg(.clk(clk),.reset(reset),.d(hold_next),.q(a_hold[lane]));
assign active_next=load | (active & ~clear_leaf[lane>>1]);
gf_serial_reg active_reg(.clk(clk),.reset(reset),.d(active_next),.q(active));
if(lane==0)begin
gf_serial_reg b_reg(.clk(clk),.reset(reset),.d(b_input_delay[5]),.q(b_delay[lane]));
end else begin
gf_serial_reg b_reg(.clk(clk),.reset(reset),.d(b_delay[lane-1]),.q(b_delay[lane]));
end
gf_serial_reg partial_reg(.clk(clk),.reset(reset),.d(a_hold[lane]&b_delay[lane]&active),.q(partial[lane]));
if(lane<17)begin
gf_serial_reg prepare(.clk(clk),.reset(reset),.d(partial[lane]),.q(prepared[lane]));
end
end endgenerate
// LSB-first negation: leave bits through the first 1 unchanged, then invert.
// Clear on the last bit of the partial word, so the next word starts clean.
// The extra marker register avoids a third load on phase_tap[6].
wire negative_seen,negative_next,negative_bit,negative_clear;
gf_serial_reg negative_marker(.clk(clk),.reset(reset),.d(phase_tap[5]),.q(negative_clear));
gf_serial_lut #(.INIT(16'h6666)) negate(.a(partial[17]),.b(negative_seen),.c(1'b0),.d(1'b0),.q(negative_bit));
gf_serial_lut #(.INIT(16'h0e0e)) seen_next(.a(partial[17]),.b(negative_seen),.c(negative_clear),.d(1'b0),.q(negative_next));
gf_serial_reg seen_reg(.clk(clk),.reset(reset),.d(negative_next),.q(negative_seen));
gf_serial_reg negative_reg(.clk(clk),.reset(reset),.d(negative_bit),.q(prepared[17]));
wire [31:0] tree[0:5];
assign tree[0]={14'd0,prepared};
generate for(genvar level=0;level<5;level=level+1)begin:g_reduce
localparam N=16>>level;
localparam LIVE_INPUTS=(18+(1<<level)-1)>>level;
wire [N-1:0] clear;
gf_serial_fanout #(.N(N),.D(4-level),
.LIVE_LEAVES(COMPACT_FANOUT && PRUNE_ZERO_ROWS ? LIVE_INPUTS/2 : N)) clear_tree(.clk(clk),.reset(reset),
.bit_in(phase_tap[2*level+2]),.leaves(clear));
for(genvar node=0;node<N;node=node+1)begin:g_node
if(!PRUNE_ZERO_ROWS || 2*node+1<LIVE_INPUTS)begin:g_pair
gf_serial_add_retimed add(.clk(clk),.reset(reset),.clear_carry(clear[node]),
.x(tree[level][2*node]),.y(tree[level][2*node+1]),.sum(tree[level+1][node]));
end else if(2*node<LIVE_INPUTS)begin:g_single
// x+0 with reset carry=0 never creates a carry. Retain the
// exact one-clock sum delay, without carry state or LUTs.
gf_serial_reg delay(.clk(clk),.reset(reset),.d(tree[level][2*node]),.q(tree[level+1][node]));
end else begin:g_zero
// Both operands and reset carry are identically zero.
assign tree[level+1][node]=1'b0;
end
end
assign tree[level+1][31:N]=0;
end endgenerate
assign product_bit=tree[5][0];
gf_serial_reg marker_reg(.clk(clk),.reset(reset),.d(phase_tap[11]),.q(product_word_end));
endmodule
// Two full signed products and their full signed sum, still a one-bit graph.
// Feed sign-extended 18-bit operands as 40-bit words. The 37-bit signed dot
// product is entirely preserved in the 40-bit result; bit 39 is its sign.
module gf_serial_dot18 #(parameter integer SIGNED18_ROWS=1, parameter integer PRUNE_ZERO_ROWS=1,
parameter integer COMPACT_FANOUT=0)(
input wire clk,reset,ai,bi,aq,bq,
output wire sum_bit,sum_word_end
);
wire pi,pq,ei,eq_unused;
generate if(SIGNED18_ROWS)begin:g_signed_rows
gf_serial_mul18_to40 #(.PRUNE_ZERO_ROWS(PRUNE_ZERO_ROWS),.COMPACT_FANOUT(COMPACT_FANOUT)) mul_i(.clk(clk),.reset(reset),.a_bit(ai),.b_bit(bi),
.product_bit(pi),.product_word_end(ei));
gf_serial_mul18_to40 #(.PRUNE_ZERO_ROWS(PRUNE_ZERO_ROWS),.COMPACT_FANOUT(COMPACT_FANOUT)) mul_q(.clk(clk),.reset(reset),.a_bit(aq),.b_bit(bq),
.product_bit(pq),.product_word_end(eq_unused));
end else begin:g_generic_rows
gf_serial_mul40 mul_i(.clk(clk),.reset(reset),.a_bit(ai),.b_bit(bi),
.product_bit(pi),.product_word_end(ei));
gf_serial_mul40 mul_q(.clk(clk),.reset(reset),.a_bit(aq),.b_bit(bq),
.product_bit(pq),.product_word_end(eq_unused));
end endgenerate
gf_serial_add_retimed add(.clk(clk),.reset(reset),.clear_carry(ei),
.x(pi),.y(pq),.sum(sum_bit));
gf_serial_reg end_reg(.clk(clk),.reset(reset),.d(ei),.q(sum_word_end));
endmodule
// Fit-only registered interface. Package I/O timing is excluded by the fit
// constraints; every arithmetic launch/capture path uses the routed clock.
module gf_serial_dot18_fit_top #(parameter integer SIGNED18_ROWS=1, parameter integer PRUNE_ZERO_ROWS=1,
parameter integer COMPACT_FANOUT=0)(
input wire clk,reset,ai,bi,aq,bq,
output wire sum_bit,sum_word_end
);
wire air,bir,aqr,bqr,s,e;
gf_serial_reg in_ai(.clk(clk),.reset(reset),.d(ai),.q(air));
gf_serial_reg in_bi(.clk(clk),.reset(reset),.d(bi),.q(bir));
gf_serial_reg in_aq(.clk(clk),.reset(reset),.d(aq),.q(aqr));
gf_serial_reg in_bq(.clk(clk),.reset(reset),.d(bq),.q(bqr));
gf_serial_dot18 #(.SIGNED18_ROWS(SIGNED18_ROWS),.PRUNE_ZERO_ROWS(PRUNE_ZERO_ROWS),.COMPACT_FANOUT(COMPACT_FANOUT)) graph(.clk(clk),.reset(reset),.ai(air),.bi(bir),.aq(aqr),.bq(bqr),
.sum_bit(s),.sum_word_end(e));
gf_serial_reg out_sum(.clk(clk),.reset(reset),.d(s),.q(sum_bit));
gf_serial_reg out_end(.clk(clk),.reset(reset),.d(e),.q(sum_word_end));
endmodule
`default_nettype wire
wifi_e310_link/fpga/open_e310/tb_gf_barker_radio_bridge.sv · 60 lines
Download this file · Permanent section link
`timescale 1ns/1fs
module tb_gf_barker_radio_bridge #(parameter integer FAST_PHASE_PS=0);
localparam integer SAMPLES=1024;
reg clk=0,fast=0,resetn=0,enable=0,locked=1,valid=0;
always #12.5 clk=~clk;
initial begin #(FAST_PHASE_PS*0.001);forever #1.5625 fast=~fast;end
reg signed [15:0] i=0,q=0;
wire receiver_resetn,result_valid,fault;
wire signed [23:0] ci,cq;
gf_barker_radio_bridge dut(clk,fast,resetn,enable,locked,valid,i,q,receiver_resetn,result_valid,fault,ci,cq);
integer iv[0:SAMPLES+31],qv[0:SAMPLES+31],ei[0:SAMPLES+31],eq[0:SAMPLES+31];
integer epoch,n,k,c,index,count=0,fault_clock=-1;
reg wanted;
function automatic integer coefficient(input integer lag);
coefficient=(lag<5 || (lag>=10 && lag<12) || (lag>=16 && lag<18))?-1:1;
endfunction
initial begin
for(epoch=0;epoch<4;epoch=epoch+1)begin
@(negedge clk);resetn=0;enable=0;valid=0;
repeat(5+epoch)@(negedge clk);
for(n=0;n<SAMPLES+32;n=n+1)begin
iv[n]=n<SAMPLES?$signed(16'($random)):0;qv[n]=n<SAMPLES?$signed(16'($random)):0;
if(n%19==0)begin iv[n]=-32768;qv[n]=32767;end
ei[n]=0;eq[n]=0;
for(k=0;k<20;k=k+1)if(n>=k)begin
ei[n]=ei[n]+coefficient(k)*iv[n-k];eq[n]=eq[n]+coefficient(k)*qv[n-k];
end
end
resetn=1;enable=1;fault_clock=-1;
for(c=0;c<2*SAMPLES+18;c=c+1)begin
valid=c%2==0;i=valid?iv[c/2]:$random;q=valid?qv[c/2]:$random;
if(epoch==2 && c==600)valid=0;
if(epoch==3 && c==601)valid=1;
@(posedge clk);#0.1;
if(fault && fault_clock<0)fault_clock=c;
if(epoch<2 || c<600)begin
if(fault)$fatal(1,"unexpected bridge fault phase=%0d epoch=%0d cycle=%0d",FAST_PHASE_PS,epoch,c);
wanted=c>=45 && (c-7)%2==0;
if(result_valid!==wanted)$fatal(1,"bridge valid/age phase=%0d epoch=%0d cycle=%0d got=%b expected=%b",FAST_PHASE_PS,epoch,c,result_valid,wanted);
end
if(result_valid)begin
index=(c-7)/2;
if(ci!==24'(ei[index]) || cq!==24'(eq[index]))$fatal(1,"bridge IQ phase=%0d epoch=%0d cycle=%0d sample=%0d",FAST_PHASE_PS,epoch,c,index);
if(index<SAMPLES)count=count+1;
end
if(fault && result_valid)$fatal(1,"fault failed to veto result");
if(epoch>=2 && c>=605 && !fault)$fatal(1,"missing cadence fault");
@(negedge clk);
end
// Lock loss must veto outputs immediately without fast-clock work.
locked=0;#0.1;
if(result_valid || receiver_resetn)$fatal(1,"clock-loss veto failed");
locked=1;
end
$display("BARKER_RADIO_BRIDGE_PASS phase_ps=%0d comparisons=%0d latency_radio_clocks=7 cadence_faults=2 reset_epochs=4 physical_rf=false",FAST_PHASE_PS,count);
$finish;
end
initial begin #1000000;$fatal(1,"timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_barker_recurrence.sv · 66 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_barker_recurrence;
reg clk=0,clear=1,valid=0;
always #5 clk=~clk;
reg signed [15:0] sample=0;
wire signed [23:0] correlation;
wire result_valid;
reg signed [15:0] history[0:19];
localparam integer LATENCY=12;
reg signed [23:0] expected[0:LATENCY-1];
reg [LATENCY-1:0] expected_valid=0;
reg [31:0] rng=32'hde123abc;
integer sum,n,k,cycle,checks=0;
gf_dsss_barker_recurrence dut(.clk(clk),.clear(clear),.sample_valid(valid),
.sample(sample),.correlation(correlation),.result_valid(result_valid));
always @(posedge clk)begin
if(clear)begin
for(n=0;n<20;n=n+1)history[n]=0;
for(n=0;n<LATENCY;n=n+1)expected[n]<=0;
expected_valid<=0;
end else begin
expected_valid<={expected_valid[LATENCY-2:0],valid};
for(n=1;n<LATENCY;n=n+1)expected[n]<=expected[n-1];
if(valid)begin
for(n=19;n>0;n=n-1)history[n]=history[n-1];
history[0]=sample;
sum=0;
// Independent original oldest-to-newest 20-term definition.
for(n=0;n<20;n=n+1)begin
case(n)
2,3,8,9,15,16,17,18,19:sum=sum-$signed(history[19-n]);
default:sum=sum+$signed(history[19-n]);
endcase
end
expected[0]<=sum;
end
end
end
always @(negedge clk)if(!clear)begin
if(result_valid!==expected_valid[LATENCY-1])$fatal(1,"valid latency mismatch");
if(result_valid)begin
if(correlation!==expected[LATENCY-1])$fatal(1,"correlation mismatch actual=%0d expected=%0d cycle=%0d",correlation,expected[LATENCY-1],cycle);
checks=checks+1;
end
end
initial begin
repeat(4)@(negedge clk);#1;clear=0;
for(cycle=0;cycle<200000;cycle=cycle+1)begin
@(negedge clk);#1;
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
clear=(cycle%997==0);
// Consecutive inputs, regular 20-MS/s and irregular sample gaps.
valid=cycle<1000 ? 1 : cycle<2000 ? !cycle[0] : rng[0]||rng[1];
if(cycle<40)sample=cycle==1 ? 16'sh8000 : cycle==22 ? 16'sh7fff : 0;
else if(cycle<250)sample=16'sh7fff;
else if(cycle<500)sample=16'sh8000;
else if(cycle<1000)sample=cycle[0]?16'sh8000:16'sh7fff;
else sample=rng[31:16];
end
@(negedge clk);#1;clear=0;valid=0;
repeat(LATENCY+4)@(negedge clk);
$display("BARKER_RECURRENCE_PASS checks=%0d full_signed_width=24 sample_gaps=true reset_flush=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_barker_serial.sv · 65 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_barker_serial;
reg clk=0,reset=1,a=0,b=0;
always #1.5625 clk=~clk;
wire bit_out,word_end;
gf_barker_serial_lane dut(.clk(clk),.reset(reset),.current_bit(a),.previous_bit(b),
.correlation_bit(bit_out),.correlation_word_end(word_end));
integer history[0:19],expected[0:20000];
integer stream_word,output_word,bit_index,j,current,previous,result,epoch,passed=0;
reg [31:0] observed=0;
function automatic integer coefficient(input integer n);
coefficient=(n<5 || (n>=10 && n<12) || (n>=16 && n<18)) ? -1 : 1;
endfunction
task push(input integer value);
begin for(integer k=19;k>0;k=k-1)history[k]=history[k-1];history[0]=value;end
endtask
always @(posedge clk)begin
#0.1;
if(!reset)begin
observed={bit_out,observed[31:1]};
if(word_end)begin
// A marker appears during the initial pipeline fill. Input
// phase zero after reset defines the first complete word.
if(output_word>=9)begin
if(observed!==expected[output_word])
$fatal(1,"correlation mismatch epoch=%0d word=%0d got=%h expected=%h",epoch,output_word,observed,expected[output_word]);
passed=passed+1;
end
output_word=output_word+1;
end
end
end
initial begin
for(epoch=0;epoch<3;epoch=epoch+1)begin
@(negedge clk);reset=1;a=0;b=0;
repeat(5+epoch)@(negedge clk);
for(j=0;j<20;j=j+1)history[j]=0;
output_word=-1;observed=0;reset=0;
for(stream_word=0;stream_word<5000;stream_word=stream_word+1)begin
case(stream_word%17)
0:begin current=-32768;previous=32767;end
1:begin current=32767;previous=-32768;end
2:begin current=-32768;previous=-32768;end
3:begin current=32767;previous=32767;end
4:begin current=0;previous=0;end
default:begin current=$signed(16'($random));previous=$signed(16'($random));end
endcase
push(previous);push(current);result=0;
for(j=0;j<20;j=j+1)result=result+coefficient(j)*history[j];
expected[stream_word]=result;
for(bit_index=0;bit_index<32;bit_index=bit_index+1)begin
a=(current>>>bit_index)&1;b=(previous>>>bit_index)&1;
@(negedge clk);
end
end
// Abandon a partial word, reset with stale SRL storage, then fill
// fresh samples. No runtime reset of the sample history is needed.
a=0;b=0;repeat(7+epoch)@(negedge clk);
end
$display("BARKER_SERIAL_LANE_PASS comparisons=%0d signed_iq16=true word_bits=32 no_word_gaps=true reset_with_stale_history=true physical_rf=false",passed);
$finish;
end
initial begin #10000000;$fatal(1,"timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_barker_serial_interleaved.sv · 86 lines
Download this file · Permanent section link
`timescale 1ns/1fs
// Four scalar lanes: even/odd for each of I and Q. Serialization here is an
// independent TESTBENCH source, not a claimed implemented ADC interface.
module tb_gf_barker_serial_interleaved;
localparam integer SAMPLES=2048;
reg clk=0,reset_a=1,reset_b=1;
always #1.5625 clk=~clk; // 320 MHz: one new complex sample per 16 clocks.
reg ai=0,ap_i=0,aq=0,ap_q=0,bi=0,bp_i=0,bq=0,bp_q=0;
wire asi,asq,bsi,bsq,ae,aqe,be,bqe;
gf_barker_serial_lane ia(clk,reset_a,ai,ap_i,asi,ae);
gf_barker_serial_lane qa(clk,reset_a,aq,ap_q,asq,aqe);
gf_barker_serial_lane ib(clk,reset_b,bi,bp_i,bsi,be);
gf_barker_serial_lane qb(clk,reset_b,bq,bp_q,bsq,bqe);
integer samples_i[0:SAMPLES-1],samples_q[0:SAMPLES-1];
integer expected_i[0:SAMPLES-1],expected_q[0:SAMPLES-1];
integer epoch,cycle,n,k,a_index,b_index,word_a,word_b,pos_a,pos_b;
integer count=0,last_cycle=-1,qualified_cycle=0;
reg [31:0] ia_word=0,qa_word=0,ib_word=0,qb_word=0;
function automatic integer value(input integer index,input integer quadrature);
if(index<0 || index>=SAMPLES)value=0;
else value=quadrature ? samples_q[index] : samples_i[index];
endfunction
function automatic integer coefficient(input integer lag);
coefficient=(lag<5 || (lag>=10 && lag<12) || (lag>=16 && lag<18)) ? -1 : 1;
endfunction
task check(input integer index,input [31:0] iv,input [31:0] qv);
begin
if(index>=19 && index<SAMPLES)begin
if(iv!==expected_i[index] || qv!==expected_q[index])
$fatal(1,"interleaved mismatch epoch=%0d sample=%0d I=%h/%h Q=%h/%h",epoch,index,iv,expected_i[index],qv,expected_q[index]);
if(last_cycle>=0 && qualified_cycle-last_cycle!=16)
$fatal(1,"interleaved result gap clocks=%0d",qualified_cycle-last_cycle);
last_cycle=qualified_cycle;count=count+1;
end
end
endtask
always @(posedge clk)begin
#0.1;
if(!reset_a)begin
qualified_cycle=qualified_cycle+1;
ia_word={asi,ia_word[31:1]};qa_word={asq,qa_word[31:1]};
if(ae!==aqe)$fatal(1,"even IQ marker alignment");
if(ae)begin check(a_index,ia_word,qa_word);a_index=a_index+2;end
end
if(!reset_b)begin
ib_word={bsi,ib_word[31:1]};qb_word={bsq,qb_word[31:1]};
if(be!==bqe)$fatal(1,"odd IQ marker alignment");
if(be)begin check(b_index,ib_word,qb_word);b_index=b_index+2;end
end
end
initial begin
for(epoch=0;epoch<3;epoch=epoch+1)begin
@(negedge clk);reset_a=1;reset_b=1;
ai=0;ap_i=0;aq=0;ap_q=0;bi=0;bp_i=0;bq=0;bp_q=0;
repeat(7+epoch)@(negedge clk);
for(n=0;n<SAMPLES;n=n+1)begin
samples_i[n]=$signed(16'($random));samples_q[n]=$signed(16'($random));
if(n%19==0)begin samples_i[n]=-32768;samples_q[n]=32767;end
if(n%19==1)begin samples_i[n]=32767;samples_q[n]=-32768;end
expected_i[n]=0;expected_q[n]=0;
for(k=0;k<20;k=k+1)begin
expected_i[n]=expected_i[n]+coefficient(k)*value(n-k,0);
expected_q[n]=expected_q[n]+coefficient(k)*value(n-k,1);
end
end
a_index=-2;b_index=-1;last_cycle=-1;qualified_cycle=0;
ia_word=0;qa_word=0;ib_word=0;qb_word=0;
reset_a=0;
for(cycle=0;cycle<SAMPLES*16+64;cycle=cycle+1)begin
if(cycle==16)reset_b=0;
word_a=(cycle/32)*2;pos_a=cycle%32;
word_b=cycle<16 ? -1 : ((cycle-16)/32)*2+1;pos_b=(cycle+16)%32;
ai=(value(word_a,0)>>>pos_a)&1;ap_i=(value(word_a-1,0)>>>pos_a)&1;
aq=(value(word_a,1)>>>pos_a)&1;ap_q=(value(word_a-1,1)>>>pos_a)&1;
bi=(value(word_b,0)>>>pos_b)&1;bp_i=(value(word_b-1,0)>>>pos_b)&1;
bq=(value(word_b,1)>>>pos_b)&1;bp_q=(value(word_b-1,1)>>>pos_b)&1;
@(negedge clk);
end
end
if(count!=3*(SAMPLES-19))$fatal(1,"comparison coverage %0d",count);
$display("BARKER_SERIAL_INTERLEAVED_PASS complex_results=%0d interval_clocks=16 clock_mhz=320 precision=full_iq16 reset_epochs=3 adc_interface=testbench physical_rf=false",count);
$finish;
end
initial begin #10000000;$fatal(1,"timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_barker_serial_iq.sv · 69 lines
Download this file · Permanent section link
`timescale 1ns/1fs
// Independent parallel sample source, not a testbench serial word generator.
module tb_gf_barker_serial_iq;
localparam integer SAMPLES=2048;
reg clk=0,reset=1,sample_valid=0;
always #1.5625 clk=~clk;
reg signed [15:0] sample_i=0,sample_q=0;
wire slot,fault,valid;
wire signed [31:0] ci,cq;
gf_barker_serial_iq dut(clk,reset,sample_valid,sample_i,sample_q,slot,fault,valid,ci,cq);
integer iv[0:SAMPLES+7],qv[0:SAMPLES+7],ei[0:SAMPLES+7],eq[0:SAMPLES+7];
integer epoch,n,k,cycle,index,count=0,fault_checks=0,qualified=0;
reg expected_fault,wanted;
function automatic integer coefficient(input integer lag);
coefficient=(lag<5 || (lag>=10 && lag<12) || (lag>=16 && lag<18))?-1:1;
endfunction
initial begin
for(epoch=0;epoch<5;epoch=epoch+1)begin
@(negedge clk);reset=1;sample_valid=0;
repeat(7+epoch)@(negedge clk);
for(n=0;n<SAMPLES+8;n=n+1)begin
iv[n]=n<SAMPLES?$signed(16'($random)):0;
qv[n]=n<SAMPLES?$signed(16'($random)):0;
if(n<SAMPLES && n%19==0)begin iv[n]=-32768;qv[n]=32767;end
if(n<SAMPLES && n%19==1)begin iv[n]=32767;qv[n]=-32768;end
ei[n]=0;eq[n]=0;
for(k=0;k<20;k=k+1)if(n>=k)begin
ei[n]=ei[n]+coefficient(k)*iv[n-k];
eq[n]=eq[n]+coefficient(k)*qv[n-k];
end
end
expected_fault=0;reset=0;
for(cycle=0;cycle<SAMPLES*16+53;cycle=cycle+1)begin
sample_valid=cycle%16==0;
// Change parallel inputs even between sample slots, proving
// that the implemented PISO only consumes scheduled words.
sample_i=sample_valid?iv[cycle/16]:$random;
sample_q=sample_valid?qv[cycle/16]:$random;
if(epoch==3 && cycle==611)sample_valid=1; // unexpected sample
if(epoch==4 && cycle==608)sample_valid=0; // missing sample
#0.1;
if(slot!==(cycle%16==0))$fatal(1,"slot mismatch epoch=%0d cycle=%0d",epoch,cycle);
if(sample_valid!=(cycle%16==0))expected_fault=1;
@(posedge clk);#0.1;
if($test$plusargs("TRACE") && epoch==0 && cycle<350 && cycle%16==8)
$display("IQ_TRACE cycle=%0d phase=%h load=%h/%h cur=%b/%b prev=%b/%b words=%h/%h out=%h ready=%b",cycle,dut.phase,dut.load[0],dut.load[1],dut.current[0],dut.current[1],dut.previous[0],dut.previous[1],dut.words[0][0],dut.words[1][0],ci,dut.ready);
if($test$plusargs("TRACE") && epoch==0 && cycle<350 && cycle%16==8)
$display("CORE_TRACE phase=%h taps=%h positive=%h negative=%h clear=%b diff=%b hist=%b/%b",dut.g_parity[0].g_iq[0].core.phase,dut.g_parity[0].g_iq[0].core.taps,dut.g_parity[0].g_iq[0].core.positive[4],dut.g_parity[0].g_iq[0].core.negative[4],dut.g_parity[0].g_iq[0].core.clear_difference,dut.g_parity[0].g_iq[0].correlation_bit,dut.g_parity[0].g_iq[0].core.current_history,dut.g_parity[0].g_iq[0].core.previous_history);
if(fault!==expected_fault)$fatal(1,"cadence fault mismatch cycle=%0d",cycle);
wanted=cycle>=344 && (cycle-40)%16==0 && !expected_fault;
if(valid!==wanted)$fatal(1,"valid mismatch epoch=%0d cycle=%0d got=%b wanted=%b",epoch,cycle,valid,wanted);
if(valid)begin
index=(cycle-40)/16;
if(ci!==ei[index] || cq!==eq[index])
$fatal(1,"parallel IQ mismatch epoch=%0d cycle=%0d sample=%0d I=%h/%h Q=%h/%h",epoch,cycle,index,ci,ei[index],cq,eq[index]);
qualified=qualified+1;
if(epoch<3 && index<SAMPLES)count=count+1;
end
if(expected_fault)fault_checks=fault_checks+1;
@(negedge clk);
end
end
if(count!=3*(SAMPLES-19))$fatal(1,"coverage mismatch %0d",count);
$display("BARKER_SERIAL_IQ_PASS complete_complex_results=%0d comparisons=%0d latency_clocks=40 interval_clocks=16 clock_mhz=320 precision=full_iq16 reset_epochs=5 fault_suppression_clocks=%0d serializer=rtl clock_crossing=not_integrated physical_rf=false",count,qualified,fault_checks);
$finish;
end
initial begin #10000000;$fatal(1,"timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_compact_fanout.sv · 35 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_compact_fanout;
reg clk=0,reset=1,ai=0,bi=0,aq=0,bq=0;
always #5 clk=~clk;
wire old_bit,new_bit,old_end,new_end;
gf_serial_dot18 #(.COMPACT_FANOUT(0)) reference_graph(
.clk(clk),.reset(reset),.ai(ai),.bi(bi),.aq(aq),.bq(bq),
.sum_bit(old_bit),.sum_word_end(old_end));
gf_serial_dot18 #(.COMPACT_FANOUT(1)) candidate_graph(
.clk(clk),.reset(reset),.ai(ai),.bi(bi),.aq(aq),.bq(bq),
.sum_bit(new_bit),.sum_word_end(new_end));
integer phase,cycle,checks=0;
// Arbitrary input bits challenge graph equivalence even outside the
// signed-input protocol. Reset at each of the forty possible word phases.
initial begin
repeat(3) @(negedge clk);
for(phase=0;phase<40;phase=phase+1)begin
reset=0;
for(cycle=0;cycle<4000+phase;cycle=cycle+1)begin
ai=$random;bi=$random;aq=$random;bq=$random;
@(posedge clk);#1;
if({old_bit,old_end} !== {new_bit,new_end})
$fatal(1,"compact mismatch phase=%0d cycle=%0d",phase,cycle);
checks=checks+1;
@(negedge clk);
end
#2;reset=1;#1;
if({old_bit,old_end} !== {new_bit,new_end}) $fatal(1,"async reset mismatch");
repeat(2) @(negedge clk);
end
$display("COMPACT_FANOUT_EQUIVALENCE_PASS clocks=%0d reset_phases=40 latency_unchanged=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_control_crc.sv · 35 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_control_crc;
reg clk=0,clear=1,valid=0,data=0;
always #5 clk=~clk;
wire [31:0] fcs;
reg [31:0] reference_crc=32'hffffffff,rng=32'h4320cfe1;
integer n,k,checks=0;
gf_control_crc_bitserial dut(.clk(clk),.clear(clear),.bit_valid(valid),.data_bit(data),.fcs(fcs));
initial begin
repeat(3)@(negedge clk);clear=0;
for(n=0;n<2048;n=n+1)begin
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
valid=1;data=rng[0];
reference_crc=(reference_crc>>1)^((reference_crc[0]^data)?32'hedb88320:0);
@(negedge clk);valid=0;
repeat(5)@(negedge clk);
if(fcs!==~reference_crc)$fatal(1,"serial CRC mismatch bit=%0d",n);
checks=checks+1;
repeat(n%4)@(negedge clk);
if(n%37==0)begin
// Cancel a sampled bit at each of the five in-flight ages.
valid=1;data=1;@(negedge clk);valid=0;
repeat(n%5)@(negedge clk);
clear=1;@(negedge clk);clear=0;
reference_crc=32'hffffffff;
repeat(6)@(negedge clk);
if(fcs!==0)$fatal(1,"CRC stale bit escaped clear");
checks=checks+1;
end
end
$display("CONTROL_CRC_SERIAL_PASS checks=%0d full_crc32=true minimum_interval=6 clear_flush=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_control_crc_waveforms.sv · 85 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_control_crc_waveforms;
reg clk=0,resetn=0,arm=0,kill=0,prepare=0,start=0,is_cts=0,ready=1;
always #5 clk=~clk;
reg [15:0] duration=0;
reg [47:0] mac=0;
integer cycle=0,tick_mode=0,parity=0,frame=0,samples=0,fd,frames=0,total_samples=0;
reg capture=0;
reg [31:0] rng=32'h9049eacd;
wire tick=tick_mode==0 || (tick_mode==1 && cycle%2==parity) || (tick_mode==2 && cycle%3==parity);
always @(posedge clk)cycle<=cycle+1;
wire [1:0] path_ready,active,iq_valid,done,abort;
wire [31:0] iq[0:1],abort_count[0:1];
genvar m;
generate for(m=0;m<2;m=m+1)begin:paths
gf_dsss_1mbps_control_tx #(.SERIAL_CONTROL_CRC(m)) tx (
.clk(clk),.resetn(resetn),.arm(arm),.kill(kill),
.response_prepare(prepare),.response_start(start),.response_is_cts(is_cts),
.response_duration_us(duration),.response_station_mac(mac),
.tx_sample_tick(tick),.tx_sink_ready(ready),.response_path_ready(path_ready[m]),
.response_active(active[m]),.tx_override_valid(iq_valid[m]),.tx_override_iq(iq[m]),
.response_done(done[m]),.stream_abort(abort[m]),.stream_abort_count(abort_count[m])
);
end endgenerate
always @(posedge clk)if(resetn)begin
if(path_ready[0]!==path_ready[1] || active[0]!==active[1] ||
iq_valid[0]!==iq_valid[1] || done[0]!==done[1] || abort[0]!==abort[1] ||
abort_count[0]!==abort_count[1])$fatal(1,"formatter handshake difference");
if(iq_valid[0] && tick && ready)begin
if(iq[0]!==iq[1])$fatal(1,"formatter sample difference frame=%0d sample=%0d",frame,samples);
if(capture)begin
$fdisplay(fd,"%08x",iq[1]);samples=samples+1;total_samples=total_samples+1;
end
end
end
task prepare_start;
begin
@(negedge clk);prepare=1;
@(negedge clk);prepare=0;
repeat(frame%7)@(negedge clk);
if(path_ready!==2'b11)$fatal(1,"preparation readiness moved");
start=1;@(negedge clk);start=0;
end
endtask
task interrupt_prefix(input integer mode);
begin
capture=0;prepare_start();
wait(paths[1].tx.crc_header_bit);@(posedge clk);#1;
if(mode==0)kill=1;
else if(mode==1)ready=0;
else resetn=0;
repeat(12)@(negedge clk);
if(iq_valid!==0 || active!==0)$fatal(1,"interrupt left transmitter active");
kill=0;ready=1;resetn=1;
repeat(8)@(negedge clk);
end
endtask
string output_path;
initial begin
if(!$value$plusargs("OUTPUT=%s",output_path))$fatal(1,"OUTPUT required");
fd=$fopen(output_path,"w");if(!fd)$fatal(1,"cannot create waveform batch");
repeat(6)@(negedge clk);resetn=1;arm=1;
for(frame=0;frame<64;frame=frame+1)begin
tick_mode=frame%3;parity=frame%2;
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
mac=(frame==0)?48'd0:(frame==1)?48'hffffffffffff:{rng,rng[15:0]};
duration=(frame<32)?(16'd1<<(frame/2)):(frame<34)?16'hffff:rng[15:0];
is_cts=frame%2;
if(frame==4)interrupt_prefix(0);
if(frame==8)interrupt_prefix(1);
if(frame==12)interrupt_prefix(2);
samples=0;capture=1;
$fdisplay(fd,"FRAME %0d %0d %012x",is_cts,duration,mac);
prepare_start();wait(done==2'b11);@(negedge clk);capture=0;
if(samples!=6080)$fatal(1,"wrong control waveform length");
frames=frames+1;
repeat(3)@(negedge clk);
end
$fclose(fd);
$display("CONTROL_CRC_WAVEFORMS_PASS frames=%0d samples=%0d exact_ab=true kill_stall_reset=true physical_rf=false",frames,total_samples);
$finish;
end
initial begin #50000000;$fatal(1,"control waveform test timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_counter_offload.sv · 54 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_counter_offload;
reg source_clk=0, destination_clk=0, reset=1;
always #12.5 source_clk=~source_clk;
always #5 destination_clk=~destination_clk;
reg [31:0] count=0;
wire [31:0] binary, gray;
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(0)) legacy
(source_clk,reset,count,destination_clk,reset,binary);
gf_e310_counter_cdc #(.COUNTERS_TO_SOFTWARE(1)) offloaded
(source_clk,reset,count,destination_clk,reset,gray);
function automatic [31:0] decode(input [31:0] g);
integer bit_index;
begin
decode[31]=g[31];
for(bit_index=30;bit_index>=0;bit_index=bit_index-1)
decode[bit_index]=decode[bit_index+1]^g[bit_index];
end
endfunction
task automatic settle_check(input [31:0] n);
begin
@(negedge source_clk); count=n;
repeat(15) @(posedge destination_clk);
#1;
if(binary!==n || decode(gray)!==n || gray!==(n^(n>>1)))
$fatal(1,"settled CDC mismatch n=%h binary=%h gray=%h",n,binary,gray);
end
endtask
integer n;
reg [31:0] last;
initial begin
repeat(5) @(negedge destination_clk); reset=0;
last=0;
// Real monotonic source transitions, no multibit binary sampling.
for(n=1;n<4096;n=n+1) begin
@(negedge source_clk); count=n;
@(negedge destination_clk);
if(decode(gray)<last || decode(gray)>count) $fatal(1,"nonmonotonic coherent counter");
last=decode(gray);
end
for(n=0;n<32;n=n+1) begin
settle_check((32'd1<<n)-1);
settle_check(32'd1<<n);
settle_check((32'd1<<n)+1);
end
settle_check(32'hffffffff); settle_check(0);
@(negedge destination_clk); reset=1;
repeat(5) @(negedge source_clk);
if(binary!==0 || gray!==0) $fatal(1,"reset mismatch");
$display("COUNTER_CDC_TEST_PASS binary_gray=true boundaries=32 monotonic=4095 reset=true");
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_dsss_1mbps_psdu_tx.sv · 171 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_dsss_1mbps_psdu_tx;
reg clk = 1'b0;
always #12.5 clk = ~clk;
reg resetn = 1'b0;
reg arm = 1'b0;
reg kill = 1'b1;
reg frame_commit = 1'b0;
reg [11:0] frame_length = 12'd20;
wire [11:0] frame_address;
reg [7:0] memory [0:4095];
wire [7:0] frame_data = memory[frame_address];
reg tx_tick = 1'b0;
wire ready;
wire busy;
wire tx_rf_claim;
wire tx_valid;
wire [31:0] tx_iq;
wire frame_done;
wire frame_error;
reg [31:0] expected [0:7039];
string expected_path;
integer sample_count = 0;
integer timeout_count = 0;
integer index;
reg [31:0] frame_fcs;
reg allow_frame_error = 1'b0;
reg saw_frame_error = 1'b0;
function automatic [31:0] crc32_prefix;
input integer byte_count;
integer byte_number;
integer bit_number;
reg [31:0] crc;
begin
crc = 32'hffff_ffff;
for (byte_number = 0; byte_number < byte_count;
byte_number = byte_number + 1) begin
crc = crc ^ memory[byte_number];
for (bit_number = 0; bit_number < 8;
bit_number = bit_number + 1) begin
if (crc[0])
crc = (crc >> 1) ^ 32'hedb8_8320;
else
crc = crc >> 1;
end
end
crc32_prefix = ~crc;
end
endfunction
gf_dsss_1mbps_psdu_tx #(
.RF_LEAD_CYCLES(4)
) dut (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(kill),
.frame_commit(frame_commit),
.frame_length_bytes(frame_length),
.frame_read_address(frame_address),
.frame_read_data(frame_data),
.tx_channel_available(1'b1),
.tx_sample_tick(tx_tick),
.tx_sink_ready(1'b1),
.ready(ready),
.busy(busy),
.tx_rf_claim(tx_rf_claim),
.tx_valid(tx_valid),
.tx_iq(tx_iq),
.frame_done(frame_done),
.frame_error(frame_error)
);
always @(posedge clk) begin
tx_tick <= ~tx_tick;
if (tx_valid && tx_tick) begin
if (sample_count >= 7040) begin
$display("unexpected extra sample %0d", sample_count);
$fatal(1);
end
if (tx_iq !== expected[sample_count]) begin
$display("sample mismatch index=%0d actual=%08x expected=%08x",
sample_count, tx_iq, expected[sample_count]);
$fatal(1);
end
sample_count <= sample_count + 1;
end
if (frame_error && !allow_frame_error) begin
$display("unexpected frame error");
$fatal(1);
end
if (frame_error)
saw_frame_error <= 1'b1;
end
initial begin
if (!$value$plusargs("EXPECTED=%s", expected_path)) begin
$display("missing +EXPECTED=path");
$fatal(1);
end
$readmemh(expected_path, expected);
for (index = 0; index < 4096; index = index + 1)
memory[index] = 8'd0;
// RTS: AP receiver, fixed station transmitter, duration 3934 us.
memory[0] = 8'hb4;
memory[1] = 8'h00;
memory[2] = 8'h5e;
memory[3] = 8'h0f;
memory[4] = 8'h02;
memory[5] = 8'h47;
memory[6] = 8'h46;
memory[7] = 8'h41;
memory[8] = 8'h50;
memory[9] = 8'h31;
memory[10] = 8'hdc;
memory[11] = 8'h4f;
memory[12] = 8'h22;
memory[13] = 8'h5e;
memory[14] = 8'hd0;
memory[15] = 8'h2a;
frame_fcs = crc32_prefix(16);
memory[16] = frame_fcs[7:0];
memory[17] = frame_fcs[15:8];
memory[18] = frame_fcs[23:16];
memory[19] = frame_fcs[31:24];
repeat (6) @(posedge clk);
resetn <= 1'b1;
arm <= 1'b1;
kill <= 1'b0;
repeat (2) @(posedge clk);
if (!ready) $fatal(1, "transmitter did not become ready");
frame_commit <= 1'b1;
@(posedge clk);
frame_commit <= 1'b0;
while (!frame_done && timeout_count < 30000) begin
@(posedge clk);
timeout_count = timeout_count + 1;
end
if (!frame_done) $fatal(1, "transmitter timed out");
if (sample_count != 7040)
$fatal(1, "sample count %0d != 7040", sample_count);
if (busy || tx_valid || tx_rf_claim)
$fatal(1, "transmitter did not return idle");
// A kill during the next packet must remove TX ownership immediately.
frame_commit <= 1'b1;
@(posedge clk);
frame_commit <= 1'b0;
wait (tx_valid);
allow_frame_error <= 1'b1;
kill <= 1'b1;
@(posedge clk);
@(posedge clk);
#1;
if (busy || tx_valid || tx_rf_claim)
$fatal(1, "kill did not close the packet path");
if (!saw_frame_error)
$fatal(1, "kill did not report the aborted packet");
$display("E310_GENERAL_DSSS_TX_SELFTEST_PASS samples=%0d", sample_count);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_dsss_plcp_service.sv · 34 lines
Download this file · Permanent section link
// Unit check of supported PLCP fields; not a physical RF experiment.
`timescale 1ns/1ps
module tb_gf_dsss_plcp_service;
reg clk = 0;
always #12.5 clk = ~clk;
gf_dsss_1mbps_rx receiver (
.clk(clk), .resetn(1'b0), .enable(1'b0),
.rx_sample_valid(1'b0), .rx_i(16'sd0), .rx_q(16'sd0)
);
integer service;
initial begin
force receiver.plcp_signal = 8'h0a;
force receiver.plcp_length_us = 16'd360;
for (service = 0; service < 256; service = service + 1) begin
force receiver.plcp_service = service;
#1;
if (receiver.plcp_fields_valid !== (service == 0 || service == 4))
$fatal(1, "Incorrect supported SERVICE mask: %02x", service);
end
force receiver.plcp_service = 8'h04;
force receiver.plcp_signal = 8'h14;
#1; if (receiver.plcp_fields_valid) $fatal(1, "Unsupported rate accepted");
force receiver.plcp_signal = 8'h0a;
force receiver.plcp_length_us = 16'd0;
#1; if (receiver.plcp_fields_valid) $fatal(1, "Zero length accepted");
force receiver.plcp_length_us = 16'd361;
#1; if (receiver.plcp_fields_valid) $fatal(1, "Unaligned length accepted");
force receiver.plcp_length_us = 16'd32768;
#1; if (receiver.plcp_fields_valid) $fatal(1, "Oversize length accepted");
$display("DSSS_PLCP_SERVICE_FIELDS_PASS service00=true service04=true other_service_bits_rejected=true");
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_async_fifo.sv · 101 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_e310_async_fifo #(
parameter integer USE_BLOCK_RAM=0, ADDRESS_WIDTH=4,
parameter integer WRITE_HALF=13, READ_HALF=5
);
localparam integer DEPTH=1<<ADDRESS_WIDTH;
reg write_clk=0,read_clk=0,reset=1,write_enable=0,read_pop=0;
always #(WRITE_HALF) write_clk=~write_clk;
always #(READ_HALF) read_clk=~read_clk;
reg [9:0] write_data=0;
wire [9:0] read_data;
wire write_ready,write_overflow,read_valid;
gf_e310_async_fifo #(.WIDTH(10),.ADDRESS_WIDTH(ADDRESS_WIDTH),.USE_BLOCK_RAM(USE_BLOCK_RAM)) dut
(write_clk,reset,write_data,write_enable,write_ready,write_overflow,
read_clk,reset,read_data,read_valid,read_pop);
reg [9:0] expected [0:100000];
integer written=0,consumed=0,overflows=0,held_cycles=0;
reg held=0;
reg [9:0] held_word=0;
always @(posedge write_clk) begin
if(!reset) begin
if(write_enable && write_ready) begin
expected[written]=write_data; written=written+1;
end
if(write_overflow) overflows=overflows+1;
end
end
always @(posedge read_clk) begin
if(reset) held=0;
else begin
if(held && (!read_valid || read_data!==held_word)) $fatal(1,"stalled output changed");
if(read_valid && read_pop) begin
if(consumed>=written || read_data!==expected[consumed])
$fatal(1,"FIFO ordering bram=%0d index=%0d got=%h expected=%h",USE_BLOCK_RAM,consumed,read_data,expected[consumed]);
consumed=consumed+1;
end
held=read_valid&&!read_pop; held_word=read_data;
if(held) held_cycles=held_cycles+1;
end
end
integer n,w,r,discarded;
reg [31:0] wr_random=32'h9abc7654,rd_random=32'h1234defa;
task automatic drain;
begin
@(negedge read_clk);read_pop=1;
repeat(DEPTH+30) @(negedge read_clk);
read_pop=0;
if(consumed!=written || read_valid) $fatal(1,"drain/empty mismatch");
end
endtask
initial begin
repeat(5) @(negedge write_clk); reset=0;
for(n=0;n<DEPTH+4;n=n+1) begin
@(negedge write_clk); write_enable=1;write_data=n;
end
@(negedge write_clk);write_enable=0;
if(written!=DEPTH || overflows!=4 || write_ready) $fatal(1,"capacity/overflow mismatch");
// The full stalled output must remain valid and identical.
repeat(15) @(negedge read_clk);
drain();
fork
begin
for(w=0;w<20000;w=w+1) begin
@(negedge write_clk);
wr_random=wr_random^(wr_random<<13);wr_random=wr_random^(wr_random>>17);wr_random=wr_random^(wr_random<<5);
write_enable=wr_random[0]|wr_random[1];write_data=wr_random[11:2];
end
@(negedge write_clk);write_enable=0;
end
begin
for(r=0;r<60000;r=r+1) begin
@(negedge read_clk);
rd_random=rd_random^(rd_random<<13);rd_random=rd_random^(rd_random>>17);rd_random=rd_random^(rd_random<<5);
read_pop=rd_random[0]|rd_random[1];
end
@(negedge read_clk);read_pop=0;
end
join
drain();
// Reset while data is present must not expose an old word afterward.
repeat(10) @(negedge write_clk);
for(n=0;n<3;n=n+1) begin
@(negedge write_clk);write_enable=1;write_data=10'h155+n;
end
@(negedge write_clk);write_enable=0;
repeat(10) @(negedge read_clk);
discarded=written-consumed;reset=1;
repeat(5) @(negedge write_clk);
consumed=written;reset=0;
repeat(10) @(negedge read_clk);
if(read_valid) $fatal(1,"reset leaked stale data");
@(negedge write_clk);write_enable=1;write_data=10'h2ab;
@(negedge write_clk);write_enable=0;
drain();
if(held_cycles<10 || discarded!=3) $fatal(1,"test coverage incomplete");
$display("FIFO_TEST_PASS bram=%0d depth=%0d writes=%0d checked=%0d overflows=%0d stalled=%0d reset_discarded=%0d",USE_BLOCK_RAM,DEPTH,written,consumed-discarded,overflows,held_cycles,discarded);
$finish;
end
initial begin #10000000; $fatal(1,"timeout"); end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_frontend_2g4.sv · 115 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_e310_frontend_2g4;
reg arm = 1'b0;
reg kill = 1'b1;
reg tx_pll_locked = 1'b0;
reg rx_pll_locked = 1'b0;
reg tx_claim = 1'b0;
reg logical_tx_channel = 1'b0;
reg [1:0] rx_use_txrx = 2'b00;
wire [2:0] tx_bandsel, rx1_bandsel, rx2_bandsel;
wire [1:0] rx1b, rx1c, rx2b, rx2c;
wire tx1a, tx1b, tx2a, tx2b;
wire vctx1v1, vctx1v2, vctx2v1, vctx2v2;
wire vcrx1v1, vcrx1v2, vcrx2v1, vcrx2v2;
wire led1tx, led1trxrx, led1rx, led2tx, led2trxrx, led2rx;
wire tx_rf_active;
gf_e310_frontend_2g4 dut (
.arm(arm), .kill(kill),
.tx_pll_locked(tx_pll_locked),
.rx_pll_locked(rx_pll_locked),
.tx_claim(tx_claim),
.logical_tx_channel(logical_tx_channel),
.rx_use_txrx(rx_use_txrx),
.TX_BANDSEL(tx_bandsel),
.RX1_BANDSEL(rx1_bandsel),
.RX2_BANDSEL(rx2_bandsel),
.RX1B_BANDSEL(rx1b), .RX1C_BANDSEL(rx1c),
.RX2B_BANDSEL(rx2b), .RX2C_BANDSEL(rx2c),
.TX_ENABLE1A(tx1a), .TX_ENABLE1B(tx1b),
.TX_ENABLE2A(tx2a), .TX_ENABLE2B(tx2b),
.VCTXRX1_V1(vctx1v1), .VCTXRX1_V2(vctx1v2),
.VCTXRX2_V1(vctx2v1), .VCTXRX2_V2(vctx2v2),
.VCRX1_V1(vcrx1v1), .VCRX1_V2(vcrx1v2),
.VCRX2_V1(vcrx2v1), .VCRX2_V2(vcrx2v2),
.LED_TXRX1_TX(led1tx), .LED_TXRX1_RX(led1trxrx),
.LED_RX1_RX(led1rx),
.LED_TXRX2_TX(led2tx), .LED_TXRX2_RX(led2trxrx),
.LED_RX2_RX(led2rx),
.tx_rf_active(tx_rf_active)
);
task automatic require;
input condition;
input [8*80-1:0] message;
begin
if (!condition) begin
$display("FAIL: %0s", message);
$fatal(1);
end
end
endtask
initial begin
#1;
require(tx_bandsel == 3'b000 &&
rx1_bandsel == 3'b101 && rx2_bandsel == 3'b100 &&
rx1b == 2'b01 && rx1c == 2'b00 &&
rx2b == 2'b10 && rx2c == 2'b00,
"2.4 GHz filter codes");
require({tx1b,tx1a,tx2b,tx2a} == 4'b0000 &&
{vctx1v1,vctx1v2,vctx2v1,vctx2v2} == 4'b0000 &&
{vcrx1v1,vcrx1v2,vcrx2v1,vcrx2v2} == 4'b0000,
"killed state is not RF closed");
arm = 1'b1;
kill = 1'b0;
tx_pll_locked = 1'b1;
rx_pll_locked = 1'b1;
#1;
require({vcrx1v1,vcrx1v2} == 2'b01 &&
{vcrx2v1,vcrx2v2} == 2'b01,
"dedicated RX routing");
require(led1rx && led2rx && !tx_rf_active,
"dedicated RX LEDs");
rx_use_txrx = 2'b11;
#1;
require({vcrx1v1,vcrx1v2} == 2'b10 &&
{vcrx2v1,vcrx2v2} == 2'b10 &&
{vctx1v1,vctx1v2} == 2'b10 &&
{vctx2v1,vctx2v2} == 2'b01,
"shared TX/RX routing");
tx_claim = 1'b1;
logical_tx_channel = 1'b0;
#1;
require({tx2b,tx2a} == 2'b10 && {tx1b,tx1a} == 2'b00,
"logical channel zero did not select front end two");
require({vctx2v1,vctx2v2} == 2'b10 &&
{vctx1v1,vctx1v2} == 2'b00 && led2tx && !led1tx,
"front end two TX routing");
logical_tx_channel = 1'b1;
#1;
require({tx1b,tx1a} == 2'b10 && {tx2b,tx2a} == 2'b00,
"logical channel one did not select front end one");
require({vctx1v1,vctx1v2} == 2'b01 && led1tx && !led2tx,
"front end one TX routing");
tx_pll_locked = 1'b0;
#1;
require(!tx_rf_active &&
{tx1b,tx1a,tx2b,tx2a} == 4'b0000 &&
{vctx1v1,vctx1v2,vctx2v1,vctx2v2} == 4'b0000 &&
{vcrx1v1,vcrx1v2,vcrx2v1,vcrx2v2} == 4'b0000,
"PLL loss did not close RF paths");
$display("E310_FRONTEND_2G4_SELFTEST_PASS");
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_gp0_backpressure.sv · 338 lines
Download this file · Permanent section link
`timescale 1ns/1ps
// Host-bus logic only. Undefined PS7/radio primitives are deliberately omitted
// with iverilog -i; their interface nets are driven below, never treated as RF.
module tb_gf_e310_gp0_backpressure;
reg clk = 0;
always #5 clk = ~clk;
reg resetn = 0;
reg arvalid = 0, rready = 0;
reg [31:0] araddr = 0;
reg [11:0] arid = 0;
reg [3:0] arlen = 0;
reg awvalid = 0, wvalid = 0, bready = 0;
reg [31:0] awaddr = 0, wdata = 0;
reg [11:0] awid = 0;
reg [3:0] awlen = 0;
reg [11:0] wid = 0;
reg wlast = 1;
gf_e310_open_shell_top #(.SPI_CS_INDEX(1)) dut(.AVR_CS_R(1'b1), .AVR_SCK_R(1'b0), .AVR_MOSI_R(1'b0));
initial begin
force dut.pmu_irq = 1'b0;
#1;
if (dut.ps_irq_f2p !== 20'd0) $fatal(1, "Inactive PMU raised PS interrupt");
force dut.pmu_irq = 1'b1;
#1;
if (dut.ps_irq_f2p !== 20'h00008)
$fatal(1, "PMU must map to F2P[3]/GIC64/DT SPI32, not button release");
release dut.pmu_irq;
$display("E310_PMU_IRQ_ROUTING_PASS fabric_bit=3 gic_id=64 dt_spi=32");
end
integer accepted_reads = 0, completed_reads = 0;
integer accepted_addresses = 0, accepted_data = 0, completed_writes = 0;
integer register_reads = 0;
integer register_writes = 0;
integer burst_length;
reg [11:0] held_id;
reg [31:0] held_data;
wire [3:0] driven_clocks = {3'd0, clk};
wire [3:0] driven_resets = {3'd0, resetn};
// Registers support individual transfers, not bursts. Reject each burst
// without touching the register/FIFO, but return every AXI3 response beat.
task reject_read_burst(input integer length);
integer beat, reads_before, responses_before, touches_before;
reg [11:0] expected_id;
begin
reads_before = accepted_reads;
responses_before = completed_reads;
touches_before = register_reads;
expected_id = 12'h600 + length;
rready = 0;
arvalid = 1;
arlen = length - 1;
araddr = 32'h4001021c;
arid = expected_id;
while (!dut.gp0_arready) @(negedge clk);
@(negedge clk);
arvalid = 0;
// Live address/ID changes cannot alter the accepted transaction.
araddr = 32'h40010200;
arid = 12'hbad;
arlen = 0;
for (beat = 0; beat < length; beat = beat + 1) begin
repeat (3) begin
if (!dut.gp0_rvalid || dut.gp0_rid !== expected_id ||
dut.gp0_rdata !== 0 || dut.gp0_rresp !== 2'b11 ||
dut.gp0_rlast !== (beat == length - 1))
$fatal(1, "GP0 malformed rejected burst length=%0d beat=%0d last=%b resp=%b",
length, beat, dut.gp0_rlast, dut.gp0_rresp);
if (dut.gp0_arready)
$fatal(1, "GP0 opened read slot inside incomplete burst");
@(negedge clk);
end
rready = 1;
@(negedge clk);
rready = 0;
end
repeat (4) @(negedge clk);
if (dut.gp0_rvalid || accepted_reads != reads_before + 1 ||
completed_reads != responses_before + length ||
register_reads != touches_before)
$fatal(1, "GP0 rejected burst count or register side effect mismatch");
end
endtask
// One outstanding address, non-interleaved AXI3 WID stream. Exercise
// data-before-address as well as address-before-data, B backpressure,
// unsupported burst draining, and malformed ID/LAST rejection.
task check_write(input integer length, input integer data_first,
input integer bad_id, input integer bad_last);
integer beat, addresses_before, data_before, responses_before, touches_before;
reg [11:0] expected_id;
reg [1:0] expected_resp;
begin
addresses_before = accepted_addresses;
data_before = accepted_data;
responses_before = completed_writes;
touches_before = register_writes;
expected_id = 12'h700 + length;
expected_resp = (bad_id || bad_last) ? 2'b10 : ((length == 1) ? 2'b00 : 2'b11);
bready = 0;
awaddr = 32'h40010230;
awid = expected_id;
awlen = length - 1;
awvalid = !data_first;
if (!data_first) begin
while (!dut.gp0_awready) @(negedge clk);
@(negedge clk);
awvalid = 0;
awid = 12'hbad;
awlen = 0;
end
for (beat = 0; beat < length; beat = beat + 1) begin
wdata = 32'h80 + beat;
wid = (bad_id && beat == 0) ? 12'hbad : expected_id;
wlast = (beat == length - 1);
if (bad_last) wlast = !wlast;
wvalid = 1;
while (!dut.gp0_wready) begin
if (dut.gp0_bvalid) $fatal(1, "GP0 responded before final write beat");
@(negedge clk);
end
if (dut.gp0_bvalid) $fatal(1, "GP0 responded before accepting write data");
@(negedge clk);
wvalid = 0;
if (data_first && beat == 0) begin
repeat (4) @(negedge clk);
if (dut.gp0_bvalid || accepted_data != data_before + 1)
$fatal(1, "GP0 data-first slot did not wait for its address");
awvalid = 1;
while (!dut.gp0_awready) @(negedge clk);
@(negedge clk);
awvalid = 0;
awid = 12'hbad;
awlen = 0;
end
repeat (3) @(negedge clk);
if (beat != length - 1 && dut.gp0_bvalid)
$fatal(1, "GP0 early write response length=%0d beat=%0d", length, beat);
end
// Offer a new transaction while the old B response is blocked.
// Neither slot may accept it until the old response completes.
awvalid = 1;
wvalid = 1;
repeat (4) begin
if (!dut.gp0_bvalid || dut.gp0_bid !== expected_id ||
dut.gp0_bresp !== expected_resp || dut.gp0_awready || dut.gp0_wready)
$fatal(1, "GP0 write response mismatch length=%0d id=%h resp=%b expected=%b",
length, dut.gp0_bid, dut.gp0_bresp, expected_resp);
@(negedge clk);
end
awvalid = 0;
wvalid = 0;
bready = 1;
repeat (4) @(negedge clk);
if (accepted_addresses != addresses_before + 1 || accepted_data != data_before + length ||
completed_writes != responses_before + 1 ||
register_writes != touches_before + ((expected_resp == 0) ? 1 : 0))
$fatal(1, "GP0 write transaction count or side effect mismatch");
wlast = 1;
end
endtask
task reset_bus;
begin
arvalid = 0; awvalid = 0; wvalid = 0;
rready = 0; bready = 0; resetn = 0;
repeat (3) @(negedge clk);
if (dut.gp0_rvalid || dut.gp0_bvalid || dut.register_write || dut.register_read)
$fatal(1, "GP0 reset retained a response or register effect");
resetn = 1;
repeat (3) @(negedge clk);
end
endtask
task check_address(input [31:0] address, input [1:0] response,
input [31:0] expected, input integer wifi);
integer before_read, before_write;
begin
before_read = register_reads; before_write = register_writes;
araddr = address; arlen = 0; arid = 12'h949;
arvalid = 1; rready = 0;
while (!dut.gp0_arready) @(negedge clk);
@(negedge clk); arvalid = 0;
repeat (3) begin
if (!dut.gp0_rvalid || dut.gp0_rresp !== response || dut.gp0_rdata !== expected)
$fatal(1, "GP0 aperture read address=%h response=%b data=%h", address, dut.gp0_rresp, dut.gp0_rdata);
@(negedge clk);
end
rready = 1; repeat (4) @(negedge clk);
awaddr = address; awlen = 0; awid = 12'h949; awvalid = 1;
while (!dut.gp0_awready) @(negedge clk);
@(negedge clk); awvalid = 0;
wdata = 0; wid = 12'h949; wlast = 1; wvalid = 1; bready = 0;
while (!dut.gp0_wready) @(negedge clk);
@(negedge clk); wvalid = 0;
while (!dut.gp0_bvalid) @(negedge clk);
if (dut.gp0_bresp !== response) $fatal(1, "GP0 aperture write response");
bready = 1; repeat (4) @(negedge clk);
if (register_reads != before_read + wifi || register_writes != before_write + wifi)
$fatal(1, "GP0 aperture isolation failure");
end
endtask
initial begin
force dut.fclk_clk = driven_clocks;
force dut.fclk_resetn = driven_resets;
force dut.gp0_aresetn = resetn;
force dut.gp0_arvalid = arvalid;
force dut.gp0_araddr = araddr;
force dut.gp0_arid = arid;
force dut.gp0_arlen = arlen;
force dut.gp0_rready = rready;
force dut.gp0_awvalid = awvalid;
force dut.gp0_awaddr = awaddr;
force dut.gp0_awid = awid;
force dut.gp0_awlen = awlen;
force dut.gp0_wvalid = wvalid;
force dut.gp0_wdata = wdata;
force dut.gp0_wid = wid;
force dut.gp0_wlast = wlast;
force dut.gp0_wstrb = 4'hf;
force dut.gp0_bready = bready;
force dut.register_read_data = araddr;
repeat (4) @(negedge clk);
resetn = 1;
repeat (3) @(negedge clk);
araddr = 32'h40010200;
arid = 12'h123;
arvalid = 1;
@(negedge clk);
if (!dut.gp0_rvalid) $fatal(1, "first read response missing");
held_id = dut.gp0_rid;
held_data = dut.gp0_rdata;
// A second read arrives while the first response is backpressured.
araddr = 32'h40010210;
arid = 12'h456;
repeat (4) begin
@(negedge clk);
if (!dut.gp0_rvalid || dut.gp0_rid != held_id || dut.gp0_rdata != held_data)
$fatal(1, "GP0 overwrote a backpressured read response");
end
arvalid = 0;
rready = 1;
repeat (4) @(negedge clk);
if (accepted_reads != 1 || completed_reads != 1)
$fatal(1, "GP0 lost or duplicated an accepted read");
// Independent AXI address/data arrivals must each have one slot.
awvalid = 1;
awid = 12'habc;
awaddr = 32'h40010230;
@(negedge clk);
awid = 12'hdef;
awaddr = 32'h40010234;
repeat (4) @(negedge clk);
if (accepted_addresses != 1)
$fatal(1, "GP0 accepted a write address with no free slot");
awvalid = 0;
wvalid = 1;
wid = 12'habc;
wdata = 32'h00000080;
@(negedge clk);
wdata = 32'h00000099;
repeat (4) @(negedge clk);
if (accepted_data != 1 || !dut.gp0_bvalid || dut.gp0_bid != 12'habc)
$fatal(1, "GP0 write slot or response ID was overwritten");
wvalid = 0;
bready = 1;
repeat (4) @(negedge clk);
if (completed_writes != 1) $fatal(1, "GP0 write response count mismatch");
for (burst_length = 2; burst_length <= 16; burst_length = burst_length + 1)
reject_read_burst(burst_length);
// The next individual read must still operate normally.
rready = 1;
araddr = 32'h40010200;
arid = 12'h135;
arvalid = 1;
while (!dut.gp0_arready) @(negedge clk);
@(negedge clk);
arvalid = 0;
if (!dut.gp0_rvalid || dut.gp0_rid !== 12'h135 ||
dut.gp0_rdata !== 32'h40010200 || dut.gp0_rresp !== 0 || !dut.gp0_rlast)
$fatal(1, "GP0 individual read failed after rejected bursts");
repeat (4) @(negedge clk);
if (register_reads != 2) $fatal(1, "GP0 register read count mismatch");
for (burst_length = 1; burst_length <= 16; burst_length = burst_length + 1) begin
check_write(burst_length, 0, 0, 0);
check_write(burst_length, 1, 0, 0);
end
check_write(1, 0, 1, 0);
check_write(1, 1, 0, 1);
check_write(2, 1, 0, 1);
check_write(16, 0, 1, 0);
check_write(1, 1, 0, 0);
// Reset with a backpressured multi-beat read still outstanding.
arlen = 15; arid = 12'h811; arvalid = 1; rready = 0;
while (!dut.gp0_arready) @(negedge clk);
@(negedge clk);
arvalid = 0;
if (!dut.gp0_rvalid || dut.gp0_rlast) $fatal(1, "GP0 reset-read setup failed");
reset_bus();
reject_read_burst(2);
// Reset after the first beat of a rejected write. No B response yet.
awlen = 15; awid = 12'h812; awvalid = 1;
while (!dut.gp0_awready) @(negedge clk);
@(negedge clk);
awvalid = 0;
wvalid = 1; wid = 12'h812; wlast = 0;
while (!dut.gp0_wready) @(negedge clk);
@(negedge clk);
wvalid = 0;
repeat (3) @(negedge clk);
if (dut.gp0_bvalid) $fatal(1, "GP0 reset-write setup completed early");
reset_bus();
check_write(1, 1, 0, 0);
check_address(32'h40300004, 0, 0, 0);
check_address(32'h40000240, 3, 0, 0);
check_address(32'h40300240, 3, 0, 0);
check_address(32'h50010240, 3, 0, 0);
check_address(32'h40011240, 3, 0, 0);
check_address(32'h40010240, 0, 32'h40010240, 1);
$display("E310_GP0_ADDRESS_ISOLATION_SELFTEST_PASS legacy_pmu=40300000 wifi=40010000 physical_test=false");
$display("E310_GP0_BACKPRESSURE_SELFTEST_PASS");
$display("E310_GP0_READ_BURST_REJECTION_SELFTEST_PASS lengths=2..16 physical_test=false");
$display("E310_GP0_WRITE_BURST_SELFTEST_PASS lengths=1..16 arrival_orders=2 malformed_id_last_rejected=true physical_test=false");
$display("E310_GP0_MID_BURST_RESET_SELFTEST_PASS physical_test=false");
$finish;
end
always @(posedge clk) if (resetn) begin
if (arvalid && dut.gp0_arready) accepted_reads <= accepted_reads + 1;
if (rready && dut.gp0_rvalid) completed_reads <= completed_reads + 1;
if (dut.register_read) register_reads <= register_reads + 1;
if (dut.register_write) register_writes <= register_writes + 1;
if (awvalid && dut.gp0_awready) accepted_addresses <= accepted_addresses + 1;
if (wvalid && dut.gp0_wready) accepted_data <= accepted_data + 1;
if (bready && dut.gp0_bvalid) completed_writes <= completed_writes + 1;
end
initial begin
#100000;
$fatal(1, "GP0 test timeout");
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_gp0_regs.sv · 247 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_e310_gp0_regs #(parameter integer HOST_WAVEFORM=0, TX_BLOCK_RAM=0, COUNTERS_TO_SOFTWARE=0, RX_FIFO_BLOCK_RAM=0, PEAKS_TO_SOFTWARE=0);
reg bus_clk = 1'b0;
reg radio_clk = 1'b0;
always #5 bus_clk = ~bus_clk;
always #12.5 radio_clk = ~radio_clk;
reg bus_reset = 1'b1;
reg radio_reset = 1'b1;
reg bus_write = 1'b0;
reg [11:0] bus_write_address = 12'd0;
reg [31:0] bus_write_data = 32'd0;
reg [3:0] bus_write_strobe = 4'hf;
reg bus_read = 1'b0;
reg [11:0] bus_read_address = 12'd0;
wire [31:0] bus_read_data;
reg radio_path_ready = 1'b1;
wire radio_arm, radio_kill;
wire [47:0] radio_ap_mac;
wire radio_channel;
wire [1:0] radio_rx_use_txrx;
wire radio_tx_commit;
wire [11:0] radio_tx_length;
reg [11:0] radio_tx_read_address = 12'd0;
wire [7:0] radio_tx_read_data;
reg radio_tx_busy = 1'b0;
reg radio_tx_done = 1'b0;
reg radio_tx_error = 1'b0;
reg psdu_start = 1'b0;
reg psdu_byte_valid = 1'b0;
reg [7:0] psdu_byte = 8'd0;
reg psdu_byte_last = 1'b0;
gf_e310_gp0_regs #(.USE_HOST_WAVEFORM(HOST_WAVEFORM),.USE_TX_BLOCK_RAM(TX_BLOCK_RAM),
.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE),.RX_FIFO_BLOCK_RAM(RX_FIFO_BLOCK_RAM),
.PEAKS_TO_SOFTWARE(PEAKS_TO_SOFTWARE)) dut (
.bus_clk(bus_clk), .bus_reset(bus_reset),
.bus_write(bus_write), .bus_write_address(bus_write_address),
.bus_write_data(bus_write_data),
.bus_write_strobe(bus_write_strobe),
.bus_read(bus_read), .bus_read_address(bus_read_address),
.bus_read_data(bus_read_data),
.radio_clk(radio_clk), .radio_reset(radio_reset),
.radio_path_ready(radio_path_ready),
.radio_arm(radio_arm), .radio_kill(radio_kill),
.radio_ap_mac(radio_ap_mac),
.radio_logical_tx_channel(radio_channel),
.radio_rx_use_txrx(radio_rx_use_txrx),
.radio_tx_commit(radio_tx_commit),
.radio_tx_length(radio_tx_length),
.radio_tx_read_address(radio_tx_read_address),
.radio_tx_read_data(radio_tx_read_data),
.radio_tx_busy(radio_tx_busy),
.radio_tx_done(radio_tx_done),
.radio_tx_error(radio_tx_error),
.psdu_start(psdu_start),
.psdu_byte_valid(psdu_byte_valid),
.psdu_byte(psdu_byte), .psdu_byte_last(psdu_byte_last),
.response_pending(1'b0), .response_active(1'b0),
.tx_override_valid(1'b0), .mode_fault(1'b0),
.rx_psdu_count(32'd7), .response_count(32'd3),
.deadline_miss_count(32'd0), .rejected_count(32'd1),
.rx_sample_valid(1'b0), .rx_sample_iq(32'd0), .rx_capture_veto(1'b0),
.rx_sfd_count(32'd0), .rx_plcp_ok_count(32'd0), .rx_plcp_error_count(32'd0)
);
task automatic write_register;
input [11:0] address;
input [31:0] value;
begin
@(negedge bus_clk);
bus_write_address = address;
bus_write_data = value;
bus_write = 1'b1;
@(negedge bus_clk);
bus_write = 1'b0;
end
endtask
task automatic read_register;
input [11:0] address;
output [31:0] value;
begin
@(negedge bus_clk);
bus_read_address = address;
bus_read = 1'b1;
#1 value = bus_read_data;
@(negedge bus_clk);
bus_read = 1'b0;
end
endtask
task automatic push_psdu_byte;
input [7:0] value;
input first;
input last;
begin
@(negedge radio_clk);
psdu_byte = value;
psdu_start = first;
psdu_byte_last = last;
psdu_byte_valid = 1'b1;
@(negedge radio_clk);
psdu_byte_valid = 1'b0;
psdu_start = 1'b0;
psdu_byte_last = 1'b0;
end
endtask
reg [31:0] value;
integer wait_count;
initial begin
repeat (5) @(posedge bus_clk);
bus_reset = 1'b0;
repeat (3) @(posedge radio_clk);
radio_reset = 1'b0;
read_register(12'h200, value);
if (value != 32'h47464531) $fatal(1, "magic mismatch");
read_register(12'h22c, value);
if (value != 32'h00010003) $fatal(1, "version mismatch");
read_register(12'h284, value);
if(value !== (PEAKS_TO_SOFTWARE ? 32'h504b5357 : 32'd0)) $fatal(1,"Peak offload capability mismatch");
read_register(12'h27c, value);
if (value !== (HOST_WAVEFORM ? 32'h57463230 : 32'd0))
$fatal(1, "waveform capability mismatch");
read_register(12'h280, value);
if(value !== (COUNTERS_TO_SOFTWARE ? 32'h47523332 : 32'd0))
$fatal(1,"counter contract mismatch");
read_register(12'h21c, value);
if(value !== (COUNTERS_TO_SOFTWARE ? 32'd4 : 32'd7))
$fatal(1,"RX counter encoding mismatch");
read_register(12'h220, value);
if(value !== (COUNTERS_TO_SOFTWARE ? 32'd2 : 32'd3))
$fatal(1,"response counter encoding mismatch");
// Arm without the key must fail closed.
write_register(12'h204, 32'h1);
repeat (5) @(posedge radio_clk);
if (radio_arm || !radio_kill)
$fatal(1, "unkeyed arm escaped fail-close");
write_register(12'h204, 32'h2);
write_register(12'h208, 32'h46415031);
write_register(12'h20c, 32'h00000247);
write_register(12'h244, 32'h00000007);
write_register(12'h240, 32'h47324641);
write_register(12'h204, 32'h1);
repeat (6) @(posedge radio_clk);
if (!radio_arm || radio_kill)
$fatal(1, "keyed arm did not reach radio domain");
if (radio_ap_mac != 48'h024746415031)
$fatal(1, "AP MAC crossing mismatch");
if (!radio_channel || radio_rx_use_txrx != 2'b11)
$fatal(1, "RF configuration crossing mismatch");
write_register(12'h230, 32'h000000b4);
write_register(12'h230, 32'h00000100);
write_register(12'h230, 32'h00000200);
write_register(12'h230, 32'h00000300);
write_register(12'h234, 32'h00000004);
wait_count = 0;
while (!radio_tx_commit && wait_count < 20) begin
@(posedge radio_clk);
wait_count = wait_count + 1;
end
if (!radio_tx_commit) $fatal(1, "TX commit did not cross domains");
if (radio_tx_length != 4) $fatal(1, "TX length mismatch");
radio_tx_read_address = 0;
#1 if (radio_tx_read_data != 8'hb4) $fatal(1, "TX byte zero");
radio_tx_read_address = 3;
if(TX_BLOCK_RAM) @(posedge radio_clk);
#1 if (radio_tx_read_data != 8'h00) $fatal(1, "TX byte three");
radio_tx_busy = 1'b1;
repeat (3) @(posedge radio_clk);
@(negedge radio_clk);
radio_tx_busy = 1'b0;
radio_tx_done = 1'b1;
@(negedge radio_clk);
radio_tx_done = 1'b0;
repeat (20) @(posedge bus_clk);
read_register(12'h23c, value);
if (value != 1) $fatal(1, "TX done counter mismatch");
// A telemetry carry must never release packet RAM ownership. Seed a
// long-running count while idle, then cross 0xffff -> 0x10000 with an
// actual completion. Simulation is a logic check, not CDC metrology.
@(negedge radio_clk);
dut.tx_done_count_radio = 32'h0000ffff;
repeat (24) @(posedge bus_clk);
write_register(12'h230, 32'h000000ab);
write_register(12'h234, 32'h00000001);
repeat (12) @(posedge radio_clk);
read_register(12'h238, value);
if (!value[0]) $fatal(1, "counter change released in-flight frame");
write_register(12'h230, 32'h000000cd);
radio_tx_read_address = 0;
if(TX_BLOCK_RAM) @(posedge radio_clk);
#1 if (radio_tx_read_data != 8'hab) $fatal(1, "in-flight packet RAM was overwritten");
@(negedge radio_clk);
radio_tx_done = 1'b1;
@(negedge radio_clk);
radio_tx_done = 1'b0;
repeat (24) @(posedge bus_clk);
read_register(12'h23c, value);
if (value != (COUNTERS_TO_SOFTWARE ? 32'h18000 : 32'h10000)) $fatal(1, "Gray counter carry mismatch");
read_register(12'h238, value);
if (value[0]) $fatal(1, "completion did not release packet RAM");
write_register(12'h230, 32'h000000ef);
write_register(12'h234, 32'h00000001);
repeat (12) @(posedge radio_clk);
@(negedge radio_clk);
radio_tx_error = 1'b1;
@(negedge radio_clk);
radio_tx_error = 1'b0;
repeat (24) @(posedge bus_clk);
read_register(12'h238, value);
if (value[0]) $fatal(1, "TX error left packet RAM stuck in flight");
read_register(12'h24c, value);
if (value != 1) $fatal(1, "TX error counter mismatch");
push_psdu_byte(8'h12, 1'b1, 1'b0);
push_psdu_byte(8'h34, 1'b0, 1'b1);
repeat (8) @(posedge bus_clk);
read_register(12'h214, value);
if (value != 32'h80000112)
$fatal(1, "first RX FIFO word %08x", value);
read_register(12'h214, value);
if (value != 32'h80000234)
$fatal(1, "last RX FIFO word %08x", value);
radio_path_ready = 1'b0;
#1;
if (radio_arm || !radio_kill)
$fatal(1, "PLL loss did not kill radio arm");
$display("E310_GP0_REGS_SELFTEST_PASS");
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_pmu_regs.sv · 87 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_e310_pmu_regs;
reg clk = 0, reset = 1;
always #5 clk = ~clk;
reg ss = 1, mosi = 0, sck = 0, write_enable = 0;
reg [7:0] write_address = 0, read_address = 0;
reg [31:0] write_data = 0;
reg [3:0] write_strobe = 15;
wire miso, irq, write_error;
wire [31:0] read_data;
reg [63:0] received;
integer index;
gf_e310_pmu_regs dut (
.clk(clk), .reset(reset), .spi_ss(ss), .spi_mosi(mosi),
.spi_sck(sck), .spi_miso(miso), .write_enable(write_enable),
.write_address(write_address), .write_data(write_data),
.write_strobe(write_strobe), .write_error(write_error),
.read_address(read_address), .read_data(read_data), .irq(irq)
);
task transfer(input [63:0] value);
integer bit_index;
begin
ss = 1; sck = 0; #100;
ss = 0; #100; received = 0;
for (bit_index = 63; bit_index >= 0; bit_index = bit_index - 1) begin
mosi = value[bit_index]; #100;
received = {received[62:0], miso}; sck = 1; #100;
sck = 0; #100;
end
ss = 1; #100;
end
endtask
task expect_read(input [7:0] address, input [31:0] expected);
begin
read_address = address; #10;
if (read_data !== expected)
$fatal(1, "PMU read %h got=%h expected=%h", address, read_data, expected);
end
endtask
task write_register(input [7:0] address, input [31:0] value, input integer error_expected);
begin
@(negedge clk); write_address = address; write_data = value; write_enable = 1;
#1;
if (write_error !== error_expected[0]) $fatal(1, "PMU write error mismatch");
@(negedge clk); write_enable = 0;
end
endtask
initial begin
#40; @(negedge clk); reset = 0;
expect_read(4, 0);
transfer(64'h1234_0008_0000_2200);
if (received !== 0) $fatal(1, "PMU invented a startup command");
expect_read(4, 32'h22);
expect_read(8, 32'h00341200);
expect_read(12, 4);
transfer(64'h0112_3456_789a_bc01);
expect_read(16, 32'h7856bc9a);
expect_read(20, 32'h00013412);
transfer(64'h0000_0000_03ef_ab02);
expect_read(24, 32'h0000abef);
expect_read(28, 3);
write_register(4, 32'h100, 0); #10;
if (!irq || dut.command_count != 0) $fatal(1, "PMU IRQ mask became SPI command");
write_register(28, 3, 0);
transfer(64'h0000_0000_03ef_ab02);
if (received !== 0) $fatal(1, "PMU command pipeline changed");
transfer(64'h0000_0000_03ef_ab02);
if (received !== 64'h80000000_03001c01) $fatal(1, "PMU setting command format %h", received);
transfer(0);
if (received !== 0) $fatal(1, "PMU duplicated command");
write_strobe = 1;
write_register(0, 32'h7a, 1);
write_strobe = 15;
if (dut.command_count != 0 || dut.shutdown != 0) $fatal(1, "PMU rejected write had side effect");
for (index = 0; index < 32; index = index + 1)
write_register(28, index, 0);
write_register(0, 32'h7a, 1);
if (dut.command_count != 32 || dut.shutdown != 0) $fatal(1, "PMU queue overflow");
@(negedge clk); reset = 1; repeat (4) @(negedge clk); reset = 0;
transfer(0);
if (received !== 0 || irq || dut.command_count != 0) $fatal(1, "PMU reset did not clear pending commands");
$display("E310_PMU_SPI_SELFTEST_PASS status_from_serial=true command_format=true queue_full_rejected=true physical_test=false");
$finish;
end
initial begin #1000000; $fatal(1, "PMU test timeout"); end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_rx_capture.sv · 98 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_e310_rx_capture #(parameter integer COUNTERS_TO_SOFTWARE=0,
parameter integer PEAKS_TO_SOFTWARE=0);
reg bus_clk=0, radio_clk=0;
always #5 bus_clk=~bus_clk;
always #12.5 radio_clk=~radio_clk;
reg reset=1, wr=0, sample_valid=0, veto=0;
reg [11:0] addr=0, rdaddr=0;
reg [31:0] wdata=0, iq=0;
wire [31:0] rdata;
reg [31:0] sfd_input=7;
gf_e310_rx_capture #(.ADDRESS_BITS(4),.COUNTERS_TO_SOFTWARE(COUNTERS_TO_SOFTWARE),
.PEAKS_TO_SOFTWARE(PEAKS_TO_SOFTWARE)) dut (
.bus_clk(bus_clk), .bus_reset(reset), .bus_write(wr),
.bus_address(addr), .bus_read_address(rdaddr), .bus_write_data(wdata),
.bus_read_data(rdata), .radio_clk(radio_clk), .radio_reset(reset),
.sample_valid(sample_valid), .sample_iq(iq), .capture_veto(veto),
.sfd_count(sfd_input), .plcp_ok_count(32'd3), .plcp_error_count(32'd4)
);
task write_reg(input [11:0] address, input [31:0] value);
begin
@(negedge bus_clk); addr=address; wdata=value; wr=1;
@(negedge bus_clk); wr=0;
repeat(4) @(negedge bus_clk);
end
endtask
task sample(input [31:0] value);
begin
@(negedge radio_clk); iq=value; sample_valid=1;
@(negedge radio_clk); sample_valid=0;
end
endtask
integer n;
reg [31:0] expected;
initial begin
repeat(6) @(negedge radio_clk); reset=0;
write_reg(12'h254, 256);
write_reg(12'h250, 32'h52584341);
repeat(6) @(negedge radio_clk);
sample(32'd128);
if (!dut.armed || dut.capturing) $fatal(1,"threshold gate");
veto=1; sample(32'd1000);
if (dut.capturing) $fatal(1,"own TX veto");
veto=0;
sample(32'd1000);
if (dut.capturing) $fatal(1,"post TX guard");
repeat(810) @(negedge radio_clk);
for(n=0;n<16;n=n+1) sample(((65536-100-n)<<16)|(300+n));
repeat(20) @(negedge bus_clk);
rdaddr=12'h250; #1;
if(rdata!==32'h404) $fatal(1,"complete status %h",rdata);
for(n=0;n<16;n=n+1) begin
write_reg(12'h258,n); rdaddr=12'h25c; #1;
expected=((65536-100-n)<<16)|(300+n);
if(rdata!==expected) $fatal(1,"retained IQ index=%d got=%h expected=%h",n,rdata,expected);
end
sample(32'h12345678);
write_reg(12'h258,15); rdaddr=12'h25c; #1;
if(rdata!==32'hff8d013b) $fatal(1,"frozen record changed");
rdaddr=12'h270; #1;
if(rdata!==(PEAKS_TO_SOFTWARE ? 32'd0 : 32'h0073013b)) $fatal(1,"captured peaks %h",rdata);
repeat(20) @(negedge bus_clk); // settle both legacy and raw-Gray CDC latency
rdaddr=12'h260; #1;
if(rdata!==(COUNTERS_TO_SOFTWARE ? 32'd4 : 32'd7)) $fatal(1,"SFD counter CDC");
rdaddr=12'h264; #1;
if(rdata!==(COUNTERS_TO_SOFTWARE ? 32'd2 : 32'd3)) $fatal(1,"PLCP good counter CDC");
rdaddr=12'h268; #1;
if(rdata!==(COUNTERS_TO_SOFTWARE ? 32'd6 : 32'd4)) $fatal(1,"PLCP bad counter CDC");
rdaddr=12'h26c; #1;
if(rdata!==(COUNTERS_TO_SOFTWARE ? 32'd30 : 32'd20)) $fatal(1,"sample counter CDC");
write_reg(12'h254,0); write_reg(12'h250,32'h52584341);
repeat(6) @(negedge radio_clk);
rdaddr=12'h25c; #1;
if(rdata!==0) $fatal(1,"incomplete record exposed");
for(n=0;n<16;n=n+1) sample(n);
repeat(6) @(negedge bus_clk);
write_reg(12'h258,15); rdaddr=12'h25c; #1;
if(rdata!==15) $fatal(1,"rearm record");
write_reg(12'h278,1); write_reg(12'h250,32'h52584341);
repeat(6) @(negedge radio_clk);
for(n=0;n<20;n=n+1) sample(100+n);
if(dut.capturing || dut.done) $fatal(1,"SFD mode triggered on amplitude");
sfd_input=8;
for(n=0;n<12;n=n+1) sample(200+n);
repeat(10) @(negedge bus_clk);
rdaddr=12'h250; #1;
if(rdata!==(PEAKS_TO_SOFTWARE ? 32'h40c : 32'h404)) $fatal(1,"SFD completion %h",rdata);
for(n=0;n<16;n=n+1) begin
write_reg(12'h258,n); rdaddr=12'h25c; #1;
expected=n<4 ? 116+n : 200+n-4;
if(rdata!==expected) $fatal(1,"SFD pretrigger order index=%0d got=%h expected=%h",n,rdata,expected);
end
$display("E310_RX_CAPTURE_TB_PASS retained_order=true own_tx_veto=true frozen_until_rearm=true sfd_pretrigger=true software_peaks=%0d",PEAKS_TO_SOFTWARE);
$finish;
end
initial begin #200000; $fatal(1,"timeout"); end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_rx_replay.sv · 85 lines
Download this file · Permanent section link
// Replay retained hardware ADC samples through both receiver arithmetic paths.
// This is offline analysis of a physical recording, not a new RF reception.
`timescale 1ns/1ps
module tb_gf_e310_rx_replay;
reg clk=0;
always #12.5 clk=~clk;
reg resetn=0, sample_valid=0;
reg [31:0] iq=0;
integer expect_one_frame=0;
function automatic [31:0] crc32_byte(input [31:0] current, input [7:0] value);
reg [31:0] crc;
integer bit_number;
begin
crc=current ^ value;
for(bit_number=0;bit_number<8;bit_number=bit_number+1)
crc=(crc>>1) ^ (crc[0] ? 32'hedb88320 : 32'd0);
crc32_byte=crc;
end
endfunction
wire [31:0] sfd[0:1], plcp_ok[0:1], plcp_error[0:1], psdu_count[0:1];
genvar model;
generate for(model=0;model<2;model=model+1) begin: receivers
wire first, valid, last;
wire [7:0] byte_value;
wire [15:0] age;
wire active;
reg [31:0] frame_crc=32'hffffffff;
integer frame_bytes=0;
gf_dsss_1mbps_rx #(.PIPELINED_DIFFERENTIAL(model)) receiver (
.clk(clk), .resetn(resetn), .enable(1'b1),
.rx_sample_valid(sample_valid), .rx_i($signed(iq[15:0])),
.rx_q($signed(iq[31:16])), .psdu_start(first), .psdu_byte_valid(valid),
.psdu_byte(byte_value), .psdu_byte_last(last), .psdu_end_age_cycles(age),
.receiver_active(active), .sfd_count(sfd[model]),
.plcp_ok_count(plcp_ok[model]), .plcp_error_count(plcp_error[model]),
.psdu_count(psdu_count[model])
);
always @(negedge clk) if(valid) begin
if(first) begin frame_crc=32'hffffffff; frame_bytes=0; end
frame_crc=crc32_byte(frame_crc,byte_value);
frame_bytes=frame_bytes+1;
$display("REPLAY_BYTE model=%0d first=%0d last=%0d byte=%02x", model, first, last, byte_value);
if(last) begin
$display("REPLAY_FCS model=%0d bytes=%0d valid=%0d",model,frame_bytes,frame_crc==32'hdebb20e3);
if(expect_one_frame && frame_crc!=32'hdebb20e3)
$fatal(1,"Decoded physical recording failed FCS");
end
end
always @(negedge clk)
if(resetn && sample_valid && receiver.receive_state==1 && receiver.phase_index==receiver.locked_phase && receiver.plcp_bit_index==47)
$display("REPLAY_PLCP model=%0d signal=%02x service=%02x length=%0d received_crc=%04x expected_crc=%04x", model,
receiver.plcp_signal, receiver.plcp_service, receiver.plcp_length_us,
receiver.completed_plcp_crc, receiver.plcp_crc_state ^ 16'hffff);
end endgenerate
string filename;
integer file_handle, result, count=0;
reg [31:0] word_value;
initial begin
expect_one_frame=$test$plusargs("EXPECT_ONE_FRAME");
if(!$value$plusargs("IQ_FILE=%s",filename)) $fatal(1,"IQ_FILE required");
file_handle=$fopen(filename,"r");
if(!file_handle) $fatal(1,"Cannot open retained IQ hex");
repeat(6) @(negedge clk); resetn=1;
while(!$feof(file_handle)) begin
result=$fscanf(file_handle,"%h\n",word_value);
if(result==1) begin
@(negedge clk); iq=word_value; sample_valid=1;
@(negedge clk); sample_valid=0; count=count+1;
end else if(!$feof(file_handle)) $fatal(1,"Bad IQ hex");
end
$fclose(file_handle);
repeat(50) @(negedge clk);
$display("REPLAY_DONE samples=%0d physical_recording_only=true",count);
$display("REPLAY_MODEL direct sfd=%0d plcp_ok=%0d plcp_error=%0d psdu=%0d",sfd[0],plcp_ok[0],plcp_error[0],psdu_count[0]);
$display("REPLAY_MODEL pipelined sfd=%0d plcp_ok=%0d plcp_error=%0d psdu=%0d",sfd[1],plcp_ok[1],plcp_error[1],psdu_count[1]);
if(expect_one_frame) begin
if(sfd[0]!=1 || sfd[1]!=1 || plcp_ok[0]!=1 || plcp_ok[1]!=1 ||
plcp_error[0]!=0 || plcp_error[1]!=0 || psdu_count[0]!=1 || psdu_count[1]!=1)
$fatal(1,"Expected exactly one complete valid frame in both models");
$display("REPLAY_ONE_FRAME_REGRESSION_PASS physical_recording_analysis_only=true");
end
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_e310_serial_clock.sv · 49 lines
Download this file · Permanent section link
`timescale 1ns/1ps
// Uses AMD's installed UNISIM MMCM model, not a replacement clock generator.
// This is a digital model test of the reset supervisor, not physical RF proof.
module tb_gf_e310_serial_clock;
reg radio=0,control=0,run_radio=1,request=1;
wire fast,locked;
always #5 control=~control;
always #12.5 if(run_radio)radio=~radio;else radio=0;
gf_e310_serial_clock dut(radio,control,request,fast,locked);
integer fast_edges=0,begin_edges,cycles;
always @(posedge fast)if(locked)fast_edges=fast_edges+1;
task automatic await_lock;
begin
cycles=0;
while(!locked && cycles<20000)begin @(posedge control);cycles=cycles+1;end
if(!locked)$fatal(1,"MMCM failed to lock");
repeat(50)@(posedge radio);
begin_edges=fast_edges;
repeat(100)@(posedge radio);
#0.01;
if(fast_edges-begin_edges<799 || fast_edges-begin_edges>801)
$fatal(1,"40:320 ratio failed: %0d",fast_edges-begin_edges);
end
endtask
initial begin
#250;request=0;
await_lock();
// A stopped input must drop lock and provoke a reset from the existing
// 100-MHz control clock; no software or board reset is supplied.
@(negedge radio);run_radio=0;
cycles=0;
while(locked && cycles<20000)begin @(posedge control);cycles=cycles+1;end
if(locked)$fatal(1,"UNISIM did not report input-clock loss");
cycles=0;
while(!dut.mmcm_reset && cycles<20)begin @(posedge control);cycles=cycles+1;end
if(!dut.mmcm_reset)$fatal(1,"No autonomous reset after lock loss");
repeat(30)@(posedge control);
run_radio=1;
await_lock();
request=1;#0.01;
if(locked || !dut.mmcm_reset)$fatal(1,"Asynchronous reset veto failed");
repeat(8)@(posedge control);request=0;
await_lock();
$display("E310_SERIAL_CLOCK_PASS vendor_model=UNISIM ratio=8 startup=true stopped_input_recovery=true explicit_reset=true physical_rf=false");
$finish;
end
initial begin #800000;$fatal(1,"Clock test timeout");end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_header_index.sv · 99 lines
Download this file · Permanent section link
`timescale 1ns/1ps
// Differential gate for the bounded PSDU contract, not physical RF evidence.
module tb_gf_header_index;
reg clk=0;
always #5 clk=~clk;
reg resetn=0, start=0, valid=0, last=0, checked=0;
reg [7:0] data=0;
reg [15:0] age=0;
reg [47:0] ap_mac=48'h024746415031;
wire [199:0] observed [0:1];
integer checks=0, frames=0;
reg [31:0] rng=32'h167afd36;
function automatic [31:0] next_random(input [31:0] x);
reg [31:0] y;
begin y=x^(x<<13); y=y^(y>>17); next_random=y^(y<<5); end
endfunction
generate for(genvar version=0;version<2;version=version+1) begin : g_version
gf_low_mac_classifier #(.EXTERNAL_FCS(1),.SATURATING_HEADER_INDEX(version)) dut (
.clk(clk),.resetn(resetn),.ap_mac(ap_mac),.psdu_start(start),
.psdu_byte_valid(valid),.psdu_byte(data),.psdu_byte_last(last),
.psdu_end_age_cycles(age),.checked_fcs_ok(checked),
.decision_valid(observed[version][0]),
.decision_age_cycles(observed[version][16:1]),
.decision_fcs_ok(observed[version][17]),
.decision_ra_matches_ap(observed[version][18]),
.decision_response_required(observed[version][19]),
.decision_is_rts(observed[version][20]),
.decision_response_mac(observed[version][68:21]),
.decision_duration_us(observed[version][84:69]),
.decision_malformed(observed[version][85]),
.frame_count(observed[version][117:86]),
.fcs_ok_count(observed[version][149:118]),
.response_candidate_count(observed[version][181:150]),
.malformed_count()
);
assign observed[version][199:182]=0;
end endgenerate
task automatic compare;
begin
if(observed[0] !== observed[1] ||
g_version[0].dut.malformed_count !== g_version[1].dut.malformed_count)
$fatal(1,"HEADER_INDEX_MISMATCH frame=%0d checks=%0d old_idx=%0d new_idx=%0d old=%h new=%h",
frames,checks,g_version[0].dut.byte_index,g_version[1].dut.byte_index,observed[0],observed[1]);
checks=checks+1;
end
endtask
task automatic tick;
begin #1; compare(); @(posedge clk); #1; compare(); @(negedge clk); end
endtask
task automatic frame(input integer length, input [7:0] fc, input integer variant);
reg [47:0] receiver;
integer i;
begin
receiver=(variant%3==0) ? ap_mac : (variant%3==1 ? 48'hffffffffffff : 48'h024746415032);
for(i=0;i<length;i=i+1) begin
rng=next_random(rng); data=rng[7:0]; age=rng[31:16];
case(i)
0: data=fc;
2: data=8'h10;
3: data=variant[0] ? 8'hc0 : 8'h00;
4: data=receiver[47:40]; 5: data=receiver[39:32];
6: data=receiver[31:24]; 7: data=receiver[23:16];
8: data=receiver[15:8]; 9: data=receiver[7:0];
default: begin end
endcase
start=(i==0); valid=1; last=(i==length-1); checked=variant[1]; tick();
// Gaps exercise the state hold; back-to-back bytes exercise
// the strongest timing contract (real DSSS bytes are farther apart).
if((rng & 63)==0) begin start=0; valid=0; last=0; tick(); end
end
frames=frames+1; start=0; valid=0; last=0; tick();
end
endtask
initial begin
@(negedge clk); tick(); resetn=1;
// All FC type/subtype/version values around every header threshold.
for(integer fc=0;fc<256;fc=fc+1)
for(integer length=1;length<=35;length=length+1)
frame(length,fc[7:0],fc+length);
// Every supported PSDU length, with legal/malformed management,
// data, RTS, PS-Poll, ACK and reserved frame controls in rotation.
for(integer length=1;length<=4095;length=length+1) begin
case(length%8)
0: frame(length,8'h00,length); 1: frame(length,8'h08,length);
2: frame(length,8'hb4,length); 3: frame(length,8'ha4,length);
4: frame(length,8'hd4,length); 5: frame(length,8'h88,length);
6: frame(length,8'hff,length); 7: frame(length,8'h40,length);
endcase
end
// Interrupted frame / new start and explicit reset must forget a
// saturated index. Last can coincide with the first byte.
valid=1; start=1; last=0; data=8'h08; tick(); start=0;
repeat(100) tick(); frame(20,8'ha4,3);
resetn=0; valid=0; tick(); resetn=1; frame(1,8'h08,2); frame(4095,8'h08,3);
$display("HEADER_INDEX_PASS checks=%0d frames=%0d all_lengths=1..4095 all_fc=256 exact_ps_poll=true physical_rf=false",checks,frames);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_host_waveform_tx.sv · 86 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_host_waveform_tx #(parameter integer MEMORY_READ_LATENCY=0);
reg clk=0; always #12.5 clk=~clk;
reg resetn=0, arm=0, kill=1, frame_commit=0;
reg [11:0] frame_length_bytes=0;
wire [11:0] frame_read_address;
reg [7:0] memory[0:4095];
reg [7:0] synchronous_read_data;
always @(posedge clk) synchronous_read_data<=memory[frame_read_address];
wire [7:0] frame_read_data=MEMORY_READ_LATENCY ? synchronous_read_data : memory[frame_read_address];
reg tx_channel_available=1, tx_sample_tick=0, tx_sink_ready=1;
always @(negedge clk) tx_sample_tick<=!tx_sample_tick;
wire ready,busy,tx_rf_claim,tx_valid,frame_done,frame_error;
wire [31:0] tx_iq;
gf_host_waveform_tx #(.MEMORY_READ_LATENCY(MEMORY_READ_LATENCY)) dut(.*);
reg [31:0] expected[0:653279];
integer length, count=0, errors=0, dones=0, limit, j;
reg check_iq=0;
string base, path;
always @(posedge clk) begin
if(frame_done) dones=dones+1;
if(frame_error) errors=errors+1;
if(tx_valid && tx_sample_tick && tx_sink_ready) begin
if(check_iq && (count>=limit || tx_iq!==expected[count]))
$fatal(1,"IQ mismatch sample=%0d got=%h want=%h",count,tx_iq,expected[count]);
count=count+1;
end
end
task commit;
begin
@(negedge clk); frame_commit=1;
@(negedge clk); frame_commit=0;
end
endtask
task settle;
begin repeat(5) @(negedge clk); end
endtask
initial begin
if(!$value$plusargs("BASE=%s",base) || !$value$plusargs("LENGTH=%d",length)) $fatal(1,"args");
path={base,".hex"}; $readmemh(path,memory,0,length+35);
path={base,"-iq.hex"}; limit=(length+24)*160;
$readmemh(path,expected,0,limit-1);
settle(); resetn=1; arm=1; kill=0; frame_length_bytes=length+36;
check_iq=1; commit(); wait(frame_done); settle();
if(count!=limit || errors || dones!=1) $fatal(1,"completion %0d %0d %0d",count,errors,dones);
// A second exact frame catches stale increment/header state.
count=0; commit(); wait(frame_done); settle();
if(count!=limit || errors || dones!=2) $fatal(1,"repeat completion");
check_iq=0; count=0;
tx_channel_available=0; commit(); repeat(800) @(negedge clk);
if(tx_rf_claim || tx_valid || count) $fatal(1,"transmitted while SIFS path claimed channel");
tx_channel_available=1; wait(tx_valid); repeat(10) @(negedge clk);
kill=1; #1; if(tx_valid || tx_rf_claim) $fatal(1,"asynchronous kill gating");
settle(); if(busy || errors!=1) $fatal(1,"kill completion"); kill=0;
commit(); wait(tx_valid); @(negedge clk); tx_sink_ready=0;
settle(); if(busy || tx_rf_claim || errors!=2) $fatal(1,"sink failure gating");
tx_sink_ready=1;
memory[3]=19; commit(); wait(frame_error); settle();
if(busy || tx_rf_claim || errors!=3) $fatal(1,"malformed format accepted");
memory[3]=20; memory[2]=8'hff; commit(); wait(frame_error); settle();
if(busy || errors!=4) $fatal(1,"reserved pattern bits accepted");
frame_length_bytes=12; commit(); settle();
if(busy || errors!=5) $fatal(1,"empty phases accepted");
$display("HOST_WAVEFORM_RTL_PASS psdu=%0d samples=%0d read_latency=%0d repeat=true kill=true sink=true malformed=true",length,limit,MEMORY_READ_LATENCY);
$finish;
end
initial begin #100000000; $fatal(1,"waveform timeout"); end
endmodule
module tb_gf_serial_increment12;
reg clk=0; always #1 clk=~clk;
reg resetn=0; reg [11:0] value=0;
wire [11:0] source,result; wire valid;
gf_serial_increment12 dut(.*);
integer n;
initial begin
repeat(3) @(negedge clk); resetn=1;
for(n=0;n<4096;n=n+1) begin
value=n;
repeat(25) @(negedge clk);
if(!valid || source!==value || result!==12'(n+1)) $fatal(1,"serial increment mismatch %0d",n);
end
$display("SERIAL_INCREMENT12_PASS inputs=4096 continuous=true"); $finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_rx_crc_residue.sv · 46 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_rx_crc_residue;
reg clk=0,clear=1,valid=0,data=0;
always #5 clk=~clk;
wire [31:0] fcs;
wire good;
reg [31:0] reference_crc,rng=32'h01234567,transmit_fcs;
integer frame,n,k,checks=0;
gf_control_crc_bitserial crc(.clk(clk),.clear(clear),.bit_valid(valid),.data_bit(data),.fcs(fcs));
gf_rx_crc_residue residue(.clk(clk),.clear(clear),.fcs(fcs),.good(good));
task send_bit(input bit value);
begin
valid=1;data=value;
reference_crc=(reference_crc>>1)^((reference_crc[0]^value)?32'hedb88320:0);
@(negedge clk);valid=0;
repeat(39)@(negedge clk);
if(fcs!==~reference_crc || good!==(reference_crc==32'hdebb20e3))
$fatal(1,"RX CRC/residue mismatch frame=%0d bit=%0d",frame,n);
checks=checks+1;
end
endtask
initial begin
repeat(3)@(negedge clk);
for(frame=0;frame<66;frame=frame+1)begin
clear=1;valid=0;@(negedge clk);clear=0;reference_crc=32'hffffffff;
// Include long-frame state and all 32 independently flipped FCS bits.
for(n=0;n<(frame==65?32728:8*(1+frame*7));n=n+1)begin
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
send_bit(rng[0]);
end
transmit_fcs=~reference_crc;
if(frame<32)transmit_fcs=transmit_fcs^(32'h1<<frame);
for(n=0;n<32;n=n+1)send_bit(transmit_fcs[n]);
if(good!==(frame>=32))$fatal(1,"final residue acceptance mismatch");
// Cancel at each in-flight stage; a later packet must start fresh.
valid=1;data=1;@(negedge clk);valid=0;
repeat(frame%10)@(negedge clk);
clear=1;@(negedge clk);clear=0;
repeat(12)@(negedge clk);
if(fcs!==0 || good!==0)$fatal(1,"stale CRC/residue escaped clear");
end
$display("RX_CRC_RESIDUE_PASS checks=%0d frames=66 corrupt_fcs_bits=32 maximum_psdu=4095 clear_flush=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_rx_single_phase.sv · 206 lines
Download this file · Permanent section link
// Offline replay of retained physical ADC samples, not new RF evidence.
`timescale 1ns/1fs
module tb_gf_rx_single_phase #(parameter SERIAL_COMPARE=0, parameter SCORE_RAM_COMPARE=0, parameter RECURSIVE_COMPARE=0, parameter SERIAL_BARKER_COMPARE=0, parameter BRIDGE_COMPARE=0);
localparam integer BARKER_AGE=BRIDGE_COMPARE?7:6;
reg clk=0,resetn=0,valid=0;
always #12.5 clk=~clk;
reg [31:0] iq=0;
wire external_valid;
wire bridge_resetn;
wire signed [23:0] external_i,external_q;
reg [5:0] delayed_resetn=0;
always @(posedge clk)begin
if(!resetn)delayed_resetn<=0;else delayed_resetn<={delayed_resetn[4:0],1'b1};
end
generate if(SERIAL_BARKER_COMPARE)begin:g_serial_barker_source
reg fast_clk=0,fast_reset=1;
always #1.5625 fast_clk=~fast_clk;
if(BRIDGE_COMPARE)begin:g_actual_bridge
wire fault;
gf_barker_radio_bridge bridge(clk,fast_clk,resetn,1'b1,1'b1,valid,
$signed(iq[15:0]),$signed(iq[31:16]),bridge_resetn,external_valid,fault,external_i,external_q);
always @(posedge clk)if(resetn && fault)$fatal(1,"implemented bridge replay cadence fault");
end else begin:g_ideal_bridge
wire slot,fault,correlation_valid;
wire signed [31:0] ci,cq;
gf_barker_serial_iq graph(fast_clk,fast_reset,slot&valid,$signed(iq[15:0]),$signed(iq[31:16]),
slot,fault,correlation_valid,ci,cq);
initial begin
wait(resetn && valid);@(posedge clk);#0.1;
@(negedge fast_clk);fast_reset=0;
end
// TESTBENCH delivery into the 40-MHz same-clock receiver boundary.
// This deliberately does not claim an implemented hardware CDC. The
// implemented fast graph consumes the actual parallel sample stream.
reg toggle=0,seen=0;
reg signed [23:0] held_i=0,held_q=0;
always @(posedge fast_clk)if(!fast_reset)begin
if(fault)$fatal(1,"serial Barker replay sample cadence fault");
if(correlation_valid)begin
if(ci[31:24]!=={8{ci[23]}} || cq[31:24]!=={8{cq[23]}})
$fatal(1,"serial correlation not representable at unchanged 24-bit receiver boundary");
held_i<=ci[23:0];held_q<=cq[23:0];toggle<=~toggle;
end
end
always @(posedge clk)seen<=toggle;
assign external_valid=toggle!=seen;
assign external_i=held_i;assign external_q=held_q;
assign bridge_resetn=delayed_resetn[5];
end
end else begin:g_no_external
assign external_valid=0;assign external_i=0;assign external_q=0;
assign bridge_resetn=resetn;
end endgenerate
integer bytes_seen[0:1],frames[0:1];
reg [9:0] decoded[0:1][0:8191];
time end_time[0:1];
integer end_age[0:1];
reg [31:0] crc[0:1];
function automatic [31:0] crc_byte(input [31:0] old,input [7:0] b);
reg [31:0] c;integer n;
begin c=old^b;for(n=0;n<8;n=n+1)c=(c>>1)^(c[0]?32'hedb88320:0);crc_byte=c;end
endfunction
genvar m;
generate for(m=0;m<2;m=m+1) begin: paths
wire first,byte_valid,last,active;
wire [7:0] b;
wire [15:0] age;
wire [31:0] sfd,plcp_ok,plcp_error,psdu;
// Keep the serial detector's word epoch shifted with its input path.
// Otherwise its fixed word boundaries quantize the added latency and
// frame-end feedback can run against a different sample position.
wire rx_resetn=(SERIAL_BARKER_COMPARE && m==1)?bridge_resetn:resetn;
reg [31:0] prior_plcp_ok=0;
gf_dsss_1mbps_rx #(.PIPELINED_DIFFERENTIAL(1),.SINGLE_PHASE_RX((SERIAL_COMPARE || SCORE_RAM_COMPARE || RECURSIVE_COMPARE || SERIAL_BARKER_COMPARE) ? 1 : m),
.SERIAL_DIFFERENTIAL(SERIAL_BARKER_COMPARE || RECURSIVE_COMPARE || SCORE_RAM_COMPARE || (SERIAL_COMPARE && m==1)),
.TIMING_SCORE_RAM(SERIAL_BARKER_COMPARE || RECURSIVE_COMPARE || (SCORE_RAM_COMPARE && m==1)),
.RECURSIVE_CORRELATOR(RECURSIVE_COMPARE && m==1),
.EXTERNAL_CORRELATOR(SERIAL_BARKER_COMPARE && m==1),.EXTERNAL_CORRELATOR_LATENCY(BARKER_AGE)) rx (
.clk(clk),.resetn(rx_resetn),.enable(1'b1),.rx_sample_valid(valid),
.rx_i($signed(iq[15:0])),.rx_q($signed(iq[31:16])),
.psdu_start(first),.psdu_byte_valid(byte_valid),.psdu_byte(b),.psdu_byte_last(last),
.psdu_end_age_cycles(age),.receiver_active(active),
.sfd_count(sfd),.plcp_ok_count(plcp_ok),.plcp_error_count(plcp_error),.psdu_count(psdu),
.external_correlation_valid(external_valid),.external_correlation_i(external_i),.external_correlation_q(external_q)
);
initial begin bytes_seen[m]=0;frames[m]=0;crc[m]=32'hffffffff;end
// Optional diagnostic only. It does not weaken the complete-frame gate.
always @(negedge clk) begin
if($test$plusargs("PACKET_TRACE") && plcp_ok!=prior_plcp_ok)
$display("RX_PLCP_TRACE path=%0d sample=%0d signal=%h length_us=%0d expected_bytes=%0d plcp_ok=%0d plcp_error=%0d",
m,samples,rx.plcp_signal,rx.plcp_length_us,rx.plcp_length_us>>3,plcp_ok,plcp_error);
prior_plcp_ok=plcp_ok;
end
always @(negedge clk) if(byte_valid) begin
if($test$plusargs("PACKET_TRACE"))
$display("RX_BYTE_TRACE path=%0d sample=%0d index=%0d byte=%h first=%0d last=%0d",m,samples,bytes_seen[m],b,first,last);
if(bytes_seen[m]>=8192)$fatal(1,"excess replay bytes");
decoded[m][bytes_seen[m]]={first,last,b};bytes_seen[m]=bytes_seen[m]+1;
if(first)crc[m]=32'hffffffff;
crc[m]=crc_byte(crc[m],b);
if(last) begin
frames[m]=frames[m]+1;end_time[m]=$time;end_age[m]=age;
$display("RX_PHASE_FRAME single=%0d bytes=%0d fcs=%0d phase=%0d end_ns=%0d age=%0d",m,bytes_seen[m],crc[m]==32'hdebb20e3,rx.locked_phase,$time,age);
if(crc[m]!=32'hdebb20e3)$fatal(1,"physical-recording frame FCS failed");
end
end
end endgenerate
generate if(SERIAL_BARKER_COMPARE) begin:g_serial_barker_alignment
reg [119:0] baseline_operands[0:BARKER_AGE-1];
reg baseline_request[0:BARKER_AGE-1];
wire request=paths[1].rx.selected_sample && paths[1].rx.phase_has_previous[0] && !paths[1].rx.serial_overflow;
integer p;
always @(posedge clk)begin
baseline_operands[0]<={paths[0].rx.decision_correlation_i,paths[0].rx.decision_correlation_q,
paths[0].rx.g_serial_differential.ai_pipe,paths[0].rx.g_serial_differential.bi_pipe,
paths[0].rx.g_serial_differential.aq_pipe,paths[0].rx.g_serial_differential.bq_pipe};
baseline_request[0]<=paths[0].rx.selected_sample && paths[0].rx.phase_has_previous[0] && !paths[0].rx.serial_overflow;
for(p=1;p<BARKER_AGE;p=p+1)begin baseline_operands[p]<=baseline_operands[p-1];baseline_request[p]<=baseline_request[p-1];end
if(resetn && paths[1].rx.process_sample_valid && paths[1].rx.window_fill>=20)begin
if({paths[1].rx.decision_correlation_i,paths[1].rx.decision_correlation_q}!==baseline_operands[BARKER_AGE-1][119:72])
$fatal(1,"serial Barker correlation age mismatch sample=%0d",samples);
if(request!==baseline_request[BARKER_AGE-1])$fatal(1,"serial Barker detector request mismatch sample=%0d",samples);
// The first incomplete differential operand is not consumed:
// phase_has_previous is false. Compare ALL actual requests,
// including both current and previous I/Q signed operands.
if(request && {paths[1].rx.g_serial_differential.ai_pipe,paths[1].rx.g_serial_differential.bi_pipe,
paths[1].rx.g_serial_differential.aq_pipe,paths[1].rx.g_serial_differential.bq_pipe}!==baseline_operands[BARKER_AGE-1][71:0])
$fatal(1,"serial Barker consumed operand mismatch sample=%0d",samples);
end
end
end endgenerate
string file_name;
integer fd,result,n,samples=0,leading=0;
reg [31:0] word_value;
time delta;
reg [4:0] prior_candidate=0;
reg prior_hold=0;
generate if(RECURSIVE_COMPARE) begin : g_recursive_alignment
reg [83:0] baseline_operands[0:11];
integer pipe_index;
always @(posedge clk) begin
baseline_operands[0]<={paths[0].rx.decision_correlation_i,
paths[0].rx.decision_correlation_q,paths[0].rx.g_serial_differential.ai_pipe,
paths[0].rx.g_serial_differential.aq_pipe};
for(pipe_index=1;pipe_index<12;pipe_index=pipe_index+1)
baseline_operands[pipe_index]<=baseline_operands[pipe_index-1];
if(resetn && paths[1].rx.process_sample_valid && paths[1].rx.window_fill>=20 &&
{paths[1].rx.decision_correlation_i,paths[1].rx.decision_correlation_q,
paths[1].rx.g_serial_differential.ai_pipe,paths[1].rx.g_serial_differential.aq_pipe}
!== baseline_operands[11])
$fatal(1,"recursive correlation/operand alignment mismatch sample=%0d",samples);
end
end endgenerate
always @(negedge clk) if (resetn && SCORE_RAM_COMPARE) begin
if (paths[0].rx.current_timing_score !== paths[1].rx.current_timing_score ||
paths[0].rx.timing_best_score !== paths[1].rx.timing_best_score ||
paths[0].rx.timing_best_phase !== paths[1].rx.timing_best_phase ||
paths[0].rx.candidate_phase !== paths[1].rx.candidate_phase)
$fatal(1,"timing score RAM cycle-equivalence mismatch sample=%0d",samples);
end
always @(negedge clk) if($test$plusargs("PHASE_TRACE") && valid) begin
if(samples>=1000 && samples<=4400 &&
(paths[1].rx.candidate_phase!=prior_candidate || paths[1].rx.candidate_hold!=prior_hold))
$display("PHASE_TRACE sample=%0d candidate=%0d best=%0d hold=%0d ones=%0d budget=%0d",samples,
paths[1].rx.candidate_phase,paths[1].rx.timing_best_phase,paths[1].rx.candidate_hold,
paths[1].rx.lane_one_run[0],paths[1].rx.lane_sfd_budget[0]);
prior_candidate=paths[1].rx.candidate_phase;prior_hold=paths[1].rx.candidate_hold;
end
initial begin
if(!$value$plusargs("IQ_FILE=%s",file_name))$fatal(1,"IQ_FILE required");
result=$value$plusargs("LEADING_SAMPLES=%d",leading);
fd=$fopen(file_name,"r");if(!fd)$fatal(1,"cannot open recording");
repeat(6)@(negedge clk);resetn=1;
repeat(leading) begin
@(negedge clk);iq=0;valid=1;
@(negedge clk);valid=0;samples=samples+1;
end
while(!$feof(fd))begin
result=$fscanf(fd,"%h\n",word_value);
if(result==1)begin
@(negedge clk);iq=word_value;valid=1;
@(negedge clk);valid=0;samples=samples+1;
end else if(!$feof(fd))$fatal(1,"malformed retained hex");
end
$fclose(fd);
if(SERIAL_BARKER_COMPARE)begin
repeat(40)begin @(negedge clk);iq=0;valid=1;@(negedge clk);valid=0;end
end else repeat(80)@(negedge clk);
$display("RX_PHASE_REPLAY samples=%0d baseline_frames=%0d single_frames=%0d baseline_bytes=%0d single_bytes=%0d",samples,frames[0],frames[1],bytes_seen[0],bytes_seen[1]);
if(frames[0]<1 || frames[1]!=frames[0] || bytes_seen[0]!=bytes_seen[1])$fatal(1,"phase acquisition/frame count mismatch");
for(n=0;n<bytes_seen[0];n=n+1)if(decoded[0][n]!==decoded[1][n])$fatal(1,"PSDU mismatch index=%0d",n);
if(SERIAL_COMPARE || RECURSIVE_COMPARE || SERIAL_BARKER_COMPARE) begin
// Compare reconstructed decision boundaries from measured RTL
// clock age; arithmetic latency itself is deliberately nonzero.
delta=(end_time[0]-25*end_age[0])>(end_time[1]-25*end_age[1]) ?
(end_time[0]-25*end_age[0])-(end_time[1]-25*end_age[1]) :
(end_time[1]-25*end_age[1])-(end_time[0]-25*end_age[0]);
end else delta=end_time[0]>end_time[1]?end_time[0]-end_time[1]:end_time[1]-end_time[0];
// A deliberately tight A/B gate, not a claimed RF timing measurement.
if(delta>100)$fatal(1,"phase-selection boundary moved more than 100 ns");
if((SERIAL_COMPARE || SCORE_RAM_COMPARE || RECURSIVE_COMPARE || SERIAL_BARKER_COMPARE) && delta!=0)$fatal(1,"end-age/boundary mismatch");
$display("RX_SINGLE_PHASE_REPLAY_PASS identical_psdu=true boundary_delta_ns=%0d new_rf=false",delta);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_serial_differential.sv · 71 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_serial_differential;
reg clk=0,resetn=0,enable=1,flush=0,request=0;
always #12.5 clk=~clk;
reg signed [17:0] ai=0,bi=0,aq=0,bq=0;
reg [4:0] phase=0;
reg [7:0] age=3;
wire ready,valid,sign_bit,overflow;
wire [4:0] result_phase;
wire [15:0] result_age;
gf_serial_differential dut(.clk(clk),.resetn(resetn),.enable(enable),.flush(flush),
.request_valid(request),.ai(ai),.bi(bi),.aq(aq),.bq(bq),
.request_phase(phase),.request_age(age),.request_ready(ready),
.result_valid(valid),.result_sign(sign_bit),.result_phase(result_phase),
.result_age(result_age),.overflow(overflow));
integer clock_count=0,wr=0,rd=0,offset,k,cycle,checks=0;
reg expected_sign[0:4095];
reg [4:0] expected_phase[0:4095];
integer request_tick[0:4095];
reg signed [39:0] ae,be,qe,re,expected;
always @(posedge clk) begin
clock_count=clock_count+1;
if(resetn && enable) begin
if(flush) rd=wr;
else if(request) begin
if(!ready)$fatal(1,"unexpected backpressure");
ae=ai;be=bi;qe=aq;re=bq;
expected=ae*be+qe*re;
expected_sign[wr]=expected[39];
expected_phase[wr]=phase;
request_tick[wr]=clock_count;
wr=wr+1;
end
end
#1;
if(overflow)$fatal(1,"serial adapter overflow");
if(valid) begin
if(rd>=wr)$fatal(1,"unsolicited/stale result");
if(sign_bit!==expected_sign[rd] || result_phase!==expected_phase[rd])
$fatal(1,"tagged differential mismatch request=%0d sign=%b want=%b phase=%0d want=%0d",rd,sign_bit,expected_sign[rd],result_phase,expected_phase[rd]);
if(result_age !== 3+clock_count-request_tick[rd])
$fatal(1,"age mismatch got=%0d want=%0d",result_age,3+clock_count-request_tick[rd]);
if(result_age>100)$fatal(1,"unexpectedly old decision");
rd=rd+1;checks=checks+1;
end
end
initial begin
repeat(5)@(negedge clk);resetn=1;
for(offset=0;offset<40;offset=offset+1) begin
flush=1;@(negedge clk);flush=0;
repeat(offset)@(negedge clk);
for(k=0;k<24;k=k+1) begin
ai=$random;bi=$random;aq=$random;bq=$random;phase=offset%20;
request=1;@(negedge clk);request=0;
repeat(39)@(negedge clk);
end
repeat(120)@(negedge clk);
if(rd!=wr)$fatal(1,"missing results");
end
// Flush with requests in flight, then immediately use another phase.
request=1;ai=-131072;bi=131071;aq=0;bq=0;phase=2;
@(negedge clk);request=0;repeat(10)@(negedge clk);
flush=1;@(negedge clk);flush=0;
request=1;ai=131071;bi=131071;phase=7;
@(negedge clk);request=0;repeat(120)@(negedge clk);
if(rd!=wr)$fatal(1,"flush lost the new request");
$display("SERIAL_DIFFERENTIAL_ADAPTER_PASS checks=%0d offsets=40 exact_age=true stale_phase_discard=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_serial_dot18.sv · 50 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_serial_dot18 #(parameter integer COMPACT_FANOUT=0);
reg clk=0,reset=1,ai=0,bi=0,aq=0,bq=0;
always #5 clk=~clk;
wire sum_bit,word_end;
gf_serial_dot18 #(.COMPACT_FANOUT(COMPACT_FANOUT)) dut(.clk(clk),.reset(reset),.ai(ai),.bi(bi),.aq(aq),.bq(bq),
.sum_bit(sum_bit),.sum_word_end(word_end));
localparam WORDS=1024;
reg signed [17:0] aiv[0:WORDS-1],biv[0:WORDS-1],aqv[0:WORDS-1],bqv[0:WORDS-1];
reg signed [39:0] expected[0:WORDS-1];
reg signed [39:0] aext,bext,qext,rext,actual;
integer i,cycle,outbit,outword,checks=0;
initial begin
for(i=0;i<WORDS;i=i+1) begin
aiv[i]=$random; biv[i]=$random; aqv[i]=$random; bqv[i]=$random;
case(i)
0: begin aiv[i]=0; biv[i]=0; aqv[i]=0; bqv[i]=0; end
1: begin aiv[i]=-131072; biv[i]=-131072; aqv[i]=-131072; bqv[i]=-131072; end
2: begin aiv[i]=-131072; biv[i]=131071; aqv[i]=-131072; bqv[i]=131071; end
3: begin aiv[i]=131071; biv[i]=131071; aqv[i]=-131071; bqv[i]=131071; end
endcase
aext=aiv[i]; bext=biv[i]; qext=aqv[i]; rext=bqv[i];
expected[i]=aext*bext+qext*rext;
end
repeat(3) @(negedge clk);
reset=0; actual=0;
for(cycle=0;cycle<WORDS*40+14;cycle=cycle+1) begin
if(cycle<WORDS*40) begin
i=cycle/40;
aext=aiv[i]; bext=biv[i]; qext=aqv[i]; rext=bqv[i];
ai=aext[cycle%40]; bi=bext[cycle%40]; aq=qext[cycle%40]; bq=rext[cycle%40];
end else begin ai=0; bi=0; aq=0; bq=0; end
@(posedge clk); #1;
if(cycle>=14) begin
outbit=(cycle-14)%40; outword=(cycle-14)/40;
actual[outbit]=sum_bit;
if(word_end !== (outbit==39)) $fatal(1,"dot word alignment cycle=%0d",cycle);
if(outbit==39) begin
if(actual!==expected[outword]) $fatal(1,"dot word=%0d got=%h want=%h",outword,actual,expected[outword]);
checks=checks+1;
end
end
@(negedge clk);
end
if(checks!=WORDS) $fatal(1,"missing dot products");
$display("SERIAL_DOT18_PASS words=%0d full_signed_sum=true input_ii=40 latency=14 physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_serial_mul18.sv · 63 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_serial_mul18 #(parameter integer WORDS=1024);
reg clk=0,reset=1,a_bit=0,b_bit=0;
always #5 clk=~clk;
wire actual_bit,actual_end,reference_bit,reference_end;
wire padded_bit,padded_end;
gf_serial_mul18_to40 candidate(.clk(clk),.reset(reset),.a_bit(a_bit),.b_bit(b_bit),
.product_bit(actual_bit),.product_word_end(actual_end));
gf_serial_mul40 reference(.clk(clk),.reset(reset),.a_bit(a_bit),.b_bit(b_bit),
.product_bit(reference_bit),.product_word_end(reference_end));
gf_serial_mul18_to40 #(.PRUNE_ZERO_ROWS(0)) padded(.clk(clk),.reset(reset),.a_bit(a_bit),.b_bit(b_bit),
.product_bit(padded_bit),.product_word_end(padded_end));
reg signed [39:0] expected[0:WORDS-1];
reg signed [17:0] a18,b18;
reg signed [39:0] a,b,actual;
reg [31:0] rng=32'h91e18267;
integer cycle,word_index,bit_index,checks=0;
initial begin
repeat(3)@(negedge clk); reset=0; actual=0;
for(cycle=0;cycle<WORDS*40+13;cycle=cycle+1)begin
if(cycle<WORDS*40)begin
word_index=cycle/40;
if(cycle%40==0)begin
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
a18=word_index-16-131072; b18=rng[17:0];
a=a18; b=b18;
// Consecutive extreme sign changes and all sign bit patterns.
if(word_index<16)begin
case(word_index%4)
0: begin a=-131072;b=-131072;end
1: begin a=131071;b=131071;end
2: begin a=-1;b=1;end
3: begin a=0;b=-1;end
endcase
end
// Also cover B outside signed-18: the identity is exact
// for arbitrary B modulo 2^40, not merely the lab operands.
if(word_index>=262160) b={rng[7:0],rng};
expected[word_index]=a*b;
end
a_bit=a[cycle%40];b_bit=b[cycle%40];
end else begin a_bit=0;b_bit=0;end
@(posedge clk);#1;
if(cycle>=13)begin
word_index=(cycle-13)/40;bit_index=(cycle-13)%40;
if(actual_bit!==reference_bit || actual_end!==reference_end ||
actual_bit!==padded_bit || actual_end!==padded_end)
$fatal(1,"SIGNED_ROW_BIT_MISMATCH cycle=%0d word=%0d bit=%0d got=%b reference=%b",cycle,word_index,bit_index,actual_bit,reference_bit);
if(actual_end!==(bit_index==39))$fatal(1,"word marker moved");
actual[bit_index]=actual_bit;
if(bit_index==39)begin
if(actual!==expected[word_index])$fatal(1,"SIGNED_ROW_PRODUCT_MISMATCH word=%0d actual=%h expected=%h",word_index,actual,expected[word_index]);
checks=checks+1;
end
end
@(negedge clk);
end
if(checks!=WORDS)$fatal(1,"missing products");
$display("SIGNED_ROW_MULTIPLIER_PASS words=%0d full_result_bits=40 latency=13 input_ii=40 physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_serial_mul40.sv · 53 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_serial_mul40;
reg clk=0, reset=1,a_bit=0,b_bit=0;
always #5 clk=~clk;
wire product_bit,word_end;
gf_serial_mul40 dut(.clk(clk),.reset(reset),.a_bit(a_bit),.b_bit(b_bit),
.product_bit(product_bit),.product_word_end(word_end));
localparam WORDS=2048;
reg [39:0] av[0:WORDS-1],bv[0:WORDS-1],expected[0:WORDS-1];
reg signed [17:0] asigned,bsigned;
reg [39:0] actual;
integer i,cycle,outbit,outword,checks=0;
initial begin
for(i=0;i<WORDS;i=i+1) begin
asigned=$random; bsigned=$random;
case(i)
0: begin asigned=0; bsigned=0; end
1: begin asigned=-131072; bsigned=-131072; end
2: begin asigned=-131072; bsigned=131071; end
3: begin asigned=131071; bsigned=131071; end
4: begin asigned=-1; bsigned=-1; end
endcase
av[i]={{22{asigned[17]}},asigned};
bv[i]={{22{bsigned[17]}},bsigned};
if(i>=1024) begin av[i]={$random,$random}; bv[i]={$random,$random}; end
// Independent testbench arithmetic, not synthesizable DSP RTL.
expected[i]=av[i]*bv[i];
end
repeat(3) @(negedge clk);
reset=0;
actual=0;
for(cycle=0;cycle<WORDS*40+13;cycle=cycle+1) begin
if(cycle<WORDS*40) begin
a_bit=av[cycle/40][cycle%40]; b_bit=bv[cycle/40][cycle%40];
end else begin a_bit=0; b_bit=0; end
@(posedge clk); #1;
if(cycle>=13) begin
outbit=(cycle-13)%40; outword=(cycle-13)/40;
actual[outbit]=product_bit;
if(word_end !== (outbit==39)) $fatal(1,"word alignment cycle=%0d",cycle);
if(outbit==39) begin
if(actual!==expected[outword]) $fatal(1,"product word=%0d a=%h b=%h got=%h want=%h",outword,av[outword],bv[outword],actual,expected[outword]);
checks=checks+1;
end
end
@(negedge clk);
end
if(checks!=WORDS) $fatal(1,"missing products");
$display("SERIAL_MUL40_PASS words=%0d bits=40 input_ii=40 latency=13 full_signed_18x18=true physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/open_e310/tb_gf_timing_score_ram.sv · 40 lines
Download this file · Permanent section link
`timescale 1ns/1ps
module tb_gf_timing_score_ram;
reg clk=0,clear=1,we=0;
always #5 clk=~clk;
reg [4:0] address=0;
reg [28:0] data=0;
wire [28:0] result;
reg [28:0] reference [0:19];
reg [31:0] rng=32'h132904ab;
integer n,k,checks=0;
gf_dsss_timing_score_ram dut(.clk(clk),.clear(clear),.write_enable(we),
.address(address),.write_data(data),.read_data(result));
task check_all;
begin
for(k=0;k<32;k=k+1) begin
address=k; #0.01;
if(result !== ((k<20) ? reference[k] : 29'd0))
$fatal(1,"score bank mismatch cycle=%0d address=%0d",n,k);
checks=checks+1;
end
end
endtask
initial begin
for(k=0;k<20;k=k+1)reference[k]=0;
@(posedge clk);#1;check_all();
for(n=0;n<5000;n=n+1) begin
@(negedge clk);
rng=rng^(rng<<13);rng=rng^(rng>>17);rng=rng^(rng<<5);
address=rng[4:0];data=rng[28:0];
clear=(n%37==0);we=(rng[31:30]!=0);
@(posedge clk);
if(clear)for(k=0;k<20;k=k+1)reference[k]=0;
else if(we && address<20)reference[address]=data;
#1;check_all();
end
$display("TIMING_SCORE_RAM_PASS checks=%0d clear_write_priority=true full_width=29 physical_rf=false",checks);
$finish;
end
endmodule
wifi_e310_link/fpga/rtl/gf_e31x_sifs_inline.sv · 141 lines
Download this file · Permanent section link
// Greenforest hard-real-time Wi-Fi receive/response insertion for Ettus E31x.
//
// The stock E31x radio sample format is signed IQ16 with the AD9361's twelve
// data bits left aligned. Channel zero occupies rx_flat/tx_flat[31:0] as
// {I[15:0], Q[15:0]}. Channel one is passed through unchanged.
`timescale 1ns/1ps
module gf_e31x_sifs_inline #(
// At 20 MS/s in the stock two-channel (MIMO) interface, radio_clk is 40 MHz
// and rx_sample_valid/tx_sample_tick assert once per channel-zero sample.
parameter integer CLOCK_HZ = 40_000_000,
parameter integer SIFS_US = 10,
parameter integer SINGLE_PHASE_RX = 0,
parameter integer SERIAL_DIFFERENTIAL = 0,
parameter integer TIMING_SCORE_RAM = 0,
parameter integer SERIAL_CONTROL_CRC = 0,
parameter integer SERIAL_RX_CRC = 0,
parameter integer RECURSIVE_CORRELATOR = 0,
parameter integer SERIAL_BARKER = 0
) (
input wire clk,
input wire resetn,
input wire mode_mimo,
input wire arm,
input wire kill,
input wire [47:0] ap_mac,
input wire rx_sample_valid,
input wire [63:0] rx_flat,
input wire tx_sample_tick,
input wire [63:0] host_tx_flat,
output wire [63:0] air_tx_flat,
output wire host_psdu_start,
output wire host_psdu_byte_valid,
output wire [7:0] host_psdu_byte,
output wire host_psdu_byte_last,
output wire [15:0] host_psdu_end_age_cycles,
output wire response_pending,
output wire response_active,
output wire response_start,
output wire response_is_cts,
output wire [47:0] response_mac,
output wire [15:0] response_duration_us,
output wire tx_override_valid,
output wire mode_fault,
output wire [31:0] rx_sfd_count,
output wire [31:0] rx_plcp_ok_count,
output wire [31:0] rx_plcp_error_count,
output wire [31:0] rx_psdu_count,
output wire [31:0] classified_frame_count,
output wire [31:0] classified_fcs_ok_count,
output wire [31:0] response_candidate_count,
output wire [31:0] malformed_count,
output wire [31:0] response_count,
output wire [31:0] deadline_miss_count,
output wire [31:0] rejected_count,
output wire [31:0] stream_abort_count,
input wire fast_clk,serial_clock_locked
);
wire [31:0] tx_override_iq;
(* ASYNC_REG = "TRUE" *) reg [1:0] mode_mimo_sync = 2'b00;
always @(posedge clk) begin
if (!resetn)
mode_mimo_sync <= 2'b00;
else
mode_mimo_sync <= {mode_mimo_sync[0], mode_mimo};
end
wire mode_mimo_radio = mode_mimo_sync[1];
wire local_kill = kill || !mode_mimo_radio;
wire channel_zero_claim = response_pending || response_active ||
tx_override_valid;
wire serial_barker_fault;
assign mode_fault = arm && (!mode_mimo_radio || serial_barker_fault);
// gf_dsss_* uses {Q, I}; E31x channel zero uses {I, Q}.
wire [31:0] override_channel_zero = {
tx_override_iq[15:0], tx_override_iq[31:16]
};
assign air_tx_flat[63:32] = host_tx_flat[63:32];
assign air_tx_flat[31:0] = tx_override_valid ? override_channel_zero :
(channel_zero_claim ? 32'd0 :
host_tx_flat[31:0]);
gf_dsss_rx_sifs_ap #(
.CLOCK_HZ(CLOCK_HZ),
.SIFS_US(SIFS_US),
.SINGLE_PHASE_RX(SINGLE_PHASE_RX),
.SERIAL_DIFFERENTIAL(SERIAL_DIFFERENTIAL),
.TIMING_SCORE_RAM(TIMING_SCORE_RAM),
.SERIAL_CONTROL_CRC(SERIAL_CONTROL_CRC),
.SERIAL_RX_CRC(SERIAL_RX_CRC),
.RECURSIVE_CORRELATOR(RECURSIVE_CORRELATOR),.SERIAL_BARKER(SERIAL_BARKER)
) low_mac (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(local_kill),
.ap_mac(ap_mac),
.rx_sample_valid(rx_sample_valid),
.rx_i($signed(rx_flat[31:16])),
.rx_q($signed(rx_flat[15:0])),
.tx_sample_tick(tx_sample_tick),
.tx_sink_ready(1'b1),
.tx_override_valid(tx_override_valid),
.tx_override_iq(tx_override_iq),
.response_pending(response_pending),
.response_active(response_active),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_mac(response_mac),
.response_duration_us(response_duration_us),
.host_psdu_start(host_psdu_start),
.host_psdu_byte_valid(host_psdu_byte_valid),
.host_psdu_byte(host_psdu_byte),
.host_psdu_byte_last(host_psdu_byte_last),
.host_psdu_end_age_cycles(host_psdu_end_age_cycles),
.rx_sfd_count(rx_sfd_count),
.rx_plcp_ok_count(rx_plcp_ok_count),
.rx_plcp_error_count(rx_plcp_error_count),
.rx_psdu_count(rx_psdu_count),
.classified_frame_count(classified_frame_count),
.classified_fcs_ok_count(classified_fcs_ok_count),
.response_candidate_count(response_candidate_count),
.malformed_count(malformed_count),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count),
.stream_abort_count(stream_abort_count),.fast_clk(fast_clk),.serial_clock_locked(serial_clock_locked),
.serial_barker_fault(serial_barker_fault)
);
endmodule
wifi_e310_link/host/e310_capture_stats.cpp · 96 lines
Download this file · Permanent section link
// Portable offline analysis of unchanged little-endian IQ16 ADC records.
// No radio access, filtering, resampling or waveform reconstruction.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
using Bytes=std::vector<std::uint8_t>;
int signed16(unsigned low,unsigned high) {
const auto value=low|(high<<8);
return value>=32768u ? static_cast<int>(value)-65536 : static_cast<int>(value);
}
struct Stats {
std::size_t count=0,nonzero=0,alignment=0;
double sum_i=0,sum_q=0,energy_i=0,energy_q=0;
int peak_i=0,peak_q=0,post_i=0,post_q=0;
};
Stats analyze(const Bytes& bytes,std::size_t pretrigger) {
if(bytes.empty() || bytes.size()%4 || pretrigger>=bytes.size()/4)
throw std::runtime_error("Invalid IQ16 record/pretrigger length");
Stats s; s.count=bytes.size()/4;
for(std::size_t n=0;n<s.count;++n) {
const auto p=n*4;
const int i=signed16(bytes[p],bytes[p+1]),q=signed16(bytes[p+2],bytes[p+3]);
s.sum_i+=i; s.sum_q+=q; s.energy_i+=static_cast<double>(i)*i; s.energy_q+=static_cast<double>(q)*q;
s.peak_i=std::max(s.peak_i,std::abs(i)); s.peak_q=std::max(s.peak_q,std::abs(q));
if(n>=pretrigger) { s.post_i=std::max(s.post_i,std::abs(i)); s.post_q=std::max(s.post_q,std::abs(q)); }
if(i || q) ++s.nonzero;
if((bytes[p]|bytes[p+2])&15u) ++s.alignment;
}
return s;
}
void selftest() {
Bytes bytes;
for(unsigned raw=0;raw<65536;++raw) {
const unsigned other=65535-raw;
bytes.push_back(static_cast<std::uint8_t>(raw)); bytes.push_back(static_cast<std::uint8_t>(raw>>8));
bytes.push_back(static_cast<std::uint8_t>(other)); bytes.push_back(static_cast<std::uint8_t>(other>>8));
const int expected=raw<32768 ? static_cast<int>(raw) : static_cast<int>(raw)-65536;
if(signed16(bytes[4*raw],bytes[4*raw+1])!=expected) throw std::runtime_error("Signed decode mismatch");
}
const auto full=analyze(bytes,0);
if(full.peak_i!=32768 || full.peak_q!=32768 || full.sum_i!=-32768 || full.sum_q!=-32768)
throw std::runtime_error("Full signed-range statistics mismatch");
for(std::size_t cut : {std::size_t{0},std::size_t{4},std::size_t{15}}) {
Bytes small;
for(unsigned n=0;n<16;++n) {
const unsigned i=n<4 ? 32768u : 300u+n, q=65536u-100u-n;
small.insert(small.end(),{static_cast<std::uint8_t>(i),static_cast<std::uint8_t>(i>>8),
static_cast<std::uint8_t>(q),static_cast<std::uint8_t>(q>>8)});
}
const auto s=analyze(small,cut);
if(s.peak_i!=32768 || s.post_i!=(cut<4 ? 32768 : 315) || s.post_q!=115)
throw std::runtime_error("Pretrigger peak boundary mismatch");
}
for(const Bytes invalid : {Bytes{},Bytes{1,2,3}}) {
bool rejected=false; try { (void)analyze(invalid,0); } catch(const std::runtime_error&) { rejected=true; }
if(!rejected) throw std::runtime_error("Malformed record accepted");
}
bool rejected=false; try { (void)analyze(Bytes{0,0,0,0},1); } catch(const std::runtime_error&) { rejected=true; }
if(!rejected) throw std::runtime_error("Invalid pretrigger accepted");
std::cout<<"E310_CAPTURE_STATS_SELFTEST_PASS signed_values=65536 pretrigger=true malformed=true physical_rf=false\n";
}
int main(int argc,char** argv) {
try {
if(argc==2 && std::string(argv[1])=="--self-test") { selftest(); return 0; }
if(argc!=4 || std::string(argv[2])!="--pretrigger")
throw std::runtime_error("Use IQ16_FILE --pretrigger SAMPLE_COUNT | --self-test");
const std::string text=argv[3];
if(text.empty() || text.find_first_not_of("0123456789")!=std::string::npos)
throw std::runtime_error("Invalid pretrigger count");
const auto pre=std::stoull(text);
std::ifstream file(argv[1],std::ios::binary);
if(!file) throw std::runtime_error("Cannot open IQ16 record");
Bytes bytes((std::istreambuf_iterator<char>(file)),{});
if(file.bad() || bytes.size()!=65536) throw std::runtime_error("Expected complete 16384-sample capture");
if(pre>=bytes.size()/4) throw std::runtime_error("Pretrigger exceeds record");
const auto s=analyze(bytes,static_cast<std::size_t>(pre));
const auto count=static_cast<double>(s.count);
std::cout<<std::setprecision(15)<<"{\"Execution\":\"Windows_C++\",\"ComplexSamples\":"<<s.count
<<",\"PretriggerSamples\":"<<pre<<",\"NonzeroSamples\":"<<s.nonzero<<",\"AlignmentFaults\":"<<s.alignment
<<",\"MeanI\":"<<s.sum_i/count<<",\"MeanQ\":"<<s.sum_q/count
<<",\"RmsI\":"<<std::sqrt(s.energy_i/count)<<",\"RmsQ\":"<<std::sqrt(s.energy_q/count)
<<",\"PeakI\":"<<s.peak_i<<",\"PeakQ\":"<<s.peak_q
<<",\"PostTriggerPeakI\":"<<s.post_i<<",\"PostTriggerPeakQ\":"<<s.post_q
<<",\"Resampled\":false,\"Reconstructed\":false}\n";
return 0;
} catch(const std::exception& error) { std::cerr<<"fatal: "<<error.what()<<'\n'; return 1; }
}
wifi_e310_link/host/e310_checked_loader.cpp · 194 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// C++14 loader for the observed E310 Linux 3.14 xdevcfg interface.
// Unlike UHD 3.10 e300_common.cpp, check every write and close, and require
// PCFG_DONE to clear on open and assert BEFORE release re-enables PMU access.
// This is not an AXI drain mechanism or a guarantee of runtime recovery.
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <sys/stat.h>
#include <thread>
#include <unistd.h>
#include <vector>
namespace {
using Bytes = std::vector<uint8_t>;
std::runtime_error system_error(const char* action) {
return std::runtime_error(std::string(action) + ": " + std::strerror(errno));
}
struct ImageInfo { std::string part; size_t payload; size_t payload_size; };
ImageInfo inspect(const Bytes& bytes) {
size_t pos = 0;
auto number = [&](unsigned width) {
if (width > bytes.size() - pos) throw std::runtime_error("truncated bit header");
uint32_t value = 0;
while (width--) value = (value << 8) | bytes[pos++];
return value;
};
const Bytes magic = {0x0f,0xf0,0x0f,0xf0,0x0f,0xf0,0x0f,0xf0,0};
if (number(2) != magic.size() || bytes.size() - pos < magic.size() ||
!std::equal(magic.begin(), magic.end(), bytes.begin() + pos))
throw std::runtime_error("not a Xilinx .bit container");
pos += magic.size();
if (number(2) != 1) throw std::runtime_error("invalid bit header marker");
std::string part;
for (char tag = 'a'; tag <= 'd'; ++tag) {
if (number(1) != static_cast<unsigned>(tag))
throw std::runtime_error("invalid bit metadata sequence");
const auto length = number(2);
if (!length || length > bytes.size() - pos || bytes[pos + length - 1] != 0)
throw std::runtime_error("invalid bit metadata length/terminator");
if (tag == 'b') part.assign(bytes.begin() + pos, bytes.begin() + pos + length - 1);
pos += length;
}
if (part != "7z020clg484" && part != "xc7z020clg484" &&
part != "7z020clg484-3" && part != "xc7z020clg484-3")
throw std::runtime_error("bitstream is not for the E310 XC7Z020 CLG484");
if (number(1) != 'e') throw std::runtime_error("missing bit payload tag");
const auto length = number(4);
if (!length || length != bytes.size() - pos || length % 4)
throw std::runtime_error("truncated, extra or unaligned bit payload");
const Bytes sync = {0xaa,0x99,0x55,0x66};
auto end = bytes.begin() + pos + std::min<size_t>(length, 1024);
if (std::search(bytes.begin() + pos, end, sync.begin(), sync.end()) == end)
throw std::runtime_error("configuration sync word missing near payload start");
return {part, pos, length};
}
Bytes read_image(const char* path) {
const int fd = ::open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
if (fd < 0) throw system_error("open source bitstream");
try {
struct stat st = {};
if (fstat(fd, &st) < 0) throw system_error("stat source bitstream");
if (!S_ISREG(st.st_mode) || st.st_size < 32 || st.st_size > 16 * 1024 * 1024)
throw std::runtime_error("source must be a regular .bit file, 32 bytes..16 MiB");
Bytes bytes(static_cast<size_t>(st.st_size));
size_t offset = 0;
while (offset < bytes.size()) {
const auto count = read(fd, bytes.data() + offset, bytes.size() - offset);
if (count < 0 && errno == EINTR) continue;
if (count < 0) throw system_error("read source bitstream");
if (!count) throw std::runtime_error("source bitstream shortened while reading");
offset += static_cast<size_t>(count);
}
uint8_t extra;
ssize_t tail;
do { tail = read(fd, &extra, 1); } while (tail < 0 && errno == EINTR);
if (tail != 0) throw std::runtime_error("source bitstream changed/read failed");
::close(fd);
return bytes;
} catch (...) { ::close(fd); throw; }
}
int sysfs_flag(const char* leaf) {
std::ifstream file(std::string("/sys/class/xdevcfg/xdevcfg/device/") + leaf);
int value = -1;
std::string extra;
if (!(file >> value) || (value != 0 && value != 1) || (file >> extra))
throw std::runtime_error(std::string("invalid/missing xdevcfg flag: ") + leaf);
return value;
}
struct Device {
virtual ~Device() = default;
virtual void open() = 0;
virtual ssize_t write(const uint8_t*, size_t) = 0;
virtual int done() = 0;
virtual void wait() = 0;
virtual void close() = 0;
};
struct Xdevcfg final : Device {
int fd = -1;
~Xdevcfg() override { if (fd >= 0) ::close(fd); }
void open() override {
// Do not silently modify a global driver mode or create a device node.
if (sysfs_flag("is_partial_bitstream") != 0)
throw std::runtime_error("full-image loader refuses partial-bitstream mode");
struct stat st = {};
if (lstat("/dev/xdevcfg", &st) < 0 || !S_ISCHR(st.st_mode))
throw std::runtime_error("/dev/xdevcfg is not a character device");
fd = ::open("/dev/xdevcfg", O_WRONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) throw system_error("open xdevcfg");
}
ssize_t write(const uint8_t* data, size_t length) override { return ::write(fd, data, length); }
int done() override { return sysfs_flag("prog_done"); }
void wait() override { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }
void close() override {
const int closing = fd; fd = -1;
// Linux close must not be retried on EINTR: the descriptor is released.
if (closing >= 0 && ::close(closing) < 0) throw system_error("close xdevcfg");
}
};
void program(const Bytes& bytes, Device& device, std::ostream& log) {
// Full file already in memory; check the container before opening hardware.
// Callers must additionally verify their expected candidate hash. This
// format check is not an image allowlist or an RF/runtime qualification.
const auto info = inspect(bytes);
log << "E310_CHECKED_LOAD_BEGIN bytes=" << bytes.size() << " payload_bytes="
<< info.payload_size << " part=" << info.part << std::endl;
device.open();
try {
if (device.done() != 0) throw std::runtime_error("PCFG_DONE did not clear on device open");
size_t sent = 0;
unsigned interrupted = 0;
while (sent < bytes.size()) {
const auto request = std::min<size_t>(16384, bytes.size() - sent);
const auto result = device.write(bytes.data() + sent, request);
if (result < 0 && errno == EINTR && ++interrupted <= 16) continue;
if (result < 0) throw system_error("write xdevcfg");
if (result == 0 || static_cast<size_t>(result) > request)
throw std::runtime_error("invalid/zero xdevcfg write result");
sent += static_cast<size_t>(result);
interrupted = 0;
}
bool configured = false;
for (unsigned attempt = 0; attempt < 101; ++attempt) {
if (device.done() == 1) { configured = true; break; }
if (attempt != 100) device.wait();
}
if (!configured) throw std::runtime_error("all bytes accepted but PCFG_DONE did not assert");
log << "E310_CHECKED_LOAD_TRANSFER_COMPLETE bytes=" << sent
<< " prog_done=1 before_release=true" << std::endl;
} catch (...) {
try { device.close(); } catch (...) { log << "E310_CHECKED_LOAD_CLEANUP_CLOSE_FAILED\n"; }
throw;
}
device.close();
if (device.done() != 1) throw std::runtime_error("PCFG_DONE lost after device release");
log << "E310_CHECKED_LOAD_PASS bytes=" << bytes.size()
<< " prog_done=1 runtime_verified=false rf_armed_by_loader=false" << std::endl;
}
} // namespace
#ifndef E310_CHECKED_LOADER_TEST
int main(int argc, char** argv) {
try {
if (argc != 3 || (std::string(argv[1]) != "--inspect" && std::string(argv[1]) != "--load"))
throw std::runtime_error("Use --inspect IMAGE.bit or --load IMAGE.bit (requires recovery guardian)");
auto bytes = read_image(argv[2]);
const auto info = inspect(bytes);
if (std::string(argv[1]) == "--inspect") {
std::cout << "E310_BIT_CONTAINER_PASS bytes=" << bytes.size() << " part="
<< info.part << " payload_bytes=" << info.payload_size
<< " hardware_access=false\n";
return 0;
}
const char* guard = std::getenv("GF_E310_RECOVERY_GUARD");
if (!guard || std::string(guard) != "1")
throw std::runtime_error("run through gf_e310_recovery_guard; recovery is not yet board-qualified");
Xdevcfg device;
program(bytes, device, std::cout);
return 0;
} catch (const std::exception& error) {
std::cerr << "E310_CHECKED_LOADER_FAILED " << error.what() << '\n';
return 1;
}
}
#endif
wifi_e310_link/host/e310_counter_format.hpp · 37 lines
Download this file · Permanent section link
#pragma once
#include <array>
#include <cstdint>
namespace gf::e310::counters {
// Absolute GP0 offsets. All other fields, including TX-rejected at 0x248,
// are already binary/status/payload and must never pass through this decoder.
constexpr std::uint32_t capability_address = 0x280;
constexpr std::uint32_t gray32 = 0x47523332; // GR32
constexpr std::array<std::uint32_t,11> addresses{
0x218,0x21c,0x220,0x224,0x228,0x23c,0x24c,0x260,0x264,0x268,0x26c};
using Words = std::array<std::uint32_t,addresses.size()>;
constexpr bool supported(std::uint32_t capability) noexcept {
return capability == 0 || capability == 0xdead0280 || capability == gray32;
}
constexpr bool encoded_address(std::uint32_t address) noexcept {
switch (address) {
case 0x218: case 0x21c: case 0x220: case 0x224: case 0x228:
case 0x23c: case 0x24c:
case 0x260: case 0x264: case 0x268: case 0x26c: return true;
default: return false;
}
}
constexpr std::uint32_t decode(std::uint32_t gray) noexcept {
gray ^= gray >> 1;
gray ^= gray >> 2;
gray ^= gray >> 4;
gray ^= gray >> 8;
gray ^= gray >> 16;
return gray;
}
constexpr std::uint32_t normalize(bool is_gray, std::uint32_t address,
std::uint32_t value) noexcept {
return is_gray && encoded_address(address) ? decode(value) : value;
}
} // namespace gf::e310::counters
wifi_e310_link/host/e310_counter_snapshot.hpp · 46 lines
Download this file · Permanent section link
#pragma once
#include "e310_counter_format.hpp"
#include "e310_packet_wire.hpp"
#include <sstream>
namespace gf::e310::counters {
constexpr std::size_t snapshot_bytes=4+4*addresses.size();
struct Snapshot {
std::uint32_t format=0;
Words raw{},values{};
};
inline wire::Bytes pack_snapshot(std::uint32_t format,const Words& words) {
if(format!=0 && format!=gray32) throw std::runtime_error("Unknown raw counter format");
wire::Bytes bytes(snapshot_bytes,0);
wire::put(bytes,0,format,4);
for(std::size_t n=0;n<words.size();++n) wire::put(bytes,4+4*n,words[n],4);
return bytes;
}
inline Snapshot unpack_snapshot(const wire::Bytes& bytes) {
if(bytes.size()!=snapshot_bytes) throw std::runtime_error("Invalid counter snapshot length");
Snapshot result;
result.format=static_cast<std::uint32_t>(wire::get(bytes,0,4));
if(result.format!=0 && result.format!=gray32) throw std::runtime_error("Unknown counter snapshot encoding");
for(std::size_t n=0;n<addresses.size();++n) {
result.raw[n]=static_cast<std::uint32_t>(wire::get(bytes,4+4*n,4));
result.values[n]=normalize(result.format==gray32,addresses[n],result.raw[n]);
}
return result;
}
inline std::string snapshot_fields(const Snapshot& snapshot) {
constexpr std::array<const char*,11> names{"fifo_overflow_count","rx_psdu_count",
"response_count","deadline_miss_count","rejected_count","tx_done_count",
"tx_error_count","rx_sfd_count","rx_plcp_ok_count","rx_plcp_error_count","rx_sample_count"};
std::ostringstream out;
out<<"\"source\":\"fpga_gp0\",\"atomic_across_counters\":false,\"counter_format\":\""
<<(snapshot.format==gray32?"gray32":"binary32")<<"\",\"raw_words\":[";
for(std::size_t n=0;n<addresses.size();++n) {
if(n) out<<',';
out<<snapshot.raw[n];
}
out<<']';
for(std::size_t n=0;n<names.size();++n) out<<",\""<<names[n]<<"\":"<<snapshot.values[n];
return out.str();
}
} // namespace gf::e310::counters
wifi_e310_link/host/e310_counter_snapshot_test.cpp · 43 lines
Download this file · Permanent section link
#include "e310_counter_snapshot.hpp"
#include <iostream>
using namespace gf::e310;
static void require(bool ok) { if(!ok) throw std::runtime_error("Raw counter snapshot mismatch"); }
int main() {
counters::Words values{};
std::uint32_t random=0x91ac583bu;
for(unsigned n=0;n<100100;++n) {
for(std::size_t bit=0;bit<values.size();++bit) {
random^=random<<13;random^=random>>17;random^=random<<5;
values[bit]=n<32 ? ((std::uint32_t{1}<<n)+static_cast<std::uint32_t>(bit)-5u) : random;
}
for(auto format:{0u,counters::gray32}) {
auto raw=values;
if(format) for(auto& value:raw)value^=value>>1;
const auto bytes=counters::pack_snapshot(format,raw);
const auto result=counters::unpack_snapshot(bytes);
require(result.raw==raw && result.values==values && result.format==format);
for(std::size_t i=0;i<raw.size();++i)require((raw[i]==0)==(values[i]==0));
}
}
auto bytes=counters::pack_snapshot(counters::gray32,values);
wire::Decoder decoder;
unsigned deliveries=0;
auto encoded=wire::encode({wire::Kind::counter_snapshot,123,42,bytes});
for(auto byte:encoded)decoder.feed(&byte,1,[&](wire::Message message) {
require(message.kind==wire::Kind::counter_snapshot && message.payload==bytes &&
message.session==123 && message.sequence==42);++deliveries;
});
require(deliveries==1 && decoder.rejected()==0);
for(std::size_t length=0;length<100;++length)if(length!=counters::snapshot_bytes) {
bool rejected=false;
try {counters::unpack_snapshot(wire::Bytes(length));}catch(const std::exception&){rejected=true;}
require(rejected);
}
for(auto format:{1u,0xdead0280u,0xffffffffu}) {
wire::put(bytes,0,format,4);bool rejected=false;
try {counters::unpack_snapshot(bytes);}catch(const std::exception&){rejected=true;}
require(rejected);
}
std::cout<<"E310_COUNTER_SNAPSHOT_TEST_PASS words=11 full_width=true boundary_bits=32 vectors=100100 legacy_binary=true fragmented_wire=true malformed_rejected=true physical_rf=false\n";
}
wifi_e310_link/host/e310_host_waveform.hpp · 56 lines
Download this file · Permanent section link
#pragma once
// Lossless, compact description of the current rectangular 20 MS/s waveform.
// This is NOT arbitrary IQ streaming. Windows owns PLCP, scrambling, DBPSK,
// Barker spreading and the 11:20 sampling pattern; hardware only plays it.
#include "e310_packet_wire.hpp"
#include <array>
namespace gf::e310::waveform {
constexpr std::uint32_t kCapability = 0x57463230u; // WF20, GP0 offset 0x27c
constexpr std::size_t kHeaderBytes = 12;
constexpr std::size_t kMaxPsdu = wire::kMaxPayload - kHeaderBytes - 24;
inline void validate(const wire::Bytes& data) {
if(data.size() <= kHeaderBytes || data.size() > wire::kMaxPayload ||
data[3] != 20 || (data[2] & 0xf0))
throw std::runtime_error("Invalid WF20 waveform description");
}
inline wire::Bytes encode(const wire::Bytes& psdu) {
if(psdu.empty() || psdu.size() > kMaxPsdu)
throw std::runtime_error("Host waveform PSDU exceeds 1..4059 bytes");
wire::Bytes plain(16,0xff); // long SYNC, 128 ones
plain.push_back(0xa0); plain.push_back(0xf3); // long SFD, LSB first
const auto duration=static_cast<std::uint16_t>(psdu.size()*8);
std::array<std::uint8_t,4> plcp{0x0a,0,std::uint8_t(duration),std::uint8_t(duration>>8)};
std::uint16_t crc=0xffff;
for(auto byte:plcp) {
plain.push_back(byte);
for(unsigned bit=0;bit<8;++bit) {
const bool mix=(crc^(byte>>bit))&1;
crc=static_cast<std::uint16_t>((crc>>1)^(mix?0x8408:0));
}
}
crc^=0xffff;
plain.push_back(std::uint8_t(crc)); plain.push_back(std::uint8_t(crc>>8));
plain.insert(plain.end(),psdu.begin(),psdu.end());
wire::Bytes out(kHeaderBytes+plain.size(),0);
constexpr std::array<bool,11> negative{false,true,false,false,true,false,false,false,true,true,true};
std::uint32_t pattern=0;
for(unsigned sample=0;sample<20;++sample)
if(negative[sample*11/20]) pattern|=1u<<sample;
wire::put(out,0,pattern,3); out[3]=20;
wire::put(out,4,0x00002000,4); // I=+8192, Q=0, no precision reduction
wire::put(out,8,0x0000e000,4); // I=-8192, Q=0
std::uint8_t state=0x5d;
bool phase=false;
for(std::size_t index=0;index<plain.size();++index) {
for(unsigned bit=0;bit<8;++bit) {
const auto scrambled=((plain[index]>>bit)^(state>>3)^(state>>6))&1u;
state=static_cast<std::uint8_t>(((state<<1)|scrambled)&0x7f);
phase^=scrambled!=0;
out[kHeaderBytes+index]|=std::uint8_t(unsigned(phase)<<bit);
}
}
return out;
}
} // namespace gf::e310::waveform
wifi_e310_link/host/e310_legacy_probe.cpp · 130 lines
Download this file · Permanent section link
// Narrow board bring-up: no RF arming and no TX samples in any mode.
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <linux/spi/spidev.h>
#include <stdexcept>
#include <string>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
struct Fd {
int value;
explicit Fd(const char* path) : value(open(path, O_RDWR | O_SYNC)) {
if (value < 0) throw std::runtime_error(std::string(path) + ": " + strerror(errno));
}
~Fd() { close(value); }
};
int main(int argc, char** argv) {
try {
if (argc != 2) throw std::runtime_error("Use spi-read, rx-delay-15, or mmio-check");
const std::string mode(argv[1]);
if (mode == "pmu-watch") {
Fd memory("/dev/mem");
void* mapping = mmap(nullptr, 4096, PROT_READ, MAP_SHARED, memory.value, 0x40300000);
if (mapping == MAP_FAILED) throw std::runtime_error("PMU mapping failed");
volatile uint32_t* pmu = static_cast<volatile uint32_t*>(mapping);
// Real board was identified as PMU firmware 2.2 on the stock image.
// Wait for AVR SPI status after reconfiguration, then cover several
// periods of the installed Linux PMU worker without radio setup.
bool good = true;
for (unsigned second = 0; second < 15; ++second) {
usleep(1000000);
__sync_synchronize();
const auto misc = pmu[1], charger = pmu[3], settings = pmu[7];
__sync_synchronize();
std::cout << "E310_PMU_LIVE second=" << std::dec << second + 1
<< " misc=0x" << std::hex << misc << " charger=0x" << charger
<< " settings=0x" << settings << std::dec
<< " rf_arm_performed=false" << std::endl;
if ((misc & 255) != 0x22 || (charger & ~31u) || (settings & ~255u)) good = false;
}
munmap(mapping, 4096);
if (!good) throw std::runtime_error("Real PMU status was missing or invalid");
std::cout << "E310_PMU_LIVE_CHECK_PASS duration_seconds=15 rf_arm_performed=false" << std::endl;
return 0;
}
if (mode == "spi-read" || mode == "rx-delay-15") {
Fd spi("/dev/spidev0.1");
uint8_t spi_mode = SPI_CPHA;
uint8_t bits = 8;
uint32_t speed = 2000000;
if (ioctl(spi.value, SPI_IOC_WR_MODE, &spi_mode) < 0)
throw std::runtime_error("SPI mode setup failed");
if (ioctl(spi.value, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 ||
ioctl(spi.value, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0)
throw std::runtime_error("SPI word/speed setup failed");
auto transfer = [&](unsigned address, bool write, unsigned value) {
uint8_t tx[3] = {uint8_t((address >> 8) | (write ? 0x80 : 0)), uint8_t(address), uint8_t(value)};
uint8_t rx[3] = {};
spi_ioc_transfer tr = {};
tr.tx_buf = reinterpret_cast<uintptr_t>(tx);
tr.rx_buf = reinterpret_cast<uintptr_t>(rx);
tr.len = 3;
tr.speed_hz = 2000000;
tr.bits_per_word = 8;
tr.tx_nbits = 1;
tr.rx_nbits = 1;
if (ioctl(spi.value, SPI_IOC_MESSAGE(1), &tr) != 3)
throw std::runtime_error("SPI transfer failed");
return unsigned(rx[2]);
};
const auto id = transfer(0x037, false, 0);
std::cout << "AD9361_SPI_ID value=0x" << std::hex << id << '\n';
if ((id & 0x08) != 0x08 || id == 0xff) throw std::runtime_error("AD9361 ID mismatch");
if (mode == "rx-delay-15") {
const auto original = transfer(0x006, false, 0);
if (original != 0x08 && original != 0x0f)
throw std::runtime_error("Unexpected RX timing value; not modified");
transfer(0x006, true, 0x0f);
if (transfer(0x006, false, 0) != 0x0f)
throw std::runtime_error("RX data delay readback failed");
}
for (unsigned address : {0x037, 0x002, 0x003, 0x006, 0x007, 0x010, 0x011, 0x012, 0x014, 0x017, 0x035, 0x036})
std::cout << "AD9361 reg=0x" << std::hex << address << " value=0x" << transfer(address, false, 0) << '\n';
return 0;
}
if (mode != "mmio-check" && mode != "mmio-stress") throw std::runtime_error("Unknown mode");
Fd memory("/dev/mem");
void* mapping = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, memory.value, 0x40010000);
if (mapping == MAP_FAILED) throw std::runtime_error("MMIO mapping failed");
volatile uint32_t* regs = static_cast<volatile uint32_t*>(mapping);
const auto magic = regs[0x200 / 4];
const auto version = regs[0x22c / 4];
const auto status = regs[0x210 / 4];
std::cout << "E310_LEGACY_MMIO base=0x40010000 magic=0x" << std::hex << magic
<< " version=0x" << version << " status=0x" << status << '\n';
// No write, including kill, may hit an unrecognized register bank.
if (magic == 0x47464531 && (version >> 16) == 1) {
regs[0x204 / 4] = 2;
__sync_synchronize();
usleep(1000);
const auto killed = regs[0x210 / 4];
std::cout << "E310_LEGACY_KILL_READBACK status=0x" << std::hex << killed << '\n';
if (mode == "mmio-stress" && (killed & 3) == 2) {
for (unsigned iteration = 0; iteration < 1000000; ++iteration) {
if (regs[0x200 / 4] != 0x47464531 || regs[0x22c / 4] != 0x10001 ||
(regs[0x210 / 4] & 3) != 2)
throw std::runtime_error("MMIO stress register readback failed");
if (iteration % 100 == 0) {
regs[0x204 / 4] = 2;
__sync_synchronize();
}
}
std::cout << "E310_MMIO_STRESS_PASS reads=3000000 kill_writes=10000 rf_armed=false" << std::endl;
}
munmap(mapping, 4096);
return ((killed & 3) == 2) ? 0 : 2;
}
munmap(mapping, 4096);
return 2;
} catch (const std::exception& error) {
std::cerr << "fatal: " << error.what() << '\n';
return 1;
}
}
wifi_e310_link/host/e310_legacy_radio_init.cpp · 171 lines
Download this file · Permanent section link
// SPDX-License-Identifier: GPL-3.0-or-later
// Post-PL-load AD9361 initialization on the 2017 E310 Linux image.
// Uses the matching upstream calibration driver, not a stale register replay.
// The custom RF gate must remain killed throughout; this sends no TX samples.
#include <cstdint>
#include "ad9361_device.h"
#include <boost/make_shared.hpp>
#include <cmath>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <linux/spi/spidev.h>
#include <stdexcept>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
namespace {
struct Fd {
int value;
explicit Fd(const char* path) : value(open(path, O_RDWR | O_SYNC)) {
if (value < 0) throw std::runtime_error(std::string(path) + ": " + strerror(errno));
}
~Fd() { close(value); }
Fd(const Fd&) = delete;
Fd& operator=(const Fd&) = delete;
};
class Spi final : public uhd::usrp::ad9361_io {
Fd fd_{"/dev/spidev0.1"};
uint8_t transfer(uint32_t reg, uint8_t value, bool write) {
if (reg > 0x3ff) throw std::runtime_error("Invalid AD9361 register");
uint8_t tx[3] = {uint8_t((reg >> 8) | (write ? 0x80 : 0)), uint8_t(reg), value};
uint8_t rx[3] = {};
spi_ioc_transfer tr = {};
tr.tx_buf = reinterpret_cast<uintptr_t>(tx);
tr.rx_buf = reinterpret_cast<uintptr_t>(rx);
tr.len = 3;
tr.speed_hz = 2000000;
tr.bits_per_word = 8;
tr.tx_nbits = 1;
tr.rx_nbits = 1;
if (ioctl(fd_.value, SPI_IOC_MESSAGE(1), &tr) != 3)
throw std::runtime_error("AD9361 SPI transfer failed");
return rx[2];
}
public:
Spi() {
uint8_t mode = SPI_CPHA, bits = 8;
uint32_t speed = 2000000;
if (ioctl(fd_.value, SPI_IOC_WR_MODE, &mode) < 0 ||
ioctl(fd_.value, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 ||
ioctl(fd_.value, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0)
throw std::runtime_error("AD9361 SPI setup failed");
}
uint8_t peek8(uint32_t reg) override { return transfer(reg, 0, false); }
void poke8(uint32_t reg, uint8_t value) override { transfer(reg, value, true); }
};
class Board final : public uhd::usrp::ad9361_params {
public:
uhd::usrp::digital_interface_delays_t get_digital_interface_timing() override {
// Match the physical I/O timing constraints of the custom image.
return {0, 15, 0, 15};
}
uhd::usrp::digital_interface_mode_t get_digital_interface_mode() override {
return uhd::usrp::AD9361_DDR_FDD_LVCMOS;
}
uhd::usrp::clocking_mode_t get_clocking_mode() override {
return uhd::usrp::AD9361_XTAL_N_CLK_PATH;
}
double get_band_edge(uhd::usrp::frequency_band_t band) override {
switch (band) {
case uhd::usrp::AD9361_RX_BAND0: return 1.2e9;
case uhd::usrp::AD9361_RX_BAND1: return 2.6e9;
case uhd::usrp::AD9361_TX_BAND0: return 2.94e9;
default: return 0;
}
}
};
struct KilledMapping {
Fd mem{"/dev/mem"};
volatile uint32_t* regs;
KilledMapping() {
void* p = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, mem.value, 0x40010000);
if (p == MAP_FAILED) throw std::runtime_error("Custom MMIO mapping failed");
regs = static_cast<volatile uint32_t*>(p);
const auto version = regs[0x22c / 4];
// v1.2/v1.3 add observation-only registers; the RF kill/control ABI is
// unchanged. Keep the list explicit instead of accepting any image.
if (regs[0x200 / 4] != 0x47464531 ||
(version != 0x00010001 && version != 0x00010002 && version != 0x00010003) ||
(regs[0x210 / 4] & 3) != 2) {
munmap(p, 4096);
throw std::runtime_error("Expected recognized, killed custom FPGA before radio setup");
}
}
~KilledMapping() {
regs[0x204 / 4] = 2;
__sync_synchronize();
munmap(const_cast<uint32_t*>(regs), 4096);
}
};
}
int main(int argc, char** argv) {
try {
if (argc < 2 || std::string(argv[1]) != "--prepare")
throw std::runtime_error("Use --prepare [--channel N] [--tx-gain DB] [--rx-gain DB]");
int channel = 6;
double tx_gain = 0, rx_gain = 20;
for (int index = 2; index < argc; index += 2) {
if (index + 1 >= argc) throw std::runtime_error("Missing radio option value");
const std::string option(argv[index]);
if (option == "--channel") channel = std::stoi(argv[index + 1]);
else if (option == "--tx-gain") tx_gain = std::stod(argv[index + 1]);
else if (option == "--rx-gain") rx_gain = std::stod(argv[index + 1]);
else throw std::runtime_error("Unknown radio setup option");
}
if (channel < 1 || channel > 11 || !std::isfinite(tx_gain) ||
!std::isfinite(rx_gain) || tx_gain < 0 || tx_gain > 89.75 ||
rx_gain < 0 || rx_gain > 76)
throw std::runtime_error("Radio option outside supported range");
const double center = (2407 + 5 * channel) * 1e6;
KilledMapping gate;
auto spi = boost::make_shared<Spi>();
auto board = boost::make_shared<Board>();
using Device = uhd::usrp::ad9361_device_t;
Device radio(board, spi);
radio.initialize();
const double rate = radio.set_clock_rate(20e6);
radio.set_active_chains(true, true, true, true);
const double rx_hz = radio.tune(Device::RX, center);
const double tx_hz = radio.tune(Device::TX, center);
for (auto chain : {Device::CHAIN_1, Device::CHAIN_2}) {
radio.set_agc(chain, false);
radio.set_gain(Device::RX, chain, rx_gain);
radio.set_gain(Device::TX, chain, tx_gain);
}
radio.set_bw_filter(Device::RX, 20e6);
radio.set_bw_filter(Device::TX, 20e6);
radio.data_port_loopback(false);
radio.digital_test_tone(false);
if (std::abs(rate - 20e6) > 1 || std::abs(rx_hz - center) > 100 ||
std::abs(tx_hz - center) > 100 || spi->peek8(0x006) != 0x0f ||
spi->peek8(0x007) != 0x0f || (spi->peek8(0x247) & 2) == 0 ||
(spi->peek8(0x287) & 2) == 0)
throw std::runtime_error("Radio setup readback failed");
for (unsigned address : {0x073u, 0x075u}) {
const unsigned attenuation = spi->peek8(address) | ((spi->peek8(address + 1) & 1) << 8);
if (std::abs((89.75 - attenuation * 0.25) - tx_gain) > 0.251)
throw std::runtime_error("TX attenuation readback failed");
}
usleep(10000);
const auto status = gate.regs[0x210 / 4];
std::cout << "E310_POST_LOAD_RADIO rate=" << rate << " rx_hz=" << rx_hz
<< " tx_hz=" << tx_hz << " tx_gain_db=" << tx_gain << " rx_gain_db=" << rx_gain
<< " status=0x" << std::hex << status << std::dec
<< " radio_ready=" << ((status >> 13) & 1)
<< " rf_armed=false tx_samples=0 calibrated_power=false" << std::endl;
if ((status & 0x2003) != 0x2002)
throw std::runtime_error("Custom FPGA radio-ready/killed gate failed");
return 0;
} catch (const std::exception& error) {
std::cerr << "fatal: " << error.what() << std::endl;
return 1;
}
}
wifi_e310_link/host/e310_packet_agent.cpp · 229 lines
Download this file · Permanent section link
// E310-side byte/IQ hardware adapter for the Windows AP. No WPA2 keys/IP stack.
#include "e310_packet_wire.hpp"
#include "e310_sifs_uio.hpp"
#include "e310_host_waveform.hpp"
#include "e310_counter_snapshot.hpp"
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <deque>
#include <fcntl.h>
#include <iostream>
#include <termios.h>
#include <sys/ioctl.h>
#include <thread>
#include <unistd.h>
namespace {
using namespace gf::e310;
using Clock=std::chrono::steady_clock;
volatile sig_atomic_t stopping=0;
void on_signal(int) { stopping=1; }
class BinaryStdio {
int in_flags_=-1,out_flags_=-1;
termios saved_{};
bool terminal_=false;
public:
BinaryStdio() {
in_flags_=fcntl(0,F_GETFL); out_flags_=fcntl(1,F_GETFL);
if(in_flags_<0 || out_flags_<0) throw std::runtime_error("Cannot inspect packet descriptors");
if(isatty(0)) {
if(tcgetattr(0,&saved_)<0) throw std::runtime_error("Cannot save UART mode");
auto raw=saved_; cfmakeraw(&raw);
if(tcsetattr(0,TCSANOW,&raw)<0) throw std::runtime_error("Cannot set raw UART mode");
terminal_=true;
}
if(fcntl(0,F_SETFL,in_flags_|O_NONBLOCK)<0 || fcntl(1,F_SETFL,out_flags_|O_NONBLOCK)<0) {
fcntl(0,F_SETFL,in_flags_); fcntl(1,F_SETFL,out_flags_);
if(terminal_) tcsetattr(0,TCSANOW,&saved_);
throw std::runtime_error("Cannot set nonblocking packet descriptors");
}
}
~BinaryStdio() {
fcntl(0,F_SETFL,in_flags_); fcntl(1,F_SETFL,out_flags_);
if(terminal_) tcsetattr(0,TCSANOW,&saved_);
}
};
class Output {
std::deque<wire::Bytes> queue_;
std::size_t offset_=0,bytes_=0;
public:
bool empty() const { return queue_.empty(); }
void push(const wire::Message& message) {
auto encoded=wire::encode(message);
if(bytes_+encoded.size()>131072) throw std::runtime_error("Host packet queue overflow");
bytes_+=encoded.size(); queue_.push_back(std::move(encoded));
}
void flush() {
if(queue_.empty()) return;
const auto& front=queue_.front();
const auto sent=write(1,front.data()+offset_,front.size()-offset_);
if(sent<0) {
if(errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR) return;
throw std::runtime_error("Packet UART write failed");
}
offset_+=static_cast<std::size_t>(sent); bytes_-=static_cast<std::size_t>(sent);
if(offset_==front.size()) { queue_.pop_front(); offset_=0; }
}
};
void healthy(const SifsStatus& status) {
// Binary and full-width Gray code both encode zero as zero. No magnitude
// or arithmetic on native counter words is performed by the adapter.
if(!status.armed || status.killed || !status.radio_path_ready || status.mode_fault ||
status.config_fault || status.tx_error_seen || status.fifo_overflowed ||
status.fifo_overflow_count || status.deadline_miss_count)
throw std::runtime_error("E310 packet hardware unhealthy");
}
int run(int argc,char** argv) {
bool enabled=false,legacy=false;
int channel=6;
for(int i=1;i<argc;++i) {
const std::string arg=argv[i];
if(arg=="--run") enabled=true;
else if(arg=="--legacy-devmem") legacy=true;
else if(arg=="--channel" && i+1<argc) channel=std::stoi(argv[++i]);
else if(arg=="--help") {
std::cout<<"gf_e310_packet_agent --run --legacy-devmem --channel 6\n"
"Requires guarded, exclusively owned, preconfigured and killed custom FPGA.\n"
"Binary stdin/stdout; progress/faults on stderr; no credentials or IP processing.\n";
return 0;
} else throw std::runtime_error("Unknown packet agent option");
}
if(!enabled || !legacy || channel<1 || channel>11) throw std::runtime_error("Explicit legacy packet-agent run required");
const char* guarded=std::getenv("GF_E310_RECOVERY_GUARD");
if(!guarded || std::string(guarded)!="1") throw std::runtime_error("Hardware watchdog guard required");
if(std::filesystem::exists("/run/gf-e310-live.lock") || std::filesystem::exists("/run/gf-e310-ap.pid"))
throw std::runtime_error("Existing AP owns the radio; packet agent not started");
if(isatty(0)) {
// The watchdog owns a separate child process group. Detach only this
// non-session-leader from job control, retaining its open UART FDs.
// Otherwise a background UART read can stop the agent with SIGTTIN.
// A session leader must never detach: that would hang up the shell.
if(getsid(0)==getpid()) throw std::runtime_error("Packet agent must not be a terminal session leader");
if(ioctl(0,TIOCNOTTY)<0 && errno!=ENOTTY)
throw std::runtime_error("Cannot detach packet agent terminal job control");
}
BinaryStdio terminal;
SifsUio bridge("mboard-regs",true,CounterAccess::native_words);
if(bridge.register_version()<0x10003u || !bridge.status().killed)
throw std::runtime_error("Expected killed register-v1.3 packet image; launcher must pin the qualified bitstream");
bridge.kill();
std::signal(SIGINT,on_signal); std::signal(SIGTERM,on_signal); std::signal(SIGPIPE,SIG_IGN);
Output output;
wire::Decoder decoder;
std::uint64_t session=0,events_rx=0,frames_tx=0;
std::uint32_t received_sequence=0,sent_sequence=0;
wire::Bytes batch; batch.reserve(2*wire::kRxBatchEvents);
bool armed=false,clean_stop=false;
const auto started=Clock::now();
auto peer_at=started,hello_at=started,status_at=started,report_at=started,counters_at=started;
auto batch_deadline=started;
const auto send=[&](wire::Kind kind,wire::Bytes payload={}) {
if(sent_sequence==0xffffffffu) throw std::runtime_error("Packet agent sequence exhausted");
output.push({kind,session,++sent_sequence,std::move(payload)});
};
try {
while(!stopping) {
const auto now=Clock::now();
if(!armed && now>=hello_at) {
const std::string identity="GF_E310_PACKET_AGENT_V1";
output.push({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
hello_at=now+std::chrono::milliseconds(500);
}
std::uint8_t bytes[8192];
const auto count=read(0,bytes,sizeof(bytes));
if(count<0 && errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR)
throw std::runtime_error("Packet UART read failed");
if(count==0 && !isatty(0)) throw std::runtime_error("Host packet input closed");
if(count>0) decoder.feed(bytes,static_cast<std::size_t>(count),[&](wire::Message msg) {
if(msg.kind==wire::Kind::initialize && !armed) {
if(!msg.session || msg.sequence!=1 || msg.payload.size()!=8 ||
msg.payload[6]!=channel || msg.payload[7]!=1 || (msg.payload[0]&1))
throw std::runtime_error("Invalid host packet configuration");
Mac bssid{}; std::copy_n(msg.payload.begin(),6,bssid.begin());
if(bssid==Mac{}) throw std::runtime_error("Zero BSSID rejected");
session=msg.session; received_sequence=1;
bridge.configure_and_arm(bssid,RfConfig{}); healthy(bridge.status()); armed=true;
const auto capability=bridge.waveform_capability();
if(capability!=0 && capability!=waveform::kCapability)
throw std::runtime_error("Unknown hardware waveform contract");
wire::Bytes contract(25,0);
wire::put(contract,0,bridge.register_version(),4);
wire::put(contract,4,20000000,4); wire::put(contract,8,40000000,4);
contract[12]=static_cast<std::uint8_t>(channel);
wire::put(contract,13,capability,4);
wire::put(contract,17,wire::kRxEventCapability,4);
wire::put(contract,21,wire::kCounterSnapshotCapability,4);
send(wire::Kind::ready,std::move(contract));
} else {
if(!armed || msg.session!=session || msg.sequence<=received_sequence) return;
received_sequence=msg.sequence;
if(msg.kind==wire::Kind::tx_psdu) {
std::uint32_t crc=0xffffffffu;
for(auto byte:msg.payload) crc=wire::crc_byte(crc,byte);
if(msg.payload.size()<10 || crc!=0xdebb20e3u) throw std::runtime_error("Invalid host TX PSDU/FCS");
bridge.send_psdu(msg.payload); ++frames_tx;
wire::Bytes completed(4,0); wire::put(completed,0,msg.sequence,4);
send(wire::Kind::tx_done,std::move(completed));
} else if(msg.kind==wire::Kind::tx_waveform) {
bridge.send_waveform(msg.payload); ++frames_tx;
wire::Bytes completed(4,0); wire::put(completed,0,msg.sequence,4);
send(wire::Kind::tx_done,std::move(completed));
} else if(msg.kind==wire::Kind::ping && msg.payload.empty()) send(wire::Kind::pong);
else if(msg.kind==wire::Kind::stop && msg.payload.empty()) {
bridge.kill(); armed=false; clean_stop=true; stopping=1;
send(wire::Kind::stopped);
} else throw std::runtime_error("Wrong-direction host packet command");
}
peer_at=Clock::now();
});
if(armed) {
if(now-peer_at>std::chrono::seconds(2)) throw std::runtime_error("Windows host lease expired");
// Relay raw FIFO metadata; no per-frame buffer or start/end
// interpretation remains here. Batch by capacity or 1 ms,
// not frame boundaries; this is not a packet rate limiter.
for(unsigned drained=0;drained<wire::kRxBatchEvents && batch.size()<2*wire::kRxBatchEvents;++drained) {
const auto event=bridge.read_event(); if(!event) break;
if(batch.empty()) batch_deadline=Clock::now()+std::chrono::milliseconds(1);
batch.push_back(event->byte);
batch.push_back(static_cast<std::uint8_t>((event->first?1u:0u)|(event->last?2u:0u)));
++events_rx;
}
if(!batch.empty() && (batch.size()==2*wire::kRxBatchEvents || Clock::now()>=batch_deadline)) {
send(wire::Kind::rx_events,std::move(batch)); batch.clear();
batch.reserve(2*wire::kRxBatchEvents);
}
if(now>=status_at) { healthy(bridge.status()); status_at=now+std::chrono::milliseconds(100); }
if(now>=counters_at) {
send(wire::Kind::counter_snapshot,counters::pack_snapshot(
bridge.counter_format(),bridge.raw_counter_snapshot()));
counters_at=now+std::chrono::seconds(1);
}
} else if(!clean_stop && now-started>std::chrono::seconds(30))
throw std::runtime_error("Windows host did not initialize packet agent");
if(now>=report_at) {
std::cerr<<"E310_PACKET_AGENT_PROGRESS armed="<<armed<<" rx_events="<<events_rx
<<" tx_completed="<<frames_tx<<" wire_rejected="<<decoder.rejected()
<<" frame_assembly=Windows_C++ counter_decode=Windows_C++"<<'\n'<<std::flush;
report_at=now+std::chrono::seconds(1);
}
output.flush();
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
bridge.kill();
const auto until=Clock::now()+std::chrono::milliseconds(500);
while(!output.empty() && Clock::now()<until) { output.flush(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); }
std::cerr<<"E310_PACKET_AGENT_STOPPED killed="<<bridge.status().killed<<" acknowledged="<<clean_stop<<'\n';
return clean_stop?0:1;
} catch(const std::exception& error) {
bridge.kill();
std::cerr<<"E310_PACKET_AGENT_FAULT killed=true reason="<<error.what()<<'\n'<<std::flush;
throw;
}
}
}
int main(int argc,char** argv) {
try { return run(argc,argv); }
catch(const std::exception& error) { std::cerr<<"fatal: "<<error.what()<<'\n'; return 1; }
}
wifi_e310_link/host/e310_packet_ap_core.hpp · 158 lines
Download this file · Permanent section link
#pragma once
#define GF_AP_PROTOCOL_ONLY 1
#define GF_AP_LIBRARY_ONLY 1
#include "../../wifi_pluto_link/host/ap_realtime.cpp"
#undef GF_AP_LIBRARY_ONLY
#undef GF_AP_PROTOCOL_ONLY
#include "e310_packet_wire.hpp"
#include "e310_host_waveform.hpp"
#include "e310_rx_events.hpp"
#include "e310_counter_snapshot.hpp"
#include <memory>
namespace gf::e310 {
// Owns all Wi-Fi management, keys, decrypted IP, DHCP/ARP/TCP and HTTP on host.
// Peer owns only FPGA I/O and ACK/CTS. Link completion is not an over-air ACK.
class PacketApCore {
ap::ProtocolConfig config_;
ap::ApProtocol protocol_;
std::uint64_t session_;
std::uint32_t tx_sequence_=0, rx_sequence_=0;
bool initialized_=false, ready_=false, stopped_=false;
bool host_waveform_=false;
bool host_rx_assembly_=false;
bool host_counter_decode_=false;
std::optional<counters::Snapshot> counters_;
std::uint64_t counter_snapshots_=0;
ap::ApProtocol::EventHandler events_;
RxEventAssembler rx_assembler_;
std::function<void(const wire::Bytes&)> rx_observer_;
rt::Clock::time_point next_beacon_{}, next_ping_{};
std::uint64_t stale_messages_=0;
std::uint64_t tx_completed_=0, rx_frames_=0;
wire::Message make(wire::Kind kind,wire::Bytes payload={}) {
if(tx_sequence_==0xffffffffu) throw std::runtime_error("Packet link sequence exhausted");
return {kind,session_,++tx_sequence_,std::move(payload)};
}
wire::Message transmit(const wire::Bytes& psdu) {
return host_waveform_ ? make(wire::Kind::tx_waveform,waveform::encode(psdu))
: make(wire::Kind::tx_psdu,psdu);
}
void append(std::vector<wire::Message>& out,const std::vector<ap::Outbound>& packets) {
for(const auto& packet:packets) {
// No Windows/UART round trip participates in the SIFS response.
if(!packet.sifs_deadline) out.push_back(transmit(packet.psdu));
}
}
void receive(std::vector<wire::Message>& out,const wire::Bytes& frame) {
++rx_frames_;
if(rx_observer_) rx_observer_(frame);
append(out,protocol_.ingest(frame,-std::numeric_limits<double>::infinity()));
}
public:
PacketApCore(ap::ProtocolConfig config,std::uint64_t session,
ap::ApProtocol::EventHandler events={})
: config_(std::move(config)),protocol_(config_,events),session_(session),events_(std::move(events)) {
if(!session_ || !config_.dsss_1mbps_only) throw std::runtime_error("Invalid E310 host protocol configuration");
}
bool ready() const { return ready_; }
bool host_waveform() const { return host_waveform_; }
bool host_rx_assembly() const { return host_rx_assembly_; }
bool host_counter_decode() const { return host_counter_decode_; }
std::uint64_t counter_snapshots() const { return counter_snapshots_; }
const auto& hardware_counters() const { return counters_; }
std::uint64_t rx_events() const { return rx_assembler_.events(); }
std::uint64_t rx_discarded() const { return rx_assembler_.discarded(); }
void set_rx_observer(std::function<void(const wire::Bytes&)> observer) { rx_observer_=std::move(observer); }
bool initialized() const { return initialized_; }
bool stopped() const { return stopped_; }
std::uint64_t stale_messages() const { return stale_messages_; }
std::uint64_t tx_completed() const { return tx_completed_; }
std::uint64_t rx_frames() const { return rx_frames_; }
std::vector<wire::Message> ingest(const wire::Message& message) {
std::vector<wire::Message> out;
if(message.kind==wire::Kind::hello) {
const std::string identity(message.payload.begin(),message.payload.end());
if(message.session || message.sequence || identity!="GF_E310_PACKET_AGENT_V1")
throw std::runtime_error("Unexpected packet agent identity");
if(ready_) throw std::runtime_error("Packet agent restarted; old WPA2 session must not be reused");
if(!initialized_) {
wire::Bytes configuration(config_.bssid.begin(),config_.bssid.end());
configuration.push_back(static_cast<std::uint8_t>(config_.channel));
configuration.push_back(1); // Long-preamble 1 Mb/s PHY contract.
out.push_back(make(wire::Kind::initialize,std::move(configuration)));
initialized_=true;
}
return out;
}
if(message.session!=session_ || message.sequence<=rx_sequence_) { ++stale_messages_; return out; }
if(!initialized_) throw std::runtime_error("Agent message before hello");
if(message.sequence!=rx_sequence_+1) rx_assembler_.discontinuity();
rx_sequence_=message.sequence;
switch(message.kind) {
case wire::Kind::ready:
if(ready_ || (message.payload.size()!=13 && message.payload.size()!=17 && message.payload.size()!=21 && message.payload.size()!=25) || wire::get(message.payload,0,4)<0x10003u ||
(wire::get(message.payload,0,4)>>16)!=1 || wire::get(message.payload,4,4)!=20000000u ||
wire::get(message.payload,8,4)!=40000000u || message.payload[12]!=config_.channel)
throw std::runtime_error("Packet agent PHY/radio contract mismatch");
if(message.payload.size()>=17) {
const auto capability=wire::get(message.payload,13,4);
if(capability!=waveform::kCapability && !(message.payload.size()>=21 && capability==0))
throw std::runtime_error("Unknown packet agent waveform capability");
host_waveform_=capability==waveform::kCapability;
}
if(message.payload.size()>=21) {
if(wire::get(message.payload,17,4)!=wire::kRxEventCapability)
throw std::runtime_error("Unknown RX event capability");
host_rx_assembly_=true;
}
if(message.payload.size()==25) {
if(wire::get(message.payload,21,4)!=wire::kCounterSnapshotCapability)
throw std::runtime_error("Unknown counter snapshot capability");
host_counter_decode_=true;
}
ready_=true; break;
case wire::Kind::counter_snapshot: {
if(!ready_ || !host_counter_decode_) throw std::runtime_error("Unexpected raw counter snapshot");
const auto snapshot=counters::unpack_snapshot(message.payload);
if(counters_ && counters_->format!=snapshot.format)
throw std::runtime_error("Counter encoding changed within radio session");
counters_=snapshot; ++counter_snapshots_;
if(events_) events_("hardware_counters",counters::snapshot_fields(snapshot));
break;
}
case wire::Kind::rx_psdu:
if(!ready_ || host_rx_assembly_) throw std::runtime_error("Unexpected assembled RX PSDU");
receive(out,message.payload); break;
case wire::Kind::rx_events:
if(!ready_ || !host_rx_assembly_) throw std::runtime_error("Unexpected raw RX events");
rx_assembler_.feed(message.payload,[&](wire::Bytes frame) { receive(out,frame); }); break;
case wire::Kind::tx_done:
if(!ready_ || message.payload.size()!=4) throw std::runtime_error("Invalid TX completion");
++tx_completed_;
break;
case wire::Kind::pong:
if(!ready_ || !message.payload.empty()) throw std::runtime_error("Invalid packet agent pong");
break;
case wire::Kind::stopped: rx_assembler_.discontinuity(); ready_=false; stopped_=true; break;
case wire::Kind::fault: throw std::runtime_error("Radio agent fault: "+std::string(message.payload.begin(),message.payload.end()));
default: throw std::runtime_error("Wrong-direction packet agent message");
}
return out;
}
std::vector<wire::Message> tick(rt::Clock::time_point now) {
std::vector<wire::Message> out;
if(!ready_) return out;
append(out,protocol_.maintenance(now));
if(now>=next_beacon_) {
out.push_back(transmit(protocol_.beacon(static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count()))));
next_beacon_=now+std::chrono::microseconds(1024ULL*config_.beacon_interval_tu);
}
if(now>=next_ping_) { out.push_back(make(wire::Kind::ping)); next_ping_=now+std::chrono::milliseconds(500); }
return out;
}
wire::Message stop() { ready_=false; return make(wire::Kind::stop); }
};
} // namespace gf::e310
wifi_e310_link/host/e310_packet_tx_wait.hpp · 34 lines
Download this file · Permanent section link
#pragma once
#include "e310_sifs_uio.hpp"
namespace gf::e310 {
enum class PacketTxWaitResult { completed, fault, timed_out };
// Status bits, RAM ownership and the completion counter cross clock domains
// independently. Separate MMIO reads are not an atomic hardware snapshot.
// A completion paired with stale busy/ownership bits is pending, not a fault.
// Keep the original deadline: a permanently inconsistent device still fails.
// Counters can be binary or native full-width Gray words: equality/change is
// preserved by the bijection. Never subtract/order these opaque adapter tokens.
template <typename ReadStatus, typename Now, typename Pause>
PacketTxWaitResult wait_packet_tx_completion(
const PacketTxStatus& before,
std::chrono::steady_clock::time_point deadline,
ReadStatus read_status, Now now, Pause pause) {
while (now() < deadline) {
const auto current = read_status();
if (current.rejected_count != before.rejected_count ||
current.error_count != before.error_count || current.config_fault)
return PacketTxWaitResult::fault;
if (current.done_count != before.done_count && !current.inflight &&
!current.busy && current.bytes_written == 0)
return PacketTxWaitResult::completed;
pause();
}
return PacketTxWaitResult::timed_out;
}
} // namespace gf::e310
wifi_e310_link/host/e310_packet_tx_wait_selftest.cpp · 85 lines
Download this file · Permanent section link
#include "e310_packet_tx_wait.hpp"
#include <algorithm>
#include <iostream>
#include <limits>
#include <stdexcept>
using namespace gf::e310;
using namespace std::chrono_literals;
static void check(const char* name, PacketTxStatus before,
const std::vector<PacketTxStatus>& samples,
PacketTxWaitResult expected, std::size_t expected_reads) {
for(bool native_gray:{false,true}) {
auto initial=before;
auto sequence=samples;
if(native_gray) {
const auto encode=[](PacketTxStatus& value) {
value.done_count^=value.done_count>>1;
value.error_count^=value.error_count>>1;
// TX-rejected is bus-domain binary even on GR32 hardware.
};
encode(initial);for(auto& value:sequence)encode(value);
}
auto clock = std::chrono::steady_clock::time_point{};
std::size_t reads = 0;
const auto actual = wait_packet_tx_completion(initial, clock + 200us,
[&] { return sequence.at(std::min(reads++, sequence.size() - 1)); },
[&] { return clock; }, [&] { clock += 50us; });
if (actual != expected || reads != expected_reads)
throw std::runtime_error(name);
}
}
int main() {
try {
PacketTxStatus before;
before.done_count = 17;
auto idle = before;
auto complete = before;
++complete.done_count;
auto mixed = complete;
mixed.inflight = true;
mixed.busy = true;
mixed.bytes_written = 129;
auto ownership = complete;
ownership.inflight = true;
check("completion with old status settles", before,
{mixed, ownership, complete}, PacketTxWaitResult::completed, 3);
check("idle before completion is not success", before,
{idle, idle, complete}, PacketTxWaitResult::completed, 3);
check("permanent mixed state times out", before,
{mixed}, PacketTxWaitResult::timed_out, 4);
check("no completion times out", before,
{idle}, PacketTxWaitResult::timed_out, 4);
auto fault = complete;
++fault.error_count;
check("error wins over completion", before,
{fault}, PacketTxWaitResult::fault, 1);
fault = mixed;
++fault.rejected_count;
check("rejection never treated as settling", before,
{fault}, PacketTxWaitResult::fault, 1);
fault = mixed;
fault.config_fault = true;
check("configuration fault is immediate", before,
{fault}, PacketTxWaitResult::fault, 1);
before.done_count = std::numeric_limits<std::uint32_t>::max();
complete = before;
complete.done_count = 0;
check("counter rollover", before,
{complete}, PacketTxWaitResult::completed, 1);
for(unsigned bit=0;bit<32;++bit) {
before.done_count=(std::uint32_t{1}<<bit)-1;
complete=before;++complete.done_count;
check("full-width carry boundary",before,{complete},PacketTxWaitResult::completed,1);
}
std::cout << "E310_PACKET_TX_WAIT_SELFTEST_PASS cases=80 binary_and_native_gray=true boundary_bits=32 physical_rf=false\n";
return 0;
} catch (const std::exception& error) {
std::cerr << "E310_PACKET_TX_WAIT_SELFTEST_FAIL " << error.what() << '\n';
return 1;
}
}
wifi_e310_link/host/e310_packet_wire.hpp · 123 lines
Download this file · Permanent section link
#pragma once
// Transport-independent Windows <-> E310 PSDU link. This is not a Wi-Fi PHY.
// COBS-delimited packets carry a version, type, session, sequence and CRC32.
// All Wi-Fi PSDUs include their FCS. No credentials belong in the radio agent.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <stdexcept>
#include <utility>
#include <vector>
namespace gf::e310::wire {
using Bytes = std::vector<std::uint8_t>;
constexpr std::size_t kHeader = 24;
constexpr std::size_t kMaxPayload = 4095;
constexpr std::size_t kMaxEncoded = kHeader + kMaxPayload + 32;
constexpr std::uint32_t kRxEventCapability = 0x45563130u; // EV10: byte/first/last in LE16
constexpr std::uint32_t kCounterSnapshotCapability = 0x43523131u; // CR11: eleven native words
constexpr std::size_t kRxBatchEvents = 128;
enum class Kind : std::uint8_t {
hello=1, initialize=2, ready=3, rx_psdu=4, tx_psdu=5,
tx_done=6, ping=7, pong=8, stop=9, stopped=10, fault=11, tx_waveform=12,
rx_events=13, counter_snapshot=14
};
struct Message {
Kind kind=Kind::hello;
std::uint64_t session=0;
std::uint32_t sequence=0;
Bytes payload;
};
inline std::uint32_t crc_byte(std::uint32_t crc, std::uint8_t byte) {
crc ^= byte;
for(int i=0;i<8;++i) crc=(crc>>1)^((crc&1)?0xedb88320u:0u);
return crc;
}
inline std::uint32_t checksum(const Bytes& raw) {
std::uint32_t crc=0xffffffffu;
for(std::size_t i=0;i<raw.size();++i)
if(i<20 || i>=kHeader) crc=crc_byte(crc,raw[i]);
return crc^0xffffffffu;
}
inline void put(Bytes& raw,std::size_t at,std::uint64_t value,std::size_t count) {
for(std::size_t i=0;i<count;++i) raw.at(at+i)=static_cast<std::uint8_t>(value>>(8*i));
}
inline std::uint64_t get(const Bytes& raw,std::size_t at,std::size_t count) {
std::uint64_t value=0;
for(std::size_t i=0;i<count;++i) value|=std::uint64_t(raw.at(at+i))<<(8*i);
return value;
}
inline Bytes encode(const Message& message) {
if(message.payload.size()>kMaxPayload) throw std::runtime_error("PSDU link payload too large");
Bytes raw(kHeader+message.payload.size(),0);
raw[0]='G'; raw[1]='F'; raw[2]='A'; raw[3]='P'; raw[4]=1;
raw[5]=static_cast<std::uint8_t>(message.kind);
put(raw,6,message.payload.size(),2); put(raw,8,message.session,8);
put(raw,16,message.sequence,4);
std::copy(message.payload.begin(),message.payload.end(),raw.begin()+kHeader);
put(raw,20,checksum(raw),4);
Bytes encoded(1,0);
encoded.reserve(kMaxEncoded+1);
std::size_t code_at=0;
std::uint8_t code=1;
for(auto byte:raw) {
if(byte==0) {
encoded[code_at]=code; code_at=encoded.size(); encoded.push_back(0); code=1;
} else {
encoded.push_back(byte);
if(++code==255) {
encoded[code_at]=code; code_at=encoded.size(); encoded.push_back(0); code=1;
}
}
}
encoded[code_at]=code;
encoded.push_back(0);
return encoded;
}
class Decoder {
Bytes encoded_;
bool discard_=false;
std::uint64_t rejected_=0;
bool decode(Message& message) {
Bytes raw;
raw.reserve(kHeader+kMaxPayload);
std::size_t offset=0;
while(offset<encoded_.size()) {
const auto code=encoded_[offset++];
if(code==0 || offset+code-1>encoded_.size()) return false;
for(unsigned i=1;i<code;++i) raw.push_back(encoded_[offset++]);
if(code!=255 && offset<encoded_.size()) raw.push_back(0);
if(raw.size()>kHeader+kMaxPayload) return false;
}
if(raw.size()<kHeader || raw[0]!='G' || raw[1]!='F' || raw[2]!='A' ||
raw[3]!='P' || raw[4]!=1 || raw[5]<1 || raw[5]>14 ||
get(raw,6,2)!=raw.size()-kHeader || get(raw,20,4)!=checksum(raw)) return false;
message.kind=static_cast<Kind>(raw[5]);
message.session=get(raw,8,8);
message.sequence=static_cast<std::uint32_t>(get(raw,16,4));
message.payload.assign(raw.begin()+kHeader,raw.end());
return true;
}
public:
Decoder() { encoded_.reserve(kMaxEncoded); }
std::uint64_t rejected() const { return rejected_; }
template<class Handler> void feed(const std::uint8_t* bytes,std::size_t size,Handler&& handler) {
for(std::size_t i=0;i<size;++i) {
if(bytes[i]==0) {
if(!discard_ && !encoded_.empty()) {
Message message;
const bool valid=decode(message);
encoded_.clear(); // A handler exception must not poison the next frame.
if(valid) handler(std::move(message)); else ++rejected_;
}
encoded_.clear(); discard_=false;
} else if(!discard_) {
if(encoded_.size()==kMaxEncoded) { ++rejected_; encoded_.clear(); discard_=true; }
else encoded_.push_back(bytes[i]);
}
}
}
};
} // namespace gf::e310::wire
wifi_e310_link/host/e310_packet_wire_selftest.cpp · 48 lines
Download this file · Permanent section link
#include "e310_packet_wire.hpp"
#include <iostream>
int main() {
using namespace gf::e310::wire;
try {
for(auto size:{std::size_t(0),std::size_t(1),std::size_t(254),std::size_t(255),kMaxPayload}) {
for(unsigned pattern=0;pattern<3;++pattern) {
Message original{Kind::rx_psdu,0x0123456789abcdefULL,123,{}};
for(std::size_t i=0;i<size;++i)
original.payload.push_back(static_cast<std::uint8_t>(pattern==0?0:pattern==1?255:i));
const auto encoded=encode(original);
Decoder decoder;
unsigned delivered=0;
const auto receive=[&](Message got) {
if(got.kind!=original.kind || got.session!=original.session ||
got.sequence!=original.sequence || got.payload!=original.payload)
throw std::runtime_error("Wire roundtrip mismatch");
++delivered;
};
for(auto byte:encoded) decoder.feed(&byte,1,receive);
if(delivered!=1 || decoder.rejected()) throw std::runtime_error("Fragmented wire packet lost");
auto corrupt=encoded;
corrupt[corrupt.size()/2]^=0x40;
decoder.feed(corrupt.data(),corrupt.size(),receive);
decoder.feed(encoded.data(),encoded.size(),receive);
if(delivered!=2 || decoder.rejected()==0) throw std::runtime_error("Corrupt packet accepted or failed to resynchronize");
Bytes oversized(kMaxEncoded+50,0x31); oversized.push_back(0);
decoder.feed(oversized.data(),oversized.size(),receive);
decoder.feed(encoded.data(),encoded.size(),receive);
if(delivered!=3 || decoder.rejected()<2) throw std::runtime_error("Oversize resynchronization failed");
}
}
bool rejected=false;
try { encode({Kind::tx_psdu,1,1,Bytes(kMaxPayload+1)}); } catch(const std::exception&) { rejected=true; }
if(!rejected) throw std::runtime_error("Oversize transmit accepted");
Decoder after_exception;
auto packet=encode({Kind::ping,1,1,{}});
try {
after_exception.feed(packet.data(),packet.size(),[](Message) { throw std::runtime_error("handler test"); });
} catch(const std::exception&) {}
unsigned recovered=0;
after_exception.feed(packet.data(),packet.size(),[&](Message) { ++recovered; });
if(recovered!=1 || after_exception.rejected()) throw std::runtime_error("Callback exception poisoned framing");
std::cout<<"E310_PACKET_WIRE_SELFTEST_PASS fragmented=true crc_rejection=true resync=true bounded=true physical_rf=false\n";
return 0;
} catch(const std::exception& error) { std::cerr<<error.what()<<'\n'; return 1; }
}
wifi_e310_link/host/e310_recovery_guard.cpp · 244 lines
Download this file · Permanent section link
// SPDX-License-Identifier: MIT
// Native C++14 recovery guard for the observed E310 Linux 3.14 image.
// Own /dev/watchdog exclusively. Its old device tree requests IRQ-only mode;
// change only the live PS watchdog MODE, never the boot DT, clock or image.
// Register contract: Xilinx linux-xlnx xilinx-v2014.4 cadence_wdt.c and UG585.
#include <cerrno>
#include <chrono>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <linux/watchdog.h>
#include <stdexcept>
#include <string>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
namespace {
volatile sig_atomic_t interrupted = 0;
void signal_handler(int) { interrupted = 1; }
void barrier() { __sync_synchronize(); }
std::runtime_error error(const char* what) {
return std::runtime_error(std::string(what) + ": " + std::strerror(errno));
}
class PsWatchdog {
int mem_ = -1, fd_ = -1;
volatile uint32_t* regs_ = nullptr;
public:
PsWatchdog() {
// This is a PS peripheral, independent of GP0 and the radio clock.
std::ifstream compatible("/proc/device-tree/amba@0/ps7-wdt@f8005000/compatible");
std::string name;
std::getline(compatible, name, '\0');
if (name != "xlnx,zynq-wdt-r1p2")
throw std::runtime_error("Unexpected watchdog device-tree identity");
mem_ = open("/dev/mem", O_RDWR | O_SYNC | O_CLOEXEC);
if (mem_ < 0) throw error("open PS /dev/mem");
void* mapping = mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, mem_, 0xf8005000);
if (mapping == MAP_FAILED) {
close(mem_);
throw error("map PS watchdog");
}
regs_ = static_cast<volatile uint32_t*>(mapping);
}
~PsWatchdog() {
// No magic close on an unexpected exception: retain reset fallback.
if (fd_ >= 0) close(fd_);
if (regs_) munmap(const_cast<uint32_t*>(regs_), 4096);
if (mem_ >= 0) close(mem_);
}
uint32_t mode() const { barrier(); auto value = regs_[0]; barrier(); return value; }
void start(int seconds) {
if (mode() & 1u) throw std::runtime_error("Watchdog already enabled; not taking over");
fd_ = open("/dev/watchdog", O_WRONLY | O_CLOEXEC);
if (fd_ < 0) throw error("exclusive watchdog open");
try {
watchdog_info info = {};
if (ioctl(fd_, WDIOC_GETSUPPORT, &info) < 0) throw error("watchdog identity");
if (std::string(reinterpret_cast<char*>(info.identity)) != "cdns_wdt watchdog")
throw std::runtime_error("Unexpected Linux watchdog driver");
if (ioctl(fd_, WDIOC_SETTIMEOUT, &seconds) < 0) throw error("watchdog timeout");
// The stock driver selected IRQ-only because DT reset=<0>.
// Disable before changing MODE. Leave CONTROL/clock untouched.
regs_[0] = 0x00abc000; barrier();
regs_[0] = 0x00abc033; barrier(); // WDEN + RSTEN, no IRQ, 16-cycle pulse
ping();
if ((mode() & 7u) != 3u) throw std::runtime_error("Watchdog reset mode readback failed");
std::cout << "E310_RECOVERY_WATCHDOG_ARMED timeout=" << seconds
<< " mode=0x" << std::hex << mode() << std::dec
<< " peripheral=PS reset_output=true" << std::endl;
} catch (...) {
stop();
throw;
}
}
void ping() {
if (ioctl(fd_, WDIOC_KEEPALIVE, nullptr) < 0) throw error("watchdog keepalive");
}
void stop() {
if (fd_ < 0) return;
int options = WDIOS_DISABLECARD;
if (ioctl(fd_, WDIOC_SETOPTIONS, &options) < 0) throw error("watchdog disable");
if (write(fd_, "V", 1) != 1) throw error("watchdog magic close");
close(fd_); fd_ = -1;
if (mode() & 1u) throw std::runtime_error("Watchdog did not disable");
std::cout << "E310_RECOVERY_WATCHDOG_STOPPED enabled=false" << std::endl;
}
};
int expire_and_wait(PsWatchdog& watchdog, int timeout) {
std::cout << "E310_WATCHDOG_EXPIRY_REQUESTED reset_is_hardware_not_reboot_command=true"
<< std::endl;
// fsync is meaningful for retained regular-file evidence, not a pipe or
// UART. In particular, piping this command must not cancel the test.
struct stat output_status = {};
if (fstat(STDOUT_FILENO, &output_status) < 0) throw error("inspect evidence output");
if (S_ISREG(output_status.st_mode) && fsync(STDOUT_FILENO) < 0)
throw error("persist watchdog expiry evidence");
// No ioctl, ping, reboot syscall or shell reboot occurs in this interval.
const auto started = std::chrono::steady_clock::now();
const auto end = started + std::chrono::seconds(timeout + 15);
auto report_at = started + std::chrono::seconds(1);
while (std::chrono::steady_clock::now() < end) {
const auto now = std::chrono::steady_clock::now();
if (now >= report_at) {
std::cout << "E310_WATCHDOG_EXPIRY_WAIT elapsed_ms="
<< std::chrono::duration_cast<std::chrono::milliseconds>(now - started).count()
<< " keepalive_sent=false" << std::endl;
report_at = now + std::chrono::seconds(1);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
watchdog.stop();
std::cerr << "E310_WATCHDOG_RESET_FAILED system_still_running=true\n";
return 1;
}
}
int main(int argc, char** argv) {
try {
if (argc < 2) throw std::runtime_error("Use --probe, --self-test, --reset-test, --guard, or --guard-reset-test DEADLINE -- COMMAND...");
const std::string mode(argv[1]);
if (mode != "--probe" && mode != "--self-test" && mode != "--reset-test" && mode != "--guard" && mode != "--guard-reset-test" && mode != "--service")
throw std::runtime_error("Unknown recovery mode");
int deadline = 0;
const bool continuous = mode == "--service";
std::string progress_path;
if (continuous) {
if (argc < 5 || std::string(argv[3]) != "--")
throw std::runtime_error("Use --service NEW_PROGRESS_LOG -- COMMAND ARG...");
progress_path = argv[2];
struct stat previous = {};
if (lstat(progress_path.c_str(), &previous) == 0 || errno != ENOENT)
throw std::runtime_error("Continuous guard requires a new, absent progress log");
} else if (mode == "--guard" || mode == "--guard-reset-test") {
if (argc < 5 || std::string(argv[3]) != "--")
throw std::runtime_error("Use --guard DEADLINE -- COMMAND ARG...");
deadline = std::stoi(argv[2]);
if (deadline < 5 || deadline > 600) throw std::runtime_error("Diagnostic deadline must be 5..600 seconds");
} else if (argc != 2) throw std::runtime_error("Unexpected recovery arguments");
PsWatchdog watchdog;
std::cout << "E310_RECOVERY_PROBE mode=0x" << std::hex << watchdog.mode()
<< std::dec << " watchdog_opened=false rf_arm_performed=false rf_state_not_inspected=true" << std::endl;
if (mode == "--probe") return 0;
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
// Flush project evidence before enabling a reset fallback, not after
// a potential bus stall. No board boot files are modified here.
sync();
const int timeout = 10;
watchdog.start(timeout);
if (mode == "--reset-test") return expire_and_wait(watchdog, timeout);
if (mode == "--self-test") {
for (int i = 0; i < 3 && !interrupted; ++i) { sleep(1); watchdog.ping(); }
watchdog.stop();
std::cout << "E310_WATCHDOG_START_PING_STOP_PASS reset_tested=false" << std::endl;
return 0;
}
const pid_t child = fork();
if (child < 0) { watchdog.stop(); throw error("fork guarded command"); }
if (child == 0) {
setpgid(0, 0);
setenv("GF_E310_RECOVERY_GUARD", "1", 1);
execvp(argv[4], argv + 4);
_exit(127);
}
setpgid(child, child);
const auto started = std::chrono::steady_clock::now();
auto next_report = started;
auto progress_deadline = started + std::chrono::seconds(90);
off_t progress_size = 0;
std::cout << "E310_GUARD_CHILD_STARTED pid=" << child
<< " deadline_seconds=" << deadline
<< " continuous=" << (continuous ? "true" : "false") << std::endl;
int status = 0;
while (!interrupted && (continuous || std::chrono::steady_clock::now() - started < std::chrono::seconds(deadline))) {
const pid_t waited = waitpid(child, &status, WNOHANG);
if (waited == child) {
// Flush before reading another hardware register. The last
// custom-image failure stopped between child cleanup and the
// old combined mode-read/expiry line; do not hide that edge.
std::cout << "E310_GUARD_CHILD_EXIT wait_status=" << status << std::endl;
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
if (mode == "--guard-reset-test") {
std::cout << "E310_CUSTOM_IMAGE_MODE_READ_BEGIN" << std::endl;
std::cout << "E310_CUSTOM_IMAGE_EXPIRY_TEST mode=0x"
<< std::hex << watchdog.mode() << std::dec << std::endl;
return expire_and_wait(watchdog, timeout);
}
watchdog.stop();
std::cout << "E310_GUARDED_COMMAND_PASS" << std::endl;
return 0;
}
break;
}
if (waited < 0 && errno != EINTR) break;
const auto now = std::chrono::steady_clock::now();
if (continuous) {
// AP emits a beacon log record only after its real MMIO TX
// completes. Require that file to keep advancing; merely
// keeping this watchdog process alive is not service health.
struct stat progress = {};
if (lstat(progress_path.c_str(), &progress) == 0) {
if (!S_ISREG(progress.st_mode) || progress.st_size < progress_size) break;
if (progress.st_size > progress_size) {
progress_size = progress.st_size;
progress_deadline = now + std::chrono::seconds(10);
}
} else if (errno != ENOENT) break;
if (now >= progress_deadline) {
std::cout << "E310_SERVICE_PROGRESS_TIMEOUT last_bytes=" << progress_size << std::endl;
break;
}
}
const bool report = now >= next_report;
if (report)
std::cout << "E310_GUARD_KEEPALIVE_BEGIN elapsed_ms="
<< std::chrono::duration_cast<std::chrono::milliseconds>(now - started).count()
<< std::endl;
watchdog.ping();
if (report) {
std::cout << "E310_GUARD_KEEPALIVE_OK" << std::endl;
next_report = now + std::chrono::seconds(5);
}
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
// Give the child its normal RF-off/stock-restore cleanup opportunity.
kill(-child, SIGTERM);
std::cout << "E310_GUARDED_COMMAND_FAULT recovery=hardware_watchdog" << std::endl;
return expire_and_wait(watchdog, timeout);
} catch (const std::exception& ex) {
std::cerr << "fatal: " << ex.what() << '\n';
return 1;
}
}
wifi_e310_link/host/e310_rf_preset.cpp · 264 lines
Download this file · Permanent section link
// One-shot AD9361 setup while the stock E310 image is still active.
// This process never starts RX and never submits a TX sample. no_reload_fpga
// keeps MPM from replacing the configured full image with its idle image when
// the session closes; the open PL shell is loaded only after this tool exits.
#include <uhd/types/device_addr.hpp>
#include <uhd/types/tune_request.hpp>
#include <uhd/usrp/multi_usrp.hpp>
#include <uhd/version.hpp>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include <chrono>
#include <thread>
#include <dirent.h>
#include <unistd.h>
namespace {
constexpr double kSampleRate = 20'000'000.0;
constexpr double kBandwidth = 20'000'000.0;
struct Options {
bool prepare = false;
bool self_test = false;
std::string uhd_args =
"type=e3xx,master_clock_rate=20e6,no_reload_fpga=1";
int channel = 6;
double rx_gain_db = 30.0;
double tx_gain_db = 0.0;
std::string rx_antenna = "RX2";
std::string tx_antenna = "TX/RX";
int legacy_keep_seconds = 0;
};
std::string normalized_uhd_args(const std::string& text) {
uhd::device_addr_t args(text);
if (args.has_key("no_reload_fpga")) {
auto value = args["no_reload_fpga"];
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
if (!value.empty() && value != "1" && value != "true" &&
value != "yes" && value != "t") {
throw std::runtime_error(
"no_reload_fpga must be true; refusing a teardown-capable session");
}
}
// Rewrite even an accepted spelling to one unambiguous MPM value.
args["no_reload_fpga"] = "1";
return args.to_string();
}
Options parse_options(int argc, char** argv) {
Options options;
auto next = [&](int& index, const char* name) -> std::string {
if (++index >= argc)
throw std::runtime_error(std::string("missing value for ") + name);
return argv[index];
};
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--prepare") options.prepare = true;
else if (argument == "--self-test") options.self_test = true;
else if (argument == "--uhd-args")
options.uhd_args = next(index, "--uhd-args");
else if (argument == "--channel")
options.channel = std::stoi(next(index, "--channel"));
else if (argument == "--rx-gain")
options.rx_gain_db = std::stod(next(index, "--rx-gain"));
else if (argument == "--tx-gain")
options.tx_gain_db = std::stod(next(index, "--tx-gain"));
else if (argument == "--rx-antenna")
options.rx_antenna = next(index, "--rx-antenna");
else if (argument == "--tx-antenna")
options.tx_antenna = next(index, "--tx-antenna");
else if (argument == "--legacy-keep-seconds")
options.legacy_keep_seconds = std::stoi(next(index, "--legacy-keep-seconds"));
else if (argument == "--help" || argument == "-h") {
std::cout
<< "gf_e310_rf_preset --self-test\n"
"gf_e310_rf_preset --prepare [--channel 1..11] "
"[--rx-gain DB] [--tx-gain DB] "
"[--rx-antenna NAME] [--tx-antenna NAME] "
"[--uhd-args ARGS]\n"
"--legacy-keep-seconds N retains the legacy AXI descriptor after UHD teardown; -1 holds until terminated.\n"
"Configures the stock AD9361 for the later open shell; "
"sends zero samples.\n";
std::exit(0);
} else {
throw std::runtime_error("unknown argument: " + argument);
}
}
if (options.prepare == options.self_test)
throw std::runtime_error(
"choose exactly one of --prepare or --self-test");
if (options.channel < 1 || options.channel > 11)
throw std::runtime_error("--channel must be in 1..11");
if (!std::isfinite(options.rx_gain_db) || !std::isfinite(options.tx_gain_db))
throw std::runtime_error("RF gains must be finite");
if (options.legacy_keep_seconds < -1 || options.legacy_keep_seconds > 300)
throw std::runtime_error("legacy keep duration must be -1 or 0..300 seconds");
options.uhd_args = normalized_uhd_args(options.uhd_args);
return options;
}
bool contains(const std::vector<std::string>& values,
const std::string& requested) {
return std::find(values.begin(), values.end(), requested) != values.end();
}
void run_self_test() {
for (const auto* value : {
"type=e3xx",
"type=e3xx,no_reload_fpga=1",
"type=e3xx,no_reload_fpga=TRUE",
"type=e3xx,no_reload_fpga=yes",
"type=e3xx,no_reload_fpga="}) {
const uhd::device_addr_t normalized(normalized_uhd_args(value));
if (!normalized.has_key("no_reload_fpga") ||
normalized["no_reload_fpga"] != "1") {
throw std::runtime_error("safe no_reload_fpga normalization failed");
}
}
for (const auto* value : {
"type=e3xx,no_reload_fpga=0",
"type=e3xx,no_reload_fpga=false",
"type=e3xx,no_reload_fpga=no"}) {
bool rejected = false;
try {
(void)normalized_uhd_args(value);
} catch (const std::runtime_error&) {
rejected = true;
}
if (!rejected)
throw std::runtime_error("unsafe no_reload_fpga value was accepted");
}
std::cout << "E310_RF_PRESET_SELFTEST_PASS tx_samples=0 "
"no_reload_fpga=forced_true hardware_access=false\n";
}
} // namespace
int main(int argc, char** argv) {
try {
const auto options = parse_options(argc, argv);
if (options.self_test) {
run_self_test();
return 0;
}
const auto center_hz =
static_cast<double>(2407 + 5 * options.channel) * 1e6;
auto usrp = uhd::usrp::multi_usrp::make(options.uhd_args);
if (std::abs(usrp->get_master_clock_rate() - kSampleRate) > 1.0)
throw std::runtime_error("E310 did not enter exact 20 MHz clock mode");
if (usrp->get_tx_num_channels() < 2 ||
usrp->get_rx_num_channels() < 2) {
throw std::runtime_error("E310 does not expose two RF channels");
}
for (std::size_t channel = 0; channel < 2; ++channel) {
const auto tx_range = usrp->get_tx_gain_range(channel);
const auto rx_range = usrp->get_rx_gain_range(channel);
if (options.tx_gain_db < tx_range.start() ||
options.tx_gain_db > tx_range.stop() ||
options.rx_gain_db < rx_range.start() ||
options.rx_gain_db > rx_range.stop()) {
throw std::runtime_error("requested RF gain is outside the device range");
}
if (!contains(usrp->get_rx_antennas(channel), options.rx_antenna))
throw std::runtime_error("requested RX antenna is unavailable");
if (!contains(usrp->get_tx_antennas(channel), options.tx_antenna))
throw std::runtime_error("requested TX antenna is unavailable");
usrp->set_rx_rate(kSampleRate, channel);
usrp->set_tx_rate(kSampleRate, channel);
usrp->set_rx_bandwidth(kBandwidth, channel);
usrp->set_tx_bandwidth(kBandwidth, channel);
usrp->set_rx_gain(options.rx_gain_db, channel);
usrp->set_tx_gain(options.tx_gain_db, channel);
const auto actual_tx_gain = usrp->get_tx_gain(channel);
const auto actual_rx_gain = usrp->get_rx_gain(channel);
if (std::abs(actual_tx_gain - options.tx_gain_db) > 0.126 ||
std::abs(actual_rx_gain - options.rx_gain_db) > 0.51) {
throw std::runtime_error("RF gain readback does not match the requested setting");
}
std::cout << "E310_GAIN_READBACK logical_channel=" << channel
<< " tx_gain_db=" << actual_tx_gain
<< " rx_gain_db=" << actual_rx_gain
<< " tx_gain_min_db=" << tx_range.start()
<< " tx_gain_max_db=" << tx_range.stop()
<< " calibrated_output_power=false\n";
usrp->set_rx_antenna(options.rx_antenna, channel);
usrp->set_tx_antenna(options.tx_antenna, channel);
usrp->set_rx_freq(uhd::tune_request_t(center_hz), channel);
usrp->set_tx_freq(uhd::tune_request_t(center_hz), channel);
}
// Connecting both directions and both channels makes UHD request the
// AD9361 2R2T timing mode. No stream command and no send() follows.
uhd::stream_args_t stream_args("sc16", "sc16");
stream_args.channels = {0, 1};
auto tx_stream = usrp->get_tx_stream(stream_args);
auto rx_stream = usrp->get_rx_stream(stream_args);
if (!tx_stream || !rx_stream || tx_stream->get_num_channels() != 2 ||
rx_stream->get_num_channels() != 2) {
throw std::runtime_error("E310 could not establish 2R2T topology");
}
for (std::size_t channel = 0; channel < 2; ++channel) {
if (std::abs(usrp->get_rx_rate(channel) - kSampleRate) > 1.0 ||
std::abs(usrp->get_tx_rate(channel) - kSampleRate) > 1.0) {
throw std::runtime_error("E310 rate readback is not 20 MS/s");
}
}
std::cout << "E310_RF_PRESET_PASS channel=" << options.channel
<< " center_hz=" << static_cast<unsigned long long>(center_hz)
<< " channels=2 sample_rate=20000000 tx_samples=0"
" no_reload_fpga=true uhd_version="
<< uhd::get_version_string() << '\n';
if (options.legacy_keep_seconds != 0) {
// The legacy driver is exclusive-open. dup() retains that same
// open file description without reopening or retaining UHD tasks.
DIR* descriptors = opendir("/proc/self/fd");
if (!descriptors) throw std::runtime_error("cannot inspect own AXI descriptor");
int keep_fd = -1;
while (const dirent* entry = readdir(descriptors)) {
const std::string path = std::string("/proc/self/fd/") + entry->d_name;
char target[256] = {};
const auto count = readlink(path.c_str(), target, sizeof(target) - 1);
if (count > 0 && std::string(target, static_cast<std::size_t>(count)) == "/dev/axi_fpga") {
keep_fd = dup(std::stoi(entry->d_name));
break;
}
}
closedir(descriptors);
if (keep_fd < 0) throw std::runtime_error("legacy AXI descriptor not found");
rx_stream.reset();
tx_stream.reset();
usrp.reset();
std::cout << "E310_LEGACY_HANDOFF_READY uhd_destroyed=true kept_axi_fd="
<< keep_fd << " seconds=" << options.legacy_keep_seconds << std::endl;
if (options.legacy_keep_seconds == -1) {
// No UHD task remains. Keep only the exclusive-open descriptor;
// the supervising launcher restores stock before terminating us.
for (;;) pause();
}
std::this_thread::sleep_for(std::chrono::seconds(options.legacy_keep_seconds));
close(keep_fd);
std::cout << "E310_LEGACY_HANDOFF_CLOSED" << std::endl;
}
return 0;
} catch (const std::exception& error) {
std::cerr << "fatal: " << error.what() << '\n';
return 1;
}
}
wifi_e310_link/host/e310_rx_capture.cpp · 122 lines
Download this file · Permanent section link
// Read actual FPGA-retained ADC samples. Does not arm or reconfigure RF.
#include "e310_counter_format.hpp"
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <stdexcept>
#include <string>
#include <sys/mman.h>
#include <unistd.h>
#include <vector>
class CaptureRegisters {
int fd_ = -1;
volatile std::uint32_t* regs_ = nullptr;
bool counters_gray_ = false;
bool software_peaks_ = false;
public:
CaptureRegisters() {
fd_ = open("/dev/mem", O_RDWR | O_SYNC);
if (fd_ < 0) throw std::runtime_error("Cannot open /dev/mem");
void* mapping = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0x40010000);
if (mapping == MAP_FAILED) {
close(fd_); fd_ = -1;
throw std::runtime_error("Cannot map RX diagnostic registers");
}
regs_ = static_cast<volatile std::uint32_t*>(mapping);
}
~CaptureRegisters() { if(regs_) munmap(const_cast<std::uint32_t*>(regs_),4096); if(fd_>=0) close(fd_); }
std::uint32_t read(unsigned address) const {
__sync_synchronize(); const auto value = regs_[address/4]; __sync_synchronize();
return gf::e310::counters::normalize(counters_gray_,address,value);
}
void write(unsigned address, std::uint32_t value) {
regs_[address/4] = value; __sync_synchronize();
}
void validate() {
if (read(0x200)!=0x47464531 || (read(0x22c)!=0x10002 && read(0x22c)!=0x10003))
throw std::runtime_error("RX diagnostic image v1.2/v1.3 required; no write performed");
const auto format=read(gf::e310::counters::capability_address);
if(!gf::e310::counters::supported(format))
throw std::runtime_error("Unknown counter format; no write performed");
counters_gray_=format==gf::e310::counters::gray32;
const auto peak_format=read(0x284);
if(peak_format!=0 && peak_format!=0xdead0284u && peak_format!=0x504b5357u)
throw std::runtime_error("Unknown capture peak format; no write performed");
software_peaks_=peak_format==0x504b5357u;
}
bool software_peaks() const { return software_peaks_; }
void status() const {
std::cout << "E310_RX_DIAGNOSTIC status=0x" << std::hex << read(0x250);
if(software_peaks_) std::cout << " post_trigger_peaks_qi=not_in_fpga peak_execution=Windows_C++";
else std::cout << " post_trigger_peaks_qi=0x" << read(0x270);
std::cout << std::dec << " sample_strobes=" << read(0x26c) << " sfd=" << read(0x260)
<< " plcp_ok=" << read(0x264) << " plcp_error=" << read(0x268)
<< " rf_arm_performed=false" << std::endl;
}
};
int main(int argc, char** argv) {
try {
if(argc<2 || argc>3) throw std::runtime_error("Use status | arm THRESHOLD_IQ16 | arm-sfd | dump NEW_FILE");
const std::string mode(argv[1]);
if (mode!="status" && mode!="arm" && mode!="arm-sfd" && mode!="dump") throw std::runtime_error("Unknown mode");
if ((mode=="status" || mode=="arm-sfd") != (argc==2)) throw std::runtime_error("Invalid argument count");
CaptureRegisters registers;
registers.validate();
registers.status();
if(mode=="arm" || mode=="arm-sfd") {
std::size_t consumed=0;
const auto threshold=mode=="arm-sfd" ? 0 : std::stoul(argv[2],&consumed);
if(mode=="arm" && (consumed!=std::strlen(argv[2]) || threshold>32768)) throw std::runtime_error("Invalid IQ16 threshold");
if(mode=="arm-sfd" && registers.read(0x22c)!=0x10003) throw std::runtime_error("SFD capture requires v1.3");
if(registers.read(0x22c)==0x10003) registers.write(0x278,mode=="arm-sfd" ? 1 : 0);
registers.write(0x254,threshold);
if(registers.read(0x254)!=threshold) throw std::runtime_error("Threshold readback mismatch");
registers.write(0x250,0x52584341);
usleep(1000);
if(!(registers.read(0x250)&7)) throw std::runtime_error("Capture did not acknowledge ARM");
std::cout << "E310_RX_CAPTURE_ARMED mode=" << mode << " threshold_iq16=" << threshold << " rf_arm_performed=false" << std::endl;
} else if(mode=="dump") {
const auto status=registers.read(0x250);
if(!(status&4)) throw std::runtime_error("Capture incomplete; no file created");
const unsigned bits=(status>>8)&255;
if(bits!=14) throw std::runtime_error("Unexpected capture capacity");
std::vector<std::uint32_t> samples(1u<<bits);
for(unsigned n=0;n<samples.size();++n) {
registers.write(0x258,n);
if(registers.read(0x258)!=n) throw std::runtime_error("Capture address mismatch");
samples[n]=registers.read(0x25c);
}
if(registers.read(0x250)!=status) throw std::runtime_error("Capture changed during read");
if(registers.software_peaks())
std::cout << "E310_RX_CAPTURE_METADATA peak_execution=Windows_C++ pretrigger_samples="
<< ((status&8u) ? samples.size()/4 : 0) << " total_samples=" << samples.size() << std::endl;
int file=open(argv[2],O_CREAT|O_EXCL|O_WRONLY,0600);
if(file<0) throw std::runtime_error(std::string("Cannot create new capture: ")+strerror(errno));
std::size_t offset=0;
const auto* data=reinterpret_cast<const char*>(samples.data());
const auto bytes=samples.size()*sizeof(samples[0]);
bool success=true;
while(offset<bytes) {
const auto count=write(file,data+offset,bytes-offset);
if(count<0 && errno==EINTR) continue;
if(count<=0) {success=false;break;}
offset+=count;
}
if(fsync(file)<0) success=false;
if(close(file)<0) success=false;
if(!success) throw std::runtime_error("Capture file write failed; partial file retained");
std::cout << "E310_RX_CAPTURE_RETAINED path=" << argv[2] << " samples=" << samples.size()
<< " bytes=" << bytes << " format=interleaved_i16le_q16le rate=20000000"
<< " resampled=false reconstructed=false" << std::endl;
}
return 0;
} catch(const std::exception& error) {
std::cerr << "fatal: " << error.what() << std::endl;
return 1;
}
}
wifi_e310_link/host/e310_rx_event_selftest.cpp · 59 lines
Download this file · Permanent section link
#include "e310_rx_events.hpp"
#include <iostream>
using namespace gf::e310;
static void require(bool ok) { if(!ok) throw std::runtime_error("RX event assembly test failed"); }
static wire::Bytes events(const wire::Bytes& frame) {
wire::Bytes out;
for(std::size_t i=0;i<frame.size();++i) {
out.push_back(frame[i]); out.push_back(static_cast<std::uint8_t>((i==0?1:0)|(i+1==frame.size()?2:0)));
}
return out;
}
int main() {
try {
std::uint64_t frames=0;
for(std::size_t length=1;length<=wire::kMaxPayload;++length) {
wire::Bytes frame(length); for(std::size_t i=0;i<length;++i) frame[i]=static_cast<std::uint8_t>(i*73+length);
const auto raw=events(frame);
for(std::size_t batch_events:{1u,2u,7u,31u,127u,128u}) {
RxEventAssembler assembler; unsigned complete=0;
for(std::size_t at=0;at<raw.size();at+=2*batch_events) {
const auto end=std::min(raw.size(),at+2*batch_events);
assembler.feed(wire::Bytes(raw.begin()+at,raw.begin()+end),[&](wire::Bytes got) { require(got==frame); ++complete; });
}
require(complete==1 && assembler.events()==length && assembler.discarded()==0); ++frames;
}
}
// Capacity, orphan bytes, aborted frames, and explicit link discontinuity.
RxEventAssembler assembler; std::vector<wire::Bytes> output;
const auto deliver=[&](wire::Bytes got) { output.push_back(std::move(got)); };
assembler.feed({1,0,2,2},deliver); require(output.empty() && assembler.discarded()==2);
assembler.feed({3,1},deliver); assembler.discontinuity();
assembler.feed({4,2},deliver); require(output.empty() && assembler.discarded()==4);
assembler.feed({5,1,6,1,7,2,8,3},deliver);
require(output==std::vector<wire::Bytes>{{6,7},{8}} && assembler.discarded()==5);
auto oversized=events(wire::Bytes(wire::kMaxPayload+1,0x42));
for(std::size_t at=0;at<oversized.size();at+=256)
assembler.feed(wire::Bytes(oversized.begin()+at,oversized.begin()+std::min(oversized.size(),at+256)),deliver);
require(output.size()==2 && assembler.discarded()==6);
for(const auto& bad:std::vector<wire::Bytes>{{},{1},{1,4},{1,3,2,0x80},wire::Bytes(258)}) {
bool rejected=false;
try { assembler.feed(bad,deliver); } catch(const std::exception&) { rejected=true; }
require(rejected && output.size()==2); // validate whole batch before delivery
}
// Real COBS/CRC transport fragmentation around multiple frame boundaries.
wire::Decoder decoder; unsigned delivered=0;
const auto encoded=wire::encode({wire::Kind::rx_events,19,2,{0,3,255,1,0,2}});
RxEventAssembler transported;
for(auto b:encoded) decoder.feed(&b,1,[&](wire::Message msg) {
require(msg.kind==wire::Kind::rx_events);
transported.feed(msg.payload,[&](wire::Bytes got) {
require(got==(delivered==0?wire::Bytes{0}:wire::Bytes{255,0})); ++delivered;
});
});
require(delivered==2 && decoder.rejected()==0);
std::cout<<"E310_RX_EVENT_SELFTEST_PASS frames="<<frames<<" lengths=1..4095 batch_sizes=6 interrupted=true oversized=true malformed=true fragmented_wire=true physical_rf=false\n";
return 0;
} catch(const std::exception& error) { std::cerr<<error.what()<<'\n'; return 1; }
}
wifi_e310_link/host/e310_rx_events.hpp · 40 lines
Download this file · Permanent section link
#pragma once
// Windows-side frame assembly. The E310 relays bounded batches of raw FIFO
// events; it no longer accumulates/interprets an entire received PSDU.
#include "e310_packet_wire.hpp"
namespace gf::e310 {
class RxEventAssembler {
wire::Bytes frame_;
bool receiving_=false;
std::uint64_t discarded_=0, events_=0;
public:
std::uint64_t discarded() const noexcept { return discarded_; }
std::uint64_t events() const noexcept { return events_; }
void discontinuity() {
if(receiving_) ++discarded_;
frame_.clear(); receiving_=false;
}
template<class Handler> void feed(const wire::Bytes& batch,Handler&& deliver) {
if(batch.empty() || batch.size()%2 || batch.size()>2*wire::kRxBatchEvents)
throw std::runtime_error("Invalid RX FIFO event batch length");
// Validate the whole batch before any frame can reach the protocol.
for(std::size_t i=1;i<batch.size();i+=2)
if(batch[i]&0xfcu) throw std::runtime_error("Reserved RX FIFO event bits");
for(std::size_t i=0;i<batch.size();i+=2) {
++events_;
const bool first=(batch[i+1]&1u)!=0, last=(batch[i+1]&2u)!=0;
if(first) { discontinuity(); receiving_=true; }
if(!receiving_) { ++discarded_; continue; }
if(frame_.size()==wire::kMaxPayload) { discontinuity(); continue; }
frame_.push_back(batch[i]);
if(last) {
receiving_=false;
auto complete=std::move(frame_); frame_.clear();
deliver(std::move(complete));
}
}
}
};
} // namespace gf::e310
wifi_e310_link/host/e310_rx_pcap.hpp · 33 lines
Download this file · Permanent section link
#pragma once
#include "e310_packet_wire.hpp"
namespace gf::e310 {
// Received FPGA PSDUs only. Timestamp is Windows receipt time, NOT RF time.
// Radiotap contains just the FCS-presence/result flag: no invented RSSI/TSFT.
inline wire::Bytes rx_pcap_header() {
wire::Bytes header(24, 0);
wire::put(header, 0, 0xa1b2c3d4u, 4);
wire::put(header, 4, 2, 2); wire::put(header, 6, 4, 2);
wire::put(header, 16, 65535, 4);
wire::put(header, 20, 127, 4); // LINKTYPE_IEEE802_11_RADIOTAP
return header;
}
inline wire::Bytes rx_pcap_record(const wire::Bytes& psdu, std::uint64_t unix_us) {
if (psdu.size() > 4095) throw std::runtime_error("RX capture PSDU exceeds wire limit");
const auto length = static_cast<std::uint32_t>(psdu.size() + 9);
wire::Bytes record(16 + length, 0);
wire::put(record, 0, unix_us / 1000000, 4);
wire::put(record, 4, unix_us % 1000000, 4);
wire::put(record, 8, length, 4); wire::put(record, 12, length, 4);
// Radiotap version 0, length 9, presence bit 1 = one-byte Flags field.
wire::put(record, 18, 9, 2); wire::put(record, 20, 2, 4);
std::uint32_t crc = 0xffffffffu;
for (const auto byte : psdu) crc = wire::crc_byte(crc, byte);
record[24] = static_cast<std::uint8_t>(0x10u |
(psdu.size() >= 4 && crc == 0xdebb20e3u ? 0u : 0x40u));
std::copy(psdu.begin(), psdu.end(), record.begin() + 25);
return record;
}
} // namespace gf::e310
wifi_e310_link/host/e310_sifs_uio.cpp · 464 lines
Download this file · Permanent section link
#include "e310_sifs_uio.hpp"
#include "e310_packet_tx_wait.hpp"
#include "e310_host_waveform.hpp"
#include "e310_counter_format.hpp"
#include <atomic>
#include <chrono>
#include <cerrno>
#include <charconv>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iomanip>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <sys/mman.h>
#include <thread>
#include <unistd.h>
namespace gf::e310 {
namespace {
constexpr std::uint32_t kMagic = 0x00;
constexpr std::uint32_t kControl = 0x04;
constexpr std::uint32_t kApMacLo = 0x08;
constexpr std::uint32_t kApMacHi = 0x0c;
constexpr std::uint32_t kStatus = 0x10;
constexpr std::uint32_t kPsduEvent = 0x14;
constexpr std::uint32_t kFifoOverflow = 0x18;
constexpr std::uint32_t kRxPsduCount = 0x1c;
constexpr std::uint32_t kResponseCount = 0x20;
constexpr std::uint32_t kDeadlineMiss = 0x24;
constexpr std::uint32_t kRejectedCount = 0x28;
constexpr std::uint32_t kVersion = 0x2c;
constexpr std::uint32_t kTxWrite = 0x30;
constexpr std::uint32_t kTxCommit = 0x34;
constexpr std::uint32_t kTxStatus = 0x38;
constexpr std::uint32_t kTxDoneCount = 0x3c;
constexpr std::uint32_t kArmKey = 0x40;
constexpr std::uint32_t kRfConfig = 0x44;
constexpr std::uint32_t kTxRejected = 0x48;
constexpr std::uint32_t kTxErrorCount = 0x4c;
constexpr std::uint32_t kExpectedMagic = 0x47464531;
constexpr std::uint32_t kExpectedVersionMajor = 1;
constexpr std::uint32_t kExpectedArmKey = 0x47324641;
constexpr std::uint32_t kControlArm = 1u << 0;
constexpr std::uint32_t kControlKill = 1u << 1;
constexpr std::size_t kMaximumPsduBytes = 4095;
inline void complete_device_access() noexcept {
#if defined(__arm__) || defined(__aarch64__)
// Compiler fences do not drain the Cortex-A9 device transaction queue.
// Bring-up uses a full-system completion barrier, including the GP0
// peripheral domain, rather than assuming a cache-coherent RAM mapping.
__asm__ __volatile__("dsb sy" ::: "memory");
#else
std::atomic_thread_fence(std::memory_order_seq_cst);
#endif
}
std::string trim(std::string value) {
while (!value.empty() &&
(value.back() == '\n' || value.back() == '\r' ||
value.back() == ' ' || value.back() == '\t')) {
value.pop_back();
}
const auto first = value.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return {};
value.erase(0, first);
return value;
}
std::uint64_t read_sysfs_integer(const std::filesystem::path& path) {
std::ifstream input(path);
std::string text;
if (!input || !std::getline(input, text))
throw std::runtime_error("cannot read UIO map attribute " +
path.string());
text = trim(std::move(text));
std::size_t consumed = 0;
std::uint64_t value = 0;
try {
value = std::stoull(text, &consumed, 0);
} catch (const std::exception&) {
throw std::runtime_error("invalid UIO map attribute " +
path.string() + ": " + text);
}
if (consumed != text.size())
throw std::runtime_error("invalid UIO map attribute " +
path.string() + ": " + text);
return value;
}
std::string hex_value(std::uint64_t value) {
std::ostringstream output;
output << "0x" << std::hex << value;
return output.str();
}
std::runtime_error system_error(const std::string& operation) {
return std::runtime_error(operation + ": " + std::strerror(errno));
}
int hex_nibble(char value) {
if (value >= '0' && value <= '9') return value - '0';
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
return -1;
}
} // namespace
UioMapInfo SifsUio::inspect_uio_map(
const std::string& label,
const std::filesystem::path& class_path,
const std::filesystem::path& device_path) {
std::optional<UioMapInfo> match;
std::error_code error;
for (const auto& entry :
std::filesystem::directory_iterator(class_path, error)) {
const auto map_path = entry.path() / "maps" / "map0";
const auto name_path = map_path / "name";
std::ifstream input(name_path);
std::string value;
if (!input || !std::getline(input, value) || trim(value) != label)
continue;
if (match)
throw std::runtime_error("multiple UIO map0 entries are labelled " +
label);
const auto raw_size = read_sysfs_integer(map_path / "size");
const auto raw_offset = read_sysfs_integer(map_path / "offset");
if (raw_size > std::numeric_limits<std::size_t>::max() ||
raw_offset > std::numeric_limits<std::size_t>::max()) {
throw std::runtime_error("UIO map dimensions exceed host size_t");
}
match = UioMapInfo{
.device = device_path / entry.path().filename(),
.physical_base = read_sysfs_integer(map_path / "addr"),
.size = static_cast<std::size_t>(raw_size),
.offset = static_cast<std::size_t>(raw_offset),
};
}
if (error)
throw std::runtime_error("cannot enumerate " + class_path.string() +
": " + error.message());
if (!match)
throw std::runtime_error("no UIO map is labelled " + label);
if (match->physical_base != kExpectedUioPhysicalBase ||
match->size != kExpectedUioSize || match->offset != 0) {
throw std::runtime_error(
"unexpected E310 UIO map for " + label + ": addr=" +
hex_value(match->physical_base) + " size=" +
hex_value(match->size) + " offset=" +
hex_value(match->offset) + "; expected addr=" +
hex_value(kExpectedUioPhysicalBase) + " size=" +
hex_value(kExpectedUioSize) + " offset=0x0");
}
return *match;
}
SifsUio::SifsUio(std::string uio_label, bool legacy_devmem, CounterAccess counter_access)
: counter_access_(counter_access) {
if (legacy_devmem) {
// Explicit opt-in for the observed 2017 E310 image with no UIO nodes.
// Use the same physically verified custom aperture, never legacy PMU.
if (!std::filesystem::exists("/dev/axi_fpga") ||
read_sysfs_integer("/sys/class/xdevcfg/xdevcfg/device/prog_done") != 1)
throw std::runtime_error("legacy E310 device/image is not available");
map_info_ = {"/dev/mem", kExpectedUioPhysicalBase, kExpectedUioSize, 0};
} else {
map_info_ = inspect_uio_map(uio_label);
}
file_descriptor_ = ::open(map_info_.device.c_str(), O_RDWR | O_CLOEXEC | O_SYNC);
if (file_descriptor_ < 0)
throw system_error("open " + map_info_.device.string());
map_length_ = map_info_.size;
void* mapping = ::mmap(nullptr, map_length_, PROT_READ | PROT_WRITE,
MAP_SHARED, file_descriptor_,
legacy_devmem ? static_cast<off_t>(map_info_.physical_base) : 0);
if (mapping == MAP_FAILED) {
const auto error_value = errno;
::close(file_descriptor_);
file_descriptor_ = -1;
errno = error_value;
throw system_error("mmap " + map_info_.device.string());
}
registers_ = static_cast<volatile std::uint32_t*>(mapping);
const auto magic = read(kMagic);
if (magic != kExpectedMagic) {
::munmap(const_cast<std::uint32_t*>(registers_), map_length_);
registers_ = nullptr;
::close(file_descriptor_);
file_descriptor_ = -1;
std::ostringstream message;
message << "Greenforest E310 register magic mismatch: expected 0x"
<< std::hex << kExpectedMagic << ", got 0x" << magic;
throw std::runtime_error(message.str());
}
version_ = read(kVersion);
const auto counter_format = read(counters::capability_address - kRegisterWindowOffset);
if ((version_ >> 16) != kExpectedVersionMajor || !counters::supported(counter_format)) {
::munmap(const_cast<std::uint32_t*>(registers_), map_length_);
registers_ = nullptr;
::close(file_descriptor_);
file_descriptor_ = -1;
throw std::runtime_error("unsupported Greenforest E310 register version/counter format");
}
counters_gray_ = counter_format == counters::gray32;
}
SifsUio::~SifsUio() {
kill();
if (registers_) {
::munmap(const_cast<std::uint32_t*>(registers_), map_length_);
registers_ = nullptr;
}
if (file_descriptor_ >= 0) {
::close(file_descriptor_);
file_descriptor_ = -1;
}
}
std::uint32_t SifsUio::read(std::uint32_t offset) const {
const auto value=read_native(offset);
return counter_access_==CounterAccess::native_words ? value :
counters::normalize(counters_gray_, kRegisterWindowOffset + offset, value);
}
std::uint32_t SifsUio::read_native(std::uint32_t offset) const {
if (!registers_ || (kRegisterWindowOffset + offset + 4) > map_length_)
throw std::runtime_error("invalid E310 register read");
complete_device_access();
const auto value = registers_[(kRegisterWindowOffset + offset) / 4];
complete_device_access();
return value;
}
counters::Words SifsUio::raw_counter_snapshot() const {
counters::Words words{};
// Fixed read-only diagnostic addresses only; no FIFO-pop or PMU reads.
// Each counter is coherent. The eleven separate reads are NOT atomic.
for(std::size_t n=0;n<words.size();++n)
words[n]=read_native(counters::addresses[n]-kRegisterWindowOffset);
return words;
}
void SifsUio::write(std::uint32_t offset, std::uint32_t value) {
if (!registers_ || (kRegisterWindowOffset + offset + 4) > map_length_)
throw std::runtime_error("invalid E310 register write");
complete_device_access();
registers_[(kRegisterWindowOffset + offset) / 4] = value;
complete_device_access();
}
void SifsUio::configure_and_arm(const Mac& mac, const RfConfig& rf) {
// Configuration changes happen only while killed. The FPGA synchronizes the
// stable values into radio_clk before arm can become active.
write(kControl, kControlKill);
::usleep(1000);
const auto killed = status();
if (!killed.killed || killed.armed) {
throw std::runtime_error(
"E310 SIFS engine did not enter killed state before configuration");
}
const std::uint32_t low =
(static_cast<std::uint32_t>(mac[2]) << 24) |
(static_cast<std::uint32_t>(mac[3]) << 16) |
(static_cast<std::uint32_t>(mac[4]) << 8) |
static_cast<std::uint32_t>(mac[5]);
const std::uint32_t high =
(static_cast<std::uint32_t>(mac[0]) << 8) |
static_cast<std::uint32_t>(mac[1]);
write(kApMacLo, low);
write(kApMacHi, high);
if (read(kApMacLo) != low || (read(kApMacHi) & 0xffffu) != high) {
throw std::runtime_error("E310 SIFS AP MAC register readback failed");
}
if (packet_tx_supported()) {
const std::uint32_t rf_value =
(rf.logical_tx_channel_1 ? 1u : 0u) |
(rf.logical_rx0_uses_txrx ? 2u : 0u) |
(rf.logical_rx1_uses_txrx ? 4u : 0u);
write(kRfConfig, rf_value);
if ((read(kRfConfig) & 7u) != rf_value)
throw std::runtime_error("E310 RF switch register readback failed");
write(kArmKey, kExpectedArmKey);
}
write(kControl, kControlArm);
armed_ = true;
// Allow the two-stage bus-to-radio and radio-to-bus synchronizers to
// settle before treating the status as evidence that the path is armed.
::usleep(1000);
const auto current = status();
if (!current.armed || current.killed || current.mode_fault) {
kill();
throw std::runtime_error(
"E310 SIFS engine did not arm in the required two-channel mode");
}
}
void SifsUio::kill() noexcept {
if (!registers_) return;
complete_device_access();
registers_[(kRegisterWindowOffset + kControl) / 4] = kControlKill;
complete_device_access();
armed_ = false;
}
SifsStatus SifsUio::status() const {
const auto bits = read(kStatus);
SifsStatus output;
output.armed = (bits & (1u << 0)) != 0;
output.killed = (bits & (1u << 1)) != 0;
output.fifo_has_data = (bits & (1u << 2)) != 0;
output.fifo_overflowed = (bits & (1u << 3)) != 0;
output.response_pending = (bits & (1u << 4)) != 0;
output.response_active = (bits & (1u << 5)) != 0;
output.tx_override_valid = (bits & (1u << 6)) != 0;
output.mode_fault = (bits & (1u << 7)) != 0;
output.tx_inflight = (bits & (1u << 8)) != 0;
output.tx_busy = (bits & (1u << 9)) != 0;
output.tx_done_seen = (bits & (1u << 10)) != 0;
output.tx_error_seen = (bits & (1u << 11)) != 0;
output.config_fault = (bits & (1u << 12)) != 0;
output.radio_path_ready = (bits & (1u << 13)) != 0;
output.fifo_overflow_count = read(kFifoOverflow);
output.rx_psdu_count = read(kRxPsduCount);
output.response_count = read(kResponseCount);
output.deadline_miss_count = read(kDeadlineMiss);
output.rejected_count = read(kRejectedCount);
return output;
}
PacketTxStatus SifsUio::packet_tx_status() const {
if (!packet_tx_supported())
throw std::runtime_error(
"E310 register image does not provide GP0 packet TX");
const auto bits = read(kTxStatus);
return PacketTxStatus{
.inflight = (bits & (1u << 0)) != 0,
.busy = (bits & (1u << 1)) != 0,
.config_fault = (bits & (1u << 2)) != 0,
.bytes_written = static_cast<std::uint16_t>((bits >> 3) & 0x1fffu),
.done_count = read(kTxDoneCount),
.rejected_count = read(kTxRejected),
.error_count = read(kTxErrorCount),
};
}
void SifsUio::send_psdu(const std::vector<std::uint8_t>& psdu,
std::chrono::milliseconds timeout) {
if(waveform_capability()!=0)
throw std::runtime_error("PSDU TX rejected by host-waveform hardware");
send_frame_bytes(psdu,timeout);
}
std::uint32_t SifsUio::waveform_capability() const {
const auto value=read(0x7c);
// Qualified older images return this exact unimplemented-register sentinel.
return value==0xdead027cu ? 0 : value;
}
void SifsUio::send_waveform(const std::vector<std::uint8_t>& data,
std::chrono::milliseconds timeout) {
if(waveform_capability()!=waveform::kCapability)
throw std::runtime_error("WF20 TX rejected by incompatible hardware");
waveform::validate(data);
send_frame_bytes(data,timeout);
}
void SifsUio::send_frame_bytes(const std::vector<std::uint8_t>& psdu,
std::chrono::milliseconds timeout) {
if (!packet_tx_supported())
throw std::runtime_error(
"E310 register image does not provide GP0 packet TX");
if (psdu.empty() || psdu.size() > kMaximumPsduBytes)
throw std::runtime_error("E310 GP0 PSDU must contain 1..4095 bytes");
if (timeout <= std::chrono::milliseconds::zero())
throw std::runtime_error("E310 GP0 TX timeout must be positive");
const auto before = packet_tx_status();
const auto health = status();
if (!health.armed || health.killed || !health.radio_path_ready ||
health.mode_fault || health.config_fault || health.tx_error_seen) {
throw std::runtime_error("E310 RF path is not healthy for packet TX");
}
if (before.inflight || before.busy || before.bytes_written != 0 ||
before.config_fault) {
throw std::runtime_error("E310 GP0 packet TX is not idle");
}
for (std::size_t index = 0; index < psdu.size(); ++index) {
write(kTxWrite,
(static_cast<std::uint32_t>(index) << 8) |
static_cast<std::uint32_t>(psdu[index]));
}
const auto loaded = packet_tx_status();
if (loaded.bytes_written != psdu.size() ||
loaded.rejected_count != before.rejected_count ||
loaded.error_count != before.error_count || loaded.config_fault) {
kill();
throw std::runtime_error("E310 GP0 packet load was rejected");
}
write(kTxCommit, static_cast<std::uint32_t>(psdu.size()));
const auto deadline = std::chrono::steady_clock::now() + timeout;
const auto result = wait_packet_tx_completion(before, deadline,
[this] { return packet_tx_status(); },
[] { return std::chrono::steady_clock::now(); },
[] { std::this_thread::sleep_for(std::chrono::microseconds(50)); });
if (result == PacketTxWaitResult::completed) return;
kill();
if (result == PacketTxWaitResult::fault)
throw std::runtime_error("E310 GP0 packet TX faulted; RF killed");
throw std::runtime_error("E310 GP0 packet TX timed out; RF killed");
}
std::optional<PsduByteEvent> SifsUio::read_event() {
const auto value = read(kPsduEvent);
if ((value & (1u << 31)) == 0) return std::nullopt;
return PsduByteEvent{
.byte = static_cast<std::uint8_t>(value),
.first = (value & (1u << 8)) != 0,
.last = (value & (1u << 9)) != 0,
};
}
Mac SifsUio::parse_mac(const std::string& text) {
if (text.size() != 17)
throw std::runtime_error("MAC must have form aa:bb:cc:dd:ee:ff");
Mac output{};
for (std::size_t index = 0; index < output.size(); ++index) {
const auto offset = index * 3;
if (index != 0 && text[offset - 1] != ':')
throw std::runtime_error("MAC must have form aa:bb:cc:dd:ee:ff");
const auto high = hex_nibble(text[offset]);
const auto low = hex_nibble(text[offset + 1]);
if (high < 0 || low < 0)
throw std::runtime_error("MAC contains a non-hexadecimal digit");
output[index] = static_cast<std::uint8_t>((high << 4) | low);
}
return output;
}
std::string SifsUio::format_mac(const Mac& mac) {
std::ostringstream output;
output << std::hex << std::setfill('0');
for (std::size_t index = 0; index < mac.size(); ++index) {
if (index) output << ':';
output << std::setw(2) << static_cast<unsigned>(mac[index]);
}
return output.str();
}
} // namespace gf::e310
wifi_e310_link/host/e310_sifs_uio.hpp · 143 lines
Download this file · Permanent section link
#pragma once
#include "e310_counter_format.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
namespace gf::e310 {
using Mac = std::array<std::uint8_t, 6>;
enum class CounterAccess { normalized, native_words };
struct PsduByteEvent {
std::uint8_t byte = 0;
bool first = false;
bool last = false;
};
struct SifsStatus {
bool armed = false;
bool killed = true;
bool fifo_has_data = false;
bool fifo_overflowed = false;
bool response_pending = false;
bool response_active = false;
bool tx_override_valid = false;
bool mode_fault = false;
bool tx_inflight = false;
bool tx_busy = false;
bool tx_done_seen = false;
bool tx_error_seen = false;
bool config_fault = false;
bool radio_path_ready = false;
std::uint32_t fifo_overflow_count = 0;
std::uint32_t rx_psdu_count = 0;
std::uint32_t response_count = 0;
std::uint32_t deadline_miss_count = 0;
std::uint32_t rejected_count = 0;
};
struct RfConfig {
// E310 logical channel 0 is physical front end 2. The default therefore
// transmits through TX/RX2 and receives through the dedicated RX2 input.
bool logical_tx_channel_1 = false;
bool logical_rx0_uses_txrx = false;
bool logical_rx1_uses_txrx = false;
};
struct PacketTxStatus {
bool inflight = false;
bool busy = false;
bool config_fault = false;
std::uint16_t bytes_written = 0;
std::uint32_t done_count = 0;
std::uint32_t rejected_count = 0;
std::uint32_t error_count = 0;
};
struct UioMapInfo {
std::filesystem::path device;
std::uint64_t physical_base = 0;
std::size_t size = 0;
std::size_t offset = 0;
};
class SifsUio {
public:
// These values are fixed by the stock E31x device tree. In particular,
// 0x40000000 is the PMU/power-control window and must never be accepted as
// the motherboard register aperture.
static constexpr std::uint64_t kExpectedUioPhysicalBase = 0x40010000ull;
static constexpr std::size_t kExpectedUioSize = 0x2000;
static constexpr std::size_t kRegisterWindowOffset = 0x200;
// native_words keeps counts opaque on the adapter: only zero/equality
// tests are valid there. Numerical interpretation belongs to Windows.
explicit SifsUio(std::string uio_label = "mboard-regs", bool legacy_devmem = false,
CounterAccess counter_access = CounterAccess::normalized);
~SifsUio();
SifsUio(const SifsUio&) = delete;
SifsUio& operator=(const SifsUio&) = delete;
SifsUio(SifsUio&&) = delete;
SifsUio& operator=(SifsUio&&) = delete;
void configure_and_arm(const Mac& ap_mac,
const RfConfig& rf = RfConfig{});
void kill() noexcept;
SifsStatus status() const;
PacketTxStatus packet_tx_status() const;
counters::Words raw_counter_snapshot() const;
std::uint32_t counter_format() const noexcept {
return counters_gray_ ? counters::gray32 : 0;
}
void send_psdu(
const std::vector<std::uint8_t>& psdu,
std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
std::uint32_t waveform_capability() const;
void send_waveform(const std::vector<std::uint8_t>& data,
std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
std::optional<PsduByteEvent> read_event();
std::uint32_t register_version() const noexcept { return version_; }
const UioMapInfo& map_info() const noexcept { return map_info_; }
std::uint64_t register_physical_base() const noexcept {
return map_info_.physical_base + kRegisterWindowOffset;
}
bool packet_tx_supported() const noexcept {
return (version_ >> 16) == 1 && (version_ & 0xffffu) >= 1;
}
static UioMapInfo inspect_uio_map(
const std::string& label,
const std::filesystem::path& class_path = "/sys/class/uio",
const std::filesystem::path& device_path = "/dev");
static Mac parse_mac(const std::string& text);
static std::string format_mac(const Mac& mac);
private:
void send_frame_bytes(const std::vector<std::uint8_t>& data,
std::chrono::milliseconds timeout);
std::uint32_t read(std::uint32_t offset) const;
std::uint32_t read_native(std::uint32_t offset) const;
void write(std::uint32_t offset, std::uint32_t value);
int file_descriptor_ = -1;
volatile std::uint32_t* registers_ = nullptr;
std::size_t map_length_ = 0;
bool armed_ = false;
std::uint32_t version_ = 0;
bool counters_gray_ = false;
CounterAccess counter_access_ = CounterAccess::normalized;
UioMapInfo map_info_;
};
} // namespace gf::e310
wifi_e310_link/host/e310_windows_ap.cpp · 436 lines
Download this file · Permanent section link
#include "e310_packet_ap_core.hpp"
#include "e310_rx_pcap.hpp"
#include <atomic>
#include <charconv>
#include <cmath>
#include <fstream>
#include <thread>
namespace {
using namespace gf;
using namespace gf::e310;
std::atomic_bool stopping{false};
BOOL WINAPI control_handler(DWORD event) {
if(event==CTRL_C_EVENT || event==CTRL_BREAK_EVENT || event==CTRL_CLOSE_EVENT) { stopping=true; return TRUE; }
return FALSE;
}
// A process-owned timer avoids coarse Sleep(1) polling without changing any
// machine-wide timer resolution or Linux/RF power mode. Not a SIFS mechanism.
class PollWait {
HANDLE timer_=nullptr;
public:
PollWait() {
timer_=CreateWaitableTimerExW(nullptr,nullptr,CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_MODIFY_STATE|SYNCHRONIZE);
if(!timer_) throw std::runtime_error("Cannot create high-resolution host polling timer");
}
~PollWait() { if(timer_) { CancelWaitableTimer(timer_); CloseHandle(timer_); } }
PollWait(const PollWait&)=delete;
PollWait& operator=(const PollWait&)=delete;
void wait() {
LARGE_INTEGER due{}; due.QuadPart=-10000; // Relative 1 ms, 100 ns units.
if(!SetWaitableTimer(timer_,&due,0,nullptr,nullptr,FALSE) ||
WaitForSingleObject(timer_,1000)!=WAIT_OBJECT_0)
throw std::runtime_error("Host polling timer failed");
}
};
void poll_wait_benchmark() {
PollWait wait;
const auto measure=[](const char* name,auto pause) {
std::vector<std::int64_t> elapsed;
for(unsigned n=0;n<64;++n) {
const auto begin=rt::Clock::now(); pause();
elapsed.push_back(std::chrono::duration_cast<std::chrono::microseconds>(rt::Clock::now()-begin).count());
}
std::sort(elapsed.begin(),elapsed.end());
std::cout<<"E310_POLL_WAIT_BENCHMARK mode="<<name<<" requested_us=1000 count="<<elapsed.size()
<<" median_us="<<elapsed[elapsed.size()/2]<<" max_us="<<elapsed.back()
<<" serial_open=false physical_rf=false\n"<<std::flush;
};
measure("previous_sleep",[] { std::this_thread::sleep_for(std::chrono::milliseconds(1)); });
measure("high_resolution_timer",[&] { wait.wait(); });
}
class SerialPort {
HANDLE handle_=INVALID_HANDLE_VALUE;
DCB previous_{};
COMMTIMEOUTS previous_timeouts_{};
public:
SerialPort(const std::string& port,DWORD baud) {
if(port.size()<4 || port.substr(0,3)!="COM" || port.find_first_not_of("0123456789",3)!=std::string::npos)
throw std::runtime_error("Expected a COM port name");
handle_=CreateFileA(("\\\\.\\"+port).c_str(),GENERIC_READ|GENERIC_WRITE,0,nullptr,OPEN_EXISTING,0,nullptr);
if(handle_==INVALID_HANDLE_VALUE) throw std::runtime_error("Cannot exclusively open "+port);
previous_.DCBlength=sizeof(previous_);
if(!GetCommState(handle_,&previous_) || !GetCommTimeouts(handle_,&previous_timeouts_)) {
CloseHandle(handle_); handle_=INVALID_HANDLE_VALUE; throw std::runtime_error("Read serial settings failed");
}
auto config=previous_;
config.BaudRate=baud; config.ByteSize=8; config.Parity=NOPARITY; config.StopBits=ONESTOPBIT;
config.fBinary=TRUE; config.fParity=FALSE; config.fOutxCtsFlow=FALSE; config.fOutxDsrFlow=FALSE;
config.fDtrControl=DTR_CONTROL_DISABLE; config.fRtsControl=RTS_CONTROL_DISABLE;
config.fOutX=FALSE; config.fInX=FALSE; config.fAbortOnError=FALSE; config.fDsrSensitivity=FALSE;
config.fNull=FALSE; config.fErrorChar=FALSE;
COMMTIMEOUTS timeouts{};
timeouts.ReadIntervalTimeout=MAXDWORD;
timeouts.WriteTotalTimeoutConstant=1000;
if(!SetCommState(handle_,&config) || !SetCommTimeouts(handle_,&timeouts)) {
SetCommState(handle_,&previous_); SetCommTimeouts(handle_,&previous_timeouts_);
CloseHandle(handle_); handle_=INVALID_HANDLE_VALUE; throw std::runtime_error("Configure serial port failed");
}
}
~SerialPort() {
if(handle_!=INVALID_HANDLE_VALUE) {
SetCommState(handle_,&previous_); SetCommTimeouts(handle_,&previous_timeouts_); CloseHandle(handle_);
}
}
SerialPort(const SerialPort&)=delete;
SerialPort& operator=(const SerialPort&)=delete;
DWORD read(std::uint8_t* bytes,DWORD capacity) {
DWORD count=0;
if(!ReadFile(handle_,bytes,capacity,&count,nullptr)) throw std::runtime_error("Serial read failed");
return count;
}
void send(const wire::Message& message) {
const auto bytes=wire::encode(message);
DWORD sent=0;
if(!WriteFile(handle_,bytes.data(),static_cast<DWORD>(bytes.size()),&sent,nullptr) || sent!=bytes.size())
throw std::runtime_error("Serial packet write failed or timed out");
}
};
std::string read_text(const std::string& path) {
std::ifstream input(path,std::ios::binary);
if(!input) throw std::runtime_error("Cannot read "+path);
return {std::istreambuf_iterator<char>(input),std::istreambuf_iterator<char>()};
}
rt::Mac parse_mac(const std::string& text) {
if(text.size()!=17) throw std::runtime_error("Invalid BSSID");
rt::Mac mac{};
for(std::size_t i=0;i<6;++i) {
unsigned byte=0;
const auto begin=text.data()+3*i;
const auto parsed=std::from_chars(begin,begin+2,byte,16);
if(parsed.ec!=std::errc{} || parsed.ptr!=begin+2 || (i<5 && begin[2]!=':')) throw std::runtime_error("Invalid BSSID");
mac[i]=static_cast<std::uint8_t>(byte);
}
if((mac[0]&1) || mac==rt::Mac{}) throw std::runtime_error("BSSID must be nonzero unicast");
return mac;
}
void core_selftest() {
const auto valid_fcs=[](const wire::Bytes& psdu) {
std::uint32_t crc=0xffffffffu;
for(auto byte:psdu) crc=wire::crc_byte(crc,byte);
return psdu.size()>=4 && crc==0xdebb20e3u;
};
ap::run_ap_self_test();
ap::ProtocolConfig config; config.dsss_1mbps_only=true;
PacketApCore core(config,1234);
const std::string identity="GF_E310_PACKET_AGENT_V1";
auto init=core.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
if(init.size()!=1 || init[0].kind!=wire::Kind::initialize || init[0].payload.size()!=8 || core.ready())
throw std::runtime_error("Host initialization gate failed");
wire::Bytes contract(13,0);
wire::put(contract,0,0x10003,4); wire::put(contract,4,20000000,4); wire::put(contract,8,40000000,4); contract[12]=6;
core.ingest({wire::Kind::ready,1234,1,contract});
auto periodic=core.tick(rt::Clock::now());
if(!core.ready() || periodic.size()!=2 || periodic[0].kind!=wire::Kind::tx_psdu ||
!valid_fcs(periodic[0].payload) || periodic[1].kind!=wire::Kind::ping)
throw std::runtime_error("Host beacon/heartbeat gate failed");
const auto capture_header=rx_pcap_header();
const auto record=rx_pcap_record(periodic[0].payload,1234567890123456ULL);
auto corrupted=periodic[0].payload; corrupted.back()^=1;
if(capture_header.size()!=24 || wire::get(capture_header,20,4)!=127 ||
wire::get(record,0,4)!=1234567890 || wire::get(record,4,4)!=123456 ||
wire::get(record,8,4)!=periodic[0].payload.size()+9 || record[24]!=0x10 ||
record.size()!=25+periodic[0].payload.size() ||
!std::equal(periodic[0].payload.begin(),periodic[0].payload.end(),record.begin()+25) ||
rx_pcap_record(corrupted,0)[24]!=0x50)
throw std::runtime_error("RX PCAP byte-preservation/header/FCS self-test failed");
std::cout<<"E310_RX_PCAP_SELFTEST_PASS byte_exact=true fcs_flags=true timestamp_host_only=true physical_rf=false\n";
// Use an explicit clock to verify the advertised and scheduled interval
// agree. This checks host scheduling, not physical beacon departure.
auto fast_config=config; fast_config.beacon_interval_tu=20;
PacketApCore fast(fast_config,4321);
fast.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
fast.ingest({wire::Kind::ready,4321,1,contract});
const auto epoch=rt::Clock::time_point{}+std::chrono::seconds(1);
const auto first_fast=fast.tick(epoch);
if(first_fast.size()!=2 || rt::little_u16(first_fast[0].payload.data()+32)!=20 ||
!fast.tick(epoch+std::chrono::microseconds(20479)).empty())
throw std::runtime_error("Fast beacon interval advertisement/early deadline failed");
const auto second_fast=fast.tick(epoch+std::chrono::microseconds(20480));
if(second_fast.size()!=1 || !valid_fcs(second_fast[0].payload))
throw std::runtime_error("Fast beacon deadline/FCS failed");
// Construct a synthetic probe. It is deliberately separate from live RF evidence.
const rt::Mac station{2,0,0,0,0,9};
wire::Bytes probe;
rt::append_management_header(probe,0x0040,rt::kBroadcast,station,rt::kBroadcast,1);
probe.push_back(0); probe.push_back(static_cast<std::uint8_t>(config.ssid.size()));
probe.insert(probe.end(),config.ssid.begin(),config.ssid.end()); rt::append_fcs(probe);
const auto responses=core.ingest({wire::Kind::rx_psdu,1234,2,probe});
if(responses.size()!=1 || responses[0].kind!=wire::Kind::tx_psdu ||
(rt::little_u16(responses[0].payload.data())&0x00fcu)!=0x0050u || !valid_fcs(responses[0].payload))
throw std::runtime_error("Host probe response failed");
const auto fast_probe=fast.ingest({wire::Kind::rx_psdu,4321,2,probe});
if(fast_probe.size()!=1 || rt::little_u16(fast_probe[0].payload.data()+32)!=20 ||
!valid_fcs(fast_probe[0].payload))
throw std::runtime_error("Probe response beacon interval mismatch");
std::cout<<"E310_BEACON_INTERVAL_SELFTEST_PASS beacon_and_probe_consistent=true early_tx=false physical_rf=false\n";
if(!core.ingest({wire::Kind::rx_psdu,1234,2,probe}).empty() ||
!core.ingest({wire::Kind::rx_psdu,5678,3,probe}).empty() || core.stale_messages()!=2)
throw std::runtime_error("Stale/session replay rejection failed");
const auto authentication=rt::make_authentication_request(station,config.bssid,2);
const auto auth_reply=core.ingest({wire::Kind::rx_psdu,1234,3,authentication});
if(auth_reply.size()!=1 || (rt::little_u16(auth_reply[0].payload.data())&0x00fcu)!=0x00b0u)
throw std::runtime_error("Host forwarded a SIFS ACK or lost authentication response");
PacketApCore waveform_core(config,777);
waveform_core.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
auto waveform_contract=contract; waveform_contract.resize(17);
wire::put(waveform_contract,13,waveform::kCapability,4);
waveform_core.ingest({wire::Kind::ready,777,1,waveform_contract});
if(!waveform_core.host_waveform() || core.host_waveform())
throw std::runtime_error("Host waveform capability selection failed");
const auto waveform_periodic=waveform_core.tick(epoch);
if(waveform_periodic.size()!=2 || waveform_periodic[0].kind!=wire::Kind::tx_waveform ||
waveform_periodic[1].kind!=wire::Kind::ping)
throw std::runtime_error("Host waveform beacon selection failed");
waveform::validate(waveform_periodic[0].payload);
PacketApCore reference_core(config,779);
reference_core.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
reference_core.ingest({wire::Kind::ready,779,1,contract});
reference_core.tick(epoch);
const auto reference_auth=reference_core.ingest({wire::Kind::rx_psdu,779,2,authentication});
const auto waveform_auth=waveform_core.ingest({wire::Kind::rx_psdu,777,2,authentication});
if(reference_auth.size()!=1 || waveform_auth.size()!=1 || waveform_auth[0].kind!=wire::Kind::tx_waveform ||
waveform_auth[0].payload!=waveform::encode(reference_auth[0].payload))
throw std::runtime_error("Host waveform authentication or SIFS exclusion failed");
PacketApCore bad_waveform(config,778);
bad_waveform.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
waveform_contract[13]^=1; bool rejected_capability=false;
try { bad_waveform.ingest({wire::Kind::ready,778,1,waveform_contract}); }
catch(const std::exception&) { rejected_capability=true; }
if(!rejected_capability || bad_waveform.ready()) throw std::runtime_error("Unknown waveform accepted");
std::cout<<"E310_HOST_WAVEFORM_CONTRACT_PASS old_psdu=true new_waveform=true unknown_rejected=true sifs_local=true physical_rf=false\n";
PacketApCore event_core(config,880);
std::vector<wire::Bytes> observed;
event_core.set_rx_observer([&](const wire::Bytes& frame) { observed.push_back(frame); });
event_core.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
auto event_contract=contract; event_contract.resize(21);
wire::put(event_contract,13,waveform::kCapability,4);
wire::put(event_contract,17,wire::kRxEventCapability,4);
event_core.ingest({wire::Kind::ready,880,1,event_contract});
if(!event_core.host_rx_assembly()) throw std::runtime_error("RX assembly capability not selected");
std::uint32_t event_sequence=1;
std::vector<wire::Message> event_response;
for(std::size_t i=0;i<probe.size();++i) {
const wire::Bytes raw{probe[i],static_cast<std::uint8_t>((i==0?1:0)|(i+1==probe.size()?2:0))};
event_response=event_core.ingest({wire::Kind::rx_events,880,++event_sequence,raw});
if(i+1<probe.size() && (!event_response.empty() || !observed.empty()))
throw std::runtime_error("Partial RX frame delivered");
}
if(event_response.size()!=1 || event_response[0].kind!=wire::Kind::tx_waveform ||
observed!=std::vector<wire::Bytes>{probe} || event_core.rx_frames()!=1)
throw std::runtime_error("Windows RX assembly/protocol/PCAP observer mismatch");
event_core.ingest({wire::Kind::rx_events,880,++event_sequence,{probe[0],1}});
event_sequence+=2; // lost UART message must invalidate the partial frame
event_core.ingest({wire::Kind::rx_events,880,event_sequence,{probe[1],2}});
if(observed.size()!=1 || event_core.rx_discarded()!=2)
throw std::runtime_error("RX sequence gap spliced partial frames");
event_core.ingest({wire::Kind::rx_events,880,++event_sequence,{0x42,3}});
event_core.ingest({wire::Kind::rx_events,880,event_sequence,{0x42,3}}); // duplicate
event_core.ingest({wire::Kind::rx_events,881,event_sequence+1,{0x43,3}}); // stale session
if(observed.size()!=2 || observed.back()!=wire::Bytes{0x42} || event_core.stale_messages()!=2)
throw std::runtime_error("RX duplicate/session rejection failed");
PacketApCore unknown_events(config,882);
unknown_events.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
event_contract[17]^=1; bool bad_events_rejected=false;
try { unknown_events.ingest({wire::Kind::ready,882,1,event_contract}); }
catch(const std::exception&) { bad_events_rejected=true; }
if(!bad_events_rejected || unknown_events.ready()) throw std::runtime_error("Unknown RX contract accepted");
std::cout<<"E310_WINDOWS_RX_ASSEMBLY_PASS byte_exact=true protocol=true capture_observer=true sequence_gap=true stale_rejected=true old_agent_supported=true physical_rf=false\n";
auto counter_contract=contract;counter_contract.resize(25);
wire::put(counter_contract,13,waveform::kCapability,4);
wire::put(counter_contract,17,wire::kRxEventCapability,4);
wire::put(counter_contract,21,wire::kCounterSnapshotCapability,4);
unsigned counter_events=0;
PacketApCore counter_core(config,883,[&](std::string_view kind,std::string_view fields) {
if(kind=="hardware_counters") {
if(fields.find("\"response_count\":2147483649")==std::string_view::npos)
throw std::runtime_error("Host counter event not numerically decoded");
++counter_events;
}
});
counter_core.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
counter_core.ingest({wire::Kind::ready,883,1,counter_contract});
counters::Words native{};native[2]=0x80000001u^(0x80000001u>>1);
const auto snapshot=counters::pack_snapshot(counters::gray32,native);
counter_core.ingest({wire::Kind::counter_snapshot,883,2,snapshot});
counter_core.ingest({wire::Kind::counter_snapshot,883,2,snapshot});
counter_core.ingest({wire::Kind::counter_snapshot,884,3,snapshot});
if(!counter_core.host_counter_decode() || !counter_core.hardware_counters() ||
counter_core.hardware_counters()->raw!=native || counter_core.hardware_counters()->values[2]!=0x80000001u ||
counter_core.counter_snapshots()!=1 || counter_events!=1 || counter_core.stale_messages()!=2 || core.host_counter_decode())
throw std::runtime_error("Host raw counter/session/legacy gate failed");
bool format_change_rejected=false;
try { counter_core.ingest({wire::Kind::counter_snapshot,883,3,counters::pack_snapshot(0,native)}); }
catch(const std::exception&) {format_change_rejected=true;}
if(!format_change_rejected || counter_core.counter_snapshots()!=1)throw std::runtime_error("Counter format changed silently");
PacketApCore bad_counter(config,885);
bad_counter.ingest({wire::Kind::hello,0,0,{identity.begin(),identity.end()}});
counter_contract[21]^=1;bool bad_counter_rejected=false;
try {bad_counter.ingest({wire::Kind::ready,885,1,counter_contract});}
catch(const std::exception&){bad_counter_rejected=true;}
if(!bad_counter_rejected || bad_counter.ready())throw std::runtime_error("Unknown counter capability accepted");
std::cout<<"E310_WINDOWS_COUNTER_OFFLOAD_PASS numeric_decode=true raw_retained=true session_rejection=true format_change_rejected=true old_agent_supported=true physical_rf=false\n";
if(parse_mac("02:47:46:41:50:31")!=config.bssid) throw std::runtime_error("BSSID parser mismatch");
if(core.stop().kind!=wire::Kind::stop || core.ready()) throw std::runtime_error("Host stop gate failed");
std::cout<<"E310_WINDOWS_PACKET_AP_SELFTEST_PASS windows_cpp=true sifs_forwarded_to_host=false physical_rf=false\n";
}
int run(int argc,char** argv) {
bool enabled=false, selftest=false, timing_benchmark=false;
std::string port,page,passphrase_file,stop_file,rx_pcap_path;
DWORD baud=460800;
double seconds=0;
ap::ProtocolConfig config; config.dsss_1mbps_only=true;
auto next=[&](int& i) { if(++i>=argc) throw std::runtime_error("Missing option argument"); return std::string(argv[i]); };
for(int i=1;i<argc;++i) {
const std::string arg=argv[i];
if(arg=="--run") enabled=true;
else if(arg=="--self-test") selftest=true;
else if(arg=="--poll-wait-benchmark") timing_benchmark=true;
else if(arg=="--port") port=next(i);
else if(arg=="--baud") baud=static_cast<DWORD>(std::stoul(next(i)));
else if(arg=="--ssid") config.ssid=next(i);
else if(arg=="--bssid") config.bssid=parse_mac(next(i));
else if(arg=="--channel") config.channel=std::stoi(next(i));
else if(arg=="--beacon-tu") {
const auto value=std::stoul(next(i));
if(value<1 || value>65535) throw std::runtime_error("Beacon interval must be 1..65535 TU");
config.beacon_interval_tu=static_cast<std::uint16_t>(value);
}
else if(arg=="--max-stations") config.max_stations=std::stoul(next(i));
else if(arg=="--server-ip") config.server_ip=ap::parse_ip(next(i));
else if(arg=="--passphrase-file") passphrase_file=next(i);
else if(arg=="--page") page=next(i);
else if(arg=="--rx-pcap") rx_pcap_path=next(i);
else if(arg=="--stop-file") stop_file=next(i);
else if(arg=="--seconds") seconds=std::stod(next(i));
else if(arg=="--help") {
std::cout<<"gf_e310_windows_ap --self-test\n"
"gf_e310_windows_ap --poll-wait-benchmark (no serial/RF access)\n"
"gf_e310_windows_ap --run --port COM10 --page HTML [--passphrase-file FILE] [--rx-pcap NEW_FILE] [--beacon-tu 100]\n"
"Requires the separate packet agent already running on E310. Does not launch it.\n"
"No UART writes before its validated hello. No host TCP/80 socket; no UHD/libiio.\n";
return 0;
} else throw std::runtime_error("Unknown option: "+arg);
}
if(timing_benchmark) {
if(enabled || selftest) throw std::runtime_error("Polling benchmark must run alone");
poll_wait_benchmark(); return 0;
}
if(enabled==selftest) throw std::runtime_error("Choose --run or --self-test");
if(selftest) { core_selftest(); return 0; }
if(port.empty() || page.empty() || (baud!=115200 && baud!=460800 && baud!=921600) ||
config.ssid.empty() || config.ssid.size()>32 || config.channel<1 || config.channel>11 ||
config.max_stations<1 || config.max_stations>64 || !std::isfinite(seconds) || seconds<0)
throw std::runtime_error("Invalid host configuration");
config.page=read_text(page);
if(!passphrase_file.empty()) {
config.passphrase=read_text(passphrase_file);
while(!config.passphrase.empty() && (config.passphrase.back()=='\r' || config.passphrase.back()=='\n')) config.passphrase.pop_back();
}
const auto random=ap::random_array<8>();
const auto session=wire::get({random.begin(),random.end()},0,8)|1ULL;
PacketApCore core(config,session,[](std::string_view kind,std::string_view fields) {
std::cout<<"{\"kind\":"<<rt::quote(kind)<<",\"execution\":\"Windows_C++\"";
if(!fields.empty()) { if(fields.front()!=',') std::cout<<','; std::cout<<fields; }
std::cout<<"}\n"<<std::flush;
});
std::ofstream rx_pcap;
const auto capture_write=[&](const wire::Bytes& bytes) {
rx_pcap.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
rx_pcap.flush();
if(!rx_pcap) throw std::runtime_error("RX capture write failed");
};
if(!rx_pcap_path.empty()) {
if(std::filesystem::exists(rx_pcap_path)) throw std::runtime_error("Refusing to overwrite RX capture");
rx_pcap.open(rx_pcap_path,std::ios::binary);
if(!rx_pcap) throw std::runtime_error("Cannot create RX capture");
capture_write(rx_pcap_header());
std::cout<<"E310_RX_PCAP path="<<rx_pcap_path
<<" source=received_fpga_psdu timestamp=host_utc rf_timestamp=false tx_included=false\n"<<std::flush;
}
core.set_rx_observer([&](const wire::Bytes& frame) {
if(!rx_pcap.is_open()) return;
const auto received_us=std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
capture_write(rx_pcap_record(frame,static_cast<std::uint64_t>(received_us)));
});
PollWait poll_wait;
SerialPort serial(port,baud);
wire::Decoder decoder;
const auto started=rt::Clock::now();
auto last_peer=started;
auto next_progress=started+std::chrono::seconds(5);
bool was_ready=false;
auto stop_peer=[&]() {
if(!core.initialized()) return;
serial.send(core.stop());
const auto deadline=rt::Clock::now()+std::chrono::seconds(2);
while(rt::Clock::now()<deadline && !core.stopped()) {
std::uint8_t buffer[8192];
auto count=serial.read(buffer,sizeof(buffer));
decoder.feed(buffer,count,[&](wire::Message msg) {
if(msg.kind==wire::Kind::stopped && msg.session==session) core.ingest(msg);
});
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if(!core.stopped()) throw std::runtime_error("Radio stop acknowledgement missing; agent lease must expire");
std::cout<<"E310_WINDOWS_PEER_STOPPED acknowledged=true\n";
};
SetConsoleCtrlHandler(control_handler,TRUE);
try {
std::cout<<"E310_WINDOWS_WAITING_FOR_AGENT host_tcp80_bound=false credentials_logged=false\n"<<std::flush;
std::cout<<"E310_DISCOVERY_CONFIG beacon_interval_tu="<<config.beacon_interval_tu
<<" requested_interval_us="<<1024ULL*config.beacon_interval_tu
<<" host_scheduled=true physical_departure_not_measured=true\n"<<std::flush;
std::cout<<"E310_POLL_WAIT mode=high_resolution_timer requested_us=1000 global_timer_change=false\n"<<std::flush;
while(!stopping && (stop_file.empty() || !std::filesystem::exists(stop_file)) &&
(seconds==0 || std::chrono::duration<double>(rt::Clock::now()-started).count()<seconds)) {
std::uint8_t buffer[8192];
const auto count=serial.read(buffer,sizeof(buffer));
decoder.feed(buffer,count,[&](wire::Message msg) {
const auto stale_before=core.stale_messages();
for(const auto& outbound:core.ingest(msg)) serial.send(outbound);
if(core.stale_messages()==stale_before) {
last_peer=rt::Clock::now();
}
});
if(core.ready() && !was_ready) { was_ready=true; std::cout<<"E310_WINDOWS_AGENT_READY physical_peer_http_not_yet_verified=true host_waveform="<<core.host_waveform()<<" host_rx_assembly="<<core.host_rx_assembly()<<" host_counter_decode="<<core.host_counter_decode()<<'\n'<<std::flush; }
if(rt::Clock::now()-last_peer>std::chrono::seconds(was_ready?2:10)) throw std::runtime_error("Radio agent not responding");
for(const auto& outbound:core.tick(rt::Clock::now())) serial.send(outbound);
if(rt::Clock::now()>=next_progress) {
std::cout<<"E310_WINDOWS_PROGRESS ready="<<core.ready()
<<" tx_completed="<<core.tx_completed()<<" rx_psdus="<<core.rx_frames()
<<" wire_rejected="<<decoder.rejected()
<<" rx_events="<<core.rx_events()<<" rx_discarded="<<core.rx_discarded()
<<" counter_snapshots="<<core.counter_snapshots()
<<" elapsed_ms="<<std::chrono::duration_cast<std::chrono::milliseconds>(rt::Clock::now()-started).count()
<<'\n'<<std::flush;
next_progress=rt::Clock::now()+std::chrono::seconds(5);
}
poll_wait.wait();
}
stop_peer();
} catch(...) {
try { stop_peer(); } catch(const std::exception& error) { std::cerr<<error.what()<<'\n'; }
throw;
}
return 0;
}
}
int main(int argc,char** argv) {
try { return run(argc,argv); }
catch(const std::exception& error) { std::cerr<<"fatal: "<<error.what()<<'\n'; return 1; }
}
wifi_e310_link/host/windows/CMakeLists.txt · 33 lines
Download this file · Permanent section link
cmake_minimum_required(VERSION 3.24)
project(gf_e310_windows_packets LANGUAGES CXX)
if(NOT WIN32)
message(FATAL_ERROR "Use the Windows compiler for this host entry point")
endif()
add_executable(gf_e310_windows_protocol_selftest
../../../wifi_pluto_link/host/ap_realtime.cpp
../../../tools/wifi_protocol.cpp)
target_compile_definitions(gf_e310_windows_protocol_selftest PRIVATE GF_AP_PROTOCOL_ONLY=1 WIN32_LEAN_AND_MEAN NOMINMAX)
target_compile_features(gf_e310_windows_protocol_selftest PRIVATE cxx_std_20)
target_compile_options(gf_e310_windows_protocol_selftest PRIVATE /W4 /permissive- /EHsc)
target_link_libraries(gf_e310_windows_protocol_selftest PRIVATE bcrypt)
add_executable(gf_e310_packet_wire_selftest ../e310_packet_wire_selftest.cpp)
target_compile_features(gf_e310_packet_wire_selftest PRIVATE cxx_std_20)
target_compile_options(gf_e310_packet_wire_selftest PRIVATE /W4 /WX /permissive- /EHsc)
add_executable(gf_e310_rx_event_selftest ../e310_rx_event_selftest.cpp)
target_compile_features(gf_e310_rx_event_selftest PRIVATE cxx_std_20)
target_compile_options(gf_e310_rx_event_selftest PRIVATE /W4 /WX /permissive- /EHsc)
add_executable(gf_e310_counter_snapshot_test ../e310_counter_snapshot_test.cpp)
target_compile_features(gf_e310_counter_snapshot_test PRIVATE cxx_std_20)
target_compile_options(gf_e310_counter_snapshot_test PRIVATE /W4 /WX /permissive- /EHsc)
add_executable(gf_e310_packet_tx_wait_selftest ../e310_packet_tx_wait_selftest.cpp)
target_compile_features(gf_e310_packet_tx_wait_selftest PRIVATE cxx_std_20)
target_compile_options(gf_e310_packet_tx_wait_selftest PRIVATE /W4 /WX /permissive- /EHsc)
add_executable(gf_e310_windows_ap ../e310_windows_ap.cpp ../../../tools/wifi_protocol.cpp)
add_executable(gf_e310_capture_stats ../e310_capture_stats.cpp)
target_compile_features(gf_e310_capture_stats PRIVATE cxx_std_20)
target_compile_options(gf_e310_capture_stats PRIVATE /W4 /WX /permissive- /EHsc)
target_compile_definitions(gf_e310_windows_ap PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
target_compile_features(gf_e310_windows_ap PRIVATE cxx_std_20)
target_compile_options(gf_e310_windows_ap PRIVATE /W4 /permissive- /EHsc)
target_link_libraries(gf_e310_windows_ap PRIVATE bcrypt)
wifi_e310_link/tools/build_minimal_vivado.tcl · 205 lines
Download this file · Permanent section link
# Native Windows implementation of the existing minimal RTL, without an IP
# integrator project, BSP generation, Vitis, or the version-pinned UHD shell.
set project [file normalize [lindex $argv 0]]
set part [lindex $argv 1]
set out [file normalize [lindex $argv 2]]
set spi_cs [lindex $argv 4]
set host_waveform [lindex $argv 8]
if {$host_waveform eq ""} { set host_waveform 0 }
if {$host_waveform ni {0 1}} { error "Invalid host-waveform build flag" }
set tx_block_ram [lindex $argv 9]
if {$tx_block_ram eq ""} { set tx_block_ram 0 }
if {$tx_block_ram ni {0 1} || ($tx_block_ram && !$host_waveform)} { error "Invalid TX block RAM build flag" }
set counters_to_software [lindex $argv 10]
if {$counters_to_software eq ""} { set counters_to_software 0 }
if {$counters_to_software ni {0 1} || ($counters_to_software && !$tx_block_ram)} { error "Invalid counter-offload build flag" }
set rx_fifo_block_ram [lindex $argv 11]
if {$rx_fifo_block_ram eq ""} { set rx_fifo_block_ram 0 }
if {$rx_fifo_block_ram ni {0 1} || ($rx_fifo_block_ram && !$counters_to_software)} { error "Invalid RX FIFO block RAM flag" }
set single_phase_rx [lindex $argv 12]
if {$single_phase_rx eq ""} { set single_phase_rx 0 }
if {$single_phase_rx ni {0 1} || ($single_phase_rx && !$rx_fifo_block_ram)} { error "Invalid single-phase RX flag" }
set serial_differential [lindex $argv 13]
if {$serial_differential eq ""} { set serial_differential 0 }
if {$serial_differential ni {0 1} || ($serial_differential && !$single_phase_rx)} { error "Invalid serial differential flag" }
set timing_score_ram [lindex $argv 14]
if {$timing_score_ram eq ""} { set timing_score_ram 0 }
if {$timing_score_ram ni {0 1} || ($timing_score_ram && !$serial_differential)} { error "Invalid timing score RAM flag" }
set serial_control_crc [lindex $argv 15]
if {$serial_control_crc eq ""} { set serial_control_crc 0 }
if {$serial_control_crc ni {0 1} || ($serial_control_crc && !$timing_score_ram)} { error "Invalid serial control CRC flag" }
set serial_rx_crc [lindex $argv 16]
if {$serial_rx_crc eq ""} { set serial_rx_crc 0 }
if {$serial_rx_crc ni {0 1} || ($serial_rx_crc && !$serial_control_crc)} { error "Invalid serial RX CRC flag" }
set recursive_correlator [lindex $argv 17]
if {$recursive_correlator eq ""} { set recursive_correlator 0 }
if {$recursive_correlator ni {0 1} || ($recursive_correlator && !$serial_rx_crc)} { error "Invalid recursive correlator flag" }
set peaks_to_software [lindex $argv 18]
if {$peaks_to_software eq ""} { set peaks_to_software 0 }
if {$peaks_to_software ni {0 1} || ($peaks_to_software && !$serial_rx_crc)} { error "Invalid peak offload flag" }
set serial_barker [lindex $argv 19]
if {$serial_barker eq ""} { set serial_barker 0 }
if {$serial_barker ni {0 1} || ($serial_barker && (!$peaks_to_software || $recursive_correlator))} { error "Invalid serial Barker flag" }
set compact_fanout [lindex $argv 20]
if {$compact_fanout eq ""} { set compact_fanout 0 }
if {$compact_fanout ni {0 1} || ($compact_fanout && (!$peaks_to_software || $serial_barker || $recursive_correlator))} { error "Invalid compact serial fanout flag" }
if {$spi_cs ni {0 1}} { error "SPI select must match the installed Linux image (0 or 1)" }
proc check_ps_axi_clocks {} {
set expected [get_nets -of_objects [get_pins {ps7_i/MAXIGP0ACLK}]]
if {[llength $expected] != 1} { error "No unique GP0 clock" }
foreach port {MAXIGP0ACLK MAXIGP1ACLK SAXIGP0ACLK SAXIGP1ACLK SAXIHP0ACLK SAXIHP1ACLK SAXIHP2ACLK SAXIHP3ACLK SAXIACPACLK} {
set actual [get_nets -of_objects [get_pins ps7_i/$port]]
if {$actual ne $expected} { error "PS AXI clock $port does not use the running GP0 clock: $actual" }
}
puts "E310_PS_AXI_CLOCK_CONTRACT_PASS ports=9 physical_clock_tested=false"
}
if {[lindex $argv 3] eq "inspect"} {
open_checkpoint [file join $out routed.dcp]
report_utilization -hierarchical -hierarchical_depth 6 -file [file join $out utilization_hierarchical.rpt]
report_cdc -details -file [file join $out cdc_details.rpt]
set ps_report [open [file join $out ps7_connections.rpt] w]
puts $ps_report "BITSTREAM.CONFIG.UNUSEDPIN=[get_property BITSTREAM.CONFIG.UNUSEDPIN [current_design]]"
puts "E310_UNUSED_PIN_CONFIGURATION value=[get_property BITSTREAM.CONFIG.UNUSEDPIN [current_design]]"
foreach pin [get_pins -of_objects [get_cells ps7_i]] {
puts $ps_report "[get_property NAME $pin] direction=[get_property DIRECTION $pin] nets=[get_nets -quiet -of_objects $pin]"
}
foreach port [get_ports -quiet {MIO* DDR* PS_*}] {
puts $ps_report "TOP [get_property NAME $port] [get_nets -quiet -of_objects $port]"
}
close $ps_report
check_ps_axi_clocks
exit
}
set rebitgen_source [lindex $argv 6]
if {$rebitgen_source ne "" && $rebitgen_source ne "-"} {
open_checkpoint $rebitgen_source
puts "E310_REBITGEN_SAME_ROUTED_LOGIC source=$rebitgen_source"
} else {
if {[lindex $argv 5] eq "1"} {
# Explicit recovery of the just-created, constrained synthesis checkpoint.
# Still run optimization, routing, all reports and the timing gates.
set_param general.maxThreads 8
open_checkpoint [file join $out synthesized.dcp]
} else {
set hitl [file dirname $project]
set open [file join $project fpga open_e310]
set sifs [file join $hitl wifi_pluto_link fpga_sifs rtl]
set io [file join $hitl wifi_pluto_link fpga_sifs open_xc7]
set control [file join $hitl vendor_uhd_4_9 fpga usrp3 lib control]
set_param general.maxThreads 8
create_project -in_memory -part $part
if {$compact_fanout} { set_property verilog_define {GF_COMPACT_SERIAL_FANOUT} [current_fileset] }
foreach src [list \
[file join $control synchronizer_impl.v] \
[file join $control synchronizer.v] \
[file join $hitl vendor_uhd_4_9 fpga usrp3 top e31x spi_slave.v] \
[file join $open gf_e310_pmu_regs.sv] \
[file join $io gf_e310_io_open.sv] \
[file join $sifs gf_sifs_scheduler.sv] \
[file join $sifs gf_dsss_1mbps_control_tx.sv] \
[file join $sifs gf_dsss_sifs_island.sv] \
[file join $sifs gf_low_mac_classifier.sv] \
[file join $sifs gf_dsss_sifs_low_mac.sv] \
[file join $sifs gf_dsss_1mbps_rx.sv] \
[file join $open gf_serial_mul40.sv] \
[file join $open gf_serial_differential.sv] \
[file join $open gf_barker_serial.sv] \
[file join $open gf_barker_serial_iq.sv] \
[file join $open gf_barker_radio_bridge.sv] \
[file join $open gf_e310_serial_clock.sv] \
[file join $sifs gf_dsss_rx_sifs_ap.sv] \
[file join $project fpga rtl gf_e31x_sifs_inline.sv] \
[file join $open gf_e310_async_fifo.sv] \
[file join $open gf_dsss_1mbps_psdu_tx.sv] \
[file join $open gf_host_waveform_tx.sv] \
[file join $open gf_e310_frontend_2g4.sv] \
[file join $open gf_e310_gp0_regs.sv] \
[file join $open gf_e310_open_shell_top.sv]] {
read_verilog -sv $src
}
synth_design -top gf_e310_open_shell_top -part $part -flatten_hierarchy none -generic [list USE_RX_BUFR=1 SPI_CS_INDEX=$spi_cs USE_HOST_WAVEFORM=$host_waveform USE_TX_BLOCK_RAM=$tx_block_ram COUNTERS_TO_SOFTWARE=$counters_to_software RX_FIFO_BLOCK_RAM=$rx_fifo_block_ram SINGLE_PHASE_RX=$single_phase_rx SERIAL_DIFFERENTIAL=$serial_differential TIMING_SCORE_RAM=$timing_score_ram SERIAL_CONTROL_CRC=$serial_control_crc SERIAL_RX_CRC=$serial_rx_crc RECURSIVE_CORRELATOR=$recursive_correlator PEAKS_TO_SOFTWARE=$peaks_to_software SERIAL_BARKER=$serial_barker]
if {$peaks_to_software && [llength [get_cells -quiet -hier -filter {NAME =~ *rx_diagnostic*peak* && IS_SEQUENTIAL == 1}]]} { error "Diagnostic peak tracking remains in FPGA" }
if {$serial_differential && [llength [get_cells -quiet -hier -filter {REF_NAME =~ DSP*}]] != 0} { error "Serial RX unexpectedly retains a DSP" }
if {$timing_score_ram && ![llength [get_cells -quiet -hier -filter {NAME =~ *g_score_ram* && REF_NAME =~ RAM*}]]} { error "Timing scores did not infer RAM" }
write_checkpoint -force [file join $out synthesized_unconstrained.dcp]
# Reuse the same package constraints, excluding nextpnr's BUFG-output clock.
# Vivado propagates its clock from the external input port instead.
set pin_file [open [file join $open e310_open_shell.xdc] r]
set pin_constraints [read $pin_file]
close $pin_file
set removed [regsub -all -line {^create_clock[^\n]*$} $pin_constraints {} pin_constraints]
if {$removed != 1} { error "Expected exactly one nextpnr clock in package constraints" }
eval $pin_constraints
create_clock -name radio_clk -period 25.000 [get_ports CAT_DATA_CLK]
create_clock -name bus_clk -period 10.000 [get_pins {ps7_i/FCLKCLK[0]}]
set_input_jitter bus_clk 0.300
set_input_jitter radio_clk 2.5005
create_generated_clock -name CAT_FB_CLK -multiply_by 1 \
-source [get_pins ad9361_io/clock_output_ddr/C] [get_ports CAT_FB_CLK]
set_clock_groups -asynchronous \
-group [get_clocks -include_generated_clocks radio_clk] \
-group [get_clocks bus_clk]
# Same non-clock-capable E310 package input exception as Ettus e31x_pins.xdc.
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets -of_objects [get_pins ad9361_io/radio_clock_buffer/I]]
set capture_buffers [get_cells -hier -filter {REF_NAME == BUFR}]
if {[llength $capture_buffers] != 1} { error "Expected one regional RX capture buffer" }
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets -of_objects [get_pins -of_objects $capture_buffers -filter {REF_PIN_NAME == I}]]
# Stock E310 AD9361 delay settings: 4.5 ns programmed data delay. Physical
# setup must confirm these settings before this I/O timing model applies.
foreach edge {rise fall} {
set extra {}
if {$edge eq "fall"} { set extra {-clock_fall -add_delay} }
set_input_delay -clock radio_clk -max 5.7 {*}$extra [get_ports {CAT_P0_D* CAT_RX_FRAME}]
set_input_delay -clock radio_clk -min 4.5 {*}$extra [get_ports {CAT_P0_D* CAT_RX_FRAME}]
set_output_delay -clock CAT_FB_CLK -max 5.5 {*}$extra [get_ports {CAT_P1_D* CAT_TX_FRAME}]
set_output_delay -clock CAT_FB_CLK -min 4.5 {*}$extra [get_ports {CAT_P1_D* CAT_TX_FRAME}]
}
foreach {pin port} [list EMIOSPI0MO CAT_MOSI EMIOSPI0SCLKO CAT_SCLK [format {EMIOSPI0SSON[%d]} $spi_cs] CAT_CS] {
set_max_delay 10 -from [get_pins ps7_i/$pin] -to [get_ports $port] -datapath_only
set_min_delay 1 -to [get_ports $port]
}
set_max_delay 10 -from [get_ports CAT_MISO] -to [get_pins ps7_i/EMIOSPI0MI] -datapath_only
set_min_delay 1 -from [get_ports CAT_MISO] -to [get_pins ps7_i/EMIOSPI0MI]
# Preserve Gray pointer coherence independently of the asynchronous clock cut.
foreach {src dst} {write_gray_reg write_gray_read_sync_1_reg read_gray_reg read_gray_write_sync_1_reg gray_radio_reg gray_sync_1_reg} {
set starts [get_cells -hier -filter "NAME =~ */${src}* && IS_SEQUENTIAL"]
set ends [get_cells -hier -filter "NAME =~ */${dst}* && IS_SEQUENTIAL"]
if {[llength $starts] && [llength $ends]} { set_bus_skew 10 -from $starts -to $ends }
}
if {$serial_barker} { source [file join $project tools constrain_barker_radio_bridge.tcl] }
write_checkpoint -force [file join $out synthesized.dcp]
if {[lindex $argv 3] eq "synth"} { puts "E310_CONSTRAINED_SYNTHESIS_PASS hardware_loaded=false";exit 0 }
}
opt_design
place_design
phys_opt_design
route_design
}
check_ps_axi_clocks
set unused_pull [lindex $argv 7]
# The identical routed image passed physical runtime and watchdog/relogin only
# after disabling unused-pin pulls. Do not silently restore Vivado's default.
if {$unused_pull eq "" || $unused_pull eq "-"} { set unused_pull Pullnone }
if {$unused_pull ne "" && $unused_pull ne "-"} {
if {$unused_pull ni {Pullnone Pulldown}} { error "Unsupported unused-pin setting" }
set_property BITSTREAM.CONFIG.UNUSEDPIN $unused_pull [current_design]
puts "E310_EXPLICIT_UNUSED_PIN_CONFIGURATION value=[get_property BITSTREAM.CONFIG.UNUSEDPIN [current_design]]"
}
report_timing_summary -delay_type min_max -report_unconstrained -file [file join $out timing.rpt]
report_utilization -file [file join $out utilization.rpt]
if {$compact_fanout} { report_utilization -hierarchical -hierarchical_depth 6 -file [file join $out utilization_hierarchical.rpt] }
report_cdc -file [file join $out cdc.rpt]
report_bus_skew -file [file join $out bus_skew.rpt]
report_drc -file [file join $out drc.rpt]
if {$serial_barker} { source [file join $project tools check_barker_radio_fanout.tcl] }
write_checkpoint -force [file join $out routed.dcp]
set setup [get_timing_paths -delay_type max -max_paths 1]
set hold [get_timing_paths -delay_type min -max_paths 1]
if {![llength $setup] || ![llength $hold]} { error "No timed paths" }
if {[get_property SLACK $setup] < 0 || [get_property SLACK $hold] < 0} {
error "Timing failed; inspect timing.rpt"
}
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
write_bitstream -force -bin_file [file join $out gf_e310_minimal.bit]
puts "E310_MINIMAL_VIVADO_ROUTE_PASS part=$part hardware_loaded=false"
wifi_e310_link/vendor/uhd_3_10_ad9361/SOURCE.md · 11 lines
Download this file · Permanent section link
# Legacy AD9361 calibration driver
Unmodified files from Ettus UHD tag `release_003_010_001_001`, matching the
installed E310 library. Source directory:
https://github.com/EttusResearch/uhd/tree/release_003_010_001_001/host/lib/usrp/common/ad9361_driver
The six source/header files retain their upstream GPL-3.0-or-later notices.
They are used only by the separate native radio-setup executable. The AP
protocol implementation does not link this driver. No FPGA waveform tables
are generated from these RFIC calibration/filter tables.
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_client.h · 87 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef INCLUDED_AD9361_CLIENT_H
#define INCLUDED_AD9361_CLIENT_H
#include <boost/shared_ptr.hpp>
namespace uhd { namespace usrp {
/*!
* Frequency band settings
*/
typedef enum {
AD9361_RX_BAND0,
AD9361_RX_BAND1,
AD9361_TX_BAND0
} frequency_band_t;
/*!
* Clocking mode
*/
typedef enum {
AD9361_XTAL_P_CLK_PATH,
AD9361_XTAL_N_CLK_PATH
} clocking_mode_t;
/*!
* Digital interface specific
*/
typedef enum {
AD9361_DDR_FDD_LVCMOS,
AD9361_DDR_FDD_LVDS
} digital_interface_mode_t;
/*!
* Interface timing
*/
typedef struct {
uint8_t rx_clk_delay;
uint8_t rx_data_delay;
uint8_t tx_clk_delay;
uint8_t tx_data_delay;
} digital_interface_delays_t;
class ad9361_params {
public:
typedef boost::shared_ptr<ad9361_params> sptr;
virtual ~ad9361_params() {}
virtual digital_interface_delays_t get_digital_interface_timing() = 0;
virtual digital_interface_mode_t get_digital_interface_mode() = 0;
virtual clocking_mode_t get_clocking_mode() = 0;
virtual double get_band_edge(frequency_band_t band) = 0;
};
class ad9361_io
{
public:
typedef boost::shared_ptr<ad9361_io> sptr;
virtual ~ad9361_io() {}
virtual uint8_t peek8(uint32_t reg) = 0;
virtual void poke8(uint32_t reg, uint8_t val) = 0;
};
}}
#endif /* INCLUDED_AD9361_CLIENT_H */
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_device.cpp · 2845 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "ad9361_filter_taps.h"
#include "ad9361_gain_tables.h"
#include "ad9361_synth_lut.h"
#include "ad9361_client.h"
#include "ad9361_device.h"
#define _USE_MATH_DEFINES
#include <cmath>
#include <uhd/exception.hpp>
#include <uhd/utils/log.hpp>
#include <uhd/utils/msg.hpp>
#include <stdint.h>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/thread/thread.hpp>
#include <boost/scoped_array.hpp>
#include <boost/format.hpp>
#include <boost/math/special_functions.hpp>
////////////////////////////////////////////////////////////
// the following macros evaluate to a compile time constant
// macros By Tom Torfs - donated to the public domain
/* turn a numeric literal into a hex constant
(avoids problems with leading zeroes)
8-bit constants max value 0x11111111, always fits in unsigned long
*/
#define HEX__(n) 0x##n##LU
/* 8-bit conversion function */
#define B8__(x) ((x&0x0000000FLU)?1:0) \
+((x&0x000000F0LU)?2:0) \
+((x&0x00000F00LU)?4:0) \
+((x&0x0000F000LU)?8:0) \
+((x&0x000F0000LU)?16:0) \
+((x&0x00F00000LU)?32:0) \
+((x&0x0F000000LU)?64:0) \
+((x&0xF0000000LU)?128:0)
/* for upto 8-bit binary constants */
#define B8(d) ((unsigned char)B8__(HEX__(d)))
////////////////////////////////////////////////////////////
namespace uhd { namespace usrp {
/* This is a simple comparison for very large double-precision floating
* point numbers. It is used to prevent re-tunes for frequencies that are
* the same but not 'exactly' because of data precision issues. */
// TODO: see if we can avoid the need for this function
int freq_is_nearly_equal(double a, double b) {
return std::max(a,b) - std::min(a,b) < 1;
}
/***********************************************************************
* Filter functions
**********************************************************************/
/* This function takes in the calculated maximum number of FIR taps, and
* returns a number of taps that makes AD9361 happy. */
int get_num_taps(int max_num_taps) {
int num_taps = 0;
int num_taps_list[] = {16, 32, 48, 64, 80, 96, 112, 128};
int i;
for(i = 1; i < 8; i++) {
if(max_num_taps >= num_taps_list[i]) {
continue;
} else {
num_taps = num_taps_list[i - 1];
break;
}
} if(num_taps == 0) { num_taps = 128; }
return num_taps;
}
const double ad9361_device_t::AD9361_MAX_GAIN = 89.75;
const double ad9361_device_t::AD9361_MIN_CLOCK_RATE = 220e3;
const double ad9361_device_t::AD9361_MAX_CLOCK_RATE = 61.44e6;
const double ad9361_device_t::AD9361_CAL_VALID_WINDOW = 100e6;
// Max bandwdith is due to filter rolloff in analog filter stage
const double ad9361_device_t::AD9361_RECOMMENDED_MAX_BANDWIDTH = 56e6;
/* Startup RF frequencies */
const double ad9361_device_t::DEFAULT_RX_FREQ = 800e6;
const double ad9361_device_t::DEFAULT_TX_FREQ = 850e6;
/* Program either the RX or TX FIR filter.
*
* The process is the same for both filters, but the function must be told
* how many taps are in the filter, and given a vector of the taps
* themselves. */
void ad9361_device_t::_program_fir_filter(direction_t direction, chain_t chain, int num_taps, uint16_t *coeffs)
{
uint16_t base;
/* RX and TX filters use largely identical sets of programming registers.
Select the appropriate bank of registers here. */
if (direction == RX) {
base = 0x0f0;
} else {
base = 0x060;
}
/* Encode number of filter taps for programming register */
uint8_t reg_numtaps = (((num_taps / 16) - 1) & 0x07) << 5;
uint8_t reg_chain = 0;
switch (chain) {
case CHAIN_1:
reg_chain = 0x01 << 3;
break;
case CHAIN_2:
reg_chain = 0x02 << 3;
break;
default:
reg_chain = 0x03 << 3;
}
/* Turn on the filter clock. */
_io_iface->poke8(base + 5, reg_numtaps | reg_chain | 0x02);
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
/* Zero the unused taps just in case they have stale data */
int addr;
for (addr = num_taps; addr < 128; addr++) {
_io_iface->poke8(base + 0, addr);
_io_iface->poke8(base + 1, 0x0);
_io_iface->poke8(base + 2, 0x0);
_io_iface->poke8(base + 5, reg_numtaps | reg_chain | (1 << 1) | (1 << 2));
_io_iface->poke8(base + 4, 0x00);
_io_iface->poke8(base + 4, 0x00);
}
/* Iterate through indirect programming of filter coeffs using ADI recomended procedure */
for (addr = 0; addr < num_taps; addr++) {
_io_iface->poke8(base + 0, addr);
_io_iface->poke8(base + 1, (coeffs[addr]) & 0xff);
_io_iface->poke8(base + 2, (coeffs[addr] >> 8) & 0xff);
_io_iface->poke8(base + 5, reg_numtaps | reg_chain | (1 << 1) | (1 << 2));
_io_iface->poke8(base + 4, 0x00);
_io_iface->poke8(base + 4, 0x00);
}
/* UG-671 states (page 25) (paraphrased and clarified):
" After the table has been programmed, write to register BASE+5 with the write bit D2 cleared and D1 high.
Then, write to register BASE+5 again with D1 clear, thus ensuring that the write bit resets internally
before the clock stops. Wait 4 sample clock periods after setting D2 high while that data writes into the table"
*/
_io_iface->poke8(base + 5, reg_numtaps | reg_chain | (1 << 1));
if (direction == RX) {
_io_iface->poke8(base + 5, reg_numtaps | reg_chain );
/* Rx Gain, set to prevent digital overflow/saturation in filters
0:+6dB, 1:0dB, 2:-6dB, 3:-12dB
page 35 of UG-671 */
_io_iface->poke8(base + 6, 0x02); /* Also turn on -6dB Rx gain here, to stop filter overfow.*/
} else {
/* Tx Gain. bit[0]. set to prevent digital overflow/saturation in filters
0: 0dB, 1:-6dB
page 25 of UG-671 */
_io_iface->poke8(base + 5, reg_numtaps | reg_chain );
}
}
/* Program the RX FIR Filter. */
void ad9361_device_t::_setup_rx_fir(size_t num_taps, int32_t decimation)
{
if (not (decimation == 1 or decimation == 2 or decimation == 4)) {
throw uhd::runtime_error("[ad9361_device_t] Invalid Rx FIR decimation.");
}
boost::scoped_array<uint16_t> coeffs(new uint16_t[num_taps]);
for (size_t i = 0; i < num_taps; i++) {
switch (num_taps) {
case 128:
coeffs[i] = uint16_t((decimation==4) ? fir_128_x4_coeffs[i] : hb127_coeffs[i]);
break;
case 96:
coeffs[i] = uint16_t((decimation==4) ? fir_96_x4_coeffs[i] : hb95_coeffs[i]);
break;
case 64:
coeffs[i] = uint16_t((decimation==4) ? fir_64_x4_coeffs[i] : hb63_coeffs[i]);
break;
case 48:
coeffs[i] = uint16_t((decimation==4) ? fir_48_x4_coeffs[i] : hb47_coeffs[i]);
break;
default:
throw uhd::runtime_error("[ad9361_device_t] Unsupported number of Rx FIR taps.");
}
}
_program_fir_filter(RX, CHAIN_BOTH, num_taps, coeffs.get());
}
/* Program the TX FIR Filter. */
void ad9361_device_t::_setup_tx_fir(size_t num_taps, int32_t interpolation)
{
if (not (interpolation == 1 or interpolation == 2 or interpolation == 4)) {
throw uhd::runtime_error("[ad9361_device_t] Invalid Tx FIR interpolation.");
}
if (interpolation == 1 and num_taps > 64) {
throw uhd::runtime_error("[ad9361_device_t] Too many Tx FIR taps for interpolation value.");
}
boost::scoped_array<uint16_t> coeffs(new uint16_t[num_taps]);
for (size_t i = 0; i < num_taps; i++) {
switch (num_taps) {
case 128:
coeffs[i] = uint16_t((interpolation==4) ? fir_128_x4_coeffs[i] : hb127_coeffs[i]);
break;
case 96:
coeffs[i] = uint16_t((interpolation==4) ? fir_96_x4_coeffs[i] : hb95_coeffs[i]);
break;
case 64:
coeffs[i] = uint16_t((interpolation==4) ? fir_64_x4_coeffs[i] : hb63_coeffs[i]);
break;
case 48:
coeffs[i] = uint16_t((interpolation==4) ? fir_48_x4_coeffs[i] : hb47_coeffs[i]);
break;
default:
throw uhd::runtime_error("[ad9361_device_t] Unsupported number of Tx FIR taps.");
}
}
_program_fir_filter(TX, CHAIN_BOTH, num_taps, coeffs.get());
}
/***********************************************************************
* Calibration functions
***********************************************************************/
/* Calibrate and lock the BBPLL.
*
* This function should be called anytime the BBPLL is tuned. */
void ad9361_device_t::_calibrate_lock_bbpll()
{
_io_iface->poke8(0x03F, 0x05); // Start the BBPLL calibration
_io_iface->poke8(0x03F, 0x01); // Clear the 'start' bit
/* Increase BBPLL KV and phase margin. */
_io_iface->poke8(0x04c, 0x86);
_io_iface->poke8(0x04d, 0x01);
_io_iface->poke8(0x04d, 0x05);
/* Wait for BBPLL lock. */
size_t count = 0;
while (!(_io_iface->peek8(0x05e) & 0x80)) {
if (count > 1000) {
throw uhd::runtime_error("[ad9361_device_t] BBPLL not locked");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(2));
}
}
/* Calibrate the synthesizer charge pumps.
*
* Technically, this calibration only needs to be done once, at device
* initialization. */
void ad9361_device_t::_calibrate_synth_charge_pumps()
{
/* If this function ever gets called, and the ENSM isn't already in the
* ALERT state, then something has gone horribly wrong. */
if ((_io_iface->peek8(0x017) & 0x0F) != 5) {
throw uhd::runtime_error("[ad9361_device_t] AD9361 not in ALERT during cal");
}
/* Calibrate the RX synthesizer charge pump. */
size_t count = 0;
_io_iface->poke8(0x23d, 0x04);
while (!(_io_iface->peek8(0x244) & 0x80)) {
if (count > 5) {
throw uhd::runtime_error("[ad9361_device_t] RX charge pump cal failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
}
_io_iface->poke8(0x23d, 0x00);
/* Calibrate the TX synthesizer charge pump. */
count = 0;
_io_iface->poke8(0x27d, 0x04);
while (!(_io_iface->peek8(0x284) & 0x80)) {
if (count > 5) {
throw uhd::runtime_error("[ad9361_device_t] TX charge pump cal failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
}
_io_iface->poke8(0x27d, 0x00);
}
/* Calibrate the analog BB RX filter.
*
* Note that the filter calibration depends heavily on the baseband
* bandwidth, so this must be re-done after any change to the RX sample
* rate.
* UG570 Page 33 states that this filter should be calibrated to 1.4 * bbbw*/
double ad9361_device_t::_calibrate_baseband_rx_analog_filter(double req_rfbw)
{
double bbbw = req_rfbw / 2.0;
if(bbbw > _baseband_bw / 2.0)
{
UHD_LOG << "baseband bandwidth too large for current sample rate. Setting bandwidth to: "<<_baseband_bw;
bbbw = _baseband_bw / 2.0;
}
/* Baseband BW must be between 28e6 and 0.143e6.
* Max filter BW is 39.2 MHz. 39.2 / 1.4 = 28
* Min filter BW is 200kHz. 200 / 1.4 = 143 */
if (bbbw > 28e6) {
bbbw = 28e6;
} else if (bbbw < 0.143e6) {
bbbw = 0.143e6;
}
double rxtune_clk = ((1.4 * bbbw * 2 * M_PI) / M_LN2);
_rx_bbf_tunediv = std::min<uint16_t>(511, uint16_t(std::ceil(_bbpll_freq / rxtune_clk)));
_regs.bbftune_config = (_regs.bbftune_config & 0xFE)
| ((_rx_bbf_tunediv >> 8) & 0x0001);
double bbbw_mhz = bbbw / 1e6;
double temp = ((bbbw_mhz - std::floor(bbbw_mhz)) * 1000) / 7.8125;
uint8_t bbbw_khz = std::min<uint8_t>(127, uint8_t(std::floor(temp + 0.5)));
/* Set corner frequencies and dividers. */
_io_iface->poke8(0x1fb, (uint8_t) (bbbw_mhz));
_io_iface->poke8(0x1fc, bbbw_khz);
_io_iface->poke8(0x1f8, (_rx_bbf_tunediv & 0x00FF));
_io_iface->poke8(0x1f9, _regs.bbftune_config);
/* RX Mix Voltage settings - only change with apps engineer help. */
_io_iface->poke8(0x1d5, 0x3f);
_io_iface->poke8(0x1c0, 0x03);
/* Enable RX1 & RX2 filter tuners. */
_io_iface->poke8(0x1e2, 0x02);
_io_iface->poke8(0x1e3, 0x02);
/* Run the calibration! */
size_t count = 0;
_io_iface->poke8(0x016, 0x80);
while (_io_iface->peek8(0x016) & 0x80) {
if (count > 100) {
throw uhd::runtime_error("[ad9361_device_t] RX baseband filter cal FAILURE");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
}
/* Disable RX1 & RX2 filter tuners. */
_io_iface->poke8(0x1e2, 0x03);
_io_iface->poke8(0x1e3, 0x03);
return bbbw;
}
/* Calibrate the analog BB TX filter.
*
* Note that the filter calibration depends heavily on the baseband
* bandwidth, so this must be re-done after any change to the TX sample
* rate.
* UG570 Page 32 states that this filter should be calibrated to 1.6 * bbbw*/
double ad9361_device_t::_calibrate_baseband_tx_analog_filter(double req_rfbw)
{
double bbbw = req_rfbw / 2.0;
if(bbbw > _baseband_bw / 2.0)
{
UHD_LOG << "baseband bandwidth too large for current sample rate. Setting bandwidth to: "<<_baseband_bw;
bbbw = _baseband_bw / 2.0;
}
/* Baseband BW must be between 20e6 and 0.391e6.
* Max filter BW is 32 MHz. 32 / 1.6 = 20
* Min filter BW is 625 kHz. 625 / 1.6 = 391 */
if (bbbw > 20e6) {
bbbw = 20e6;
} else if (bbbw < 0.391e6) {
bbbw = 0.391e6;
}
double txtune_clk = ((1.6 * bbbw * 2 * M_PI) / M_LN2);
uint16_t txbbfdiv = std::min<uint16_t>(511, uint16_t(std::ceil(_bbpll_freq / txtune_clk)));
_regs.bbftune_mode = (_regs.bbftune_mode & 0xFE)
| ((txbbfdiv >> 8) & 0x0001);
/* Program the divider values. */
_io_iface->poke8(0x0d6, (txbbfdiv & 0x00FF));
_io_iface->poke8(0x0d7, _regs.bbftune_mode);
/* Enable the filter tuner. */
_io_iface->poke8(0x0ca, 0x22);
/* Calibrate! */
size_t count = 0;
_io_iface->poke8(0x016, 0x40);
while (_io_iface->peek8(0x016) & 0x40) {
if (count > 100) {
throw uhd::runtime_error("[ad9361_device_t] TX baseband filter cal FAILURE");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
}
/* Disable the filter tuner. */
_io_iface->poke8(0x0ca, 0x26);
return bbbw;
}
/* Calibrate the secondary TX filter.
*
* This filter also depends on the TX sample rate, so if a rate change is
* made, the previous calibration will no longer be valid.
* UG570 Page 32 states that this filter should be calibrated to 5 * bbbw*/
double ad9361_device_t::_calibrate_secondary_tx_filter(double req_rfbw)
{
double bbbw = req_rfbw / 2.0;
if(bbbw > _baseband_bw / 2.0)
{
UHD_LOG << "baseband bandwidth too large for current sample rate. Setting bandwidth to: "<<_baseband_bw;
bbbw = _baseband_bw / 2.0;
}
/* Baseband BW must be between 20e6 and 0.54e6.
* Max filter BW is 100 MHz. 100 / 5 = 20
* Min filter BW is 2.7 MHz. 2.7 / 5 = 0.54 */
if (bbbw > 20e6) {
bbbw = 20e6;
} else if (bbbw < 0.54e6) {
bbbw = 0.54e6;
}
double bbbw_mhz = bbbw / 1e6;
/* Start with a resistor value of 100 Ohms. */
int res = 100;
/* Calculate target corner frequency. */
double corner_freq = 5 * bbbw_mhz * 2 * M_PI;
/* Iterate through RC values to determine correct combination. */
int cap = 0;
int i;
for (i = 0; i <= 3; i++) {
cap = static_cast<int>(std::floor(0.5 + ((1 / ((corner_freq * res) * 1e6)) * 1e12)))
- 12;
if (cap <= 63) {
break;
}
res = res * 2;
}
if (cap > 63) {
cap = 63;
}
uint8_t reg0d0, reg0d1, reg0d2;
/* Translate baseband bandwidths to register settings. */
if ((bbbw_mhz * 2) <= 9) {
reg0d0 = 0x59;
} else if (((bbbw_mhz * 2) > 9) && ((bbbw_mhz * 2) <= 24)) {
reg0d0 = 0x56;
} else if ((bbbw_mhz * 2) > 24) {
reg0d0 = 0x57;
} else {
reg0d0 = 0x00;
throw uhd::runtime_error("[ad9361_device_t] Cal2ndTxFil: INVALID_CODE_PATH bad bbbw_mhz");
}
/* Translate resistor values to register settings. */
if (res == 100) {
reg0d1 = 0x0c;
} else if (res == 200) {
reg0d1 = 0x04;
} else if (res == 400) {
reg0d1 = 0x03;
} else if (res == 800) {
reg0d1 = 0x01;
} else {
reg0d1 = 0x0c;
}
reg0d2 = cap;
/* Program the above-calculated values. Sweet. */
_io_iface->poke8(0x0d2, reg0d2);
_io_iface->poke8(0x0d1, reg0d1);
_io_iface->poke8(0x0d0, reg0d0);
return bbbw;
}
/* Calibrate the RX TIAs.
*
* Note that the values in the TIA register, after calibration, vary with
* the RX gain settings.
* We do not really program the BW here. Most settings are taken form the BB LPF registers
* UG570 page 33 states that this filter should be calibrated to 2.5 * bbbw */
double ad9361_device_t::_calibrate_rx_TIAs(double req_rfbw)
{
uint8_t reg1eb = _io_iface->peek8(0x1eb) & 0x3F;
uint8_t reg1ec = _io_iface->peek8(0x1ec) & 0x7F;
uint8_t reg1e6 = _io_iface->peek8(0x1e6) & 0x07;
uint8_t reg1db = 0x00;
uint8_t reg1dc = 0x00;
uint8_t reg1dd = 0x00;
uint8_t reg1de = 0x00;
uint8_t reg1df = 0x00;
double bbbw = req_rfbw / 2.0;
if(bbbw > _baseband_bw / 2.0)
{
UHD_LOG << "baseband bandwidth too large for current sample rate. Setting bandwidth to: "<<_baseband_bw;
bbbw = _baseband_bw / 2.0;
}
/* Baseband BW must be between 28e6 and 0.4e6.
* Max filter BW is 70 MHz. 70 / 2.5 = 28
* Min filter BW is 1 MHz. 1 / 2.5 = 0.4*/
if (bbbw > 28e6) {
bbbw = 28e6;
} else if (bbbw < 0.40e6) {
bbbw = 0.40e6;
}
double ceil_bbbw_mhz = std::ceil(bbbw / 1e6);
/* Do some crazy resistor and capacitor math. */
int Cbbf = (reg1eb * 160) + (reg1ec * 10) + 140;
int R2346 = 18300 * (reg1e6 & 0x07);
double CTIA_fF = (Cbbf * R2346 * 0.56) / 3500;
/* Translate baseband BW to register settings. */
if (ceil_bbbw_mhz <= 3) {
reg1db = 0xe0;
} else if ((ceil_bbbw_mhz > 3) && (ceil_bbbw_mhz <= 10)) {
reg1db = 0x60;
} else if (ceil_bbbw_mhz > 10) {
reg1db = 0x20;
} else {
throw uhd::runtime_error("[ad9361_device_t] CalRxTias: INVALID_CODE_PATH bad bbbw_mhz");
}
if (CTIA_fF > 2920) {
reg1dc = 0x40;
reg1de = 0x40;
uint8_t temp = (uint8_t) std::min<uint8_t>(127,
uint8_t(std::floor(0.5 + ((CTIA_fF - 400.0) / 320.0))));
reg1dd = temp;
reg1df = temp;
} else {
uint8_t temp = uint8_t(std::floor(0.5 + ((CTIA_fF - 400.0) / 40.0)) + 0x40);
reg1dc = temp;
reg1de = temp;
reg1dd = 0;
reg1df = 0;
}
/* w00t. Settings calculated. Program them and roll out. */
_io_iface->poke8(0x1db, reg1db);
_io_iface->poke8(0x1dd, reg1dd);
_io_iface->poke8(0x1df, reg1df);
_io_iface->poke8(0x1dc, reg1dc);
_io_iface->poke8(0x1de, reg1de);
return bbbw;
}
/* Setup the AD9361 ADC.
*
* There are 40 registers that control the ADC's operation, most of the
* values of which must be derived mathematically, dependent on the current
* setting of the BBPLL. Note that the order of calculation is critical, as
* some of the 40 registers depend on the values in others. */
void ad9361_device_t::_setup_adc()
{
double bbbw_mhz = (((_bbpll_freq / 1e6) / _rx_bbf_tunediv) * M_LN2) \
/ (1.4 * 2 * M_PI);
/* For calibration, baseband BW is half the complex BW, and must be
* between 28e6 and 0.2e6. */
if(bbbw_mhz > 28) {
bbbw_mhz = 28;
} else if (bbbw_mhz < 0.20) {
bbbw_mhz = 0.20;
}
uint8_t rxbbf_c3_msb = _io_iface->peek8(0x1eb) & 0x3F;
uint8_t rxbbf_c3_lsb = _io_iface->peek8(0x1ec) & 0x7F;
uint8_t rxbbf_r2346 = _io_iface->peek8(0x1e6) & 0x07;
double fsadc = _adcclock_freq / 1e6;
/* Sort out the RC time constant for our baseband bandwidth... */
double rc_timeconst = 0.0;
if(bbbw_mhz < 18) {
rc_timeconst = (1 / ((1.4 * 2 * M_PI) \
* (18300 * rxbbf_r2346)
* ((160e-15 * rxbbf_c3_msb)
+ (10e-15 * rxbbf_c3_lsb) + 140e-15)
* (bbbw_mhz * 1e6)));
} else {
rc_timeconst = (1 / ((1.4 * 2 * M_PI) \
* (18300 * rxbbf_r2346)
* ((160e-15 * rxbbf_c3_msb)
+ (10e-15 * rxbbf_c3_lsb) + 140e-15)
* (bbbw_mhz * 1e6) * (1 + (0.01 * (bbbw_mhz - 18)))));
}
double scale_res = sqrt(1 / rc_timeconst);
double scale_cap = sqrt(1 / rc_timeconst);
double scale_snr = (_adcclock_freq < 80e6) ? 1.0 : 1.584893192;
double maxsnr = 640 / 160;
/* Calculate the values for all 40 settings registers.
*
* DO NOT TOUCH THIS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING. kthx.*/
uint8_t data[40];
data[0] = 0; data[1] = 0; data[2] = 0; data[3] = 0x24;
data[4] = 0x24; data[5] = 0; data[6] = 0;
data[7] = std::min<uint8_t>(124, uint8_t(std::floor(-0.5
+ (80.0 * scale_snr * scale_res
* std::min<double>(1.0, sqrt(maxsnr * fsadc / 640.0))))));
double data007 = data[7];
data[8] = std::min<uint8_t>(255, uint8_t(std::floor(0.5
+ ((20.0 * (640.0 / fsadc) * ((data007 / 80.0))
/ (scale_res * scale_cap))))));
data[10] = std::min<uint8_t>(127, uint8_t(std::floor(-0.5 + (77.0 * scale_res
* std::min<double>(1.0, sqrt(maxsnr * fsadc / 640.0))))));
double data010 = data[10];
data[9] = std::min<uint8_t>(127, uint8_t(std::floor(0.8 * data010)));
data[11] = std::min<uint8_t>(255, uint8_t(std::floor(0.5
+ (20.0 * (640.0 / fsadc) * ((data010 / 77.0)
/ (scale_res * scale_cap))))));
data[12] = std::min<uint8_t>(127, uint8_t(std::floor(-0.5
+ (80.0 * scale_res * std::min<double>(1.0,
sqrt(maxsnr * fsadc / 640.0))))));
double data012 = data[12];
data[13] = std::min<uint8_t>(255, uint8_t(std::floor(-1.5
+ (20.0 * (640.0 / fsadc) * ((data012 / 80.0)
/ (scale_res * scale_cap))))));
data[14] = 21 * uint8_t(std::floor(0.1 * 640.0 / fsadc));
data[15] = std::min<uint8_t>(127, uint8_t(1.025 * data007));
double data015 = data[15];
data[16] = std::min<uint8_t>(127, uint8_t(std::floor((data015
* (0.98 + (0.02 * std::max<double>(1.0,
(640.0 / fsadc) / maxsnr)))))));
data[17] = data[15];
data[18] = std::min<uint8_t>(127, uint8_t(0.975 * (data010)));
double data018 = data[18];
data[19] = std::min<uint8_t>(127, uint8_t(std::floor((data018
* (0.98 + (0.02 * std::max<double>(1.0,
(640.0 / fsadc) / maxsnr)))))));
data[20] = data[18];
data[21] = std::min<uint8_t>(127, uint8_t(0.975 * data012));
double data021 = data[21];
data[22] = std::min<uint8_t>(127, uint8_t(std::floor((data021
* (0.98 + (0.02 * std::max<double>(1.0,
(640.0 / fsadc) / maxsnr)))))));
data[23] = data[21];
data[24] = 0x2e;
data[25] = uint8_t(std::floor(128.0 + std::min<double>(63.0,
63.0 * (fsadc / 640.0))));
data[26] = uint8_t(std::floor(std::min<double>(63.0, 63.0 * (fsadc / 640.0)
* (0.92 + (0.08 * (640.0 / fsadc))))));
data[27] = uint8_t(std::floor(std::min<double>(63.0,
32.0 * sqrt(fsadc / 640.0))));
data[28] = uint8_t(std::floor(128.0 + std::min<double>(63.0,
63.0 * (fsadc / 640.0))));
data[29] = uint8_t(std::floor(std::min<double>(63.0,
63.0 * (fsadc / 640.0)
* (0.92 + (0.08 * (640.0 / fsadc))))));
data[30] = uint8_t(std::floor(std::min<double>(63.0,
32.0 * sqrt(fsadc / 640.0))));
data[31] = uint8_t(std::floor(128.0 + std::min<double>(63.0,
63.0 * (fsadc / 640.0))));
data[32] = uint8_t(std::floor(std::min<double>(63.0,
63.0 * (fsadc / 640.0) * (0.92
+ (0.08 * (640.0 / fsadc))))));
data[33] = uint8_t(std::floor(std::min<double>(63.0,
63.0 * sqrt(fsadc / 640.0))));
data[34] = std::min<uint8_t>(127, uint8_t(std::floor(64.0
* sqrt(fsadc / 640.0))));
data[35] = 0x40;
data[36] = 0x40;
data[37] = 0x2c;
data[38] = 0x00;
data[39] = 0x00;
/* Program the registers! */
for(size_t i = 0; i < 40; i++) {
_io_iface->poke8(0x200+i, data[i]);
}
}
/* Calibrate the baseband DC offset.
* Disables tracking
*/
void ad9361_device_t::_calibrate_baseband_dc_offset()
{
_io_iface->poke8(0x18b, 0x83); //Reset RF DC tracking flag
_io_iface->poke8(0x193, 0x3f); // Calibration settings
_io_iface->poke8(0x190, 0x0f); // Set tracking coefficient
//write_ad9361_reg(device, 0x190, /*0x0f*//*0xDF*/0x80*1 | 0x40*1 | (16+8/*+4*/)); // Set tracking coefficient: don't *4 counter, do decim /4, increased gain shift
_io_iface->poke8(0x194, 0x01); // More calibration settings
/* Start that calibration, baby. */
size_t count = 0;
_io_iface->poke8(0x016, 0x01);
while (_io_iface->peek8(0x016) & 0x01) {
if (count > 100) {
throw uhd::runtime_error("[ad9361_device_t] Baseband DC Offset Calibration Failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(5));
}
}
/* Calibrate the RF DC offset.
* Disables tracking
*/
void ad9361_device_t::_calibrate_rf_dc_offset()
{
/* Some settings are frequency-dependent. */
if (_rx_freq < 4e9) {
_io_iface->poke8(0x186, 0x32); // RF DC Offset count
_io_iface->poke8(0x187, 0x24);
_io_iface->poke8(0x188, 0x05);
} else {
_io_iface->poke8(0x186, 0x28); // RF DC Offset count
_io_iface->poke8(0x187, 0x34);
_io_iface->poke8(0x188, 0x06);
}
_io_iface->poke8(0x185, 0x20); // RF DC Offset wait count
_io_iface->poke8(0x18b, 0x83); // Disable tracking
_io_iface->poke8(0x189, 0x30);
/* Run the calibration! */
size_t count = 0;
_io_iface->poke8(0x016, 0x02);
while (_io_iface->peek8(0x016) & 0x02) {
if (count > 200) {
throw uhd::runtime_error("[ad9361_device_t] RF DC Offset Calibration Failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(50));
}
_io_iface->poke8(0x18b, 0x8d); // Enable RF DC tracking
}
void ad9361_device_t::_configure_bb_dc_tracking()
{
if (_use_dc_offset_tracking)
_io_iface->poke8(0x18b, 0xad); // Enable BB tracking
else
_io_iface->poke8(0x18b, 0x8d); // Disable BB tracking
}
void ad9361_device_t::_configure_rx_iq_tracking()
{
if (_use_iq_balance_tracking)
_io_iface->poke8(0x169, 0xcf); // Enable Rx IQ tracking
else
_io_iface->poke8(0x169, 0xc0); // Disable Rx IQ tracking
}
/* Single shot Rx quadrature calibration
*
* Procedure documented in "AD9361 Calibration Guide". Prior to calibration,
* state should be set to ALERT, FDD, and Dual Synth Mode. Rx quadrature
* tracking will be disabled, so run before or instead of enabling Rx
* quadrature tracking.
*/
void ad9361_device_t::_calibrate_rx_quadrature()
{
/* Configure RX Quadrature calibration settings. */
_io_iface->poke8(0x168, 0x03); // Set tone level for cal
_io_iface->poke8(0x16e, 0x25); // RX Gain index to use for cal
_io_iface->poke8(0x16a, 0x75); // Set Kexp phase
_io_iface->poke8(0x16b, 0x95); // Set Kexp amplitude
_io_iface->poke8(0x057, 0x33); // Power down Tx mixer
_io_iface->poke8(0x169, 0xc0); // Disable tracking and free run mode
/* Place Tx LO within passband of Rx spectrum */
double current_tx_freq = _tx_freq;
_tune_helper(TX, _rx_freq + _rx_bb_lp_bw / 2.0);
size_t count = 0;
_io_iface->poke8(0x016, 0x20);
while (_io_iface->peek8(0x016) & 0x20) {
if (count > 1000) {
throw uhd::runtime_error("[ad9361_device_t] Rx Quadrature Calibration Failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(5));
}
_io_iface->poke8(0x057, 0x30); // Re-enable Tx mixers
_tune_helper(TX, current_tx_freq);
}
/* TX quadrature calibration routine.
*
* The TX quadrature needs to be done twice, once for each TX chain, with
* only one register change in between. Thus, this function enacts the
* calibrations, and it is called from calibrate_tx_quadrature. */
void ad9361_device_t::_tx_quadrature_cal_routine() {
/* This is a weird process, but here is how it works:
* 1) Read the calibrated NCO frequency bits out of 0A3.
* 2) Write the two bits to the RX NCO freq part of 0A0.
* 3) Re-read 0A3 to get bits [5:0] because maybe they changed?
* 4) Update only the TX NCO freq bits in 0A3.
* 5) Profit (I hope). */
uint8_t reg0a3 = _io_iface->peek8(0x0a3);
uint8_t nco_freq = (reg0a3 & 0xC0);
_io_iface->poke8(0x0a0, 0x15 | (nco_freq >> 1));
reg0a3 = _io_iface->peek8(0x0a3);
_io_iface->poke8(0x0a3, (reg0a3 & 0x3F) | nco_freq);
/* It is possible to reach a configuration that won't operate correctly,
* where the two test tones used for quadrature calibration are outside
* of the RX BBF, and therefore don't make it to the ADC. We will check
* for that scenario here. */
double max_cal_freq = (((_baseband_bw * _tfir_factor)
* ((nco_freq >> 6) + 1)) / 32) * 2;
double bbbw = _baseband_bw / 2.0; // bbbw represents the one-sided BW
if (bbbw > 28e6) {
bbbw = 28e6;
} else if (bbbw < 0.20e6) {
bbbw = 0.20e6;
}
if (max_cal_freq > bbbw)
throw uhd::runtime_error("[ad9361_device_t] max_cal_freq > bbbw");
_io_iface->poke8(0x0a1, 0x7B); // Set tracking coefficient
_io_iface->poke8(0x0a9, 0xff); // Cal count
_io_iface->poke8(0x0a2, 0x7f); // Cal Kexp
_io_iface->poke8(0x0a5, 0x01); // Cal magnitude threshold VVVV
_io_iface->poke8(0x0a6, 0x01);
/* The gain table index used for calibration must be adjusted for the
* mid-table to get a TIA index = 1 and LPF index = 0. */
if (_rx_freq < 1300e6) {
_io_iface->poke8(0x0aa, 0x22); // Cal gain table index
} else {
_io_iface->poke8(0x0aa, 0x25); // Cal gain table index
}
_io_iface->poke8(0x0a4, 0xf0); // Cal setting conut
_io_iface->poke8(0x0ae, 0x00); // Cal LPF gain index (split mode)
/* Now, calibrate the TX quadrature! */
size_t count = 0;
_io_iface->poke8(0x016, 0x10);
while (_io_iface->peek8(0x016) & 0x10) {
if (count > 100) {
throw uhd::runtime_error("[ad9361_device_t] TX Quadrature Calibration Failure");
break;
}
count++;
boost::this_thread::sleep(boost::posix_time::milliseconds(10));
}
}
/* Run the TX quadrature calibration.
*/
void ad9361_device_t::_calibrate_tx_quadrature()
{
/* Make sure we are, in fact, in the ALERT state. If not, something is
* terribly wrong in the driver execution flow. */
if ((_io_iface->peek8(0x017) & 0x0F) != 5) {
throw uhd::runtime_error("[ad9361_device_t] TX Quad Cal started, but not in ALERT");
}
/* Turn off free-running and continuous calibrations. Note that this
* will get turned back on at the end of the RX calibration routine. */
_io_iface->poke8(0x169, 0xc0);
/* This calibration must be done in a certain order, and for both TX_A
* and TX_B, separately. Store the original setting so that we can
* restore it later. */
uint8_t orig_reg_inputsel = _regs.inputsel;
/***********************************************************************
* TX1/2-A Calibration
**********************************************************************/
_regs.inputsel = _regs.inputsel & 0xBF;
_io_iface->poke8(0x004, _regs.inputsel);
_tx_quadrature_cal_routine();
/***********************************************************************
* TX1/2-B Calibration
**********************************************************************/
_regs.inputsel = _regs.inputsel | 0x40;
_io_iface->poke8(0x004, _regs.inputsel);
_tx_quadrature_cal_routine();
/***********************************************************************
* fin
**********************************************************************/
_regs.inputsel = orig_reg_inputsel;
_io_iface->poke8(0x004, orig_reg_inputsel);
}
/***********************************************************************
* Other Misc Setup Functions
***********************************************************************/
/* Program the mixer gain table.
*
* Note that this table is fixed for all frequency settings. */
void ad9361_device_t::_program_mixer_gm_subtable()
{
uint8_t gain[] = { 0x78, 0x74, 0x70, 0x6C, 0x68, 0x64, 0x60, 0x5C, 0x58,
0x54, 0x50, 0x4C, 0x48, 0x30, 0x18, 0x00 };
uint8_t gm[] = { 0x00, 0x0D, 0x15, 0x1B, 0x21, 0x25, 0x29, 0x2C, 0x2F, 0x31,
0x33, 0x34, 0x35, 0x3A, 0x3D, 0x3E };
/* Start the clock. */
_io_iface->poke8(0x13f, 0x02);
/* Program the GM Sub-table. */
int i;
for (i = 15; i >= 0; i--) {
_io_iface->poke8(0x138, i);
_io_iface->poke8(0x139, gain[(15 - i)]);
_io_iface->poke8(0x13A, 0x00);
_io_iface->poke8(0x13B, gm[(15 - i)]);
_io_iface->poke8(0x13F, 0x06);
_io_iface->poke8(0x13C, 0x00);
_io_iface->poke8(0x13C, 0x00);
}
/* Clear write bit and stop clock. */
_io_iface->poke8(0x13f, 0x02);
_io_iface->poke8(0x13C, 0x00);
_io_iface->poke8(0x13C, 0x00);
_io_iface->poke8(0x13f, 0x00);
}
/* Program the gain table.
*
* There are three different gain tables for different frequency ranges! */
void ad9361_device_t::_program_gain_table() {
/* Figure out which gain table we should be using for our current
* frequency band. */
uint8_t (*gain_table)[3] = NULL;
uint8_t new_gain_table;
if (_rx_freq < 1300e6) {
gain_table = gain_table_sub_1300mhz;
new_gain_table = 1;
} else if (_rx_freq < 4e9) {
gain_table = gain_table_1300mhz_to_4000mhz;
new_gain_table = 2;
} else if (_rx_freq <= 6e9) {
gain_table = gain_table_4000mhz_to_6000mhz;
new_gain_table = 3;
} else {
new_gain_table = 1;
throw uhd::runtime_error("[ad9361_device_t] Wrong _rx_freq value");
}
/* Only re-program the gain table if there has been a band change. */
if (_curr_gain_table == new_gain_table) {
return;
} else {
_curr_gain_table = new_gain_table;
}
/* Okay, we have to program a new gain table. Sucks, brah. Start the
* gain table clock. */
_io_iface->poke8(0x137, 0x1A);
/* IT'S PROGRAMMING TIME. */
uint8_t index = 0;
for (; index < 77; index++) {
_io_iface->poke8(0x130, index);
_io_iface->poke8(0x131, gain_table[index][0]);
_io_iface->poke8(0x132, gain_table[index][1]);
_io_iface->poke8(0x133, gain_table[index][2]);
_io_iface->poke8(0x137, 0x1E);
_io_iface->poke8(0x134, 0x00);
_io_iface->poke8(0x134, 0x00);
}
/* Everything above the 77th index is zero. */
for (; index < 91; index++) {
_io_iface->poke8(0x130, index);
_io_iface->poke8(0x131, 0x00);
_io_iface->poke8(0x132, 0x00);
_io_iface->poke8(0x133, 0x00);
_io_iface->poke8(0x137, 0x1E);
_io_iface->poke8(0x134, 0x00);
_io_iface->poke8(0x134, 0x00);
}
/* Clear the write bit and stop the gain clock. */
_io_iface->poke8(0x137, 0x1A);
_io_iface->poke8(0x134, 0x00);
_io_iface->poke8(0x134, 0x00);
_io_iface->poke8(0x137, 0x00);
}
/* Setup gain control registers.
*
* This really only needs to be done once, at initialization.
* If AGC is used the mode select bits (Reg 0x0FA) must be written manually */
void ad9361_device_t::_setup_gain_control(bool agc)
{
/* The AGC mode configuration should be good for all cases.
* However, non AGC configuration still used for backward compatibility. */
if (agc) {
/*mode select bits must be written before hand!*/
_io_iface->poke8(0x0FB, 0x08); // Table, Digital Gain, Man Gain Ctrl
_io_iface->poke8(0x0FC, 0x23); // Incr Step Size, ADC Overrange Size
_io_iface->poke8(0x0FD, 0x4C); // Max Full/LMT Gain Table Index
_io_iface->poke8(0x0FE, 0x44); // Decr Step Size, Peak Overload Time
_io_iface->poke8(0x100, 0x6F); // Max Digital Gain
_io_iface->poke8(0x101, 0x0A); // Max Digital Gain
_io_iface->poke8(0x103, 0x08); // Max Digital Gain
_io_iface->poke8(0x104, 0x2F); // ADC Small Overload Threshold
_io_iface->poke8(0x105, 0x3A); // ADC Large Overload Threshold
_io_iface->poke8(0x106, 0x22); // Max Digital Gain
_io_iface->poke8(0x107, 0x2B); // Large LMT Overload Threshold
_io_iface->poke8(0x108, 0x31);
_io_iface->poke8(0x111, 0x0A);
_io_iface->poke8(0x11A, 0x1C);
_io_iface->poke8(0x120, 0x0C);
_io_iface->poke8(0x121, 0x44);
_io_iface->poke8(0x122, 0x44);
_io_iface->poke8(0x123, 0x11);
_io_iface->poke8(0x124, 0xF5);
_io_iface->poke8(0x125, 0x3B);
_io_iface->poke8(0x128, 0x03);
_io_iface->poke8(0x129, 0x56);
_io_iface->poke8(0x12A, 0x22);
} else {
_io_iface->poke8(0x0FA, 0xE0); // Gain Control Mode Select
_io_iface->poke8(0x0FB, 0x08); // Table, Digital Gain, Man Gain Ctrl
_io_iface->poke8(0x0FC, 0x23); // Incr Step Size, ADC Overrange Size
_io_iface->poke8(0x0FD, 0x4C); // Max Full/LMT Gain Table Index
_io_iface->poke8(0x0FE, 0x44); // Decr Step Size, Peak Overload Time
_io_iface->poke8(0x100, 0x6F); // Max Digital Gain
_io_iface->poke8(0x104, 0x2F); // ADC Small Overload Threshold
_io_iface->poke8(0x105, 0x3A); // ADC Large Overload Threshold
_io_iface->poke8(0x107, 0x31); // Large LMT Overload Threshold
_io_iface->poke8(0x108, 0x39); // Small LMT Overload Threshold
_io_iface->poke8(0x109, 0x23); // Rx1 Full/LMT Gain Index
_io_iface->poke8(0x10A, 0x58); // Rx1 LPF Gain Index
_io_iface->poke8(0x10B, 0x00); // Rx1 Digital Gain Index
_io_iface->poke8(0x10C, 0x23); // Rx2 Full/LMT Gain Index
_io_iface->poke8(0x10D, 0x18); // Rx2 LPF Gain Index
_io_iface->poke8(0x10E, 0x00); // Rx2 Digital Gain Index
_io_iface->poke8(0x114, 0x30); // Low Power Threshold
_io_iface->poke8(0x11A, 0x27); // Initial LMT Gain Limit
_io_iface->poke8(0x081, 0x00); // Tx Symbol Gain Control
}
}
/* Setup the RX or TX synthesizers.
*
* This setup depends on a fixed look-up table, which is stored in an
* included header file. The table is indexed based on the passed VCO rate.
*/
void ad9361_device_t::_setup_synth(direction_t direction, double vcorate)
{
/* The vcorates in the vco_index array represent lower boundaries for
* rates. Once we find a match, we use that index to look-up the rest of
* the register values in the LUT. */
int vcoindex = 0;
for (size_t i = 0; i < 53; i++) {
vcoindex = i;
if (vcorate > vco_index[i]) {
break;
}
}
if (vcoindex > 53)
throw uhd::runtime_error("[ad9361_device_t] vcoindex > 53");
/* Parse the values out of the LUT based on our calculated index... */
uint8_t vco_output_level = synth_cal_lut[vcoindex][0];
uint8_t vco_varactor = synth_cal_lut[vcoindex][1];
uint8_t vco_bias_ref = synth_cal_lut[vcoindex][2];
uint8_t vco_bias_tcf = synth_cal_lut[vcoindex][3];
uint8_t vco_cal_offset = synth_cal_lut[vcoindex][4];
uint8_t vco_varactor_ref = synth_cal_lut[vcoindex][5];
uint8_t charge_pump_curr = synth_cal_lut[vcoindex][6];
uint8_t loop_filter_c2 = synth_cal_lut[vcoindex][7];
uint8_t loop_filter_c1 = synth_cal_lut[vcoindex][8];
uint8_t loop_filter_r1 = synth_cal_lut[vcoindex][9];
uint8_t loop_filter_c3 = synth_cal_lut[vcoindex][10];
uint8_t loop_filter_r3 = synth_cal_lut[vcoindex][11];
/* ... annnd program! */
if (direction == RX) {
_io_iface->poke8(0x23a, 0x40 | vco_output_level);
_io_iface->poke8(0x239, 0xC0 | vco_varactor);
_io_iface->poke8(0x242, vco_bias_ref | (vco_bias_tcf << 3));
_io_iface->poke8(0x238, (vco_cal_offset << 3));
_io_iface->poke8(0x245, 0x00);
_io_iface->poke8(0x251, vco_varactor_ref);
_io_iface->poke8(0x250, 0x70);
_io_iface->poke8(0x23b, 0x80 | charge_pump_curr);
_io_iface->poke8(0x23e, loop_filter_c1 | (loop_filter_c2 << 4));
_io_iface->poke8(0x23f, loop_filter_c3 | (loop_filter_r1 << 4));
_io_iface->poke8(0x240, loop_filter_r3);
} else if (direction == TX) {
_io_iface->poke8(0x27a, 0x40 | vco_output_level);
_io_iface->poke8(0x279, 0xC0 | vco_varactor);
_io_iface->poke8(0x282, vco_bias_ref | (vco_bias_tcf << 3));
_io_iface->poke8(0x278, (vco_cal_offset << 3));
_io_iface->poke8(0x285, 0x00);
_io_iface->poke8(0x291, vco_varactor_ref);
_io_iface->poke8(0x290, 0x70);
_io_iface->poke8(0x27b, 0x80 | charge_pump_curr);
_io_iface->poke8(0x27e, loop_filter_c1 | (loop_filter_c2 << 4));
_io_iface->poke8(0x27f, loop_filter_c3 | (loop_filter_r1 << 4));
_io_iface->poke8(0x280, loop_filter_r3);
} else {
throw uhd::runtime_error("[ad9361_device_t] [_setup_synth] INVALID_CODE_PATH");
}
}
/* Tune the baseband VCO.
*
* This clock signal is what gets fed to the ADCs and DACs. This function is
* not exported outside of this file, and is invoked based on the rate
* fed to the public set_clock_rate function. */
double ad9361_device_t::_tune_bbvco(const double rate)
{
UHD_LOG << boost::format("[ad9361_device_t::_tune_bbvco] rate=%.10f\n") % rate;
/* Let's not re-tune to the same frequency over and over... */
if (freq_is_nearly_equal(rate, _req_coreclk)) {
return _adcclock_freq;
}
_req_coreclk = rate;
const double fref = 40e6;
const int modulus = 2088960;
const double vcomax = 1430e6;
const double vcomin = 672e6;
double vcorate;
int vcodiv;
/* Iterate over VCO dividers until appropriate divider is found. */
int i = 1;
for (; i <= 6; i++) {
vcodiv = 1 << i;
vcorate = rate * vcodiv;
if (vcorate >= vcomin && vcorate <= vcomax)
break;
}
if (i == 7)
throw uhd::runtime_error("[ad9361_device_t] _tune_bbvco: wrong vcorate");
UHD_LOG << boost::format("[ad9361_device_t::_tune_bbvco] vcodiv=%d vcorate=%.10f\n") % vcodiv % vcorate;
/* Fo = Fref * (Nint + Nfrac / mod) */
int nint = static_cast<int>(vcorate / fref);
UHD_LOG << boost::format("[ad9361_device_t::_tune_bbvco] (nint)=%.10f\n") % (vcorate / fref);
int nfrac = static_cast<int>(boost::math::round(((vcorate / fref) - (double) nint) * (double) modulus));
UHD_LOG << boost::format("[ad9361_device_t::_tune_bbvco] (nfrac)=%.10f\n") % (((vcorate / fref) - (double) nint) * (double) modulus);
UHD_LOG << boost::format("[ad9361_device_t::_tune_bbvco] nint=%d nfrac=%d\n") % nint % nfrac;
double actual_vcorate = fref
* ((double) nint + ((double) nfrac / (double) modulus));
/* Scale CP current according to VCO rate */
const double icp_baseline = 150e-6;
const double freq_baseline = 1280e6;
double icp = icp_baseline * (actual_vcorate / freq_baseline);
int icp_reg = static_cast<int>(icp / 25e-6) - 1;
_io_iface->poke8(0x045, 0x00); // REFCLK / 1 to BBPLL
_io_iface->poke8(0x046, icp_reg & 0x3F); // CP current
_io_iface->poke8(0x048, 0xe8); // BBPLL loop filters
_io_iface->poke8(0x049, 0x5b); // BBPLL loop filters
_io_iface->poke8(0x04a, 0x35); // BBPLL loop filters
_io_iface->poke8(0x04b, 0xe0);
_io_iface->poke8(0x04e, 0x10); // Max accuracy
_io_iface->poke8(0x043, nfrac & 0xFF); // Nfrac[7:0]
_io_iface->poke8(0x042, (nfrac >> 8) & 0xFF); // Nfrac[15:8]
_io_iface->poke8(0x041, (nfrac >> 16) & 0xFF); // Nfrac[23:16]
_io_iface->poke8(0x044, nint); // Nint
_calibrate_lock_bbpll();
_regs.bbpll = (_regs.bbpll & 0xF8) | i;
_bbpll_freq = actual_vcorate;
_adcclock_freq = (actual_vcorate / vcodiv);
return _adcclock_freq;
}
/* This function re-programs all of the gains in the system.
*
* Because the gain values match to different gain indices based on the
* current operating band, this function can be called to update all gain
* settings to the appropriate index after a re-tune. */
void ad9361_device_t::_reprogram_gains()
{
set_gain(RX, CHAIN_1,_rx1_gain);
set_gain(RX, CHAIN_2,_rx2_gain);
set_gain(TX, CHAIN_1,_tx1_gain);
set_gain(TX, CHAIN_2,_tx2_gain);
}
/* This is the internal tune function, not available for a host call.
*
* Calculate the VCO settings for the requested frquency, and then either
* tune the RX or TX VCO. */
double ad9361_device_t::_tune_helper(direction_t direction, const double value)
{
/* The RFPLL runs from 6 GHz - 12 GHz */
const double fref = 80e6;
const int modulus = 8388593;
const double vcomax = 12e9;
const double vcomin = 6e9;
double vcorate;
int vcodiv;
/* Iterate over VCO dividers until appropriate divider is found. */
int i;
for (i = 0; i <= 6; i++) {
vcodiv = 2 << i;
vcorate = value * vcodiv;
if (vcorate >= vcomin && vcorate <= vcomax)
break;
}
if (i == 7)
throw uhd::runtime_error("[ad9361_device_t] RFVCO can't find valid VCO rate!");
int nint = static_cast<int>(vcorate / fref);
int nfrac = static_cast<int>(((vcorate / fref) - nint) * modulus);
double actual_vcorate = fref * (nint + (double) (nfrac) / modulus);
double actual_lo = actual_vcorate / vcodiv;
if (direction == RX) {
_req_rx_freq = value;
/* Set band-specific settings. */
if (value < _client_params->get_band_edge(AD9361_RX_BAND0)) {
_regs.inputsel = (_regs.inputsel & 0xC0) | 0x30; // Port C, balanced
} else if ((value
>= _client_params->get_band_edge(AD9361_RX_BAND0))
&& (value
< _client_params->get_band_edge(AD9361_RX_BAND1))) {
_regs.inputsel = (_regs.inputsel & 0xC0) | 0x0C; // Port B, balanced
} else if ((value
>= _client_params->get_band_edge(AD9361_RX_BAND1))
&& (value <= 6e9)) {
_regs.inputsel = (_regs.inputsel & 0xC0) | 0x03; // Port A, balanced
} else {
throw uhd::runtime_error("[ad9361_device_t] [_tune_helper] INVALID_CODE_PATH");
}
_io_iface->poke8(0x004, _regs.inputsel);
/* Store vcodiv setting. */
_regs.vcodivs = (_regs.vcodivs & 0xF0) | (i & 0x0F);
/* Setup the synthesizer. */
_setup_synth(RX, actual_vcorate);
/* Tune!!!! */
_io_iface->poke8(0x233, nfrac & 0xFF);
_io_iface->poke8(0x234, (nfrac >> 8) & 0xFF);
_io_iface->poke8(0x235, (nfrac >> 16) & 0xFF);
_io_iface->poke8(0x232, (nint >> 8) & 0xFF);
_io_iface->poke8(0x231, nint & 0xFF);
_io_iface->poke8(0x005, _regs.vcodivs);
/* Lock the PLL! */
boost::this_thread::sleep(boost::posix_time::milliseconds(2));
if ((_io_iface->peek8(0x247) & 0x02) == 0) {
throw uhd::runtime_error("[ad9361_device_t] RX PLL NOT LOCKED");
}
_rx_freq = actual_lo;
return actual_lo;
} else {
_req_tx_freq = value;
/* Set band-specific settings. */
if (value < _client_params->get_band_edge(AD9361_TX_BAND0)) {
_regs.inputsel = _regs.inputsel | 0x40;
} else if ((value
>= _client_params->get_band_edge(AD9361_TX_BAND0))
&& (value <= 6e9)) {
_regs.inputsel = _regs.inputsel & 0xBF;
} else {
throw uhd::runtime_error("[ad9361_device_t] [_tune_helper] INVALID_CODE_PATH");
}
_io_iface->poke8(0x004, _regs.inputsel);
/* Store vcodiv setting. */
_regs.vcodivs = (_regs.vcodivs & 0x0F) | ((i & 0x0F) << 4);
/* Setup the synthesizer. */
_setup_synth(TX, actual_vcorate);
/* Tune it, homey. */
_io_iface->poke8(0x273, nfrac & 0xFF);
_io_iface->poke8(0x274, (nfrac >> 8) & 0xFF);
_io_iface->poke8(0x275, (nfrac >> 16) & 0xFF);
_io_iface->poke8(0x272, (nint >> 8) & 0xFF);
_io_iface->poke8(0x271, nint & 0xFF);
_io_iface->poke8(0x005, _regs.vcodivs);
/* Lock the PLL! */
boost::this_thread::sleep(boost::posix_time::milliseconds(2));
if ((_io_iface->peek8(0x287) & 0x02) == 0) {
throw uhd::runtime_error("[ad9361_device_t] TX PLL NOT LOCKED");
}
_tx_freq = actual_lo;
return actual_lo;
}
}
/* Configure the various clock / sample rates in the RX and TX chains.
*
* Functionally, this function configures AD9361's RX and TX rates. For
* a requested TX & RX rate, it sets the interpolation & decimation filters,
* and tunes the VCO that feeds the ADCs and DACs.
*/
double ad9361_device_t::_setup_rates(const double rate)
{
/* If we make it into this function, then we are tuning to a new rate.
* Store the new rate. */
_req_clock_rate = rate;
UHD_LOG << boost::format("[ad9361_device_t::_setup_rates] rate=%.6d\n") % rate;
/* Set the decimation and interpolation values in the RX and TX chains.
* This also switches filters in / out. Note that all transmitters and
* receivers have to be turned on for the calibration portion of
* bring-up, and then they will be switched out to reflect the actual
* user-requested antenna selections. */
int divfactor = 0;
_tfir_factor = 0;
_rfir_factor = 0;
if (rate < 0.33e6) {
// RX1 + RX2 enabled, 3, 2, 2, 4
_regs.rxfilt = B8(11101111);
// TX1 + TX2 enabled, 3, 2, 2, 4
_regs.txfilt = B8(11101111);
divfactor = 48;
_tfir_factor = 4;
_rfir_factor = 4;
} else if (rate < 0.66e6) {
// RX1 + RX2 enabled, 2, 2, 2, 4
_regs.rxfilt = B8(11011111);
// TX1 + TX2 enabled, 2, 2, 2, 4
_regs.txfilt = B8(11011111);
divfactor = 32;
_tfir_factor = 4;
_rfir_factor = 4;
} else if (rate <= 20e6) {
// RX1 + RX2 enabled, 2, 2, 2, 2
_regs.rxfilt = B8(11011110);
// TX1 + TX2 enabled, 2, 2, 2, 2
_regs.txfilt = B8(11011110);
divfactor = 16;
_tfir_factor = 2;
_rfir_factor = 2;
} else if ((rate > 20e6) && (rate < 23e6)) {
// RX1 + RX2 enabled, 3, 2, 2, 2
_regs.rxfilt = B8(11101110);
// TX1 + TX2 enabled, 3, 1, 2, 2
_regs.txfilt = B8(11100110);
divfactor = 24;
_tfir_factor = 2;
_rfir_factor = 2;
} else if ((rate >= 23e6) && (rate < 41e6)) {
// RX1 + RX2 enabled, 2, 2, 2, 2
_regs.rxfilt = B8(11011110);
// TX1 + TX2 enabled, 1, 2, 2, 2
_regs.txfilt = B8(11001110);
divfactor = 16;
_tfir_factor = 2;
_rfir_factor = 2;
} else if ((rate >= 41e6) && (rate <= 58e6)) {
// RX1 + RX2 enabled, 3, 1, 2, 2
_regs.rxfilt = B8(11100110);
// TX1 + TX2 enabled, 3, 1, 1, 2
_regs.txfilt = B8(11100010);
divfactor = 12;
_tfir_factor = 2;
_rfir_factor = 2;
} else if ((rate > 58e6) && (rate <= 61.44e6)) {
// RX1 + RX2 enabled, 2, 1, 2, 2
_regs.rxfilt = B8(11010110);
// TX1 + TX2 enabled, 2, 1, 1, 2
_regs.txfilt = B8(11010010);
divfactor = 8;
_tfir_factor = 2;
_rfir_factor = 2;
} else {
// should never get in here
throw uhd::runtime_error("[ad9361_device_t] [_setup_rates] INVALID_CODE_PATH");
}
UHD_LOG << boost::format("[ad9361_device_t::_setup_rates] divfactor=%d\n") % divfactor;
/* Tune the BBPLL to get the ADC and DAC clocks. */
const double adcclk = _tune_bbvco(rate * divfactor);
double dacclk = adcclk;
/* The DAC clock must be <= 336e6, and is either the ADC clock or 1/2 the
* ADC clock.*/
if (adcclk > 336e6) {
/* Make the DAC clock = ADC/2 */
_regs.bbpll = _regs.bbpll | 0x08;
dacclk = adcclk / 2.0;
} else {
_regs.bbpll = _regs.bbpll & 0xF7;
}
/* Set the dividers / interpolators in AD9361. */
_io_iface->poke8(0x002, _regs.txfilt);
_io_iface->poke8(0x003, _regs.rxfilt);
_io_iface->poke8(0x004, _regs.inputsel);
_io_iface->poke8(0x00A, _regs.bbpll);
UHD_LOG << boost::format("[ad9361_device_t::_setup_rates] adcclk=%f\n") % adcclk;
_baseband_bw = (adcclk / divfactor);
/*
The Tx & Rx FIR calculate 16 taps per clock cycle. This limits the number of available taps to the ratio of DAC_CLK/ADC_CLK
to the input data rate multiplied by 16. For example, if the input data rate is 25 MHz and DAC_CLK is 100 MHz,
then the ratio of DAC_CLK to the input data rate is 100/25 or 4. In this scenario, the total number of taps available is 64.
Also, whilst the Rx FIR filter always has memory available for 128 taps, the Tx FIR Filter can only support a maximum length of 64 taps
in 1x interpolation mode, and 128 taps in 2x & 4x modes.
*/
const size_t max_tx_taps = std::min<size_t>(
std::min<size_t>((16 * (int)((dacclk / rate) + 0.5)), 128),
(_tfir_factor == 1) ? 64 : 128);
const size_t max_rx_taps = std::min<size_t>((16 * (size_t)((adcclk / rate) + 0.5)),
128);
const size_t num_tx_taps = get_num_taps(max_tx_taps);
const size_t num_rx_taps = get_num_taps(max_rx_taps);
_setup_tx_fir(num_tx_taps,_tfir_factor);
_setup_rx_fir(num_rx_taps,_rfir_factor);
return _baseband_bw;
}
/***********************************************************************
* Publicly exported functions to host calls
**********************************************************************/
void ad9361_device_t::initialize()
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
/* Initialize shadow registers. */
_regs.vcodivs = 0x00;
_regs.inputsel = 0x30;
_regs.rxfilt = 0x00;
_regs.txfilt = 0x00;
_regs.bbpll = 0x02;
_regs.bbftune_config = 0x1e;
_regs.bbftune_mode = 0x1e;
/* Initialize private VRQ fields. */
_rx_freq = DEFAULT_RX_FREQ;
_tx_freq = DEFAULT_TX_FREQ;
_req_rx_freq = 0.0;
_req_tx_freq = 0.0;
_baseband_bw = 0.0;
_req_clock_rate = 0.0;
_req_coreclk = 0.0;
_bbpll_freq = 0.0;
_adcclock_freq = 0.0;
_rx_bbf_tunediv = 0;
_curr_gain_table = 0;
_rx1_gain = 0;
_rx2_gain = 0;
_tx1_gain = 0;
_tx2_gain = 0;
_use_dc_offset_tracking = true;
_use_iq_balance_tracking = true;
_rx1_agc_mode = GAIN_MODE_SLOW_AGC;
_rx2_agc_mode = GAIN_MODE_SLOW_AGC;
_rx1_agc_enable = false;
_rx2_agc_enable = false;
_rx_analog_bw = 0;
_tx_analog_bw = 0;
_rx_tia_lp_bw = 0;
_tx_sec_lp_bw = 0;
_rx_bb_lp_bw = 0;
_tx_bb_lp_bw = 0;
/* Reset the device. */
_io_iface->poke8(0x000, 0x01);
_io_iface->poke8(0x000, 0x00);
boost::this_thread::sleep(boost::posix_time::milliseconds(20));
/* Check device ID to make sure iface works */
uint32_t device_id = (_io_iface->peek8(0x037) & 0x8);
if (device_id != 0x8) {
throw uhd::runtime_error(str(boost::format("[ad9361_device_t::initialize] Device ID readback failure. Expected: 0x8, Received: 0x%x") % device_id));
}
/* There is not a WAT big enough for this. */
_io_iface->poke8(0x3df, 0x01);
_io_iface->poke8(0x2a6, 0x0e); // Enable master bias
_io_iface->poke8(0x2a8, 0x0e); // Set bandgap trim
/* Set RFPLL ref clock scale to REFCLK * 2 */
_io_iface->poke8(0x2ab, 0x07);
_io_iface->poke8(0x2ac, 0xff);
/* Enable clocks. */
switch (_client_params->get_clocking_mode()) {
case AD9361_XTAL_N_CLK_PATH: {
_io_iface->poke8(0x009, 0x17);
} break;
case AD9361_XTAL_P_CLK_PATH: {
_io_iface->poke8(0x009, 0x07);
_io_iface->poke8(0x292, 0x08);
_io_iface->poke8(0x293, 0x80);
_io_iface->poke8(0x294, 0x00);
_io_iface->poke8(0x295, 0x14);
} break;
default:
throw uhd::runtime_error("[ad9361_device_t] NOT IMPLEMENTED");
}
boost::this_thread::sleep(boost::posix_time::milliseconds(20));
/* Tune the BBPLL, write TX and RX FIRS. */
_setup_rates(50e6);
/* Setup data ports (FDD dual port DDR):
* FDD dual port DDR CMOS no swap.
* Force TX on one port, RX on the other. */
switch (_client_params->get_digital_interface_mode()) {
case AD9361_DDR_FDD_LVCMOS: {
_io_iface->poke8(0x010, 0xc8); // Swap I&Q on Tx, Swap I&Q on Rx, Toggle frame sync mode
_io_iface->poke8(0x011, 0x00);
_io_iface->poke8(0x012, 0x02);
} break;
case AD9361_DDR_FDD_LVDS: {
_io_iface->poke8(0x010, 0xcc); // Swap I&Q on Tx, Swap I&Q on Rx, Toggle frame sync mode, 2R2T timing.
_io_iface->poke8(0x011, 0x00);
_io_iface->poke8(0x012, 0x10);
//LVDS Specific
_io_iface->poke8(0x03C, 0x23);
_io_iface->poke8(0x03D, 0xFF);
_io_iface->poke8(0x03E, 0x0F);
} break;
default:
throw uhd::runtime_error("[ad9361_device_t] NOT IMPLEMENTED");
}
/* Data delay for TX and RX data clocks */
digital_interface_delays_t timing =
_client_params->get_digital_interface_timing();
uint8_t rx_delays = ((timing.rx_clk_delay & 0xF) << 4)
| (timing.rx_data_delay & 0xF);
uint8_t tx_delays = ((timing.tx_clk_delay & 0xF) << 4)
| (timing.tx_data_delay & 0xF);
_io_iface->poke8(0x006, rx_delays);
_io_iface->poke8(0x007, tx_delays);
/* Setup AuxDAC */
_io_iface->poke8(0x018, 0x00); // AuxDAC1 Word[9:2]
_io_iface->poke8(0x019, 0x00); // AuxDAC2 Word[9:2]
_io_iface->poke8(0x01A, 0x00); // AuxDAC1 Config and Word[1:0]
_io_iface->poke8(0x01B, 0x00); // AuxDAC2 Config and Word[1:0]
_io_iface->poke8(0x023, 0xFF); // AuxDAC Manaul/Auto Control
_io_iface->poke8(0x026, 0x00); // AuxDAC Manual Select Bit/GPO Manual Select
_io_iface->poke8(0x030, 0x00); // AuxDAC1 Rx Delay
_io_iface->poke8(0x031, 0x00); // AuxDAC1 Tx Delay
_io_iface->poke8(0x032, 0x00); // AuxDAC2 Rx Delay
_io_iface->poke8(0x033, 0x00); // AuxDAC2 Tx Delay
/* LNA bypass polarity inversion
* According to the register map, we should invert the bypass path to
* match LNA phase. Extensive testing, however, shows otherwise and that
* to align bypass and LNA phases, the bypass inversion switch should be
* turned off.
*/
_io_iface->poke8(0x022, 0x0A);
/* Setup AuxADC */
_io_iface->poke8(0x00B, 0x00); // Temp Sensor Setup (Offset)
_io_iface->poke8(0x00C, 0x00); // Temp Sensor Setup (Temp Window)
_io_iface->poke8(0x00D, 0x00); // Temp Sensor Setup (Manual Measure)
_io_iface->poke8(0x00F, 0x04); // Temp Sensor Setup (Decimation)
_io_iface->poke8(0x01C, 0x10); // AuxADC Setup (Clock Div)
_io_iface->poke8(0x01D, 0x01); // AuxADC Setup (Decimation/Enable)
/* Setup control outputs. */
_io_iface->poke8(0x035, 0x01);
_io_iface->poke8(0x036, 0xFF);
/* Setup GPO */
_io_iface->poke8(0x03a, 0x27); //set delay register
_io_iface->poke8(0x020, 0x00); // GPO Auto Enable Setup in RX and TX
_io_iface->poke8(0x027, 0x03); // GPO Manual and GPO auto value in ALERT
_io_iface->poke8(0x028, 0x00); // GPO_0 RX Delay
_io_iface->poke8(0x029, 0x00); // GPO_1 RX Delay
_io_iface->poke8(0x02A, 0x00); // GPO_2 RX Delay
_io_iface->poke8(0x02B, 0x00); // GPO_3 RX Delay
_io_iface->poke8(0x02C, 0x00); // GPO_0 TX Delay
_io_iface->poke8(0x02D, 0x00); // GPO_1 TX Delay
_io_iface->poke8(0x02E, 0x00); // GPO_2 TX Delay
_io_iface->poke8(0x02F, 0x00); // GPO_3 TX Delay
_io_iface->poke8(0x261, 0x00); // RX LO power
_io_iface->poke8(0x2a1, 0x00); // TX LO power
_io_iface->poke8(0x248, 0x0b); // en RX VCO LDO
_io_iface->poke8(0x288, 0x0b); // en TX VCO LDO
_io_iface->poke8(0x246, 0x02); // pd RX cal Tcf
_io_iface->poke8(0x286, 0x02); // pd TX cal Tcf
_io_iface->poke8(0x249, 0x8e); // rx vco cal length
_io_iface->poke8(0x289, 0x8e); // rx vco cal length
_io_iface->poke8(0x23b, 0x80); // set RX MSB?, FIXME 0x89 magic cp
_io_iface->poke8(0x27b, 0x80); // "" TX //FIXME 0x88 see above
_io_iface->poke8(0x243, 0x0d); // set rx prescaler bias
_io_iface->poke8(0x283, 0x0d); // "" TX
_io_iface->poke8(0x23d, 0x00); // Clear half VCO cal clock setting
_io_iface->poke8(0x27d, 0x00); // Clear half VCO cal clock setting
/* The order of the following process is EXTREMELY important. If the
* below functions are modified at all, device initialization and
* calibration might be broken in the process! */
_io_iface->poke8(0x015, 0x04); // dual synth mode, synth en ctrl en
_io_iface->poke8(0x014, 0x05); // use SPI for TXNRX ctrl, to ALERT, TX on
_io_iface->poke8(0x013, 0x01); // enable ENSM
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
_calibrate_synth_charge_pumps();
_tune_helper(RX, _rx_freq);
_tune_helper(TX, _tx_freq);
_program_mixer_gm_subtable();
_program_gain_table();
_setup_gain_control(false);
set_bw_filter(RX, _baseband_bw);
set_bw_filter(TX, _baseband_bw);
_setup_adc();
_calibrate_baseband_dc_offset();
_calibrate_rf_dc_offset();
_calibrate_rx_quadrature();
/*
* Rx BB DC and IQ tracking are both disabled by calibration at this
* point. Only issue commands if tracking needs to be turned on.
*/
if (_use_dc_offset_tracking)
_configure_bb_dc_tracking();
if (_use_iq_balance_tracking)
_configure_rx_iq_tracking();
_last_rx_cal_freq = _rx_freq;
_last_tx_cal_freq = _tx_freq;
// cals done, set PPORT config
switch (_client_params->get_digital_interface_mode()) {
case AD9361_DDR_FDD_LVCMOS: {
_io_iface->poke8(0x012, 0x02);
} break;
case AD9361_DDR_FDD_LVDS: {
_io_iface->poke8(0x012, 0x10);
} break;
default:
throw uhd::runtime_error("[ad9361_device_t] NOT IMPLEMENTED");
}
_io_iface->poke8(0x013, 0x01); // Set ENSM FDD bit
_io_iface->poke8(0x015, 0x04); // dual synth mode, synth en ctrl en
/* Default TX attentuation to 10dB on both TX1 and TX2 */
_io_iface->poke8(0x073, 0x00);
_io_iface->poke8(0x074, 0x00);
_io_iface->poke8(0x075, 0x00);
_io_iface->poke8(0x076, 0x00);
/* Setup RSSI Measurements */
_io_iface->poke8(0x150, 0x0E); // RSSI Measurement Duration 0, 1
_io_iface->poke8(0x151, 0x00); // RSSI Measurement Duration 2, 3
_io_iface->poke8(0x152, 0xFF); // RSSI Weighted Multiplier 0
_io_iface->poke8(0x153, 0x00); // RSSI Weighted Multiplier 1
_io_iface->poke8(0x154, 0x00); // RSSI Weighted Multiplier 2
_io_iface->poke8(0x155, 0x00); // RSSI Weighted Multiplier 3
_io_iface->poke8(0x156, 0x00); // RSSI Delay
_io_iface->poke8(0x157, 0x00); // RSSI Wait
_io_iface->poke8(0x158, 0x0D); // RSSI Mode Select
_io_iface->poke8(0x15C, 0x67); // Power Measurement Duration
/* Turn on the default RX & TX chains. */
set_active_chains(true, false, false, false);
/* Set TXers & RXers on (only works in FDD mode) */
_io_iface->poke8(0x014, 0x21);
}
void ad9361_device_t::set_io_iface(ad9361_io::sptr io_iface)
{
_io_iface = io_iface;
}
/* This function sets the RX / TX rate between AD9361 and the FPGA, and
* thus determines the interpolation / decimation required in the FPGA to
* achieve the user's requested rate.
*
* This is the only clock setting function that is exposed to the outside. */
double ad9361_device_t::set_clock_rate(const double req_rate)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
if (req_rate > 61.44e6) {
throw uhd::runtime_error("[ad9361_device_t] Requested master clock rate outside range");
}
UHD_LOG << boost::format("[ad9361_device_t::set_clock_rate] req_rate=%.10f\n") % req_rate;
/* UHD has a habit of requesting the same rate like four times when it
* starts up. This prevents that, and any bugs in user code that request
* the same rate over and over. */
if (freq_is_nearly_equal(req_rate, _req_clock_rate)) {
// We return _baseband_bw, because that's closest to the
// actual value we're currently running.
return _baseband_bw;
}
/* We must be in the SLEEP / WAIT state to do this. If we aren't already
* there, transition the ENSM to State 0. */
uint8_t current_state = _io_iface->peek8(0x017) & 0x0F;
switch (current_state) {
case 0x05:
/* We are in the ALERT state. */
_io_iface->poke8(0x014, 0x21);
boost::this_thread::sleep(boost::posix_time::milliseconds(5));
_io_iface->poke8(0x014, 0x00);
break;
case 0x0A:
/* We are in the FDD state. */
_io_iface->poke8(0x014, 0x00);
break;
default:
throw uhd::runtime_error("[ad9361_device_t] [set_clock_rate:1] AD9361 in unknown state");
break;
};
/* Store the current chain / antenna selections so that we can restore
* them at the end of this routine; all chains will be enabled from
* within setup_rates for calibration purposes. */
uint8_t orig_tx_chains = _regs.txfilt & 0xC0;
uint8_t orig_rx_chains = _regs.rxfilt & 0xC0;
/* Call into the clock configuration / settings function. This is where
* all the hard work gets done. */
double rate = _setup_rates(req_rate);
UHD_LOG << boost::format("[ad9361_device_t::set_clock_rate] rate=%.10f\n") % rate;
/* Transition to the ALERT state and calibrate everything. */
_io_iface->poke8(0x015, 0x04); //dual synth mode, synth en ctrl en
_io_iface->poke8(0x014, 0x05); //use SPI for TXNRX ctrl, to ALERT, TX on
_io_iface->poke8(0x013, 0x01); //enable ENSM
boost::this_thread::sleep(boost::posix_time::milliseconds(1));
_calibrate_synth_charge_pumps();
_tune_helper(RX, _rx_freq);
_tune_helper(TX, _tx_freq);
_program_mixer_gm_subtable();
_program_gain_table();
_setup_gain_control(false);
_reprogram_gains();
set_bw_filter(RX, _baseband_bw);
set_bw_filter(TX, _baseband_bw);
_setup_adc();
_calibrate_baseband_dc_offset();
_calibrate_rf_dc_offset();
_calibrate_rx_quadrature();
/*
* Rx BB DC and IQ tracking are both disabled by calibration at this
* point. Only issue commands if tracking needs to be turned on.
*/
if (_use_dc_offset_tracking)
_configure_bb_dc_tracking();
if (_use_iq_balance_tracking)
_configure_rx_iq_tracking();
_last_rx_cal_freq = _rx_freq;
_last_tx_cal_freq = _tx_freq;
// cals done, set PPORT config
switch (_client_params->get_digital_interface_mode()) {
case AD9361_DDR_FDD_LVCMOS: {
_io_iface->poke8(0x012, 0x02);
}break;
case AD9361_DDR_FDD_LVDS: {
_io_iface->poke8(0x012, 0x10);
}break;
default:
throw uhd::runtime_error("[ad9361_device_t] NOT IMPLEMENTED");
}
_io_iface->poke8(0x013, 0x01); // Set ENSM FDD bit
_io_iface->poke8(0x015, 0x04); // dual synth mode, synth en ctrl en
/* End the function in the same state as the entry state. */
switch (current_state) {
case 0x05:
/* We are already in ALERT. */
break;
case 0x0A:
/* Transition back to FDD, and restore the original antenna
* / chain selections. */
_regs.txfilt = (_regs.txfilt & 0x3F) | orig_tx_chains;
_regs.rxfilt = (_regs.rxfilt & 0x3F) | orig_rx_chains;
_io_iface->poke8(0x002, _regs.txfilt);
_io_iface->poke8(0x003, _regs.rxfilt);
_io_iface->poke8(0x014, 0x21);
break;
default:
throw uhd::runtime_error("[ad9361_device_t] [set_clock_rate:2] AD9361 in unknown state");
break;
};
return rate;
}
/* Set which of the four TX / RX chains provided by AD9361 are active.
*
* AD9361 provides two sets of chains, Side A and Side B. Each side
* provides one TX antenna, and one RX antenna. The B200 maintains the USRP
* standard of providing one antenna connection that is both TX & RX, and
* one that is RX-only - for each chain. Thus, the possible antenna and
* chain selections are:
*
* B200 Antenna AD9361 Side AD9361 Chain
* -------------------------------------------------------------------
* TX / RX1 Side A TX1 (when switched to TX)
* TX / RX1 Side A RX1 (when switched to RX)
* RX1 Side A RX1
*
* TX / RX2 Side B TX2 (when switched to TX)
* TX / RX2 Side B RX2 (when switched to RX)
* RX2 Side B RX2
*/
void ad9361_device_t::set_active_chains(bool tx1, bool tx2, bool rx1, bool rx2)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
/* Clear out the current active chain settings. */
_regs.txfilt = _regs.txfilt & 0x3F;
_regs.rxfilt = _regs.rxfilt & 0x3F;
/* Turn on the different chains based on the passed parameters. */
if (tx1) {
_regs.txfilt = _regs.txfilt | 0x40;
}
if (tx2) {
_regs.txfilt = _regs.txfilt | 0x80;
}
if (rx1) {
_regs.rxfilt = _regs.rxfilt | 0x40;
}
if (rx2) {
_regs.rxfilt = _regs.rxfilt | 0x80;
}
/* Check for FDD state */
uint8_t set_back_to_fdd = 0;
uint8_t ensm_state = _io_iface->peek8(0x017) & 0x0F;
if (ensm_state == 0xA) // FDD
{
/* Put into ALERT state (via the FDD flush state). */
_io_iface->poke8(0x014, 0x01);
set_back_to_fdd = 1;
}
/* Wait for FDD flush state to complete (if necessary) */
while (ensm_state == 0xA || ensm_state == 0xB)
ensm_state = _io_iface->peek8(0x017) & 0x0F;
/* Turn on / off the chains. */
_io_iface->poke8(0x002, _regs.txfilt);
_io_iface->poke8(0x003, _regs.rxfilt);
/*
* Last unconditional Tx calibration point. Any later Tx calibration will
* require user intervention (currently triggered by tuning difference that
* is > 100 MHz). Late calibration provides better performance.
*/
if (tx1 | tx2)
_calibrate_tx_quadrature();
/* Put back into FDD state if necessary */
if (set_back_to_fdd)
_io_iface->poke8(0x014, 0x21);
}
/* Tune the RX or TX frequency.
*
* This is the publicly-accessible tune function. It makes sure the tune
* isn't a redundant request, and if not, passes it on to the class's
* internal tune function.
*
* After tuning, it runs any appropriate calibrations. */
double ad9361_device_t::tune(direction_t direction, const double value)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
double last_cal_freq;
if (direction == RX) {
if (freq_is_nearly_equal(value, _req_rx_freq)) {
return _rx_freq;
}
last_cal_freq = _last_rx_cal_freq;
} else if (direction == TX) {
if (freq_is_nearly_equal(value, _req_tx_freq)) {
return _tx_freq;
}
last_cal_freq = _last_tx_cal_freq;
} else {
throw uhd::runtime_error("[ad9361_device_t] [tune] INVALID_CODE_PATH");
}
/* If we aren't already in the ALERT state, we will need to return to
* the FDD state after tuning. */
int not_in_alert = 0;
if ((_io_iface->peek8(0x017) & 0x0F) != 5) {
/* Force the device into the ALERT state. */
not_in_alert = 1;
_io_iface->poke8(0x014, 0x01);
}
/* Tune the RF VCO! */
double tune_freq = _tune_helper(direction, value);
/* Run any necessary calibrations / setups */
if (direction == RX) {
_program_gain_table();
}
/* Update the gain settings. */
_reprogram_gains();
/*
* Only run the following calibrations if we are more than 100MHz away
* from the previous Tx or Rx calibration point. Leave out single shot
* Rx quadrature unless Rx quad-cal is disabled.
*/
if (std::abs(last_cal_freq - tune_freq) > AD9361_CAL_VALID_WINDOW) {
/* Run the calibration algorithms. */
if (direction == RX) {
_calibrate_rf_dc_offset();
if (!_use_iq_balance_tracking)
_calibrate_rx_quadrature();
if (_use_dc_offset_tracking)
_configure_bb_dc_tracking();
_last_rx_cal_freq = tune_freq;
} else {
_calibrate_tx_quadrature();
_last_tx_cal_freq = tune_freq;
}
/* Rx IQ tracking can be disabled on Rx or Tx re-calibration */
if (_use_iq_balance_tracking)
_configure_rx_iq_tracking();
}
/* If we were in the FDD state, return it now. */
if (not_in_alert) {
_io_iface->poke8(0x014, 0x21);
}
return tune_freq;
}
/* Get the current RX or TX frequency. */
double ad9361_device_t::get_freq(direction_t direction)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
if (direction == RX)
return _rx_freq;
else
return _tx_freq;
}
/* Set the gain of RX1, RX2, TX1, or TX2.
*
* Note that the 'value' passed to this function is the gain index
* for RX. Also note that the RX chains are done in terms of gain, and
* the TX chains are done in terms of attenuation. */
double ad9361_device_t::set_gain(direction_t direction, chain_t chain, const double value)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
if (direction == RX) {
int gain_index = static_cast<int>(value);
/* Clip the gain values to the proper min/max gain values. */
if (gain_index > 76)
gain_index = 76;
if (gain_index < 0)
gain_index = 0;
if (chain == CHAIN_1) {
_rx1_gain = value;
_io_iface->poke8(0x109, gain_index);
} else {
_rx2_gain = value;
_io_iface->poke8(0x10c, gain_index);
}
return gain_index;
} else {
/* Setting the below bits causes a change in the TX attenuation word
* to immediately take effect. */
_io_iface->poke8(0x077, 0x40);
_io_iface->poke8(0x07c, 0x40);
/* Each gain step is -0.25dB. Calculate the attenuation necessary
* for the requested gain, convert it into gain steps, then write
* the attenuation word. Max gain (so zero attenuation) is 89.75.
* Ugly values will be written to the attenuation registers if
* "value" is out of bounds, so range checking must be performed
* outside this function.
*/
double atten = AD9361_MAX_GAIN - value;
uint32_t attenreg = uint32_t(atten * 4);
if (chain == CHAIN_1) {
_tx1_gain = value;
_io_iface->poke8(0x073, attenreg & 0xFF);
_io_iface->poke8(0x074, (attenreg >> 8) & 0x01);
} else {
_tx2_gain = value;
_io_iface->poke8(0x075, attenreg & 0xFF);
_io_iface->poke8(0x076, (attenreg >> 8) & 0x01);
}
return AD9361_MAX_GAIN - ((double) (attenreg) / 4);
}
}
void ad9361_device_t::output_test_tone() // On RF side!
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
/* Output a 480 kHz tone at 800 MHz */
_io_iface->poke8(0x3F4, 0x0B);
_io_iface->poke8(0x3FC, 0xFF);
_io_iface->poke8(0x3FD, 0xFF);
_io_iface->poke8(0x3FE, 0x3F);
}
void ad9361_device_t::digital_test_tone(bool enb) // Digital output
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
_io_iface->poke8(0x3F4, 0x02 | (enb ? 0x01 : 0x00));
}
void ad9361_device_t::data_port_loopback(const bool loopback_enabled)
{
boost::lock_guard<boost::recursive_mutex> lock(_mutex);
_io_iface->poke8(0x3F5, (loopback_enabled ? 0x01 : 0x00));
}
/* Read back the internal RSSI measurement data. The result is in dB
* but not in absolute units. If absolute units are required
* a bench calibration should be done.
* -0.25dB / bit 9bit resolution.*/
double ad9361_device_t::get_rssi(chain_t chain)
{
uint32_t reg_rssi = 0;
uint8_t lsb_bit_pos = 0;
if (chain == CHAIN_1) {
reg_rssi = 0x1A7;
lsb_bit_pos = 0;
}else {
reg_rssi = 0x1A9;
lsb_bit_pos = 1;
}
uint8_t msbs = _io_iface->peek8(reg_rssi);
uint8_t lsb = ((_io_iface->peek8(0x1AB)) >> lsb_bit_pos) & 0x01;
uint16_t val = ((msbs << 1) | lsb);
double rssi = (-0.25f * ((double)val)); //-0.25dB/lsb (See Gain Control Users Guide p. 25)
return rssi;
}
/*
* Returns the reading of the internal temperature sensor.
* One point calibration of the sensor was done according to datasheet
* leading to the given default constant correction factor.
*/
double ad9361_device_t::_get_temperature(const double cal_offset, const double timeout)
{
//set 0x01D[0] to 1 to disable AuxADC GPIO reading
uint8_t tmp = 0;
tmp = _io_iface->peek8(0x01D);
_io_iface->poke8(0x01D, (tmp | 0x01));
_io_iface->poke8(0x00B, 0); //set offset to 0
_io_iface->poke8(0x00C, 0x01); //start reading, clears bit 0x00C[1]
boost::posix_time::ptime start_time = boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration elapsed;
//wait for valid data (toggle of bit 1 in 0x00C)
while(((_io_iface->peek8(0x00C) >> 1) & 0x01) == 0) {
boost::this_thread::sleep(boost::posix_time::microseconds(100));
elapsed = boost::posix_time::microsec_clock::local_time() - start_time;
if(elapsed.total_milliseconds() > (timeout*1000))
{
throw uhd::runtime_error("[ad9361_device_t] timeout while reading temperature");
}
}
_io_iface->poke8(0x00C, 0x00); //clear read flag
uint8_t temp = _io_iface->peek8(0x00E); //read temperature.
double tmp_temp = temp/1.140f; //according to ADI driver
tmp_temp = tmp_temp + cal_offset; //Constant offset acquired by one point calibration.
return tmp_temp;
}
double ad9361_device_t::get_average_temperature(const double cal_offset, const size_t num_samples)
{
double d_temp = 0;
for(size_t i = 0; i < num_samples; i++) {
double tmp_temp = _get_temperature(cal_offset);
d_temp += (tmp_temp/num_samples);
}
return d_temp;
}
/*
* Enable/Disable DC offset tracking
*
* Only disable BB tracking while leaving static RF and BB DC calibrations enabled.
* According to correspondance from ADI, turning off Rx BB DC tracking clears the
* correction words so we don't need to be concerned with leaving the calibration
* in a bad state upon disabling. Testing also confirms this behavior.
*
* Note that Rx IQ tracking does not show similar state clearing behavior when
* disabled.
*/
void ad9361_device_t::set_dc_offset_auto(direction_t direction, const bool on)
{
if (direction == RX) {
_use_dc_offset_tracking = on;
_configure_bb_dc_tracking();
} else {
throw uhd::runtime_error("[ad9361_device_t] [set_dc_offset_auto] Tx DC tracking not supported");
}
}
/*
* Enable/Disable IQ balance tracking
*
* Run static Rx quadrature calibration after disabling quadrature tracking.
* This avoids the situation where a user might disable tracking when the loop
* is in a confused state (e.g. at or near saturation). Otherwise, the
* calibration setting could be forced to and left in a bad state.
*/
void ad9361_device_t::set_iq_balance_auto(direction_t direction, const bool on)
{
if (direction == RX) {
_use_iq_balance_tracking = on;
_configure_rx_iq_tracking();
if (!on) {
_io_iface->poke8(0x014, 0x05); // ALERT mode
_calibrate_rx_quadrature();
_io_iface->poke8(0x014, 0x21); // FDD mode
}
} else {
throw uhd::runtime_error("[ad9361_device_t] [set_iq_balance_auto] Tx IQ tracking not supported");
}
}
/* Sets the RX gain mode to be used.
* If a transition from an AGC to an non AGC mode occurs (or vice versa)
* the gain configuration will be reloaded. */
void ad9361_device_t::_setup_agc(chain_t chain, gain_mode_t gain_mode)
{
uint8_t gain_mode_reg = 0;
uint8_t gain_mode_prev = 0;
uint8_t gain_mode_bits_pos = 0;
gain_mode_reg = _io_iface->peek8(0x0FA);
gain_mode_prev = (gain_mode_reg & 0x0F);
if (chain == CHAIN_1) {
gain_mode_bits_pos = 0;
} else if (chain == CHAIN_2) {
gain_mode_bits_pos = 2;
} else
{
throw uhd::runtime_error("[ad9361_device_t] Wrong value for chain");
}
gain_mode_reg = (gain_mode_reg & (~(0x03<<gain_mode_bits_pos))); //clear mode bits
switch (gain_mode) {
case GAIN_MODE_MANUAL:
//leave bits cleared
break;
case GAIN_MODE_SLOW_AGC:
gain_mode_reg = (gain_mode_reg | (0x02<<gain_mode_bits_pos));
break;
case GAIN_MODE_FAST_AGC:
gain_mode_reg = (gain_mode_reg | (0x01<<gain_mode_bits_pos));
break;
default:
throw uhd::runtime_error("[ad9361_device_t] Gain mode does not exist");
}
_io_iface->poke8(0x0FA, gain_mode_reg);
uint8_t gain_mode_status = _io_iface->peek8(0x0FA);
gain_mode_status = (gain_mode_status & 0x0F);
/*Check if gain mode configuration needs to be reprogrammed*/
if (((gain_mode_prev == 0) && (gain_mode_status != 0)) || ((gain_mode_prev != 0) && (gain_mode_status == 0))) {
if (gain_mode_status == 0) {
/*load manual mode config*/
_setup_gain_control(false);
} else {
/*load agc mode config*/
_setup_gain_control(true);
}
}
}
void ad9361_device_t::set_agc(chain_t chain, bool enable)
{
if(chain == CHAIN_1) {
_rx1_agc_enable = enable;
if(enable) {
_setup_agc(chain, _rx1_agc_mode);
} else {
_setup_agc(chain, GAIN_MODE_MANUAL);
}
} else if (chain == CHAIN_2){
_rx2_agc_enable = enable;
if(enable) {
_setup_agc(chain, _rx2_agc_mode);
} else {
_setup_agc(chain, GAIN_MODE_MANUAL);
}
} else
{
throw uhd::runtime_error("[ad9361_device_t] Wrong value for chain");
}
}
void ad9361_device_t::set_agc_mode(chain_t chain, gain_mode_t gain_mode)
{
if(chain == CHAIN_1) {
_rx1_agc_mode = gain_mode;
if(_rx1_agc_enable) {
_setup_agc(chain, _rx1_agc_mode);
}
} else if(chain == CHAIN_2){
_rx2_agc_mode = gain_mode;
if(_rx2_agc_enable) {
_setup_agc(chain, _rx2_agc_mode);
}
} else
{
throw uhd::runtime_error("[ad9361_device_t] Wrong value for chain");
}
}
std::vector<std::string> ad9361_device_t::get_filter_names(direction_t direction)
{
std::vector<std::string> ret;
if(direction == RX) {
for(std::map<std::string, filter_query_helper>::iterator it = _rx_filters.begin(); it != _rx_filters.end(); ++it) {
ret.push_back(it->first);
}
} else if (direction == TX)
{
for(std::map<std::string, filter_query_helper>::iterator it = _tx_filters.begin(); it != _tx_filters.end(); ++it) {
ret.push_back(it->first);
}
}
return ret;
}
filter_info_base::sptr ad9361_device_t::get_filter(direction_t direction, chain_t chain, const std::string &name)
{
if(direction == RX) {
if (not _rx_filters[name].get)
{
throw uhd::runtime_error("ad9361_device_t::get_filter this filter can not be read.");
}
return _rx_filters[name].get(direction, chain);
} else if (direction == TX) {
if (not _tx_filters[name].get)
{
throw uhd::runtime_error("ad9361_device_t::get_filter this filter can not be read.");
}
return _tx_filters[name].get(direction, chain);
}
throw uhd::runtime_error("ad9361_device_t::get_filter wrong direction parameter.");
}
void ad9361_device_t::set_filter(direction_t direction, chain_t chain, const std::string &name, filter_info_base::sptr filter)
{
if(direction == RX) {
if(not _rx_filters[name].set)
{
throw uhd::runtime_error("ad9361_device_t::set_filter this filter can not be written.");
}
_rx_filters[name].set(direction, chain, filter);
} else if (direction == TX) {
if(not _tx_filters[name].set)
{
throw uhd::runtime_error("ad9361_device_t::set_filter this filter can not be written.");
}
_tx_filters[name].set(direction, chain, filter);
}
}
double ad9361_device_t::set_bw_filter(direction_t direction, const double rf_bw)
{
//both low pass filters are programmed to the same bw. However, their cutoffs will differ.
//Together they should create the requested bb bw.
double set_analog_bb_bw = 0;
if(direction == RX)
{
_rx_bb_lp_bw = _calibrate_baseband_rx_analog_filter(rf_bw); //returns bb bw
_rx_tia_lp_bw = _calibrate_rx_TIAs(rf_bw);
_rx_analog_bw = _rx_bb_lp_bw;
set_analog_bb_bw = _rx_analog_bw;
} else {
_tx_bb_lp_bw = _calibrate_baseband_tx_analog_filter(rf_bw); //returns bb bw
_tx_sec_lp_bw = _calibrate_secondary_tx_filter(rf_bw);
_tx_analog_bw = _tx_bb_lp_bw;
set_analog_bb_bw = _tx_analog_bw;
}
return (2.0 * set_analog_bb_bw);
}
void ad9361_device_t::_set_fir_taps(direction_t direction, chain_t chain, const std::vector<int16_t>& taps)
{
size_t num_taps = taps.size();
size_t num_taps_avail = _get_num_fir_taps(direction);
if(num_taps == num_taps_avail)
{
boost::scoped_array<uint16_t> coeffs(new uint16_t[num_taps_avail]);
for (size_t i = 0; i < num_taps_avail; i++)
{
coeffs[i] = uint16_t(taps[i]);
}
_program_fir_filter(direction, chain, num_taps_avail, coeffs.get());
} else if(num_taps < num_taps_avail){
throw uhd::runtime_error("ad9361_device_t::_set_fir_taps not enough coefficients.");
} else {
throw uhd::runtime_error("ad9361_device_t::_set_fir_taps too many coefficients.");
}
}
size_t ad9361_device_t::_get_num_fir_taps(direction_t direction)
{
uint8_t num = 0;
if(direction == RX)
num = _io_iface->peek8(0x0F5);
else
num = _io_iface->peek8(0x065);
num = ((num >> 5) & 0x07);
return ((num + 1) * 16);
}
size_t ad9361_device_t::_get_fir_dec_int(direction_t direction)
{
uint8_t dec_int = 0;
if(direction == RX)
dec_int = _io_iface->peek8(0x003);
else
dec_int = _io_iface->peek8(0x002);
/*
* 0 = dec/int by 1 and bypass filter
* 1 = dec/int by 1
* 2 = dec/int by 2
* 3 = dec/int by 4 */
dec_int = (dec_int & 0x03);
if(dec_int == 3)
{
return 4;
}
return dec_int;
}
std::vector<int16_t> ad9361_device_t::_get_fir_taps(direction_t direction, chain_t chain)
{
int base;
size_t num_taps = _get_num_fir_taps(direction);
uint8_t config;
uint8_t reg_numtaps = (((num_taps / 16) - 1) & 0x07) << 5;
config = reg_numtaps | 0x02; //start the programming clock
if(chain == CHAIN_1)
{
config = config | (1 << 3);
} else if (chain == CHAIN_2){
config = config | (1 << 4);
} else {
throw uhd::runtime_error("[ad9361_device_t] Can not read both chains synchronously");
}
if(direction == RX)
{
base = 0xF0;
} else {
base = 0x60;
}
_io_iface->poke8(base+5,config);
std::vector<int16_t> taps;
uint8_t lower_val;
uint8_t higher_val;
uint16_t coeff;
for(size_t i = 0;i < num_taps;i++)
{
_io_iface->poke8(base,0x00+i);
lower_val = _io_iface->peek8(base+3);
higher_val = _io_iface->peek8(base+4);
coeff = ((higher_val << 8) | lower_val);
taps.push_back(int16_t(coeff));
}
config = (config & (~(1 << 1))); //disable filter clock
_io_iface->poke8(base+5,config);
return taps;
}
/*
* Returns either RX TIA LPF or TX Secondary LPF
* depending on the direction.
* See UG570 for details on used scaling factors. */
filter_info_base::sptr ad9361_device_t::_get_filter_lp_tia_sec(direction_t direction)
{
double cutoff = 0;
if(direction == RX)
{
cutoff = 2.5 * _rx_tia_lp_bw;
} else {
cutoff = 5 * _tx_sec_lp_bw;
}
filter_info_base::sptr lp(new analog_filter_lp(filter_info_base::ANALOG_LOW_PASS, false, 0, "single-pole", cutoff, 20));
return lp;
}
/*
* Returns RX/TX BB LPF.
* See UG570 for details on used scaling factors. */
filter_info_base::sptr ad9361_device_t::_get_filter_lp_bb(direction_t direction)
{
double cutoff = 0;
if(direction == RX)
{
cutoff = 1.4 * _rx_bb_lp_bw;
} else {
cutoff = 1.6 * _tx_bb_lp_bw;
}
filter_info_base::sptr bb_lp(new analog_filter_lp(filter_info_base::ANALOG_LOW_PASS, false, 1, "third-order Butterworth", cutoff, 60));
return bb_lp;
}
/*
* For RX direction the DEC3 is returned.
* For TX direction the INT3 is returned. */
filter_info_base::sptr ad9361_device_t::_get_filter_dec_int_3(direction_t direction)
{
uint8_t enable = 0;
double rate = _adcclock_freq;
double full_scale;
size_t dec = 0;
size_t interpol = 0;
filter_info_base::filter_type type = filter_info_base::DIGITAL_I16;
std::string name;
int16_t taps_array_rx[] = {55, 83, 0, -393, -580, 0, 1914, 4041, 5120, 4041, 1914, 0, -580, -393, 0, 83, 55};
int16_t taps_array_tx[] = {36, -19, 0, -156, -12, 0, 479, 233, 0, -1215, -993, 0, 3569, 6277, 8192, 6277, 3569, 0, -993, -1215, 0, 223, 479, 0, -12, -156, 0, -19, 36};
std::vector<int16_t> taps;
filter_info_base::sptr ret;
if(direction == RX)
{
full_scale = 16384;
dec = 3;
interpol = 1;
enable = _io_iface->peek8(0x003);
enable = ((enable >> 4) & 0x03);
taps.assign(taps_array_rx, taps_array_rx + sizeof(taps_array_rx) / sizeof(int16_t) );
} else {
full_scale = 8192;
dec = 1;
interpol = 3;
uint8_t use_dac_clk_div = _io_iface->peek8(0x00A);
use_dac_clk_div = ((use_dac_clk_div >> 3) & 0x01);
if(use_dac_clk_div == 1)
{
rate = rate / 2;
}
enable = _io_iface->peek8(0x002);
enable = ((enable >> 4) & 0x03);
if(enable == 2) //0 => int. by 1, 1 => int. by 2 (HB3), 2 => int. by 3
{
rate /= 3;
}
taps.assign(taps_array_tx, taps_array_tx + sizeof(taps_array_tx) / sizeof(int16_t) );
}
ret = filter_info_base::sptr(new digital_filter_base<int16_t>(type, (enable != 2) ? true : false, 2, rate, interpol, dec, full_scale, taps.size(), taps));
return ret;
}
filter_info_base::sptr ad9361_device_t::_get_filter_hb_3(direction_t direction)
{
uint8_t enable = 0;
double rate = _adcclock_freq;
double full_scale = 0;
size_t dec = 1;
size_t interpol = 1;
filter_info_base::filter_type type = filter_info_base::DIGITAL_I16;
int16_t taps_array_rx[] = {1, 4, 6, 4, 1};
int16_t taps_array_tx[] = {1, 2, 1};
std::vector<int16_t> taps;
if(direction == RX)
{
full_scale = 16;
dec = 2;
enable = _io_iface->peek8(0x003);
enable = ((enable >> 4) & 0x03);
taps.assign(taps_array_rx, taps_array_rx + sizeof(taps_array_rx) / sizeof(int16_t) );
} else {
full_scale = 2;
interpol = 2;
uint8_t use_dac_clk_div = _io_iface->peek8(0x00A);
use_dac_clk_div = ((use_dac_clk_div >> 3) & 0x01);
if(use_dac_clk_div == 1)
{
rate = rate / 2;
}
enable = _io_iface->peek8(0x002);
enable = ((enable >> 4) & 0x03);
if(enable == 1)
{
rate /= 2;
}
taps.assign(taps_array_tx, taps_array_tx + sizeof(taps_array_tx) / sizeof(int16_t) );
}
filter_info_base::sptr hb = filter_info_base::sptr(new digital_filter_base<int16_t>(type, (enable != 1) ? true : false, 2, rate, interpol, dec, full_scale, taps.size(), taps));
return hb;
}
filter_info_base::sptr ad9361_device_t::_get_filter_hb_2(direction_t direction)
{
uint8_t enable = 0;
double rate = _adcclock_freq;
double full_scale = 0;
size_t dec = 1;
size_t interpol = 1;
filter_info_base::filter_type type = filter_info_base::DIGITAL_I16;
int16_t taps_array[] = {-9, 0, 73, 128, 73, 0, -9};
std::vector<int16_t> taps(taps_array, taps_array + sizeof(taps_array) / sizeof(int16_t) );
digital_filter_base<int16_t>::sptr hb_3 = boost::dynamic_pointer_cast<digital_filter_base<int16_t> >(_get_filter_hb_3(direction));
digital_filter_base<int16_t>::sptr dec_int_3 = boost::dynamic_pointer_cast<digital_filter_base<int16_t> >(_get_filter_dec_int_3(direction));
if(direction == RX)
{
full_scale = 256;
dec = 2;
enable = _io_iface->peek8(0x003);
} else {
full_scale = 128;
interpol = 2;
enable = _io_iface->peek8(0x002);
}
enable = ((enable >> 3) & 0x01);
if(!(hb_3->is_bypassed()))
{
if(direction == RX)
{
rate = hb_3->get_output_rate();
}else if (direction == TX) {
rate = hb_3->get_input_rate();
if(enable)
{
rate /= 2;
}
}
} else { //else dec3/int3 or none of them is used.
if(direction == RX)
{
rate = dec_int_3->get_output_rate();
}else if (direction == TX) {
rate = dec_int_3->get_input_rate();
if(enable)
{
rate /= 2;
}
}
}
filter_info_base::sptr hb(new digital_filter_base<int16_t>(type, (enable == 0) ? true : false, 3, rate, interpol, dec, full_scale, taps.size(), taps));
return hb;
}
filter_info_base::sptr ad9361_device_t::_get_filter_hb_1(direction_t direction)
{
uint8_t enable = 0;
double rate = 0;
double full_scale = 0;
size_t dec = 1;
size_t interpol = 1;
filter_info_base::filter_type type = filter_info_base::DIGITAL_I16;
std::vector<int16_t> taps;
int16_t taps_rx_array[] = {-8, 0, 42, 0, -147, 0, 619, 1013, 619, 0, -147, 0, 42, 0, -8};
int16_t taps_tx_array[] = {-53, 0, 313, 0, -1155, 0, 4989, 8192, 4989, 0, -1155, 0, 313, 0, -53};
digital_filter_base<int16_t>::sptr hb_2 = boost::dynamic_pointer_cast<digital_filter_base<int16_t> >(_get_filter_hb_2(direction));
if(direction == RX)
{
full_scale = 2048;
dec = 2;
enable = _io_iface->peek8(0x003);
enable = ((enable >> 2) & 0x01);
rate = hb_2->get_output_rate();
taps.assign(taps_rx_array, taps_rx_array + sizeof(taps_rx_array) / sizeof(int16_t) );
} else if (direction == TX) {
full_scale = 8192;
interpol = 2;
enable = _io_iface->peek8(0x002);
enable = ((enable >> 2) & 0x01);
rate = hb_2->get_input_rate();
if(enable)
{
rate /= 2;
}
taps.assign(taps_tx_array, taps_tx_array + sizeof(taps_tx_array) / sizeof(int16_t) );
}
filter_info_base::sptr hb(new digital_filter_base<int16_t>(type, (enable == 0) ? true : false, 4, rate, interpol, dec, full_scale, taps.size(), taps));
return hb;
}
filter_info_base::sptr ad9361_device_t::_get_filter_fir(direction_t direction, chain_t chain)
{
double rate = 0;
size_t dec = 1;
size_t interpol = 1;
size_t max_num_taps = 128;
uint8_t enable = 1;
digital_filter_base<int16_t>::sptr hb_1 = boost::dynamic_pointer_cast<digital_filter_base<int16_t> >(_get_filter_hb_1(direction));
if(direction == RX)
{
dec = _get_fir_dec_int(direction);
if(dec == 0)
{
enable = 0;
dec = 1;
}
interpol = 1;
rate = hb_1->get_output_rate();
}else if (direction == TX) {
interpol = _get_fir_dec_int(direction);
if(interpol == 0)
{
enable = 0;
interpol = 1;
}
dec = 1;
rate = hb_1->get_input_rate();
if(enable)
{
rate /= interpol;
}
}
max_num_taps = _get_num_fir_taps(direction);
filter_info_base::sptr fir(new digital_filter_fir<int16_t>(filter_info_base::DIGITAL_FIR_I16, (enable == 0) ? true : false, 5, rate, interpol, dec, 32767, max_num_taps, _get_fir_taps(direction, chain)));
return fir;
}
void ad9361_device_t::_set_filter_fir(direction_t direction, chain_t channel, filter_info_base::sptr filter)
{
digital_filter_fir<int16_t>::sptr fir = boost::dynamic_pointer_cast<digital_filter_fir<int16_t> >(filter);
//only write taps. Ignore everything else for now
_set_fir_taps(direction, channel, fir->get_taps());
}
/*
* If BW of one of the analog filters gets overwritten manually,
* _tx_analog_bw and _rx_analog_bw are not valid any more!
* For useful data in those variables set_bw_filter method should be used
*/
void ad9361_device_t::_set_filter_lp_bb(direction_t direction, filter_info_base::sptr filter)
{
analog_filter_lp::sptr lpf = boost::dynamic_pointer_cast<analog_filter_lp>(filter);
double bw = lpf->get_cutoff();
if(direction == RX)
{
//remember: this function takes rf bw as its input and calibrated to 1.4 x the given value
_rx_bb_lp_bw = _calibrate_baseband_rx_analog_filter(2 * bw / 1.4); //returns bb bw
} else {
//remember: this function takes rf bw as its input and calibrates to 1.6 x the given value
_tx_bb_lp_bw = _calibrate_baseband_tx_analog_filter(2 * bw / 1.6);
}
}
void ad9361_device_t::_set_filter_lp_tia_sec(direction_t direction, filter_info_base::sptr filter)
{
analog_filter_lp::sptr lpf = boost::dynamic_pointer_cast<analog_filter_lp>(filter);
double bw = lpf->get_cutoff();
if(direction == RX)
{
//remember: this function takes rf bw as its input and calibrated to 2.5 x the given value
_rx_tia_lp_bw = _calibrate_rx_TIAs(2 * bw / 2.5); //returns bb bw
} else {
//remember: this function takes rf bw as its input and calibrates to 5 x the given value
_tx_sec_lp_bw = _calibrate_secondary_tx_filter(2 * bw / 5);
}
}
}}
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_device.h · 289 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef INCLUDED_AD9361_DEVICE_H
#define INCLUDED_AD9361_DEVICE_H
#include <ad9361_client.h>
#include <boost/noncopyable.hpp>
#include <boost/thread/recursive_mutex.hpp>
#include <uhd/types/filters.hpp>
#include <uhd/types/sensors.hpp>
#include <complex>
#include <vector>
#include <map>
#include "boost/assign.hpp"
#include "boost/bind.hpp"
#include "boost/function.hpp"
namespace uhd { namespace usrp {
class ad9361_device_t : public boost::noncopyable
{
public:
enum direction_t { RX, TX };
enum gain_mode_t {GAIN_MODE_MANUAL, GAIN_MODE_SLOW_AGC, GAIN_MODE_FAST_AGC};
enum chain_t { CHAIN_1, CHAIN_2, CHAIN_BOTH };
ad9361_device_t(ad9361_params::sptr client, ad9361_io::sptr io_iface) :
_client_params(client), _io_iface(io_iface),
_bbpll_freq(0.0), _adcclock_freq(0.0), _req_clock_rate(0.0),
_req_coreclk(0.0), _rx_bbf_tunediv(0), _curr_gain_table(0),
_rx1_gain(0.0), _rx2_gain(0.0), _tx1_gain(0.0), _tx2_gain(0.0),
_tfir_factor(0), _rfir_factor(0),
_rx1_agc_mode(GAIN_MODE_MANUAL), _rx2_agc_mode(GAIN_MODE_MANUAL),
_rx1_agc_enable(false), _rx2_agc_enable(false),
_use_dc_offset_tracking(false), _use_iq_balance_tracking(false)
{
/*
* This Boost.Assign to_container() workaround is necessary because STL containers
* apparently confuse newer versions of MSVC.
*
* Source: http://www.boost.org/doc/libs/1_55_0/libs/assign/doc/#portability
*/
_rx_filters = (boost::assign::map_list_of("LPF_TIA", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_lp_tia_sec, this, _1),
boost::bind(&ad9361_device_t::_set_filter_lp_tia_sec, this, _1, _3)))
("LPF_BB", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_lp_bb, this, _1),
boost::bind(&ad9361_device_t::_set_filter_lp_bb, this, _1, _3)))
("HB_3", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_3, this, _1), 0))
("DEC_3", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_dec_int_3, this, _1), 0))
("HB_2", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_2, this, _1), 0))
("HB_1", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_1, this, _1), 0))
("FIR_1", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_fir, this, _1, _2),
boost::bind(&ad9361_device_t::_set_filter_fir, this, _1, _2, _3)))).to_container(_rx_filters);
_tx_filters = (boost::assign::map_list_of("LPF_SECONDARY", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_lp_tia_sec, this, _1),
boost::bind(&ad9361_device_t::_set_filter_lp_tia_sec, this, _1, _3)))
("LPF_BB", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_lp_bb, this, _1),
boost::bind(&ad9361_device_t::_set_filter_lp_bb, this, _1, _3)))
("HB_3", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_3, this, _1), 0))
("INT_3", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_dec_int_3, this, _1), 0))
("HB_2", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_2, this, _1), 0))
("HB_1", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_hb_1, this, _1), 0))
("FIR_1", filter_query_helper(boost::bind(&ad9361_device_t::_get_filter_fir, this, _1, _2),
boost::bind(&ad9361_device_t::_set_filter_fir, this, _1, _2, _3)))).to_container(_tx_filters);
}
/* Initialize the AD9361 codec. */
void initialize();
/* Set SPI interface */
void set_io_iface(ad9361_io::sptr io_iface);
/* This function sets the RX / TX rate between AD9361 and the FPGA, and
* thus determines the interpolation / decimation required in the FPGA to
* achieve the user's requested rate.
*/
double set_clock_rate(const double req_rate);
/* Set which of the four TX / RX chains provided by AD9361 are active.
*
* AD9361 provides two sets of chains, Side A and Side B. Each side
* provides one TX antenna, and one RX antenna. The B200 maintains the USRP
* standard of providing one antenna connection that is both TX & RX, and
* one that is RX-only - for each chain. Thus, the possible antenna and
* chain selections are:
*
*/
void set_active_chains(bool tx1, bool tx2, bool rx1, bool rx2);
/* Tune the RX or TX frequency.
*
* This is the publicly-accessible tune function. It makes sure the tune
* isn't a redundant request, and if not, passes it on to the class's
* internal tune function.
*
* After tuning, it runs any appropriate calibrations. */
double tune(direction_t direction, const double value);
/* Get the current RX or TX frequency. */
double get_freq(direction_t direction);
/* Set the gain of RX1, RX2, TX1, or TX2.
*
* Note that the 'value' passed to this function is the actual gain value,
* _not_ the gain index. This is the opposite of the eval software's GUI!
* Also note that the RX chains are done in terms of gain, and the TX chains
* are done in terms of attenuation. */
double set_gain(direction_t direction, chain_t chain, const double value);
/* Make AD9361 output its test tone. */
void output_test_tone();
void digital_test_tone(bool enb); // Digital output
/* Turn on/off AD9361's TX port --> RX port loopback. */
void data_port_loopback(const bool loopback_enabled);
/* Read back the internal RSSI measurement data. */
double get_rssi(chain_t chain);
/*! Read the internal temperature sensor
*\param calibrate return raw sensor readings or apply calibration factor.
*\param num_samples number of measurements to average over
*/
double get_average_temperature(const double cal_offset = -30.0, const size_t num_samples = 3);
/* Turn on/off AD9361's RX DC offset correction */
void set_dc_offset_auto(direction_t direction, const bool on);
/* Turn on/off AD9361's RX IQ imbalance correction */
void set_iq_balance_auto(direction_t direction, const bool on);
/* Configure AD9361's AGC module to use either fast or slow AGC mode. */
void set_agc_mode(chain_t chain, gain_mode_t gain_mode);
/* Enable AD9361's AGC gain mode. */
void set_agc(chain_t chain, bool enable);
/* Set bandwidth of AD9361's analog LP filters.
* Bandwidth should be RF bandwidth */
double set_bw_filter(direction_t direction, const double rf_bw);
/*
* Filter API implementation
* */
filter_info_base::sptr get_filter(direction_t direction, chain_t chain, const std::string &name);
void set_filter(direction_t direction, chain_t chain, const std::string &name, filter_info_base::sptr filter);
std::vector<std::string> get_filter_names(direction_t direction);
//Constants
static const double AD9361_MAX_GAIN;
static const double AD9361_MAX_CLOCK_RATE;
static const double AD9361_MIN_CLOCK_RATE;
static const double AD9361_CAL_VALID_WINDOW;
static const double AD9361_RECOMMENDED_MAX_BANDWIDTH;
static const double DEFAULT_RX_FREQ;
static const double DEFAULT_TX_FREQ;
private: //Methods
void _program_fir_filter(direction_t direction, int num_taps, uint16_t *coeffs);
void _setup_tx_fir(size_t num_taps, int32_t interpolation);
void _setup_rx_fir(size_t num_taps, int32_t decimation);
void _program_fir_filter(direction_t direction, chain_t chain, int num_taps, uint16_t *coeffs);
void _setup_tx_fir(size_t num_taps);
void _setup_rx_fir(size_t num_taps);
void _calibrate_lock_bbpll();
void _calibrate_synth_charge_pumps();
double _calibrate_baseband_rx_analog_filter(double rfbw);
double _calibrate_baseband_tx_analog_filter(double rfbw);
double _calibrate_secondary_tx_filter(double rfbw);
double _calibrate_rx_TIAs(double rfbw);
void _setup_adc();
void _calibrate_baseband_dc_offset();
void _calibrate_rf_dc_offset();
void _calibrate_rx_quadrature();
void _tx_quadrature_cal_routine();
void _calibrate_tx_quadrature();
void _program_mixer_gm_subtable();
void _program_gain_table();
void _setup_gain_control(bool use_agc);
void _setup_synth(direction_t direction, double vcorate);
double _tune_bbvco(const double rate);
void _reprogram_gains();
double _tune_helper(direction_t direction, const double value);
double _setup_rates(const double rate);
double _get_temperature(const double cal_offset, const double timeout = 0.1);
void _configure_bb_dc_tracking();
void _configure_rx_iq_tracking();
void _setup_agc(chain_t chain, gain_mode_t gain_mode);
void _set_fir_taps(direction_t direction, chain_t chain, const std::vector<int16_t>& taps);
std::vector<int16_t> _get_fir_taps(direction_t direction, chain_t chain);
size_t _get_num_fir_taps(direction_t direction);
size_t _get_fir_dec_int(direction_t direction);
filter_info_base::sptr _get_filter_lp_tia_sec(direction_t direction);
filter_info_base::sptr _get_filter_lp_bb(direction_t direction);
filter_info_base::sptr _get_filter_dec_int_3(direction_t direction);
filter_info_base::sptr _get_filter_hb_3(direction_t direction);
filter_info_base::sptr _get_filter_hb_2(direction_t direction);
filter_info_base::sptr _get_filter_hb_1(direction_t direction);
filter_info_base::sptr _get_filter_fir(direction_t direction, chain_t chain);
void _set_filter_fir(direction_t direction, chain_t channel, filter_info_base::sptr filter);
void _set_filter_lp_bb(direction_t direction, filter_info_base::sptr filter);
void _set_filter_lp_tia_sec(direction_t direction, filter_info_base::sptr filter);
private: //Members
struct chip_regs_t
{
chip_regs_t():
vcodivs(0), inputsel(0), rxfilt(0), txfilt(0),
bbpll(0), bbftune_config(0), bbftune_mode(0) {}
uint8_t vcodivs;
uint8_t inputsel;
uint8_t rxfilt;
uint8_t txfilt;
uint8_t bbpll;
uint8_t bbftune_config;
uint8_t bbftune_mode;
};
struct filter_query_helper
{
filter_query_helper(
boost::function<filter_info_base::sptr (direction_t, chain_t)> p_get,
boost::function<void (direction_t, chain_t, filter_info_base::sptr)> p_set
) : get(p_get), set(p_set) { }
filter_query_helper(){ }
boost::function<filter_info_base::sptr (direction_t, chain_t)> get;
boost::function<void (direction_t, chain_t, filter_info_base::sptr)> set;
};
std::map<std::string, filter_query_helper> _rx_filters;
std::map<std::string, filter_query_helper> _tx_filters;
//Interfaces
ad9361_params::sptr _client_params;
ad9361_io::sptr _io_iface;
//Intermediate state
double _rx_freq, _tx_freq, _req_rx_freq, _req_tx_freq;
double _last_rx_cal_freq, _last_tx_cal_freq;
double _rx_analog_bw, _tx_analog_bw, _rx_bb_lp_bw, _tx_bb_lp_bw;
double _rx_tia_lp_bw, _tx_sec_lp_bw;
//! Current baseband sampling rate (this is the actual rate the device is
// is running at)
double _baseband_bw;
double _bbpll_freq, _adcclock_freq;
//! This was the last clock rate value that was requested.
// It is cached so we don't need to re-set the clock rate
// if another call to set_clock_rate() actually has the same value.
double _req_clock_rate;
double _req_coreclk;
uint16_t _rx_bbf_tunediv;
uint8_t _curr_gain_table;
double _rx1_gain, _rx2_gain, _tx1_gain, _tx2_gain;
int32_t _tfir_factor;
int32_t _rfir_factor;
gain_mode_t _rx1_agc_mode, _rx2_agc_mode;
bool _rx1_agc_enable, _rx2_agc_enable;
//Register soft-copies
chip_regs_t _regs;
//Synchronization
boost::recursive_mutex _mutex;
bool _use_dc_offset_tracking;
bool _use_iq_balance_tracking;
};
}} //namespace
#endif /* INCLUDED_AD9361_DEVICE_H */
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_filter_taps.h · 122 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef INCLUDED_AD9361_FILTER_TAPS_HPP
#define INCLUDED_AD9361_FILTER_TAPS_HPP
#include <stdint.h>
/* A default 128-tap filter that can be used for generic circumstances. */
/* static uint16_t default_128tap_coeffs[] = {
0x0001,0xfff1,0xffcf,0xffc0,0xffe8,0x0020,0x001a,0xffe3,
0xffe1,0x001f,0x0028,0xffdf,0xffcc,0x0024,0x0043,0xffdb,
0xffac,0x0026,0x0068,0xffdb,0xff80,0x0022,0x009a,0xffe2,
0xff47,0x0017,0x00db,0xfff3,0xfeff,0xffff,0x012b,0x0013,
0xfea5,0xffd7,0x0190,0x0046,0xfe35,0xff97,0x020e,0x0095,
0xfda7,0xff36,0x02ae,0x010d,0xfcf0,0xfea1,0x0383,0x01c6,
0xfbf3,0xfdb6,0x04b7,0x02f8,0xfa6d,0xfc1a,0x06be,0x0541,
0xf787,0xf898,0x0b60,0x0b6d,0xee88,0xea40,0x2786,0x7209
};
*/
/* The below pair of filters is from ADI and "optimized for a 10MHz LTE application". */
/*
static uint16_t lte10mhz_rx_coeffs[] = {
0xffe2,0x0042,0x0024,0x0095,0x0056,0x004d,0xffcf,0xffb7,
0xffb1,0x0019,0x0059,0x006a,0x0004,0xff9d,0xff72,0xffd4,
0x0063,0x00b7,0x0062,0xffac,0xff21,0xff59,0x0032,0x0101,
0x00f8,0x0008,0xfeea,0xfeac,0xffa3,0x0117,0x01b5,0x00d0,
0xff05,0xfdea,0xfe9e,0x00ba,0x026f,0x0215,0xffb5,0xfd4a,
0xfd18,0xffa0,0x02de,0x03dc,0x0155,0xfd2a,0xfb0d,0xfd54,
0x0287,0x062f,0x048a,0xfe37,0xf862,0xf8c1,0x004d,0x0963,
0x0b88,0x02a4,0xf3e7,0xebdd,0xf5f8,0x1366,0x3830,0x518b
};
static uint16_t lte10mhz_tx_coeffs[] = {
0xfffb,0x0000,0x0004,0x0017,0x0024,0x0028,0x0013,0xfff3,
0xffdc,0xffe5,0x000b,0x0030,0x002e,0xfffe,0xffc4,0xffb8,
0xfff0,0x0045,0x0068,0x002b,0xffb6,0xff72,0xffad,0x0047,
0x00b8,0x0088,0xffc8,0xff1c,0xff33,0x001a,0x0110,0x0124,
0x0019,0xfec8,0xfe74,0xff9a,0x0156,0x0208,0x00d3,0xfe9b,
0xfd68,0xfe96,0x015d,0x033f,0x0236,0xfecd,0xfc00,0xfcb5,
0x00d7,0x04e5,0x04cc,0xffd5,0xf9fe,0xf8fb,0xfef2,0x078c,
0x0aae,0x036d,0xf5c0,0xed89,0xf685,0x12af,0x36a4,0x4faa
};
*/
/************************************************************/
/* These filters suitable for decimation/interpolation by 2 */
/************************************************************/
/* 127 tap Halfband designed with: round(2^16 * halfgen4(0.9/4,32)) (center tap tweaked to 32767) */
static int16_t hb127_coeffs[] = {
-0,0,1,-0,-2,0,3,-0,-5,0,8,-0,-11,0,17,-0,-24,0,33,-0,-45,0,61,-0,-80,0,104,-0,-134,0,169,-0,
-213,0,264,-0,-327,0,401,-0,-489,0,595,-0,-724,0,880,-0,-1075,0,1323,-0,-1652,0,2114,-0,-2819,0,4056,-0,-6883,0,20837,32767,
20837,0,-6883,-0,4056,0,-2819,-0,2114,0,-1652,-0,1323,0,-1075,-0,880,0,-724,-0,595,0,-489,-0,401,0,-327,-0,264,0,-213,-0,
169,0,-134,-0,104,0,-80,-0,61,0,-45,-0,33,0,-24,-0,17,0,-11,-0,8,0,-5,-0,3,0,-2,-0,1,0,-0, 0 };
/* 95 tap Halfband designed with: round(2^16 * halfgen4(0.9/4,24)) (center tap tweaked to 32767) */
static int16_t hb95_coeffs[] = {
-4,0,8,-0,-14,0,23,-0,-36,0,52,-0,-75,0,104,-0,-140,0,186,-0,-243,0,314,-0,-400,0,505,-0,-634,0,793,-0,
-993,0,1247,-0,-1585,0,2056,-0,-2773,0,4022,-0,-6862,0,20830,32767,20830,0,-6862,-0,4022,0,-2773,-0,2056,0,-1585,-0,1247,0,-993,-0,
793,0,-634,-0,505,0,-400,-0,314,0,-243,-0,186,0,-140,-0,104,0,-75,-0,52,0,-36,-0,23,0,-14,-0,8,0,-4,0};
/* 63 tap Halfband designed with: round(2^16 * halfgen4(0.9/4,16)) (center tap tweaked to 32767) */
static int16_t hb63_coeffs[] = {
-58,0,83,-0,-127,0,185,-0,-262,0,361,-0,-488,0,648,-0,-853,0,1117,-0,-1466,0,1954,-0,-2689,0,3960,-0,-6825,0,20818,32767,
20818,0,-6825,-0,3960,0,-2689,-0,1954,0,-1466,-0,1117,0,-853,-0,648,0,-488,-0,361,0,-262,-0,185,0,-127,-0,83,0,-58,0};
/* 47 tap Halfband designed with: round(2^16 * halfgen4(0.85/4,12)) (center tap tweaked to 32767) */
static int16_t hb47_coeffs[] = {
-50,0,98,-0,-181,0,307,-0,-489,0,747,-0,-1109,0,1628,-0,-2413,0,3750,-0,-6693,0,20773,32767,20773,0,-6693,-0,3750,0,-2413,-0,
1628,0,-1109,-0,747,0,-489,-0,307,0,-181,-0,98,0,-50,0};
/************************************************************/
/* These filters suitable for decimation/interpolation by 4 */
/* Designed for -3dB rolloff @ Fs/4 */
/************************************************************/
/* 128 tap equiripple FIR low-pass designed with: round(2^16 * fir1(127,0.25)); */
static int16_t fir_128_x4_coeffs[] = {
-15,-27,-23,-6,17,33,31,9,-23,-47,-45,-13,34,69,67,21,-49,-102,-99,-32,69,146,143,48,-96,-204,-200,-69,129,278,275,97,-170,
-372,-371,-135,222,494,497,187,-288,-654,-665,-258,376,875,902,363,-500,-1201,-1265,-530,699,1748,1906,845,-1089,-2922,-3424,
-1697,2326,7714,12821,15921,15921,12821,7714,2326,-1697,-3424,-2922,-1089,845,1906,1748,699,-530,-1265,-1201,-500,363,902,875,
376,-258,-665,-654,-288,187,497,494,222,-135,-371,-372,-170,97,275,278,129,-69,-200,-204,-96,48,143,146,69,-32,-99,-102,-49,21,
67,69,34,-13,-45,-47,-23,9,31,33,17,-6,-23,-27,-15};
/* 96 tap equiripple FIR low-pass designed with: round(2^16 * fir1(95,0.25)); */
static int16_t fir_96_x4_coeffs[] = {
-18,-35,-33,-11,23,50,51,18,-37,-83,-86,-31,62,140,145,54,-98,-224,-232,-88,149,343,356,138,-218,-509,-530,-211,313,743,781,
320,-447,-1089,-1163,-494,658,1663,1830,819,-1062,-2868,-3379,-1682,2314,7695,12812,15924,15924,12812,7695,2314,-1682,-3379,
-2868,-1062,819,1830,1663,658,-494,-1163,-1089,-447,320,781,743,313,-211,-530,-509,-218,138,356,343,149,-88,-232,-224,-98,54,
145,140,62,-31,-86,-83,-37,18,51,50,23,-11,-33,-35,-18};
/* 64 tap equiripple FIR low-pass designed with: round(2^16 * fir1(63,0.25)); */
static int16_t fir_64_x4_coeffs[] = {
-25,-54,-56,-22,41,102,117,50,-87,-223,-253,-109,174,443,496,215,-317,-809,-903,-398,550,1434,1623,744,-987,-2715,-3251,
-1640,2279,7638,12782,15928,15928,12782,7638,2279,-1640,-3251,-2715,-987,744,1623,1434,550,-398,-903,-809,-317,215,496,
443,174,-109,-253,-223,-87,50,117,102,41,-22,-56,-54,-25};
/* 48 tap equiripple FIR low-pass designed with: round(2^16 * fir1(47,0.25)); */
static int16_t fir_48_x4_coeffs[] = {
-32,-74,-84,-39,68,191,237,114,-183,-508,-609,-287,419,1149,1358,647,-887,-2508,-3073,-1580,2230,7555,12736,15928,15928,
12736,7555,2230,-1580,-3073,-2508,-887,647,1358,1149,419,-287,-609,-508,-183,114,237,191,68,-39,-84,-74,-32};
/* NOTE: To write coeffs directly from Octave in a format that's useful in C use: dlmwrite ("file.csv",bb,","); */
#endif // INCLUDED_AD9361_FILTER_TAPS_HPP
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_gain_tables.h · 111 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef INCLUDED_AD9361_GAIN_TABLES_HPP
#define INCLUDED_AD9361_GAIN_TABLES_HPP
#include <stdint.h>
uint8_t gain_table_sub_1300mhz[77][3] = {
{ 0x00, 0x00, 0x20 }, { 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x00 },
{ 0x00, 0x01, 0x00 }, { 0x00, 0x02, 0x00 }, { 0x00, 0x03, 0x00 },
{ 0x00, 0x04, 0x00 }, { 0x00, 0x05, 0x00 }, { 0x01, 0x03, 0x20 },
{ 0x01, 0x04, 0x00 }, { 0x01, 0x05, 0x00 }, { 0x01, 0x06, 0x00 },
{ 0x01, 0x07, 0x00 }, { 0x01, 0x08, 0x00 }, { 0x01, 0x09, 0x00 },
{ 0x01, 0x0A, 0x00 }, { 0x01, 0x0B, 0x00 }, { 0x01, 0x0C, 0x00 },
{ 0x01, 0x0D, 0x00 }, { 0x01, 0x0E, 0x00 }, { 0x02, 0x09, 0x20 },
{ 0x02, 0x0A, 0x00 }, { 0x02, 0x0B, 0x00 }, { 0x02, 0x0C, 0x00 },
{ 0x02, 0x0D, 0x00 }, { 0x02, 0x0E, 0x00 }, { 0x02, 0x0F, 0x00 },
{ 0x02, 0x10, 0x00 }, { 0x02, 0x2B, 0x20 }, { 0x02, 0x2C, 0x00 },
{ 0x04, 0x28, 0x20 }, { 0x04, 0x29, 0x00 }, { 0x04, 0x2A, 0x00 },
{ 0x04, 0x2B, 0x00 }, { 0x24, 0x20, 0x20 }, { 0x24, 0x21, 0x00 },
{ 0x44, 0x20, 0x20 }, { 0x44, 0x21, 0x00 }, { 0x44, 0x22, 0x00 },
{ 0x44, 0x23, 0x00 }, { 0x44, 0x24, 0x00 }, { 0x44, 0x25, 0x00 },
{ 0x44, 0x26, 0x00 }, { 0x44, 0x27, 0x00 }, { 0x44, 0x28, 0x00 },
{ 0x44, 0x29, 0x00 }, { 0x44, 0x2A, 0x00 }, { 0x44, 0x2B, 0x00 },
{ 0x44, 0x2C, 0x00 }, { 0x44, 0x2D, 0x00 }, { 0x44, 0x2E, 0x00 },
{ 0x44, 0x2F, 0x00 }, { 0x44, 0x30, 0x00 }, { 0x44, 0x31, 0x00 },
{ 0x44, 0x32, 0x00 }, { 0x64, 0x2E, 0x20 }, { 0x64, 0x2F, 0x00 },
{ 0x64, 0x30, 0x00 }, { 0x64, 0x31, 0x00 }, { 0x64, 0x32, 0x00 },
{ 0x64, 0x33, 0x00 }, { 0x64, 0x34, 0x00 }, { 0x64, 0x35, 0x00 },
{ 0x64, 0x36, 0x00 }, { 0x64, 0x37, 0x00 }, { 0x64, 0x38, 0x00 },
{ 0x65, 0x38, 0x20 }, { 0x66, 0x38, 0x20 }, { 0x67, 0x38, 0x20 },
{ 0x68, 0x38, 0x20 }, { 0x69, 0x38, 0x20 }, { 0x6A, 0x38, 0x20 },
{ 0x6B, 0x38, 0x20 }, { 0x6C, 0x38, 0x20 }, { 0x6D, 0x38, 0x20 },
{ 0x6E, 0x38, 0x20 }, { 0x6F, 0x38, 0x20 } };
uint8_t gain_table_1300mhz_to_4000mhz[77][3] = {
{ 0x00, 0x00, 0x20 }, { 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x00 },
{ 0x00, 0x01, 0x00 }, { 0x00, 0x02, 0x00 }, { 0x00, 0x03, 0x00 },
{ 0x00, 0x04, 0x00 }, { 0x00, 0x05, 0x00 }, { 0x01, 0x03, 0x20 },
{ 0x01, 0x04, 0x00 }, { 0x01, 0x05, 0x00 }, { 0x01, 0x06, 0x00 },
{ 0x01, 0x07, 0x00 }, { 0x01, 0x08, 0x00 }, { 0x01, 0x09, 0x00 },
{ 0x01, 0x0A, 0x00 }, { 0x01, 0x0B, 0x00 }, { 0x01, 0x0C, 0x00 },
{ 0x01, 0x0D, 0x00 }, { 0x01, 0x0E, 0x00 }, { 0x02, 0x09, 0x20 },
{ 0x02, 0x0A, 0x00 }, { 0x02, 0x0B, 0x00 }, { 0x02, 0x0C, 0x00 },
{ 0x02, 0x0D, 0x00 }, { 0x02, 0x0E, 0x00 }, { 0x02, 0x0F, 0x00 },
{ 0x02, 0x10, 0x00 }, { 0x02, 0x2B, 0x20 }, { 0x02, 0x2C, 0x00 },
{ 0x04, 0x27, 0x20 }, { 0x04, 0x28, 0x00 }, { 0x04, 0x29, 0x00 },
{ 0x04, 0x2A, 0x00 }, { 0x04, 0x2B, 0x00 }, { 0x24, 0x21, 0x20 },
{ 0x24, 0x22, 0x00 }, { 0x44, 0x20, 0x20 }, { 0x44, 0x21, 0x00 },
{ 0x44, 0x22, 0x00 }, { 0x44, 0x23, 0x00 }, { 0x44, 0x24, 0x00 },
{ 0x44, 0x25, 0x00 }, { 0x44, 0x26, 0x00 }, { 0x44, 0x27, 0x00 },
{ 0x44, 0x28, 0x00 }, { 0x44, 0x29, 0x00 }, { 0x44, 0x2A, 0x00 },
{ 0x44, 0x2B, 0x00 }, { 0x44, 0x2C, 0x00 }, { 0x44, 0x2D, 0x00 },
{ 0x44, 0x2E, 0x00 }, { 0x44, 0x2F, 0x00 }, { 0x44, 0x30, 0x00 },
{ 0x44, 0x31, 0x00 }, { 0x64, 0x2E, 0x20 }, { 0x64, 0x2F, 0x00 },
{ 0x64, 0x30, 0x00 }, { 0x64, 0x31, 0x00 }, { 0x64, 0x32, 0x00 },
{ 0x64, 0x33, 0x00 }, { 0x64, 0x34, 0x00 }, { 0x64, 0x35, 0x00 },
{ 0x64, 0x36, 0x00 }, { 0x64, 0x37, 0x00 }, { 0x64, 0x38, 0x00 },
{ 0x65, 0x38, 0x20 }, { 0x66, 0x38, 0x20 }, { 0x67, 0x38, 0x20 },
{ 0x68, 0x38, 0x20 }, { 0x69, 0x38, 0x20 }, { 0x6A, 0x38, 0x20 },
{ 0x6B, 0x38, 0x20 }, { 0x6C, 0x38, 0x20 }, { 0x6D, 0x38, 0x20 },
{ 0x6E, 0x38, 0x20 }, { 0x6F, 0x38, 0x20 } };
uint8_t gain_table_4000mhz_to_6000mhz[77][3] = {
{ 0x00, 0x00, 0x20 }, { 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x00 },
{ 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x00 }, { 0x00, 0x01, 0x00 },
{ 0x00, 0x02, 0x00 }, { 0x00, 0x03, 0x00 }, { 0x01, 0x01, 0x20 },
{ 0x01, 0x02, 0x00 }, { 0x01, 0x03, 0x00 }, { 0x01, 0x04, 0x20 },
{ 0x01, 0x05, 0x00 }, { 0x01, 0x06, 0x00 }, { 0x01, 0x07, 0x00 },
{ 0x01, 0x08, 0x00 }, { 0x01, 0x09, 0x00 }, { 0x01, 0x0A, 0x00 },
{ 0x01, 0x0B, 0x00 }, { 0x01, 0x0C, 0x00 }, { 0x02, 0x08, 0x20 },
{ 0x02, 0x09, 0x00 }, { 0x02, 0x0A, 0x00 }, { 0x02, 0x0B, 0x20 },
{ 0x02, 0x0C, 0x00 }, { 0x02, 0x0D, 0x00 }, { 0x02, 0x0E, 0x00 },
{ 0x02, 0x0F, 0x00 }, { 0x02, 0x2A, 0x20 }, { 0x02, 0x2B, 0x00 },
{ 0x04, 0x27, 0x20 }, { 0x04, 0x28, 0x00 }, { 0x04, 0x29, 0x00 },
{ 0x04, 0x2A, 0x00 }, { 0x04, 0x2B, 0x00 }, { 0x04, 0x2C, 0x00 },
{ 0x04, 0x2D, 0x00 }, { 0x24, 0x20, 0x20 }, { 0x24, 0x21, 0x00 },
{ 0x24, 0x22, 0x00 }, { 0x44, 0x20, 0x20 }, { 0x44, 0x21, 0x00 },
{ 0x44, 0x22, 0x00 }, { 0x44, 0x23, 0x00 }, { 0x44, 0x24, 0x00 },
{ 0x44, 0x25, 0x00 }, { 0x44, 0x26, 0x00 }, { 0x44, 0x27, 0x00 },
{ 0x44, 0x28, 0x00 }, { 0x44, 0x29, 0x00 }, { 0x44, 0x2A, 0x00 },
{ 0x44, 0x2B, 0x00 }, { 0x44, 0x2C, 0x00 }, { 0x44, 0x2D, 0x00 },
{ 0x44, 0x2E, 0x00 }, { 0x64, 0x2E, 0x20 }, { 0x64, 0x2F, 0x00 },
{ 0x64, 0x30, 0x00 }, { 0x64, 0x31, 0x00 }, { 0x64, 0x32, 0x00 },
{ 0x64, 0x33, 0x00 }, { 0x64, 0x34, 0x00 }, { 0x64, 0x35, 0x00 },
{ 0x64, 0x36, 0x00 }, { 0x64, 0x37, 0x00 }, { 0x64, 0x38, 0x00 },
{ 0x65, 0x38, 0x20 }, { 0x66, 0x38, 0x20 }, { 0x67, 0x38, 0x20 },
{ 0x68, 0x38, 0x20 }, { 0x69, 0x38, 0x20 }, { 0x6A, 0x38, 0x20 },
{ 0x6B, 0x38, 0x20 }, { 0x6C, 0x38, 0x20 }, { 0x6D, 0x38, 0x20 },
{ 0x6E, 0x38, 0x20 }, { 0x6F, 0x38, 0x20 } };
#endif /* INCLUDED_AD9361_GAIN_TABLES_HPP */
wifi_e310_link/vendor/uhd_3_10_ad9361/ad9361_synth_lut.h · 149 lines
Download this file · Permanent section link
//
// Copyright 2014 Ettus Research
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef INCLUDED_AD9361_SYNTH_LUT_HPP
#define INCLUDED_AD9361_SYNTH_LUT_HPP
double vco_index[53] = {12605000000.0, 12245000000.0, 11906000000.0, 11588000000.0,
11288000000.0, 11007000000.0, 10742000000.0, 10492000000.0,
10258000000.0, 10036000000.0, 9827800000.0, 9631100000.0,
9445300000.0, 9269800000.0, 9103600000.0, 8946300000.0,
8797000000.0, 8655300000.0, 8520600000.0, 8392300000.0,
8269900000.0, 8153100000.0, 8041400000.0, 7934400000.0,
7831800000.0, 7733200000.0, 7638400000.0, 7547100000.0,
7459000000.0, 7374000000.0, 7291900000.0, 7212400000.0,
7135500000.0, 7061000000.0, 6988700000.0, 6918600000.0,
6850600000.0, 6784600000.0, 6720500000.0, 6658200000.0,
6597800000.0, 6539200000.0, 6482300000.0, 6427000000.0,
6373400000.0, 6321400000.0, 6270900000.0, 6222000000.0,
6174500000.0, 6128400000.0, 6083600000.0, 6040100000.0,
5997700000.0};
int synth_cal_lut[53][12] = { {10, 0, 4, 0, 15, 8, 8, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 15, 8, 9, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 15, 8, 10, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 15, 8, 11, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 15, 8, 11, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 14, 8, 12, 13, 4, 13, 15, 9},
{10, 0, 4, 0, 14, 8, 13, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 14, 9, 13, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 14, 9, 14, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 14, 9, 15, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 14, 9, 15, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 13, 9, 16, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 13, 9, 17, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 13, 9, 18, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 13, 9, 18, 13, 4, 13, 15, 9},
{10, 0, 5, 1, 13, 9, 19, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 14, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 14, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 15, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 15, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 16, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 16, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 17, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 17, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 18, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 18, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 19, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 19, 13, 4, 13, 15, 9},
{10, 1, 6, 1, 15, 11, 20, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 12, 20, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 12, 21, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 12, 21, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 22, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 22, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 23, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 23, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 24, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 24, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 25, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 25, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 26, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 26, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 27, 13, 4, 13, 15, 9},
{10, 1, 7, 2, 15, 14, 27, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 18, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 18, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 18, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 19, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 19, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 19, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 19, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 20, 13, 4, 13, 15, 9},
{10, 3, 7, 3, 15, 12, 20, 13, 4, 13, 15, 9}};
#if 0 /* This is the table for a 40MHz RFPLL Reference */
int synth_cal_lut[53][12] = { {10, 0, 4, 0, 15, 8, 8, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 15, 8, 9, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 15, 8, 9, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 15, 8, 10, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 15, 8, 11, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 15, 8, 11, 12, 3, 14, 15, 11},
{10, 0, 4, 0, 14, 8, 12, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 13, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 13, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 14, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 15, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 15, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 16, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 17, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 17, 12, 3, 14, 15, 11},
{10, 0, 5, 1, 14, 9, 18, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 13, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 14, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 14, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 15, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 15, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 16, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 16, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 17, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 17, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 17, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 18, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 18, 12, 3, 14, 15, 11},
{10, 1, 6, 1, 15, 11, 19, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 12, 19, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 12, 20, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 12, 20, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 21, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 21, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 22, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 22, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 23, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 23, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 24, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 24, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 25, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 25, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 26, 12, 3, 14, 15, 11},
{10, 1, 7, 2, 15, 14, 26, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 17, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 17, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 17, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 18, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 18, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 18, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 18, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 19, 12, 3, 14, 15, 11},
{10, 3, 7, 3, 15, 12, 19, 12, 3, 14, 15, 11} };
#endif
#endif /* INCLUDED_AD9361_SYNTH_LUT_HPP */
wifi_pluto_link/fpga_sifs/open_xc7/gf_e310_io_open.sv · 161 lines
Download this file · Permanent section link
// SPDX-License-Identifier: LGPL-3.0-or-later
// Open-tool E310 CMOS interface, functionally derived from Ettus e310_io.v.
// Default open flow uses direct BUFG capture because nextpnr-xilinx does not
// expose BUFR as a placeable BEL. USE_RX_BUFR selects the stock regional
// capture clock and BUFR->BUFG arrangement in the native Vivado build.
`timescale 1ns/1ps
module gf_e310_io_open #(
parameter integer USE_RX_BUFR = 0
) (
input wire areset,
input wire mimo,
output wire radio_clk,
output wire radio_rst,
output reg [11:0] rx_i0,
output reg [11:0] rx_q0,
output reg [11:0] rx_i1,
output reg [11:0] rx_q1,
output reg rx_stb,
input wire [11:0] tx_i0,
input wire [11:0] tx_q0,
input wire [11:0] tx_i1,
input wire [11:0] tx_q1,
output reg tx_stb,
input wire rx_clk,
input wire rx_frame,
input wire [11:0] rx_data,
output wire tx_clk,
output wire tx_frame,
output wire [11:0] tx_data
);
wire radio_clock_input;
wire capture_clk;
generate
if (USE_RX_BUFR != 0) begin : g_rx_bufr
// The native Vivado path can use the stock E310 regional input
// capture clock, avoiding the global tree's input hold penalty.
BUFR #(.BUFR_DIVIDE("BYPASS")) capture_buffer (
.I(rx_clk), .CE(1'b1), .CLR(1'b0), .O(radio_clock_input));
assign capture_clk = radio_clock_input;
end else begin : g_rx_global
assign radio_clock_input = rx_clk;
assign capture_clk = radio_clk;
end
endgenerate
BUFG radio_clock_buffer (.I(radio_clock_input), .O(radio_clk));
synchronizer #(.STAGES(3), .INITIAL_VAL(1'b1)) radio_reset_sync (
.clk(radio_clk), .rst(areset), .in(1'b0), .out(radio_rst));
wire mimo_sync;
synchronizer mimo_mode_sync (
.clk(radio_clk), .rst(radio_rst), .in(mimo), .out(mimo_sync));
wire [11:0] rx_i;
wire [11:0] rx_q;
genvar bit_index;
generate
for (bit_index = 0; bit_index < 12; bit_index = bit_index + 1) begin : rx_ddr
IDDR #(.DDR_CLK_EDGE("SAME_EDGE")) input_ddr (
.C(capture_clk), .CE(1'b1), .R(1'b0), .S(1'b0),
.D(rx_data[bit_index]),
.Q1(rx_q[bit_index]), .Q2(rx_i[bit_index]));
end
endgenerate
wire rx_frame_rising;
wire rx_frame_falling;
IDDR #(.DDR_CLK_EDGE("SAME_EDGE")) frame_input_ddr (
.C(capture_clk), .CE(1'b1), .R(1'b0), .S(1'b0),
.D(rx_frame), .Q1(rx_frame_rising), .Q2(rx_frame_falling));
always @(posedge radio_clk or posedge radio_rst) begin
if (radio_rst) begin
rx_stb <= 1'b0;
rx_i0 <= 12'd0;
rx_q0 <= 12'd0;
rx_i1 <= 12'd0;
rx_q1 <= 12'd0;
end else if (mimo_sync) begin
if (rx_frame_rising) begin
rx_i0 <= rx_i;
rx_q0 <= rx_q;
end else begin
rx_i1 <= rx_i;
rx_q1 <= rx_q;
end
rx_stb <= ~rx_frame_rising;
end else begin
rx_i0 <= rx_i;
rx_q0 <= rx_q;
rx_i1 <= rx_i;
rx_q1 <= rx_q;
rx_stb <= 1'b1;
end
end
reg [11:0] tx_i = 12'd0;
reg [11:0] tx_q = 12'd0;
reg tx_frame_internal = 1'b1;
// Match the stock E310 AD9361 source-synchronous output exactly. Each
// pin uses its I/O-site ODDR: I on the rising edge, Q on the falling edge,
// with a same-domain forwarded clock. The open nextpnr database includes
// the fixed OUTFF -> OMUX -> IOB path required by these primitives.
generate
for (bit_index = 0; bit_index < 12; bit_index = bit_index + 1) begin : tx_ddr
ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) output_ddr (
.C(radio_clk), .CE(1'b1), .R(1'b0), .S(1'b0),
.D1(tx_i[bit_index]), .D2(tx_q[bit_index]),
.Q(tx_data[bit_index]));
end
endgenerate
ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) frame_output_ddr (
.C(radio_clk), .CE(1'b1), .R(1'b0), .S(1'b0),
.D1(tx_frame_internal), .D2(tx_frame_internal & mimo_sync),
.Q(tx_frame));
ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) clock_output_ddr (
.C(radio_clk), .CE(1'b1), .R(1'b0), .S(1'b0),
.D1(1'b1), .D2(1'b0), .Q(tx_clk));
reg [11:0] tx_i1_hold = 12'd0;
reg [11:0] tx_q1_hold = 12'd0;
always @(posedge radio_clk or posedge radio_rst) begin
if (radio_rst) begin
tx_stb <= 1'b0;
tx_frame_internal <= 1'b1;
tx_i <= 12'd0;
tx_q <= 12'd0;
tx_i1_hold <= 12'd0;
tx_q1_hold <= 12'd0;
end else if (mimo_sync) begin
tx_stb <= ~tx_stb;
tx_frame_internal <= tx_stb;
if (tx_stb) begin
tx_i <= tx_i0;
tx_q <= tx_q0;
tx_i1_hold <= tx_i1;
tx_q1_hold <= tx_q1;
end else begin
tx_i <= tx_i1_hold;
tx_q <= tx_q1_hold;
end
end else begin
tx_stb <= 1'b1;
tx_frame_internal <= 1'b1;
if ({tx_i0, tx_q0} != 24'd0) begin
tx_i <= tx_i0;
tx_q <= tx_q0;
end else begin
tx_i <= tx_i1;
tx_q <= tx_q1;
end
end
end
wire unused_rx_frame_falling = rx_frame_falling;
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_dsss_1mbps_control_tx.sv · 404 lines
Download this file · Permanent section link
// Compact IEEE 802.11 1 Mb/s long-preamble ACK/CTS formatter.
//
// This block stores no I/Q waveform. It formats the 14-byte control PSDU,
// computes its FCS, scrambles the complete PPDU, applies DBPSK and the
// 11-chip Barker sequence, and uses a 11/20 fractional chip clock to emit
// 6,080 complex samples at 20 MS/s. The initial implementation deliberately
// uses rectangular chip pulses; retained-I/Q testing decides the pulse-shaping
// refinement before this path is connected to Pluto's AD9361 TX datapath.
`timescale 1ns/1ps
module gf_dsss_1mbps_control_tx #(
parameter integer IQ_WIDTH = 32,
parameter integer AMPLITUDE = 8192,
parameter integer COUNT_WIDTH = 32,
parameter integer SERIAL_CONTROL_CRC = 0
) (
input wire clk,
input wire resetn,
input wire arm,
input wire kill,
input wire response_prepare,
input wire response_start,
input wire response_is_cts,
input wire [15:0] response_duration_us,
input wire [47:0] response_station_mac,
// tx_sample_tick is the AD9361 DAC sample-rate strobe. The formatter
// holds state on intervening fabric clocks and advances only when a sample
// is consumed. tx_sink_ready is an independent mux/backpressure gate.
input wire tx_sample_tick,
input wire tx_sink_ready,
output wire response_path_ready,
output wire response_active,
output wire tx_override_valid,
output wire [IQ_WIDTH-1:0] tx_override_iq,
output reg response_done,
output reg stream_abort,
output reg [COUNT_WIDTH-1:0] stream_abort_count
);
localparam integer PPDU_BITS = 304;
localparam integer BARKER_CHIPS = 11;
localparam integer PPDU_CHIPS = PPDU_BITS * BARKER_CHIPS;
localparam integer SAMPLE_TICKS = 20;
localparam integer CHIP_TICKS = 11;
localparam integer PPDU_SAMPLES =
(PPDU_CHIPS * SAMPLE_TICKS) / CHIP_TICKS;
localparam integer SAMPLE_INDEX_WIDTH = $clog2(PPDU_SAMPLES);
localparam signed [15:0] AMP = AMPLITUDE;
reg prepared;
reg active;
reg start_pending;
reg frame_is_cts;
reg [15:0] frame_duration_us;
reg [47:0] frame_station_mac;
wire [31:0] frame_fcs;
reg [8:0] bit_index;
reg [3:0] barker_index;
reg [4:0] sample_phase;
reg [6:0] scrambler_state;
reg carrier_negative;
reg [SAMPLE_INDEX_WIDTH-1:0] sample_index;
reg [8:0] next_bit_index;
reg [3:0] next_barker_index;
reg [4:0] next_sample_phase;
reg [6:0] next_scrambler_state;
reg next_carrier_negative;
reg following_plain_bit;
reg following_scrambled_bit;
reg [5:0] phase_sum;
initial begin
if (IQ_WIDTH != 32)
$error("The compact DSSS formatter requires 16-bit I and Q");
if (PPDU_SAMPLES != 6080)
$error("Unexpected 1 Mb/s ACK/CTS sample count");
if (AMPLITUDE < 1 || AMPLITUDE > 32767)
$error("AMPLITUDE must fit positive signed IQ16");
end
function automatic [7:0] control_prefix_byte;
input integer byte_number;
input is_cts;
input [15:0] duration_us;
input [47:0] station_mac;
begin
case (byte_number)
0: control_prefix_byte = is_cts ? 8'hc4 : 8'hd4;
1: control_prefix_byte = 8'h00;
2: control_prefix_byte = is_cts ? duration_us[7:0] : 8'h00;
3: control_prefix_byte = is_cts ? duration_us[15:8] : 8'h00;
4: control_prefix_byte = station_mac[47:40];
5: control_prefix_byte = station_mac[39:32];
6: control_prefix_byte = station_mac[31:24];
7: control_prefix_byte = station_mac[23:16];
8: control_prefix_byte = station_mac[15:8];
9: control_prefix_byte = station_mac[7:0];
default: control_prefix_byte = 8'h00;
endcase
end
endfunction
function automatic [31:0] control_crc32;
input is_cts;
input [15:0] duration_us;
input [47:0] station_mac;
integer byte_number;
integer crc_bit;
reg [7:0] value;
reg [31:0] crc;
begin
crc = 32'hffff_ffff;
for (byte_number = 0; byte_number < 10;
byte_number = byte_number + 1) begin
value = control_prefix_byte(byte_number, is_cts,
duration_us, station_mac);
crc = crc ^ value;
for (crc_bit = 0; crc_bit < 8; crc_bit = crc_bit + 1) begin
if (crc[0])
crc = (crc >> 1) ^ 32'hedb8_8320;
else
crc = crc >> 1;
end
end
control_crc32 = ~crc;
end
endfunction
function automatic plain_bit_at;
input [8:0] requested_bit;
input is_cts;
input [15:0] duration_us;
input [47:0] station_mac;
input [31:0] fcs;
integer offset;
integer byte_number;
integer bit_number;
reg [7:0] value;
reg [15:0] sfd;
begin
value = 8'h00;
sfd = 16'hf3a0;
if (requested_bit < 128) begin
plain_bit_at = 1'b1;
end else if (requested_bit < 144) begin
plain_bit_at = sfd[requested_bit - 128];
end else if (requested_bit < 192) begin
offset = requested_bit - 144;
byte_number = offset >> 3;
bit_number = offset & 7;
case (byte_number)
0: value = 8'h0a; // 1 Mb/s SIGNAL.
1: value = 8'h00; // SERVICE.
2: value = 8'h70; // 14 bytes * 8 us.
3: value = 8'h00;
4: value = 8'hb4; // CRC-16 of 0a 00 70 00.
5: value = 8'hd0;
default: value = 8'h00;
endcase
plain_bit_at = value[bit_number];
end else begin
offset = requested_bit - 192;
byte_number = offset >> 3;
bit_number = offset & 7;
if (byte_number < 10)
value = control_prefix_byte(byte_number, is_cts,
duration_us, station_mac);
else begin
case (byte_number)
10: value = fcs[7:0];
11: value = fcs[15:8];
12: value = fcs[23:16];
13: value = fcs[31:24];
default: value = 8'h00;
endcase
end
plain_bit_at = value[bit_number];
end
end
endfunction
function automatic barker_negative;
input [3:0] index;
begin
case (index)
1, 4, 8, 9, 10: barker_negative = 1'b1;
default: barker_negative = 1'b0;
endcase
end
endfunction
// The SIFS scheduler runs on the fabric clock, while DAC samples may be a
// sparse clock-enable in that domain. Accept the deadline command on any
// fabric cycle and hold it until the exact next DAC sample boundary.
wire launch = (response_start || start_pending) && prepared && !active &&
tx_sample_tick && tx_sink_ready && arm && !kill;
wire chip_negative =
carrier_negative ^ barker_negative(barker_index);
wire signed [15:0] output_i = chip_negative ? -AMP : AMP;
wire signed [15:0] first_output_i = -AMP;
assign response_path_ready = prepared && !active && !start_pending &&
tx_sink_ready && arm && !kill;
assign response_active = active || start_pending || launch;
assign tx_override_valid = arm && !kill && (launch || active);
assign tx_override_iq = launch
? {16'h0000, first_output_i}
: {16'h0000, output_i};
// A header bit enters the transmitter once per 20 DAC samples. Advance
// the FCS over that same plain bit; it is not needed until bit 272.
// The registered feedback tree commits in five clocks, well before the
// next bit at either 20 or 40 fabric clocks/symbol. First-sample timing,
// preparation readiness and every emitted sample remain unchanged.
wire crc_header_bit = active && tx_sample_tick && tx_sink_ready &&
phase_sum >= SAMPLE_TICKS && barker_index == BARKER_CHIPS-1 &&
bit_index >= 191 && bit_index < 271 && !response_prepare;
generate if (SERIAL_CONTROL_CRC) begin : g_serial_control_crc
gf_control_crc_bitserial crc (
.clk(clk),.clear(!resetn || !arm || kill || response_prepare),
.bit_valid(crc_header_bit),.data_bit(following_plain_bit),.fcs(frame_fcs)
);
end else begin : g_parallel_control_crc
reg [31:0] prepared_fcs;
always @(posedge clk) begin
if (!resetn) prepared_fcs <= 32'd0;
else if (arm && !kill && response_prepare)
prepared_fcs <= control_crc32(response_is_cts,
response_duration_us,response_station_mac);
end
assign frame_fcs = prepared_fcs;
end endgenerate
always @* begin
next_bit_index = bit_index;
next_barker_index = barker_index;
next_sample_phase = sample_phase + CHIP_TICKS;
next_scrambler_state = scrambler_state;
next_carrier_negative = carrier_negative;
following_plain_bit = 1'b0;
following_scrambled_bit = 1'b0;
phase_sum = sample_phase + CHIP_TICKS;
if (phase_sum >= SAMPLE_TICKS) begin
next_sample_phase = phase_sum - SAMPLE_TICKS;
if (barker_index < BARKER_CHIPS - 1) begin
next_barker_index = barker_index + 1'b1;
end else begin
next_barker_index = 4'd0;
next_bit_index = bit_index + 1'b1;
following_plain_bit = plain_bit_at(
bit_index + 1'b1, frame_is_cts, frame_duration_us,
frame_station_mac, frame_fcs);
following_scrambled_bit = following_plain_bit ^
scrambler_state[3] ^ scrambler_state[6];
next_scrambler_state =
{scrambler_state[5:0], following_scrambled_bit};
if (following_scrambled_bit)
next_carrier_negative = ~carrier_negative;
end
end
end
always @(posedge clk) begin
if (!resetn) begin
prepared <= 1'b0;
active <= 1'b0;
start_pending <= 1'b0;
frame_is_cts <= 1'b0;
frame_duration_us <= 16'd0;
frame_station_mac <= 48'd0;
bit_index <= 9'd0;
barker_index <= 4'd0;
sample_phase <= 5'd0;
// Plain sync bit zero scrambles to one from seed 0x5d.
scrambler_state <= 7'h3b;
carrier_negative <= 1'b1;
sample_index <= {SAMPLE_INDEX_WIDTH{1'b0}};
response_done <= 1'b0;
stream_abort <= 1'b0;
stream_abort_count <= {COUNT_WIDTH{1'b0}};
end else begin
response_done <= 1'b0;
stream_abort <= 1'b0;
if (kill || !arm) begin
prepared <= 1'b0;
active <= 1'b0;
start_pending <= 1'b0;
end else begin
if (response_prepare) begin
prepared <= 1'b1;
active <= 1'b0;
start_pending <= 1'b0;
frame_is_cts <= response_is_cts;
frame_duration_us <= response_duration_us;
frame_station_mac <= response_station_mac;
bit_index <= 9'd0;
barker_index <= 4'd0;
sample_phase <= 5'd0;
scrambler_state <= 7'h3b;
carrier_negative <= 1'b1;
sample_index <= {SAMPLE_INDEX_WIDTH{1'b0}};
end
if (response_start && !response_path_ready) begin
prepared <= 1'b0;
active <= 1'b0;
start_pending <= 1'b0;
stream_abort <= 1'b1;
stream_abort_count <= stream_abort_count + 1'b1;
end else if (response_start && !tx_sample_tick) begin
start_pending <= 1'b1;
end else if (launch) begin
prepared <= 1'b0;
active <= 1'b1;
start_pending <= 1'b0;
sample_index <= {{(SAMPLE_INDEX_WIDTH-1){1'b0}}, 1'b1};
bit_index <= next_bit_index;
barker_index <= next_barker_index;
sample_phase <= next_sample_phase;
scrambler_state <= next_scrambler_state;
carrier_negative <= next_carrier_negative;
end else if (start_pending && tx_sample_tick) begin
// A sink stall at the one legal launch boundary is fatal;
// never slide an ACK/CTS to a later sample.
if (!tx_sink_ready) begin
prepared <= 1'b0;
active <= 1'b0;
start_pending <= 1'b0;
stream_abort <= 1'b1;
stream_abort_count <= stream_abort_count + 1'b1;
end
end else if (active && tx_sample_tick) begin
if (!tx_sink_ready) begin
active <= 1'b0;
stream_abort <= 1'b1;
stream_abort_count <= stream_abort_count + 1'b1;
end else if (sample_index + 1 >= PPDU_SAMPLES) begin
active <= 1'b0;
response_done <= 1'b1;
end else begin
sample_index <= sample_index + 1'b1;
bit_index <= next_bit_index;
barker_index <= next_barker_index;
sample_phase <= next_sample_phase;
scrambler_state <= next_scrambler_state;
carrier_negative <= next_carrier_negative;
end
end
end
end
end
endmodule
// Reflected CRC-32, one input bit at a time. Store the complemented CRC so
// reset-to-zero represents the all-ones seed and the output is directly FCS.
// Reuse the Greenforest registered binary duplication trees: no feedback or
// bit-valid leaf drives more than two CRC-state bits. Minimum input interval
// is six clocks; the formatter supplies at least twenty. Clear discards all
// in-flight bits and wins over a pending state update.
module gf_control_crc_bitserial (
input wire clk,clear,bit_valid,data_bit,
output wire [31:0] fcs
);
localparam [31:0] POLYNOMIAL=32'hedb88320;
wire [15:0] feedback_leaf,valid_leaf;
gf_serial_fanout #(.N(16)) feedback_tree (
.clk(clk),.reset(clear),.bit_in((~fcs[0]) ^ data_bit),.leaves(feedback_leaf)
);
gf_serial_fanout #(.N(16)) valid_tree (
.clk(clk),.reset(clear),.bit_in(bit_valid),.leaves(valid_leaf)
);
generate for(genvar bit_number=0;bit_number<32;bit_number=bit_number+1) begin : g_bit
wire state;
wire shifted;
if(bit_number==31) assign shifted=1'b1;
else assign shifted=fcs[bit_number+1];
wire next_state=shifted ^ (POLYNOMIAL[bit_number] && feedback_leaf[bit_number/2]);
`ifdef SYNTHESIS
// Keep enable in the flip-flop, not an inferred Q-feedback hold mux:
// Q then drives only its successor/feedback and the output consumer.
(* DONT_TOUCH = "true" *) FDCE #(.INIT(1'b0)) state_reg (
.C(clk),.CE(valid_leaf[bit_number/2]),.CLR(clear),.D(next_state),.Q(state)
);
`else
reg state_model;
always @(posedge clk or posedge clear) begin
if(clear) state_model<=1'b0;
else if(valid_leaf[bit_number/2])
state_model<=next_state;
end
assign state=state_model;
`endif
assign fcs[bit_number]=state;
end endgenerate
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_dsss_1mbps_rx.sv · 842 lines
Download this file · Permanent section link
// Streaming IEEE 802.11 long-preamble 1 Mb/s DSSS receiver.
//
// This is the narrow RX PHY needed by the hard-real-time ACK/CTS path. It
// consumes complex IQ16 samples at 20 MS/s, correlates the 20-sample image of
// the 11-chip Barker code, tracks all 20 symbol phases, differentially detects
// DBPSK, self-synchronizes the scrambler, validates SFD and PLCP CRC-16, and
// emits the PSDU bytes (including FCS). Higher-rate PHYs remain separate.
`timescale 1ns/1ps
module gf_dsss_1mbps_rx #(
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer MAX_PSDU_BYTES = 4095,
parameter integer COUNT_WIDTH = 32,
parameter integer PIPELINED_DIFFERENTIAL = 0,
parameter integer SINGLE_PHASE_RX = 0,
parameter integer SERIAL_DIFFERENTIAL = 0,
parameter integer TIMING_SCORE_RAM = 0,
parameter integer RECURSIVE_CORRELATOR = 0,
// Optional SAME-clk correlation boundary. The producer suppresses the
// first 19 incomplete windows, then supplies x[n..n-19] every sample.
// This is not itself a clock-domain crossing or a radio deployment.
parameter integer EXTERNAL_CORRELATOR = 0,
parameter integer EXTERNAL_CORRELATOR_LATENCY = 0
) (
input wire clk,
input wire resetn,
input wire enable,
input wire rx_sample_valid,
input wire signed [15:0] rx_i,
input wire signed [15:0] rx_q,
output reg psdu_start,
output reg psdu_byte_valid,
output reg [7:0] psdu_byte,
output reg psdu_byte_last,
output reg [DECISION_AGE_WIDTH-1:0] psdu_end_age_cycles,
output wire receiver_active,
// Sideband before byte assembly, including FCS bits. The restart is at
// accepted PLCP end, never at PSDU end while the final CRC is in flight.
output wire psdu_bit_valid,
output wire psdu_bit_value,
output wire psdu_crc_restart,
output reg [COUNT_WIDTH-1:0] sfd_count,
output reg [COUNT_WIDTH-1:0] plcp_ok_count,
output reg [COUNT_WIDTH-1:0] plcp_error_count,
output reg [COUNT_WIDTH-1:0] psdu_count,
input wire external_correlation_valid,
input wire signed [23:0] external_correlation_i,
input wire signed [23:0] external_correlation_q
);
localparam integer SYMBOL_SAMPLES = 20;
localparam integer PHASE_WIDTH = 5;
localparam integer CORRELATION_WIDTH = 24;
localparam integer DIFFERENTIAL_INPUT_WIDTH = 18;
localparam integer DIFFERENTIAL_SCALE_BITS = 3;
localparam integer TIMING_SCORE_WIDTH = CORRELATION_WIDTH + 5;
localparam integer PSDU_LENGTH_WIDTH = 12;
localparam [1:0] SEARCH_SFD = 2'd0;
localparam [1:0] READ_PLCP = 2'd1;
localparam [1:0] READ_PSDU = 2'd2;
reg [1:0] receive_state;
reg [PHASE_WIDTH-1:0] phase_index;
reg [PHASE_WIDTH-1:0] locked_phase;
reg [PHASE_WIDTH-1:0] candidate_phase;
wire [PHASE_WIDTH-1:0] decode_phase_index = SINGLE_PHASE_RX ? 5'd0 : phase_index;
reg [5:0] window_fill;
reg [DECISION_AGE_WIDTH-1:0] clocks_since_sample;
reg signed [15:0] i_delay [0:SYMBOL_SAMPLES-1];
reg signed [15:0] q_delay [0:SYMBOL_SAMPLES-1];
reg signed [CORRELATION_WIDTH-1:0] previous_i [0:SYMBOL_SAMPLES-1];
reg signed [CORRELATION_WIDTH-1:0] previous_q [0:SYMBOL_SAMPLES-1];
reg phase_has_previous [0:SYMBOL_SAMPLES-1];
reg [6:0] lane_scrambler [0:SYMBOL_SAMPLES-1];
reg [7:0] lane_one_run [0:SYMBOL_SAMPLES-1];
reg [5:0] lane_sfd_budget [0:SYMBOL_SAMPLES-1];
reg [15:0] lane_sfd_shift [0:SYMBOL_SAMPLES-1];
reg [TIMING_SCORE_WIDTH-1:0] timing_score [0:SYMBOL_SAMPLES-1];
reg [TIMING_SCORE_WIDTH-1:0] timing_best_score;
reg [PHASE_WIDTH-1:0] timing_best_phase;
// Select timing from all 20 unchanged full-precision correlation scores,
// then keep one differential/descrambler context. Eight consecutive
// preamble ones freeze its phase through SFD; an early zero releases it.
// This gives the later serial detector one symbol period per decision.
wire candidate_hold = lane_one_run[0] >= 8 || lane_sfd_budget[0] != 0;
reg serial_fault_seen;
integer reset_index;
reg [5:0] plcp_bit_index;
reg [7:0] plcp_signal;
reg [7:0] plcp_service;
reg [15:0] plcp_length_us;
reg [15:0] plcp_crc_state;
reg [15:0] plcp_crc_received;
reg [2:0] psdu_bit_index;
reg [7:0] psdu_byte_accumulator;
reg [PSDU_LENGTH_WIDTH-1:0] psdu_bytes_expected;
reg [PSDU_LENGTH_WIDTH-1:0] psdu_bytes_emitted;
function automatic template_negative;
input integer sample_index;
begin
case (sample_index)
2, 3, 8, 9, 15, 16, 17, 18, 19:
template_negative = 1'b1;
default:
template_negative = 1'b0;
endcase
end
endfunction
function automatic [15:0] crc16_plcp_bit;
input [15:0] crc_in;
input data_bit;
reg mix;
begin
mix = crc_in[0] ^ data_bit;
crc16_plcp_bit = crc_in >> 1;
if (mix)
crc16_plcp_bit = crc16_plcp_bit ^ 16'h8408;
end
endfunction
// The delay line is newest at index 0 and oldest at index 19. Correlate
// the oldest-to-newest window against floor(sample*11/20) Barker chips.
// Keep the sum mathematically identical to the former loop, but express it
// as a balanced tree. The loop synthesized as a 20-adder serial chain;
// five balanced logic levels are substantially easier to place and route.
wire signed [CORRELATION_WIDTH-1:0] correlation_i_tap [0:19];
wire signed [CORRELATION_WIDTH-1:0] correlation_q_tap [0:19];
wire signed [CORRELATION_WIDTH-1:0] correlation_i_pair [0:9];
wire signed [CORRELATION_WIDTH-1:0] correlation_q_pair [0:9];
wire signed [CORRELATION_WIDTH-1:0] correlation_i_quad [0:4];
wire signed [CORRELATION_WIDTH-1:0] correlation_q_quad [0:4];
genvar correlation_index;
generate
for (correlation_index = 0; correlation_index < SYMBOL_SAMPLES;
correlation_index = correlation_index + 1) begin : g_corr_taps
wire signed [CORRELATION_WIDTH-1:0] i_extended =
{{(CORRELATION_WIDTH-16){i_delay[19-correlation_index][15]}},
i_delay[19-correlation_index]};
wire signed [CORRELATION_WIDTH-1:0] q_extended =
{{(CORRELATION_WIDTH-16){q_delay[19-correlation_index][15]}},
q_delay[19-correlation_index]};
assign correlation_i_tap[correlation_index] =
template_negative(correlation_index) ? -i_extended : i_extended;
assign correlation_q_tap[correlation_index] =
template_negative(correlation_index) ? -q_extended : q_extended;
end
for (correlation_index = 0; correlation_index < 10;
correlation_index = correlation_index + 1) begin : g_corr_pairs
assign correlation_i_pair[correlation_index] =
correlation_i_tap[2*correlation_index] +
correlation_i_tap[2*correlation_index+1];
assign correlation_q_pair[correlation_index] =
correlation_q_tap[2*correlation_index] +
correlation_q_tap[2*correlation_index+1];
end
for (correlation_index = 0; correlation_index < 5;
correlation_index = correlation_index + 1) begin : g_corr_quads
assign correlation_i_quad[correlation_index] =
correlation_i_pair[2*correlation_index] +
correlation_i_pair[2*correlation_index+1];
assign correlation_q_quad[correlation_index] =
correlation_q_pair[2*correlation_index] +
correlation_q_pair[2*correlation_index+1];
end
endgenerate
wire signed [CORRELATION_WIDTH-1:0] correlation_i_octet_0 =
correlation_i_quad[0] + correlation_i_quad[1];
wire signed [CORRELATION_WIDTH-1:0] correlation_q_octet_0 =
correlation_q_quad[0] + correlation_q_quad[1];
wire signed [CORRELATION_WIDTH-1:0] correlation_i_octet_1 =
correlation_i_quad[2] + correlation_i_quad[3];
wire signed [CORRELATION_WIDTH-1:0] correlation_q_octet_1 =
correlation_q_quad[2] + correlation_q_quad[3];
wire signed [CORRELATION_WIDTH-1:0] correlation_i_sixteen =
correlation_i_octet_0 + correlation_i_octet_1;
wire signed [CORRELATION_WIDTH-1:0] correlation_q_sixteen =
correlation_q_octet_0 + correlation_q_octet_1;
wire signed [CORRELATION_WIDTH-1:0] legacy_correlation_i =
correlation_i_sixteen + correlation_i_quad[4];
wire signed [CORRELATION_WIDTH-1:0] legacy_correlation_q =
correlation_q_sixteen + correlation_q_quad[4];
wire signed [CORRELATION_WIDTH-1:0] correlation_i,correlation_q;
wire process_sample_valid;
generate if(EXTERNAL_CORRELATOR) begin : g_external_correlator
reg signed [23:0] aligned_i,aligned_q;
assign process_sample_valid=external_correlation_valid;
always @(posedge clk)begin
if(!resetn || !enable)begin aligned_i<=0;aligned_q<=0;end
else if(process_sample_valid)begin
aligned_i<=external_correlation_i;aligned_q<=external_correlation_q;
end
end
// Preserve the detector's previous-window/operand pipeline contract.
assign correlation_i=aligned_i;assign correlation_q=aligned_q;
end else if(RECURSIVE_CORRELATOR) begin : g_recursive_correlator
wire q_valid_unused;
wire signed [23:0] next_i,next_q;
reg signed [23:0] aligned_i,aligned_q;
gf_dsss_barker_recurrence i_path(
.clk(clk),.clear(!resetn || !enable),.sample_valid(rx_sample_valid),
.sample(rx_i),.correlation(next_i),.result_valid(process_sample_valid)
);
gf_dsss_barker_recurrence q_path(
.clk(clk),.clear(!resetn || !enable),.sample_valid(rx_sample_valid),
.sample(rx_q),.correlation(next_q),.result_valid(q_valid_unused)
);
// Match the original history update edge. On process_sample_valid the
// detector must still see the PREVIOUS window; merely forwarding the
// completed recurrence makes its pipelined operand one sample newer.
always @(posedge clk)begin
if(!resetn || !enable)begin aligned_i<=0;aligned_q<=0;end
else if(process_sample_valid)begin aligned_i<=next_i;aligned_q<=next_q;end
end
assign correlation_i=aligned_i;
assign correlation_q=aligned_q;
end else begin : g_direct_correlator
assign process_sample_valid=rx_sample_valid;
assign correlation_i=legacy_correlation_i;
assign correlation_q=legacy_correlation_q;
end endgenerate
wire signed [CORRELATION_WIDTH-1:0] decision_correlation_i;
wire signed [CORRELATION_WIDTH-1:0] decision_correlation_q;
wire signed [DIFFERENTIAL_INPUT_WIDTH-1:0] differential_input_i =
$signed(decision_correlation_i) >>> DIFFERENTIAL_SCALE_BITS;
wire signed [DIFFERENTIAL_INPUT_WIDTH-1:0] differential_input_q =
$signed(decision_correlation_q) >>> DIFFERENTIAL_SCALE_BITS;
wire signed [DIFFERENTIAL_INPUT_WIDTH-1:0] differential_previous_i =
$signed(previous_i[decode_phase_index]) >>> DIFFERENTIAL_SCALE_BITS;
wire signed [DIFFERENTIAL_INPUT_WIDTH-1:0] differential_previous_q =
$signed(previous_q[decode_phase_index]) >>> DIFFERENTIAL_SCALE_BITS;
wire signed [(2*DIFFERENTIAL_INPUT_WIDTH)-1:0] differential_i;
wire signed [(2*DIFFERENTIAL_INPUT_WIDTH)-1:0] differential_q;
wire candidate_reselect = process_sample_valid && SINGLE_PHASE_RX &&
phase_index == SYMBOL_SAMPLES-1 && receive_state == SEARCH_SFD &&
!candidate_hold && candidate_phase != timing_best_phase;
wire selected_sample = process_sample_valid && window_fill >= SYMBOL_SAMPLES &&
(!SINGLE_PHASE_RX || phase_index == candidate_phase) && !candidate_reselect;
wire serial_result_valid,serial_result_sign,serial_overflow;
wire [4:0] serial_result_phase;
wire [15:0] serial_result_age;
wire [7:0] serial_request_age = current_sample_interval + 1'b1;
wire decode_bit_valid = SERIAL_DIFFERENTIAL
? serial_result_valid && !candidate_reselect && !serial_overflow
: selected_sample && phase_has_previous[decode_phase_index];
wire [4:0] decoded_bit_phase = SERIAL_DIFFERENTIAL ? serial_result_phase : phase_index;
wire [DECISION_AGE_WIDTH-1:0] decoded_end_age = SERIAL_DIFFERENTIAL
? serial_result_age + (EXTERNAL_CORRELATOR ? 1+EXTERNAL_CORRELATOR_LATENCY : RECURSIVE_CORRELATOR ? 13 : 1)
: current_sample_interval + 1'b1 + (EXTERNAL_CORRELATOR ? EXTERNAL_CORRELATOR_LATENCY : 0);
generate
if (SERIAL_DIFFERENTIAL) begin : g_serial_differential
reg signed [23:0] correlation_i_pipe,correlation_q_pipe;
reg signed [17:0] ai_pipe,bi_pipe,aq_pipe,bq_pipe;
wire ready_unused;
always @(posedge clk) begin
if (!resetn || !enable) begin
correlation_i_pipe<=0;correlation_q_pipe<=0;
ai_pipe<=0;bi_pipe<=0;aq_pipe<=0;bq_pipe<=0;
end else begin
correlation_i_pipe<=correlation_i;
correlation_q_pipe<=correlation_q;
// Exactly the operands consumed by the previous cycle's
// pipelined parallel multiplier, not a newer IQ window.
ai_pipe<=differential_input_i;bi_pipe<=differential_previous_i;
aq_pipe<=differential_input_q;bq_pipe<=differential_previous_q;
end
end
assign decision_correlation_i=correlation_i_pipe;
assign decision_correlation_q=correlation_q_pipe;
assign differential_i=0;
assign differential_q=0;
gf_serial_differential detector(
.clk(clk),.resetn(resetn),.enable(enable),.flush(candidate_reselect),
.request_valid(selected_sample && phase_has_previous[0] && !serial_overflow),
.ai(ai_pipe),.bi(bi_pipe),.aq(aq_pipe),.bq(bq_pipe),
.request_phase(phase_index),.request_age(serial_request_age),
.request_ready(ready_unused),.result_valid(serial_result_valid),
.result_sign(serial_result_sign),.result_phase(serial_result_phase),
.result_age(serial_result_age),.overflow(serial_overflow)
);
end else if (PIPELINED_DIFFERENTIAL != 0) begin : g_pipelined_differential
reg signed [CORRELATION_WIDTH-1:0] correlation_i_pipe;
reg signed [CORRELATION_WIDTH-1:0] correlation_q_pipe;
reg signed [(2*DIFFERENTIAL_INPUT_WIDTH)-1:0]
differential_i_pipe;
reg signed [(2*DIFFERENTIAL_INPUT_WIDTH)-1:0]
differential_q_pipe;
always @(posedge clk) begin
if (!resetn || !enable) begin
correlation_i_pipe <= {CORRELATION_WIDTH{1'b0}};
correlation_q_pipe <= {CORRELATION_WIDTH{1'b0}};
differential_i_pipe <=
{(2*DIFFERENTIAL_INPUT_WIDTH){1'b0}};
differential_q_pipe <=
{(2*DIFFERENTIAL_INPUT_WIDTH){1'b0}};
end else begin
correlation_i_pipe <= correlation_i;
correlation_q_pipe <= correlation_q;
differential_i_pipe <= $signed(differential_input_i) *
$signed(differential_previous_i);
differential_q_pipe <= $signed(differential_input_q) *
$signed(differential_previous_q);
end
end
assign decision_correlation_i = correlation_i_pipe;
assign decision_correlation_q = correlation_q_pipe;
assign differential_i = differential_i_pipe;
assign differential_q = differential_q_pipe;
end else begin : g_direct_differential
assign decision_correlation_i = correlation_i;
assign decision_correlation_q = correlation_q;
assign differential_i = $signed(differential_input_i) *
$signed(differential_previous_i);
assign differential_q = $signed(differential_input_q) *
$signed(differential_previous_q);
end
endgenerate
generate if (!SERIAL_DIFFERENTIAL) begin : g_no_serial_metadata
assign serial_result_valid=1'b0;
assign serial_result_sign=1'b0;
assign serial_result_phase=5'd0;
assign serial_result_age=16'd0;
assign serial_overflow=1'b0;
end endgenerate
wire signed [(2*DIFFERENTIAL_INPUT_WIDTH):0] differential_real =
$signed({differential_i[(2*DIFFERENTIAL_INPUT_WIDTH)-1],
differential_i}) +
$signed({differential_q[(2*DIFFERENTIAL_INPUT_WIDTH)-1],
differential_q});
wire scrambled_bit = SERIAL_DIFFERENTIAL ? serial_result_sign :
differential_real[2*DIFFERENTIAL_INPUT_WIDTH];
wire plain_bit = scrambled_bit ^ lane_scrambler[decode_phase_index][3] ^
lane_scrambler[decode_phase_index][6];
// Long-preamble bits can be decoded at several fractional symbol phases,
// but only the phase with the complete Barker window identifies the real
// PPDU boundary needed for SIFS. Accumulate a short leaky magnitude score
// independently for all 20 phases and accept SFD only on the strongest.
// This uses no additional multipliers and naturally follows slow gain
// changes while forgetting idle/noise history.
wire [CORRELATION_WIDTH-1:0] correlation_i_magnitude =
decision_correlation_i[CORRELATION_WIDTH-1]
? (~decision_correlation_i + 1'b1) : decision_correlation_i;
wire [CORRELATION_WIDTH-1:0] correlation_q_magnitude =
decision_correlation_q[CORRELATION_WIDTH-1]
? (~decision_correlation_q + 1'b1) : decision_correlation_q;
wire [CORRELATION_WIDTH:0] correlation_magnitude =
{1'b0, correlation_i_magnitude} +
{1'b0, correlation_q_magnitude};
wire [TIMING_SCORE_WIDTH-1:0] current_timing_score;
wire [TIMING_SCORE_WIDTH-1:0] extended_correlation_magnitude =
{{(TIMING_SCORE_WIDTH-(CORRELATION_WIDTH+1)){1'b0}},
correlation_magnitude};
wire [TIMING_SCORE_WIDTH-1:0] next_timing_score =
current_timing_score - (current_timing_score >> 4) +
extended_correlation_magnitude;
// Reset validity, not RAM contents. Every visible score is still cleared
// on exactly the original clock, including frame-end and fault/disable.
// Async read preserves the existing sample-side read/modify/write timing.
wire score_frame_end = decode_bit_valid && receive_state == READ_PSDU &&
decoded_bit_phase == locked_phase && psdu_bit_index == 3'd7 &&
psdu_bytes_emitted + 1'b1 >= psdu_bytes_expected;
generate if (TIMING_SCORE_RAM) begin : g_score_ram
gf_dsss_timing_score_ram #(.WIDTH(TIMING_SCORE_WIDTH)) scores (
.clk(clk),.clear(!resetn || !enable || serial_overflow || score_frame_end),
.write_enable(process_sample_valid && window_fill >= SYMBOL_SAMPLES &&
receive_state == SEARCH_SFD),
.address(phase_index),.write_data(next_timing_score),
.read_data(current_timing_score)
);
end else begin : g_score_registers
assign current_timing_score = timing_score[phase_index];
end endgenerate
wire begin_sfd_search = !plain_bit &&
lane_one_run[decode_phase_index] >= 8'd87;
wire sfd_search_active = begin_sfd_search ||
lane_sfd_budget[decode_phase_index] != 0;
wire [15:0] shifted_sfd = begin_sfd_search
? {15'd0, plain_bit}
: {lane_sfd_shift[decode_phase_index][14:0], plain_bit};
wire sfd_match = sfd_search_active && shifted_sfd == 16'h05cf;
wire [15:0] plcp_crc_next =
crc16_plcp_bit(plcp_crc_state, plain_bit);
wire [15:0] completed_plcp_crc =
{plain_bit, plcp_crc_received[14:0]};
// HR/DSSS SERVICE bit 2 indicates a shared transmit carrier/chip clock.
// It is independent of the 1 Mb/s payload format. A real ESP8266 capture
// has SERVICE=04 with a valid PLCP CRC; requiring 00 discarded that frame.
// Keep unsupported modulation/length-extension/reserved bits rejected.
wire plcp_fields_valid = plcp_signal == 8'h0a &&
(plcp_service & 8'hfb) == 8'h00 && plcp_length_us != 0 &&
plcp_length_us[2:0] == 3'b000 &&
(plcp_length_us >> 3) <= MAX_PSDU_BYTES;
wire [7:0] completed_psdu_byte =
{plain_bit, psdu_byte_accumulator[6:0]};
wire [DECISION_AGE_WIDTH-1:0] current_sample_interval =
clocks_since_sample + 1'b1;
assign receiver_active = receive_state != SEARCH_SFD;
assign psdu_bit_valid = enable && decode_bit_valid &&
receive_state == READ_PSDU && decoded_bit_phase == locked_phase;
assign psdu_bit_value = plain_bit;
assign psdu_crc_restart = decode_bit_valid && receive_state == READ_PLCP &&
decoded_bit_phase == locked_phase && plcp_bit_index == 47 &&
plcp_fields_valid && completed_plcp_crc == (plcp_crc_state ^ 16'hffff);
initial begin
if(RECURSIVE_CORRELATOR && !SERIAL_DIFFERENTIAL)
$error("Recursive correlator requires the qualified serial 40-MHz receiver");
if (SERIAL_DIFFERENTIAL && (!SINGLE_PHASE_RX || !PIPELINED_DIFFERENTIAL || DECISION_AGE_WIDTH < 16))
$error("Serial differential requires single-phase pipelined RX and 16-bit age");
if (MAX_PSDU_BYTES < 1 || MAX_PSDU_BYTES > 4095)
$error("MAX_PSDU_BYTES must fit the long-PLCP LENGTH field");
if (DECISION_AGE_WIDTH < 2)
$error("DECISION_AGE_WIDTH is too small");
end
always @(posedge clk) begin
if (!resetn) begin
serial_fault_seen <= 1'b0;
receive_state <= SEARCH_SFD;
phase_index <= EXTERNAL_CORRELATOR ? 5'd19 : {PHASE_WIDTH{1'b0}};
locked_phase <= {PHASE_WIDTH{1'b0}};
candidate_phase <= {PHASE_WIDTH{1'b0}};
window_fill <= EXTERNAL_CORRELATOR ? 6'd19 : 6'd0;
clocks_since_sample <= {DECISION_AGE_WIDTH{1'b0}};
plcp_bit_index <= 6'd0;
plcp_signal <= 8'd0;
plcp_service <= 8'd0;
plcp_length_us <= 16'd0;
plcp_crc_state <= 16'hffff;
plcp_crc_received <= 16'd0;
psdu_bit_index <= 3'd0;
psdu_byte_accumulator <= 8'd0;
psdu_bytes_expected <= {PSDU_LENGTH_WIDTH{1'b0}};
psdu_bytes_emitted <= {PSDU_LENGTH_WIDTH{1'b0}};
psdu_start <= 1'b0;
psdu_byte_valid <= 1'b0;
psdu_byte <= 8'd0;
psdu_byte_last <= 1'b0;
psdu_end_age_cycles <= {DECISION_AGE_WIDTH{1'b0}};
sfd_count <= {COUNT_WIDTH{1'b0}};
plcp_ok_count <= {COUNT_WIDTH{1'b0}};
plcp_error_count <= {COUNT_WIDTH{1'b0}};
psdu_count <= {COUNT_WIDTH{1'b0}};
timing_best_score <= {TIMING_SCORE_WIDTH{1'b0}};
timing_best_phase <= {PHASE_WIDTH{1'b0}};
for (reset_index = 0; reset_index < SYMBOL_SAMPLES;
reset_index = reset_index + 1) begin
if(!RECURSIVE_CORRELATOR) begin
i_delay[reset_index] <= 16'sd0;
q_delay[reset_index] <= 16'sd0;
end
previous_i[reset_index] <=
{CORRELATION_WIDTH{1'b0}};
previous_q[reset_index] <=
{CORRELATION_WIDTH{1'b0}};
phase_has_previous[reset_index] <= 1'b0;
lane_scrambler[reset_index] <= 7'd0;
lane_one_run[reset_index] <= 8'd0;
lane_sfd_budget[reset_index] <= 6'd0;
lane_sfd_shift[reset_index] <= 16'd0;
if (!TIMING_SCORE_RAM) timing_score[reset_index] <=
{TIMING_SCORE_WIDTH{1'b0}};
end
end else begin
psdu_start <= 1'b0;
psdu_byte_valid <= 1'b0;
psdu_byte_last <= 1'b0;
if (!enable || serial_overflow) begin
if (!enable) serial_fault_seen <= 1'b0;
else if (!serial_fault_seen) begin
serial_fault_seen <= 1'b1;
plcp_error_count <= plcp_error_count + 1'b1;
end
receive_state <= SEARCH_SFD;
candidate_phase <= {PHASE_WIDTH{1'b0}};
phase_index <= EXTERNAL_CORRELATOR ? 5'd19 : {PHASE_WIDTH{1'b0}};
window_fill <= EXTERNAL_CORRELATOR ? 6'd19 : 6'd0;
clocks_since_sample <= {DECISION_AGE_WIDTH{1'b0}};
timing_best_score <= {TIMING_SCORE_WIDTH{1'b0}};
timing_best_phase <= {PHASE_WIDTH{1'b0}};
for (reset_index = 0; reset_index < SYMBOL_SAMPLES;
reset_index = reset_index + 1) begin
phase_has_previous[reset_index] <= 1'b0;
lane_scrambler[reset_index] <= 7'd0;
lane_one_run[reset_index] <= 8'd0;
lane_sfd_budget[reset_index] <= 6'd0;
lane_sfd_shift[reset_index] <= 16'd0;
if (!TIMING_SCORE_RAM) timing_score[reset_index] <=
{TIMING_SCORE_WIDTH{1'b0}};
end
end else begin
if (process_sample_valid)
clocks_since_sample <= {DECISION_AGE_WIDTH{1'b0}};
else if (!( &clocks_since_sample))
clocks_since_sample <= clocks_since_sample + 1'b1;
if (process_sample_valid) begin
if(!RECURSIVE_CORRELATOR) begin
for (reset_index = SYMBOL_SAMPLES - 1; reset_index > 0;
reset_index = reset_index - 1) begin
i_delay[reset_index] <= i_delay[reset_index - 1];
q_delay[reset_index] <= q_delay[reset_index - 1];
end
i_delay[0] <= rx_i;
q_delay[0] <= rx_q;
end
if (window_fill < SYMBOL_SAMPLES)
window_fill <= window_fill + 1'b1;
if (phase_index == SYMBOL_SAMPLES - 1)
phase_index <= {PHASE_WIDTH{1'b0}};
else
phase_index <= phase_index + 1'b1;
if (window_fill >= SYMBOL_SAMPLES && receive_state == SEARCH_SFD) begin
if (!TIMING_SCORE_RAM) timing_score[phase_index] <= next_timing_score;
if (phase_index == timing_best_phase)
timing_best_score <= next_timing_score;
if (next_timing_score > timing_best_score) begin
timing_best_score <= next_timing_score;
timing_best_phase <= phase_index;
end
end
// The sample-side history does not wait for arithmetic.
// A new phase cancels all pending old-phase decisions.
if (candidate_reselect) begin
candidate_phase <= timing_best_phase;
phase_has_previous[0] <= 1'b0;
lane_scrambler[0] <= 7'd0;
lane_one_run[0] <= 8'd0;
lane_sfd_budget[0] <= 6'd0;
lane_sfd_shift[0] <= 16'd0;
end else if (selected_sample) begin
previous_i[decode_phase_index] <= decision_correlation_i;
previous_q[decode_phase_index] <= decision_correlation_q;
phase_has_previous[decode_phase_index] <= 1'b1;
end
end
if (decode_bit_valid) begin
lane_scrambler[decode_phase_index] <=
{lane_scrambler[decode_phase_index][5:0],
scrambled_bit};
if (receive_state == SEARCH_SFD) begin
if (plain_bit) begin
if (!( &lane_one_run[decode_phase_index]))
lane_one_run[decode_phase_index] <=
lane_one_run[decode_phase_index] + 1'b1;
end else begin
lane_one_run[decode_phase_index] <= 8'd0;
end
if (begin_sfd_search) begin
lane_sfd_shift[decode_phase_index] <=
{15'd0, plain_bit};
lane_sfd_budget[decode_phase_index] <= 6'd31;
end else if (
lane_sfd_budget[decode_phase_index] != 0) begin
lane_sfd_shift[decode_phase_index] <= shifted_sfd;
lane_sfd_budget[decode_phase_index] <=
lane_sfd_budget[decode_phase_index] - 1'b1;
end
if (sfd_match &&
(SINGLE_PHASE_RX || decoded_bit_phase == timing_best_phase)) begin
receive_state <= READ_PLCP;
locked_phase <= decoded_bit_phase;
plcp_bit_index <= 6'd0;
plcp_signal <= 8'd0;
plcp_service <= 8'd0;
plcp_length_us <= 16'd0;
plcp_crc_state <= 16'hffff;
plcp_crc_received <= 16'd0;
sfd_count <= sfd_count + 1'b1;
for (reset_index = 0;
reset_index < SYMBOL_SAMPLES;
reset_index = reset_index + 1) begin
lane_one_run[reset_index] <= 8'd0;
lane_sfd_budget[reset_index] <= 6'd0;
lane_sfd_shift[reset_index] <= 16'd0;
end
end
end else if (decoded_bit_phase == locked_phase) begin
if (receive_state == READ_PLCP) begin
if (plcp_bit_index < 8)
plcp_signal[plcp_bit_index] <= plain_bit;
else if (plcp_bit_index < 16)
plcp_service[plcp_bit_index - 8] <=
plain_bit;
else if (plcp_bit_index < 32)
plcp_length_us[plcp_bit_index - 16] <=
plain_bit;
if (plcp_bit_index < 32)
plcp_crc_state <= plcp_crc_next;
else
plcp_crc_received[
plcp_bit_index - 32] <= plain_bit;
if (plcp_bit_index == 47) begin
if (plcp_fields_valid &&
completed_plcp_crc ==
(plcp_crc_state ^ 16'hffff)) begin
receive_state <= READ_PSDU;
psdu_bit_index <= 3'd0;
psdu_byte_accumulator <= 8'd0;
psdu_bytes_expected <=
plcp_length_us >> 3;
psdu_bytes_emitted <=
{PSDU_LENGTH_WIDTH{1'b0}};
plcp_ok_count <=
plcp_ok_count + 1'b1;
end else begin
receive_state <= SEARCH_SFD;
plcp_error_count <=
plcp_error_count + 1'b1;
end
plcp_bit_index <= 6'd0;
end else begin
plcp_bit_index <=
plcp_bit_index + 1'b1;
end
end else begin
psdu_byte_accumulator[psdu_bit_index] <=
plain_bit;
if (psdu_bit_index == 3'd7) begin
psdu_byte <= completed_psdu_byte;
psdu_byte_valid <= 1'b1;
psdu_start <=
psdu_bytes_emitted == 0;
psdu_byte_last <=
psdu_bytes_emitted + 1'b1 >=
psdu_bytes_expected;
if (psdu_bytes_emitted + 1'b1 >=
psdu_bytes_expected) begin
psdu_end_age_cycles <=
decoded_end_age;
receive_state <= SEARCH_SFD;
psdu_count <= psdu_count + 1'b1;
for (reset_index = 0;
reset_index < SYMBOL_SAMPLES;
reset_index = reset_index + 1) begin
lane_one_run[reset_index] <= 8'd0;
lane_sfd_budget[reset_index] <=
6'd0;
lane_sfd_shift[reset_index] <=
16'd0;
if (!TIMING_SCORE_RAM) timing_score[reset_index] <=
{TIMING_SCORE_WIDTH{1'b0}};
end
timing_best_score <=
{TIMING_SCORE_WIDTH{1'b0}};
timing_best_phase <=
{PHASE_WIDTH{1'b0}};
end else begin
psdu_bytes_emitted <=
psdu_bytes_emitted + 1'b1;
end
psdu_bit_index <= 3'd0;
psdu_byte_accumulator <= 8'd0;
end else begin
psdu_bit_index <=
psdu_bit_index + 1'b1;
end
end
end
end
end
end
end
endmodule
// Exact existing 20-sample Barker FIR; no resampling or equalization.
// C[n]=C[n-1]-x[n]-x[n-20]+2*(x[n-5]-x[n-10]+x[n-12]-x[n-16]+x[n-18]).
// Sparse history taps and all sign-extension branches are registered explicitly.
// Exact ranges: first differences/sum 17 bits; a=2*(p0+p1) 19 bits;
// b=2*x18-p3 18 bits; delta 20 bits. EVERY signed IQ16 input is represented;
// no information is discarded. The recurrence and output remain full 24-bit.
// Twelve clocks of latency; supports consecutive inputs. At one input per two
// radio clocks, serial 24-bit addition would require another clock domain.
module gf_dsss_barker_recurrence(
input wire clk,clear,sample_valid,
input wire signed [15:0] sample,
output wire signed [23:0] correlation,
output wire result_valid
);
wire [15:0] history[0:19],taps[0:6];
wire [16:0] extended[0:6];
reg [11:0] valid_pipe;
always @(posedge clk) begin
if(clear) valid_pipe<=0;
else valid_pipe<={valid_pipe[10:0],sample_valid};
end
generate for(genvar n=0;n<20;n=n+1)begin:g_history
wire [15:0] previous;
if(n==0) assign previous=sample;
else assign previous=history[n-1];
gf_barker_word_reg #(.WIDTH(16)) r(.clk(clk),.clear(clear),
.enable(sample_valid),.d(previous),.q(history[n]));
end
for(genvar n=0;n<7;n=n+1)begin:g_tap
// History successor and tap are separate loads (maximum two).
localparam integer OFFSET=n==0?0:n==1?5:n==2?10:n==3?12:n==4?16:n==5?18:20;
wire [15:0] previous;
if(OFFSET==0) assign previous=sample;
else assign previous=history[OFFSET-1];
gf_barker_word_reg #(.WIDTH(16)) tap(.clk(clk),.clear(clear),
.enable(sample_valid),.d(previous),.q(taps[n]));
gf_barker_sign_extend #(.INPUT_WIDTH(16),.OUTPUT_WIDTH(17),.DEPTH(1)) extend(
.clk(clk),.clear(clear),.data_in(taps[n]),.data_out(extended[n]));
end endgenerate
wire signed [16:0] p0,p1,p2,p3;
wire [16:0] p0_next=$signed(extended[1])-$signed(extended[2]);
wire [16:0] p1_next=$signed(extended[3])-$signed(extended[4]);
wire [16:0] p2_next={extended[5][15:0],1'b0}; // exact signed IQ16 times two
wire [16:0] p3_next=$signed(extended[0])+$signed(extended[6]);
gf_barker_word_reg #(.WIDTH(17)) p0_r(.clk(clk),.clear(clear),.enable(valid_pipe[1]),.d(p0_next),.q(p0));
gf_barker_word_reg #(.WIDTH(17)) p1_r(.clk(clk),.clear(clear),.enable(valid_pipe[1]),.d(p1_next),.q(p1));
gf_barker_word_reg #(.WIDTH(17)) p2_r(.clk(clk),.clear(clear),.enable(valid_pipe[1]),.d(p2_next),.q(p2));
gf_barker_word_reg #(.WIDTH(17)) p3_r(.clk(clk),.clear(clear),.enable(valid_pipe[1]),.d(p3_next),.q(p3));
wire signed [17:0] e0,e1,e2,e3;
gf_barker_sign_extend #(.INPUT_WIDTH(17),.OUTPUT_WIDTH(18),.DEPTH(1)) e0_r(.clk(clk),.clear(clear),.data_in(p0),.data_out(e0));
gf_barker_sign_extend #(.INPUT_WIDTH(17),.OUTPUT_WIDTH(18),.DEPTH(1)) e1_r(.clk(clk),.clear(clear),.data_in(p1),.data_out(e1));
gf_barker_sign_extend #(.INPUT_WIDTH(17),.OUTPUT_WIDTH(18),.DEPTH(1)) e2_r(.clk(clk),.clear(clear),.data_in(p2),.data_out(e2));
gf_barker_sign_extend #(.INPUT_WIDTH(17),.OUTPUT_WIDTH(18),.DEPTH(1)) e3_r(.clk(clk),.clear(clear),.data_in(p3),.data_out(e3));
wire [17:0] a_sum=e0+e1;
wire signed [18:0] a;
wire signed [17:0] b;
gf_barker_word_reg #(.WIDTH(19)) a_r(.clk(clk),.clear(clear),.enable(valid_pipe[3]),.d({a_sum,1'b0}),.q(a));
gf_barker_word_reg #(.WIDTH(18)) b_r(.clk(clk),.clear(clear),.enable(valid_pipe[3]),.d(e2-e3),.q(b));
wire signed [19:0] ae,be,delta;
gf_barker_sign_extend #(.INPUT_WIDTH(19),.OUTPUT_WIDTH(20),.DEPTH(2)) ae_r(.clk(clk),.clear(clear),.data_in(a),.data_out(ae));
gf_barker_sign_extend #(.INPUT_WIDTH(18),.OUTPUT_WIDTH(20),.DEPTH(2)) be_r(.clk(clk),.clear(clear),.data_in(b),.data_out(be));
gf_barker_word_reg #(.WIDTH(20)) delta_r(.clk(clk),.clear(clear),.enable(valid_pipe[6]),.d(ae+be),.q(delta));
wire signed [23:0] delta24,feedback;
gf_barker_sign_extend #(.INPUT_WIDTH(20),.OUTPUT_WIDTH(24),.DEPTH(3)) delta_extend(
.clk(clk),.clear(clear),.data_in(delta),.data_out(delta24));
wire [23:0] next_correlation=feedback+delta24;
// Two loads on each adder output. Feedback Q drives only the adder;
// the separate output register drives only the downstream graph.
gf_barker_word_reg #(.WIDTH(24)) feedback_r(.clk(clk),.clear(clear),
.enable(valid_pipe[10]),.d(next_correlation),.q(feedback));
gf_barker_word_reg #(.WIDTH(24)) output_r(.clk(clk),.clear(clear),
.enable(valid_pipe[10]),.d(next_correlation),.q(correlation));
assign result_valid=valid_pipe[11];
endmodule
// Protected clock-enabled registers: no shared SRL reset-mask data net and no
// synthesis merging of deliberate history/sign/feedback duplication.
module gf_barker_word_reg #(parameter integer WIDTH=1)(
input wire clk,clear,enable,
input wire [WIDTH-1:0] d,
output wire [WIDTH-1:0] q
);
generate for(genvar bit_index=0;bit_index<WIDTH;bit_index=bit_index+1)begin:g_bit
`ifdef SYNTHESIS
(* DONT_TOUCH="true" *) FDRE #(.INIT(1'b0)) r(
.C(clk),.R(clear),.CE(enable),.D(d[bit_index]),.Q(q[bit_index]));
`else
reg value=0;
always @(posedge clk) if(clear)value<=0;else if(enable)value<=d[bit_index];
assign q[bit_index]=value;
`endif
end endgenerate
endmodule
// Exact signed extension, not precision truncation. Each sign-tree node drives
// at most two registered children; payload bits receive the same delay.
module gf_barker_sign_extend #(
parameter integer INPUT_WIDTH=16, OUTPUT_WIDTH=17, DEPTH=1
)(
input wire clk,clear,
input wire [INPUT_WIDTH-1:0] data_in,
output wire [OUTPUT_WIDTH-1:0] data_out
);
localparam integer LEAVES=1<<DEPTH;
localparam integer SIGN_BITS=OUTPUT_WIDTH-INPUT_WIDTH+1;
initial if(DEPTH<1 || SIGN_BITS>LEAVES || OUTPUT_WIDTH<INPUT_WIDTH)
$error("Invalid registered signed extension");
wire [LEAVES-1:0] sign_tree[0:DEPTH];
wire [INPUT_WIDTH-2:0] payload[0:DEPTH];
assign sign_tree[0][0]=data_in[INPUT_WIDTH-1];
assign payload[0]=data_in[INPUT_WIDTH-2:0];
generate for(genvar level=1;level<=DEPTH;level=level+1)begin:g_level
gf_barker_word_reg #(.WIDTH(INPUT_WIDTH-1)) body(
.clk(clk),.clear(clear),.enable(1'b1),.d(payload[level-1]),.q(payload[level]));
for(genvar node=0;node<(1<<level);node=node+1)begin:g_branch
gf_barker_word_reg branch(.clk(clk),.clear(clear),.enable(1'b1),
.d(sign_tree[level-1][node>>1]),.q(sign_tree[level][node]));
end
end endgenerate
assign data_out={sign_tree[DEPTH][SIGN_BITS-1:0],payload[DEPTH]};
endmodule
// Full-width twenty-phase score bank. Clear has priority over a coincident
// write. Unwritten entries return zero; no stale RAM word can enter the score
// recurrence after a reset, disable, fault or completed frame.
module gf_dsss_timing_score_ram #(parameter integer WIDTH = 29) (
input wire clk,
input wire clear,
input wire write_enable,
input wire [4:0] address,
input wire [WIDTH-1:0] write_data,
output wire [WIDTH-1:0] read_data
);
(* ram_style = "distributed" *) reg [WIDTH-1:0] storage [0:31];
reg [19:0] written;
wire valid_address = address < 20;
always @(posedge clk) begin
if (write_enable && !clear && valid_address)
storage[address] <= write_data;
if (clear) written <= 20'd0;
else if (write_enable && valid_address) written[address] <= 1'b1;
end
assign read_data = valid_address && written[address] ? storage[address] : {WIDTH{1'b0}};
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_dsss_rx_sifs_ap.sv · 256 lines
Download this file · Permanent section link
// Complete hard-real-time 1 Mb/s receive-to-response path for a Pluto AP.
//
// RX1 IQ -> DSSS PHY -> decoded PSDU bytes -> FCS/RA/TA low-MAC ->
// air-relative SIFS scheduler -> locally generated ACK/CTS IQ for TX1.
// The decoded byte stream is also exposed unchanged for the host C++ stack.
`timescale 1ns/1ps
module gf_dsss_rx_sifs_ap #(
parameter integer CLOCK_HZ = 20_000_000,
parameter integer SIFS_US = 10,
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer IQ_WIDTH = 32,
parameter integer SINGLE_PHASE_RX = 0,
parameter integer SERIAL_DIFFERENTIAL = 0,
parameter integer TIMING_SCORE_RAM = 0,
parameter integer SERIAL_CONTROL_CRC = 0,
parameter integer SERIAL_RX_CRC = 0,
parameter integer RECURSIVE_CORRELATOR = 0,
parameter integer SERIAL_BARKER = 0
) (
input wire clk,
input wire resetn,
input wire arm,
input wire kill,
input wire [47:0] ap_mac,
input wire rx_sample_valid,
input wire signed [15:0] rx_i,
input wire signed [15:0] rx_q,
input wire tx_sample_tick,
input wire tx_sink_ready,
output wire tx_override_valid,
output wire [IQ_WIDTH-1:0] tx_override_iq,
output wire response_pending,
output wire response_active,
output wire response_start,
output wire response_is_cts,
output wire [47:0] response_mac,
output wire [15:0] response_duration_us,
output wire host_psdu_start,
output wire host_psdu_byte_valid,
output wire [7:0] host_psdu_byte,
output wire host_psdu_byte_last,
output wire [DECISION_AGE_WIDTH-1:0] host_psdu_end_age_cycles,
output wire [31:0] rx_sfd_count,
output wire [31:0] rx_plcp_ok_count,
output wire [31:0] rx_plcp_error_count,
output wire [31:0] rx_psdu_count,
output wire [31:0] classified_frame_count,
output wire [31:0] classified_fcs_ok_count,
output wire [31:0] response_candidate_count,
output wire [31:0] malformed_count,
output wire [31:0] response_count,
output wire [31:0] deadline_miss_count,
output wire [31:0] rejected_count,
output wire [31:0] stream_abort_count,
input wire fast_clk,
input wire serial_clock_locked,
output wire serial_barker_fault
);
initial if (SERIAL_DIFFERENTIAL && CLOCK_HZ != 40_000_000)
$error("Serial RX requires the qualified 40 MHz / 20 MS/s schedule");
initial if(SERIAL_BARKER && (!SERIAL_DIFFERENTIAL || RECURSIVE_CORRELATOR))
$error("Serial Barker requires serial differential and replaces the direct/recursive correlator");
wire phy_resetn,external_valid;
wire signed [23:0] external_i,external_q;
wire phy_kill=kill || (SERIAL_BARKER && (!serial_clock_locked || serial_barker_fault));
generate if(SERIAL_BARKER)begin:g_serial_barker
gf_barker_radio_bridge bridge(
.radio_clk(clk),.fast_clk(fast_clk),.resetn(resetn),.enable(arm && !kill),
.clock_locked(serial_clock_locked),.sample_valid(rx_sample_valid),.sample_i(rx_i),.sample_q(rx_q),
.receiver_resetn(phy_resetn),.result_valid(external_valid),.fault(serial_barker_fault),
.correlation_i(external_i),.correlation_q(external_q));
end else begin:g_legacy_barker
assign phy_resetn=resetn;assign external_valid=0;assign external_i=0;assign external_q=0;
assign serial_barker_fault=0;
end endgenerate
wire receiver_active_unused;
wire decision_valid_unused;
wire decision_fcs_ok_unused;
wire decision_ra_matches_ap_unused;
wire decision_response_required_unused;
wire decision_is_rts_unused;
wire [47:0] decision_response_mac_unused;
wire [15:0] decision_duration_us_unused;
wire decision_malformed_unused;
wire rx_bit_valid, rx_bit_value, rx_crc_restart;
wire mac_start, mac_valid, mac_last, checked_fcs_ok;
wire [7:0] mac_byte;
wire [DECISION_AGE_WIDTH-1:0] mac_age;
generate if (SERIAL_RX_CRC) begin : g_rx_crc
// Final decoded bit -> CRC update: 5 clocks. Residue reduction: 3
// registered levels. Delay only the classifier's final-byte event by
// ten clocks; the host's byte stream and its timestamp stay unchanged.
localparam integer FINAL_DELAY = 10;
wire clear = !resetn || !arm || phy_kill;
wire [31:0] fcs;
gf_control_crc_bitserial crc (
.clk(clk),.clear(clear || rx_crc_restart),
.bit_valid(rx_bit_valid),.data_bit(rx_bit_value),.fcs(fcs)
);
gf_rx_crc_residue residue (
.clk(clk),.clear(clear || rx_crc_restart),.fcs(fcs),.good(checked_fcs_ok)
);
reg [FINAL_DELAY-1:0] last_pipe;
reg last_start;
always @(posedge clk) begin
if (clear) begin
last_pipe <= 0;
last_start <= 0;
end else begin
last_pipe <= {last_pipe[FINAL_DELAY-2:0],
host_psdu_byte_valid && host_psdu_byte_last};
if (host_psdu_byte_valid && host_psdu_byte_last) begin
last_start <= host_psdu_start;
end
end
end
assign mac_last = last_pipe[FINAL_DELAY-1];
assign mac_valid = (host_psdu_byte_valid && !host_psdu_byte_last) || mac_last;
assign mac_start = (host_psdu_start && !host_psdu_byte_last) || (mac_last && last_start);
// The PHY keeps byte/age registered until the next decoded byte, at
// least 320 clocks later. Reuse those registers. Absorb the ten-clock
// decision offset in the scheduler constant, not another wide adder.
assign mac_byte = host_psdu_byte;
assign mac_age = host_psdu_end_age_cycles;
end else begin : g_byte_crc
assign mac_start = host_psdu_start;
assign mac_valid = host_psdu_byte_valid;
assign mac_byte = host_psdu_byte;
assign mac_last = host_psdu_byte_last;
assign mac_age = host_psdu_end_age_cycles;
assign checked_fcs_ok = 1'b0;
end endgenerate
gf_dsss_1mbps_rx #(
.DECISION_AGE_WIDTH(DECISION_AGE_WIDTH),
.PIPELINED_DIFFERENTIAL(CLOCK_HZ >= 40_000_000),
.SINGLE_PHASE_RX(SINGLE_PHASE_RX),
.SERIAL_DIFFERENTIAL(SERIAL_DIFFERENTIAL),
.TIMING_SCORE_RAM(TIMING_SCORE_RAM),
.RECURSIVE_CORRELATOR(RECURSIVE_CORRELATOR),
.EXTERNAL_CORRELATOR(SERIAL_BARKER),.EXTERNAL_CORRELATOR_LATENCY(7)
) receiver (
.clk(clk),
.resetn(phy_resetn),
.enable(arm && !phy_kill),
.rx_sample_valid(rx_sample_valid),
.rx_i(rx_i),
.rx_q(rx_q),
.psdu_start(host_psdu_start),
.psdu_byte_valid(host_psdu_byte_valid),
.psdu_byte(host_psdu_byte),
.psdu_byte_last(host_psdu_byte_last),
.psdu_end_age_cycles(host_psdu_end_age_cycles),
.receiver_active(receiver_active_unused),
.psdu_bit_valid(rx_bit_valid),
.psdu_bit_value(rx_bit_value),
.psdu_crc_restart(rx_crc_restart),
.sfd_count(rx_sfd_count),
.plcp_ok_count(rx_plcp_ok_count),
.plcp_error_count(rx_plcp_error_count),
.psdu_count(rx_psdu_count),
.external_correlation_valid(external_valid),.external_correlation_i(external_i),.external_correlation_q(external_q)
);
gf_dsss_sifs_low_mac #(
.CLOCK_HZ(CLOCK_HZ),
.SIFS_US(SIFS_US),
.DECISION_AGE_WIDTH(DECISION_AGE_WIDTH),
.IQ_WIDTH(IQ_WIDTH),
.SERIAL_CONTROL_CRC(SERIAL_CONTROL_CRC),
.EXTERNAL_FCS(SERIAL_RX_CRC),
.DECISION_AGE_OFFSET(SERIAL_RX_CRC ? 10 : 0)
) low_mac_response (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(phy_kill),
.ap_mac(ap_mac),
.psdu_start(mac_start),
.psdu_byte_valid(mac_valid),
.psdu_byte(mac_byte),
.psdu_byte_last(mac_last),
.psdu_end_age_cycles(mac_age),
.checked_fcs_ok(checked_fcs_ok),
.tx_sample_tick(tx_sample_tick),
.tx_sink_ready(tx_sink_ready),
.tx_override_valid(tx_override_valid),
.tx_override_iq(tx_override_iq),
.response_pending(response_pending),
.response_active(response_active),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_mac(response_mac),
.response_duration_us(response_duration_us),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count),
.stream_abort_count(stream_abort_count),
.decision_valid(decision_valid_unused),
.decision_fcs_ok(decision_fcs_ok_unused),
.decision_ra_matches_ap(decision_ra_matches_ap_unused),
.decision_response_required(decision_response_required_unused),
.decision_is_rts(decision_is_rts_unused),
.decision_response_mac(decision_response_mac_unused),
.decision_duration_us(decision_duration_us_unused),
.decision_malformed(decision_malformed_unused),
.classified_frame_count(classified_frame_count),
.classified_fcs_ok_count(classified_fcs_ok_count),
.response_candidate_count(response_candidate_count),
.malformed_count(malformed_count)
);
endmodule
// Complemented IEEE CRC-32 residue = ~32'hdebb20e3. Explicit registered
// LUT4 reduction, with a register at every level. Each CRC bit drives only
// its recurrence and one comparator input; no wide inferred comparator.
module gf_rx_crc_residue (
input wire clk, clear,
input wire [31:0] fcs,
output wire good
);
localparam [31:0] EXPECTED = 32'h2144df1c;
wire [7:0] groups;
wire [1:0] halves;
generate for (genvar node=0;node<8;node=node+1) begin : g_groups
wire matched;
gf_serial_lut #(.INIT(16'h0001 << EXPECTED[node*4 +: 4])) compare (
.a(fcs[node*4]),.b(fcs[node*4+1]),
.c(fcs[node*4+2]),.d(fcs[node*4+3]),.q(matched)
);
gf_serial_reg r(.clk(clk),.reset(clear),.d(matched),.q(groups[node]));
end
for (genvar node=0;node<2;node=node+1) begin : g_halves
wire matched;
gf_serial_lut #(.INIT(16'h8000)) combine (
.a(groups[node*4]),.b(groups[node*4+1]),
.c(groups[node*4+2]),.d(groups[node*4+3]),.q(matched)
);
gf_serial_reg r(.clk(clk),.reset(clear),.d(matched),.q(halves[node]));
end endgenerate
wire both;
gf_serial_lut #(.INIT(16'h8888)) combine (
.a(halves[0]),.b(halves[1]),.c(1'b0),.d(1'b0),.q(both)
);
gf_serial_reg result(.clk(clk),.reset(clear),.d(both),.q(good));
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_dsss_sifs_island.sv · 120 lines
Download this file · Permanent section link
// BRAM-free low-MAC SIFS island. The RX PHY supplies the transmitter MAC with
// each decision, so a previously unseen station can receive an immediate
// ACK/CTS without a host-configured lookup. ACK/CTS bytes, FCS, DSSS, and I/Q
// are generated locally after that decision.
`timescale 1ns/1ps
module gf_dsss_sifs_island #(
parameter integer CLOCK_HZ = 20_000_000,
parameter integer SIFS_US = 10,
parameter integer STATION_SLOTS = 2,
parameter integer STATION_WIDTH =
(STATION_SLOTS <= 1) ? 1 : $clog2(STATION_SLOTS),
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer IQ_WIDTH = 32,
parameter integer SERIAL_CONTROL_CRC = 0,
parameter integer DECISION_AGE_OFFSET = 0
) (
input wire clk,
input wire resetn,
input wire arm,
input wire kill,
input wire rx_frame_end,
input wire [DECISION_AGE_WIDTH-1:0] rx_decision_age_cycles,
input wire rx_fcs_ok,
input wire rx_ra_matches_ap,
input wire rx_response_required,
input wire rx_is_rts,
input wire [STATION_WIDTH-1:0] rx_station,
input wire [47:0] rx_response_mac,
input wire [15:0] rx_duration_us,
input wire tx_sample_tick,
input wire tx_sink_ready,
output wire tx_override_valid,
output wire [IQ_WIDTH-1:0] tx_override_iq,
output wire response_pending,
output wire response_active,
output wire response_start,
output wire response_is_cts,
output wire [STATION_WIDTH-1:0] response_station,
output wire [47:0] response_mac,
output wire [15:0] response_duration_us,
output wire [31:0] response_count,
output wire [31:0] deadline_miss_count,
output wire [31:0] rejected_count,
output wire [31:0] stream_abort_count
);
wire response_prepare;
wire response_path_ready;
wire response_done;
wire deadline_miss;
wire event_rejected;
wire stream_abort;
gf_sifs_scheduler #(
.CLOCK_HZ(CLOCK_HZ),
.SIFS_US(SIFS_US),
.DECISION_AGE_OFFSET(DECISION_AGE_OFFSET),
.STATION_SLOTS(STATION_SLOTS),
.STATION_WIDTH(STATION_WIDTH),
.DECISION_AGE_WIDTH(DECISION_AGE_WIDTH)
) scheduler (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(kill),
.rx_frame_end(rx_frame_end),
.rx_decision_age_cycles(rx_decision_age_cycles),
.rx_fcs_ok(rx_fcs_ok),
.rx_ra_matches_ap(rx_ra_matches_ap),
.rx_response_required(rx_response_required),
.rx_is_rts(rx_is_rts),
.rx_station(rx_station),
.rx_response_mac(rx_response_mac),
.rx_duration_us(rx_duration_us),
.response_path_busy(response_active),
.response_path_ready(response_path_ready),
.response_pending(response_pending),
.response_prepare(response_prepare),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_station(response_station),
.response_mac(response_mac),
.response_duration_us(response_duration_us),
.deadline_miss(deadline_miss),
.event_rejected(event_rejected),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count)
);
gf_dsss_1mbps_control_tx #(
.IQ_WIDTH(IQ_WIDTH),
.SERIAL_CONTROL_CRC(SERIAL_CONTROL_CRC)
) formatter (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(kill),
.response_prepare(response_prepare),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_duration_us(response_duration_us),
.response_station_mac(response_mac),
.tx_sample_tick(tx_sample_tick),
.tx_sink_ready(tx_sink_ready),
.response_path_ready(response_path_ready),
.response_active(response_active),
.tx_override_valid(tx_override_valid),
.tx_override_iq(tx_override_iq),
.response_done(response_done),
.stream_abort(stream_abort),
.stream_abort_count(stream_abort_count)
);
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_dsss_sifs_low_mac.sv · 130 lines
Download this file · Permanent section link
// End-to-end hard-real-time boundary from decoded PSDU bytes to TX1 I/Q.
// A separate RX PHY owns synchronization/demodulation and supplies this byte
// stream. This wrapper performs the minimum low-MAC classification and then
// generates an ACK or CTS locally at the original air-relative SIFS deadline.
`timescale 1ns/1ps
module gf_dsss_sifs_low_mac #(
parameter integer CLOCK_HZ = 20_000_000,
parameter integer SIFS_US = 10,
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer IQ_WIDTH = 32,
parameter integer SERIAL_CONTROL_CRC = 0,
parameter integer EXTERNAL_FCS = 0,
parameter integer DECISION_AGE_OFFSET = 0
) (
input wire clk,
input wire resetn,
input wire arm,
input wire kill,
input wire [47:0] ap_mac,
input wire psdu_start,
input wire psdu_byte_valid,
input wire [7:0] psdu_byte,
input wire psdu_byte_last,
input wire [DECISION_AGE_WIDTH-1:0] psdu_end_age_cycles,
input wire checked_fcs_ok,
input wire tx_sample_tick,
input wire tx_sink_ready,
output wire tx_override_valid,
output wire [IQ_WIDTH-1:0] tx_override_iq,
output wire response_pending,
output wire response_active,
output wire response_start,
output wire response_is_cts,
output wire [47:0] response_mac,
output wire [15:0] response_duration_us,
output wire [31:0] response_count,
output wire [31:0] deadline_miss_count,
output wire [31:0] rejected_count,
output wire [31:0] stream_abort_count,
output wire decision_valid,
output wire decision_fcs_ok,
output wire decision_ra_matches_ap,
output wire decision_response_required,
output wire decision_is_rts,
output wire [47:0] decision_response_mac,
output wire [15:0] decision_duration_us,
output wire decision_malformed,
output wire [31:0] classified_frame_count,
output wire [31:0] classified_fcs_ok_count,
output wire [31:0] response_candidate_count,
output wire [31:0] malformed_count
);
wire [DECISION_AGE_WIDTH-1:0] decision_age_cycles;
wire response_station_unused;
gf_low_mac_classifier #(
.DECISION_AGE_WIDTH(DECISION_AGE_WIDTH),
.EXTERNAL_FCS(EXTERNAL_FCS)
) classifier (
.clk(clk),
.resetn(resetn),
.ap_mac(ap_mac),
.psdu_start(psdu_start),
.psdu_byte_valid(psdu_byte_valid),
.psdu_byte(psdu_byte),
.psdu_byte_last(psdu_byte_last),
.psdu_end_age_cycles(psdu_end_age_cycles),
.checked_fcs_ok(checked_fcs_ok),
.decision_valid(decision_valid),
.decision_age_cycles(decision_age_cycles),
.decision_fcs_ok(decision_fcs_ok),
.decision_ra_matches_ap(decision_ra_matches_ap),
.decision_response_required(decision_response_required),
.decision_is_rts(decision_is_rts),
.decision_response_mac(decision_response_mac),
.decision_duration_us(decision_duration_us),
.decision_malformed(decision_malformed),
.frame_count(classified_frame_count),
.fcs_ok_count(classified_fcs_ok_count),
.response_candidate_count(response_candidate_count),
.malformed_count(malformed_count)
);
gf_dsss_sifs_island #(
.CLOCK_HZ(CLOCK_HZ),
.SIFS_US(SIFS_US),
.STATION_SLOTS(1),
.DECISION_AGE_OFFSET(DECISION_AGE_OFFSET),
.DECISION_AGE_WIDTH(DECISION_AGE_WIDTH),
.IQ_WIDTH(IQ_WIDTH),
.SERIAL_CONTROL_CRC(SERIAL_CONTROL_CRC)
) response_island (
.clk(clk),
.resetn(resetn),
.arm(arm),
.kill(kill),
.rx_frame_end(decision_valid),
.rx_decision_age_cycles(decision_age_cycles),
.rx_fcs_ok(decision_fcs_ok),
.rx_ra_matches_ap(decision_ra_matches_ap),
.rx_response_required(decision_response_required),
.rx_is_rts(decision_is_rts),
.rx_station(1'b0),
.rx_response_mac(decision_response_mac),
.rx_duration_us(decision_duration_us),
.tx_sample_tick(tx_sample_tick),
.tx_sink_ready(tx_sink_ready),
.tx_override_valid(tx_override_valid),
.tx_override_iq(tx_override_iq),
.response_pending(response_pending),
.response_active(response_active),
.response_start(response_start),
.response_is_cts(response_is_cts),
.response_station(response_station_unused),
.response_mac(response_mac),
.response_duration_us(response_duration_us),
.response_count(response_count),
.deadline_miss_count(deadline_miss_count),
.rejected_count(rejected_count),
.stream_abort_count(stream_abort_count)
);
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_low_mac_classifier.sv · 192 lines
Download this file · Permanent section link
// Minimum IEEE 802.11 low-MAC classifier for immediate ACK/CTS decisions.
//
// The RX PHY supplies decoded PSDU bytes, including the four FCS bytes. This
// block retains only the fields needed before SIFS expires: Frame Control,
// Duration, receiver address, transmitter address, and CRC-32 state. Full
// management, WPA2, IP, TCP, and payload handling remains on the host.
//
// MAC values use network display order: 48'h024746415031 corresponds to
// 02:47:46:41:50:31 and byte 0 occupies bits [47:40].
`timescale 1ns/1ps
module gf_low_mac_classifier #(
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer COUNT_WIDTH = 32,
parameter integer EXTERNAL_FCS = 0,
// The PHY accepts at most 4095 PSDU bytes. Low MAC only distinguishes
// header offsets 0..27 and "beyond the minimum header". Never count the
// remaining payload here; Windows receives every byte independently.
// Zero retains the old implementation for cycle-exact differential tests.
parameter integer SATURATING_HEADER_INDEX = 1
) (
input wire clk,
input wire resetn,
input wire [47:0] ap_mac,
// psdu_start may accompany the first valid byte. psdu_byte_last marks the
// final FCS byte. psdu_end_age_cycles is the PHY's age of that decision
// relative to the received PPDU end.
input wire psdu_start,
input wire psdu_byte_valid,
input wire [7:0] psdu_byte,
input wire psdu_byte_last,
input wire [DECISION_AGE_WIDTH-1:0] psdu_end_age_cycles,
input wire checked_fcs_ok,
// These signals are sampled on the clock edge where decision_valid is 1.
output wire decision_valid,
output wire [DECISION_AGE_WIDTH-1:0] decision_age_cycles,
output wire decision_fcs_ok,
output wire decision_ra_matches_ap,
output wire decision_response_required,
output wire decision_is_rts,
output wire [47:0] decision_response_mac,
output wire [15:0] decision_duration_us,
output wire decision_malformed,
output reg [COUNT_WIDTH-1:0] frame_count,
output reg [COUNT_WIDTH-1:0] fcs_ok_count,
output reg [COUNT_WIDTH-1:0] response_candidate_count,
output reg [COUNT_WIDTH-1:0] malformed_count
);
localparam [31:0] GOOD_FCS_RESIDUE = 32'hdebb_20e3;
reg frame_active;
localparam integer INDEX_WIDTH = SATURATING_HEADER_INDEX ? 5 : 16;
reg [INDEX_WIDTH-1:0] byte_index;
reg [31:0] crc_state;
reg [7:0] frame_control_0;
reg [15:0] duration_us;
reg [47:0] receiver_address;
reg [47:0] transmitter_address;
function automatic [31:0] crc32_byte;
input [31:0] crc_in;
input [7:0] data;
integer bit_number;
reg [31:0] value;
begin
value = crc_in ^ data;
for (bit_number = 0; bit_number < 8;
bit_number = bit_number + 1) begin
if (value[0])
value = (value >> 1) ^ 32'hedb8_8320;
else
value = value >> 1;
end
crc32_byte = value;
end
endfunction
wire accepting_byte = psdu_byte_valid && (frame_active || psdu_start);
wire [INDEX_WIDTH-1:0] active_byte_index = psdu_start ? {INDEX_WIDTH{1'b0}} : byte_index;
wire [31:0] active_crc_state = psdu_start
? 32'hffff_ffff : crc_state;
wire [31:0] crc_after_byte = crc32_byte(active_crc_state, psdu_byte);
wire version_valid = frame_control_0[1:0] == 2'b00;
wire [1:0] frame_type = frame_control_0[3:2];
wire [3:0] frame_subtype = frame_control_0[7:4];
wire is_management = frame_type == 2'b00;
wire is_control = frame_type == 2'b01;
wire is_data = frame_type == 2'b10;
wire is_rts = is_control && frame_subtype == 4'b1011;
wire is_ps_poll = is_control && frame_subtype == 4'b1010;
wire aid_marker_ok = !is_ps_poll || duration_us[15:14] == 2'b11;
wire ordinary_ack_candidate = is_management || is_data;
wire minimum_length_ok = is_ps_poll ? active_byte_index == 16'd19 : is_rts
? active_byte_index >= 16'd19
: (ordinary_ack_candidate ? active_byte_index >= 16'd27 : 1'b0);
wire unicast_receiver = !receiver_address[40];
wire response_candidate = version_valid && minimum_length_ok &&
aid_marker_ok && unicast_receiver &&
(ordinary_ack_candidate || is_rts || is_ps_poll);
wire malformed_header = !version_valid || !aid_marker_ok ||
((ordinary_ack_candidate || is_rts || is_ps_poll) && !minimum_length_ok);
assign decision_valid = accepting_byte && psdu_byte_last;
assign decision_age_cycles = psdu_end_age_cycles;
assign decision_fcs_ok = EXTERNAL_FCS ? checked_fcs_ok :
crc_after_byte == GOOD_FCS_RESIDUE;
assign decision_ra_matches_ap = receiver_address == ap_mac;
assign decision_response_required = response_candidate;
assign decision_is_rts = is_rts;
assign decision_response_mac = transmitter_address;
assign decision_duration_us = duration_us;
// PS-Poll receives an ordinary ACK (not CTS). Its Duration/ID value is
// therefore never used as a NAV duration by the existing SIFS scheduler.
// Association/AID ownership and buffered delivery remain host decisions.
assign decision_malformed = malformed_header;
always @(posedge clk) begin
if (!resetn) begin
frame_active <= 1'b0;
byte_index <= 16'd0;
if (!EXTERNAL_FCS) crc_state <= 32'hffff_ffff;
frame_control_0 <= 8'd0;
duration_us <= 16'd0;
receiver_address <= 48'd0;
transmitter_address <= 48'd0;
frame_count <= {COUNT_WIDTH{1'b0}};
fcs_ok_count <= {COUNT_WIDTH{1'b0}};
response_candidate_count <= {COUNT_WIDTH{1'b0}};
malformed_count <= {COUNT_WIDTH{1'b0}};
end else begin
if (psdu_start) begin
frame_active <= 1'b1;
byte_index <= 16'd0;
if (!EXTERNAL_FCS) crc_state <= 32'hffff_ffff;
frame_control_0 <= 8'd0;
duration_us <= 16'd0;
receiver_address <= 48'd0;
transmitter_address <= 48'd0;
end
if (accepting_byte) begin
if (!EXTERNAL_FCS) crc_state <= crc_after_byte;
case (active_byte_index)
16'd0: frame_control_0 <= psdu_byte;
16'd2: duration_us[7:0] <= psdu_byte;
16'd3: duration_us[15:8] <= psdu_byte;
16'd4: receiver_address[47:40] <= psdu_byte;
16'd5: receiver_address[39:32] <= psdu_byte;
16'd6: receiver_address[31:24] <= psdu_byte;
16'd7: receiver_address[23:16] <= psdu_byte;
16'd8: receiver_address[15:8] <= psdu_byte;
16'd9: receiver_address[7:0] <= psdu_byte;
16'd10: transmitter_address[47:40] <= psdu_byte;
16'd11: transmitter_address[39:32] <= psdu_byte;
16'd12: transmitter_address[31:24] <= psdu_byte;
16'd13: transmitter_address[23:16] <= psdu_byte;
16'd14: transmitter_address[15:8] <= psdu_byte;
16'd15: transmitter_address[7:0] <= psdu_byte;
default: begin end
endcase
if (psdu_byte_last) begin
frame_active <= 1'b0;
byte_index <= 16'd0;
frame_count <= frame_count + 1'b1;
if (decision_fcs_ok)
fcs_ok_count <= fcs_ok_count + 1'b1;
if (response_candidate)
response_candidate_count <=
response_candidate_count + 1'b1;
if (malformed_header)
malformed_count <= malformed_count + 1'b1;
end else begin
// Saturate at 28, not 19 or 27: PS-Poll must be exactly
// 20 bytes including FCS, while ordinary frames need at
// least 28. Longer data/RTS frames remain distinguishable
// from exact-length PS-Poll without payload-length state.
if (!SATURATING_HEADER_INDEX || active_byte_index < 28)
byte_index <= active_byte_index + 1'b1;
end
end
end
end
endmodule
wifi_pluto_link/fpga_sifs/rtl/gf_sifs_scheduler.sv · 170 lines
Download this file · Permanent section link
// Deterministic IEEE 802.11 immediate-response scheduler.
//
// This block deliberately owns only the hard real-time low-MAC boundary. A
// receive PHY presents a decision after checking FCS and the receiver address,
// together with the number of clocks already elapsed since the PPDU ended.
// The scheduler subtracts that decode age, then either starts ACK/CTS exactly
// SIFS after the air-frame boundary or drops the response. It never emits late.
//
// The response formatter/player is downstream. Windows remains responsible
// for management, WPA2, DHCP, ARP, TCP, and HTTP.
`timescale 1ns/1ps
module gf_sifs_scheduler #(
parameter integer CLOCK_HZ = 100_000_000,
parameter integer SIFS_US = 10,
parameter integer CTS_AIRTIME_US = 304,
parameter integer STATION_SLOTS = 2,
parameter integer STATION_WIDTH =
(STATION_SLOTS <= 1) ? 1 : $clog2(STATION_SLOTS),
parameter integer DECISION_AGE_WIDTH = 16,
parameter integer COUNT_WIDTH = 32,
// Fixed pipeline delay after the PHY timestamp, folded into constants.
parameter integer DECISION_AGE_OFFSET = 0
) (
input wire clk,
input wire resetn,
// The host arms a bounded experiment. kill has immediate priority.
input wire arm,
input wire kill,
// Pulse when the receive decision is valid. rx_decision_age_cycles plus
// DECISION_AGE_OFFSET is the elapsed time from PPDU end to this clock edge.
input wire rx_frame_end,
input wire [DECISION_AGE_WIDTH-1:0] rx_decision_age_cycles,
input wire rx_fcs_ok,
input wire rx_ra_matches_ap,
input wire rx_response_required,
input wire rx_is_rts,
input wire [STATION_WIDTH-1:0] rx_station,
input wire [47:0] rx_response_mac,
input wire [15:0] rx_duration_us,
// Must already be true at the deadline. There is intentionally no retry.
input wire response_path_busy,
input wire response_path_ready,
output reg response_pending,
output reg response_prepare,
output reg response_start,
output reg response_is_cts,
output reg [STATION_WIDTH-1:0] response_station,
output reg [47:0] response_mac,
output reg [15:0] response_duration_us,
output reg deadline_miss,
output reg event_rejected,
output reg [COUNT_WIDTH-1:0] response_count,
output reg [COUNT_WIDTH-1:0] deadline_miss_count,
output reg [COUNT_WIDTH-1:0] rejected_count
);
localparam integer CLOCKS_PER_US = CLOCK_HZ / 1_000_000;
localparam integer SIFS_CYCLES = CLOCKS_PER_US * SIFS_US;
localparam integer DECISION_BUDGET = SIFS_CYCLES - DECISION_AGE_OFFSET;
localparam integer WAIT_WIDTH =
(SIFS_CYCLES <= 1) ? 1 : $clog2(SIFS_CYCLES + 1);
localparam integer CTS_SUBTRACT_US = SIFS_US + CTS_AIRTIME_US;
reg [WAIT_WIDTH-1:0] wait_cycles;
initial begin
if ((CLOCK_HZ % 1_000_000) != 0)
$error("CLOCK_HZ must be an integer multiple of 1 MHz");
if (SIFS_CYCLES < 1)
$error("SIFS must contain at least one clock cycle");
if (DECISION_AGE_OFFSET < 0 || DECISION_BUDGET < 1)
$error("Fixed decision pipeline must fit inside SIFS");
if (STATION_SLOTS < 1 ||
STATION_SLOTS > (1 << STATION_WIDTH))
$error("STATION_SLOTS does not fit STATION_WIDTH");
if (DECISION_AGE_WIDTH < WAIT_WIDTH)
$error("DECISION_AGE_WIDTH cannot represent the SIFS interval");
end
wire station_in_range = rx_station < STATION_SLOTS;
wire eligible_event = rx_fcs_ok && rx_ra_matches_ap &&
rx_response_required && station_in_range;
wire [15:0] cts_duration =
(rx_duration_us > CTS_SUBTRACT_US)
? rx_duration_us - CTS_SUBTRACT_US
: 16'd0;
wire decision_before_deadline =
rx_decision_age_cycles < DECISION_BUDGET;
wire [WAIT_WIDTH-1:0] remaining_wait_cycles =
DECISION_BUDGET[WAIT_WIDTH-1:0] -
rx_decision_age_cycles[WAIT_WIDTH-1:0];
always @(posedge clk) begin
if (!resetn) begin
response_pending <= 1'b0;
response_prepare <= 1'b0;
response_start <= 1'b0;
response_is_cts <= 1'b0;
response_station <= {STATION_WIDTH{1'b0}};
response_mac <= 48'd0;
response_duration_us <= 16'd0;
deadline_miss <= 1'b0;
event_rejected <= 1'b0;
response_count <= {COUNT_WIDTH{1'b0}};
deadline_miss_count <= {COUNT_WIDTH{1'b0}};
rejected_count <= {COUNT_WIDTH{1'b0}};
wait_cycles <= {WAIT_WIDTH{1'b0}};
end else begin
response_prepare <= 1'b0;
response_start <= 1'b0;
deadline_miss <= 1'b0;
event_rejected <= 1'b0;
if (kill || !arm) begin
response_pending <= 1'b0;
wait_cycles <= {WAIT_WIDTH{1'b0}};
end else begin
if (response_pending) begin
if (wait_cycles > 1) begin
wait_cycles <= wait_cycles - 1'b1;
end else begin
// Exact deadline: start now or abandon this response.
response_pending <= 1'b0;
wait_cycles <= {WAIT_WIDTH{1'b0}};
if (response_path_ready) begin
response_start <= 1'b1;
response_count <= response_count + 1'b1;
end else begin
deadline_miss <= 1'b1;
deadline_miss_count <= deadline_miss_count + 1'b1;
end
end
end
if (rx_frame_end && rx_response_required) begin
if (!eligible_event || response_pending ||
response_path_busy) begin
event_rejected <= 1'b1;
rejected_count <= rejected_count + 1'b1;
end else if (!decision_before_deadline) begin
// The PHY decision arrived at or after the air SIFS
// boundary. Count it, but never transmit a late reply.
deadline_miss <= 1'b1;
deadline_miss_count <= deadline_miss_count + 1'b1;
end else begin
response_pending <= 1'b1;
response_prepare <= 1'b1;
wait_cycles <= remaining_wait_cycles;
response_is_cts <= rx_is_rts;
response_station <= rx_station;
response_mac <= rx_response_mac;
response_duration_us <= rx_is_rts
? cts_duration : 16'd0;
end
end
end
end
end
endmodule
wifi_pluto_link/host/ap_realtime.cpp · 3885 lines
Download this file · Permanent section link
#ifdef GF_AP_PROTOCOL_ONLY
#include "../../tools/wifi_ap_portable_rt.hpp"
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#else
#include <openssl/rand.h>
#endif
#else
#define main gf_station_realtime_embedded_main
#include "realtime_link.cpp"
#undef main
#include <bcrypt.h>
#endif
#include "../../tools/wifi_dsss_tx.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
namespace gf::ap {
using Mac = rt::Mac;
using Ipv4 = std::array<std::uint8_t, 4>;
constexpr Mac kDefaultBssid = {0x02, 0x47, 0x46, 0x41, 0x50, 0x31};
constexpr Ipv4 kDefaultServerIp = {192, 168, 44, 1};
constexpr std::string_view kDefaultSsid = "PLUTO-2.4";
constexpr std::string_view kDefaultPassphrase = "ChangeThisExample2026!";
std::uint16_t read_be16(const std::uint8_t* data) {
return static_cast<std::uint16_t>(
(static_cast<std::uint16_t>(data[0]) << 8) | data[1]);
}
std::uint32_t read_be32(const std::uint8_t* data) {
return (static_cast<std::uint32_t>(data[0]) << 24) |
(static_cast<std::uint32_t>(data[1]) << 16) |
(static_cast<std::uint32_t>(data[2]) << 8) |
static_cast<std::uint32_t>(data[3]);
}
void append_be32(std::vector<std::uint8_t>& output, std::uint32_t value) {
output.push_back(static_cast<std::uint8_t>(value >> 24));
output.push_back(static_cast<std::uint8_t>(value >> 16));
output.push_back(static_cast<std::uint8_t>(value >> 8));
output.push_back(static_cast<std::uint8_t>(value));
}
void append_le64(std::vector<std::uint8_t>& output, std::uint64_t value) {
for (int index = 0; index < 8; ++index)
output.push_back(static_cast<std::uint8_t>(value >> (8 * index)));
}
bool is_group(const Mac& mac) { return (mac[0] & 1u) != 0; }
std::string ip_text(const Ipv4& ip) {
return std::to_string(ip[0]) + "." + std::to_string(ip[1]) + "." +
std::to_string(ip[2]) + "." + std::to_string(ip[3]);
}
Ipv4 parse_ip(std::string_view text) {
Ipv4 result{};
std::size_t offset = 0;
for (std::size_t index = 0; index < result.size(); ++index) {
const auto separator = text.find('.', offset);
const auto token = text.substr(
offset, separator == std::string_view::npos
? text.size() - offset : separator - offset);
if (token.empty()) throw std::runtime_error("invalid IPv4 address");
unsigned value = 0;
for (const char character : token) {
if (character < '0' || character > '9')
throw std::runtime_error("invalid IPv4 address");
value = value * 10 + static_cast<unsigned>(character - '0');
if (value > 255) throw std::runtime_error("invalid IPv4 address");
}
result[index] = static_cast<std::uint8_t>(value);
if (index + 1 != result.size()) {
if (separator == std::string_view::npos)
throw std::runtime_error("invalid IPv4 address");
offset = separator + 1;
} else if (separator != std::string_view::npos) {
throw std::runtime_error("invalid IPv4 address");
}
}
return result;
}
struct Outbound {
std::string kind;
std::string reason;
Mac destination{};
std::vector<std::uint8_t> psdu;
bool sifs_deadline = false;
int repeats = 1;
// Host-only tracking; never serialized into the 802.11 frame.
std::uint16_t tcp_response_port = 0;
};
std::vector<std::uint8_t> rsn_ie() {
return {48, 20,
1, 0, // RSN version 1.
0x00, 0x0f, 0xac, 4, // Group cipher CCMP-128.
1, 0, 0x00, 0x0f, 0xac, 4, // One pairwise cipher: CCMP.
1, 0, 0x00, 0x0f, 0xac, 2, // One AKM: PSK.
0, 0}; // RSN capabilities.
}
void append_erp_rate_ies(std::vector<std::uint8_t>& frame) {
// Advertise exactly the PHYs the realtime receiver can decode: DSSS 1
// Mb/s plus every legacy OFDM rate. Mark the mandatory ERP rates basic,
// and explicitly clear Use Protection so an 11g client does not wrap each
// small IP packet in an unnecessary RTS/CTS exchange.
frame.insert(frame.end(), {
1, 8,
0x82, // 1 Mb/s DSSS, basic.
0x8c, // 6 Mb/s OFDM, basic.
0x12, // 9 Mb/s OFDM.
0x98, // 12 Mb/s OFDM, basic.
0x24, // 18 Mb/s OFDM.
0xb0, // 24 Mb/s OFDM, basic.
0x48, // 36 Mb/s OFDM.
0x60, // 48 Mb/s OFDM.
50, 1, 0x6c, // Extended supported rate: 54 Mb/s OFDM.
42, 1, 0x00 // ERP: no non-ERP station; no protection required.
});
}
void append_rate_ies(std::vector<std::uint8_t>& frame,
bool dsss_1mbps_only) {
if (dsss_1mbps_only) {
// One mandatory long-preamble DSSS rate. This prevents a station from
// selecting OFDM when the active FPGA receive path cannot decode it.
frame.insert(frame.end(), {1, 1, 0x82});
return;
}
append_erp_rate_ies(frame);
}
void append_ap_ies(std::vector<std::uint8_t>& frame,
std::string_view ssid, int channel,
bool dsss_1mbps_only) {
frame.push_back(0);
frame.push_back(static_cast<std::uint8_t>(ssid.size()));
frame.insert(frame.end(), ssid.begin(), ssid.end());
append_rate_ies(frame, dsss_1mbps_only);
frame.insert(frame.end(),
{3, 1, static_cast<std::uint8_t>(channel)});
}
template <std::size_t Size>
std::array<std::uint8_t, Size> random_array() {
std::array<std::uint8_t, Size> output{};
#if defined(GF_AP_PROTOCOL_ONLY) && !defined(_WIN32)
static_assert(Size <= static_cast<std::size_t>(
std::numeric_limits<int>::max()));
if (RAND_bytes(output.data(), static_cast<int>(output.size())) != 1)
throw std::runtime_error("OpenSSL RAND_bytes failed");
#else
const NTSTATUS status = BCryptGenRandom(
nullptr, output.data(), static_cast<ULONG>(output.size()),
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (status < 0)
throw std::runtime_error("BCryptGenRandom failed");
#endif
return output;
}
void put_be16(std::vector<std::uint8_t>& bytes, std::size_t offset,
std::uint16_t value) {
bytes.at(offset) = static_cast<std::uint8_t>(value >> 8);
bytes.at(offset + 1) = static_cast<std::uint8_t>(value);
}
void put_be64(std::vector<std::uint8_t>& bytes, std::size_t offset,
std::uint64_t value) {
for (int index = 7; index >= 0; --index) {
bytes.at(offset + static_cast<std::size_t>(7 - index)) =
static_cast<std::uint8_t>(value >> (index * 8));
}
}
std::vector<std::uint8_t> make_eapol_key(
std::uint16_t key_info, std::uint64_t replay_counter,
const wifi::Wpa2Nonce& nonce, const std::vector<std::uint8_t>& key_data,
const std::optional<wifi::Wpa2Key>& kck = std::nullopt) {
const auto body_size = static_cast<std::uint16_t>(95 + key_data.size());
std::vector<std::uint8_t> eapol(4 + body_size, 0);
eapol[0] = 2; // IEEE 802.1X-2004.
eapol[1] = 3; // EAPOL-Key.
put_be16(eapol, 2, body_size);
eapol[4] = 2; // RSN Key descriptor.
put_be16(eapol, 5, key_info);
put_be16(eapol, 7, 16);
put_be64(eapol, 9, replay_counter);
std::copy(nonce.begin(), nonce.end(), eapol.begin() + 17);
put_be16(eapol, 97, static_cast<std::uint16_t>(key_data.size()));
std::copy(key_data.begin(), key_data.end(), eapol.begin() + 99);
if (kck) {
const auto mic = wifi::wpa2_eapol_mic(*kck, eapol);
std::copy(mic.begin(), mic.end(), eapol.begin() + 81);
}
return eapol;
}
std::pair<std::vector<std::uint8_t>, std::vector<std::uint8_t>>
ccmp_aad_nonce(const std::vector<std::uint8_t>& psdu,
const wifi::DataLayout& layout,
const std::uint8_t* ccmp) {
std::uint16_t control = layout.frame_control;
control &= static_cast<std::uint16_t>(~(0x0800u | 0x1000u | 0x2000u));
control &= static_cast<std::uint16_t>(~0x0070u);
control |= 0x4000u;
if (layout.qos) control &= static_cast<std::uint16_t>(~0x8000u);
std::vector<std::uint8_t> aad;
aad.push_back(static_cast<std::uint8_t>(control));
aad.push_back(static_cast<std::uint8_t>(control >> 8));
aad.insert(aad.end(), psdu.begin() + 4, psdu.begin() + 22);
aad.push_back(static_cast<std::uint8_t>(psdu[22] & 0x0f));
aad.push_back(0);
if (layout.has_address4)
aad.insert(aad.end(), psdu.begin() + 24, psdu.begin() + 30);
if (layout.qos) {
aad.push_back(layout.tid);
aad.push_back(0);
}
std::vector<std::uint8_t> nonce;
nonce.push_back(layout.qos ? layout.tid : 0);
nonce.insert(nonce.end(), layout.address2.begin(), layout.address2.end());
nonce.insert(nonce.end(), {ccmp[7], ccmp[6], ccmp[5],
ccmp[4], ccmp[1], ccmp[0]});
return {std::move(aad), std::move(nonce)};
}
std::vector<std::uint8_t> ccmp_encrypt_frame(
const std::vector<std::uint8_t>& plain_psdu,
const wifi::Wpa2Key& temporal_key, std::uint64_t packet_number,
int key_id = 0) {
if (packet_number == 0 || packet_number > 0xffffffffffffULL ||
key_id < 0 || key_id > 3)
throw std::runtime_error("CCMP PN/key ID invalid or exhausted; rekey required");
const auto plain_layout = wifi::parse_data_layout(plain_psdu);
if (!plain_layout || plain_layout->protected_frame ||
plain_psdu.size() < plain_layout->header_bytes + 4)
throw std::runtime_error("CCMP input is not a plain data MPDU");
std::vector<std::uint8_t> output(
plain_psdu.begin(), plain_psdu.begin() +
static_cast<std::ptrdiff_t>(plain_layout->header_bytes));
auto control = rt::little_u16(output.data());
control |= 0x4000u;
output[0] = static_cast<std::uint8_t>(control);
output[1] = static_cast<std::uint8_t>(control >> 8);
std::array<std::uint8_t, 8> ccmp = {
static_cast<std::uint8_t>(packet_number),
static_cast<std::uint8_t>(packet_number >> 8),
0,
static_cast<std::uint8_t>(0x20u | ((key_id & 3) << 6)),
static_cast<std::uint8_t>(packet_number >> 16),
static_cast<std::uint8_t>(packet_number >> 24),
static_cast<std::uint8_t>(packet_number >> 32),
static_cast<std::uint8_t>(packet_number >> 40)};
output.insert(output.end(), ccmp.begin(), ccmp.end());
const auto protected_layout = wifi::parse_data_layout(output);
if (!protected_layout)
throw std::runtime_error("CCMP protected header layout failed");
const auto [aad, nonce] = ccmp_aad_nonce(
output, *protected_layout, output.data() + protected_layout->header_bytes);
const auto plain_begin = plain_psdu.begin() +
static_cast<std::ptrdiff_t>(plain_layout->header_bytes);
std::vector<std::uint8_t> plaintext(
plain_begin, plain_psdu.end() - 4);
std::vector<std::uint8_t> tag(8, 0);
const auto encrypted = wifi::wpa2_aes_ccm(
false, temporal_key, nonce, aad, plaintext, tag);
if (!encrypted) throw std::runtime_error("CCMP encryption failed");
output.insert(output.end(), encrypted->begin(), encrypted->end());
output.insert(output.end(), tag.begin(), tag.end());
rt::append_fcs(output);
return output;
}
struct CcmpPlain {
std::vector<std::uint8_t> llc;
std::uint64_t packet_number = 0;
int key_id = 0;
};
std::optional<CcmpPlain> ccmp_decrypt_frame(
const std::vector<std::uint8_t>& psdu,
const wifi::Wpa2Key& temporal_key) {
const auto layout = wifi::parse_data_layout(psdu);
if (!layout || !layout->protected_frame ||
psdu.size() < layout->header_bytes + 8 + 8 + 4)
return std::nullopt;
const auto* ccmp = psdu.data() + layout->header_bytes;
if (ccmp[2] != 0 || (ccmp[3] & 0x3fu) != 0x20u) return std::nullopt;
const auto packet_number = static_cast<std::uint64_t>(ccmp[0]) |
(static_cast<std::uint64_t>(ccmp[1]) << 8) |
(static_cast<std::uint64_t>(ccmp[4]) << 16) |
(static_cast<std::uint64_t>(ccmp[5]) << 24) |
(static_cast<std::uint64_t>(ccmp[6]) << 32) |
(static_cast<std::uint64_t>(ccmp[7]) << 40);
const std::size_t encrypted_size =
psdu.size() - layout->header_bytes - 8 - 8 - 4;
std::vector<std::uint8_t> encrypted(
ccmp + 8, ccmp + 8 + encrypted_size);
std::vector<std::uint8_t> tag(
ccmp + 8 + encrypted_size, ccmp + 16 + encrypted_size);
const auto [aad, nonce] = ccmp_aad_nonce(psdu, *layout, ccmp);
auto plain = wifi::wpa2_aes_ccm(
true, temporal_key, nonce, aad, encrypted, tag);
if (!plain) return std::nullopt;
return CcmpPlain{std::move(*plain), packet_number,
(ccmp[3] >> 6) & 3};
}
struct TcpConnection {
std::uint32_t client_next = 0;
std::uint32_t server_isn = 0;
std::uint32_t server_next = 0;
std::uint32_t last_client_payload_sequence = 0;
std::uint32_t last_response_sequence = 0;
std::vector<std::uint8_t> last_response;
std::string request_headers;
std::uint32_t request_segments = 0;
Ipv4 client_ip{};
rt::Clock::time_point deferred_http_at{};
bool deferred_http = false;
bool response_pending = false;
bool response_queued = false;
std::uint32_t response_acked = 0;
unsigned response_retries = 0;
std::chrono::milliseconds response_rto{1000};
rt::Clock::time_point response_retry_at{};
bool syn_seen = false;
bool established = false;
bool closed = false;
};
struct Station {
Mac mac{};
std::uint16_t aid = 0;
Ipv4 lease{};
bool authenticated = false;
bool associated = false;
bool ptk_valid = false;
bool handshake_complete = false;
wifi::Wpa2Nonce anonce{};
wifi::Wpa2Nonce snonce{};
wifi::Wpa2Ptk ptk{};
std::uint64_t m1_replay = 0;
std::uint64_t handshake_replay = 0;
std::uint64_t tx_packet_number = 1;
// Separate monotonic replay state for each QoS TID and non-QoS data.
// Radio retransmissions can still be ACKed but never redeliver plaintext.
std::array<std::uint64_t, 17> rx_packet_number{};
double last_power_dbfs = -std::numeric_limits<double>::infinity();
std::uint64_t received_frames = 0;
std::optional<bool> observed_power_save;
std::deque<Outbound> power_save_queue;
std::uint16_t last_assoc_sequence = 0xffff;
rt::Clock::time_point last_assoc_reply{};
rt::Clock::time_point last_cts_reply{};
// 0: no pending handshake, 2: waiting for M2, 4: waiting for M4.
std::uint8_t handshake_wait = 0;
std::uint8_t handshake_retry_count = 0;
rt::Clock::time_point last_handshake_tx{};
std::map<std::uint16_t, TcpConnection> tcp;
};
struct ProtocolConfig {
std::string ssid = std::string(kDefaultSsid);
std::string passphrase = std::string(kDefaultPassphrase);
Mac bssid = kDefaultBssid;
Ipv4 server_ip = kDefaultServerIp;
int channel = 6;
std::uint16_t beacon_interval_tu = 100;
std::size_t max_stations = 8;
bool dsss_1mbps_only = false;
std::string page;
};
class ApProtocol {
public:
using EventHandler =
std::function<void(std::string_view, std::string_view)>;
explicit ApProtocol(ProtocolConfig config, EventHandler event = {})
: config_(std::move(config)), event_(std::move(event)),
pmk_(wifi::wpa2_derive_pmk(config_.passphrase, config_.ssid)),
gtk_(random_array<16>()) {
if (config_.page.empty()) {
config_.page =
"<!doctype html><meta name=viewport content='width=device-width'>"
"<title>Pluto C++ AP</title><style>body{font:18px system-ui;"
"max-width:42rem;margin:12vh auto;padding:1rem;background:#07131d;"
"color:#dff}code{color:#7ff}</style><h1>PlutoSDR C++ AP</h1>"
"<p>This page arrived through <code>PC ↔ Pluto I/Q ↔ air"
"</code>, not a Windows network adapter.</p>";
}
if (config_.page.size() > 1400)
throw std::runtime_error("RF HTTP page must be at most 1400 bytes");
if (!config_.beacon_interval_tu)
throw std::runtime_error("Beacon interval must be nonzero");
if (config_.passphrase.size() < 8 || config_.passphrase.size() > 63)
throw std::runtime_error(
"WPA2 passphrase must contain 8 through 63 bytes");
}
const ProtocolConfig& config() const { return config_; }
std::vector<std::uint8_t> beacon(std::uint64_t timestamp_us) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, 0x0080, rt::kBroadcast,
config_.bssid, config_.bssid,
next_sequence());
append_le64(frame, timestamp_us);
rt::append_le16(frame, config_.beacon_interval_tu);
// ESS + privacy. Do not advertise short-preamble support until the
// realtime RX PHY can actually receive short-preamble stations.
rt::append_le16(frame, 0x0011);
append_ap_ies(frame, config_.ssid, config_.channel,
config_.dsss_1mbps_only);
// Legacy TIM: DTIM every beacon; all protocol data is per-station
// unicast. Bitmap offset is an even octet index, not an AID index.
std::array<std::uint8_t, 251> bitmap{};
std::size_t last = 0;
for (const auto& [mac, station] : stations_) {
(void)mac;
if (station.associated && !station.power_save_queue.empty() && station.aid <= 2007) {
const auto octet = static_cast<std::size_t>(station.aid / 8);
bitmap[octet] |= static_cast<std::uint8_t>(1u << (station.aid % 8));
last = std::max(last, octet);
}
}
std::size_t first = 0;
while (first < last && bitmap[first] == 0) ++first;
first &= ~std::size_t{1};
frame.insert(frame.end(), {5, static_cast<std::uint8_t>(4 + last - first),
0, 1, static_cast<std::uint8_t>(first)});
frame.insert(frame.end(), bitmap.begin() + first, bitmap.begin() + last + 1);
const auto rsn = rsn_ie();
frame.insert(frame.end(), rsn.begin(), rsn.end());
rt::append_fcs(frame);
return frame;
}
std::vector<Outbound> ingest(const std::vector<std::uint8_t>& psdu,
double power_dbfs) {
std::vector<Outbound> output;
if (psdu.size() < 10 || !valid_fcs(psdu)) return output;
const auto frame_control = rt::little_u16(psdu.data());
const int type = (frame_control >> 2) & 0x3;
const int subtype = (frame_control >> 4) & 0xf;
if (type == 0 && psdu.size() >= 28) {
Mac destination{};
Mac source{};
Mac bssid{};
std::copy_n(psdu.begin() + 4, 6, destination.begin());
std::copy_n(psdu.begin() + 10, 6, source.begin());
std::copy_n(psdu.begin() + 16, 6, bssid.begin());
if (!is_group(destination) && source != config_.bssid)
output.push_back(make_ack(source));
process_management(psdu, subtype, destination, source, bssid,
power_dbfs, output);
return route_power_save(std::move(output));
}
if (type == 1 && subtype == 10 && psdu.size() == 20) {
Mac receiver{}, transmitter{};
std::copy_n(psdu.begin() + 4, 6, receiver.begin());
std::copy_n(psdu.begin() + 10, 6, transmitter.begin());
const auto duration_id = rt::little_u16(psdu.data() + 2);
const auto found = stations_.find(transmitter);
if (receiver != config_.bssid || (duration_id & 0xc000u) != 0xc000u ||
found == stations_.end() || !found->second.associated ||
(duration_id & 0x3fffu) != found->second.aid) return output;
auto& station = found->second;
output.push_back(make_ack(transmitter)); // FPGA SIFS; never sent by Windows.
if (station.power_save_queue.empty()) {
output.push_back(make_null_data(station));
} else {
auto frame = std::move(station.power_save_queue.front());
station.power_save_queue.pop_front();
set_more_data(frame, !station.power_save_queue.empty());
track_http_dispatch(frame, false, rt::Clock::now());
output.push_back(std::move(frame));
}
publish("ps_poll", "\"station\":" + rt::quote(rt::mac_text(transmitter)) +
",\"aid\":" + std::to_string(station.aid) +
",\"remaining\":" + std::to_string(station.power_save_queue.size()));
return output; // A poll-released frame must not be buffered again.
}
if (type == 1 && subtype == 11 && psdu.size() >= 20) {
Mac receiver{};
Mac transmitter{};
std::copy_n(psdu.begin() + 4, 6, receiver.begin());
std::copy_n(psdu.begin() + 10, 6, transmitter.begin());
if (receiver != config_.bssid) return output;
Station* station = touch_station(transmitter, power_dbfs);
if (!station) return output;
const auto now = rt::Clock::now();
if (station->last_cts_reply.time_since_epoch().count() == 0 ||
now - station->last_cts_reply >=
std::chrono::milliseconds(100)) {
output.push_back(make_cts(
transmitter, rt::little_u16(psdu.data() + 2)));
station->last_cts_reply = now;
}
publish("rts", "\"station\":" +
rt::quote(rt::mac_text(transmitter)) +
",\"duration_us\":" +
std::to_string(rt::little_u16(psdu.data() + 2)));
return output;
}
if (type == 2) {
const auto layout = wifi::parse_data_layout(psdu);
if (!layout) return output;
const Mac source = layout->address2;
if (layout->to_ds && layout->address1 == config_.bssid &&
!is_group(layout->address1)) {
output.push_back(make_ack(source));
}
process_data(psdu, *layout, power_dbfs, output);
}
return route_power_save(std::move(output));
}
std::size_t station_count() const { return stations_.size(); }
std::size_t buffered_for(const Mac& mac) const {
const auto found = stations_.find(mac);
return found == stations_.end() ? 0 : found->second.power_save_queue.size();
}
std::size_t associated_count() const {
return static_cast<std::size_t>(std::count_if(
stations_.begin(), stations_.end(),
[](const auto& item) { return item.second.associated; }));
}
std::optional<Ipv4> lease_for(const Mac& mac) const {
const auto found = stations_.find(mac);
if (found == stations_.end()) return std::nullopt;
return found->second.lease;
}
bool handshake_complete(const Mac& mac) const {
const auto found = stations_.find(mac);
return found != stations_.end() && found->second.handshake_complete;
}
std::vector<Outbound> maintenance(
rt::Clock::time_point now = rt::Clock::now()) {
constexpr auto retry_interval = std::chrono::milliseconds(500);
constexpr std::uint8_t maximum_retries = 4;
std::vector<Outbound> output;
for (auto& [mac, station] : stations_) {
if (station.associated && station.handshake_complete) {
for (auto& [port, connection] : station.tcp) {
if (connection.deferred_http && now >= connection.deferred_http_at)
emit_http_response(station, connection, port, output, true, now);
if (connection.response_pending && !connection.response_queued &&
now >= connection.response_retry_at) {
if (connection.response_retries == 8) {
connection.response_pending = false;
publish("tcp_response_timeout", "\"station\":" + rt::quote(rt::mac_text(mac)) +
",\"client_port\":" + std::to_string(port));
continue;
}
++connection.response_retries;
connection.response_rto = std::min(connection.response_rto * 2,
std::chrono::milliseconds(60000));
output.push_back(make_http_retransmit(station, connection, port));
publish("tcp_response_retry", "\"station\":" + rt::quote(rt::mac_text(mac)) +
",\"client_port\":" + std::to_string(port) +
",\"attempt\":" + std::to_string(connection.response_retries) +
",\"next_rto_ms\":" + std::to_string(connection.response_rto.count()));
}
}
}
if (!station.associated || station.handshake_complete ||
station.handshake_wait == 0 ||
station.last_handshake_tx.time_since_epoch().count() == 0 ||
now - station.last_handshake_tx < retry_interval) {
continue;
}
if (station.handshake_retry_count >= maximum_retries) {
publish("wpa2_timeout", "\"station\":" +
rt::quote(rt::mac_text(mac)) +
",\"waiting_for\":\"M" +
std::to_string(station.handshake_wait) + "\"");
station.handshake_wait = 0;
continue;
}
if (station.handshake_wait == 2) {
output.push_back(make_four_way_m1(station));
} else if (station.handshake_wait == 4 && station.ptk_valid) {
output.push_back(make_four_way_m3(station));
} else {
station.handshake_wait = 0;
continue;
}
++station.handshake_retry_count;
station.last_handshake_tx = now;
publish("wpa2_retry", "\"station\":" +
rt::quote(rt::mac_text(mac)) +
",\"message\":" +
std::to_string(station.handshake_wait) +
",\"attempt\":" +
std::to_string(station.handshake_retry_count + 1));
}
return route_power_save(std::move(output), now);
}
private:
static void set_more_data(Outbound& frame, bool more) {
if (frame.psdu.size() < 28) throw std::runtime_error("Short buffered data frame");
// More Data is excluded from CCMP AAD. Preserve PN/ciphertext/tag and
// sequence; update only that MAC flag and the outer FCS.
frame.psdu[1] = static_cast<std::uint8_t>((frame.psdu[1] & ~0x20u) | (more ? 0x20u : 0u));
frame.psdu.resize(frame.psdu.size() - 4);
rt::append_fcs(frame.psdu);
}
Outbound make_null_data(Station& station) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, 0x0248, station.mac, config_.bssid,
config_.bssid, next_sequence());
rt::append_fcs(frame);
return {"ps_null", "empty legacy power-save queue", station.mac, std::move(frame), false, 1};
}
void track_http_dispatch(const Outbound& frame, bool queued, rt::Clock::time_point now) {
if (!frame.tcp_response_port) return;
const auto station = stations_.find(frame.destination);
if (station == stations_.end()) return;
const auto found = station->second.tcp.find(frame.tcp_response_port);
if (found == station->second.tcp.end() || !found->second.response_pending) return;
auto& connection = found->second;
connection.response_queued = queued;
connection.response_retry_at = now + connection.response_rto;
}
static void discard_queued_http(Station& station, std::uint16_t port) {
std::erase_if(station.power_save_queue,
[port](const Outbound& frame) { return frame.tcp_response_port == port; });
}
std::vector<Outbound> route_power_save(std::vector<Outbound> output,
rt::Clock::time_point now = rt::Clock::now()) {
std::vector<Outbound> ready;
ready.reserve(output.size());
for (auto& frame : output) {
const auto found = stations_.find(frame.destination);
const bool data = frame.psdu.size() >= 28 &&
((rt::little_u16(frame.psdu.data()) >> 2) & 3) == 2;
if (!frame.sifs_deadline && data && found != stations_.end() &&
found->second.associated && found->second.observed_power_save.value_or(false)) {
auto& station = found->second;
constexpr std::size_t max_buffered = 64;
if (station.power_save_queue.size() == max_buffered) {
track_http_dispatch(frame, false, now); // Retry after an actual queue drop.
publish("ps_buffer_full", "\"station\":" + rt::quote(rt::mac_text(station.mac)));
continue; // Storage bound, not an airtime/rate limit.
}
track_http_dispatch(frame, true, now);
station.power_save_queue.push_back(std::move(frame));
publish("ps_buffered", "\"station\":" + rt::quote(rt::mac_text(station.mac)) +
",\"frames\":" + std::to_string(station.power_save_queue.size()));
} else {
track_http_dispatch(frame, false, now);
ready.push_back(std::move(frame));
}
}
return ready;
}
static bool valid_fcs(const std::vector<std::uint8_t>& psdu) {
if (psdu.size() < 4) return false;
const auto offset = psdu.size() - 4;
const std::uint32_t received =
static_cast<std::uint32_t>(psdu[offset]) |
(static_cast<std::uint32_t>(psdu[offset + 1]) << 8) |
(static_cast<std::uint32_t>(psdu[offset + 2]) << 16) |
(static_cast<std::uint32_t>(psdu[offset + 3]) << 24);
return rt::crc32_80211(psdu.data(), offset) == received;
}
std::uint16_t next_sequence() {
return static_cast<std::uint16_t>(
sequence_.fetch_add(1, std::memory_order_relaxed) & 0x0fffu);
}
void publish(std::string_view kind, std::string fields) const {
if (event_) event_(kind, fields);
}
Station* touch_station(const Mac& mac, double power_dbfs) {
auto found = stations_.find(mac);
if (found == stations_.end()) {
if (stations_.size() >= config_.max_stations) {
publish("station_rejected", "\"station\":" +
rt::quote(rt::mac_text(mac)) +
",\"reason\":\"station table full\"");
return nullptr;
}
Station station;
station.mac = mac;
station.aid = next_aid_++;
station.lease = config_.server_ip;
station.lease[3] = static_cast<std::uint8_t>(
100 + stations_.size());
found = stations_.emplace(mac, std::move(station)).first;
publish("station_observed", "\"station\":" +
rt::quote(rt::mac_text(mac)) + ",\"aid\":" +
std::to_string(found->second.aid) +
",\"lease\":" + rt::quote(ip_text(found->second.lease)));
}
found->second.last_power_dbfs = power_dbfs;
++found->second.received_frames;
return &found->second;
}
static std::optional<std::string> ssid_ie(
const std::vector<std::uint8_t>& psdu, std::size_t offset) {
const std::size_t end = psdu.size() >= 4 ? psdu.size() - 4 : 0;
while (offset + 2 <= end) {
const auto id = psdu[offset];
const auto length = static_cast<std::size_t>(psdu[offset + 1]);
offset += 2;
if (offset + length > end) return std::nullopt;
if (id == 0)
return std::string(psdu.begin() +
static_cast<std::ptrdiff_t>(offset),
psdu.begin() + static_cast<std::ptrdiff_t>(
offset + length));
offset += length;
}
return std::nullopt;
}
static bool has_rsn_ccmp_psk(const std::vector<std::uint8_t>& psdu,
std::size_t offset) {
const std::size_t end = psdu.size() >= 4 ? psdu.size() - 4 : 0;
while (offset + 2 <= end) {
const auto id = psdu[offset];
const auto length = static_cast<std::size_t>(psdu[offset + 1]);
offset += 2;
if (offset + length > end) return false;
if (id == 48 && length >= 18) {
const auto* value = psdu.data() + offset;
if (rt::little_u16(value) != 1 ||
std::memcmp(value + 2, "\x00\x0f\xac\x04", 4) != 0)
return false;
const auto pairwise_count = rt::little_u16(value + 6);
std::size_t cursor = 8;
bool ccmp = false;
for (std::uint16_t index = 0; index < pairwise_count; ++index) {
if (cursor + 4 > length) return false;
ccmp |= std::memcmp(value + cursor,
"\x00\x0f\xac\x04", 4) == 0;
cursor += 4;
}
if (cursor + 2 > length) return false;
const auto akm_count = rt::little_u16(value + cursor);
cursor += 2;
bool psk = false;
for (std::uint16_t index = 0; index < akm_count; ++index) {
if (cursor + 4 > length) return false;
psk |= std::memcmp(value + cursor,
"\x00\x0f\xac\x02", 4) == 0;
cursor += 4;
}
return ccmp && psk;
}
offset += length;
}
return false;
}
Outbound make_ack(const Mac& receiver) const {
std::vector<std::uint8_t> frame;
rt::append_le16(frame, 0x00d4);
rt::append_le16(frame, 0);
rt::append_mac(frame, receiver);
rt::append_fcs(frame);
return {"ack", "MAC ACK requires 10 us SIFS", receiver,
std::move(frame), true, 1};
}
Outbound make_cts(const Mac& receiver,
std::uint16_t rts_duration_us) const {
std::vector<std::uint8_t> frame;
rt::append_le16(frame, 0x00c4);
// Long-preamble 1 Mb/s CTS airtime is 304 us. Remove it and SIFS
// from the RTS reservation as required for the CTS Duration field.
constexpr std::uint16_t kCtsPlusSifsUs = 314;
rt::append_le16(frame, rts_duration_us > kCtsPlusSifsUs
? static_cast<std::uint16_t>(rts_duration_us - kCtsPlusSifsUs)
: 0);
rt::append_mac(frame, receiver);
rt::append_fcs(frame);
return {"cts", "CTS requires 10 us SIFS", receiver,
std::move(frame), true, 8};
}
Outbound make_probe_response(const Mac& station,
std::uint64_t timestamp_us) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, 0x0050, station, config_.bssid,
config_.bssid, next_sequence());
append_le64(frame, timestamp_us);
rt::append_le16(frame, config_.beacon_interval_tu);
rt::append_le16(frame, 0x0011);
append_ap_ies(frame, config_.ssid, config_.channel,
config_.dsss_1mbps_only);
const auto rsn = rsn_ie();
frame.insert(frame.end(), rsn.begin(), rsn.end());
rt::append_fcs(frame);
return {"probe_response", "matching probe request", station,
std::move(frame), false, 1};
}
Outbound make_authentication_response(const Mac& station,
std::uint16_t status) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, 0x00b0, station, config_.bssid,
config_.bssid, next_sequence());
rt::append_le16(frame, 0);
rt::append_le16(frame, 2);
rt::append_le16(frame, status);
rt::append_fcs(frame);
return {"authentication_response", "open-system transaction 2",
station, std::move(frame), false, 1};
}
Outbound make_association_response(const Mac& station,
std::uint16_t aid,
std::uint16_t status,
bool reassociation) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame,
reassociation ? 0x0030 : 0x0010, station, config_.bssid,
config_.bssid, next_sequence());
rt::append_le16(frame, 0x0011);
rt::append_le16(frame, status);
rt::append_le16(frame, static_cast<std::uint16_t>(0xc000u | aid));
append_rate_ies(frame, config_.dsss_1mbps_only);
const auto rsn = rsn_ie();
frame.insert(frame.end(), rsn.begin(), rsn.end());
rt::append_fcs(frame);
return {reassociation ? "reassociation_response" :
"association_response",
status == 0 ? "station admitted" : "station rejected",
station, std::move(frame), false, 1};
}
Outbound make_four_way_m1(Station& station) {
const auto eapol = make_eapol_key(
0x008a, station.m1_replay, station.anonce, {});
return {"wpa2_m1", "WPA2 four-way handshake message 1",
station.mac,
make_unprotected_data_frame(station.mac, 0x888e, eapol),
false, 1};
}
Outbound begin_four_way_handshake(
Station& station, rt::Clock::time_point now) {
station.power_save_queue.clear();
station.observed_power_save = false;
station.tcp.clear();
station.anonce = random_array<32>();
station.snonce.fill(0);
station.ptk.fill(0);
station.ptk_valid = false;
station.handshake_complete = false;
station.tx_packet_number = 1;
station.rx_packet_number.fill(0);
station.m1_replay = replay_counter_.fetch_add(
2, std::memory_order_relaxed);
station.handshake_replay = station.m1_replay + 1;
station.handshake_wait = 2;
station.handshake_retry_count = 0;
station.last_handshake_tx = now;
publish("wpa2_m1", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"replay\":" + std::to_string(station.m1_replay));
return make_four_way_m1(station);
}
Outbound make_four_way_m3(Station& station) {
if (!station.ptk_valid)
throw std::runtime_error("cannot construct M3 without a PTK");
std::vector<std::uint8_t> key_data = rsn_ie();
key_data.insert(key_data.end(), {0xdd, 22, 0x00, 0x0f, 0xac, 1,
1, 0});
key_data.insert(key_data.end(), gtk_.begin(), gtk_.end());
if (key_data.size() < 16 || (key_data.size() & 7u) != 0) {
key_data.push_back(0xdd);
while ((key_data.size() & 7u) != 0) key_data.push_back(0);
}
wifi::Wpa2Key kck{};
wifi::Wpa2Key kek{};
std::copy_n(station.ptk.begin(), 16, kck.begin());
std::copy_n(station.ptk.begin() + 16, 16, kek.begin());
const auto wrapped = wifi::wpa2_aes_key_wrap(kek, key_data);
if (!wrapped) throw std::runtime_error("WPA2 GTK key wrap failed");
const auto eapol = make_eapol_key(
0x13ca, station.handshake_replay, station.anonce, *wrapped, kck);
return {"wpa2_m3", "WPA2 four-way handshake message 3",
station.mac,
make_unprotected_data_frame(station.mac, 0x888e, eapol),
false, 1};
}
void process_eapol(const std::uint8_t* eapol, std::size_t size,
Station& station, std::vector<Outbound>& output) {
const auto key = wifi::wpa2_parse_eapol_key(eapol, size);
if (!key || key->descriptor_version != 2 || !key->pairwise ||
!key->mic || key->ack) return;
const bool nonce_present = std::any_of(
key->nonce.begin(), key->nonce.end(),
[](std::uint8_t value) { return value != 0; });
if (!key->secure && nonce_present &&
key->replay_counter == station.m1_replay) {
const auto candidate = wifi::wpa2_derive_ptk(
pmk_, config_.bssid, station.mac, station.anonce, key->nonce);
if (!wifi::wpa2_eapol_mic_valid(*key, candidate)) {
publish("wpa2_m2_rejected", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"reason\":\"MIC verification failed\"");
return;
}
station.snonce = key->nonce;
station.ptk = candidate;
station.ptk_valid = true;
const auto now = rt::Clock::now();
station.handshake_wait = 4;
station.handshake_retry_count = 0;
station.last_handshake_tx = now;
// A repeated valid M2 is a request to retransmit M3, not traffic
// to suppress. The EAPOL replay counter and PTK remain unchanged.
output.push_back(make_four_way_m3(station));
publish("wpa2_m2_verified", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"replay\":" +
std::to_string(key->replay_counter));
return;
}
if (key->secure && station.ptk_valid &&
key->replay_counter == station.handshake_replay &&
wifi::wpa2_eapol_mic_valid(*key, station.ptk)) {
station.handshake_complete = true;
station.handshake_wait = 0;
station.handshake_retry_count = 0;
publish("wpa2_m4_verified", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"ptk_installed\":true,\"cipher\":\"CCMP-128\"");
}
}
void process_management(const std::vector<std::uint8_t>& psdu,
int subtype, const Mac& destination,
const Mac& source, const Mac& bssid,
double power_dbfs,
std::vector<Outbound>& output) {
if (source == config_.bssid) return;
if (subtype == 4) { // Probe request.
const auto requested = ssid_ie(psdu, 24);
if (!requested || (!requested->empty() &&
*requested != config_.ssid)) return;
const auto timestamp_us = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
rt::Clock::now().time_since_epoch()).count());
output.push_back(make_probe_response(source, timestamp_us));
publish("probe_request", "\"station\":" +
rt::quote(rt::mac_text(source)) + ",\"ssid\":" +
rt::quote(*requested));
return;
}
if (destination != config_.bssid || bssid != config_.bssid) return;
Station* station = touch_station(source, power_dbfs);
if (!station) return;
if (subtype == 11 && psdu.size() >= 34) {
const auto algorithm = rt::little_u16(psdu.data() + 24);
const auto transaction = rt::little_u16(psdu.data() + 26);
const std::uint16_t status =
algorithm == 0 && transaction == 1 ? 0 : 13;
station->authenticated = status == 0;
if (status != 0) station->associated = false;
// A station repeats the request precisely when our previous
// response was lost. Always answer it again.
output.push_back(make_authentication_response(source, status));
publish("authentication", "\"station\":" +
rt::quote(rt::mac_text(source)) + ",\"algorithm\":" +
std::to_string(algorithm) + ",\"transaction\":" +
std::to_string(transaction) + ",\"status\":" +
std::to_string(status));
return;
}
if ((subtype == 0 || subtype == 2) && psdu.size() >= 32) {
const auto request_sequence = static_cast<std::uint16_t>(
rt::little_u16(psdu.data() + 22) >> 4);
const bool reassociation = subtype == 2;
const auto ie_offset = reassociation ? 34u : 28u;
const auto requested = ssid_ie(psdu, ie_offset);
const std::uint16_t status =
station->authenticated && requested &&
*requested == config_.ssid &&
has_rsn_ccmp_psk(psdu, ie_offset) ? 0 : 13;
station->associated = status == 0;
const auto now = rt::Clock::now();
const bool recent_duplicate =
station->last_assoc_sequence == request_sequence &&
station->last_assoc_reply.time_since_epoch().count() != 0 &&
now - station->last_assoc_reply < std::chrono::milliseconds(200);
output.push_back(make_association_response(
source, station->aid, status, reassociation));
station->last_assoc_sequence = request_sequence;
station->last_assoc_reply = now;
publish("association", "\"station\":" +
rt::quote(rt::mac_text(source)) + ",\"aid\":" +
std::to_string(station->aid) + ",\"status\":" +
std::to_string(status) + ",\"ssid\":" +
rt::quote(requested.value_or("")));
if (status == 0) {
if (!recent_duplicate) {
output.push_back(begin_four_way_handshake(*station, now));
} else if (!station->handshake_complete &&
station->handshake_wait == 2) {
output.push_back(make_four_way_m1(*station));
station->last_handshake_tx = now;
} else if (!station->handshake_complete &&
station->handshake_wait == 4 &&
station->ptk_valid) {
output.push_back(make_four_way_m3(*station));
station->last_handshake_tx = now;
}
}
return;
}
if (subtype == 10 || subtype == 12) {
station->associated = false;
if (subtype == 12) station->authenticated = false;
station->ptk_valid = false;
station->handshake_complete = false;
station->handshake_wait = 0;
station->handshake_retry_count = 0;
station->tcp.clear();
station->power_save_queue.clear();
station->observed_power_save = false;
publish(subtype == 12 ? "deauthentication" : "disassociation",
"\"station\":" + rt::quote(rt::mac_text(source)));
}
}
std::vector<std::uint8_t> make_unprotected_data_frame(
const Mac& station, std::uint16_t ether_type,
const std::vector<std::uint8_t>& payload) {
std::vector<std::uint8_t> frame;
frame.reserve(36 + payload.size());
rt::append_le16(frame, 0x0208); // Data, From DS.
rt::append_le16(frame, 0);
rt::append_mac(frame, station);
rt::append_mac(frame, config_.bssid);
rt::append_mac(frame, config_.bssid);
rt::append_le16(frame,
static_cast<std::uint16_t>(next_sequence() << 4));
frame.insert(frame.end(), {0xaa, 0xaa, 0x03, 0x00, 0x00, 0x00});
rt::append_be16(frame, ether_type);
frame.insert(frame.end(), payload.begin(), payload.end());
rt::append_fcs(frame);
return frame;
}
std::vector<std::uint8_t> make_data_frame(
Station& station, std::uint16_t ether_type,
const std::vector<std::uint8_t>& payload) {
if (!station.handshake_complete)
throw std::runtime_error(
"refusing protected data before WPA2 handshake completion");
wifi::Wpa2Key temporal_key{};
std::copy_n(station.ptk.begin() + 32, temporal_key.size(),
temporal_key.begin());
const auto plain = make_unprotected_data_frame(
station.mac, ether_type, payload);
return ccmp_encrypt_frame(plain, temporal_key,
station.tx_packet_number++);
}
std::vector<std::uint8_t> ipv4_packet(
const Ipv4& source, const Ipv4& destination, std::uint8_t protocol,
const std::vector<std::uint8_t>& payload) {
std::vector<std::uint8_t> packet(20, 0);
packet[0] = 0x45;
const auto total = static_cast<std::uint16_t>(20 + payload.size());
packet[2] = static_cast<std::uint8_t>(total >> 8);
packet[3] = static_cast<std::uint8_t>(total);
const auto id = ip_identifier_.fetch_add(1, std::memory_order_relaxed);
packet[4] = static_cast<std::uint8_t>(id >> 8);
packet[5] = static_cast<std::uint8_t>(id);
packet[6] = 0x40;
packet[8] = 64;
packet[9] = protocol;
std::copy(source.begin(), source.end(), packet.begin() + 12);
std::copy(destination.begin(), destination.end(), packet.begin() + 16);
const auto checksum = rt::internet_checksum(packet.data(), packet.size());
packet[10] = static_cast<std::uint8_t>(checksum >> 8);
packet[11] = static_cast<std::uint8_t>(checksum);
packet.insert(packet.end(), payload.begin(), payload.end());
return packet;
}
std::vector<std::uint8_t> udp_packet(
const Ipv4& source_ip, const Ipv4& destination_ip,
std::uint16_t source_port, std::uint16_t destination_port,
const std::vector<std::uint8_t>& payload) {
std::vector<std::uint8_t> udp;
const auto length = static_cast<std::uint16_t>(8 + payload.size());
rt::append_be16(udp, source_port);
rt::append_be16(udp, destination_port);
rt::append_be16(udp, length);
rt::append_be16(udp, 0);
udp.insert(udp.end(), payload.begin(), payload.end());
std::vector<std::uint8_t> pseudo;
pseudo.insert(pseudo.end(), source_ip.begin(), source_ip.end());
pseudo.insert(pseudo.end(), destination_ip.begin(), destination_ip.end());
pseudo.push_back(0);
pseudo.push_back(17);
rt::append_be16(pseudo, length);
pseudo.insert(pseudo.end(), udp.begin(), udp.end());
auto checksum = rt::internet_checksum(pseudo.data(), pseudo.size());
if (checksum == 0) checksum = 0xffff;
udp[6] = static_cast<std::uint8_t>(checksum >> 8);
udp[7] = static_cast<std::uint8_t>(checksum);
return ipv4_packet(source_ip, destination_ip, 17, udp);
}
std::vector<std::uint8_t> tcp_packet(
const Ipv4& source_ip, const Ipv4& destination_ip,
std::uint16_t source_port, std::uint16_t destination_port,
std::uint32_t sequence, std::uint32_t acknowledgment,
std::uint8_t flags, const std::vector<std::uint8_t>& payload,
bool syn_options = false) {
std::vector<std::uint8_t> tcp(20, 0);
tcp[0] = static_cast<std::uint8_t>(source_port >> 8);
tcp[1] = static_cast<std::uint8_t>(source_port);
tcp[2] = static_cast<std::uint8_t>(destination_port >> 8);
tcp[3] = static_cast<std::uint8_t>(destination_port);
tcp[4] = static_cast<std::uint8_t>(sequence >> 24);
tcp[5] = static_cast<std::uint8_t>(sequence >> 16);
tcp[6] = static_cast<std::uint8_t>(sequence >> 8);
tcp[7] = static_cast<std::uint8_t>(sequence);
tcp[8] = static_cast<std::uint8_t>(acknowledgment >> 24);
tcp[9] = static_cast<std::uint8_t>(acknowledgment >> 16);
tcp[10] = static_cast<std::uint8_t>(acknowledgment >> 8);
tcp[11] = static_cast<std::uint8_t>(acknowledgment);
tcp[12] = 0x50;
tcp[13] = flags;
tcp[14] = 0x10;
tcp[15] = 0x00;
if (syn_options) {
tcp.insert(tcp.end(), {2, 4, 0x04, 0xb0}); // MSS 1200.
tcp[12] = 0x60;
}
tcp.insert(tcp.end(), payload.begin(), payload.end());
std::vector<std::uint8_t> pseudo;
pseudo.insert(pseudo.end(), source_ip.begin(), source_ip.end());
pseudo.insert(pseudo.end(), destination_ip.begin(), destination_ip.end());
pseudo.push_back(0);
pseudo.push_back(6);
rt::append_be16(pseudo, static_cast<std::uint16_t>(tcp.size()));
pseudo.insert(pseudo.end(), tcp.begin(), tcp.end());
const auto checksum = rt::internet_checksum(pseudo.data(), pseudo.size());
tcp[16] = static_cast<std::uint8_t>(checksum >> 8);
tcp[17] = static_cast<std::uint8_t>(checksum);
return ipv4_packet(source_ip, destination_ip, 6, tcp);
}
std::vector<std::uint8_t> dhcp_reply(
const Station& station, std::uint32_t transaction,
std::uint16_t flags, std::uint8_t message_type) {
std::vector<std::uint8_t> bootp(240, 0);
bootp[0] = 2;
bootp[1] = 1;
bootp[2] = 6;
bootp[4] = static_cast<std::uint8_t>(transaction >> 24);
bootp[5] = static_cast<std::uint8_t>(transaction >> 16);
bootp[6] = static_cast<std::uint8_t>(transaction >> 8);
bootp[7] = static_cast<std::uint8_t>(transaction);
bootp[10] = static_cast<std::uint8_t>(flags >> 8);
bootp[11] = static_cast<std::uint8_t>(flags);
std::copy(station.lease.begin(), station.lease.end(), bootp.begin() + 16);
std::copy(config_.server_ip.begin(), config_.server_ip.end(),
bootp.begin() + 20);
std::copy(station.mac.begin(), station.mac.end(), bootp.begin() + 28);
bootp[236] = 99;
bootp[237] = 130;
bootp[238] = 83;
bootp[239] = 99;
const Ipv4 mask = {255, 255, 255, 0};
const Ipv4 broadcast = {config_.server_ip[0], config_.server_ip[1],
config_.server_ip[2], 255};
bootp.insert(bootp.end(), {53, 1, message_type, 54, 4});
bootp.insert(bootp.end(), config_.server_ip.begin(),
config_.server_ip.end());
bootp.insert(bootp.end(), {51, 4, 0, 0, 0x0e, 0x10, 1, 4});
bootp.insert(bootp.end(), mask.begin(), mask.end());
bootp.insert(bootp.end(), {3, 4});
bootp.insert(bootp.end(), config_.server_ip.begin(),
config_.server_ip.end());
bootp.insert(bootp.end(), {6, 4});
bootp.insert(bootp.end(), config_.server_ip.begin(),
config_.server_ip.end());
bootp.insert(bootp.end(), {28, 4});
bootp.insert(bootp.end(), broadcast.begin(), broadcast.end());
bootp.insert(bootp.end(), {58, 4, 0, 0, 0x07, 0x08,
59, 4, 0, 0, 0x0c, 0x4e, 255});
return bootp;
}
struct DhcpRequest {
std::uint8_t type = 0;
std::uint32_t transaction = 0;
std::uint16_t flags = 0;
std::optional<Ipv4> requested;
std::optional<Ipv4> server;
};
static std::optional<DhcpRequest> parse_dhcp(
const std::uint8_t* data, std::size_t size) {
if (size < 240 || data[0] != 1 || data[1] != 1 || data[2] != 6 ||
data[236] != 99 || data[237] != 130 || data[238] != 83 ||
data[239] != 99) return std::nullopt;
DhcpRequest request;
request.transaction = read_be32(data + 4);
request.flags = read_be16(data + 10);
std::size_t offset = 240;
while (offset < size) {
const auto code = data[offset++];
if (code == 255) break;
if (code == 0) continue;
if (offset >= size) return std::nullopt;
const auto length = static_cast<std::size_t>(data[offset++]);
if (offset + length > size) return std::nullopt;
if (code == 53 && length == 1) request.type = data[offset];
if ((code == 50 || code == 54) && length == 4) {
Ipv4 ip{};
std::copy_n(data + offset, 4, ip.begin());
if (code == 50) request.requested = ip;
else request.server = ip;
}
offset += length;
}
return request.type == 0 ? std::nullopt :
std::optional<DhcpRequest>(request);
}
void process_dhcp(const std::uint8_t* payload, std::size_t size,
Station& station, std::vector<Outbound>& output) {
const auto request = parse_dhcp(payload, size);
if (!request) return;
if (request->server && *request->server != config_.server_ip) return;
std::uint8_t reply_type = 0;
std::string kind;
if (request->type == 1) {
reply_type = 2;
kind = "dhcp_offer";
} else if (request->type == 3 || request->type == 8) {
reply_type = 5;
kind = "dhcp_ack";
} else if (request->type == 7) {
station.tcp.clear();
publish("dhcp_release", "\"station\":" +
rt::quote(rt::mac_text(station.mac)));
return;
} else {
return;
}
const Ipv4 broadcast = {255, 255, 255, 255};
const auto bootp = dhcp_reply(station, request->transaction,
request->flags, reply_type);
const auto ip = udp_packet(config_.server_ip, broadcast, 67, 68, bootp);
output.push_back({kind, "DHCP transaction " +
std::to_string(request->transaction), station.mac,
make_data_frame(station, 0x0800, ip), false, 2});
publish(kind, "\"station\":" + rt::quote(rt::mac_text(station.mac)) +
",\"lease\":" + rt::quote(ip_text(station.lease)) +
",\"transaction\":" +
std::to_string(request->transaction));
}
void process_arp(const std::uint8_t* arp, std::size_t size,
Station& station, std::vector<Outbound>& output) {
if (size < 28 || read_be16(arp) != 1 || read_be16(arp + 2) != 0x0800 ||
arp[4] != 6 || arp[5] != 4 || read_be16(arp + 6) != 1) return;
Ipv4 sender_ip{};
Ipv4 target_ip{};
std::copy_n(arp + 14, 4, sender_ip.begin());
std::copy_n(arp + 24, 4, target_ip.begin());
if (target_ip != config_.server_ip) return;
std::vector<std::uint8_t> reply;
rt::append_be16(reply, 1);
rt::append_be16(reply, 0x0800);
reply.push_back(6);
reply.push_back(4);
rt::append_be16(reply, 2);
rt::append_mac(reply, config_.bssid);
reply.insert(reply.end(), config_.server_ip.begin(),
config_.server_ip.end());
reply.insert(reply.end(), arp + 8, arp + 14);
reply.insert(reply.end(), sender_ip.begin(), sender_ip.end());
output.push_back({"arp_reply", "virtual AP IPv4 address", station.mac,
make_data_frame(station, 0x0806, reply), false, 2});
publish("arp_reply", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) + ",\"target_ip\":" +
rt::quote(ip_text(config_.server_ip)));
}
void process_dns(const std::uint8_t* payload, std::size_t size,
const Ipv4& client_ip, std::uint16_t client_port,
Station& station, std::vector<Outbound>& output) {
if (size < 12 || read_be16(payload + 4) == 0) return;
std::size_t end = 12;
while (end < size && payload[end] != 0) {
const std::size_t label = payload[end];
if (label == 0 || end + 1 + label > size) return;
end += 1 + label;
}
if (end + 5 > size) return;
end += 5;
std::vector<std::uint8_t> dns;
dns.insert(dns.end(), payload, payload + 2);
dns.insert(dns.end(), {0x81, 0x80, 0, 1, 0, 1, 0, 0, 0, 0});
dns.insert(dns.end(), payload + 12, payload + end);
dns.insert(dns.end(), {0xc0, 0x0c, 0, 1, 0, 1,
0, 0, 0, 30, 0, 4});
dns.insert(dns.end(), config_.server_ip.begin(), config_.server_ip.end());
const auto ip = udp_packet(config_.server_ip, client_ip, 53,
client_port, dns);
output.push_back({"dns_response", "captive lab A record", station.mac,
make_data_frame(station, 0x0800, ip), false, 1});
}
std::string http_response(bool head) const {
const std::string body = head ? std::string{} : config_.page;
return "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n"
"Cache-Control: no-store\r\nConnection: close\r\nContent-Length: " +
std::to_string(config_.page.size()) + "\r\n\r\n" + body;
}
Outbound make_http_retransmit(Station& station, TcpConnection& connection,
std::uint16_t port) {
const auto skip = std::min<std::size_t>(connection.response_acked,
connection.last_response.size());
const std::vector<std::uint8_t> remaining(connection.last_response.begin() + skip,
connection.last_response.end());
const auto segment = tcp_packet(config_.server_ip, connection.client_ip, 80, port,
connection.last_response_sequence + connection.response_acked,
connection.client_next, remaining.empty() ? 0x11 : 0x19, remaining);
// Same TCP sequence/data; fresh CCMP PN and MAC sequence on every retry.
return {"http_retransmit", "unacknowledged HTTP bytes/FIN", station.mac,
make_data_frame(station, 0x0800, segment), false, 1, port};
}
void emit_http_response(Station& station, TcpConnection& connection,
std::uint16_t port, std::vector<Outbound>& output,
bool delayed = false, rt::Clock::time_point now = rt::Clock::now()) {
// Encrypt only when releasing the response, so the CCMP PN cannot
// become older than other packets sent during the diagnostic delay.
const auto segment = tcp_packet(
config_.server_ip, connection.client_ip, 80, port,
connection.last_response_sequence, connection.client_next, 0x19,
connection.last_response);
connection.server_next = connection.last_response_sequence +
static_cast<std::uint32_t>(connection.last_response.size()) + 1;
connection.closed = true;
connection.deferred_http = false;
connection.response_pending = true;
connection.response_queued = false;
connection.response_acked = 0;
connection.response_retries = 0;
connection.response_rto = std::chrono::milliseconds(1000);
connection.response_retry_at = now + connection.response_rto;
if (connection.request_headers.starts_with("GET /retry-test HTTP/")) {
// Explicit loss-test URL only: ACK the request, withhold this first
// response from RF, then require the real TCP recovery path.
publish("http_test_drop", "\"station\":" + rt::quote(rt::mac_text(station.mac)) +
",\"client_port\":" + std::to_string(port) +
",\"first_response_transmitted\":false");
} else {
output.push_back({"http_response", "HTTP/1.1 200 RF page", station.mac,
make_data_frame(station, 0x0800, segment), false, 3, port});
}
publish("http_request", "\"station\":" + rt::quote(rt::mac_text(station.mac)) +
",\"client_port\":" + std::to_string(port) +
",\"request_bytes\":" + std::to_string(connection.request_headers.size()) +
",\"request_segments\":" + std::to_string(connection.request_segments) +
",\"response_bytes\":" + std::to_string(connection.last_response.size()) +
",\"diagnostic_delay_ms\":" + std::to_string(delayed ? 1500 : 0) +
",\"station_pm_observed\":" + (station.observed_power_save
? (*station.observed_power_save ? "true" : "false") : "null"));
}
void process_tcp(const std::uint8_t* tcp, std::size_t size,
const Ipv4& client_ip, Station& station,
std::vector<Outbound>& output) {
if (size < 20) return;
const auto source_port = read_be16(tcp);
const auto destination_port = read_be16(tcp + 2);
if (destination_port != 80) return;
const auto sequence = read_be32(tcp + 4);
const auto acknowledgment = read_be32(tcp + 8);
const std::size_t header_bytes = (tcp[12] >> 4) * 4u;
if (header_bytes < 20 || header_bytes > size) return;
const auto flags = tcp[13];
const auto* payload = tcp + header_bytes;
const auto payload_size = size - header_bytes;
auto& connection = station.tcp[source_port];
if ((flags & 0x02u) != 0) {
if (!connection.syn_seen) {
connection.syn_seen = true;
connection.client_ip = client_ip;
connection.client_next = sequence + 1;
connection.server_isn = 0x47500000u ^
(static_cast<std::uint32_t>(station.aid) << 16) ^ source_port;
connection.server_next = connection.server_isn + 1;
}
const auto segment = tcp_packet(
config_.server_ip, client_ip, 80, source_port,
connection.server_isn, connection.client_next, 0x12, {}, true);
output.push_back({"tcp_syn_ack", "RF-only HTTP port 80", station.mac,
make_data_frame(station, 0x0800, segment),
false, 2});
publish("tcp_syn", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"client_port\":" + std::to_string(source_port));
return;
}
if (!connection.syn_seen) return;
if ((flags & 0x04u) != 0) {
if (sequence == connection.client_next) {
connection.deferred_http = false;
connection.response_pending = false;
connection.response_queued = false;
discard_queued_http(station, source_port);
connection.closed = true;
connection.last_response.clear();
}
return;
}
if ((flags & 0x10u) != 0 && acknowledgment == connection.server_next)
connection.established = true;
if ((flags & 0x10u) != 0 && connection.response_pending) {
const auto acknowledged = acknowledgment - connection.last_response_sequence;
const auto total = static_cast<std::uint32_t>(connection.last_response.size()) + 1;
// Unsigned offset rejects older/future ACKs, including sequence wrap.
if (acknowledged > connection.response_acked && acknowledged <= total) {
connection.response_acked = acknowledged;
connection.response_pending = acknowledged != total;
connection.response_queued = false;
discard_queued_http(station, source_port);
connection.response_retry_at = rt::Clock::now() + connection.response_rto;
publish("tcp_response_ack", "\"station\":" + rt::quote(rt::mac_text(station.mac)) +
",\"client_port\":" + std::to_string(source_port) +
",\"acknowledged_bytes_and_fin\":" + std::to_string(acknowledged) +
",\"complete\":" + (connection.response_pending ? "false" : "true"));
}
}
if (payload_size != 0) {
const auto acknowledge = [&] {
const auto segment = tcp_packet(
config_.server_ip, client_ip, 80, source_port,
connection.server_next, connection.client_next, 0x10, {});
output.push_back({"tcp_ack", "cumulative stream acknowledgment",
station.mac, make_data_frame(station, 0x0800, segment), false, 1});
};
if (sequence == connection.last_client_payload_sequence &&
!connection.last_response.empty()) {
if (connection.deferred_http) {
acknowledge();
return;
}
if (connection.response_pending) {
discard_queued_http(station, source_port);
output.push_back(make_http_retransmit(station, connection, source_port));
return;
}
const auto segment = tcp_packet(
config_.server_ip, client_ip, 80, source_port,
connection.last_response_sequence, connection.client_next,
0x19, connection.last_response);
output.push_back({"http_response", "retransmitted HTTP response",
station.mac,
make_data_frame(station, 0x0800, segment), false, 2});
return;
}
// Accept only contiguous bytes. Unsigned serial-number arithmetic
// handles wraparound; gaps are duplicate-ACKed, not buffered. Trim
// already received prefixes when a retransmission overlaps new data.
const auto ahead = sequence - connection.client_next;
if (connection.closed || (ahead != 0 && ahead < 0x80000000u)) {
acknowledge();
return;
}
const auto skip = static_cast<std::size_t>(connection.client_next - sequence);
if (skip >= payload_size) {
acknowledge();
return;
}
const auto new_bytes = payload_size - skip;
constexpr std::size_t kMaxRequestHeaders = 8192;
if (new_bytes > kMaxRequestHeaders - connection.request_headers.size()) {
const auto segment = tcp_packet(
config_.server_ip, client_ip, 80, source_port,
connection.server_next, connection.client_next, 0x14, {});
output.push_back({"tcp_reset", "HTTP headers exceed 8192 bytes",
station.mac, make_data_frame(station, 0x0800, segment), false, 1});
connection.closed = true;
connection.request_headers.clear();
return;
}
connection.request_headers.append(
reinterpret_cast<const char*>(payload + skip), new_bytes);
connection.client_next += static_cast<std::uint32_t>(new_bytes);
++connection.request_segments;
if (connection.request_headers.find("\r\n\r\n") == std::string::npos) {
acknowledge();
return;
}
{
const std::string_view request(connection.request_headers);
const bool get = request.starts_with("GET ");
const bool head = request.starts_with("HEAD ");
if (!get && !head) {
acknowledge();
return;
}
connection.last_client_payload_sequence = sequence;
const auto text = http_response(head);
connection.last_response.assign(text.begin(), text.end());
connection.last_response_sequence = connection.server_next;
if (request.starts_with("GET /sleep-test HTTP/")) {
// Explicit diagnostic route only. Normal HTTP is unchanged;
// the event loop, other stations and beacons never sleep.
connection.deferred_http = true;
connection.deferred_http_at = rt::Clock::now() +
std::chrono::milliseconds(1500);
acknowledge();
publish("http_response_deferred", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) +
",\"client_port\":" + std::to_string(source_port) +
",\"delay_ms\":1500");
return;
}
if (request.starts_with("GET /retry-test HTTP/")) acknowledge();
emit_http_response(station, connection, source_port, output);
return;
}
}
if ((flags & 0x01u) != 0) {
connection.deferred_http = false;
if (sequence == connection.client_next) ++connection.client_next;
const auto segment = tcp_packet(
config_.server_ip, client_ip, 80, source_port,
connection.server_next, connection.client_next, 0x11, {});
++connection.server_next;
output.push_back({"tcp_fin_ack", "client close", station.mac,
make_data_frame(station, 0x0800, segment),
false, 1});
}
}
void process_ipv4(const std::uint8_t* ip, std::size_t size,
Station& station, std::vector<Outbound>& output) {
if (size < 20 || (ip[0] >> 4) != 4) return;
const std::size_t header_bytes = (ip[0] & 0xfu) * 4u;
const auto total = static_cast<std::size_t>(read_be16(ip + 2));
if (header_bytes < 20 || total < header_bytes || total > size) return;
Ipv4 source{};
Ipv4 destination{};
std::copy_n(ip + 12, 4, source.begin());
std::copy_n(ip + 16, 4, destination.begin());
const auto* transport = ip + header_bytes;
const auto transport_size = total - header_bytes;
publish("ipv4_rx", "\"station\":" +
rt::quote(rt::mac_text(station.mac)) + ",\"source_ip\":" +
rt::quote(ip_text(source)) + ",\"destination_ip\":" +
rt::quote(ip_text(destination)) + ",\"protocol\":" +
std::to_string(ip[9]) + ",\"header_bytes\":" +
std::to_string(header_bytes) + ",\"total_bytes\":" +
std::to_string(total));
if (ip[9] == 17 && transport_size >= 8) {
const auto source_port = read_be16(transport);
const auto destination_port = read_be16(transport + 2);
const auto udp_length = static_cast<std::size_t>(
read_be16(transport + 4));
if (udp_length < 8 || udp_length > transport_size) return;
const auto* payload = transport + 8;
const auto payload_size = udp_length - 8;
if (source_port == 68 && destination_port == 67) {
process_dhcp(payload, payload_size, station, output);
} else if (destination_port == 53) {
if (source == Ipv4{}) source = station.lease;
process_dns(payload, payload_size, source, source_port,
station, output);
}
return;
}
if (ip[9] == 1 && transport_size >= 8 && transport[0] == 8 &&
destination == config_.server_ip) {
std::vector<std::uint8_t> icmp(transport,
transport + transport_size);
icmp[0] = 0;
icmp[2] = 0;
icmp[3] = 0;
const auto checksum = rt::internet_checksum(icmp.data(), icmp.size());
icmp[2] = static_cast<std::uint8_t>(checksum >> 8);
icmp[3] = static_cast<std::uint8_t>(checksum);
const auto reply = ipv4_packet(config_.server_ip, source, 1, icmp);
output.push_back({"icmp_echo_reply", "virtual AP ping", station.mac,
make_data_frame(station, 0x0800, reply),
false, 1});
return;
}
if (ip[9] == 6 && destination == config_.server_ip)
process_tcp(transport, transport_size, source, station, output);
}
void process_data(const std::vector<std::uint8_t>& psdu,
const wifi::DataLayout& layout, double power_dbfs,
std::vector<Outbound>& output) {
if (!layout.to_ds || layout.address1 != config_.bssid) return;
Station* station = touch_station(layout.address2, power_dbfs);
if (!station) return;
const auto observe_pm = [&] {
const bool asleep = (layout.frame_control & 0x1000u) != 0;
if (station->associated && station->observed_power_save != asleep) {
station->observed_power_save = asleep;
publish("station_pm_observed", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"asleep\":" + (asleep ? "true" : "false"));
if (!asleep) {
const auto count = station->power_save_queue.size();
while (!station->power_save_queue.empty()) {
auto frame = std::move(station->power_save_queue.front());
station->power_save_queue.pop_front();
set_more_data(frame, !station->power_save_queue.empty());
output.push_back(std::move(frame));
}
if (count) publish("ps_wake_flush", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) + ",\"frames\":" + std::to_string(count));
}
}
};
const auto subtype = (layout.frame_control >> 4) & 15;
if (!layout.protected_frame && (subtype == 4 || subtype == 12)) {
observe_pm();
return;
}
std::vector<std::uint8_t> decrypted;
const std::uint8_t* llc = nullptr;
std::size_t llc_size = 0;
if (layout.protected_frame) {
if (!station->ptk_valid) {
publish("ccmp_rejected", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"reason\":\"no verified PTK\"");
return;
}
wifi::Wpa2Key temporal_key{};
std::copy_n(station->ptk.begin() + 32, temporal_key.size(),
temporal_key.begin());
const auto plain = ccmp_decrypt_frame(psdu, temporal_key);
if (!plain) {
publish("ccmp_rejected", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"reason\":\"CCMP tag verification failed\"");
return;
}
if (plain->key_id != 0) {
publish("ccmp_rejected", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"reason\":\"pairwise key ID must be zero\"");
return;
}
const std::size_t replay_queue = layout.qos ? layout.tid : 16;
auto& last_pn = station->rx_packet_number.at(replay_queue);
if (plain->packet_number <= last_pn) {
publish("ccmp_rejected", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"reason\":\"replayed or zero packet number\"" +
",\"packet_number\":" + std::to_string(plain->packet_number) +
",\"last_packet_number\":" + std::to_string(last_pn) +
",\"replay_queue\":" + std::to_string(replay_queue));
return;
}
last_pn = plain->packet_number;
decrypted = plain->llc;
llc = decrypted.data();
llc_size = decrypted.size();
publish("ccmp_verified", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"packet_number\":" +
std::to_string(plain->packet_number) +
",\"replay_queue\":" + std::to_string(replay_queue) +
",\"plaintext_bytes\":" +
std::to_string(decrypted.size()));
if (!station->handshake_complete) {
// A valid CCMP tag under the freshly derived temporal key is
// stronger evidence than an unobserved M4 retransmission that
// the station received M3 and installed this PTK. Recover the
// authenticator state when M4 was obscured by our own M3 train
// instead of discarding the station's first protected DHCP
// packet. M2's KCK MIC has already been verified above.
station->handshake_complete = true;
station->handshake_wait = 0;
station->handshake_retry_count = 0;
publish("wpa2_key_confirmed_by_ccmp", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"packet_number\":" +
std::to_string(plain->packet_number) +
",\"cipher\":\"CCMP-128\"");
}
} else {
const std::size_t end = psdu.size() >= 4 ? psdu.size() - 4 : 0;
if (layout.header_bytes > end) return;
llc = psdu.data() + layout.header_bytes;
llc_size = end - layout.header_bytes;
}
observe_pm();
if (llc_size < 8) return;
if (std::memcmp(llc, "\xaa\xaa\x03\x00\x00\x00", 6) != 0) return;
const auto ether_type = read_be16(llc + 6);
const auto* payload = llc + 8;
const auto payload_size = llc_size - 8;
if (ether_type == 0x888e && !layout.protected_frame) {
process_eapol(payload, payload_size, *station, output);
return;
}
if (!station->handshake_complete || !layout.protected_frame) {
publish("data_rejected", "\"station\":" +
rt::quote(rt::mac_text(station->mac)) +
",\"reason\":\"WPA2 handshake incomplete\"");
return;
}
if (ether_type == 0x0806)
process_arp(payload, payload_size, *station, output);
else if (ether_type == 0x0800)
process_ipv4(payload, payload_size, *station, output);
}
ProtocolConfig config_;
EventHandler event_;
wifi::Wpa2Pmk pmk_{};
wifi::Wpa2Key gtk_{};
std::map<Mac, Station> stations_;
std::atomic<std::uint16_t> sequence_{1};
std::atomic<std::uint16_t> ip_identifier_{1};
std::atomic<std::uint64_t> replay_counter_{1};
std::uint16_t next_aid_ = 1;
};
} // namespace gf::ap
namespace gf::ap {
std::vector<std::uint8_t> make_wpa2_association_request(
const std::string& ssid, const Mac& station, const Mac& bssid,
int channel, std::uint16_t sequence) {
auto frame = rt::make_association_request(
ssid, station, bssid, channel, sequence);
frame.resize(frame.size() - 4);
const auto rsn = rsn_ie();
frame.insert(frame.end(), rsn.begin(), rsn.end());
rt::append_fcs(frame);
return frame;
}
std::optional<std::pair<const std::uint8_t*, std::size_t>>
eapol_from_data_frame(const std::vector<std::uint8_t>& psdu) {
const auto layout = wifi::parse_data_layout(psdu);
if (!layout || layout->protected_frame ||
psdu.size() < layout->header_bytes + 8 + 4)
return std::nullopt;
const auto* llc = psdu.data() + layout->header_bytes;
if (std::memcmp(llc, "\xaa\xaa\x03\x00\x00\x00\x88\x8e", 8) != 0)
return std::nullopt;
return std::make_pair(llc + 8,
psdu.size() - layout->header_bytes - 8 - 4);
}
std::vector<std::uint8_t> make_client_eapol_frame(
const Mac& station, const Mac& bssid,
const std::vector<std::uint8_t>& eapol, std::uint16_t sequence) {
return rt::make_data_frame(station, bssid, bssid, 0x888e, eapol,
sequence);
}
std::vector<std::uint8_t> unprotected_from_ccmp(
const std::vector<std::uint8_t>& protected_psdu,
const std::vector<std::uint8_t>& llc) {
const auto layout = wifi::parse_data_layout(protected_psdu);
if (!layout || !layout->protected_frame)
throw std::runtime_error("expected a protected MPDU");
std::vector<std::uint8_t> plain(
protected_psdu.begin(), protected_psdu.begin() +
static_cast<std::ptrdiff_t>(layout->header_bytes));
auto control = rt::little_u16(plain.data());
control &= static_cast<std::uint16_t>(~0x4000u);
plain[0] = static_cast<std::uint8_t>(control);
plain[1] = static_cast<std::uint8_t>(control >> 8);
plain.insert(plain.end(), llc.begin(), llc.end());
rt::append_fcs(plain);
return plain;
}
std::vector<std::uint8_t> make_client_ipv4_packet(
const Ipv4& source, const Ipv4& destination, std::uint8_t protocol,
const std::vector<std::uint8_t>& payload, std::uint16_t identifier) {
constexpr std::size_t header_bytes = 20;
if (payload.size() >
std::numeric_limits<std::uint16_t>::max() - header_bytes) {
throw std::runtime_error("test IPv4 payload exceeds 65535-byte packet");
}
const auto total_size = header_bytes + payload.size();
std::vector<std::uint8_t> packet(total_size, 0);
packet[0] = 0x45;
const auto total = static_cast<std::uint16_t>(total_size);
packet[2] = static_cast<std::uint8_t>(total >> 8);
packet[3] = static_cast<std::uint8_t>(total);
packet[4] = static_cast<std::uint8_t>(identifier >> 8);
packet[5] = static_cast<std::uint8_t>(identifier);
packet[6] = 0x40;
packet[8] = 64;
packet[9] = protocol;
std::copy(source.begin(), source.end(), packet.begin() + 12);
std::copy(destination.begin(), destination.end(), packet.begin() + 16);
const auto checksum = rt::internet_checksum(packet.data(), header_bytes);
packet[10] = static_cast<std::uint8_t>(checksum >> 8);
packet[11] = static_cast<std::uint8_t>(checksum);
std::copy(payload.begin(), payload.end(), packet.begin() + header_bytes);
return packet;
}
std::vector<std::uint8_t> make_client_udp_packet(
const Ipv4& source_ip, const Ipv4& destination_ip,
std::uint16_t source_port, std::uint16_t destination_port,
const std::vector<std::uint8_t>& payload, std::uint16_t identifier) {
std::vector<std::uint8_t> udp;
const auto length = static_cast<std::uint16_t>(8 + payload.size());
rt::append_be16(udp, source_port);
rt::append_be16(udp, destination_port);
rt::append_be16(udp, length);
rt::append_be16(udp, 0);
udp.insert(udp.end(), payload.begin(), payload.end());
std::vector<std::uint8_t> pseudo;
pseudo.insert(pseudo.end(), source_ip.begin(), source_ip.end());
pseudo.insert(pseudo.end(), destination_ip.begin(), destination_ip.end());
pseudo.push_back(0);
pseudo.push_back(17);
rt::append_be16(pseudo, length);
pseudo.insert(pseudo.end(), udp.begin(), udp.end());
auto checksum = rt::internet_checksum(pseudo.data(), pseudo.size());
if (checksum == 0) checksum = 0xffff;
udp[6] = static_cast<std::uint8_t>(checksum >> 8);
udp[7] = static_cast<std::uint8_t>(checksum);
return make_client_ipv4_packet(source_ip, destination_ip, 17, udp,
identifier);
}
std::vector<std::uint8_t> make_client_tcp_packet(
const Ipv4& source_ip, const Ipv4& destination_ip,
std::uint16_t source_port, std::uint16_t destination_port,
std::uint32_t sequence, std::uint32_t acknowledgment,
std::uint8_t flags, std::string_view payload, std::uint16_t identifier) {
std::vector<std::uint8_t> tcp(20, 0);
tcp[0] = static_cast<std::uint8_t>(source_port >> 8);
tcp[1] = static_cast<std::uint8_t>(source_port);
tcp[2] = static_cast<std::uint8_t>(destination_port >> 8);
tcp[3] = static_cast<std::uint8_t>(destination_port);
tcp[4] = static_cast<std::uint8_t>(sequence >> 24);
tcp[5] = static_cast<std::uint8_t>(sequence >> 16);
tcp[6] = static_cast<std::uint8_t>(sequence >> 8);
tcp[7] = static_cast<std::uint8_t>(sequence);
tcp[8] = static_cast<std::uint8_t>(acknowledgment >> 24);
tcp[9] = static_cast<std::uint8_t>(acknowledgment >> 16);
tcp[10] = static_cast<std::uint8_t>(acknowledgment >> 8);
tcp[11] = static_cast<std::uint8_t>(acknowledgment);
tcp[12] = 0x50;
tcp[13] = flags;
tcp[14] = 0x10;
tcp.insert(tcp.end(), payload.begin(), payload.end());
std::vector<std::uint8_t> pseudo;
pseudo.insert(pseudo.end(), source_ip.begin(), source_ip.end());
pseudo.insert(pseudo.end(), destination_ip.begin(), destination_ip.end());
pseudo.push_back(0);
pseudo.push_back(6);
rt::append_be16(pseudo, static_cast<std::uint16_t>(tcp.size()));
pseudo.insert(pseudo.end(), tcp.begin(), tcp.end());
const auto checksum = rt::internet_checksum(pseudo.data(), pseudo.size());
tcp[16] = static_cast<std::uint8_t>(checksum >> 8);
tcp[17] = static_cast<std::uint8_t>(checksum);
return make_client_ipv4_packet(source_ip, destination_ip, 6, tcp,
identifier);
}
std::vector<std::uint8_t> make_dhcp_discover(
const Mac& station, const Mac& bssid, std::uint32_t transaction,
std::uint16_t sequence) {
std::vector<std::uint8_t> bootp(240, 0);
bootp[0] = 1;
bootp[1] = 1;
bootp[2] = 6;
bootp[4] = static_cast<std::uint8_t>(transaction >> 24);
bootp[5] = static_cast<std::uint8_t>(transaction >> 16);
bootp[6] = static_cast<std::uint8_t>(transaction >> 8);
bootp[7] = static_cast<std::uint8_t>(transaction);
bootp[10] = 0x80;
std::copy(station.begin(), station.end(), bootp.begin() + 28);
bootp[236] = 99;
bootp[237] = 130;
bootp[238] = 83;
bootp[239] = 99;
bootp.insert(bootp.end(), {53, 1, 1, 55, 5, 1, 3, 6, 28, 51, 255});
const Ipv4 zero{};
const Ipv4 broadcast = {255, 255, 255, 255};
const auto ip = make_client_udp_packet(zero, broadcast, 68, 67, bootp,
sequence);
return rt::make_data_frame(station, bssid, bssid, 0x0800, ip, sequence);
}
std::vector<std::uint8_t> make_client_tcp_frame(
const Mac& station, const Mac& bssid, const Ipv4& source_ip,
const Ipv4& destination_ip, std::uint16_t source_port,
std::uint32_t sequence, std::uint32_t acknowledgment,
std::uint8_t flags, std::string_view payload, std::uint16_t wifi_sequence) {
const auto ip = make_client_tcp_packet(
source_ip, destination_ip, source_port, 80, sequence, acknowledgment,
flags, payload, wifi_sequence);
return rt::make_data_frame(station, bssid, bssid, 0x0800, ip,
wifi_sequence);
}
std::vector<std::uint8_t> protect_client_frame(
const std::vector<std::uint8_t>& plain, const wifi::Wpa2Ptk& ptk,
std::uint64_t packet_number) {
wifi::Wpa2Key temporal_key{};
std::copy_n(ptk.begin() + 32, temporal_key.size(), temporal_key.begin());
return ccmp_encrypt_frame(plain, temporal_key, packet_number);
}
std::optional<std::pair<const std::uint8_t*, std::size_t>>
ipv4_from_data_frame(const std::vector<std::uint8_t>& psdu) {
const auto layout = wifi::parse_data_layout(psdu);
if (!layout || psdu.size() < layout->header_bytes + 8 + 20 + 4)
return std::nullopt;
const auto* llc = psdu.data() + layout->header_bytes;
if (std::memcmp(llc, "\xaa\xaa\x03\x00\x00\x00\x08\x00", 8) != 0)
return std::nullopt;
return std::make_pair(llc + 8,
psdu.size() - layout->header_bytes - 8 - 4);
}
#ifndef GF_AP_PROTOCOL_ONLY
struct Options {
bool self_test = false;
bool serial_enabled = true;
bool emit_late_acks = false;
bool emit_late_cts = false;
bool fpga_sifs = false;
bool full_duplex_usb = false;
bool m3_retry_train = false;
bool decode_ofdm = false;
std::string uri = "ip:192.168.2.1";
std::string serial_port = "COM9";
std::string ssid = std::string(kDefaultSsid);
std::string passphrase = std::string(kDefaultPassphrase);
Mac bssid = kDefaultBssid;
Ipv4 server_ip = kDefaultServerIp;
int channel = 6;
double rx_gain_db = 20.0;
double tx_gain_db = -40.0;
std::int64_t rx_sample_rate = 6'000'000;
std::int64_t rx_rf_bandwidth = 6'000'000;
double block_ms = 2.0;
double overlap_ms = 4.0;
double run_seconds = 60.0;
int tx_budget = 512;
int beacon_period_ms = 500;
int beacon_repeats = 3;
std::size_t max_stations = 8;
std::filesystem::path page_path =
"device/index.html";
std::filesystem::path jsonl_path;
std::filesystem::path rx_iq_path;
std::filesystem::path tx_iq_directory;
std::filesystem::path stop_file;
};
Options parse_ap_options(int argc, char** argv) {
Options options;
auto next = [&](int& index, const char* name) -> std::string {
if (++index >= argc)
throw std::runtime_error(std::string("missing value for ") + name);
return argv[index];
};
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--self-test") options.self_test = true;
else if (argument == "--no-serial") options.serial_enabled = false;
else if (argument == "--emit-late-acks") options.emit_late_acks = true;
else if (argument == "--emit-late-cts") options.emit_late_cts = true;
else if (argument == "--fpga-sifs") options.fpga_sifs = true;
else if (argument == "--full-duplex-usb")
options.full_duplex_usb = true;
else if (argument == "--m3-retry-train")
options.m3_retry_train = true;
else if (argument == "--decode-ofdm")
options.decode_ofdm = true;
else if (argument == "--uri") options.uri = next(index, "--uri");
else if (argument == "--serial-port")
options.serial_port = next(index, "--serial-port");
else if (argument == "--ssid") options.ssid = next(index, "--ssid");
else if (argument == "--passphrase")
options.passphrase = next(index, "--passphrase");
else if (argument == "--bssid")
options.bssid = rt::parse_mac(next(index, "--bssid"));
else if (argument == "--server-ip")
options.server_ip = parse_ip(next(index, "--server-ip"));
else if (argument == "--channel")
options.channel = std::stoi(next(index, "--channel"));
else if (argument == "--rx-gain-db")
options.rx_gain_db = std::stod(next(index, "--rx-gain-db"));
else if (argument == "--tx-gain-db")
options.tx_gain_db = std::stod(next(index, "--tx-gain-db"));
else if (argument == "--rx-sample-rate")
options.rx_sample_rate = std::stoll(
next(index, "--rx-sample-rate"));
else if (argument == "--rx-rf-bandwidth")
options.rx_rf_bandwidth = std::stoll(
next(index, "--rx-rf-bandwidth"));
else if (argument == "--block-ms")
options.block_ms = std::stod(next(index, "--block-ms"));
else if (argument == "--overlap-ms")
options.overlap_ms = std::stod(next(index, "--overlap-ms"));
else if (argument == "--run-seconds")
options.run_seconds = std::stod(next(index, "--run-seconds"));
else if (argument == "--tx-budget")
options.tx_budget = std::stoi(next(index, "--tx-budget"));
else if (argument == "--beacon-period-ms")
options.beacon_period_ms =
std::stoi(next(index, "--beacon-period-ms"));
else if (argument == "--beacon-repeats")
options.beacon_repeats =
std::stoi(next(index, "--beacon-repeats"));
else if (argument == "--max-stations")
options.max_stations = static_cast<std::size_t>(
std::stoul(next(index, "--max-stations")));
else if (argument == "--page")
options.page_path = next(index, "--page");
else if (argument == "--jsonl")
options.jsonl_path = next(index, "--jsonl");
else if (argument == "--rx-iq")
options.rx_iq_path = next(index, "--rx-iq");
else if (argument == "--tx-iq-dir")
options.tx_iq_directory = next(index, "--tx-iq-dir");
else if (argument == "--stop-file")
options.stop_file = next(index, "--stop-file");
else if (argument == "--help") {
std::cout
<< "gf_wifi_ap [--ssid NAME] [--passphrase PSK] [--bssid MAC] "
"[--channel 1..13] "
"[--server-ip IPv4] [--serial-port COM9] [--no-serial] "
"[--rx-sample-rate HZ] [--rx-rf-bandwidth HZ] "
"[--run-seconds SEC] [--tx-budget N] "
"[--beacon-period-ms MS] [--beacon-repeats N] "
"[--max-stations N] [--page FILE] [--jsonl FILE] "
"[--rx-iq FILE] [--tx-iq-dir DIR] [--stop-file FILE] "
"[--fpga-sifs] [--full-duplex-usb] "
"[--m3-retry-train] [--decode-ofdm]\n"
"SEC=0 and N=0 select continuous runtime and unlimited "
"frame count.\n";
std::exit(0);
} else {
throw std::runtime_error("unknown argument: " + argument);
}
}
if (options.ssid.empty() || options.ssid.size() > 32)
throw std::runtime_error("SSID must contain 1 through 32 bytes");
if (options.passphrase.size() < 8 || options.passphrase.size() > 63)
throw std::runtime_error(
"WPA2 passphrase must contain 8 through 63 bytes");
if (options.channel < 1 || options.channel > 13)
throw std::runtime_error("channel must be between 1 and 13");
if (!std::isfinite(options.rx_gain_db) || options.rx_gain_db < -3.0 ||
options.rx_gain_db > 71.0)
throw std::runtime_error("RX gain is outside Pluto manual range");
if (!std::isfinite(options.tx_gain_db) || options.tx_gain_db < -89.75 ||
options.tx_gain_db > -20.0)
throw std::runtime_error("TX gain must be between -89.75 and -20 dB");
if (options.rx_sample_rate < 3'000'000 ||
options.rx_sample_rate > rt::kSampleRate)
throw std::runtime_error(
"rx-sample-rate must be between 3000000 and 20000000");
if (options.rx_rf_bandwidth < 3'000'000 ||
options.rx_rf_bandwidth > 20'000'000)
throw std::runtime_error(
"rx-rf-bandwidth must be between 3000000 and 20000000");
if (!std::isfinite(options.block_ms) || options.block_ms < 0.5 ||
options.block_ms > 100.0)
throw std::runtime_error("block-ms must be between 0.5 and 100");
if (!std::isfinite(options.overlap_ms) || options.overlap_ms < 0.5 ||
options.overlap_ms > 40.0)
throw std::runtime_error("overlap-ms must be between 0.5 and 40");
if (!std::isfinite(options.run_seconds) || options.run_seconds < 0.0)
throw std::runtime_error("run-seconds must be nonnegative");
if (options.tx_budget < 0)
throw std::runtime_error("tx-budget must be nonnegative");
if (options.beacon_period_ms < 100 || options.beacon_period_ms > 5000)
throw std::runtime_error("beacon-period-ms must be 100..5000");
if (options.beacon_repeats < 1 || options.beacon_repeats > 8)
throw std::runtime_error("beacon-repeats must be 1..8");
if (options.max_stations < 2 || options.max_stations > 64)
throw std::runtime_error("max-stations must be 2..64");
if (options.fpga_sifs && options.bssid != kDefaultBssid)
throw std::runtime_error(
"the current FPGA SIFS image is compiled for BSSID "
"02:47:46:41:50:31");
return options;
}
class FpgaSifsControl {
public:
explicit FpgaSifsControl(iio_context* context) {
device_ = rt::require_device(context, "cf-ad9361-dds-core-lpc");
std::uint32_t gpio = 0;
rt::require_iio(iio_device_reg_read(device_, kDacGpioRegister, &gpio),
"read FPGA SIFS control");
gpio = (gpio & ~kKillMask) | kArmMask;
rt::require_iio(iio_device_reg_write(device_, kDacGpioRegister, gpio),
"arm FPGA SIFS control");
std::uint32_t verified = 0;
rt::require_iio(
iio_device_reg_read(device_, kDacGpioRegister, &verified),
"verify FPGA SIFS control");
if ((verified & (kArmMask | kKillMask)) != kArmMask)
throw std::runtime_error("FPGA SIFS arm register did not latch");
armed_ = true;
}
~FpgaSifsControl() { disarm(); }
FpgaSifsControl(const FpgaSifsControl&) = delete;
FpgaSifsControl& operator=(const FpgaSifsControl&) = delete;
void disarm() noexcept {
if (!device_ || !armed_) return;
std::uint32_t gpio = 0;
if (iio_device_reg_read(device_, kDacGpioRegister, &gpio) == 0) {
gpio = (gpio & ~kArmMask) | kKillMask;
(void)iio_device_reg_write(device_, kDacGpioRegister, gpio);
}
armed_ = false;
}
private:
static constexpr std::uint32_t kDacGpioRegister = 0x00bc;
static constexpr std::uint32_t kArmMask = 1u << 31;
static constexpr std::uint32_t kKillMask = 1u << 30;
iio_device* device_ = nullptr;
bool armed_ = false;
};
class PlutoApTx {
public:
PlutoApTx(const std::string& uri, std::int64_t center_hz, double gain_db,
std::int64_t sample_rate = rt::kSampleRate,
std::int64_t rf_bandwidth = rt::kRfBandwidth)
: context_(rt::open_context(uri)) {
initialize(context_.get(), center_hz, gain_db, sample_rate,
rf_bandwidth);
}
PlutoApTx(iio_context* context, std::int64_t center_hz, double gain_db,
std::int64_t sample_rate = rt::kSampleRate,
std::int64_t rf_bandwidth = rt::kRfBandwidth) {
if (!context) throw std::runtime_error("null shared IIO context");
initialize(context, center_hz, gain_db, sample_rate, rf_bandwidth);
}
~PlutoApTx() {
shutdown();
if (i_) iio_channel_disable(i_);
if (q_) iio_channel_disable(q_);
}
ssize_t send(const std::vector<std::int16_t>& interleaved) {
if (interleaved.empty() || (interleaved.size() & 1u) != 0)
throw std::runtime_error("TX waveform must contain interleaved IQ");
const auto samples = interleaved.size() / 2;
ensure_buffer(samples);
const auto step = iio_buffer_step(buffer_.get());
auto* i_pointer = static_cast<char*>(iio_buffer_first(buffer_.get(), i_));
auto* q_pointer = static_cast<char*>(iio_buffer_first(buffer_.get(), q_));
if (step == static_cast<ptrdiff_t>(2 * sizeof(std::int16_t)) &&
q_pointer == i_pointer + sizeof(std::int16_t)) {
std::memcpy(i_pointer, interleaved.data(),
interleaved.size() * sizeof(std::int16_t));
} else {
for (std::size_t index = 0; index < samples; ++index) {
std::memcpy(i_pointer, &interleaved[index * 2],
sizeof(std::int16_t));
std::memcpy(q_pointer, &interleaved[index * 2 + 1],
sizeof(std::int16_t));
i_pointer += step;
q_pointer += step;
}
}
const auto bytes = iio_buffer_push_partial(buffer_.get(), samples);
rt::require_iio_size(bytes, "Pluto AP TX push");
return bytes;
}
std::size_t buffer_capacity_samples() const { return buffer_samples_; }
std::int64_t sample_rate() const { return actual_sample_rate_; }
std::int64_t rf_bandwidth() const { return actual_rf_bandwidth_; }
void refresh_configuration() {
long long value = 0;
rt::require_iio(iio_channel_attr_read_longlong(
phy_, "sampling_frequency", &value),
"read TX sampling_frequency");
actual_sample_rate_ = value;
rt::require_iio(iio_channel_attr_read_longlong(
phy_, "rf_bandwidth", &value),
"read TX rf_bandwidth");
actual_rf_bandwidth_ = value;
}
void shutdown() noexcept {
buffer_.reset();
buffer_samples_ = 0;
if (dma_) rt::disable_dds(dma_);
if (phy_)
(void)iio_channel_attr_write_double(phy_, "hardwaregain", -89.75);
if (lo_)
(void)iio_channel_attr_write_bool(lo_, "powerdown", true);
}
private:
void initialize(iio_context* context, std::int64_t center_hz,
double gain_db, std::int64_t sample_rate,
std::int64_t rf_bandwidth) {
auto* phy_device = rt::require_device(context, "ad9361-phy");
dma_ = rt::require_device(context, "cf-ad9361-dds-core-lpc");
phy_ = rt::require_channel(phy_device, "voltage0", true);
lo_ = rt::require_channel(phy_device, "altvoltage1", true);
i_ = rt::require_channel(dma_, "voltage0", true);
q_ = rt::require_channel(dma_, "voltage1", true);
shutdown();
rt::write_attr(phy_, "rf_port_select", std::string("A"));
rt::write_attr(phy_, "rf_bandwidth", rf_bandwidth);
rt::write_attr(phy_, "sampling_frequency", sample_rate);
rt::write_attr(lo_, "frequency", center_hz);
rt::write_attr(phy_, "hardwaregain", gain_db);
rt::write_attr(lo_, "powerdown", false);
iio_channel_enable(i_);
iio_channel_enable(q_);
ensure_buffer(kInitialBufferSamples);
refresh_configuration();
}
void ensure_buffer(std::size_t samples) {
if (buffer_ && samples <= buffer_samples_) return;
const auto capacity = std::max(samples, kInitialBufferSamples);
buffer_.reset(iio_device_create_buffer(dma_, capacity, false));
if (!buffer_)
throw std::runtime_error("cannot create reusable Pluto AP TX buffer");
buffer_samples_ = capacity;
}
static constexpr std::size_t kInitialBufferSamples = 700'000;
rt::ContextPtr context_;
iio_device* dma_ = nullptr;
iio_channel* phy_ = nullptr;
iio_channel* lo_ = nullptr;
iio_channel* i_ = nullptr;
iio_channel* q_ = nullptr;
rt::BufferPtr buffer_;
std::size_t buffer_samples_ = 0;
std::int64_t actual_sample_rate_ = 0;
std::int64_t actual_rf_bandwidth_ = 0;
};
struct TxCommand {
Outbound frame;
rt::Clock::time_point queued_at = rt::Clock::now();
};
class TxQueue {
public:
void push(TxCommand command, bool priority) {
{
std::lock_guard lock(mutex_);
if (priority_.size() + normal_.size() >= 256) {
if (!priority) return;
if (!normal_.empty()) normal_.pop_back();
else priority_.pop_back();
}
if (priority) priority_.push_back(std::move(command));
else normal_.push_back(std::move(command));
}
condition_.notify_one();
}
bool pop(TxCommand& command, std::chrono::milliseconds timeout) {
std::unique_lock lock(mutex_);
condition_.wait_for(lock, timeout, [&] {
return !priority_.empty() || !normal_.empty() ||
rt::g_stop.load(std::memory_order_acquire);
});
if (priority_.empty() && normal_.empty()) return false;
auto& source = priority_.empty() ? normal_ : priority_;
command = std::move(source.front());
source.pop_front();
return true;
}
std::size_t size() const {
std::lock_guard lock(mutex_);
return priority_.size() + normal_.size();
}
void wake() { condition_.notify_all(); }
private:
mutable std::mutex mutex_;
std::condition_variable condition_;
std::deque<TxCommand> priority_;
std::deque<TxCommand> normal_;
};
std::vector<std::int16_t> resample_iq16(
const std::vector<std::int16_t>& input, std::int64_t input_rate,
std::int64_t output_rate) {
if (input.empty()) return {};
if ((input.size() & 1u) != 0 || input_rate <= 0 || output_rate <= 0)
throw std::runtime_error("invalid IQ resampler input");
if (input_rate == output_rate) return input;
const auto input_samples = input.size() / 2;
const auto output_samples = static_cast<std::size_t>(
static_cast<unsigned long long>(input_samples) *
static_cast<unsigned long long>(output_rate) /
static_cast<unsigned long long>(input_rate));
std::vector<std::int16_t> output(output_samples * 2);
const auto divisor = std::gcd(input_rate, output_rate);
const auto exact_phase_count = output_rate / divisor;
const auto phase_count = static_cast<std::size_t>(
std::min<std::int64_t>(exact_phase_count, 1024));
std::vector<std::array<double, 4>> coefficients(phase_count);
for (std::size_t phase = 0; phase < phase_count; ++phase) {
const double t = static_cast<double>(phase) /
static_cast<double>(phase_count);
const double t2 = t * t;
const double t3 = t2 * t;
coefficients[phase] = {
-0.5 * t + t2 - 0.5 * t3,
1.0 - 2.5 * t2 + 1.5 * t3,
0.5 * t + 2.0 * t2 - 1.5 * t3,
-0.5 * t2 + 0.5 * t3};
}
auto component = [&](std::ptrdiff_t sample, std::size_t iq) {
sample = std::clamp<std::ptrdiff_t>(
sample, 0, static_cast<std::ptrdiff_t>(input_samples - 1));
return static_cast<double>(
input[static_cast<std::size_t>(sample) * 2 + iq]);
};
auto quantize = [](double value) {
return static_cast<std::int16_t>(std::llround(
std::clamp(value, -32768.0, 32767.0)));
};
std::uint64_t position_numerator = 0;
for (std::size_t index = 0; index < output_samples; ++index) {
const auto base = static_cast<std::ptrdiff_t>(
position_numerator / static_cast<std::uint64_t>(output_rate));
const auto remainder = position_numerator %
static_cast<std::uint64_t>(output_rate);
const auto phase = static_cast<std::size_t>(
remainder * phase_count /
static_cast<std::uint64_t>(output_rate));
const auto& c = coefficients[phase];
for (std::size_t iq = 0; iq < 2; ++iq) {
const double value =
c[0] * component(base - 1, iq) +
c[1] * component(base, iq) +
c[2] * component(base + 1, iq) +
c[3] * component(base + 2, iq);
output[index * 2 + iq] = quantize(value);
}
position_numerator += static_cast<std::uint64_t>(input_rate);
}
return output;
}
std::vector<std::int16_t> repeated_waveform(
const std::vector<std::uint8_t>& psdu, int repeats,
std::int64_t sample_rate = rt::kSampleRate,
double gap_seconds = 0.0005) {
// A short zero guard is sufficient because the reusable DMA buffer ends on
// zero. The previous 1.5 ms of padding cost more USB time than an ACK PPDU.
const auto one = rt::make_waveform(
psdu, 0.05, 0.10, 0.25, 32, sample_rate);
if (repeats <= 1) return one;
const std::size_t gap_samples =
static_cast<std::size_t>(gap_seconds * sample_rate);
std::vector<std::int16_t> result;
result.reserve(static_cast<std::size_t>(repeats) *
(one.size() + gap_samples * 2));
for (int repeat = 0; repeat < repeats; ++repeat) {
result.insert(result.end(), one.begin(), one.end());
if (repeat + 1 != repeats)
result.insert(result.end(), gap_samples * 2, 0);
}
return result;
}
std::vector<std::uint8_t> make_control_frame(
bool cts, const Mac& receiver, std::uint16_t duration_us) {
std::vector<std::uint8_t> frame;
rt::append_le16(frame, cts ? 0x00c4 : 0x00d4);
rt::append_le16(frame, cts ? duration_us : 0);
rt::append_mac(frame, receiver);
rt::append_fcs(frame);
return frame;
}
std::vector<std::int16_t> make_sifs_cached_waveform(
const std::vector<std::uint8_t>& psdu) {
// No host-side lead/tail or zero-chip guard: sample zero is the physical
// start of the PPDU and is what the FPGA presents at the SIFS deadline.
return rt::make_waveform(psdu, 0.0, 0.1, 0.25, 0);
}
ProtocolConfig protocol_config(const Options& options) {
ProtocolConfig config;
config.ssid = options.ssid;
config.passphrase = options.passphrase;
config.bssid = options.bssid;
config.server_ip = options.server_ip;
config.channel = options.channel;
config.max_stations = options.max_stations;
config.page = rt::read_text_file(options.page_path);
return config;
}
class ApEngine {
public:
explicit ApEngine(Options options)
: options_(std::move(options)), events_(options_.jsonl_path),
protocol_(protocol_config(options_),
[this](std::string_view kind, std::string_view fields) {
events_.publish(kind, fields);
}),
remaining_tx_(options_.tx_budget) {}
int run() {
const auto center_hz = static_cast<std::int64_t>(
2407 + 5 * options_.channel) * 1'000'000;
const auto block_samples = static_cast<std::size_t>(std::llround(
options_.block_ms * options_.rx_sample_rate / 1000.0));
if (!options_.rx_iq_path.empty() &&
std::filesystem::exists(options_.rx_iq_path))
throw std::runtime_error("refusing to overwrite RX I/Q: " +
options_.rx_iq_path.string());
if (!options_.tx_iq_directory.empty())
std::filesystem::create_directories(options_.tx_iq_directory);
if (!options_.stop_file.empty()) {
if (!options_.stop_file.parent_path().empty())
std::filesystem::create_directories(
options_.stop_file.parent_path());
std::error_code remove_error;
std::filesystem::remove(options_.stop_file, remove_error);
}
events_.publish(
"startup",
"\"role\":\"access_point\",\"topology\":"
"\"WiFi-client-air-PlutoRX1-C++-PlutoTX1-air-WiFi-client\","
"\"ssid\":" + rt::quote(options_.ssid) +
",\"bssid\":" + rt::quote(rt::mac_text(options_.bssid)) +
",\"channel\":" + std::to_string(options_.channel) +
",\"center_hz\":" + std::to_string(center_hz) +
",\"pluto_uri\":" + rt::quote(options_.uri) +
",\"rx_gain_db\":" + number(options_.rx_gain_db, 2) +
",\"tx_gain_db\":" + number(options_.tx_gain_db, 2) +
",\"rx_sample_rate_requested\":" +
std::to_string(options_.rx_sample_rate) +
",\"rx_rf_bandwidth_requested\":" +
std::to_string(options_.rx_rf_bandwidth) +
",\"decoder_sample_rate\":" +
std::to_string(rt::kSampleRate) +
",\"virtual_http\":" +
rt::quote("http://" + ip_text(options_.server_ip) + "/") +
",\"security\":\"WPA2-PSK\",\"cipher\":\"CCMP-128\","
"\"credential_logged\":false,\"windows_port_80_bound\":false,"
"\"max_stations\":" +
std::to_string(options_.max_stations) +
",\"run_seconds\":" + number(options_.run_seconds, 3) +
",\"tx_budget\":" + std::to_string(options_.tx_budget) +
",\"continuous\":" +
std::string(options_.run_seconds == 0.0 ? "true" : "false") +
",\"emit_late_acks\":" +
std::string(options_.emit_late_acks ? "true" : "false") +
",\"emit_late_cts\":" +
std::string(options_.emit_late_cts ? "true" : "false") +
",\"fpga_sifs_requested\":" +
std::string(options_.fpga_sifs ? "true" : "false") +
",\"full_duplex_usb\":" +
std::string(options_.full_duplex_usb ? "true" : "false") +
",\"m3_retry_train\":" +
std::string(options_.m3_retry_train ? "true" : "false") +
",\"decode_ofdm\":" +
std::string(options_.decode_ofdm ? "true" : "false"));
context_ = rt::open_context(options_.uri);
rx_ = std::make_unique<rt::PlutoRx>(
context_.get(), center_hz, options_.rx_gain_db, "A_BALANCED",
block_samples, options_.rx_sample_rate,
options_.rx_rf_bandwidth);
tx_ = std::make_unique<PlutoApTx>(
context_.get(), center_hz, options_.tx_gain_db,
options_.rx_sample_rate, options_.rx_rf_bandwidth);
// Stock Pluto couples the duplex baseband clocks. Re-read both paths
// after configuration and generate TX I/Q at the accepted common rate.
rx_->refresh_configuration();
tx_->refresh_configuration();
rx_sample_rate_ = rx_->sample_rate();
tx_sample_rate_ = tx_->sample_rate();
if (rx_sample_rate_ <= 0 || tx_sample_rate_ <= 0 ||
rx_sample_rate_ != tx_sample_rate_)
throw std::runtime_error(
"Pluto reported invalid or unequal duplex sample rates");
if (options_.fpga_sifs) {
fpga_sifs_ = std::make_unique<FpgaSifsControl>(context_.get());
events_.publish(
"fpga_sifs_armed",
"\"clock_hz\":80000000,\"sample_rate\":20000000,"
"\"sifs_clocks\":800,\"bssid\":" +
rt::quote(rt::mac_text(options_.bssid)));
}
events_.publish("radio_ready",
"\"rx\":\"RX1 A_BALANCED continuous\","
"\"tx\":\"TX1 A packetized; LO and gain remain enabled for the continuous service\","
"\"rx_sample_rate\":" + std::to_string(rx_sample_rate_) +
",\"rx_rf_bandwidth\":" +
std::to_string(rx_->rf_bandwidth()) +
",\"decoder_sample_rate\":20000000,\"tx_sample_rate\":" +
std::to_string(tx_sample_rate_) +
",\"tx_rf_bandwidth\":" +
std::to_string(tx_->rf_bandwidth()) + ","
"\"rx_kernel_buffers\":2,\"rx_kernel_buffer_status\":" +
std::to_string(rx_->kernel_buffer_status()) +
",\"tx_gain_db\":" +
number(options_.tx_gain_db, 2));
if (options_.serial_enabled) {
serial_ = std::make_unique<rt::SerialWorker>(
options_.serial_port, events_,
[this](const std::string& line) { handle_serial(line); });
}
rx_thread_ = std::thread([this] { rx_loop(); });
decode_thread_ = std::thread([this] { decode_loop(); });
tx_thread_ = std::thread([this] { tx_loop(); });
beacon_thread_ = std::thread([this] { beacon_loop(); });
if (serial_) {
serial_->start();
serial_->enqueue("start");
serial_->enqueue("status");
}
const auto started = rt::Clock::now();
while (!rt::g_stop.load(std::memory_order_acquire)) {
if (options_.run_seconds > 0.0 &&
std::chrono::duration<double>(
rt::Clock::now() - started).count() >=
options_.run_seconds) {
events_.publish("run_limit_reached", "\"seconds\":" +
number(options_.run_seconds, 3));
break;
}
if (!options_.stop_file.empty() &&
std::filesystem::exists(options_.stop_file)) {
events_.publish("stop_file_requested", "\"path\":" +
rt::quote(options_.stop_file.string()));
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
events_.publish("shutdown_begin");
if (serial_) {
serial_->enqueue("status");
serial_->enqueue("stop");
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
rt::g_stop.store(true, std::memory_order_release);
tx_queue_.wake();
if (rx_) rx_->cancel();
if (beacon_thread_.joinable()) beacon_thread_.join();
if (rx_thread_.joinable()) rx_thread_.join();
if (decode_thread_.joinable()) decode_thread_.join();
if (tx_thread_.joinable()) tx_thread_.join();
if (serial_) serial_->stop();
if (fpga_sifs_) {
fpga_sifs_->disarm();
events_.publish("fpga_sifs_disarmed");
}
if (tx_) tx_->shutdown();
if (!options_.stop_file.empty()) {
std::error_code remove_error;
std::filesystem::remove(options_.stop_file, remove_error);
}
events_.publish(
"stopped", "\"rx_samples\":" +
std::to_string(rx_samples_.load()) +
",\"decoded_frames\":" +
std::to_string(decoded_frames_.load()) +
",\"tx_frames\":" + std::to_string(tx_frames_.load()) +
",\"stations\":" + std::to_string(protocol_.station_count()) +
",\"associated\":" +
std::to_string(protocol_.associated_count()) +
",\"sifs_responses_not_emitted\":" +
std::to_string(sifs_responses_not_emitted_.load()) +
",\"pluto_tx_off\":true");
return fatal_error_.load(std::memory_order_acquire) ? 1 : 0;
}
private:
static std::string number(double value, int precision) {
std::ostringstream output;
output << std::fixed << std::setprecision(precision) << value;
return output.str();
}
static std::string json_number(double value, int precision) {
return std::isfinite(value) ? number(value, precision) : "null";
}
void fail(std::string_view component, const std::exception& error) {
events_.publish("error", "\"component\":" + rt::quote(component) +
",\"message\":" + rt::quote(error.what()));
fatal_error_.store(true, std::memory_order_release);
rt::g_stop.store(true, std::memory_order_release);
tx_queue_.wake();
}
bool queue_frame(Outbound frame, bool priority = true) {
const bool verbose_event = frame.kind != "beacon";
if (frame.sifs_deadline) {
const bool experimental_late_response =
frame.kind == "cts" ? options_.emit_late_cts
: options_.emit_late_acks;
if (!experimental_late_response) {
++sifs_responses_not_emitted_;
events_.publish(
"timing_boundary",
"\"frame\":" + rt::quote(frame.kind) +
",\"required_turnaround_us\":10,"
"\"action\":\"formatted_but_not_transmitted_late\","
"\"station\":" +
rt::quote(rt::mac_text(frame.destination)));
return false;
}
events_.publish(
"timing_boundary",
"\"frame\":" + rt::quote(frame.kind) +
",\"required_turnaround_us\":10,"
"\"action\":\"queued_experimental_late_train\","
"\"station\":" +
rt::quote(rt::mac_text(frame.destination)));
}
if (options_.tx_budget == 0) {
const auto kind = frame.kind;
const auto destination = frame.destination;
const auto repeats = frame.repeats;
tx_queue_.push({std::move(frame), rt::Clock::now()}, priority);
if (verbose_event) {
events_.publish("tx_queued", "\"frame\":" + rt::quote(kind) +
",\"destination\":" +
rt::quote(rt::mac_text(destination)) +
",\"repeats\":" + std::to_string(repeats) +
",\"remaining_budget\":null");
}
return true;
}
int remaining = remaining_tx_.load(std::memory_order_acquire);
while (remaining > 0) {
if (remaining_tx_.compare_exchange_weak(
remaining, remaining - 1, std::memory_order_acq_rel)) {
const auto kind = frame.kind;
const auto destination = frame.destination;
const auto repeats = frame.repeats;
tx_queue_.push({std::move(frame), rt::Clock::now()}, priority);
if (verbose_event) {
events_.publish("tx_queued", "\"frame\":" +
rt::quote(kind) + ",\"destination\":" +
rt::quote(rt::mac_text(destination)) +
",\"repeats\":" + std::to_string(repeats) +
",\"remaining_budget\":" +
std::to_string(remaining - 1));
}
return true;
}
}
events_.publish("tx_rejected", "\"frame\":" +
rt::quote(frame.kind) +
",\"reason\":\"finite TX budget exhausted\"");
return false;
}
void enqueue(std::vector<Outbound> frames) {
// Priority traffic is FIFO, so the protocol engine's response order is
// preserved even when the TX worker wakes between two insertions.
for (auto& frame : frames)
(void)queue_frame(std::move(frame), true);
}
void rx_loop() noexcept {
try {
std::ofstream iq_output;
const bool record = !options_.rx_iq_path.empty();
if (record) {
if (!options_.rx_iq_path.parent_path().empty())
std::filesystem::create_directories(
options_.rx_iq_path.parent_path());
iq_output.open(options_.rx_iq_path,
std::ios::binary | std::ios::trunc);
if (!iq_output) throw std::runtime_error(
"cannot create RX I/Q: " + options_.rx_iq_path.string());
}
std::uint64_t first_sample = 0;
std::uint64_t blocks = 0;
std::uint64_t dropped = 0;
const auto started = rt::Clock::now();
auto last_status = started;
while (!rt::g_stop.load(std::memory_order_acquire)) {
std::vector<std::int16_t> interleaved;
try {
if (options_.full_duplex_usb) {
interleaved = rx_->refill();
} else {
while (tx_waiting_.load(std::memory_order_acquire) &&
!rt::g_stop.load(std::memory_order_acquire))
std::this_thread::yield();
std::unique_lock radio_lock(radio_bus_mutex_);
if (tx_waiting_.load(std::memory_order_acquire)) {
radio_lock.unlock();
std::this_thread::yield();
continue;
}
interleaved = rx_->refill();
}
} catch (...) {
if (rt::g_stop.load(std::memory_order_acquire)) break;
throw;
}
const auto samples = interleaved.size() / 2;
if (record) {
iq_output.write(
reinterpret_cast<const char*>(interleaved.data()),
static_cast<std::streamsize>(interleaved.size() *
sizeof(std::int16_t)));
if (!iq_output)
throw std::runtime_error("RX I/Q recording failed");
}
rt::RxBlock block{first_sample, std::move(interleaved)};
if (!rx_queue_.push(std::move(block))) ++dropped;
first_sample += samples;
rx_samples_.fetch_add(samples, std::memory_order_relaxed);
++blocks;
if (rt::Clock::now() - last_status >= std::chrono::seconds(1)) {
const double wall = std::chrono::duration<double>(
rt::Clock::now() - started).count();
const double rf = static_cast<double>(first_sample) /
static_cast<double>(rx_sample_rate_);
events_.publish("rx_stream", "\"blocks\":" +
std::to_string(blocks) + ",\"samples\":" +
std::to_string(first_sample) +
",\"host_delivery_ratio\":" +
number(wall > 0 ? rf / wall : 0, 3) +
",\"queue_drops\":" + std::to_string(dropped));
last_status = rt::Clock::now();
}
}
if (record) {
iq_output.flush();
events_.publish("rx_iq_retained", "\"path\":" +
rt::quote(options_.rx_iq_path.string()) +
",\"bytes\":" +
std::to_string(first_sample * 2 * sizeof(std::int16_t)));
}
} catch (const std::exception& error) {
if (!rt::g_stop.load(std::memory_order_acquire))
fail("pluto_rx", error);
}
}
bool already_seen(const std::string& key, std::uint64_t sample) {
for (auto iterator = seen_frames_.rbegin();
iterator != seen_frames_.rend(); ++iterator) {
if (sample > iterator->second + 4000) break;
const auto delta = sample > iterator->second
? sample - iterator->second : iterator->second - sample;
if (iterator->first == key && delta < 2000) return true;
}
seen_frames_.emplace_back(key, sample);
while (seen_frames_.size() > 4096) seen_frames_.pop_front();
return false;
}
void handle_packet(const decode::Packet& packet,
std::uint64_t absolute_sample) {
wifi::ProtocolInfo protocol;
if (const auto layout = wifi::parse_data_layout(packet.psdu)) {
protocol = layout->protected_frame
? wifi::inspect_ciphertext(packet.psdu, *layout)
: wifi::inspect_unprotected(packet.psdu, *layout);
}
const bool local = packet.transmitter == rt::mac_text(options_.bssid) ||
packet.source == rt::mac_text(options_.bssid);
std::ostringstream fields;
fields << "\"origin\":" << rt::quote(local ? "local_ap_tx_leakage" :
"wifi_client")
<< ",\"capture_sample\":" << absolute_sample
<< ",\"phy\":" << rt::quote(packet.phy)
<< ",\"rate_mbps\":" << number(packet.rate_mbps, 3)
<< ",\"subtype\":" << rt::quote(packet.subtype_text)
<< ",\"source\":" << rt::quote(packet.source)
<< ",\"destination\":" << rt::quote(packet.destination)
<< ",\"transmitter\":" << rt::quote(packet.transmitter)
<< ",\"receiver\":" << rt::quote(packet.receiver)
<< ",\"bssid\":" << rt::quote(packet.bssid)
<< ",\"ssid\":" << rt::quote(packet.ssid)
<< ",\"sequence\":" << packet.sequence_number
<< ",\"retry\":" << (packet.retry ? "true" : "false")
<< ",\"power_dbfs\":" << json_number(packet.power_dbfs, 2)
<< ",\"snr_db\":" << json_number(packet.snr_db, 2)
<< ",\"cfo_hz\":" << json_number(packet.cfo_hz, 1)
<< ",\"fcs_valid\":" <<
(packet.fcs_valid ? "true" : "false")
<< ",\"network\":" << rt::quote(protocol.network)
<< ",\"transport\":" << rt::quote(protocol.transport)
<< ",\"application\":" << rt::quote(protocol.application)
<< ",\"source_ip\":" << rt::quote(protocol.source_ip)
<< ",\"destination_ip\":" <<
rt::quote(protocol.destination_ip)
<< ",\"source_port\":" << protocol.source_port
<< ",\"destination_port\":" << protocol.destination_port
<< ",\"headers\":" <<
rt::string_array_json(protocol.headers);
events_.publish("rx_frame", fields.str());
if (!local && packet.fcs_valid)
enqueue(protocol_.ingest(packet.psdu, packet.power_dbfs));
}
void decode_loop() noexcept {
try {
const auto overlap_samples = static_cast<std::size_t>(std::llround(
options_.overlap_ms * rt::kSampleRate / 1000.0));
std::vector<std::int16_t> overlap;
std::uint64_t expected_first_rx = 0;
std::uint64_t blocks = 0;
std::uint64_t stale_blocks_skipped = 0;
auto last_status = rt::Clock::now();
double last_resample_ms = 0.0;
while (!rt::g_stop.load(std::memory_order_acquire)) {
rt::RxBlock block;
if (!rx_queue_.pop(block)) {
std::this_thread::yield();
continue;
}
// Preserve FIFO order. The DSSS timing resampler is bounded
// enough for this 4 ms cadence, and dropping an apparently
// stale block can discard a one-shot WPA2 M4 or DHCP frame.
if (block.first_sample != expected_first_rx) overlap.clear();
const auto block_samples_rx = block.interleaved.size() / 2;
const auto resample_started = rt::Clock::now();
auto decoder_block = resample_iq16(
block.interleaved, rx_sample_rate_, rt::kSampleRate);
last_resample_ms = std::chrono::duration<double, std::milli>(
rt::Clock::now() - resample_started).count();
const auto prefix_samples = overlap.size() / 2;
std::vector<std::int16_t> window;
window.reserve(overlap.size() + decoder_block.size());
window.insert(window.end(), overlap.begin(), overlap.end());
window.insert(window.end(), decoder_block.begin(),
decoder_block.end());
auto batch = decode::decode_iq16(
window.data(), window.size() / 2, options_.channel, nullptr,
options_.decode_ofdm);
const auto block_first_decoder = static_cast<std::uint64_t>(
std::llround(static_cast<long double>(block.first_sample) *
rt::kSampleRate /
static_cast<long double>(rx_sample_rate_)));
const auto window_first_decoder =
block_first_decoder >= prefix_samples
? block_first_decoder - prefix_samples : 0;
for (const auto& packet : batch.packets) {
const auto local_sample = static_cast<std::uint64_t>(
std::max<long long>(0, std::llround(
packet.time_seconds * rt::kSampleRate)));
const auto absolute = window_first_decoder + local_sample;
if (already_seen(packet.key, absolute)) continue;
decoded_frames_.fetch_add(1, std::memory_order_relaxed);
handle_packet(packet, absolute);
}
const auto keep = std::min(overlap_samples, window.size() / 2);
overlap.assign(window.end() -
static_cast<std::ptrdiff_t>(keep * 2), window.end());
expected_first_rx = block.first_sample + block_samples_rx;
++blocks;
if (rt::Clock::now() - last_status >= std::chrono::seconds(1)) {
events_.publish("decoder_stream", "\"blocks\":" +
std::to_string(blocks) + ",\"frames\":" +
std::to_string(decoded_frames_.load()) +
",\"stale_blocks_skipped\":" +
std::to_string(stale_blocks_skipped) +
",\"last_resample_ms\":" +
number(last_resample_ms, 3) +
",\"last_decode_ms\":" +
number(batch.decode_seconds * 1000.0, 3));
last_status = rt::Clock::now();
}
}
} catch (const std::exception& error) {
fail("decoder", error);
}
}
void retain_tx(const TxCommand& command,
const std::vector<std::int16_t>& waveform,
std::uint64_t ordinal) {
if (options_.tx_iq_directory.empty()) return;
const auto path = options_.tx_iq_directory /
("tx_" + std::to_string(ordinal) + "_" + command.frame.kind +
"_iq16.raw");
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) throw std::runtime_error("cannot retain AP TX I/Q");
output.write(reinterpret_cast<const char*>(waveform.data()),
static_cast<std::streamsize>(waveform.size() *
sizeof(std::int16_t)));
if (!output) throw std::runtime_error("AP TX I/Q retention failed");
events_.publish("tx_iq_retained", "\"path\":" +
rt::quote(path.string()) + ",\"bytes\":" +
std::to_string(waveform.size() * sizeof(std::int16_t)));
}
void tx_loop() noexcept {
try {
std::uint64_t ordinal = 0;
while (!rt::g_stop.load(std::memory_order_acquire)) {
TxCommand command;
if (!tx_queue_.pop(command, std::chrono::milliseconds(100)))
continue;
++ordinal;
const auto synthesis_started = rt::Clock::now();
std::vector<std::int16_t> generated;
const std::vector<std::int16_t>* waveform = nullptr;
bool cache_hit = false;
if (command.frame.kind == "ack") {
const auto found = ack_waveforms_.find(command.frame.psdu);
if (found != ack_waveforms_.end()) {
waveform = &found->second;
cache_hit = true;
} else {
generated = repeated_waveform(
command.frame.psdu, command.frame.repeats,
tx_sample_rate_);
auto inserted = ack_waveforms_.emplace(
command.frame.psdu, std::move(generated));
waveform = &inserted.first->second;
}
} else {
const bool m3_retry_train =
options_.m3_retry_train &&
command.frame.kind == "wpa2_m3";
// Three 20 MS/s M3 PPDUs with 40 ms gaps exceed the
// largest reliable Pluto cyclic-buffer geometry. A
// 12 ms gap still gives the ESP MAC ample processing
// time while keeping the complete train below the
// established 700k-complex-sample reusable buffer.
const double m3_retry_gap_seconds =
tx_sample_rate_ >= 15'000'000 ? 0.012 : 0.040;
generated = repeated_waveform(
command.frame.psdu,
m3_retry_train ? 3 : command.frame.repeats,
tx_sample_rate_,
m3_retry_train ? m3_retry_gap_seconds : 0.0005);
waveform = &generated;
}
const auto synthesis_ms =
std::chrono::duration<double, std::milli>(
rt::Clock::now() - synthesis_started).count();
retain_tx(command, *waveform, ordinal);
const bool beacon = command.frame.kind == "beacon";
const auto bus_wait_started = rt::Clock::now();
std::unique_lock<std::mutex> radio_lock(
radio_bus_mutex_, std::defer_lock);
if (!options_.full_duplex_usb) {
tx_waiting_.store(true, std::memory_order_release);
radio_lock.lock();
tx_waiting_.store(false, std::memory_order_release);
}
const auto bus_wait_ms =
std::chrono::duration<double, std::milli>(
rt::Clock::now() - bus_wait_started).count();
const auto started = rt::Clock::now();
const auto queue_ms = std::chrono::duration<double, std::milli>(
started - command.queued_at).count();
const auto bytes = tx_->send(*waveform);
const auto elapsed = std::chrono::duration<double, std::milli>(
rt::Clock::now() - started).count();
if (radio_lock.owns_lock()) radio_lock.unlock();
const auto completed = tx_frames_.fetch_add(
1, std::memory_order_relaxed) + 1;
if (!beacon) {
events_.publish("tx_begin", "\"frame\":" +
rt::quote(command.frame.kind) + ",\"destination\":" +
rt::quote(rt::mac_text(command.frame.destination)) +
",\"reason\":" + rt::quote(command.frame.reason) +
",\"queue_to_push_ms\":" + number(queue_ms, 3) +
",\"synthesis_ms\":" + number(synthesis_ms, 3) +
",\"radio_bus_wait_ms\":" + number(bus_wait_ms, 3) +
",\"waveform_cache_hit\":" +
std::string(cache_hit ? "true" : "false") +
",\"psdu_bytes\":" +
std::to_string(command.frame.psdu.size()) +
",\"waveform_samples\":" +
std::to_string(waveform->size() / 2) +
",\"reported_after_push\":true,\"psdu_hex\":" +
rt::quote(rt::hex_bytes(command.frame.psdu.data(),
command.frame.psdu.size(), 96)));
events_.publish("tx_complete", "\"frame\":" +
rt::quote(command.frame.kind) + ",\"push_bytes\":" +
std::to_string(bytes) + ",\"elapsed_ms\":" +
number(elapsed, 3) + ",\"rf_airtime_ms\":" +
number(static_cast<double>(waveform->size() / 2) *
1000.0 / tx_sample_rate_, 3) +
",\"reusable_buffer_samples\":" +
std::to_string(tx_->buffer_capacity_samples()) +
",\"tx_ready_between_packets\":true");
} else if ((completed % 100) == 0) {
events_.publish("beacon_tx", "\"total_tx_frames\":" +
std::to_string(completed) + ",\"last_push_bytes\":" +
std::to_string(bytes));
}
}
} catch (const std::exception& error) {
fail("pluto_tx", error);
}
}
void beacon_loop() noexcept {
try {
const auto started = rt::Clock::now();
while (!rt::g_stop.load(std::memory_order_acquire)) {
if (tx_queue_.size() < 2) {
const auto timestamp = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
rt::Clock::now() - started).count());
Outbound beacon{"beacon", "periodic AP beacon",
rt::kBroadcast, protocol_.beacon(timestamp), false,
options_.beacon_repeats};
(void)queue_frame(std::move(beacon), false);
}
const auto until = rt::Clock::now() +
std::chrono::milliseconds(options_.beacon_period_ms);
while (!rt::g_stop.load(std::memory_order_acquire) &&
rt::Clock::now() < until)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
} catch (const std::exception& error) {
fail("beacon_scheduler", error);
}
}
void handle_serial(const std::string& line) {
std::string event = "esp_station_log";
if (line.starts_with("sta_connected")) event = "esp_connected";
else if (line.starts_with("got_ip")) event = "esp_got_ip";
else if (line.starts_with("http_connected")) event = "esp_http_connected";
else if (line.starts_with("http_status")) event = "esp_http_status";
else if (line.starts_with("http_body")) event = "esp_http_body";
else if (line.starts_with("sta_disconnected"))
event = "esp_disconnected";
events_.publish(event, "\"line\":" + rt::quote(line));
}
Options options_;
rt::EventLog events_;
ApProtocol protocol_;
rt::ContextPtr context_;
std::unique_ptr<rt::PlutoRx> rx_;
std::unique_ptr<PlutoApTx> tx_;
std::unique_ptr<FpgaSifsControl> fpga_sifs_;
std::unique_ptr<rt::SerialWorker> serial_;
rt::SpscRing<rt::RxBlock, 16> rx_queue_;
TxQueue tx_queue_;
std::mutex radio_bus_mutex_;
std::atomic_bool tx_waiting_{false};
std::thread rx_thread_;
std::thread decode_thread_;
std::thread tx_thread_;
std::thread beacon_thread_;
std::deque<std::pair<std::string, std::uint64_t>> seen_frames_;
std::map<std::vector<std::uint8_t>, std::vector<std::int16_t>>
ack_waveforms_;
std::atomic<int> remaining_tx_{0};
std::atomic_bool fatal_error_{false};
std::atomic<std::uint64_t> rx_samples_{0};
std::atomic<std::uint64_t> decoded_frames_{0};
std::atomic<std::uint64_t> tx_frames_{0};
std::atomic<std::uint64_t> sifs_responses_not_emitted_{0};
std::int64_t rx_sample_rate_ = rt::kSampleRate;
std::int64_t tx_sample_rate_ = rt::kSampleRate;
};
#endif
void require_test(bool condition, std::string_view message) {
if (!condition) throw std::runtime_error(std::string(message));
}
const Outbound& require_output(const std::vector<Outbound>& output,
std::string_view kind) {
const auto found = std::find_if(output.begin(), output.end(),
[&](const Outbound& frame) { return frame.kind == kind; });
if (found == output.end())
throw std::runtime_error("missing self-test output: " +
std::string(kind));
return *found;
}
wifi::Wpa2Ptk complete_test_handshake(ApProtocol& protocol,
const ProtocolConfig& config,
const Mac& station,
std::uint16_t sequence_base) {
const auto auth = rt::make_authentication_request(
station, config.bssid, sequence_base);
const auto auth_output = protocol.ingest(auth, -20.0);
const auto& auth_response = require_output(
auth_output, "authentication_response");
require_test(auth_response.psdu.size() >= 34 &&
rt::little_u16(auth_response.psdu.data() + 26) == 2 &&
rt::little_u16(auth_response.psdu.data() + 28) == 0,
"open authentication response is malformed");
const auto auth_retry_output = protocol.ingest(auth, -20.0);
(void)require_output(auth_retry_output, "authentication_response");
const auto association = make_wpa2_association_request(
config.ssid, station, config.bssid, config.channel,
static_cast<std::uint16_t>(sequence_base + 1));
const auto association_output = protocol.ingest(association, -20.0);
const auto& response = require_output(
association_output, "association_response");
require_test(response.psdu.size() >= 34 &&
rt::little_u16(response.psdu.data() + 26) == 0,
"WPA2 association response rejected a valid station");
const auto& m1_frame = require_output(association_output, "wpa2_m1");
const auto m1_location = eapol_from_data_frame(m1_frame.psdu);
require_test(m1_location.has_value(), "M1 is not an EAPOL data frame");
const auto m1 = wifi::wpa2_parse_eapol_key(
m1_location->first, m1_location->second);
require_test(m1 && m1->key_info == 0x008a && !m1->mic,
"M1 key information is malformed");
const auto association_retry_output =
protocol.ingest(association, -20.0);
(void)require_output(association_retry_output, "association_response");
const auto& duplicate_m1_frame =
require_output(association_retry_output, "wpa2_m1");
const auto duplicate_m1_location =
eapol_from_data_frame(duplicate_m1_frame.psdu);
const auto duplicate_m1 = duplicate_m1_location
? wifi::wpa2_parse_eapol_key(
duplicate_m1_location->first, duplicate_m1_location->second)
: std::nullopt;
require_test(duplicate_m1 &&
duplicate_m1->replay_counter == m1->replay_counter &&
duplicate_m1->nonce == m1->nonce,
"duplicate association did not replay the current M1");
const auto timed_m1_output = protocol.maintenance(
rt::Clock::now() + std::chrono::milliseconds(600));
const auto& timed_m1_frame = require_output(timed_m1_output, "wpa2_m1");
const auto timed_m1_location = eapol_from_data_frame(timed_m1_frame.psdu);
const auto timed_m1 = timed_m1_location
? wifi::wpa2_parse_eapol_key(
timed_m1_location->first, timed_m1_location->second)
: std::nullopt;
require_test(timed_m1 &&
timed_m1->replay_counter == m1->replay_counter &&
timed_m1->nonce == m1->nonce,
"timed M1 retransmission changed handshake state");
wifi::Wpa2Nonce snonce{};
for (std::size_t index = 0; index < snonce.size(); ++index)
snonce[index] = static_cast<std::uint8_t>(
index + station.back());
const auto pmk = wifi::wpa2_derive_pmk(
config.passphrase, config.ssid);
const auto ptk = wifi::wpa2_derive_ptk(
pmk, config.bssid, station, m1->nonce, snonce);
wifi::Wpa2Key kck{};
wifi::Wpa2Key kek{};
std::copy_n(ptk.begin(), 16, kck.begin());
std::copy_n(ptk.begin() + 16, 16, kek.begin());
const auto m2 = make_eapol_key(
0x010a, m1->replay_counter, snonce, rsn_ie(), kck);
const auto m2_frame = make_client_eapol_frame(
station, config.bssid, m2,
static_cast<std::uint16_t>(sequence_base + 2));
const auto m2_output = protocol.ingest(m2_frame, -20.0);
const auto& m3_frame = require_output(m2_output, "wpa2_m3");
const auto m3_location = eapol_from_data_frame(m3_frame.psdu);
require_test(m3_location.has_value(), "M3 is not an EAPOL data frame");
const auto m3 = wifi::wpa2_parse_eapol_key(
m3_location->first, m3_location->second);
require_test(m3 && m3->key_info == 0x13ca &&
m3->replay_counter == m1->replay_counter + 1 &&
wifi::wpa2_eapol_mic_valid(*m3, ptk),
"M3 MIC/replay/key information is invalid");
const auto unwrapped = wifi::wpa2_aes_key_unwrap(kek, m3->key_data);
require_test(unwrapped && unwrapped->size() >= 46 &&
(*unwrapped)[22] == 0xdd && (*unwrapped)[23] == 22,
"M3 GTK KDE did not survive AES key wrap");
const auto duplicate_m2_output = protocol.ingest(m2_frame, -20.0);
const auto& duplicate_m3_frame =
require_output(duplicate_m2_output, "wpa2_m3");
const auto duplicate_m3_location =
eapol_from_data_frame(duplicate_m3_frame.psdu);
const auto duplicate_m3 = duplicate_m3_location
? wifi::wpa2_parse_eapol_key(
duplicate_m3_location->first, duplicate_m3_location->second)
: std::nullopt;
require_test(duplicate_m3 &&
duplicate_m3->replay_counter == m3->replay_counter &&
wifi::wpa2_eapol_mic_valid(*duplicate_m3, ptk),
"duplicate M2 did not replay the current valid M3");
const auto timed_m3_output = protocol.maintenance(
rt::Clock::now() + std::chrono::milliseconds(600));
const auto& timed_m3_frame = require_output(timed_m3_output, "wpa2_m3");
const auto timed_m3_location = eapol_from_data_frame(timed_m3_frame.psdu);
const auto timed_m3 = timed_m3_location
? wifi::wpa2_parse_eapol_key(
timed_m3_location->first, timed_m3_location->second)
: std::nullopt;
require_test(timed_m3 &&
timed_m3->replay_counter == m3->replay_counter &&
wifi::wpa2_eapol_mic_valid(*timed_m3, ptk),
"timed M3 retransmission changed handshake state");
wifi::Wpa2Nonce zero_nonce{};
const auto m4 = make_eapol_key(
0x030a, m3->replay_counter, zero_nonce, {}, kck);
const auto m4_frame = make_client_eapol_frame(
station, config.bssid, m4,
static_cast<std::uint16_t>(sequence_base + 3));
(void)protocol.ingest(m4_frame, -20.0);
require_test(protocol.handshake_complete(station),
"M4 did not install the verified PTK");
return ptk;
}
void run_ccmp_replay_self_test() {
ProtocolConfig config;
std::size_t verified=0, ip_deliveries=0, rejected=0;
ApProtocol protocol(config,[&](std::string_view kind,std::string_view) {
if(kind=="ccmp_verified") ++verified;
if(kind=="ipv4_rx") ++ip_deliveries;
if(kind=="ccmp_rejected") ++rejected;
});
const Mac station{2,0,0,0,0,0x31}, other{2,0,0,0,0,0x32};
auto ptk=complete_test_handshake(protocol,config,station,100);
auto plain=make_dhcp_discover(station,config.bssid,0x55667788,104);
const auto refcs=[](std::vector<std::uint8_t> frame) {
frame.resize(frame.size()-4); rt::append_fcs(frame); return frame;
};
const auto accept=[&](const auto& frame) {
const auto before=verified;
const auto out=protocol.ingest(frame,-20.0);
(void)require_output(out,"dhcp_offer");
require_test(verified==before+1,"fresh authenticated CCMP data rejected");
};
const auto drop=[&](const auto& frame) {
const auto before=verified, before_ip=ip_deliveries, before_rejected=rejected;
const auto out=protocol.ingest(frame,-20.0);
require_test(verified==before && ip_deliveries==before_ip && rejected==before_rejected+1 &&
std::all_of(out.begin(),out.end(),[](const auto& f){return f.sifs_deadline;}),
"rejected CCMP packet redelivered plaintext or ordinary response");
(void)require_output(out,"ack"); // MAC retry acknowledgement is still permitted.
};
const auto first=protect_client_frame(plain,ptk,1);
accept(first); drop(first);
auto retry=first; retry[1]|=0x08; retry=refcs(retry); drop(retry);
accept(protect_client_frame(plain,ptk,3));
drop(protect_client_frame(plain,ptk,2));
auto tampered=protect_client_frame(plain,ptk,100);
tampered[32]^=1; drop(refcs(tampered));
accept(protect_client_frame(plain,ptk,4)); // Bad MIC did not advance PN.
auto wrong_key=protect_client_frame(plain,ptk,100);
wrong_key[27]|=0x40; drop(refcs(wrong_key));
auto reserved=protect_client_frame(plain,ptk,100);
reserved[26]=1; drop(refcs(reserved));
auto no_extiv=protect_client_frame(plain,ptk,100);
no_extiv[27]&=~0x20; drop(refcs(no_extiv));
accept(protect_client_frame(plain,ptk,5));
// Non-QoS and each QoS TID have independent receive replay state.
for(const std::uint8_t tid:{std::uint8_t(0),std::uint8_t(7),std::uint8_t(15)}) {
auto qos=plain; qos[0]|=0x80; qos.insert(qos.begin()+24,{tid,0}); qos=refcs(qos);
const auto protected_qos=protect_client_frame(qos,ptk,1);
accept(protected_qos); drop(protected_qos);
}
const auto other_ptk=complete_test_handshake(protocol,config,other,200);
accept(protect_client_frame(make_dhcp_discover(other,config.bssid,0x12345678,204),other_ptk,1));
ptk=complete_test_handshake(protocol,config,station,300);
drop(first); // An old key epoch cannot authenticate under the new key.
accept(protect_client_frame(plain,ptk,1));
wifi::Wpa2Key temporal{};
std::copy_n(ptk.begin()+32,16,temporal.begin());
for(const auto pn:{0ULL,0x1000000000000ULL}) {
bool invalid=false;
try { (void)ccmp_encrypt_frame(plain,temporal,pn); }
catch(const std::exception&) { invalid=true; }
require_test(invalid,"CCMP transmit PN zero/wrap was accepted");
}
const auto last=ccmp_encrypt_frame(plain,temporal,0xffffffffffffULL);
const auto decrypted=ccmp_decrypt_frame(last,temporal);
require_test(decrypted && decrypted->packet_number==0xffffffffffffULL,
"CCMP final 48-bit PN changed");
std::cout<<"ap_ccmp_replay_self_test=PASS equal_and_older=true retry_ack_only=true per_tid=true per_station=true bad_mic_no_advance=true key_id=true nonce_wrap=true new_key_epoch=true physical_rf=false\n";
}
void run_ap_self_test() {
#ifdef GF_AP_PROTOCOL_ONLY
wifi::self_test();
#else
decode::self_test();
#endif
run_ccmp_replay_self_test();
ProtocolConfig config;
config.page = "<!doctype html><title>RF only</title><h1>PASS</h1>";
ApProtocol protocol(config);
const Mac station_one = {0x02, 0, 0, 0, 0, 1};
const Mac station_two = {0x02, 0, 0, 0, 0, 2};
#ifndef GF_AP_PROTOCOL_ONLY
const auto cached_ack = make_sifs_cached_waveform(
make_control_frame(false, station_one, 0));
const auto cached_cts = make_sifs_cached_waveform(
make_control_frame(true, station_two, 3620));
require_test(cached_ack.size() / 2 <= 8192 &&
cached_cts.size() / 2 <= 8192 &&
!cached_ack.empty() && cached_ack.front() != 0,
"SIFS control waveform does not fit the FPGA cache");
for (std::size_t index = 1; index < cached_ack.size(); index += 2)
require_test(cached_ack[index] == 0,
"1 Mb/s cached ACK unexpectedly contains Q energy");
const auto with_sifs_capture_context = [](const auto& waveform) {
// Offline decoder windows need history before an edge. This prefix is
// test-only context and is not stored in the FPGA response bank.
std::vector<std::int16_t> captured(20'000 * 2, 0);
captured.insert(captured.end(), waveform.begin(), waveform.end());
return captured;
};
const auto cached_ack_context = with_sifs_capture_context(cached_ack);
const auto cached_cts_context = with_sifs_capture_context(cached_cts);
const auto cached_ack_decode = decode::decode_iq16(
cached_ack_context.data(), cached_ack_context.size() / 2, 6, nullptr);
const auto cached_cts_decode = decode::decode_iq16(
cached_cts_context.data(), cached_cts_context.size() / 2, 6, nullptr);
require_test(std::any_of(
cached_ack_decode.packets.begin(),
cached_ack_decode.packets.end(),
[&](const auto& packet) {
return packet.fcs_valid &&
packet.subtype_text == "ack" &&
packet.receiver == rt::mac_text(station_one);
}) &&
std::any_of(
cached_cts_decode.packets.begin(),
cached_cts_decode.packets.end(),
[&](const auto& packet) {
return packet.fcs_valid &&
packet.subtype_text == "cts" &&
packet.receiver == rt::mac_text(station_two);
}),
"cached ACK/CTS did not survive the PHY decoder");
#endif
const auto beacon = protocol.beacon(123456);
require_test(beacon.size() >= 46 &&
rt::little_u16(beacon.data() + 34) == 0x0011,
"beacon capability advertises an unsupported PHY mode");
bool beacon_has_tim = false;
for (std::size_t offset = 36; offset + 2 <= beacon.size() - 4;) {
const auto id = beacon[offset];
const auto length = static_cast<std::size_t>(beacon[offset + 1]);
offset += 2;
if (offset + length > beacon.size() - 4) break;
if (id == 5 && length >= 4) beacon_has_tim = true;
offset += length;
}
require_test(beacon_has_tim, "AP beacon is missing its TIM element");
const auto portable_beacon_waveform = dsss_tx::make_waveform(beacon);
require_test(!portable_beacon_waveform.empty() &&
(portable_beacon_waveform.size() & 1u) == 0 &&
std::any_of(portable_beacon_waveform.begin(),
portable_beacon_waveform.end(),
[](std::int16_t sample) { return sample != 0; }),
"portable DSSS formatter produced no IQ");
#ifndef GF_AP_PROTOCOL_ONLY
const auto beacon_waveform = rt::make_waveform(beacon);
require_test(portable_beacon_waveform == beacon_waveform,
"portable DSSS formatter diverged from the proven host formatter");
const auto decoded = decode::decode_iq16(
beacon_waveform.data(), beacon_waveform.size() / 2, 6, nullptr);
require_test(std::any_of(decoded.packets.begin(), decoded.packets.end(),
[](const auto& packet) {
return packet.fcs_valid && packet.subtype_text == "beacon" &&
packet.ssid == "PLUTO-2.4";
}), "AP beacon did not survive DSSS formatter/decoder round trip");
const auto e310_beacon = dsss_tx::make_waveform(
beacon, 0.05, 0.0, 0.25, 32, rt::kSampleRate);
std::vector<std::int16_t> e310_capture(20'000 * 2, 0);
e310_capture.insert(e310_capture.end(),
e310_beacon.begin(), e310_beacon.end());
const auto e310_decoded = decode::decode_iq16(
e310_capture.data(), e310_capture.size() / 2, 6, nullptr);
require_test(std::any_of(
e310_decoded.packets.begin(), e310_decoded.packets.end(),
[](const auto& packet) {
return packet.fcs_valid && packet.subtype_text == "beacon" &&
packet.ssid == "PLUTO-2.4";
}), "E310 short-tail DSSS burst did not survive the PHY decoder");
#endif
const auto ptk_one = complete_test_handshake(
protocol, config, station_one, 10);
const auto ptk_two = complete_test_handshake(
protocol, config, station_two, 20);
require_test(protocol.station_count() == 2 &&
protocol.associated_count() == 2 &&
protocol.handshake_complete(station_one) &&
protocol.handshake_complete(station_two) &&
ptk_one != ptk_two,
"multi-station state table self-test failed");
const auto lease_one = protocol.lease_for(station_one);
const auto lease_two = protocol.lease_for(station_two);
require_test(lease_one && lease_two && *lease_one != *lease_two,
"stations did not receive distinct leases");
const auto discover_plain = make_dhcp_discover(
station_one, config.bssid, 0x11223344, 12);
const auto discover_one = protect_client_frame(
discover_plain, ptk_one, 1);
const auto offer_one = protocol.ingest(discover_one, -20.0);
const auto& offer = require_output(offer_one, "dhcp_offer");
wifi::Wpa2Key temporal_key{};
std::copy_n(ptk_one.begin() + 32, temporal_key.size(),
temporal_key.begin());
const auto offer_decrypted = ccmp_decrypt_frame(
offer.psdu, temporal_key);
require_test(offer_decrypted.has_value(),
"DHCP offer CCMP authentication failed");
const auto offer_plain = unprotected_from_ccmp(
offer.psdu, offer_decrypted->llc);
const auto offer_layout = wifi::parse_data_layout(offer_plain);
require_test(offer_layout.has_value(), "DHCP offer has no data layout");
const auto offer_info = wifi::inspect_unprotected(
offer_plain, *offer_layout);
require_test(offer_info.application == "DHCP" &&
!offer_info.dhcp.empty() &&
offer_info.dhcp.front().message_type == "OFFER" &&
offer_info.checksum_status.find("INVALID") == std::string::npos,
"DHCP offer parser/checksum self-test failed");
constexpr std::uint16_t client_port = 49152;
constexpr std::uint32_t client_isn = 0x10203040;
const auto syn_plain = make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip,
client_port, client_isn, 0, 0x02, {}, 13);
const auto syn = protect_client_frame(syn_plain, ptk_one, 2);
const auto syn_output = protocol.ingest(syn, -20.0);
const auto& syn_ack = require_output(syn_output, "tcp_syn_ack");
const auto syn_decrypted = ccmp_decrypt_frame(
syn_ack.psdu, temporal_key);
require_test(syn_decrypted.has_value(),
"SYN-ACK CCMP authentication failed");
const auto syn_ack_plain = unprotected_from_ccmp(
syn_ack.psdu, syn_decrypted->llc);
const auto syn_ip = ipv4_from_data_frame(syn_ack_plain);
require_test(syn_ip && syn_ip->second >= 40 && syn_ip->first[9] == 6,
"SYN-ACK is not valid IPv4/TCP");
const auto ip_header = static_cast<std::size_t>(
(syn_ip->first[0] & 0x0f) * 4u);
const auto server_isn = read_be32(syn_ip->first + ip_header + 4);
const std::string request =
"GET / HTTP/1.1\r\nHost: 192.168.44.1\r\nConnection: close\r\n\r\n";
const auto get_plain = make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip,
client_port, client_isn + 1, server_isn + 1, 0x18, request, 14);
const auto get = protect_client_frame(get_plain, ptk_one, 3);
const auto get_output = protocol.ingest(get, -20.0);
const auto& http = require_output(get_output, "http_response");
const auto http_decrypted = ccmp_decrypt_frame(
http.psdu, temporal_key);
require_test(http_decrypted.has_value(),
"HTTP response CCMP authentication failed");
const auto http_plain = unprotected_from_ccmp(
http.psdu, http_decrypted->llc);
const auto http_layout = wifi::parse_data_layout(http_plain);
require_test(http_layout.has_value(), "HTTP response has no data layout");
const auto http_info = wifi::inspect_unprotected(
http_plain, *http_layout);
require_test(http_info.network == "IPv4" &&
http_info.transport == "TCP" &&
http_info.source_port == 80 &&
http_info.payload_ascii.find("HTTP/1.1 200 OK") !=
std::string::npos &&
http_info.checksum_status.find("INVALID") == std::string::npos,
"RF HTTP response parser/checksum self-test failed");
// TCP is a byte stream: the method and final CRLF may straddle packets.
// These synthetic packets are protocol tests, never RF/iPhone evidence.
std::uint64_t test_pn = 10;
std::uint16_t test_sequence = 20;
const auto tcp_sequence_end = [&](const Outbound& frame) {
const auto decoded = ccmp_decrypt_frame(frame.psdu, temporal_key);
require_test(decoded.has_value(), "TCP test output CCMP invalid");
const auto plain = unprotected_from_ccmp(frame.psdu, decoded->llc);
const auto ip = ipv4_from_data_frame(plain);
require_test(ip && ip->second >= 40, "TCP test output has no IP");
const auto ip_size = (ip->first[0] & 15u) * 4u;
const auto* tcp = ip->first + ip_size;
const auto tcp_size = (tcp[12] >> 4) * 4u;
return read_be32(tcp + 4) + static_cast<std::uint32_t>(read_be16(ip->first + 2) - ip_size - tcp_size) +
((tcp[13] & 1u) ? 1u : 0u) + ((tcp[13] & 2u) ? 1u : 0u);
};
(void)protocol.ingest(protect_client_frame(make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip, client_port,
client_isn + 1 + static_cast<std::uint32_t>(request.size()), tcp_sequence_end(http),
0x10, {}, test_sequence++), ptk_one, test_pn++), -20.0);
const auto tcp_acknowledgment = [&](const Outbound& frame) {
const auto decoded = ccmp_decrypt_frame(frame.psdu, temporal_key);
require_test(decoded.has_value(), "stream-test output CCMP invalid");
const auto plain = unprotected_from_ccmp(frame.psdu, decoded->llc);
const auto ip = ipv4_from_data_frame(plain);
require_test(ip && ip->second >= 40, "stream-test output missing TCP");
return read_be32(ip->first + (ip->first[0] & 15u) * 4u + 8);
};
for (const std::size_t split : {std::size_t{2}, std::size_t{18}, request.size()-2}) {
const auto port = static_cast<std::uint16_t>(client_port + split);
const auto isn = client_isn + static_cast<std::uint32_t>(split * 1024);
const auto send = [&](std::uint32_t seq, std::uint32_t ack,
std::uint8_t flags, const std::string& bytes) {
return protocol.ingest(protect_client_frame(make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip,
port, seq, ack, flags, bytes, test_sequence++), ptk_one, test_pn++), -20.0);
};
const auto opened = send(isn, 0, 0x02, {});
const auto& opening = require_output(opened, "tcp_syn_ack");
const auto decoded = ccmp_decrypt_frame(opening.psdu, temporal_key);
require_test(decoded.has_value(), "stream-test SYN CCMP invalid");
const auto opening_plain = unprotected_from_ccmp(opening.psdu, decoded->llc);
const auto opening_ip = ipv4_from_data_frame(opening_plain);
require_test(opening_ip.has_value(), "stream-test SYN has no IP");
const auto server_next = read_be32(opening_ip->first +
(opening_ip->first[0] & 15u) * 4u + 4) + 1;
const auto prefix = send(isn+1, server_next, 0x18, request.substr(0, split));
require_test(std::none_of(prefix.begin(), prefix.end(),
[](const Outbound& out) { return out.kind == "http_response"; }),
"server responded before complete HTTP headers");
require_test(tcp_acknowledgment(require_output(prefix, "tcp_ack")) == isn+1+split,
"server did not cumulatively ACK an HTTP prefix");
const auto duplicate = send(isn+1, server_next, 0x18, request.substr(0, split));
require_test(tcp_acknowledgment(require_output(duplicate, "tcp_ack")) == isn+1+split,
"duplicate HTTP prefix advanced the TCP receive sequence");
const auto suffix = send(isn+1+static_cast<std::uint32_t>(split), server_next,
0x18, request.substr(split));
const auto& response = require_output(suffix, "http_response");
require_test(tcp_acknowledgment(response) == isn+1+request.size(),
"split HTTP response ACK does not cover the whole request");
const auto body = ccmp_decrypt_frame(response.psdu, temporal_key);
require_test(body.has_value(), "split HTTP response CCMP invalid");
const std::string text(body->llc.begin(), body->llc.end());
require_test(text.ends_with(config.page), "split HTTP response lost page bytes");
send(isn + 1 + static_cast<std::uint32_t>(request.size()), tcp_sequence_end(response), 0x10, {});
}
std::cout << "ap_tcp_stream_self_test=PASS splits=3 duplicate_prefix=true physical_rf=false\n";
for (const bool cancel : {false, true}) {
const std::uint16_t port = static_cast<std::uint16_t>(57000 + (cancel ? 1 : 0));
const std::uint32_t isn = 0x12345678;
const auto send = [&](std::uint32_t seq, std::uint32_t ack,
std::uint8_t flags, const std::string& bytes) {
return protocol.ingest(protect_client_frame(make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip,
port, seq, ack, flags, bytes, test_sequence++), ptk_one, test_pn++), -20.0);
};
const auto opened = send(isn, 0, 0x02, {});
const auto& opening = require_output(opened, "tcp_syn_ack");
const auto decoded = ccmp_decrypt_frame(opening.psdu, temporal_key);
require_test(decoded.has_value(), "delayed-test SYN CCMP invalid");
const auto opening_plain = unprotected_from_ccmp(opening.psdu, decoded->llc);
const auto ip = ipv4_from_data_frame(opening_plain);
require_test(ip.has_value(), "delayed-test SYN has no IP");
const auto server_next = read_be32(ip->first + (ip->first[0] & 15u) * 4u + 4) + 1;
const std::string delayed_request = "GET /sleep-test HTTP/1.1\r\nHost: lab\r\n\r\n";
const auto deferred = send(isn+1, server_next, 0x18, delayed_request);
require_test(tcp_acknowledgment(require_output(deferred, "tcp_ack")) ==
isn+1+delayed_request.size(), "deferred GET was not ACKed");
const auto no_http = [](const std::vector<Outbound>& output) {
return std::none_of(output.begin(), output.end(),
[](const Outbound& item) { return item.kind == "http_response" || item.kind == "http_retransmit"; });
};
const auto now = rt::Clock::now();
require_test(no_http(deferred) && no_http(protocol.maintenance(now + std::chrono::seconds(1))),
"deferred HTTP was emitted before its deadline");
const auto duplicate = send(isn+1, server_next, 0x18, delayed_request);
require_test(no_http(duplicate), "duplicate request bypassed HTTP delay");
if (cancel) send(isn+1+static_cast<std::uint32_t>(delayed_request.size()), server_next, 0x14, {});
const auto due = protocol.maintenance(now + std::chrono::seconds(2));
if (cancel) {
require_test(no_http(due), "RST did not cancel deferred response");
} else {
const auto& response = require_output(due, "http_response");
require_test(tcp_acknowledgment(response) == isn+1+delayed_request.size(),
"deferred HTTP did not ACK the full request");
const auto plain = ccmp_decrypt_frame(response.psdu, temporal_key);
require_test(plain.has_value() &&
std::string(plain->llc.begin(), plain->llc.end()).ends_with(config.page),
"deferred HTTP lost the complete page");
send(isn + 1 + static_cast<std::uint32_t>(delayed_request.size()),
tcp_sequence_end(response), 0x10, {});
}
require_test(no_http(protocol.maintenance(now + std::chrono::seconds(3))),
"ACKed/cancelled deferred HTTP was retransmitted");
}
std::cout << "ap_deferred_http_self_test=PASS delay_ms=1500 duplicate_ack=true rst_cancels=true physical_rf=false\n";
// Deliberately lost HTTP, with real protocol state but synthetic frames.
// Check timing/backoff, partial ACK, FIN-only retry, RST and PS buffering.
for (const unsigned mode : {0u, 1u, 2u, 3u}) {
const auto port = static_cast<std::uint16_t>(56000 + mode);
const std::uint32_t isn = 0xfffffff0u; // Receive sequence wraps in this request.
const auto send = [&](std::uint32_t seq, std::uint32_t ack, std::uint8_t flags,
const std::string& bytes) {
return protocol.ingest(protect_client_frame(make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip,
port, seq, ack, flags, bytes, test_sequence++), ptk_one, test_pn++), -20.0);
};
const auto mark_sleep = [&](bool asleep) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, static_cast<std::uint16_t>(0x0148 | (asleep ? 0x1000 : 0)),
config.bssid, station_one, config.bssid, test_sequence++);
rt::append_fcs(frame);
return protocol.ingest(frame, -20.0);
};
const auto none_for_port = [port](const std::vector<Outbound>& frames) {
return std::none_of(frames.begin(), frames.end(), [port](const Outbound& frame) {
return frame.tcp_response_port == port;
});
};
const auto syn_output = send(isn, 0, 0x02, {});
const auto base = tcp_sequence_end(require_output(syn_output, "tcp_syn_ack"));
const std::string lost_request = "GET /retry-test HTTP/1.1\r\nHost: lab\r\n\r\n";
const auto client_next = isn + 1 + static_cast<std::uint32_t>(lost_request.size());
const auto dropped = send(isn + 1, base, 0x18, lost_request);
require_test(tcp_acknowledgment(require_output(dropped, "tcp_ack")) == client_next &&
none_for_port(dropped), "loss-test did not ACK GET and withhold HTTP");
const auto now = rt::Clock::now();
require_test(none_for_port(protocol.maintenance(now + std::chrono::milliseconds(900))),
"HTTP retransmitted before initial RTO");
if (mode == 2) {
send(client_next, base, 0x14, {});
require_test(none_for_port(protocol.maintenance(now + std::chrono::seconds(20))),
"RST did not cancel HTTP retransmission");
continue;
}
if (mode == 3) mark_sleep(true);
auto first = protocol.maintenance(now + std::chrono::milliseconds(1100));
if (mode == 3) {
require_test(none_for_port(first) && protocol.buffered_for(station_one) == 1,
"sleeping retry escaped queue");
require_test(none_for_port(protocol.maintenance(now + std::chrono::seconds(30))) &&
protocol.buffered_for(station_one) == 1, "RTO multiplied a queued response");
first = mark_sleep(false);
require_test(protocol.buffered_for(station_one) == 0, "wake did not release retry");
}
const auto response = require_output(first, "http_retransmit");
const auto end = tcp_sequence_end(response);
const auto plain = ccmp_decrypt_frame(response.psdu, temporal_key);
require_test(plain && std::string(plain->llc.begin(), plain->llc.end()).ends_with(config.page),
"retry lost the actual page bytes");
if (mode == 0) {
// Invalid future ACK cannot cancel a pending response.
send(client_next, end + 1, 0x10, {});
require_test(none_for_port(protocol.maintenance(now + std::chrono::milliseconds(3000))),
"second retry ignored exponential backoff");
const auto again = protocol.maintenance(now + std::chrono::milliseconds(3200));
const auto& retry = require_output(again, "http_retransmit");
const auto retried_plain = ccmp_decrypt_frame(retry.psdu, temporal_key);
require_test(retried_plain && retried_plain->packet_number > plain->packet_number &&
tcp_sequence_end(retry) == end && retried_plain->llc.size() == plain->llc.size(),
"retry changed sequence/size or reused CCMP PN");
} else if (mode == 1) {
send(client_next, end - 11, 0x10, {}); // Ten data bytes and FIN still missing.
auto partial = protocol.maintenance(rt::Clock::now() + std::chrono::milliseconds(2100));
const auto& suffix = require_output(partial, "http_retransmit");
const auto suffix_plain = ccmp_decrypt_frame(suffix.psdu, temporal_key);
require_test(suffix_plain && suffix_plain->llc.size() == 8 + 20 + 20 + 10 &&
tcp_sequence_end(suffix) == end, "partial ACK did not trim retry to ten bytes");
send(client_next, end - 1, 0x10, {}); // Only FIN remains unacknowledged.
auto fin = protocol.maintenance(rt::Clock::now() + std::chrono::milliseconds(4100));
const auto& fin_retry = require_output(fin, "http_retransmit");
const auto fin_plain = ccmp_decrypt_frame(fin_retry.psdu, temporal_key);
require_test(fin_plain && fin_plain->llc.size() == 8 + 20 + 20 &&
tcp_sequence_end(fin_retry) == end, "FIN-only retry contains old data");
}
send(client_next, end, 0x10, {});
require_test(none_for_port(protocol.maintenance(rt::Clock::now() + std::chrono::minutes(2))),
"complete ACK did not stop response timer");
}
std::cout << "ap_tcp_retry_self_test=PASS initial_rto=true backoff=true partial_ack=true fin=true rst=true ps_queue=true physical_rf=false\n";
const auto pm_null = [&](bool asleep) {
std::vector<std::uint8_t> frame;
rt::append_management_header(frame, static_cast<std::uint16_t>(0x0148 | (asleep ? 0x1000 : 0)),
config.bssid, station_one, config.bssid, test_sequence++);
rt::append_fcs(frame);
return protocol.ingest(frame, -20.0);
};
const auto poll = [&](std::uint16_t aid) {
std::vector<std::uint8_t> frame;
rt::append_le16(frame, 0x10a4);
rt::append_le16(frame, static_cast<std::uint16_t>(0xc000 | aid));
rt::append_mac(frame, config.bssid); rt::append_mac(frame, station_one);
rt::append_fcs(frame);
return protocol.ingest(frame, -20.0);
};
const auto sleeping_syn = [&](std::uint16_t port) {
auto frame = protect_client_frame(make_client_tcp_frame(
station_one, config.bssid, *lease_one, config.server_ip, port,
0x56780000, 0, 0x02, {}, test_sequence++), ptk_one, test_pn++);
frame[1] |= 0x10; // PM is masked out of the existing CCMP AAD.
frame.resize(frame.size()-4); rt::append_fcs(frame);
return protocol.ingest(frame, -20.0);
};
const auto tim = [&](std::uint16_t aid) {
const auto frame = protocol.beacon(123456);
for (std::size_t offset=36; offset+2 <= frame.size()-4;) {
const auto size=static_cast<std::size_t>(frame[offset+1]);
require_test(offset+2+size <= frame.size()-4, "malformed beacon IE");
if (frame[offset] == 5) {
require_test(size >= 4, "short TIM");
const auto* body=frame.data()+offset+2;
const auto first=body[2]&0xfeu;
const auto octet=aid/8u;
return octet >= first && octet-first < size-3 &&
(body[3+octet-first] & (1u << (aid%8u))) != 0;
}
offset+=2+size;
}
throw std::runtime_error("missing TIM");
};
pm_null(true);
for (const std::uint16_t port : std::array<std::uint16_t, 2>{58000, 58001}) {
const auto queued=sleeping_syn(port);
require_test(std::all_of(queued.begin(),queued.end(),
[](const Outbound& out) { return out.sifs_deadline; }), "sleeping unicast escaped buffering");
}
require_test(protocol.buffered_for(station_one)==2 && tim(1) && !tim(2),
"TIM did not identify only the buffered station");
require_test(poll(2).empty() && protocol.buffered_for(station_one)==2,
"wrong AID released a buffered packet");
for (int remaining=1; remaining>=0; --remaining) {
const auto released=poll(1);
const auto& data=require_output(released,"tcp_syn_ack");
require_test(ccmp_decrypt_frame(data.psdu,temporal_key).has_value(),
"More Data mutation broke CCMP authentication");
require_test(((data.psdu[1]&0x20)!=0)==(remaining!=0) &&
protocol.buffered_for(station_one)==static_cast<std::size_t>(remaining) &&
tim(1)==(remaining!=0), "poll/TIM/More Data queue state mismatch");
}
const auto empty_poll=poll(1);
const auto& empty_data=require_output(empty_poll,"ps_null");
require_test(rt::little_u16(empty_data.psdu.data())==0x0248,
"empty PS-Poll did not return From-DS null with More Data clear");
sleeping_syn(58002);
const auto awakened=pm_null(false);
require_output(awakened,"tcp_syn_ack");
require_test(protocol.buffered_for(station_one)==0 && !tim(1), "wake did not flush queue/TIM");
pm_null(true); sleeping_syn(58003);
std::vector<std::uint8_t> disassociation;
rt::append_management_header(disassociation,0x00a0,config.bssid,station_one,config.bssid,test_sequence++);
rt::append_le16(disassociation,8); rt::append_fcs(disassociation);
protocol.ingest(disassociation,-20.0);
require_test(protocol.buffered_for(station_one)==0 && !tim(1), "disassociation retained encrypted queue");
std::cout << "ap_power_save_self_test=PASS tim=true poll=true more_data_ccmp=true wake_flush=true physical_rf=false\n";
std::cout << "ap_self_test=PASS stations=" << protocol.station_count()
<< " leases=" << ip_text(*lease_one) << ',' << ip_text(*lease_two)
<< " beacon_bytes=" << beacon.size()
#ifndef GF_AP_PROTOCOL_ONLY
<< " sifs_ack_samples=" << cached_ack.size() / 2
<< " sifs_cts_samples=" << cached_cts.size() / 2
#endif
<< " offer=" << offer_info.summary
<< " http=" << http_info.summary << '\n';
}
} // namespace gf::ap
#ifndef GF_AP_LIBRARY_ONLY
int main(int argc, char** argv) {
try {
#ifdef GF_AP_PROTOCOL_ONLY
(void)argc;
(void)argv;
gf::ap::run_ap_self_test();
return 0;
#else
SetConsoleCtrlHandler(gf::rt::console_handler, TRUE);
gf::rt::g_stop.store(false, std::memory_order_release);
const auto options = gf::ap::parse_ap_options(argc, argv);
if (options.self_test) {
gf::ap::run_ap_self_test();
return 0;
}
return gf::ap::ApEngine(options).run();
#endif
} catch (const std::exception& error) {
std::cerr << "fatal: " << error.what() << '\n';
return 1;
}
}
#endif