Saturday, August 9, 2014

Sending data over IR with a PIC: the DAC method

Alright, so I had what I thought was a relatively mundane problem: I wanted to send data over IR with a PIC I'm using (specifically, a PIC12LF1572). Should be simple, right? Turns out it's actually a bit of a pain to do in an efficient way, and hopefully it's common enough that my write-up here will be useful to someone trying to do something similar. I certainly looked pretty hard for a solution to this online and didn't come up with anything useful.

This write-up will assume some basic familiarity with microcontrollers (PICs especially) and so I'm not going to spend any time going over the basics like PWM/interrupts/etc.

I'm also not going to go into detail about what my overall project here is right now, instead I'll just focus on this specific solution because I believe it's deserving of it's own post. For now we'll just say I'm working on a generic IR transceiver.

So, why's this an interesting problem? Well, generally when sending data over IR you really want to avoid having all the environmental garbage clutter up your signal (sunlight, a variety of household lights, etc all give off a ton of IR). This is accomplished by filtering out any frequencies that aren't specifically what your receiver accepts, i.e. with a band-pass filter. Because your IR signal has to be sent at this frequency, you actually need to modulate your data with it. For example, if your original data signal looks something like this:

Original data
Then in order to be read by the photodetector your modulated output needs to look something like the blue plotted here:

Original data with modulated signal
So, how do we generate the modulated signal? The most obvious answer would be using an AND logic gate, like so:

Combined signals with AND gate
This is actually a perfectly fine solution in many cases, but there are a couple of major drawbacks which meant that there was no way I could use it in my design. The AND gate adds a whole extra unnecessary component, which drives up the cost of your project (not a big deal if you're just making one, but I'm making 50-100 of these). The other problem is that it requires the use of an additional microcontroller pin (or external PWM source), and considering that I was already making use of all of the output pins on my 8-pin PIC, there simply wasn't an extra pin I could use.

What's the alternative solution? Use the DAC to modulate the data. Seriously. I'll walk you through the process I used to figure this out, and provide the code I used in case you'd like to implement it yourself.

I wanted a decent amount of range with my transmitter, so instead of driving my IR LED at 25mA (the max an I/O pin can safely handle), I decided to drive it with a simple BJT (2n3904). Obviously I wanted the drive signal to be the TX pin of the PIC, so I threw together a quick schematic:

You can see the IR LED hooked up to the BJT's collector, and the base is driven with the PIC's primary TX pin (pin 7), with a series resistor to limit current.

I knew that the photodetector I'm using has a band-pass filter that harshly attenuates signals outside of 38KHz, which means that I didn't have an option other than to modulate my data at 38KHz. As seems to be a common trend with my microcontroller projects, I simply decided "Oh, I'll do something clever in software later." With that, I went ahead and placed an order for a few prototype boards without looking all that closely at the PIC's datasheet.

With the boards on the way, I started cracking on the firmware. To begin with I had some vague notion that I'd write data out using the USART while simultaneously toggling the TX pin between input/output at 38KHz. Well, my boards came in and quickly after soldering one together and checking the data on a scope, I discovered that my original plan wasn't going to work. The USART was writing data out just fine, but unfortunately it just wasn't being modulated at all. The module just overwrites the pin state to output if you set the TRISA register declaring it an input.

Following a couple of days of painful troubleshooting, trying alternative methods, and just generally not getting anything to work, I noticed this unassuming chart right here:

(Taken straight from the datasheet)
Basically, functions higher on the list are able to dominate the state of the associated pin. Notice how pin RA0 (the main TX pin) has 3 functions with a higher priority than the TX function? Well, ICSPDAT is just used for programming so it's not helpful in this case. However, CWG1B (an output of the complimentary waveform generator) and the DAC (DAC1OUT) are both listed as higher priority. That means that if the CWG or the DAC to outputs to the pin while TX is writing to the output buffer, the TX is going to get overridden.

Okay, so that's one piece of the problem, the other one is how to convince the CWG or the DAC to take control of the pin at exactly 38KHz. The CWG is basically able to PWM, so you'd think it'd be the best choice in this case - it turns out that wasn't super helpful because I didn't actually want to drive the pin high then low, I just wanted to change its state between driven and undriven by a specific peripheral. Accordingly it didn't really matter whether I used the CWG or the DAC, so naturally I went with the DAC for extra hilarity (seriously, how can you resist being able to write obtuse comments like "// Enable DAC to send data over serial"?)

I used Timer1 to hit overflow at about 38KHz*2 = 76KHz, triggering an interrupt which toggles the output state of the DAC (which defaults to writing 0V). When the DAC's output is on, it drives the TX pin at 0V, and when the output is disabled the TX is allowed to regain control of the pin state. I also made sure to use a slow baud rate (4545) in order to allow enough 38KHz pulses to sufficiently modulate the signal.

Without further ado, the code:

void Setup(void)
    OSCCON = 0b01111010; //16MHz internal clock

    TRISAbits.TRISA0 = 0; // Set RA0 as output
void Send_Packet(uint8_t data)
    DACCON0bits.DACEN = 1; // Enable DAC to send data over serial

    SPBRGL = 54; // Baud rate is 4545
    TXSTAbits.SYNC = 0;  // Asynchronous mode
    RCSTAbits.SPEN = 1; // Enable EUSART
    BAUDCONbits.SCKP = 1; // Inverted mode (idle low)
    TXSTAbits.TXEN = 1; // Enable transmitter

    TXREG = data;
    while(!TXSTAbits.TRMT); // Wait for USART to send all data
    TRISAbits.TRISA0 = 0; // Allow RA0 to drive low
    DACCON0bits.DACEN = 0;

void Modulate_Serial(void){
 INTCONbits.GIE = 1; // Enable interrupts
 INTCONbits.PEIE = 1; // Enable peripheral interrupts
 PIE1bits.TMR1IE = 1; // Enable Timer1 overflow interrupt

 T1CONbits.nT1SYNC = 1; // Don't synchronize external clock input
 T1CONbits.TMR1ON = 1; // Turn on timer
 TMR1 = 65484; // Initialize TMR1 value for desired frequency

void Disable_Modulation(void){
    INTCONbits.GIE = 0;
    T1CONbits.TMR1ON = 0;

// Interrupts

void interrupt High_Priority_Interrupt(){
    if(PIR1bits.TMR1IF == 1) // TMR1 register overflowed
    DACCON0bits.DACEN ^= 1; // Use DAC to generate 38KHz carrier wave
       TMR1 += 65484;
       PIR1bits.TMR1IF = 0; // Clear the interupt flag

That's pretty much it! All you'd need to do to use this (in MPLAB X with XC8 as your compiler) is add a header file with prototypes for those functions.

He's the final scope shot showing off the modulated waveform, thanks to the DAC:

Yay! Everything works the way it's supposed to.

Over all it was an exciting few days coming up with this somewhat disgusting (but mostly hilarious) hack, and I figured it was actually worth posting about since someone might find it useful. Let me know if you have any questions!


  1. Thanks for the writeup on this, interesting solution to the problem. If I understand the method correctly the DAC is setup to always write 0 and is just enabled and disabled on timer interrupt. So in the screenshot above the DAC enable is constantly getting toggled but even though there are gaps where it is all low it is just toggling between the low data bit (0) and DAC enable (0). When there is a high bit it then then actually causes toggling between the bit (1) and DAC enable (0)

    Clever trick, I like it.

  2. Awaiting your next post, updates on latest developments or what you are up to?

    1. My apologies, I've definitely let this blog slip by - lately I've been consulting for this awesome company: and designing their electrical systems.

      I do have a lot of projects that haven't been written up yet, so I'd like to get around to that sometime soon.

      Thanks for reading!