In embedded systems, using hardware timer interrupts instead of software timing functions (like millis()) is essential for achieving precise timing in real-time applications such as radio transmission, as software timing can be disrupted by other computational tasks, causing timing deviations that affect signal quality.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
HF Transmitter Part #28: Updated SoftwareAdded:
Hi, I'm Darren and welcome to Level Up E Lab. Today I'm going to go through all the software changes I made to my HF transmitter and it's going to get very technical. So, let's get started.
All right, let's begin this dive into my software by starting with how the data link layer of the PS2 protocol works. It uses an 11bit data frame sent on a clock frequency somewhere between 10 kHz to 16.7 kHz. That's very slow by any modern datab bus standard. But on the upside, decoding it to a byte stream using a microcontroller will be easy. But converting that data stream into ASKI characters is a bit more complex because those incoming bytes are completely different from the standard ASKI character chart. Plus, there are extra bytes like F0 for key releases and E0 for special modifiers. And the host application has to keep track of the shift keys and control keys. The keyboard does not do that. But no need to reinvent the wheel. I found a really good library written by Paul Stogage and others on GitHub that does the heavy lifting. It does a lot more than I need.
So I trimmed it down to remove the foreign language support and a few other things that I didn't need just to keep the code as small as possible. Okay, so that library will give me a stream of ASKI characters. But now how do I set up a sequence of dits and dashes for each one? A simple way would be to build a table that associates each asy character with a string of its dits and dashes.
Let's say I populate an array of strings that uses the period character to indicate a dot and a hyphen character to indicate a dash. So for the letter A, for example, we have a dot, a dash, and a null terminator for a total of three bytes. Punctuation is a lot longer at up to seven bytes per character. For 26 letters, 10 numerals, and 16 punctuation symbols, these 52 characters will require 274 bytes of memory. If I were writing this code for a PC or for a smartphone, well, who cares? Memory is plentiful. But this code will be for a microcontroller, and memory is a precious resource. I can do a lot better than 274 bytes. In fact, I figured out a way to use only a single bite of memory for each character to encode its Morse code sequence, not 3 to seven bytes.
Here's how I'm going to use those eight bits in that single bite. Each of the 52 characters is made up of anywhere from one to six dits and dashes. I can assign the first six bits such that a zero means a dit and a one means a dash. I'll start with reading bit zero, then bit shift to the right to read bit one and so on. The Morse code sequence is encoded backwards this way, which is a little awkward, but I only have to build this encoding scheme one time, so it's not a big deal to figure it out for each character. That leaves me with two bits remaining, bit 6 and bit 7. I can use those to indicate the length of a character. Let's look at the 26 letters first. Each one has between 1 to four dits or dashes. So 0 0 would mean a length of one. 01 would mean a length of two. and so on up to one one equaling a length of four. My code would process only those bits that were indicated by the length. So for the letter A which has a dit and a dash, bit 0 and bit one would be read and processed while bits 2 through 5 would be ignored. So I'll just set those to be zero. Using this scheme, I get the binary sequence 0 1 0 0 0 1 0 for my encoding bite for the letter A or in hexodimal that's equal to 42. For the letter B, which is dash dit dit dit, it would be 1 1 0 01 or C1 in hex. And I just wash, rinse, and repeat for the rest of the alphabet.
Now, how about numbers and punctuation?
Those have five or six dits and dashes.
My first six bits can accommodate that, but I'll need a different length encoding scheme. I've only got bit six and bit 7 to work with, but I've already used them up for encoding lengths from 0 to 4. Well, here's where the standard ASI table comes to my rescue.
Fortunately, all of the punctuation and numbers lie between ASKI decimal codes 33 to 64, and all the letters are between 65 and 90. So all I have to do is check the ask key value of a particular character. If it's between 65 and 90, it's a letter and I'll use the encoding scheme I described previously.
If it's between 33 to 64, I'll use a second encoding scheme where 0 0 now means a length of 5 and 01 now means a length of six. Brilliant. So instead of gobbling up 274 bytes of memory, I now only need 52 bytes to encode all of these Morse code characters. Now to be fair, to do an exact memory usage comparison, I'd have to include the code overhead as well, but my gut says doing bitwise operations will always be more compact than manipulating strings. So I'll be even more ahead with this approach. There are six punctuation symbols between codes 33 and 64 that I did not include. And that's because I could not find a common accepted international standard for them. And one of them, the dollar sign, is actually seven dits and dashes, which would have completely broken my scheme. But even so, many of these punctuation symbols are rarely if ever used in amateur radio communications. But where they existed, I included them anyway. Now that I've got an encoding scheme for my 52 characters figured out, the next thing to tackle is how to transmit them. In my original code, I use a state machine to control the hardware for sideband and manual CW, and it works very well. The main triggering event comes from the variable PTT state. It's determined by reading an Arduino input pin, which is connected to the mic button and to my straight key. I tried adding code that uses the Milliey's timer to turn PTT state high and low to correspond to dits, dashes, and spaces, essentially like a virtual key. In fact, that was the code that was running in the prior episode. However, that ultimately proved to be a very bad idea. The more I used it, the more I noticed seemingly random irregularities in the transmitted code, especially if I typed fast while the rig was transmitting at 20 words per minute.
Sometimes the irregularities were tens of milliseconds. Now, when you're trying to generate 60 millisecond pulses at 20 words per minute, timing deviations like those make your code just plain unreadable. I spent quite a bit of time digging into why that was happening. And with a little bit of help from chat GPT, I discovered several blunders that I made in my choice of hardware and software. And here they are. The first was using a busdriven port expander to control the amplifier bias and side tone audio. Yeah, no surprise in hindsight using I squared C commands to control those timing critical functions was just not a good idea. I missed just how much overhead and computation time is involved. So I made the executive decision to scrap using the 008 for those two functions and I reverted back to directly driving them from microcontroller pins. That of course meant activating pins D5 and A0, which if you recall were left unused on the latest spin of the main board. For D5, all I had to do was populate the empty resistor and transistor footprints, and I was in business to use it to drive the audio side tone. For pin A0, though, I made no provisions on that board spin for those components. So, for now, I've bodgeged in this flying chunk of protoboard to hold a leaded 2N 3904 and a leaded 4.7K base resistor to control the amplifier bias. It is a bit ugly, but if I ever do another spin of the main board, I'll be sure to incorporate them directly. With the hardware issue corrected, it was time to fix my mistakes in the software. And there were many. The biggest were things like not separating the timer critical tasks from the timehoging tasks and executing those tasks by highly unreliable comparisons to the millie's timer. Philosophically, I was stuck thinking like a Windows application engineer and not thinking like a firmware engineer. I decided to reexamine each task and sort them into three groups by their priority of execution. Those tasks that need to be done the fastest include the obvious number one, turning on and turning off the bias and side tone to match the cadence timing for dits, dashes, and spaces. Handling encoder input and incoming serial commands are also a high priority, but only if the transmitter is idling. I can and should ignore them while I'm transmitting. In the middle are things like calculating the power and SWR data needed for the meter and the slower outputs that are staying on the 008 like the antenna relay and powering the sideband circuits. And then lastly are the largest time hogs like updating the display graphics and processing the keyboard input. The engine that will drive all of this sequencing will now be a dedicated hardware timer, not Milliey's math. I'm configuring timer TCB0 to fire every 250 counts, which at a count frequency of 250 kHz means this interrupt driven timer will fire every millisecond. Next comes the event handler for that timer.
This is the heart of the action and it's been optimized to execute as fast as possible. Let's break it down. Cy tick is my custom millisecond counter that I use later for triggering the medium and lowest priority events at the end of the function. KBM countdown is another millisecond resolution counter that I'm using to control the dits, dashes, and spaces. It's set as needed by examining the array TX sequence, which is a bite array of timing events that's compiled from the typed characters. Here's the encoding scheme for those bytes in TS sequence. Bits 0, 1, and two are the multiplier to bit time. This can be either 1, 3, or 7. Bit four is a flag to indicate that this sequence is the last one for this character. And bit 7 indicates that this sequence is key down or key up. I'm showing here an example of what this scheme looks like for encoding the letter Q. This scheme lets me use bitwise operations throughout, which are super fast. And these bias and tone on and off functions are just inline functions that use Vport for direct digital port manipulation. Also super fast and much faster than the digital write command. I also trigger an analog to digital conversion here for the meter. And lastly, here are the lowest priority counters and other service flags for events like checking for touchscreen presses, updating the user interface display, and checking for key presses. These vary from every 4 milliseconds to every 128 milliseconds.
So just how fast does this interrupt routine execute? I don't have a direct measurement, but if chat GPT is to be trusted, it estimates between 6 to 12 microsconds depending on the actual logic path. And that's no problem triggering every millisecond. That's only about 1% of the processor time.
Okay. Now for the typical Arduino loop function. First, I handle function key editing and transmission aboard. if they're needed. Then handle the pushto talk and the transmit state machine, both of which return quickly if nothing is happening there. Then we see that I'm checking which flags were set by the ISR and handling the ones that are set. Then lastly, I handle those tasks that are only done while idling. Things like reading the encoder, the touchcreen, and incoming serial commands, which has its own little state machine. And that's it.
Compact and efficient. Back to the transmission state machine. I've left unchanged these sections here for sideband and manual CW. For the keyboard generated CW, I've added these three new states starting with KBM idle. Here we're just waiting around for characters to appear in the buffer. Once they do appear, we move forward if the user has enabled send immediate mode or if that mode is not enabled. We wait until the user has pressed the enter key. If we are moving on, we have a lot to do. Mute the receiver, energize the antenna relay, turn on the VFO oscillator, enable meter updates, and set the KBM countdown to KB engage, which I've set for now to be 15 milliseconds. That gives a little bit of time for all of those events, especially the antenna relay to finish before the interrupt routine starts processing the dits and dashes. And we advance the state machine to KBM transmit. Now we do one of two things. Once a character has finished transmitting, we pop it from the to send buffer and push it to the sent buffer.
And we flag the display to be redrawn.
If we've finished sending all the characters in the buffer, then we reset the indexes for the TX sequence array and advance the state machine to KBM windown. Here we reverse the steps taken back when we left idle like deenergize the antenna relay, tell the receiver to unmute and so on. and then go back to the KBM idle state to resume waiting.
It's pretty simple because all the heavy work was done in advance to fill the TX sequence array and all the live work is done by the interrupt routine. So how does the TX sequence array get filled?
That happens after every key press by calling pars to TX sequence. Here I look up the DIT dash sequence bite that I described earlier. then step through it a bit at a time to build the TX sequence array. There's some finesse here to decide when to add inner element spaces versus character spaces, but otherwise the logic flow is pretty straightforward.
All right, I've got the rig set up to transmit at 20 words per minute. Uh the Siglet is in roll mode there in the background and it will show the actual transmitted waveform using my 40dB power tab. And I'm also sending that tabed signal into a comparator circuit here on this breadboard that then feeds a digital signal directly into another nano. And I'm using this to accurately record the timing of the dits, the dashes, and the spaces. And I'll put on screen the schematic of what that circuit looks like. C1, D1, and R5 make up a simple RF probe. Um, diode D1 is a 1N277 1N277, I think. Uh that's a Germanmanium signal diode. Now I chose it because it has a very low forward voltage drop. The signal from the power tap is only about 440 molts peak. So definitely want to use a germanmanium diode here. Then in capacitor C1 and R5, I chose those values just for fast response. I did some experimentation with different values to make sure that I didn't introduce too much delay in the signal rise and signal fall time and those values seem to work just fine. Then that feeds into an LM311 comparator which I've used R2, R3 and R4 to set a nominal trigger voltage of 250 molts with plus or minus 50 molts of hysteresus.
Then the output of the 311 is pulled up to 5 volts using R1 and then that feeds into digital pin D8 on the Nano. So whenever the transmitter is keyed, pin D8 is pulled to ground. I then wrote a short interruptdriven program for the Nano that monitors that port every 500 nanconds. That's more than fast enough to capture the keying transitions and then the data is stored in an array then periodically spit out on the serial port and then it appears here on the serial monitor in the Arduino IDE on the laptop so I can keep track of what's happening.
So let's give a demo of it working.
Pretty cool. Now, the purpose of going through all this is so I can measure the timing accuracy of my transmission and to quantify the improvements I get from this latest code and to also show how much more robust it is against timing variations. And one of the biggest sources of timing variation was me typing on the keyboard while it's transmitting. But now my typing has no effect.
And just to show off a little bit, I also set the code so I can go all the way up to 30 words per minute. And here's what that looks like.
No way I can uh copy it that fast, but I'm certainly capable of sending it that fast. Now, here are the results from my 20 words per minute experiment. I've plotted the timing deviations over time, where positive deviation means the d, the dash, or gap event took longer than it should, and negative means it happened faster. A couple of observations jump out right away. Typing versus not typing while transmitting are virtually identical. That's excellent.
That was the main purpose of going with this interrupt timing engine to make it robust against all other computing tasks. The average absolute deviation is around 2% and the maximum absolute deviation is 5%. Both are very good results. Now, the deviations do oscillate back and forth from positive error to negative error. And I took a deeper look at that by grouping the deviations by element type. And I see something very interesting. The errors are tightly clustered around their element type. The dits and dashes always have positive deviation error while the inner element gaps, the character gaps, and the word gaps are always negative error. Positive error I can understand.
Any interference would only add time, not subtract it. Negative error, on the other hand, is a bit of a mystery. It might just be a natural side effect of my keying envelope. I'm sampling the actual transmitted signal and it's definitely not a perfect square wave.
The onslope is different from the offs slope. So maybe that's it. The results for 30 words per minute look almost identical. The absolute error in milliseconds is about the same. The only real difference is in the percentages and that's because the duration of the dits is now smaller, 40 milliseconds instead of 60 milliseconds. So that smaller denominator in the calculation makes the percentage error higher. So how does this new ISR engine compare to my older version that used Milliey's math? It's night and day. Even without typing, the errors were consistently between 2 to 4 milliseconds with occasional spikes in excess of 10 milliseconds. But once I started typing, the errors shot into the stratosphere.
They're off the scale, sometimes in excess of 100 milliseconds. I knew something sounded funny in the transmitted code and quantifying it was a real eye openener, especially when I plot the ISR while typing results to the same scale. What an improvement. And I think there's room to reduce the error even more by increasing the frequency of the interrupt. That would be an easy software change. Let's say I double the frequency and have it trigger every 500 micros. So, two timer ticks per millisecond. I'd need to update the TX sequence encoding scheme to double my timing multiples to be 2, six, and 14 instead of 1, 3, and 7. But that's a super easy fix. In fact, I've got enough headroom to go up to nine times faster if I make bit 6 instead of bit 4 represent the last sequence in a character. That gives me bit 0 through bit 5 to encode the multiples. 9 * 7 = 63, which is precisely the largest decimal number you can represent with six bits. However, there's a price to be paid. At some point, these frequent interrupts might start starving the main loop execution, which could affect the stability of other timing sensitive tasks like the SPI and I squared C buses and reading the keyboard. I suspect, as always with software, there's an elegant balance in there somewhere. So, I might play around with it, doubling it or tripling it, just to see what happens.
The last part of the software that I want to cover today is about the function keys. My little keyboard has 10 physical function keys, plus two more accessible by holding down this key. I want to make the 10 physical keys programmable for CW messages that I regularly send, like calling CQ or sending my call sign. What's more, I want these to be programmable in real time, not hardcoded. That means I'm going to need to use the double EROM.
The AT Mega 4809 microcontroller on the Every has only 256 bytes of double EROM.
Not kilobytes and not megabytes, but 256 bytes. So, the most I can do here is limit each function key to a maximum of 24 characters plus a null terminator. 25 * 10 is 250. That'll just fit in the double EROM space. I'll need to add a section of code for all of this, and it'll be built around, you guessed it, yet another state machine. This one's simple, just three states. Dormant means what it says, doing nothing until the user presses the print screen key. Once that happens, we reconfigure the screen to show what is presently in the double EROM for the function keys. Then the state machine transitions to the running state. Here we're waiting for the user to do something. And there's only one of two allowable actions. Either press escape to save changes to double EROM and return to the main screen or press a function key to edit it. If you do the latter, that row will be shaded red and we move to the third state editing. You can add or delete characters as needed up to the maximum length. Spaces will be displayed as underscores to make them stand out. When you're done, press enter to deselect the row, change it back to white text, and return to the running state. Simple. There's obviously a lot more going on in the software than I can cover in a half hour. So, I've taken all the source files and put them on my blog, and I've included a link to that in the video description below. So, if you have further interest, check that out and let me know in the comments on this video if you have any ideas or suggestions for further improvements or enhancements. As always, I hope you guys enjoy this deep dive technical stuff that I do occasionally here on my channel. And I hope you're enjoying this continuing project on this HF transmitter. So, until next time, bye for now.
Related Videos
U.S. Military Just Flexed The Most Dangerous Aircraft Ever Built The F-47
MaxAfterburnerusa
11K viewsβ’2026-05-29
Heating Staying On On The Hottest Day Of The Year
PlumbLikeTom
507 viewsβ’2026-05-29
λ°μ ν¨μ¨μ λμ΄λ νμκ΄ μΆμ μμ€ν μ κΈ°μ μ μ리 #곡ν #곡μ #νμκ΄ #μκ³ λ¦¬μ¦ #μ¬μμλμ§
μ°νμ₯κΈ°μ
2K viewsβ’2026-05-29
Peterborough to Newark Northgate Driver's Eye View aboard an InterCity 225 - East Coast Main Line
TrainsTrainsTrains
822 viewsβ’2026-05-31
AI turbine design: hypersonic cooling leap #shorts #ai #hypersonic
bobbby_rn
671 viewsβ’2026-05-31
μ§κ΄ λ° κ³‘κ΄ λ°°κ΄ κ²°ν© κ³ μ μμ #worker #process #fabrication #pipework #clamp
μλμ΄μ΄
2K viewsβ’2026-05-30
How Far Can A Tomahawk Missile Actually Travel?
WarCurious
13K viewsβ’2026-05-28
Wire To Wire Connection Trick | Strong And Secure Electrical Joint #shortvideo #wireworks
ElectricianTips-b1h
5K viewsβ’2026-06-02











