Arduino TNC

This page documents an effort to use an Arduino (and more generically, AVR mega microcontrollers) as a KISS TNC.


A while back I purchased Scott Miller N1VG's OpenTracker 1+. I bought the device as a kit, had no trouble putting it together, and have used it a few times as part of my ammo-can tracker box. The OT1+ uses a Freescale MC08 microcontroller to perform software-based modulation and demodulation of AX.25 audio. I have also read about Gary Dion N4TXI's WhereAVR, and Byon Garrabrant N6BG's TinyTrak 4, both of which use AVR microprocessors without additional modem chips to perform mod and demod functions in software. With these as examples, I knew that the AVR Mega series CPUs were capable of functioning as a TNC with minimal support circuitry.

I recently picked up a desktop scanner on clearance at Radio Shack for under $10. I was already using my TNC-X with another scanner to listen for ARISS passes, and wanted to add the ability to monitor terrestrial APRS as well. Wouldn't it be interesting, I thought, if I could use one of my many AVR chips to do the job. Even better, I had recently received an Arduino MEGA for Christmas, and was looking for a project to use it on - wouldn't it be cool to use the MEGA, connected to my PC via USB, without having to purchase any shields or other new hardware?


As of November 2010, the goal of this project - the creation of a simple but effective TNC using Arduino hardware - has been achieved a couple of different ways. Because of the precise timing required to sample and decode AX.25 audio, it was not possible to build a working solution using the standard Arduino libraries. Instead, each solution has involved software written in C, calling AVR-libc libraries or manipulating register values directly.

Currently, my efforts are focused on an implementation that uses the freely available BeRTOS package, a collection of integrated libraries and drivers for embedded platforms. The AX.25 and AFSK modules included with BeRTOS form a high-performance packet engine that can be modified to function as a KISS TNC. I am working with the BeRTOS team to integrate KISS functionality into the main package, so hopefully those functions will be available in future versions of their product. In the mean time, a working example of the code (both source and binary for an ATmega328p-based Arduino) are attached at the bottom of this page, and a schematic for a simple test circuit is available from the BeRTOS website.

Also attached at bottom are working versions and prototypes from previous efforts. Detailed descriptions are included in the "Updates" section below.

Updates (top-posted)


Have not looked at this in a looooooong time, but I keep seeing references to this page elsewhere on the Interwebs, so I thought I would update some things. Mostly I'm working on porting parts of the BeRTOS code base into a native Arduino sketch, to make it more accessible to those folks who might find BeRTOS overwhelming. In doing this, I have had to teach myself all of the protocol innards again. It's slow going.

One detail that I had forgotten is just how deaf the ADC is. As mentioned below, the AVR ADC is looking for a 0-5VDC signal; the headphone output of my CD player was barely moving the meter, so decodes were almost impossible. Even worse, the unsquelched channel noise on Track 2 of Steve WA8LMF's APRS test CD is WAY louder than the audio of a nice, healthy, full-quieting signal; this means that the usual strategy of adjusting audio based on signal peaks gives the right amount of noise, but not nearly enough packet audio.

I had previously addressed this signal level issue by running the CD player output through some amplified speakers, and using that pumped-up audio to drive the ADC input. This time, I wanted something that would fit entirely on the prototyping shield mounted to my Duemilanove. After a few false starts, I found a page that described the circuit below, which uses a standard 741 op amp in a single-supply arrangement. This works really well - I set the gain for about 5x (+7dB or so), and my ADC input went from almost imperceptible to fairly strong. The 741 will not provide rail-to-rail output, but a little clipping at the limits is not problematic (the decode math cares more about polarity above/below the bias point than it does the exact phase or amplitude of each sample).

Here's the circuit. It's simpler than it looks here.

I have a couple of other op amps I have not tried in this config yet, but I expect they will work fine. I tried using an LM386, but the MINIMUM gain of that chip is 20 (+13dB), where I was originally looking for something far more modest (3-6dB).

Below are two screen grabs of the circuit in action. The traces are at the same scale in both images (1.0V per major division), with the same component values. Channel 1 is the zero-bias audio input, and Channel 2 is the same signal amplified. The post-amp trace is set with 0 VDC even with the bottom line, which shows its DC bias of about 2.5 volts. The amplified channel is inverted, and delayed slightly by the capacitors in the circuit.

The left-hand image shows packet audio being received. The right-hand image shows the reception of open-squelch static. You can see how very much louder that static is than the packet audio. You can also see that the amplified version of the noise is clipping pretty hard, and that the clipping levels are not symmetrical (that is, it clips at about 1.3 volts on the low end and 4.4 volts on the high end).


I started working on making a TNC from a Propeller processor.


From an email exchange with Zilvinas LY2SS, I realized that my waffling on the bias circuit (in the history below) leaves it unclear how the KISS TNC demo should be wired up. When I first started using the BeRTOS code, I re-wired my circuit to match the schematic on the BeRTOS website, which uses the +5VDC pin as the source. Even though the power from the USB port on my PC is very noisy, I am still able to get excellent decoding results.

Another point worth mentioning... the program and circuit design used here require a BIG audio signal to decode properly. Your best bet is probably the "speaker out" jack, with the volume halfway up and squelch wide open. The low-level audio available from a data jack simply is not loud enough to get the Arduino's ADC moving. Likewise, if you split the signal from the "speaker out" jack to both the Arduino =and= a low-impedance speaker, the Arduino's relatively high-impedance input pin will not get enough signal to decode anything. This is resolvable, though, and I have some ideas in mind.


Got some great feedback from Francesco Sacchi, one of BeRTOS' lead developers, and the author of the AX.25 module. At his suggestion, I am working to provide an integrated, backward compatible implementation of KISS for BeRTOS. In the mean time I have submitted a couple of patches for minor issues.


Some extremely non-scientific test results, to give a general idea of RX performance. Please, no wagering.

I played Track 2 of Stephen Smith WA8LMF's APRS test CD through an old JVC CD player, amplified it with a pair of powered PC speakers, and ran the signal from the speakers' headphone jack to the devices under test. I'm too lazy to hook up the o'scope just now, but I think that gives me something like one volt p-p of audio, simulating the speaker output of a VHF mobile. I made no attempt whatsoever to fine-tune signal levels for the hardware trackers - I just pumped up the volume until the activity light started flashing on the Arduino with the BeRTOS RX/TX firmware, and left things at that level for the other devices.

For the sound card-based solutions, I had to use un-amplified line-level output from the CD player plus a voltage divider to reduce the audio to microphone levels that the sound cards could handle. I then messed around with audio levels until things started working.

My takeaway is that the BeRTOS-based software is working just fine. I'll see if I can lay hands on some other devices, and try the same test. I know of one guy with an Argent Tracker 2, and maybe another with some Kantronics gear. And I'll re-visit the RadioShield, to see what I may have done wrong.


TX and RX working. It took a while to get the two sides to stop stepping on each other - the secret was to use the deprecated "ser_getchar_nowait" function. RX still seems to miss a packet once in a while, not sure why. I'd like to merge some of my code into a nice, clean patch to the BeRTOS AX25 module, but the code below ("Attachments" section at the bottom of the page) is functional enough to start playing with.


After spending an entire vacation day learning how little I knew about pointers to char arrays (ugh!), I was finally able to get a KISS version of the BeRTOS demo app to work, and quite well. It turns out that the AX25 modules as delivered did not retain the "used" flag for each digi in the path (not sure I knew about this before today), so I made some minor adjustments to those files. The TGZ attached below also contains a compiled binary for ATmega328p and '328p powered Arduinos. The shell script "p" calls avrdude with the parameters to program the "aprs.bin" file to flash. Once it's running, the Arduino starts passing KISS packets over USB at 57600 bps.

The AFSK module in BeRTOS uses a standard Fourier transform with 9600Hz sampling. I have not traced all of the various library details yet, so I can't claim to know too much about the magic under the covers. For example, I have no idea where the PTT pin is specified. I inserted a bit-bang for a DCD light, but it could certainly be a lot better. I'm using the (noisy) 5 volt Vcc from the USB bus, which I think is glitching the programming process on occasion (it's taking 2-3 tries with avrdude sometimes). And of course the TX side is left to implement. But all of this is just a few hours into exploring these libs... not too shabby.


Holy cow! Had not seen this before today... BeRTOS is a kernel / development framework that aims to deliver a standard set of libraries and services for a variety of embedded platforms (including AVR, ARM, etc.). Their website features an example project that demonstrates their AFSK and AX.25 modules on an Arduino. The demo comes with some pre-compiled images - I burned the hex file directly to my Duemilenova (ATmega328p), rigged up the specified resistor bridge and voltage divider (identical the design I have been using), and was instantly decoding every packet I could hear. RX performance out of the box was easily twice as good as the results from my own code, with faint and somewhat scratchy packets decoding perfectly.

It should be extremely easy to convert the sample code to output incoming packets in KISS format. It should only be a smidge more complicated to figure out their serial library, and parse incoming KISS serial data for transmission over RF. Cool!


Long overdue for an update. Amazing how life can interfere with your hobbies....

I received an email back in June from Kieran Levin, an MSEE candidate at U of Illinois Urbana-Champaign:

Hi Robert,

I just wanted to share with you what I did last week. I basically took your receive code and wrote on a transmit function based off of whereavr's logic and put them both together into a fully functional TNC. I have connected a bluetooth modem to the serial port so I now have a bluetooth to aprs modem which

i can use with my laptop or windows mobile cellphone.

It seems to work well as the stations around me can pick me up and pass my traffic along and I am receiving many stations around me in probably a 50 mile radius using a small Yaesu handheld with a Jpole antenna.

I am planning on implementing a few more features in kiss such as the transmit hold off and adjustable headers and tails settable via kiss. The hardware is basically the same as whereavr. I found the code in whereavr did not work as well on the 168 so I used your receive code instead. I did however enable Aref to be from Vcc in software and adjusted your status led pin assignments. It is not at all written for the atmega 128 only the 168 chip will work with this.

This code is attached below as "arduino_tnc_014_w_tx.pde" - thanks Kieran!

Also, since I cannot leave things well enough alone, I have been working on re-designing my receive code. The code used in version 013 and in Kieran's mash-up is based on a timer interrupt service routine that is =hundreds= of lines long. This is conceptually simple, because the entire code block is called every time the sample timer trips, but it is extremely ugly as interrupt handlers go. My re-design has been focused on limiting the timer interrupt to adding a correctly timed ADC reading to a buffer. A separate loop checks the buffer for any new entries, and then drills into the complex code, which is broken into smaller functions. This loose coupling is working ok, but still has a ways to go.

Random thought...

Having now spent hundreds of hours farting around with this project, there's a lot to be said for the simplicity of something like the OpenTracker1+ SMT from Argent ($40). Scott's code is more thoroughly tested than mine ever will be, and packs a lot more features. Since I already have a solderless breadboard attached to a prototyping shield, it would be dead simple to plug one of these into the breadboard, and use the Arduino for higher level logic. For some applications (eg telemetry), the OT1+ SMT is capable enough that an Arduino would be unnecessary.

I also have Argent's RadioShield, which is an OT1+ chip and supporting components on an Arduino shield. The firmware on the RadioShield is not directly usable as a KISS TNC (as I had hoped), but it does provide a very simple, reliable way to send and receive basic APRS packets from a sketch. I especially like the LCD interface - I have a 20x4 display connected, and am displaying (simplified) packet contents as they come in.


TX code is working - it makes all of the appropriate hoots and whistles, and the timing is tight enough that the audio even decodes correctly in multimon. Unfortunately, it also hangs, screeches, occasionally ignores incoming serial data, and frequently reboots itself. (Sigh.) Will post code once it is a bit more reliable.


Started working on TX code this weekend. The tone generator uses the same timer-plus-lookup-table I have used in my SSTV encoders, and the multi-value resistor ladder that Gary Dion used in the WhereAVR in place of the R2R design, which has a higher parts count. The TX works ok in isolation, but the timing is not quite right - it will decode when received by a TNC-X, but not much else. I'll post an update once that is working.

Also, purchased and received a Radio Shield from Argent Data Systems. Once assembled, this forms an OpenTracker 1+ in the form of an Arduino shield. The easiest application for this as a KISS TNC would almost certainly be to load the OT1+ chip with the KISS firmware, and write a minimal Arduino sketch to pass the serial data back and forth between the shield and the serial/USB port on the Arduino. This might break some of the shield-specific features, though. I have not put the shield together yet, and of course don't have any code written to demonstrate this. Fun detail - the shield can also function as a driver for an HD44780-based LCD panel (16x2). I have such a panel (snazzy white on black) on order from Sparkfun, along with some stackable headers that I'll use instead of the short pins that came with the Radio Shield. More info as I play with this.

Thank you and hello to Tom in Luxembourg, who confirmed that the RX-only code (lucky version 13, I assume) works well on his Duemilanove with the ATmega168 CPU. Great news, and thanks for the feedback!


Even better - lucky version 013 brings in true KISS decode, improved DCD performance (less flashing), and auto-correcting ADC bias. I've been decoding APRS packets for several hours, and now have an Xastir map full of stations. Also fixed a couple of random bugs, improved the comments, and cleaned up a few unused variables. Accuracy seems reasonable, though I'll have to test it against the infamous WA8LMF test CD. It's now starting to look a little bit like a serviceable TNC, albeit a receive-only one.

I also tried backing out the LPF on the ADC input, and going back to my original one-cap-two-resistor design, but with the 3.3V source providing a bias source. So far it seems to be working great. The combination of the simpler math and the self-tuning ADC bias looks to be pretty tolerant.


Catching on now. I'm a software guy, so I'm not used to dealing with things like noise on analog circuits.

My original hardware design looked like this:

This is super easy to throw together, but very, very noisy. First among many reasons: the Arduino 5V bus carries a ton of noise from the USB line. Every time I would move my mouse, the DCD would start flashing like crazy, even with no audio coming in. On an oscilloscope, the 5V line shows regular blips in the 0.4 volt range that go nuts with any USB activity (such as mouse movement), plus a thick layer of "fur" where the continuous five volts should trace a nice thin line across the screen. After some experimentation, I found that the regulated +3.3V line is =MUCH= quieter. This source has almost no blips, and very little "fur".

In both of these pics, the scope is set to ONE VOLT and 5 ms per major division, AC coupled. As I'm sure you've guessed, that's the 5V bus on the left, and the 3.3V bus on the right.

Next, I tried another approach to having the AVR tell me what ADC values it was reading. I removed all references to the "Serial" library from my code, and used the registers directly. The highest speed I could get minicom and the AVR to agree to was 230400 bps. This worked out very nicely - I could write a single 8-bit byte to the AVR USART at every 13200 Hz sample, and not disrupt either the ADC, the sample loop, or the USART. The data that came from this effort showed that I may have been either mistiming the ADC sampler (too low of a freq = too high of a pre-scale), or that the input audio was overlaid with some higher frequency audio noise. I investigated both, and ended up using a lower pre-scale value (16 = 1 MHz sampling) and a more complicated LPF for the audio input.

The new design looks like this:

Hopefully L1 and C2 reduce audio noise above the 2200 Hz tone of AX.25. Note that by using the 3.3V line to bias the input, I have to also apply the 3.3 volts to the Aref pin. This yields extra noise reduction benefits, as having the Aref pin float invites trouble. Finally, note that I lowered the resistors from 10K to 1K, on the theory that lower input impedance may help in some small way. The overall effect of these changes was very positive - ADC performance improved, and the 006 series code came even closer to actually working in real time.

But that wasn't enough.

I had spent some time reviewing any web articles I could fine that provided detailed strategies for decoding AX.25 / Bell 202 audio streams. In this research, I came across a paper written by Dennis Seguine, explaining how a mixed-signal Cypress chip could be used to perform basic DSP calculations on the incoming data. While some of Dennis' strategy would not work on a AVR (since 8-bit AVRs do not have DSP functions built in), the first half of his algorithm made sense to me. I played with it on a spreadsheet using some of the ADC-to-serial data captured from the quieter input circuit. I found that with just a couple of minutes, I was able to decode packets from those samples at about 75% accuracy. Next I ramped up this new approach to a C program on the PC, again reading the ADC-to-serial data. With a few tweaks, I was decoding data all over the place.

In the mean time, my wife had gotten me a few Arduino doo-dads for my birthday - a Duemilanove with an ATmega328P, and a protoshield. Instead of porting the new C program back to the Mega, I thought I would dive into the deep end, and code it for the Due instead. This proved to be a useful exercise - I found several bugs in previous versions of the 006 series sketches written for the Mega. Soon I had the new 012 code compiled and running on the Due. I fed it some audio, and was pleasantly surprised to find that it could decode packets immediately. I switched plugs and fed the Due some open-squelch audio from 144.390, and soon all kinds of data was flowing across the screen.

Note that the 012 sketch is still a test build - it decodes the packets into a plain-text format intended for monitoring on a serial terminal. This output will not be useful to AGWPE or Xastir, but it does demonstrate that packets are being received and decoded. There are still some more accuracy issues to work on, but it's a big step forward.

Attached Files