«

»

Print this Post

Wireless Microcontroller / PC Interface for $3.21

Here I demonstrate a dirt-cheap method of transmitting data from any microchip to any PC using $3.21 in parts.  I’ve had this idea for a while, but finally got it working tonight. On the transmit side, I’m having a an ATMEL AVR microcontroller (ATMega48) transmit data (every number from 0 to 200 over and over) wirelessly using 433mhz wireless modules. The PC receives the data through the microphone port of a sound card, and a cross-platform Python script I wrote decodes the data from the audio and graphs it on the screen. I did something similar back in 2011, but it wasn’t wireless, and the software wasn’t nearly as robust as it is now.

This is a proof-of-concept demonstration, and part of a larger project. I think there’s a need for this type of thing though! It’s unnecessarily hard to transfer data from a MCU to a PC as it is. There’s USB (For AVR V-USB is a nightmare and requires a precise, specific clock speed, DIP chips don’t have native USB, and some PIC DIP chips do but then you have to go through driver hell), USART RS-232 over serial port works (but who has serial ports these days?), or USART over USB RS-232 interface chips (like FTDI FT-232, but surface mount only), but both also require precise, specific clock speeds. Pretend I want to just measure temperature once a minute. Do I really want to etch circuit boards and solder SMT components? Well, kinda, but I don’t like feeling forced to. Some times you just want a no-nonsense way to get some numbers from your microchip to your computer. This project is a funky out-of-the-box alternative to traditional methods, and one that I hope will raise a few eyebrows.

 

Ultimately, I designed this project to eventually allow multiple “bursting” data transmitters to transmit on the same frequency routinely, thanks to syncing and forced-sync-loss (read on). It’s part of what I’m tongue-in-cheek calling the Scott Harden RF Protocol (SH-RFP). In my goal application, I wish to have about 5 wireless temperature sensors all transmitting data to my PC.  The receive side has some error checking in that it makes sure pulse sizes are intelligent and symmetrical (unlike random noise), and since each number is sent twice (with the second time being in reverse), there’s another layer of error-detection.  This is *NOT* a robust and accurate method to send critical data. It’s a cheap way to send data. It is very range limited, and only is intended to work over a distance of ten or twenty feet. First, let’s see it in action!

The RF modules are pretty simple. At 1.56 on ebay (with free shipping), they’re cheap too! I won’t go into detail documenting the ins and out of these things (that’s done well elsewhere). Briefly, you give them +5V (VCC), 0V (GND), and flip their data pin (ATAD) on and off on the transmitter module, and the receiver module’s DATA pin reflects the same state. The receiver uses a gain circuit which continuously increases gain until signal is detected, so if you’re not transmitting it WILL decode noise and start flipping its output pin. Note that persistent high or low states are prone to noise too, so any protocol you use these things for should have rapid state transitions. It’s also suggested that you maintain an average 50% duty cycle. These modules utilize amplitude shift keying (ASK) to transmit data wirelessly. The graphic below shows what that looks like at the RF level. Transmit and receive is improved by adding a quarter-wavelength vertical antenna to the “ANT” solder pad. At 433MHz, that is about 17cm, so I’m using a 17cm copper wire as an antenna.

Transmitting from the microcontroller is easy as pie! It’s just a matter of copying-in a few lines of C.  It doesn’t rely on USART, SPI, I2C, or any other protocol. Part of why I developed this method is because I often use ATTiny44A which doesn’t have USART for serial interfacing. The “SH-RFP” is easy to implement just by adding a few lines of code. I can handle that.  How does it work? I can define it simply by a few rules:

 

SHRFP (Scott Harden RF Protocol)

Pulses can be one of 3 lengths: A (0), B (1), or C (break).

Each pulse represents high, then low of that length.

Step 1: prime synchronization by sending ten ABCs

Step 2: indicate we’re starting data by sending C.

Step 3: for each number you want to send:

A: send your number bit by bit (A=0, B=1)

B: send your number bit by bit (A=1, B=0)

C: indicate number end by sending C.

 Step 4: tell PC to release the signal by sending ten Cs.

Decoding is the same thing in reverse. I use an eBay sound card at $1.29 (with free shipping) to get the signal into the PC. Syncronization is required to allow the PC to know that real data (not noise) is starting. Sending the same number twice (once with reversed bit polarity) is a proofchecking mechanisms that lets us throw-out data that isn’t accurate.

From a software side, I’m using PyAudio to collect data from the sound card, and the PythonXY distribution to handle analysis with numpy, scipy, and plotting with QwtPlot, and general GUI functionality with PyQt. I think that’s about everything.

 The demonstration interface is pretty self-explanatory. The top-right shows a sample piece of data. The top left is a histogram of the number of samples of each pulse width. A clean signal should have 3 pulses (A=0, B=1, C=break). Note that you’re supposed to look at the peaks to determine the best lengths to tell the software to use to distinguish A, B, and C. This was intentionally not hard-coded because I want to rapidly switch from one microcontroller platform to another which may be operating at a different clock speed, and if all the sudden it’s running 3 times slower it will be no problem to decide on the PC side. Slick, huh? The bottom-left shows data values coming in. The bottom-right graphs those values. Rate reporting lets us know that I’m receiving over 700 good data points a second. That’s pretty cool, especially considering I’m recording at 44,100 Hz. 

Here’s the MCU code I used. It’s an ATMega48 ATMEL AVR microcontroller. Easy code!

#define F_CPU 8000000UL

#include <avr/io.h>
#include <util/delay.h>

void tick(char ticks){
	while (ticks>0){
		_delay_us(100);
		ticks--;
	}
}

void pulse(char ticks){
	PORTB=255;
	tick(ticks);
	PORTB=0;
	tick(ticks);
}

void send_sync(){
	char i;
	for (i=0;i<10;i++){
		pulse(1);
		pulse(2);
		pulse(3);
	}
	pulse(3);
}

void send_lose(){
	char i;
	for (i=0;i<5;i++){
		pulse(3);
	}
}

void sendByte(int val){
	// TODO - make faster by only sending needed bytes
	char i;
	for (i=0;i<8;i++){
		if ((val>>i)&1){pulse(2);}
		else{pulse(1);}
	}
}

void send(int val){
	sendByte(val);  // regular
	sendByte(~val); // inverted
	pulse(3);
}

int main (void)
{
    DDRB = 255; 
	int i;

    while(1) {
		send_sync();
		for (i=0;i<200;i++){
			send(i);
		}
		send_lose();
	}
}

Here’re some relevant snippits of the PC code. Download the full project below if you’re interested.

import matplotlib
matplotlib.use('TkAgg') # -- THIS MAKES IT FAST!
import numpy
import pyaudio
import threading
import pylab
import scipy
import time
import sys 

class SwhRecorder:
    """Simple, cross-platform class to record from the microphone.
    This version is optimized for SH-RFP (Scott Harden RF Protocol) 
    Pulse data extraction. It's dirty, but it's easy and it works.

    BIG PICTURE:
    continuously record sound in buffers.
    if buffer is detected:

        ### POPULATE DELAYS[] ###
        downsample data
        find Is where data>0
        use ediff1d to get differences between Is
        append >1 values to delays[]        
        --if the old audio[] ended high, figure out how many
        --on next run, add that number to the first I

        ### PLUCK DELAYS, POPULATE VALUES ###
        only analyze delays through the last 'break'
        values[] is populated with decoded delays.

    ."""

    def __init__(self):
        """minimal garb is executed when class is loaded."""
        self.RATE=44100
        self.BUFFERSIZE=2**10
        print "BUFFER:",self.BUFFERSIZE
        self.threadsDieNow=False
        self.newAudio=[]
        self.lastAudio=[]
        self.SHRFP=True
        self.dataString=""
        self.LEFTOVER=[]

        self.pulses=[]
        self.pulsesToKeep=1000

        self.data=[]
        self.dataToKeep=1000

        self.SIZE0=5
        self.SIZE1=10
        self.SIZE2=15
        self.SIZEF=3

        self.totalBits=0
        self.totalNumbers=0
        self.totalSamples=0
        self.totalTime=0

        self.nothingNewToShow=True

    def setup(self):
        """initialize sound card."""
        #TODO - windows detection vs. alsa or something for linux
        #TODO - try/except for sound card selection/initiation       
        self.p = pyaudio.PyAudio()
        self.inStream = self.p.open(input_device_index=None,
                                    format=pyaudio.paInt16,channels=1,
                                    rate=self.RATE,input=True,
                                    frames_per_buffer=self.BUFFERSIZE)

    def close(self):
        """cleanly back out and release sound card."""
        self.p.close(self.inStream)

    def decodeBit(self,s):
        "given a good string 1001101001 etc, return number or None"
        if len(s)<2:return -2
        s=s[::-1]
        A=s[:len(s)/2] #INVERTED
        A=A.replace("0","z").replace("1","0").replace("z","1")
        B=s[len(s)/2:] #NORMAL

        if A<>B:
            return -1
        else:
            return int(A,2)

    def analyzeDataString(self):
        i=0
        bit=""
        lastB=0
        while i<len(self.dataString):
            if self.dataString[i]=="B":
                self.data.append(self.decodeBit(bit))
                self.totalNumbers+=1
                lastB=i
            if self.dataString[i] in ['B','?']:
                bit=""
            else:
                bit+=self.dataString[i]
            i+=1
        self.dataString=self.dataString[lastB+1:]
        if len(self.data)>self.dataToKeep:
            self.data=self.data[-self.dataToKeep:]

    def continuousAnalysis(self):
        """keep watching newAudio, and process it."""
        while True:
            while len(self.newAudio)< self.BUFFERSIZE:
                time.sleep(.1)

            analysisStart=time.time()

            audio=self.newAudio

            # TODO - insert previous audio sequence here

            # GET Is where data is positive        
            Ipositive=numpy.nonzero(audio>0)[0]
            diffs=numpy.ediff1d(Ipositive)
            Idiffs=numpy.where(diffs>1)[0]
            Icross=Ipositive[Idiffs]
            pulses=diffs[Idiffs]            

            # remove some of the audio buffer, leaving the overhang

            if len(Icross)>0:
                processedThrough=Icross[-1]+diffs[Idiffs[-1]]
            else:
                processedThrough=len(audio)

            self.lastAudio=self.newAudio[:processedThrough]            
            self.newAudio=self.newAudio[processedThrough:]

            if False:
                # chart audio data (use it to check algorythm)
                pylab.plot(audio,'b')
                pylab.axhline(0,color='k',ls=':')

                for i in range(len(Icross)):
                    # plot each below-zero pulse whose length is measured
                    pylab.axvspan(Icross[i],Icross[i]+diffs[Idiffs[i]],
                                  color='b',alpha=.2,lw=0)

                # plot the hangover that will be carried to next chunk
                pylab.axvspan(Icross[i]+diffs[Idiffs[i]],len(audio),
                              color='r',alpha=.2)
                pylab.show()
                return

            # TODO - histogram of this point to assess quality
            s=''        
            for pulse in pulses:
                if (self.SIZE0-self.SIZEF)<pulse<(self.SIZE0+self.SIZEF):
                    s+="0"
                elif (self.SIZE1-self.SIZEF)<pulse<(self.SIZE1+self.SIZEF):
                    s+="1"
                elif (self.SIZE2-self.SIZEF)<pulse<(self.SIZE2+self.SIZEF):
                    s+="B"
                else:
                    s+="?"

            self.pulses=pulses  
            self.totalBits+=len(pulses)         

            print "[%.02f ms took %.02f ms] T: 0=%d 1=%d B=%d ?=%d"%(
                          len(audio)*1000.0/self.RATE,
                          time.time()-analysisStart,
                          s.count('0'),s.count('1'),s.count('B'),s.count('?'))

            self.dataString+=s            
            self.analyzeDataString()

            self.totalSamples+=self.BUFFERSIZE
            self.totalTime=self.totalSamples/float(self.RATE)   
            self.totalBitRate=self.totalBits/self.totalTime  
            self.totalDataRate=self.totalNumbers/self.totalTime                   

            self.nothingNewToShow=False

    def continuousRecord(self):
        """record forever, adding to self.newAudio[]. Thread this out."""
        while self.threadsDieNow==False:
            maxSecBack=5
            while len(self.newAudio)>(maxSecBack*self.RATE):
                print "DELETING NEW AUDIO!"
                self.newAudio=self.newAudio[self.BUFFERSIZE:]
            audioString=self.inStream.read(self.BUFFERSIZE)
            audio=numpy.fromstring(audioString,dtype=numpy.int16)
            self.newAudio=numpy.hstack((self.newAudio,audio))

    def continuousDataGo(self):
        self.t = threading.Thread(target=self.continuousRecord)
        self.t.start()
        self.t2 = threading.Thread(target=self.continuousAnalysis)
        self.t2.start()

    def continuousEnd(self):
        """shut down continuous recording."""
        self.threadsDieNow=True

if __name__ == "__main__":
    SHR=SwhRecorder()
    SHR.SHRFP_decode=True
    SHR.setup()
    SHR.continuousDataGo()

    #SHR.DataStart()

    print "---DONE---"

Finally, if you’re interested, here’s the full code (and demo audio WAV files):

DOWNLOAD: SCOTT HARDEN RF PROTOCOL DEMO.zip

If you use these concepts, hardware, or ideas in your project, let me know about it! Send me an email showing me your project – I’d love to see it. Good luck!

About the author

Scott W Harden

Scott Harden has had a lifelong passion for computer programming and electrical engineering, and recently has become interested in its relationship with biomolecular sciences. He has run a personal website since he was 15, which has changed names from HardenTechnologies.com, to KnightHacker.com, to ScottIsHot.com, to its current SWHarden.com. Scott has been in college for 10 years, with 3 more years to go. He has an AA in Biology (Valencia College), BS in Cell Biology (Union University), MS in Molecular Biology and Microbiology (University of Central Florida), and is currently in a combined DMD (doctor of dental medicine) / PhD (neuroscience) program through the collaboration of the College of Dentistry and College of Medicine (Interdisciplinary Program in Biomedical Science, IDP) at the University of Florida in Gainesville, Florida. In his spare time Scott builds small electrical devices (with an emphasis on radio frequency) and enjoys writing cross-platform open-source software.

Permanent link to this article: http://www.SWHarden.com/blog/2013-05-19-wireless-microcontroller-pc-interface-for-3-21/

33 comments

  1. Mike

    What is the sampling rate of those USB sound cards? I looked on eBay and none of them disclose the rate.

    1. Scott Harden

      my little $1 card records well at 192,000Hz. Although, for my experiment I only used 44,100Hz. It can certainly be pushed to transmit much faster than I did in this demonstration

  2. Mike

    Sounds like you can now make an SDR with it. You just need a mixer to bring the RF signal down to the audio range.

  3. Zane

    Great post! thank you for the interesting project write up. i cant wait to try it.

  4. Tony

    Looks great. Any possibility of capturing the audio signal into a .wav file? I’d be interested in taking a look and possibly doing a decode/transmit app in IOS. I saw you soldered the Rx module onto a USB sound card. Would soldering to a audio jack which would plug into most laptops have worked too? Thanks

    1. Scott W Harden

      I included some demo audio in 3 wav files in the zip file available for download. One is just noise, one is weak signal, and one is strong signal. If you whip something together for IOS, let me know and I’ll link to it here!

      The audio jack would have worked fine too. The reason I soldered it was for convenience (USB audio + wireless module acts like 1 device), and I also tapped into the 5V pin of the USB audio card to supply power to the receiver module.

      1. Tony

        thanks!

      2. Tony

        Just got my pyaudio, pyqt, etc up to date. Will one of the python files decode the wave files? or do they only work with realtime audio?

        1. Scott W Harden

          You can play the audio files and with the PC look at the stereo mix channel (assuming you don’t have a mic.) For windows 7, try http://www.howtogeek.com/howto/39532/how-to-enable-stereo-mix-in-windows-7-to-record-audio/

          1. Tony

            Thanks. On a Mac but it didn’t work. Not sure my pyaudio setup is ok though either. Not sure if I’ll really get this into an app. Ordered the Tx/Rx parts but a bit rusty on the soldering and everything and wasn’t quite sure of the right USB to get either. I guess the reason you used USB rather than straight audio jack is that the USB also provided power to the Rx

  5. Zaid Pirwani

    I have the Radio module, waiting till I get the USB soundcard… this looks GREAT..

  6. Manuka

    This caught my fancy – folks jokingly say “Stan’s Law” = “You can never have too many thermometers” – but approaches with cheap PICAXE micros., DS18B20 & cheap USB-serial cables may be far easier. The PICAXE programming editor also offers an inbuilt terminal, but the likes of StampPlot & StampDAQ offer brilliant displays.

    UHF data modules abound, but note that 433 MHz offerings may not be US legal – use 315MHZ instead! For simple ASK work Chinese Dorji work a treat -see => http://www.picaxe.orconhosting.net.nz/dorjiask.pdf , although sophisticated GFSK transceivers are increasingly preferred to ease data loss & avoid band noise.

  7. Ty Tower

    Hmm first post disappeared . It tried url tags. I used transmitter/receiver pairs from http://www.oatleyelectronics.com/ for about $20 and got a line of sight range of about a mile and 100 meters through a couple of concrete block walls. Your antenna might be improved by using finer wire which will sharpen the bandwith some and help prevent breakage at the board.
    Very interesting post Thanks

    1. DOUG

      I believe the larger the diameter of an antenna the more bandwidth it will exhibit. Not sure if the bandwidth of the antenna would be an issue in these project anyway. At 70 cm RF doesn’t go through concrete block walls. The shorter wave lengths are easily reflected, and can make better use of any openings in a building.

  8. john

    Nice.

    About time someone got rid of the idea of fixed baud rate.

    I too like the idea of a simple wireless way to plot data. Eliminates MANY ground loop problems from measurements.

    Nice job.

    John

  9. john

    Downloaded it. Does not run :(.

    Says cannot import name Qwt5 (from PyQt4.

    I am running Knoppix [linux]. I didn’t find a simple way to install pythonxy since the only copy I find on
    the pythonxy website is an .exe file?

    Does pythonxy really run on Linux? Also, I didn’t find pythonxy in the debian apt pkgnames list.

    Looks like it would be most useful if I can get it to run.

    Help please.

    John

  10. Dougal

    Did you consider using Manchester encoding? I’m guessing you did, but decided against it because the timing is clock-dependent?

    I like the approach of using relative-timed pulses to make the implementation independent from the clock, though.

  11. Dan

    This is great Scott. What about using frequency relationships verses pulse width? The reason being is that it would be neat to be able to send the data very slow too, using QRSS methods. So a “1″ would be say a 1000hz tone a “0″ 1200hz and maybe 1500hz for the “break”. Great work in any case.

    -Dan WA6PZB

    1. Scott Harden

      I considered this, and a potential plan is still on the table. However, the reason I decided against it is because, to detect a frequency, it would have to take several ms transmitting that tone (maybe a few hundred up/down cycles) which ultimately is slower than measuring the width of individual pulses. The thing that would make it nice is if I could combine frequencies and transmit 1k, 1.1k, 1.2k, up through 8 tones. Then each tone would contain 8 possible frequencies, and their presence or absence can signify 1 or 0. Thus, each “pulse” transmits an 8-bit number. Finally, strength detection can be done by measuring the SNR. Lots of ideas to play with…

  12. Dan

    Thanks Scott, I figured it was in your head too. Your on the right track trying to keep it simple (and cheap!). I just got my python environment up and duplicated your demo, I hope to try a few things. I would like to see if it is possible to get better range with these 433mhz TX devices for a potential balloon project. You probably know about the RFM22′s devices, but they require a more advanced interface/MCU. Folks are using these RFM22 on balloons that are going 100,000 feet in the UK but using SSB radio’s and RTTY. -73 Dan WA6PZB

    1. Mike

      I’ve used the RFM12/22 modules. They’re really not that difficult to use. The HopeRF (manufacturer) website even has example code in the datasheet. And if you can’t program a little 8-pin MCU, well….it’s time to learn.

      1. Scott Harden

        I think you’re talking about http://www.hoperf.com/upload/rf/RFM12.pdf – I have a few, and I’m starting to work with them. They’re pretty cool! Thanks for mentioning them here.

  13. john

    Hi All,

    Is anyone running this on Linux? Looks like I gotta do the necessary work?

    Of possible interest, Bell 202 actually used 3 frequencies as well.
    One was 2200Hz, zero was 1200Hz and 900Hz +/- for end of data burst.
    Not too far from this scheme. But not synchronous.
    Also Bell 202 could carry up to 1800 BPS on some lines.

    Caller ID uses Bell 202 and adds a leading training burst somewhat similar to this scheme.

    John

  14. Jason

    Thanks. This was just the tutorial I needed to get me to finally learn how to make nice plots with qt

  15. john

    Hi All,

    What a drag. It looks like pythonxy is a windows only version.

    Any ideas welcome.

    A Linux version looks non trivial to me.

    John

  16. Randy

    These modules are great Scott, I’ve used them a bunch before with a similar perpous, monitoring temperature and humidity, with some DHT-11 (http://www.ebay.com/itm/DHT11-DHT-11-Digital-Temperature-and-Humidity-Sensor-Temperature-sensor-Arduino-/400489574221?pt=LH_DefaultDomain_0&hash=item5d3f09ef4d) sensors.

    My main problem was the range, like you I get at best 15-20 feet before the noise from all the other 433mhz stuff in my area drowns them out. I’ve been trying to figure out a simple solution to amplify the signal but have came up empty handed, there are some single chip amplifiers, but they’re kinda expensive. Have you tried anything to get a better range (other than adding an antenna!)

    1. Scott Harden

      I haven’t tried extending the range. For anything further, I’d go ahead and upgrade to something with more power. Those 2.6ghz modules linked here look cool too. Xbee is a nice option. Also there are 900mhz wireless data modems you can buy for a little higher power professional solutions. I’ve always wondered about hijacking the audio connection of a cordless telephone – would be an easy way to send data a good distance with stuff you might have in your junk box.

      1. Randy

        I’ve used the RFM-22 modules, you can also get a nRF24L01 for $1~ each on ebay too, I never got over 100 feet out of them though, they have some that have a PA/LNA that claim to do 1000m!

        The cordless phone idea is unique… but I imagine they’d the drain batteries fast, I’ve used FRS/GMRS radios in a similar fashion before, but never considered using one with a USB sound card to get capture the data… so now I’m tempted to try it again…. but its not quite legal, as data is not allowed on GMRS/FRS frequencies.

  17. David Alexander

    Any ideas on how to send data to the microcontroller using a related strategy?
    It would be useful, even if just to synchronize when a bunch of sensors send their data to the pc.

    1. Scott Harden

      Working on it as we speak! My goal is to have a dozen little wireless sensors around my room all bursting data packets received by a single AVR, then sent to a PC via USB (well, USART -> FTDI -> USB)

      1. David Alexander

        Electronics is not my expertise, which is why I loved your idea to transmit from the microcontrollers (sound cards are in the post!)
        Your solution for the microcontrollers to receive data seems a lot more complicated? Is it?
        Is there any way, using python, to get the sound card to transmit a signal that is clean enough to be interpreted by the microcontroller and its rf module?

  18. Anonymous

    The link to the USB Sound Card is broken. :/

  19. Anonymous

    Nevermind my post because of the broken link, your video wraps it up which sound card is needed.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>