Tuesday, October 28, 2014

FS-T6 and SWD hack


Preparing the FlySky-T6 RC TX for development with SWD. 


SWD = Single Wire Debug

  Well, the SWD at a minimum requires 3 wires: clock, data and ground (great marketing though).  In addition, it is very handy to add a reset line (nSRST_.  This line would reset all the devices around the core CPU in the SoC chip in addition to restarting the CPU core.  Otherwise, starting conditions would differ form run to run and I often see hangs.  I have tried to use the 3 wires (aka ST-LINK V1, as featured on STM32FVLDISCOVERY with CPU similar to the T6's stm32f1xx chip). I did not like it.  Hence later, I have hacked in the 6 pin connector.










STM32F0Discovery

Discovery

Discovery stm32f1discovery is equipped with 4 pin SWD.  All other discoveries AFAIK are ST-LINK V2 and come with 6 pin SWD.  Remove the jumpers as shown on left to disconnect the on-board SWD control from the CPU (the one that also plugs into your PC's USB; discovery docs) then you can use the SWD connector to pulg it into T6.

Notably, the V1 and V2 also differ in software. and in general the V2 is much more reliable according to openocd references.









4-pin SWD

Schematics

Here are the schematics for 4,6 pin and full 20 pin SWD connector as seen on T6.


Note that SWD lines are also used by JTAG. Through some signaling magic CPUs would cpu obey (switch into) SWD instead of JTAG.




The 4 pin is identical to the first 4 pins on 6-pin connector - handy!.




6-pin SWD
The VDO pin (pin 1 of the connectors) is not used - it is disconnected on discovery boards. I tied it to +3V3 on T6's side.











Connections to T6


T6 20-pin "full" debug connector


I have connected the following:
 U2(T6)    CN3 (DISCO)
+3.3V P1---P1 VDD 
TCK   P9---P2 TCK
GND   P20--P3 GND
TMS   P7---P4 TMS
nSRST P15--P5 NRST
GND   P18--P6 SWO

Also the SWO is not available on F1xx CPU (this is used normally for trace output). This schematics is from ar-t6 .


Here is the final result. Pin #1 is marked with a sqare, left lower row.
I have also cut out small opening for a standard 6-ping header (female) that I have crimped and than hot-glued to the case. So now I can close the T6 and have it ready for flying in no time. Additionally, I found a 12V PS with correct plug and can now power T6 on my desk w/o batteries - this is a bonus and not required.

Let me know.
...sorry for formatting - blogger is a pile of streaming google...and google really stinks recently...


Monday, October 27, 2014

Embedded software with Eclipse, Arm, Stm32fxxx, OpenOCD to develop FlySky FS-T6 trasmitter firmware

This is a very terse dump of my experience of setting up a development environment for developing software for STM32Fxxx series of ARM processors. These CPUs come in a VERY inexpensive "discovery" boards (e.g. STM32F0DISCOVERY,  STM32F3DISCOVERY, STM32F4DISCOVERY) - very fun to toy with (usually $10-20 and no need for any "emulators", "jtags" programmers etc). So this is cheaper then the famous "arduino" and you get a 32bit processor with tons of peripherals. I actually used this to start developing firmware for FlySky-T6 RC transmetter following the fantastic work here.

In order to develop embedded software one usually needs: text editor, build tools, programmer/debugger tools. Here there are in order:
- Eclipse
- gnu arb toolchain
- openocd

The Eclipse is an IDE (Integrated Development Environment) that integrates editor, project management, build, debug documentation in one common, portable, extendable GUI.

The gnu arb tool chain provides compiler/linker/stdlibraries/debugger to produce executable code.

The openocd provides access to hardware. It can communicate with hardware debugger agent and provide flash memory access and debugger server that then can interact with gdb (the gnu tools debugger). An ST-LINK/ST-UTIL (texane or STM versions) can be used instead as well.

Eclipse/GNU/OpenOCD setup

Eclipse:
http://www.eclipse.org/downloads/

this usually ends up a "java" development without CDT ("C/C++"). I have downloaded Eclipse "Luna" .
Install CDT Plugin (skip this if you've donwloaded a CDT-Eclipse):
http://www.eclipse.org/cdt/downloads.php
in the above find correct "p2 repository" link --> Copy to clipboard
in eclipse: Windows->Install New Software->Paste Link
install main CDT + GCC and HW debug
Install GNU ARM Plugin
install "p2 repository":  http://gnuarmeclipse.sourceforge.net/updates
re http://sourceforge.net/projects/gnuarmeclipse/files/Eclipse/updates
Install GCC ARM toolchain (from launchpad):
https://launchpad.net/gcc-arm-embedded/+download
setup for win32 or in debian/ubuntu apt-get install
Creating first project
select "C/C++" perspective - on the right upper, if not there "+" (add) it
file->new C/C++ project select one for your CPU; see below

WARNING - after days of somewhat working debugger I faced gradual dysfunction: 1) eclipse stopbbed being able to interract with openocd in "pipe" mode (ie when it launches oocd itself) - I have switched to running oocd manually. 2) later it decided to gray out (disable) all debug buttons even after successful start and hitting a breakpoint. This was described in this thread:
https://www.eclipse.org/forums/index.php/t/791751/. Solution was to uninstall "GDB Hardware Debug support" (the openocd pluging started to work!).

OpenOCD - Windows

Specific to the ar-t6 firmware:
Notes:
On windows you'd get 2 build errors - can't find "echo" and "make" - solution is to steal them from codesourcery as per http://gnuarmeclipse.livius.net/blog/build-tools-windows/:
http://sourceforge.net/projects/gnuarmeclipse/files/Miscellaneous/Cross%20Build%20Tools.zip/download
but an alternative would be to install gnuwin32 and add them to the path.

Based on similar w/ CodeSourcery:
http://en.radzio.dxp.pl/stm32vldiscovery/programming,with,opensource,toolchain,codesourcery,eclipse.html
http://gnuarmeclipse.livius.net/blog/openocd-install/
http://www.freddiechopin.info/en/download/category/4-openocd
usb drivers for ST-LINK:
http://www.st.com/web/catalog/tools/FM147/SC1887/PF258167
alternatively (not adviced) http://zadig.akeo.ie Options->show all->ST-LINK UpdateDriver

There may be a need to install libusb0 (can't tell if it got installed with zadig or came with ST-LINK installation). Just in case you may want to install it from here:
http://sourceforge.net/projects/libusb-win32/files/latest/download

Then in eclipse: http://gnuarmeclipse.livius.net/blog/openocd-debugging/ but it does not run openocd so had to manually run
"openocd -f stm32f0discovery.cfg -s c:\home\openocd-0.8.0\scripts"

The solution is to change to windows backslashes. Make sure the cmd params are correct as eclipse would not give you any hints as to why it failed to start it.

Other/T6 related


STM32F1xx standard peripheral library
http://www.st.com/web/catalog/tools/FM147/CL1794/SC961/SS1743/LN1734/PF257890
but no need for it as it comes with ARM plugin...

I see occasional communication problems between eclipse (gdb?) and openocd debug server. Killing the openocd process from Task Man restarts system to working condition.
git clone the ar-t6 into your "workspace" directory. the proceed to open project. If you do not know eclipse you will go through world of pain as it is not intuitive. That's okey, it will pass...

The original project builds fine with gnu tools (with some warnings about code sourcery paths) . It contains CMSIS library (arm and stm parts).

One needs to setup openocd "Debug Configuration" as per
http://gnuarmeclipse.livius.net/blog/openocd-debugging/

Also in addition I have setup a special stm32f1-fst6,cfg (saved into openocd/scripts/board)  :

# This is an FlySky T6 base on STM32F100R8
source [find interface/stlink-v2.cfg]
source [find target/stm32f1x_stlink.cfg]
# use hardware reset, connect under reset
reset_config srst_only srst_nogate

That's what goes into "config options" in the "debug" tab of debug configuration :
-f stm32f1-fst6.cfg
Try this from CLI
openocd -f stm32f1-fst6.cfg

It should report the chip id and number of hw breakpoins etc. and then wait for GDB to connect.

Also in the debug configuration that you've set up per "openocd eclipse setup" I had to change the "debug/gdb client setup/executable" to explicitly say "arm-none-eabi-gdb" as the at-t6 project does not seem to define standard "cross-" macros. The debug configuration got committed to github as "ar-t6 Debug.launch" but it's a "Windows" one.

Wednesday, April 20, 2011

Kitesurfing: Leading Edge Blowup Kite Repair

(experimental, ie do not follow)

My Flexifoil Ion3 9m blew up. To add insult to the injury it blew up on the beach, in my hands when I was inverting it to pack and go home. Darn! Looks terrible. I simply think it just got old and tried to quit on me. No such luck, buddy! The quitting I mean, not getting old.

So here is my attempted, DIYig (DIY in garage) revival report. Some things worked, some were messed up. I am yet to try to fly it...


The repair was a two step process: the bladder and the canopy. Since I am lazy by nature I decide not to pull out the bladder completely but just slide it out of the tip. This was possible since the damage was close to the tip.
So here it is, the bladder with a fist-size hole. They say, cut it open and glue a patch from the inside. I say no need to fix a hole by creating another hole.



I removed the air connection from only one strut and rolled the bladder so it does not get in the way.Then I used some sticks to stretch damage area.

I used a bigger-then-palm patch out of an old bladder and put it into the bladder through existing opening (technical term to describe a hole). Just made the patch way oversize so when glued with AquaSeal, the glue would not have a chance to glue the bladder's walls together. Here the black marks the edges of the patch (it's inside the balder). The glue already applied through the hole (I have cut out the original damaged area from the blown-out material).


I have also applied another patch from the outside. I have spread the glue to keep the edges of the outside patch glued to the bladder.

After 12 hours under load, the glue turned yellow. I have not used AquaSeal before so I have not idea if this is OK....It feels flexible and strong.

This is how it looks like cured.

Anyway, the only other problem was that there were small pockets of air left between the patches. But is this a problem really?

This part was relatively easy. The stitching required patience and a lot of ...
Here it is, the top seam opened to make more space for sewing. I had only a fairly thick dacron from my boating days. I have also used the double sided sewing tape 3/4" (the white strips already attached to the edges of the damage. I have decided to put a single band of dacron and sew it in.


In order to help with alignment I first applied a insignia cloth to the outside. This helps keeping the edges together and in good place.

Then I placed the dacron into the double sticky tape. The insignia cloth was a second take idea. I found that the sewing tape is not sticky enough to hold the edges together.


Sewing the seeds of love..ahem...adjusting my flaky walking foot machine is always a challenge, esp when I have not used it for a few years. She's got rusty, so did I.

Four rows of zig-zag from the edge to the edge. I had to rip it out only once when the sail cloth got sewn in. Only once I swear!
Then I got a brilliant idea to strengthen the cloth and put a spinnaker repair tape around the edges. Esthetic's got lost in the process (the tape is white).
But where did the bladder go? I dropped it rolled into the LE shoulder. Kept it nicely out of the way.

Sew him up, doc! I had to get the bladder out. Stretch it and somehow pray I will not sew it in while closing the leading edge.

Here, under (minor) pressure test....It worked!. Just royally miscalculated (read "screw up") one small detail. The edges got quite strong with extra spinnaker tape and with the dacron (see: it is nicely going all the way from edge to edge). But...unfortunately, the dacron I used way thicker then the original cloth and hence I could not fold it. Darn!  This made the repair downright ugly...


the good


This is plausible result. Kite survived nightly pressure test. and looks fine on the outside.




the bad and the ugly

Note the LE deformation. I suspect it could be a result of original damage and/or my sewing. It looks like the circumference is smaller in this area. Perhaps, I should redo the top seam. One day. Perhaps.

Oh, well. I only meant the repair as an experiment....I do not think I'd take it into waves anymore. But on flats or as a learning kite it is still OK.

Monday, December 20, 2010

YAPF - Yet Another Pricture Frame

with "viewer presents awareness" and geeky frame. 

Yup, yet another digital picture frame made from an old laptop. Added value: "viewer awareness intelligence". Say what? Ohh, nothing big. Simply a PIR (passive infrared sensor) to detect motion and turn on/off the back-light lamp. This prevents the lamp from burning out too quickly. To make things more interesting, a frame made out from discarded industrial printed circuit board. Perfect for the geek's house, ain't it?






Why ?


I have few of these long-obsolete, unwanted laptops in my garage and a desire to do something with them. They work...sort of. You can run Win95 on one, a Win3.1 on another and DOS on the oldest one. OK, some of them are too old. I have picked a ca.1997 Omnibook 800CT (Pentium MMX 166MHz/80MB RAM 800x600 10" TFT LCD). 


Now what?


Obviously this has been done for years. Short recipe here: 
  1. Get a CF card (>=128MB) and a laptop IDE-to-CF adapter (eBay)
  2. Using your USB card reader and your Linux Desktop (you have one, right?) install GRUB into it and a (preferably small) Linux distro.
  3. Install a picture viewer of choice and make it run at start.
  4. Copy pictures to a subdirectory.
  5. Remove the screen hinges and the skirts (front plates) from the laptop, then build a frame around it.
  6. Plug the CF & adapter in your laptop, reboot.
  7. Enjoy.

Add salt to taste.
Here are hints
CF card & adapter in IDE connector
  • Use GRUB  pre-2 version (I used 1.98). Newer may not work with old machines. There is plenty of help on-line.
  • I used TinyCore, a 10MB distro. On Omnibook 8xx series the Xvesa does not work so I had to install Xfbdev.
  • For picture viewer I used FEH, avaliable for TinyCore as add on package. The only downside of FEH is lack of transitions, but with weak CPU this may be actually be a benefit.
  • Add a script to start FEH after X started. The script that starts FEH goes into /home/tc/X.d/show.sh. It contains:
    • feh -rzFD60 --hide-pointer /mnt/hda1/Pictures &
    • as you see the pictures are in /mnt/hda1/Pictures (ie in the directory /Pictures in the top of the drive
    • the rest of options tell feh to do randomized slide show changing picture every 60s


Frame w/o laptop
Mechanical stuff

The frame is make of two identical PCBs one with elements the other blank. I have taken them from a pile of garbage. By size they are big. Old PC motherboards may be of use here. A dremel and angle grinder with cutting blade are your friends. All is held together by 6 stand-offs. The laptop slides in and is screwed into the front PCB.








PIR sensor aka "viewer presents awareness"


This was the most fun. It really had few parts: reading the PIR sensor, controlling the backlight, implementing the script to bind them all. 


To Port or not to Port


Laptop with PIR and PP connected
The CPU part is tackled under.
When this laptop was designed (ca.'95) there were no USB ports. But Parallel and Serial were in full swing. Both ports are easy to interface with and both have some lines that can be directly controlled (Serial has modem control likes, while Parallel is pretty much fully controllable). However, for the ease of software implementation I have chosen Parallel as it can be accessed from shell scripts through /dev/port. The down side of Serial is a harder control from scripts and its +/- 12V levels. So parallel it is.


However, my laptop (like many other) has and open-collector outputs that are internally pulled-up by 1kOhm resistors. Way too much for the Parallax PIR sensor I was planning to use (it ends up about 4mA drain). so I had to add a transistor "amplifier". Here is a good spill about the sensor and how to connect some load to its "Out" pin. The transistor output (collector) got connected to pint #15 of the port (which leads to bit #4 in STAT register).




Let there be...darkness


Modified inverter and PIR output amplifier.
All powered from +5V available on the inverter.
How to control backlight? There are more then a few ways. Normally, newer hardware would have ACPI which has backlight control. But my laptop has older APM which does not. So much for direct control. Another idea would be to decipher the keyboard and pretend a  "PWR" button press, which turns the display off (but let the CPU to continue). This proved hard to interface as the keyboard is connected through the thin "foil" connectors. So, in desperation I looked at the CCFL inverter and, the proverbial "bulb" went on in my brain. The inverter is build around LT1184CS chip, which has a "shutdown" pin. Bingo! Well, not quite. A little probing showed that the pin is shorted to +5V by some buried trace. So I got out my 6-pack of whoop-ass and lifted the pin. Added pull up 6kOhm resistor (probably not necessary as PP has pull-ups but I did not want the darkness when not in control). Then connected it to pin #14 (bit #1 in CONTROL register). 


Scribbling the scripts


I choose to implement the monitoring of the PIR in shell scrpt. There are 3 scripts: rdport, wrport and monitor. All placed in ~/.local/bin and invoked from .profile. Granted, I could simply hooked up the PIR to a timer such as NE555 and control the backlight directly. But writing script was more fun. As benefit, the scripts can log activity in my house (do not know why I'd need it though).


Again, few obstacles to be solved:
  1. how to read/write a port
    • access through '/dev/port'
    • must be root hence 'sudo'
    • use dd with seek/skip sudo dd bs=1 count=1 of=/dev/port seek=$1
    • convert a string '123' to actual byte :  echo -n $2 | awk '{printf("%c",$0)}'
    • convert byte to a sting: hexdump -e '/1 "%u"'
  2. how to interpret and generate binary values
    • motion=`dc $pir 8 xor 8 and p`

The script to read any ISA port 'rdport [addr]':
sudo dd bs=1 count=1 if=/dev/port skip=889 2>/dev/null | hexdump -e '/1 "%u"' 


The script to write any ISA port 'wrport [addr] [val]':

echo -n $2 | awk '{printf("%c",$0)}' | sudo dd bs=1 count=1 of=/dev/port seek=$1 2>/dev/null 
There is a small problem with this script as I cannot write 0. I think the busybox implementatino of awk's prints has problems printing 0 as this is a string terminator in C.


A script to monitor the PIR state and control the CCFL inverter "shutdown". This is the "brain". The addresses of PP CONTROL and STATUS registers are 889 and 890. The script only updates the 'shutdown' signal when its state actually changes.


The jest:
  • loop forever
    • check the sensor state
    • if motion detected update "last motion" time stamp
    • if "last motion" time stamp tool old (here 300 seconds) then turn off the backlight
    • otherwise turn on the backlight
#!/bin/sh
# monitor PIR sensor and control backlight


# handy function for time-stamping
now() { 
  echo -n `date +%s` 
}


# initialize start conditions
maxElapsed=300 # 300 SECONDS = 5 MIN
off=1
lastMotion=$(now)


# loop forever
while true; do


  # read and check PIR (bit #3 of ParPort hence mask is 2^3=8)
  pir=`rdport 889`
  motion=`dc $pir 8 xor 8 and p`
  if [ $motion -ne 0 ]; then
    lastMotion=$(now)
  fi
  # see if enough time elapsed since last registered motion
  elapsed=$(expr $(now) - $lastMotion)
  if [ $elapsed -gt $maxElapsed ]; then
    requestOff=1
  else
    requestOff=0
  fi


  #echo $pir $motion $lastMotion $elapsed $expired $off


  # if changes the state of backlight then apply it
  if [ $requestOff -ne $off ]; then
    off=$requestOff
    if [ $off == 1 ]; then
      echo -n 'OFF: ';date;
      wrport 890 2
    else
      echo -n 'ON : ';date;
      wrport 890 1
    fi
  fi


done



Todo


I'd like to have Forward/Backward buttons.
Have it WiFied to my picture archive (get a better laptop for this).

Make it serve beer.
Go on line and download some ...wait!



THE END

Monday, October 11, 2010

Kitesurfing: Replacing a strut bladder with U-Stick orange bladders

Here is a story of a bladder (replacement) on an inflatable kite.


Twice-Broken valve
Recently, I had a failure in my Flexifoil 9m Atom3 after I left it simmering in the sun for several hours (at Rio Vista, CA.) After having an "interesting" session with half-deflated kite I was faced with a repair. The heat caused delamination of the strut valve from the bladder. I have had local repair shop repair it. They glued the valve back but the repair looked terribly (with some part of bladder glued together, ripped apart and left hangin and bladder folds). I have reinstalled it anyway and tested it overnight. It held the air. I did not go out for several days (no, the kite was no longer on the sun).
Next time I have inflated it on the beach the kite quickly lost lots of air. I discovered that although the glue held, the valve become cracked and it had a serious leak. Apparently the repair damaged the valve base. The damage looked like it was heat induced. I bet it was overheated during the repair (the valve is usually completely unglued by "coocking" it). So much for "professional" repair.

Orange and original bladders.
So, a full bladder replacement was in order: U-Stick orange bladder and valve. The Airtime Kite people were awesome. They quickly advice me what I should use. The strut valves, both on main and strut bladder are called "option" as you can choose different stems. Flexifoil has a 11mm valves with straight stems. There is also a "replacement" option valve which does not have "options" as it comes only with straight stem. Also, Airtime has nice on-line bladder selection tools. My kite needed 50cm bladder. The valves fit the opening in the kite and did not need rings. Ordering and shipment was a snap, thanks Airtime!

I followed instructions attached to the bladder and valve. However, there were few small surprises.

The 50cm bladder is visibly much bigger then the original Flexifoil one. Too big is OK as the kite will restrict the expansion, too small would be bad (too much stretch). The original bladder was also 50cm (just much smaller). 


The material is more brittle sounding (it sounds more like a "shopping bag" while Flexifoil's is more like "sex-rubber", if you know what I mean.) Just observing the visual difference which has nothing to do with other physical properties like strength. I just said "oh well it must work fine."


REALLY be careful when cutting the hole in the bladder for the valve. The bladder is completely air tight when shipped so pulling it apart is hard. Never thought that vacuum can be so hard :-)
Bladder prepared for surgery


When sticking the valve there were more surprises.

The bottom of the valve protrudes from the base so it is perhaps better to use soft surface to glue on, like a towel.
Bottom of stem protrudes beyond the base
It is hard to align it with the hole before the valve sides will start catching the bladder and when they do, it is over; perhaps a helper could stretch the bladder flat when you align the valve and stick it, mine cough bit early and got slightly misaligned with the cutout.

The valve glued to the bladder.
The diameter of the opening in the end of strut is so small that I had to bend the base of the valve to fit it in. Then I had to push it in through with some force. Needed to get a bit medieval with it, but it worked. The rest of the way was traveled with a "string attached."

Size matters?

The bladder is so much longer than the original even though I followed the instructions and folded top and bottom inside to match original length. I have folded it more so it flashed with the pocket.

Too long?

The valve is a bit shallower then the original so the pipe between main the strut seems too short and bends the new valve. I wonder how it's going to work long term?.

Too short stem?

So far so good, the kite flies and makes his owner smile. The replacement was $35, a mare $15 more then the "professional" repair that failed.


References:




Wednesday, August 5, 2009

Arduino+Accelerometer=Computerized Level Vial?

What?
Ok! So I hooked up Arduino Pro Mini, an LIS302DL 3 axis accelerometer and Nokia 5110 (PCD8544 based) LCD. And What did I do with all of this? A vial level meter emulation! Right you are, not a very useful application of such a pile of technology. But so much fun! Besides, It was easier to do it this way rather then trying to melt some sand into a glass vial...


video
Really?
Yep! Both the accelerometer and LCD are SPI devices so they share MOSI, SCK and MISO (LCD does not have output). Then there are a few RESETS and SELECTs pins and this is pretty much it (plus an old cellphone battery and a charger circuit). The glue is the code.

Here!
The Arduino sketch simulates some physics rules. The LCD displays a "ball rolling in a bowl" an object rolling in a indented surface shaped like a shallow bowl. By inclining the contraption in earth gravity field, the accelerometer pick ups the motion, its readings converted into change vector acting on the virtual ball, as if someone was tilting a real bowl and hence moving the ball around. I have tossed Newton laws of motion and some mass and friction into the mix.

Pardon my CODE
Highly experimental. The code uses 2 libraries one for accelerometer and one for LCD. Warning: Since I hate to waste space and CPU cycles, I diverged from classic Arduino singleton-object style (like in the "Serial" code-space killer). With each call to such object, there is a hidden parameter passed: the "this" pointer (and pointers on Avrs are 2 bytes). And then each function code must obey such pointer when it accesses its data members but there is never ever going to be another instance of such class (and hence another value of "this" pointer), so it is a pure waste. Granted, the source code looks more like C# or Java (Serial.println())....but I am a purist when it comes to execution...So instead I have created a static classes with only a header file to include (no cpp), perhaps I should call them "includaries"? I only wish Arduino was help while it comes to #includes, intead it is so much more (pain)!

Accelerate!

acm.h

////////////////////////////////////////////////////////////////////////////////
// ACceleroMeter LIS302DL
////////////////////////////////////////////////////////////////////////////////

#ifdef PREDEFINED_PINS
// SPI bus pins
#define PIN_SSEL 10
#define PIN_SDIN 11
#define PIN_SDOU 12
#define PIN_SCLK 13

// ACCELEROMETER
#define PIN_ACM_CE 7

#define SPITRANSFER(data) { SPDR = data; while (!(SPSR & (1<<SPIF))) ; }
#define SPIINDATA SPDR

void SPIInit()
{
// set direction of pins
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
pinMode(PIN_SDOU, INPUT);
pinMode(PIN_SSEL, OUTPUT);

// SPCR = 01010000
// Set the SPCR register to 01010000
//interrupt disabled,spi enabled,msb 1st,master,clk low when idle,
//sample on leading edge of clk,system clock/4 rate
SPCR = (1<<SPE)|(1<<MSTR)|(1<<CPOL)|(1<<CPHA);
byte clr;
clr=SPSR;
clr=SPDR;
}

#endif

class ACM
{
public:

static inline char ID() { return ReadReg(0x0f); }
static inline byte STATE() { return (byte)ReadReg(0x27); }
static inline char X() { return ReadReg(0x29); }
static inline char Y() { return ReadReg(0x2b); }
static inline char Z() { return ReadReg(0x2d); }

// write to a register
static void WriteReg(byte reg, byte data)
{
// SS is active low
digitalWrite(PIN_ACM_CE, LOW);
// send the address of the register we want to write
SPITRANSFER(reg);
// send the data we're writing
SPITRANSFER(data);
// unselect the device
digitalWrite(PIN_ACM_CE, HIGH);
}

// reads a register
static char ReadReg(byte reg)
{
WriteReg(reg|128,0);
return SPIINDATA;
}

static void Init()
{
// CE pin, disable device
pinMode(PIN_ACM_CE,OUTPUT);
digitalWrite(PIN_ACM_CE,HIGH);
// start up the device
// this essentially activates the device, powers it on, enables all axes, and turn off the self test
// CTRL_REG1 set to 01000111
WriteReg(0x20, 0x47);
delay(250);
}

}; // ACM


Show!

lcd.h

#ifdef LCD_PREDEFINED_PINS
#define PIN_LCD_SCE 3
#define PIN_LCD_RESET 4
#define PIN_LCD_DC 6

// SPI bus pins
#define PIN_SSEL 10
#define PIN_SDIN 11
#define PIN_SDOU 12
#define PIN_SCLK 13

#ifdef SPI
#define SPITRANSFER(data) { while (!(SPSR & (1<<SPIF))); SPDR = data; }
#define SPIINDATA SPDR
#else
#define SPITRANSFER(data) shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data)
#define SPIINDATA 0
#endif

#endif

class LCD
{
public:
enum
{
XM=84,
YM=48
};

static void Fill(byte pattern, int count)
{
digitalWrite(PIN_LCD_SCE, LOW);
while(count-->0)
{
SPITRANSFER(pattern);
}
digitalWrite(PIN_LCD_SCE, HIGH);
}

static void Clear()
{
Fill(0, XM * YM / 8);
}

static void Char(byte character)
{
static const byte ASCII[][5] =
{
{0x00, 0x00, 0x00, 0x00, 0x00} // 20
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ?
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f ?
,{0xff, 0x81, 0x81, 0x81, 0xff} // 80 frame
,{0x18, 0x24, 0x42, 0x24, 0x18} // 81 diamont
};

digitalWrite(PIN_LCD_SCE, LOW);
if( character < 0x20 || character-0x20 >= sizeof(ASCII)/5 )
{
for (byte index = 5+2; index > 0 ; index--)
{
SPITRANSFER(character);
}
}
else
{
SPITRANSFER(0x00);
for (byte index = 0; index < 5; index++)
{
byte data = ASCII[character - 0x20][index];
SPITRANSFER(data);
}
SPITRANSFER(0x00);
}
digitalWrite(PIN_LCD_SCE, HIGH);
}

static void Init(void)
{
pinMode(PIN_LCD_SCE, OUTPUT);
pinMode(PIN_LCD_RESET, OUTPUT);
pinMode(PIN_LCD_DC, OUTPUT);
digitalWrite(PIN_LCD_DC,HIGH);
digitalWrite(PIN_LCD_SCE,HIGH);
digitalWrite(PIN_LCD_RESET, LOW);
digitalWrite(PIN_LCD_RESET, HIGH);
delay(10);
#if 1
Cmd( 0x21 ); // LCD Extended Commands.
Cmd( 0xC8 ); // Set LCD Vop (Contrast).
Cmd( 0x06 ); // Set Temp coefficent.
Cmd( 0x13 ); // LCD bias mode 1:48.
#endif
Cmd( 0x20 ); // LCD Standard Commands, Horizontal addressing mode.
Cmd( 0x0C ); // LCD in normal mode.
// Cmd( 0x0D ); // LCD in reverse mode
}

static void Goto(byte row,byte col)
{
digitalWrite(PIN_LCD_DC, LOW);
digitalWrite(PIN_LCD_SCE, LOW);
SPITRANSFER(0x40|row); //Cmd(0x40|row);
SPITRANSFER(0x80|col); // Cmd(0x80|col);
digitalWrite(PIN_LCD_SCE, HIGH);
digitalWrite(PIN_LCD_DC, HIGH);
}

static void Str(char *characters)
{
while (*characters)
{
Char(*characters++);
}
}

template<unsigned base>
static void Num(unsigned num,char digits=0)
{
unsigned div = 1;
// find biggest power of base that is not greater then num
// keep going when more digits are requested
while( true )
{
unsigned newdiv = div*base;
if( newdiv < div ) break; // overflow
digits--;
if( newdiv > num && digits<=0) break; // end
div = newdiv;
}
while( div > 0 )
{
unsigned dig = num / div;
char c = dig < 10 ? '0'+dig : 'A'+dig-10;
Char(c);
num-=dig*div;
div/=base;
}
}

template<unsigned base>
static void Num(int num,char digits)
{
if( num<0 )
{
Char('-');
num=-num;
digits--;
}
Num<base>((unsigned)num,digits);
}

private:
static void Cmd(byte cmd)
{
digitalWrite(PIN_LCD_DC, LOW);
digitalWrite(PIN_LCD_SCE, LOW);
SPITRANSFER(cmd);
digitalWrite(PIN_LCD_SCE, HIGH);
digitalWrite(PIN_LCD_DC, HIGH);
}
};

The MAIN feature

// SPI bus pins
#define PIN_SSEL 10
#define PIN_SDIN 11
#define PIN_SDOU 12
#define PIN_SCLK 13

#define SPITRANSFER(data) { SPDR = data; while (!(SPSR & (1<<SPIF))) ; }
#define SPIINDATA SPDR

////////////////////////////////////////////////////////////////////////////////

void SPIInit()
{
// set direction of pins
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
pinMode(PIN_SDOU, INPUT);
pinMode(PIN_SSEL, OUTPUT);

// SPCR = 01010000
// Set the SPCR register to 01010000
//interrupt disabled,spi enabled,msb 1st,master,clk low when idle,
//sample on leading edge of clk,system clock/4 rate
SPCR = (1<<SPE)|(1<<MSTR)|(1<<CPOL)|(1<<CPHA);
byte clr;
clr=SPSR;
clr=SPDR;
}

// ACCELEROMETER
#define PIN_ACM_CE 7
#include "{yourpathhere}\accelerometer.h"

// LCD
#define PIN_LCD_SCE 3
#define PIN_LCD_RESET 4
#define PIN_LCD_DC 6
#include "{yourpathhere}\lcd.h"


////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////

void setup(void)
{
Serial.begin(9600);

// set direction of pins
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
pinMode(PIN_SDOU, INPUT);
pinMode(PIN_SSEL, OUTPUT);

pinMode(PIN_ACM_CE,OUTPUT);
digitalWrite(PIN_ACM_CE, HIGH);
pinMode(PIN_LCD_SCE,OUTPUT);
digitalWrite(PIN_LCD_SCE, HIGH);

SPIInit();

LCD::Init();
LCD::Clear();
ACM::Init();
Serial.println(ACM::ID(),HEX);
Serial.println(ACM::STATE(),HEX);
Serial.println("=============");
}


const byte BALLSIZE = 4;

void DrawBall(byte x, byte y, byte erase)
{
byte row = y / 8;
byte shift = y % 8;
unsigned pattern = (1<<BALLSIZE)-1;
pattern <<= shift;
byte f;
f = pattern & 0xFF;
if( f )
{
LCD::Goto(row,x);
LCD::Fill(erase ? 0 : f, BALLSIZE);
}
f = pattern >> 8;
if( f )
{
LCD::Goto(row+1,x);
LCD::Fill(erase ? 0 : f, BALLSIZE);
}
}

static byte curx = 0;
static byte cury = 0;

void MoveBall(byte x, byte y)
{
if( curx != x || cury != y )
{
DrawBall(curx,cury,1);
curx = x; cury=y;
DrawBall(curx,cury,0);
}
}

//////////////////////////////////////////////////////////////////////////////////////////

// all units are SI : meters, seconds etc.

// G force (earth gravity)
const float G = 9.81; // m/s2

// pixel size
const float pixsize = 3e-2/LCD::XM; // in meters =~ width(3cm)/numpixX
// ball size
const float ballsize = BALLSIZE*pixsize;
const float xsize = LCD::XM * pixsize;
const float ysize = LCD::YM * pixsize;
float x=float(LCD::XM/2)*pixsize;
float y=float(LCD::YM/2)*pixsize;
float px=float(LCD::XM/2)*pixsize;
float py=float(LCD::YM/2)*pixsize;
float vx=0;
float vy=0;
float prevTime = 0;

float elapsed()
{
if( prevTime == 0 ) prevTime = float(1e-3*millis());
float time = float(1e-3*millis());
float delta = time - prevTime;
prevTime = time;
return delta;
}

inline float sign(float f)
{
return f > 0 ? 1 : (f < 0 ? -1 : 0);
}


float ax = 0;
float ay = 0;
float az = 0;

float azx = 0;
float azy = 0;
float azz = 0;


void zero()
{
ax=ay=az=0;
azx=azy=azz=0;
for( byte n = 0; n < 4; n++ )
{
measure();
}
azx=ax;
azy=ay;
azz=az;
}

void measure()
{
const float filtercoef = 0.25;
ax = filtercoef*(G/64.0)*float(ACM::X())+(1-filtercoef)*ax;
ay = filtercoef*(G/64.0)*float(ACM::Y())+(1-filtercoef)*ay;
az = filtercoef*(G/64.0)*float(ACM::Z())+(1-filtercoef)*az;
}

void loop(void)
{
zero();
while(1)
{
#if 0
// LIS302DL
char ax = ACM::X();
char ay = ACM::Y();
char az = ACM::Z();
char accx = map(ax,-64,64,0,LCD::XM);
LCD::Goto(3,0);LCD::Fill(0,accx-2);LCD::Fill(0xFF,4);LCD::Fill(0,LCD::XM-accx-4);
char accy = map(ay,-64,64,0,LCD::XM);
LCD::Goto(4,0);LCD::Fill(0,accy-2);LCD::Fill(0xFF,4);LCD::Fill(0,LCD::XM-accy-4);
char accz = map(az,-64,64,0,LCD::XM);
LCD::Goto(5,0);LCD::Fill(0,accz-2);LCD::Fill(0xFF,4);LCD::Fill(0,LCD::XM-accz-4);
#endif

float dt = elapsed();

#if 0
const float refrat = -1;
if( vx == 0 ) vx = 10*pixsize/1; // 10 pixels per second
if( vy == 0 ) vy = 10*pixsize/1; // 10 pixels per second
x += vx * dt;
y += vy * dt;
if( x < 0 ) { x = 0; vx = refrat * vx; }
if( x >= xsize-ballsize ) { x = xsize-ballsize; vx = refrat * vx; }
if( y < 0 ) { y = 0; vy = refrat * vy; }
if( y >= ysize-ballsize ) { y = ysize-ballsize; vy = refrat * vy; }
#else
measure();
// apply gravity the display's y axis is a z axis of acceleromiter due to mounting direction
vx += (ax-azx)*dt;
vy += (az-azz)*dt;
// apply center pull this emulates a hiperbol shape bowl-like dish in which the ball rolls
const float fudge = 0.2;
vx += -fudge*G*(x-(xsize/2-ballsize/2))/xsize*dt;
vy += -fudge*G*(y-(ysize/2-ballsize/2))/ysize*dt;
// vx += -0.5*(x>xsize/2?1:-1)*dt;
// vy += -0.5*(y>ysize/2?1:-1)*dt;
// apply friction
vx += -0.1*vx;
vy += -0.1*vy;
// move with speed
x += vx * dt;
y += vy * dt;
// bounce off walls
const float refrat = 0.9; // bounce reflection ratio (== 1 for perfect bounce)
if( x < 0 ) { x = 0; vx = refrat * vx; }
if( x >= xsize-ballsize ) { x = xsize-ballsize; vx = refrat * vx; }
if( y < 0 ) { y = 0; vy = refrat * vy; }
if( y >= ysize-ballsize ) { y = ysize-ballsize; vy = refrat * vy; }
// zero speed if we are trully not moving
if( abs(px - x) < 1e-10 ) vx = 0;
if( abs(py - y) < 1e-10 ) vy = 0;
px = x;
py = y;
#endif

byte bx = (byte)(x/pixsize);
byte by = (byte)(y/pixsize);
MoveBall(bx,by);
#if 1
Serial.println(dt);
Serial.println(ax);
Serial.println(az);
Serial.println(vx);
Serial.println(vy);
Serial.println(G*(x-(xsize/2-ballsize/2))/xsize*dt);
Serial.println(G*(y-(ysize/2-ballsize/2))/ysize*dt);
Serial.println();
#endif
// LCD::Goto(0,0);LCD::Num<10>(bx);
// LCD::Goto(0,LCD::XM/2);LCD::Num<10>(by);
delay(20);
}
}

Sunday, August 2, 2009

Arduino IR Receiver with Interrupts



Infra Red Receiver with the use of Pin Change Interrupt

Infrared remote control receiver implemented using pin change interrupt. This implementation allows the main loop to perform other tasks while the receiver code collects incoming IR message bits in the background. This method was used to receive IR control message send to the iSOBOT robot from its remote. It is runnin on Arduino Mini Pro with AVR m168 CPU.

Problem Definition

The IR remote receiver is observing output of the IR sensor/demodulator which signals presents or absents of modulated IR. The output of the sensor is connected to a pin of the CPU which is then read by the software and interpreted by as bits of the message. To recognize the beginning of the message and to interpret the waveform generated by, the code has to measure time between signal transition. Beginning of the message is signaled as 2.5ms burst of IR while 0 and 1 are coded as length of silence between 0.5ms IR bursts (0- 0.5 ms silence while 1 is 1ms silence). See IR waveforms and protocol description.

Classic Approach

The code below is uses a polling approach. When it waits for the signal change it simply reads (polls) the signal until a change is observed. While this is straight forward and very simply, it occupies the CPU in 100%. Here is an implementation of example:



// helper functions

unsigned long elapsedSince(unsigned long since, unsigned long now)
{
return since &lh; now ? now-since : 0xFFFFFFFFUL - (now - since);
}

unsigned long elapsedSince(unsigned long since)
{
return elapsedSince( since, micros() );
}

// iSOBOT IR protocol timing
#define TimeStart 2500
#define TimeZero 500
#define TimeOne 1000
#define TimeOff 500

// check if the time was in range +/- 25%
#define IS_X(t,x) ((t > 3*(x)/4) && (t < 5*(x)/4))
#define IS_0(t) IS_X(t,TimeZero)
#define IS_1(t) IS_X(t,TimeOne)
#define IS_S(t) IS_X(t,TimeStart)
#define IS_O(t) IS_X(t,TimeOff)

//////////////////////////////////////////////////////////////////////
// polling/blocking version of IR receiver
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// waits for IR carrier transitions (on->off & off->on)
// returns duration of the specified state (HIGH->IR transmitting; LOW->IR off)
// returns 0 if timeout
unsigned irRecvSignal(byte waitFor)
{
while(digitalRead(PIN_IR)==(waitFor==LOW?HIGH:LOW)) {};
unsigned long start = micros();
while(digitalRead(PIN_IR)==waitFor) {};
return elapsedSince(start);
}

////////////////////////////////////////////////////////////////////////
// receives all 22 bits of the messagge
long irRecv()
{
Serial.println("Waiting...");
unsigned time = irRecvSignal(LOW);
if( !IS_S(time) )
{
Serial.println("False Start");
Serial.println(time);
return 0;
}
long bits = 0;
byte len = 22;
for(int i = 0; i < len; i++ )
{
bits <<= 1;
time = irRecvSignal(HIGH);
if( IS_1(time) )
{
bits |= 1;
}
else if( IS_0(time) )
{
bits |= 0;
}
else
{
Serial.println("Bad Bit");
Serial.println(time);
return 0;
}
// check type of message (when recved) to guess the message length (either 22 ot 30 bits)
if( bit == 3 )
{
byte msgtype = bits & 0x3;
if( msgtype == 0 ) len = 30;
}
}
return bits;
}


The jest of the polling happens in the loops in irRecvSignal(byte waitFor) function.
The time duration is measured with Arduino's time function micros() returning number of microseconds since the CPU was started. Note that the number of microseconds will wrap around, which I am compensating for in elapsedSince() function.

When I want to receive a message I just call irRecv().


void loop()
{
...
long msg = irRecv();
...
}


This call will block until a message is received. Hence, nothing else in the main loop will be executed while I wait for IR message (ie wait for the irRecv() to return).

Interrupts

So, I have one CPU here but I'd like to perform more then one task at a time. One way is to "distract" or "interrupt" the main code and "switch" to something else for a moment is to you interrupts. Since the IR receiver cares only about changes in the IR signal and these changes are relatively infrequent, the cost of interrupting the main task will be small. At the worst case the IR signal changes every 500us which on 16MHz CPU AVR allows for about 8000 instructions to be executed in between. Granted, there is some overhead cost of interrupting (multitasking is hard for humans as well) but it should be rather small esp. since in this case the IR message are also sent infrequently.

Solution

Ok, so here is the code I came up with. It is more complicated than the polling version. Since the interrupt code is/should be active only for brief moments, the state of receiving a message must be preserved between its actions. I used a state machine to implement this functionality (by "state machine" I mean that one single function is invoke on IR signal change but this function "switches" into different state depending on its current state and elapsed time since last invocation).



// pin state change interrupt based IR receiver
// observing the IR pin changes, measure time between interrupts
// use state machine to decide what to do

enum
{
ISR_IDLE, // nothing is/was happening (quiet)
ISR_START, // start of sequence, was waiting for a header signal
ISR_BIT_ON, // transsmitting a bit (IR carrier turned on)
ISR_BIT_OFF // in an OFF bit slot (IR carrier turned off)
}
isrState = ISR_IDLE;

unsigned long isrLastTimeStamp;
unsigned long isrRcvCmd;
unsigned long isrNewCmd;
byte isrBitLen = 22;
byte isrBitCnt;


ISR( PCINT2_vect )
{
// PIN_IR == #2 --> PD2;
// receiving a modulated IR signal makes the pin go low (active low)
byte transmitting = (PIND & (1<<2)) == 0;

// compute elapsed time since last change
unsigned elapsed;
{
unsigned long timeStamp = micros();
elapsed = elapsedSince(isrLastTimeStamp,timeStamp);
isrLastTimeStamp = timeStamp;
}
switch( isrState )
{
case ISR_IDLE :
if( transmitting ) isrState = ISR_START;
break;
case ISR_START:
isrBitCnt = 0;
isrNewCmd = 0;
isrBitLen = 22;
if( !transmitting && IS_S(elapsed) )
isrState = ISR_BIT_ON; // bits are now rolling
else
isrState = ISR_IDLE; // wrong timing of start or pin state
break;
case ISR_BIT_ON:
if( transmitting )
{
isrState = ISR_BIT_OFF;
isrNewCmd <<= 1;
isrBitCnt ++;
if( IS_1(elapsed) )
{
isrNewCmd |= 1;
}
else if( IS_0(elapsed) )
{
// isrNewCmd |= 0;
}
else
isrState = ISR_START; // bad timing, start over
if( isrBitCnt == 7 ) // we have received 6 bit header (now expecting 7th bit)
{
isrBitLen = (isrNewCmd & (3<<3)) == 0 ? 30 : 22; // 2 vs 3 byte commands
}
}
else isrState = ISR_IDLE; // bad state (should never get here...)
break;
case ISR_BIT_OFF:
if( !transmitting && IS_O(elapsed) )
{
if( isrBitCnt == isrBitLen ) // is this the end?
{
isrState = ISR_IDLE;
isrRcvCmd = isrNewCmd;
}
else
isrState = ISR_BIT_ON; // keep bits rolling
}
else
if( IS_S(elapsed) )
isrState = ISR_START;
else
isrState = ISR_IDLE;
break;
}
}


To link a function to an interrupt I used
ISR( PCINT2_vect )
(assigns it to the INT2 interrupt vector which is linked to the pin I used for IR signal).

And the rest of it

Here is a function returning a new message:


long irRecv()
{
byte oldSREG = SREG;
cli();
long cmd = isrRcvCmd;
isrRcvCmd = 0;
SREG = oldSREG;
return cmd;
}


This is a non-blocking function. If there was nothing, it returns 0 (there is no message that would result in a 0). When a message code is read, a 0 is stuffed back into isrRcvCmd to clear it so next time I read it I do not get a repeat. I also turn off interrupts for the duration of the "read and zero" of isrRcvCmd so I do not conflict with the interrupt code "interrupting" my read. Note that:
long cmd = isrRcvCmd;
is not "atomic" ie it consists of several CPU instructions and hence it may be interrupted in the middle and ended up returning an inconsistent value.

So now I can really do more stuff in main loop:


void loop
{
unsigned long rcv = irRecv();
if(rcv)
{
// got a message
Serial.print("recv ");
Serial.println(rcv,HEX);
}
Serial.print('.');
... do whatever you like
}


Configuring Interrupt
I have connected the IR sensor to pin D2 (#2 in Arduino convention). The pin change interrupts are not enabled by default so I needed to configure it in setup:


void setup()
{
...
PCICR |= 4; // enable PCIE2 which services PCINT18
PCMSK2 |= 4; // enable PCINT18 --> Pin Change Interrupt of PD2
...
}