2024-06-19
TL;DR: I built an inverted vee antenna for the 10 meter amateur band and have been using it with my HackRF One and some kit modules from QRP Labs to operate on FT8. The harmonic distortion of the resulting signal is unacceptable at transmit powers above 1 W, but this is still sufficient to make contacts under reasonable conditions.
UPDATE 2024-12-15: I took this antenna down as a part of my next and more ambitious off-center-fed dipole (OCFD). This inverted vee worked well, but it didn’t make sense to keep it when I could reuse the feedline for the OCFD and get better performance on 10 m at the same time. Also, the design I used for this project is flawed because it collects rainwater. It seems that putting the PVC screw cap on the top side was a bad idea.
My initial plan was to build a horizontal dipole antenna, since that seems to almost be the canonical design. I learned quickly that it’s important to mount the antenna about half a wavelength off the ground in order to give decent gain at low elevation angles. The reason seems to be that the ground acts a passive reflector, so the way it interacts with the antenna’s radiation pattern changes with height. If the antenna is too low, then a good portion of the RF energy will be reflected vertically, which can be useful for local communication but trades away DX performance. I don’t know whether mounting the antenna too high is bad for RF propagation, but at some point adding more height becomes impractical. I never got around to figuring out why the optimal height depends on wavelength.
It has become a habit of mine to simulate projects whenever possible.
This approach has helped me inexpensively optimize and avoid mistakes in
the past. I used xnec2c
, which is a
descendant of the Numerical
Electromagnetics Code (NEC). The authors of that program wrote it
about 50 years ago for a mainframe computer in FORTRAN with input
provided on punched cards. This awkward interface carries over to the
modern version, where the user must configure virtual cards and place
them in a proper order before execution. It’s not the kind of software
we might build today, but in this case I’m willing to put up with bad UX
in exchange for maximum correctness. I like when the core logic of a
system has proven to be so useful and representative of the real world
that it has stuck around for decades. It wouldn’t surprise me if this
code persisted for much longer than it already has. After all, it’s not
like its underlying
specification is likely to change.
For a wavelength of 10 m, I was targeting a mounting height of 5 m. At first I thought I’d install three lengths of PVC on three posts from my fence project. I was almost ready to start construction when I realized that the chicken wire could have an effect on the RF propagation. I wrestled with the virtual cards until I found a way to represent the four metal mesh panels around the edges. I used conductive planes, which I think is a fair approximation given that the wavelength is much larger than the mesh size. The result was a highly distorted propagation pattern, so I dropped the idea and began searching for a new location.1
I explored other designs and eventually found the inverted vee. This design is identical to the center-fed half wave dipole that I originally considered, except the two radiating elements form a right angle instead of a straight line. The two ends are then secured to the ground with insulating connectors and rope. This design has two big advantages for my situation. One is that it only requires a single elevated mounting point instead of two or three, which simplifies construction. The other is that the input impedance of the inverted vee at resonance is ~50 Ω, while for the straight dipole it’s ~70 Ω. This removes the need for any impedance matching components, since practically all amateur equipment is standardized on 50 Ω.
It wasn’t too difficult to create a model in xnec2c
once
I minimally understood the virtual punch card thing. I wrote a tiny
Python script to give me the start and end coordinates of each radiator
given the apex height and radiator length.2 I
found that radiators that were slightly longer than the expected quarter
wavelength of 2.5 m worked best for achieving resonance near the middle
of the 10 m band. The value that seemed best was 2.63 m. This makes
sense because at the center frequency of 28.75 MHz, the RF wavelength is
about 10.43 m, and one quarter of that is about 2.61 m. As expected,
apex height mostly affected the shape of the far field pattern, with
elevations slightly higher than half a wavelength giving what seemed to
be a reasonable pattern for DX while not being excessive for my
situation.
To begin building the antenna, I duct taped a rope to a baseball and threw it over a tree branch. This took several attempts and became easier when I realized I could underhand the ball like a sling. The ball often either didn’t make it over any branches—let alone the one I was aiming for—or got stuck. After two or three dozen attempts in a few locations, I managed to snag a branch in a good spot about six meters up. Later, I cut the rope and tied it into a loop. Since the rope can slide freely over the branch, this forms a simple pulley system for raising and lowering the feed point.
For the feed point, I bought some three inch PVC pipe, a smooth end cap, and a screw cap. I cut the pipe to length and installed the caps with PVC cement. Then, I drilled a hole in the top and several in the sides. An eye hook screws into the top so that the feed point can hook onto a loop in the rope, and the coax feeds in from the side near the middle. I stripped the end and twisted the shield away from the conductor before soldering each of them to their respective radiator wires. The radiator wires then feed in and out of the side holes to provide strain relief.
On the outside, the coax makes several loops around the PVC to form an RF choke. This is important because of the way dipole antennas work. Since the shield and conductor are both connected to separate radiators, the shield in the feedline can act as part of the antenna. For long coax runs, this is bad because it messes with the antenna’s behavior and gathers noise (so I’m told). This behavior is not present in the conductor because it’s protected by the shield, which is the whole point of using coax at all. By coiling the coax several times, an air core inductor is formed out of the shield. The conductor is still protected because of the properties of the coax, but common mode RF on the shield sees a high impedance and is blocked. Differential mode RF is passed because its contribution to the inductor’s magnetic field is balanced by the equal and opposite current in the conductor. (At least, I think that’s how it works.)
I used this calculator and some key datasheet parameters to estimate how many turns I would need, assuming I wanted the impedance to be at least 500 Ω. I tested the resulting coil by connecting the conductor on my nanoVNA to the conductor and shield of the coil input. This forces the test signals from the nanoVNA to be common mode. The return loss was about 0.25 dB, which means that practically all the power was reflected. This is the expected behavior because the common mode RF input sees a high impedance at the coil.
I added clear silicone to all of the holes and dropped some dry rice in the tube to try to soak up any water that might still get in. After all that, I had an odd-looking capsule.
I ran coax from my lab bench to a window, through a CTC-50M window jumper, through more coax, and up to the feed point. I measured the total insertion loss of this feed line to be 3.1 dB on 10 m. I used RG-58 to reduce cost, and I probably could have reduced the insertion loss by one dB or so by using more costly coax, but I didn’t see the value in it. Fortunately, coax insertion loss decreases as frequency decreases, so if I ever reuse this feed line for lower HF, I’ll have lower loss. Installing all of these connectors took a long time, partly because I made a few mistakes and had to redo a few. I found by the end that I had gotten okay at the procedure just in time to not need it again for a while. Still, it was pretty fun because it’s a highly physical and manual process.
At first I thought I might buy or print some plastic insulators, but instead I cut some slices of pressure treated wood leftover from the fence project and drilled holes to allow for strain relief on the radiators. These have held up well. To secure the radiators to the ground, I used ordinary string looped through the wooden insulators on one end and bungie cords on the other. I then hooked the bungie cords to ground staples. The radiators are supposed to form a right angle, so I knew that the horizontal distances between each feed point and the anchor should both be equal to the feedpoint height. I aligned the anchor points so that the main lobes of the antenna would point roughly in the directions of the continguous US and Europe. The antenna was resonant more or less as predicted after two rounds of radiator trimming and measurement with the nanoVNA.
The antenna and HackRF both worked well for receiving on ten meters.
I used SDRAngel for all
of my testing, and after getting familiar with the interface it became
easy to configure it for convenient operation. Piping the audio into
WSJT-X on Debian was somewhat difficult, but I used qpwgraph
to
make this easier. The propagation conditions were good during this
period (early May), so I was able to hear SSB voice from as far away as
Las Vegas and the Dominican Republic, CW beacons from California and
Mexico, and FT8 from South America, southern Africa, southern Europe,
and Australia. A few weeks later, there were periods
during which it was difficult to hear anyone. It’s become clear to me
that ten meters is unreliable for long-distance communication—even
accounting for the upcoming solar maximum—but that makes it more fun in
a way because it’s a challenge. I can see why US Technicians don’t get a
lot of privileges
on lower HF but get some on ten meters. I wonder if licensing in
equatorial countries is different for ten meters due to the generally higher maximum usable
frequency in those regions.
I have avoided mentioning the transmit side of this project in order to prevent excessive front-loading. Fortunately, adding some transmission hardware to the project wasn’t too difficult compared to what I’d already done by this point, which is partly because the receiver and transmitter rely on a lot of the same equipment. The first essential part of this subsystem is the transmit/receive (TR) switch. This is a hardware device that switches between two electrical paths between the antenna and the HackRF. When the HackRF is trying to receive a signal, the TR switch must connect the HackRF directly to the feed line. When the HackRF is trying to transmit a signal, the TR switch must connect the HackRF to the amplifier and filter, which then connect to the antenna. It’s critical that the TR switch only connects exactly one of these branches to the HackRF at a time because otherwise the HackRF could end up transmitting into itself. I don’t know what happens in that case, but I assume it’s bad because the HackRF is a sensitive device.
You can think of this device in the same way that you might think about talking to someone through a long pipe. You can only put your mouth or ear up to the pipe at any one time, and given the choice you would probably prefer not to shout into your own ear. The act of changing between speaking and listening is the same task that the TR switch handles. The HackRF already does this internally, but we need to externalize it because we’re using a discrete amplifier and filter.
It turns out that the HackRF has LEDs that indicate whether it’s in transmit or receive mode, and the signals that drive those LEDs are broken out on the board as headers in group P5. With the antenna port facing up, the TX signal is at the (1,1) position, and the RX signal is at the (2,1) position within the header group. I had to remove my board from its stock enclosure and solder on some female headers to access the signals. Both signals are 3.3 V positive logic. These are the inputs to the TR switch logic circuit.
The goal of this circuit is to enable exactly zero or one of the chains at a time. That feels a lot like XOR, and it is in a way, but it’s not quite the same because there are two outputs. Here’s the truth table:
RX In | TX In | RX Out | TX Out |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 |
0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 |
Notably, if for some reason the RX and TX signals are both enabled,
this design disconnects both outputs, which I think is the safest
option. For each output, the boolean expression is
X && !Y
. I used a 7400 series quad AND gate for the
&&
part, and I used BJT common emitters for the
!
, which conveniently level shifts the input to 5 V. 3.3 V
is enough to drive the logic high for the AND gates, though, so this
isn’t strictly necessary. The outputs of the AND gates go to relays that
switch the corresponding signal chain into the HackRF.
I was surprised by how well this part of the system worked. It came together quickly and has yet to be a problem.
At first I’d hoped to design and build my own TX amplifier and filter, but for now that’s beyond my ability. I’ve ordered a few books on those topics (among many others) and have already begun greedily working through them.3 Fortunately, there are kits available from QRP Labs that have all the design work baked in, so all I had to do is wind coils and solder components. Good enough for the moment.
I ordered the 10 W linear amp and the 10 m low-pass filter for TX. The LPF was easy enough, and the amp took longer but still wasn’t too bad. The instructions are clear and written in an approachable way, so I didn’t feel like I was fending for myself too much. I made several mistakes throughout the assembly processes, but the instructions include detailed schematics, layout drawings, and advice for checking continuity. The kits perform as advertised and seem quite forgiving.
To test the amplifier, I designed and built a step attenuator like the one shown below. I say “like” because I didn’t end up building all the stages, but I built enough of the system to make it useful for my purposes and prove that I’d done it right. The trick seems to be designing each stage to be a good match for 50 Ω, which is easily accomplished with pi pad calculators. The attenuator dissipates most of the input power so that I can safely measure it with my tinySA. I used the nanoVNA to measure the attenuation, and then I set that as the negative external gain in the tinySA. This causes the tinySA to remove the effect of the attenuator for display purposes and give the actual amplifier power. Under this setup, the amplifier shows a gain of 23–29 dB, decreasing as input power increases. This closely matches the advertised gain of 26 dB.
I had some trouble when connecting the LPF to the output of the amplifier that I still can’t explain. Somehow the interaction between the LPF, the amp, and the HackRF caused a strong spike around 70 MHz on the output. I think this may have been a grounding issue, since I was using my benchtop supply to power the amp and my laptop to power and drive the HackRF. Connecting both grounds together seemed to help, but later it became unnecessary. Maybe my amateurish connectors finally settled, or something like that.
In any case, the harmonic distortion of this setup is not good. At +30 dBm fundamental output power, I managed to get the highest harmonic down to -45 dBc, which is legal but only barely. In other tests, the harmonics were much worse, particularly as power increased. I think the HackRF is mostly to blame for this, as tests with it alone seem to indicate. It would probably be better to put a preamplifier between the HackRF and the linear amp, rather than force the HackRF to operate near its max TX power (~+15 dBm) where it probably has the most distortion. Maybe in the future—when I’m hopefully smarter about this stuff—I can try to homebrew a preamp for this system.
WSJT-X is the standard software package for running FT8 and related weak signal digital modes. It can take any virtual audio source as input and send output on any virtual audio sink. It’s basically a software modem that uses the system’s existing audio interface. It also provides a GUI for monitoring the band and making contacts.
By convention, FT8 transmissions always appear on an SSB channel at a particular frequency in each band. This simplifies radio configuration, since you just have to set a particular dial frequency and then leave it alone. WSJT-X will do its modem thing on whatever 50-ish Hz channel you pick within the 3 kHz SSB channel, and the radio should happily accept it like any other audio. This makes FT8 (or WSPR, JS8Call, etc., on other dial frequencies) act like a “band within a band”. In addition to being practical for low-power systems like the one I’ve built here, FT8 is attractive to me because of its high bandwidth efficiency.
Off-the-shelf radios often support rig control, which is an interface that the radio exposes that allows external systems to modify its state. In practice, it allows software on a laptop to run the radio as if the operator were pushing the buttons and turning the knobs. WSJT-X knows how to talk to a lot of radios, but not HackRF One (as far as I know). In principle, I could fix this by manually switching between RX and TX as needed, but FT8 operates on 15-second windows, so I’d have to be pretty accurate. That would get annoying, so instead I set about creating my own interface between WSJT-X and the rest of my radio setup.
This is where I got really lucky. I thought I’d need to hack together some sort of nasty GUI-based point-and-click solution for getting WSJT-X and SDRAngel to talk to each other. It turns out that none of that was necessary because both programs have APIs. WSJT-X writes status data periodically on a UDP port in a custom but documented format, and SDRAngel has a lovely REST API with interactive docs. It only took about an hour for me to write a Python script to glue these APIs together, after which it worked great. The only problem I’ve had with this code so far is that sometimes I forget to run it and wonder why the radio isn’t working.
#!/usr/bin/env python3
import socket
import urllib.request
def skip_string(msg, idx):
return idx + msg[idx+3] + 4
= "http://localhost:8091/sdrangel/deviceset/{}/device/run"
BASE_URL = urllib.request.Request(BASE_URL.format("0"), method="POST")
start_rx = urllib.request.Request(BASE_URL.format("0"), method="DELETE")
stop_rx = urllib.request.Request(BASE_URL.format("1"), method="POST")
start_tx = urllib.request.Request(BASE_URL.format("1"), method="DELETE")
stop_tx
= False
tx
# Listen for UDP messages from WSJT-X
= socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
udp_server "127.0.0.1", 2237))
udp_server.bind((while True:
# Receive message
= list(udp_server.recv(1024))
msg if msg[:4] != [0xad, 0xbc, 0xcb, 0xda]:
print("Invalid magic number")
continue
else:
print("Valid magic number")
if msg[11] != 0x01:
print("Wrong message code")
continue
else:
print("Got status message")
# The message is a status update
= 12
idx = skip_string(msg, idx)
idx += 8
idx = skip_string(msg, idx)
idx = skip_string(msg, idx)
idx = skip_string(msg, idx)
idx = skip_string(msg, idx)
idx += 1
idx # Extract `transmitting` boolean
= msg[idx] != 0
new_tx # Forward new TR state to SDRAngel
if new_tx != tx:
= new_tx
tx if tx:
urllib.request.urlopen(stop_rx)
urllib.request.urlopen(start_tx)print("TX")
else:
urllib.request.urlopen(stop_tx)
urllib.request.urlopen(start_rx)print("RX")
print("Notified SDRAngel")
else:
print("TR state has not changed")
With this last piece completed, I had a full transceiver.
Despite the poor propagation conditions in my area over the past few weeks, I’ve made eight contacts on 1–2 W with FT8. The furthest of these was with a station in Belize about 3500 km away. Quite often I’ve been able to hear stations that can’t hear me, which is frustrating, but if I make improvements to the amplifier chain in the future then I’ll probably solve that problem. PSK Reporter has been useful for determining where I can be heard and at what level. One time I got especially lucky, and a station in Germany heard me from about 5500 km away.
PSK Reporter is also helpful for determining whether or not I’m even getting out at all. This setup is pretty messy and isn’t especially user friendly, so I have made mistakes several times where I try to transmit but e.g., SDRAngel isn’t configured correctly, the TR script isn’t running, the power supply is off, etc. But once it’s configured right, this system is pretty reliable (by semi-homebrew standards). When it all works and somebody replies to me from far away, it’s exciting. The fact that we’ve figured out how to communicate by wiggling electrons around is amazing.
I’ll probably use this station only from time to time for a while. The low transmit power limits me to those times when propagation conditions are good. Apparently summer is bad for propagation, so I might have to wait until the fall or winter before I can get reliable performance. For the rest of the time, I can at least listen. I might also poke around on the citizen’s band, which is right next door to 10 m, and see if anything interesting appears. Someday maybe I’ll improve the amplifier and get better TX performance as well, which should reduce my dependence on ideal conditions somewhat.
Even if the exact model output wasn’t accurate, the qualitative result still alerted me to the likelihood that the metal mesh would meaningfully change the situation.↩︎
This was necessary because the GW
card only accepts start and end positions in Cartesian coordinates,
but I wanted to tweak the apex height and radiator length instead. These
values are related by some trig expressions, and having a little code to
spit out the right values made iteration a lot faster.↩︎
It turns out that you get what you pay for. Scraps of knowledge from blogs and YouTube videos can be useful for answering particular questions, but a solid textbook with a focus on practical application is still the best learning tool I know of.↩︎