-
Notifications
You must be signed in to change notification settings - Fork 9
pico‐WSPRer (aka Cheapest Tracker in the World™)
This project implements an extremely low-cost WSPR beacon for tracking GPS position and other telemetry from High Altitude Balloons (HAB), specifically "pico balloons".
What unique about this tracker is that it uses a RP2040 (Raspberry Pi Pico) to directly generate the RF signal using software trickery. No radio oscillator, transmitter or amplifier is needed.
Also unique to this tracker is that it requires only two common, readily available and cheap components: A Raspberry Pi Pico, and a tiny GPS module (ATGM336H, or a uBlox clone). Two resistors and some bits of wire and solder hold it together.
Instead of using a TCXO, the extremely precise frequency base needed for the WSPR protocol is obtained by continually "disciplining" the standard crystal oscillator onboard the Pico with the PPS pulses from the GPS module.
WARNING! Performance is very dependent on a reliable GPS signal to prevent frequency drift! See the GPS Loss Mitigation section for tips and information.
The WSPR tracker uses a Raspberry Pi Pico (or RP2040) to generate RF power, driving four GPIO pins (paralleled and antiphase) to feed a half-wave dipole antenna trimmed for 20M. The altitude and 6-character maidenhead grid are calculated from GPS data and transmitted along with the callsign. Solar array voltage and RP2040 temperature are also sent as telemetry. The RF power in the 14 MHz (20 meter) band generated by the RP2040 is approximately 10 mW.
Worldwide tracking and telemetry on a 20mW solar powered transmitter that weighs less than a sheet of paper sounds impossible, but it works for 3 reasons:
- worldwide network of WSPR receivers: There are hundreds of radio enthusiasts globally that keep receiver/decoders running 24/7, and automatically feed the data via the internet to a central database. Specifically, the The Weak Signal Propagation Reporter Network collects specifically coded "WSPR" messages primarily for the purpose of analyzing changing RF propagation. Balloon tracking using WSPR involves "piggybacking" some additional position and telemetry data into the WSPR packets and taking advantage of the existing infrastructure.
- Amazing weak signal reception at the cost of bandwidth: A WSPR balloon tracker takes a full 4 minutes to transmit less than 20 bytes of data. According to Claude Shannon, this low bandwidth is what allows WSPR decoders to mathematically extract usable data from nearly nonexistent signal strength.
- Physics: Pico balloons typically float at an altitude of 7 or 8 miles, well above any obstructions. The low frequencies typically used by WSPR can easily bounce off the ionosphere giving nearly unlimited range. And even though the weight of pico balloon payloads is extremely limited, the size is not. This allows them to dangle very long (but very efficient) dipole antennas.
Radio transmitters generate precisely modulated RF signals through complex electronics. The core novelty of this project is that that entire process is instead done in software running on a micrcontroller. The original method using a PIO is credited to Roman Piksaykin, whose work at https://github.com/RPiks/pico-WSPR-tx was the initial fork and inspiration for this project.
An excellent explanation of this method has been put together by Jakub Serych at https://github.com/serych/pico-WSPRer/wiki/how-does-pico-oscilator-work. Jakub has also contributed bug fixes, additional features and documentation directly to this project.
As of January 2025, the project has switched to an amazing library created by Kazu https://github.com/kaduhi/pico-fractional-pll which utilizes an ingenious method of using the rp2040's internal PLL as a very clean and accurate RF generator.
This project usually transmits on the 20M band only, but there is an experimental band-hopping mode that does broadcasts both the 10M and 20M bands every 10 minutes. Thanks to Kevin AD6Z there is also now a multi-band branch that lets the user select the 20, 17, 15, 12 or 10 meter bands.
-
Low-Cost Implementation: Uses affordable off-the-shelf components.
-
RF Power Generation: Directly generated by the RP2040.
-
Telemetry Transmission: Includes altitude, maidenhead grid, solar array voltage, and RP2040 temperature.
-
Extended Telemetry (DEXT): Supports sending an additional 11 bytes (approx) per 10 minute period.
-
Configurable Callsign and Telemetry Encoding: Via USB and terminal program.
-
Frequency Compensation: WSPR is very sensitive to frequency drift, and transmitters typically need a precise TCXO frequency reference. But this project implements a software based "GPS disciplined oscillator" utilizing just the Pico's onboard XTAL and the PPS pulses from the GPS receiver.
-
Power Source Typical configuration is a 6 cell polycrystalline silicon solar array. Power consumption was 48mA when supplying 3.3V directly to VBUS and transmitting into a 70 ohm load. V3 PCB with boost converter pulls 96mA @ 2v or 73ma @ 2.5v. For desktop operation the USB port on the Pico is more than adequate. Use of batteries for high altitude balloon is problematic due to the low temperatures at night.
-
RP2040 Board (e.g., Raspberry Pi Pico) (as of now only genuine Raspberry Pi Pico boards have been used successfully in flights. The voltage regulators on the clones and smaller rp2040 boards are problematic during slow voltage rises at sunrise, but are fine for ground based AC powered applications)
-
GPS Module (e.g., ATGM336H)
-
Half-wave Dipole Antenna for 20M
-
Voltage Divider (two resistors) During sunrise the gradual increase of solar power can cause the RP2040 to lockup into a frozen, high current draw state. A simple voltage divider feeding the RUN input (when used with a GENUINE Pi Pico) is sufficient to prevent lockup. The custom PCB version of this project uses a watchdog monitor instead. Not required for desktop operation.
-
MOSFET or NPN Transistor (optional) The ability of the CPU to disconnect the current draw of the GPS module until it is stable is crucial to it being able to survive gradual sunrise. Alternatively, 3 IO pins in parallel can low-side the GPS module instead. Not required for desktop operation.
Balloon trackers expand on the standard WSPR message format by using either the U4B/Traquito protocol or the Zachtek protocol.
The Zachek protocol is essentially the standard WSPR "Type 3" message that conveys a suffix after the callsign and a full 6 character maidenhead grid location. The Zachtek protocol build on the Type-3 format by also encoding the altitude into the power fields of the messages.
The U4B protocol first sends a standard WSPR Type-1 messages with callsign and 4 character maidenhead, followed by a second message containing the last two maidenhead characters, altitude, temperature, knots, solar panel voltage and a GPS-valid bit.
This tracker expands on the U4B protocol slightly by sending the number of satellites in view (as reported by the GPS module) in the knots field instead of speed. (because of encoding reasons, the number displayed is twice its value. i.e. if the tracking websites show your tracker reporting 20 knots, it means that it is seeing 10 satellites). Actual speed of the balloon is inferred by change in position over time anyway.
This tracker is unique in that it can be configured for either one, or, it can even do both sequentially (ie transmit Zachtek during minutes 0-3, followed by U4B during minutes 4-7).
Each protocol has its pros and cons, and using both simultaneously covers all the bases.
The U4B conveys the most information, but it requires choosing an unused "channel" beforehand. You should also reserve the channel you plan to use by entering your flight details at http://lu7aa.org/wsprset.asp. Proper decoding of U4B is a little more difficult in adverse situations, because both packets must fall into the same frequency channel.
The Zachtek protocol does not require any channel reservation, and is perhaps a little more robust to be decoded. But it does not include temperature or voltage readings.
This project can easily be used as a standalone WSPR beacon at a base-station or other permanent location. The design can be simplified when powered from a fixed, non-solar power supply. The resistors are not needed, nor is the MOSFET (GPS module ground can be connected directly to ground). Also, you can use any Pi Pico clone or counterfeit board, including Waveshare RP2040 Zero variants (although the LED status functionality won't work). (reminder: for actual solar powered operation only genuine Pi Pico have been shown to be reliable).
Beacon mode is selected by disabling BOTH the Zachtek and U4B protocols (by entering - for suffix and -- for id13). It will do a single standard Type 1 WSPR message every 10 minutes with no suffix on the callsign, a 4 char Grid only, and a hardcoded 10dbm for the power field. Very basic operation only, no frequency hopping, no band hopping etc.
Alternatively, if you enter an "X" for suffix it will do standard Type1/Type3 messaging. Unlike "Zachtek" mode, x-mode will not append a suffix to your callsign, it will not try to encode altitude into the power field, but it will do the full 6 character grid location.
Even in beacon mode you must still have a properly wired GPS module with good access to the satellites! Without the serial data from the GPS it will not know when to begin the WSPR transmission, and without the PPS signal it will not be able to compensate for inaccuracy in the onboard crystal.
-
Voltage Issue: Use a simple voltage divider to combat gradual voltage increase at sunrise.
-
GPS Module: Stays on during transmission for continual frequency shift correction.
-
Power Consumption: Runs RP2040 at 48 MHz CPU clock, reducing power draw.
-
Onboard LED Behavior:
-
Blinks for ~three seconds on powerup.
-
Rapid blink until GPS serial comms are established.
-
1 blink: No GPS fix, not transmitting.
-
2 blinks: Valid GPS location, not yet transmitting.
-
3 blinks: Valid GPS location, transmission in process.
-
4 blinks: GPS fix lost, but transmitting.
-
"breathing" (gradual dimming of brightness): Indicates corrupted/invalid NVRAM settings. This is normal when using a brand new Pico. Press any key in a terminal to enter user-setup menu. If no key press detected for 10 seconds, will automatically reboot and try re-loading NVRAM values.
-
-
To use the PRE-COMPILED firmware download it by clicking here for the original version or here for the multi-band version and then skip to step 8. To compile it yourself follow steps 2-7.
-
Install the Raspberry Pi Pico SDK and "pico extras". This will depend on your environment. Here are the steps I followed on a fresh install of Debian on a Raspberry Pi 3:
sudo apt-get upgrade
sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential --fix-missing
(if above fails, do: sudo apt-get update , sudo apt --fix-broken install)
sudo apt install g++ libstdc++-arm-none-eabi-newlib
sudo apt install git
git clone https://github.com/raspberrypi/pico-sdk.git
git clone https://github.com/raspberrypi/pico-extras
cd pico-sdk
git submodule update --init
echo "export PICO_SDK_PATH=~/pico-sdk" >> ~/.bashrc
echo "export PICO_EXTRAS_PATH=~/pico-extras" >> ~/.bashrc
source ~/.bashrc
sudo reboot
-
Clone this repository: git clone https://github.com/EngineerGuy314/pico-WSPRer
-
cd pico-WSPRer.
4.5. To use the new multi-band branch by Kevin AD6Z: git checkout multi-band
-
./clean.sh (only needed the first time you compile)
-
./build.sh
-
Check whether output file ./build/pico-WSPRer.uf2 appears.
-
Power up pico with BOOTSEL held, copy the .uf2 file into the Pico when it shows up as a jumpdrive.
-
If you want to use the U4B protocol, go to https://traquito.github.io/channelmap/ to find an open channel. Before flight, make sure to register the channel you will be using by entering your flight details at http://lu7aa.org/wsprset.asp.
-
Connect to pico with a USB cable and a terminal program such as Putty. Hit any key to access setup menu. Configure your callsign and telemetry channel details from step 7.
-
You can use either one (or both) U4B and Zachtek protocol. If you only use one protocol its first message is sent on the starting-minute you enterred in step 8. If you are using both protocols, it starts the U4B messages on the starting minute, will then pause for two minutes and do the Zachtek messages (each protocol uses 4 minutes total). For Zachtek protocol (at this time) you must specify a numeric suffix that will be appended to your callsign. Enter dash (-) for the Suffix if you only want to use U4B protocol. Enter double dash (--) for Id13 if you only want to use Type3 (zachtek) protocol.
-
If the pico is plugged into a computer via USB while running it will appear as a COM port and basic diagnostic messages can be viewed at 115200 baud. The amount of messages shown depends on the Verbosity setting in the user config menu.
If plugged into a computer while running the pico will emulate a serial port. Connect to that port with Putty, HyperTerminal etc and you will see debug messages (unless verbosity was set to zero). press any key in the terminal and you will see this screen:
Most of the settings are self explanatory and described in the "Configuration and Use" section above.
-
Callsign: Maximium of 6 characters. no prefixes, suffixes, or slashes allowed
-
Suffix: Enter a '-' (minus sign or dash) to disable Zachtek mode. It will only transmit U4B protocol. To use Zachtek mode, enter a single digit numeric suffix to be appended to your callsign. A special case is if you enter the letter X for suffix. This will do the Zachtek (type1/type3) messages but will not append anything to your callsign.
-
U4B channel number: obtained from https://traquito.github.io/channelmap/. only needed if using U4B protocol. To disable U4B protocol (and only use Zachtek) enter a "--" (double minus) for Id13.
-
Verbosity (of debug messages on terminal): IT IS RECOMMENDED TO SET VERBOSITY TO 3 OR LOWER BEFORE FLIGHT!
- 0: none
- 1: temp/volts every second, message if no gps
- 2: GPS status every second
- 3: messages when a xmition started
- 4: x-tended messages when a xmition started
- 5: dump context every 20 secs
- 6: show PPB every second
- 7: Display GxRMC and GxGGA messages
- 8: display ALL serial input from GPS module
-
Oscillator Off: This should be set to '1'. If set to zero, the pico will output a continous RF signal even when not actively transmitting a packet.
-
custom PCB IO mappings: Set to zero if you assemble the tracker from Pi Pico and separate GPS module, per the schematics. Set to '1' if using a custom PCB based on the gerber files in /PCB -Telemetry config: (see section on extended telemetry) -Klock speed: DEPRECATED
The clock speed to run the rp2040 at (spelled with a K since thats the command to change it). Lower speeds give lower power consumption (minimum speed is 115Hz). Higher speeds can be used if you customize the program to use bands higher than 20M. The highest speed is 270Mhz, but not all RP2040's can handle that, its hit or miss. 180 is a pretty safe high speed. High speeds also produce a more pure output signal with less harmonics and more energy focused at the fundamental frequency.Rp2040 clock is fixed at 48Mhz for the fractional PLL -Datalog mode: not really used for pico ballooning. It spends most of its time in low power mode (only 2mA) and wakes up every 20 minutes to record the full GPS coordinates, temperature and battery voltage to nonvolatile flash memory (1MB total). No RF transmition. -
Battery (low power) mode: If set to '1' it will transmit only once per hour, and spend the rest of the time in sleep mode (with RTC still enabled). Current draw is about 2mA in Rp2040 sleep mode with RTC. Can't do better without an external interrupt source...
-
secret band Hopping mode: not fully tested yet, and has many limitations. It transmits U4B packets on both 20M and 10M.
- 0:
you must overclock by setting Klock to 270Mhz
- 0:
-
The multi-band branch by Kevin AD6Z lets the user select the 20, 17, 15, 12 or 10 meter bands.
-
NOTE:
For using bands other than 20M you MUST OverClock the RP2040! These are anecdotal observations, but you must use a Klock speed of at least 150Mhz for 17m, and at least 230Mhz for 10m. In general, higher klock speeds give a cleaner and more powerful output signal, at the expense of higher power consumption. But there is no guarantee that a particular board can support speeds above 133Mhz, they may cause the pico to lock up after boot. If this happens, you can can power cycle the pico and you will have a few seconds to enter the user-setup menu to try a lower speed. During initial boot and during the user-setup phase the pico is always kept at default speed of 133Mhz.
-
Standard Schematic: Low-side drives the GPS module with 3 GPIO in parallel:
-
Alternative Schematic: Uses MOSFET to low-side drive the GPS module:
-
Standalone mode: If using a hardwired power supply such as for a base station beacon, you can skip the resistors on the Run input and leave the GPS module always powered.
-
Custom PCB Schematic: In addition to the gerber and bom, the /PCB folder has the EasyEDA project file. This is a screenshot of the schematic from version 3 of the custom PCB. This one includes triple ideal-diodes (for pyramid solar panel arraignments) and a step-up voltage regulator to theoretically allow using 3 solar cell per panel.
-
Dallas OneWire Temperature sensors: The tracker will automatically read temperatures from up to 4 DS18B20 sensors, and can send the values via extended-telemetry. The sensor data bus needs to be connected to GP27 (no external pullup resistor needed). GP27 is the same pin as ADC1, and is available to connect on the custom PCB also.
We are documenting the code using Doxygen. Check the documentation here.
Tracker mounted beneath solar array, before first successful flight. This GPS module had a small battery and EEPROM, later version used bare module instead.
Shows ATGM336H GPS receiver and the Pi Pico board after trimming excess weight.
Completed V3 unit before flight. GPS receiver is flipped over and glued to the RP2040. Total weight including GPS antenna 3.5g.
New proffered configuration has the GPS module glued to the back of the Pico. This board was wired without a MOSFET, using 3 GPIO to directly drive the GPS.
V2 was launched and circumnavigated the world in 13 days.
Typical solar powered tracker and balloon before launch. Balloon is deliberately under-filled with hydrogen to allow room for expansion at high altitude. 12-13km altitude is typically reached.
Global tracking with such low powered transmissions are possible thanks to the extensive network of WSPR receive stations.
(PCB design is gratefully based on the AG6NS_PicoBalloon_RP2040_v0.4 tracker design posted here: https://github.com/kaduhi/sf-hab_rp2040_picoballoon_tracker_pcb_gen1 Thanks Kazu!)
The Goal of this project was a tracker that is not only cheap, but that the only components needed to make it were available with free overnight shipping from Amazon etc in quantities of 1.
That is still true, but a custom PCB has also been created that uses the same exact pico-WSPRer program, and keeps the minimalist RP2040/ATGM336H combo. It can be ordered relatively easily from JLCPCB. The cost of my latest order, using the cheapest shipping and taking advantage of the frequent coupons, was less than $11 a board (buying a quantity of 5). Cheaper than buying a pico and ATGM336H separately!
The advantage of the pre-made PCB (besides less hand soldering and slightly lower payload weight) is that it uses a proper voltage monitoring watchdog chip, instead of the resister voltage divider. V0.1 of the PCB has been successfully flight tested. V0.2 (show below) is electrically the same just smaller.
gerbers and BOM are in the /PCB folder. Unlike a Pi Pico, there is no BOOTSEL button. You must temporarily short the BOOTSEL terminal to ground when powering up to be able to load the firmware. After loading FW and configuring callsign etc the portion of the board with the USB connector can be snapped off to save weight.
Power consumption was 48mA when supplying 3.3V directly to VBUS and transmitting into a 70 ohm load. V3 PCB with boost converter pulls 96mA @ 2v or 73ma @ 2.5v. Tested while xmitting into 70ohm load.
Rev 2 of the PCB: (only 2.1 grams after snapping off the USB-C connector!)
Rev 3 of the PCB: (heavier at 2.4 grams due to ideal diodes and boost converter)
NOTE: The custom PCB uses the same exact binary as the original Pi Pico, but in the user config menu you must change "custom Pcb mode IO mappings" from 0 to 1.
This tracker uses the version of extended telemetry documented here**: https://traquito.github.io/pro/telemetry/extended/
This is referred to as "DEXT" (Documented EXtended Telemetry).
After the first two U4B style packets are sent, up to 3 more DEXT messages can be sent each period.
Each DEXT packet can contain multiple variables with unique ranges specified for each. A maximum of 29 effective bits of data can be in a single message.
You can modify the sourcecode and directly set the telem_vals_and_ranges array with ranges and values for up to 10 values per message. But DEXT can also be enabled and configured for a few pre-selected functions via the user interface:
- DEXT types (for each of the 3 slots)
- -: none
- 0: Minutes Since Boot, Minutes since GPS fix, GPS Valid, Sat Count (max: 1000,1000,1,60)
- 1: ADC 0, 1, 2 (in tenths) (max: 350, 350, 350)
- 2: bus volts ADC3 (in tenths, scaled), Dallas 1 (and sign), sat count (max: 900,120,1,60)
- 3: Dallas OneWire temp 1, 2 (and signs) (max 120,1,120,1)
- 4: Dallas OneWire temp 3, 4 (and signs) (max 120,1,120,1)
(analog values are sent as volt-hundreths, so the max value seen will be about 330. However, if reading ADC3 (which is connected to VBAT via a 3:1 voltage divider) the reading is automatically multiplied by 3. ie a 4.5 Volt battery voltage will be read as 450 if reading ADC3)
The options minutes-since-boot, and minutes-since-GPS-fix-aquired are useful for debugging flaky voltage supplies or GPS reception.
** adheres to the DEXT standard except for the following (to simplify implementation):
- no non-zero minimum values
- max of ten values per message
- only step size of 1 is supported
- DEXT allows for up to 5 extended telemetry messages per 10 minutes. But to maintain tracking capability on multiple sites, a set of 2 U4B style WSPR messages must always be sent each 10 minutes, so you are limited to 3 DEXT messages per period (slots 2,3 and 4).
In order to view the results of Extended Telemetry you must carefully setup your tracking on https://traquito.github.io by clicking the "gear" icon. In this example, my DEXT configuration was set to "00-" which means type-0 custom message for the first two slots, and nothing for the last slot.
The units and values and limits depend on how you configure your own custom telemetry. If you use one of the 5 pre-defined Extended Telemetry packets in pico-WSPRer, the units and values are documented at the end of Main.C, and also here:
If you want to use a Pi Pico 2 (or other board with RP2350 processor) it should work, although I have not tested it. You will need to recompile like this (and you will lose the battery mode/low power option):
- Delete the entire Build folder
- change line 12 of pico-WSPRer\CMakeLists.txt to: set(PICO_BOARD pico2 CACHE STRING "Board type")
- delete line 74 in pico-WSPRer\CMakeLists.txt
- in main.c delete lines 28, and 29
- in WSPRBeacon.c remove lines 223 through 237 and lines 12 and 13
- in TXChannel.c there are 3 places, on lines 108,109 and 110 where you change TIMER_IRQ_0 to TIMER1_IRQ_0
- run ./build.sh
- it may take much longer to build than usual
There is probably a better way to change from pico to pico2 without having to re-create the whole build folder, but this was the only way I got it to work.
When the GPS module obtains a fix, the PPS output is examined and a frequency compensation is automatically worked out to compensate for the marginal precision of the onboard xtal. This compensation will remain active even if the GPS signal is lost, but without GPS lock further compensation of drift (due to thermal variation etc) won't be compensated for, leading to missed spots!
Unfortunately, when the pico is xmitting RF the interference can easily overwhelm the sensitive GPS receiver. The pico temperature always rises a couple degrees when transmission begins, and as it does for a minute or two the output frequency drifts enough to make reception of the first transmission unreadable.
It is very important to use a dipole (not monopole) for the GPS receive antenna. This design has proven to work well, and not lose GPS lock even while transmitting. It has a total dipole lenth of 94 mm, and an approx 1 inch section of twisted feedline to act as a crude impedance match:
Another simple mitigation is to pre-heat the board before the transmissions start, which can be done by scheduling a non-critical Extended-Telemetry message in the slot before the main transmissions begin. So for instance, a DEXT telemetry configuration of "0-0" is very effective. It means that after the regular Callsign and basic-telemetry packets are sent, a "type-0" Extended telemetry is sent, followed by nothing, and finally another type-0 Extended telemetry before the process begins again. If GPS reception drops during transmission, the second Extended-telemetry transmission acts to pre-warm the pico before the important callsign packet. The gap ('-') in between Extended telemetry packets provides some downtime between transmissions to let the GPS receiver re-acquire lock. The 2nd extended telemetry may not always be reliably received, but you should have a stable frequency for the main transmissions (and at least for the first Extended Telemetry).
There are several other methods of reducing electrical noise and improving thermal stability, as documented by HB9TUP:
Balanced GPS Antenna Connection
Added a small balun (red box) to connect the 37mm dipole halves for L1 GPS signal reception to the ATGM336H GPS receiver.
Since the GPS chip has an unbalanced input, while the dipole is inherently balanced, this helps decouple ground loops between the Pico and GPS.
I verified the dipole/balun resonance on L1 using a NanoVNA.
Ferrite Toroid for HF Noise Suppression
Placed a ferrite toroid (blue box) between the ATGM336H GPS module and the Pico to reduce HF interference along the connecting wires.
Noise Filtering
Added a 1µF capacitor (green box) across the Pico’s main power rails (+ and -) to filter noise.
Interestingly, adding a capacitor directly across the GPS power rails worsened stability rather than improving it, as ground testing showed.
Quartz Crystal Thermal Isolation
Encapsulated the Pico’s quartz oscillator (yellow box) with XPS to reduce sensitivity to temperature fluctuations due to gusts.
I observed that even minor air movement caused significant frequency shifts, so encapsulating the crystal improved stability.
Prevention of the Chimney Effect
Sealed the bottom opening of the triangular prismatic solar panel structure with Saran wrap.
This prevents the chimney effect, where heated air inside the prism (due to solar heating of the dark solar panels) could create wind gusts by sucking in cold air from below.`
DISCLAIMER: This project transmits on the 20 meter ham radio band: you must have an appropriate amateur radio license to do so legally. Also, toggling a microcontroller IO pin theoretically generates a square wave, which theoretically has an infinite number of high order harmonics. However, RP2040 io pin circuitry is not particularly efficient at generating very high RF energy. Furthermore, the dipole antenna is trimmed to be resonant at 14.1Mhz and does not do a very good job at radiating anything else. During testing the amount of RF energy actually emitted outside of the passband was well within limits. You are encouraged to perform your own testing and utilize additional filtering as needed to meet your local regulations.