From Hamsterworks Wiki!
This FPGA Project was completed in March 2012.
I 'gifted' myself a cheap Infrared toy helicopter for Christmas, with the intention of hacking the protocol. I purchased a Syma S107 based on the details of the protocol published in this post http://www.rcgroups.com/forums/showthread.php?t=1417249&page=3, but have only just finished playing with it enough that I wanted to start hacking with it.
I've successfully built an FPGA interface for it. A video of it configured on a Digilent Basys 2 development board http://youtu.be/89HvwYWg838
Capturing the commands
For most Infrared projects codes can be captured using a IR receiver module (see http://www.ladyada.net/learn/sensors/ir.html) for a really good write-up.
I tried this, but the only IR sensor I had didn't give any useful signal - it would pick up my TV remote, but not the helicopter control - either the pulses were too short or too long for the receiver or the IR signal's frequency was outside of the receiver's bandpass filter.
So I opened the transmitter and inspected it. The output is a chain of three LR LEDs, a resister and an NPN surface mount transistor driven by an unlabelled 14 pin DIP IC. The negative/ground trace on the PCB is to the centre pin on the left (pin 11) and the transistor is driven by the top right (pin 8).
After a quick check of voltage levels I hooked up my cheap and cheerful Dangerous Prototypes Logic Analyzer (http://dangerousprototypes.com/category/logic-analyzer/), and captured a few frames. Here's the setup:
Captured and decoded frames
S = Start symbol, E = End Symbol, s = short pulse S 001110110 0111111 00011010 00110001 E S 001110110 s10 0111111 00111111 00110001 E S 001111110 0111111 00000000 00110001 E S 001110110 0111111 00011010 00110001 E S 001110110 s10 0111111 00111111 00110001 E S 001111110 0111111 00000000 00110001 E
Unlike the post on RC Groups, this transmitter has two different frames - one in three has three extra bits in it (well, one short bit and two data bits). I have no idea what they do, but suspect they may be for a different model with more features.
Close-up of a pulse
As you can see in the close-up of a pulse, the 38.5KHz carrier and the stream of symbols being transmitted is not synchronised - giving 'short' bits at the front and end of a pulse, making any observed timings open to error unless both the first and last pulse are 'clipped'.
By sampling the signal at 10MHz I was able to get the following specifications for the signals.
|Carrier High||12.9us||645 counts @ 50MHz|
|Carrier Length||25.8us||1290 counts @ 50Mhz|
|Frame TX interval||118,764us|
|Start symbol High||1965us|
|Start symbol Total||3935us|
|One symbol high||305.5us|
|One symbol length||993.3us||6 cycles took 5.959ms|
|Zero symbol High||296.1us||Average of four|
|Zero symbol length||593.9us||3 cycles took 1.785ms|
These differ a bit from that in the RC Groups forum post, but make a lot more sense with the symbol for '1' being 1.5x that of an '0' . I guess that their number were obtained by using an IR Receiver method. Mine are better :-)
Implementing in VHDL
This was quite easy - but took a few iterations to get something that I was happy with. I converted everything to timinig based on integer multiples of twice the carrier frequency, and ran everything on a 50MHz clock (the onboard clock of a Basys2).
-- FPGA Interface for Syma S107 helicopter -- -- Author: Mike Field <firstname.lastname@example.org> -- -- Based on capture at 10MHz -- ========================= -- -- CarrierHigh 12.9us 645 @ 50MHz -- CarrierLength 25.8us 1290 @ 50Mhz -- -- (in 12.9us half-cycles) -- Frame Length 118.764ms 9206.5 -- -- -- Start bit High 1965us 152 -- Start bit Total 3935us 304 -- -- One bit high 305.5us 23 -- One Bit length 993.3us 77 6 cycles is 5.959ms -- -- Zero bit High 296.1us 23 Average of four -- Zero bit length 593.9us 46 3 cycles is 1.785ms -- -- --Left-- --Fwd-- -Throt- -Trim- -- S 010011100 000 0000001 01111110 01001010 E High Fwd -- -- Sample Frames -- ============== -- S 001110110 0111111 00011010 00110001 E -- S 001110110 S10 0111111 00111111 00110001 E -- S 001111110 0111111 00000000 00110001 E -- S 001110110 0111111 00011010 00110001 E -- S 001110110 S10 0111111 00111111 00110001 E -- S 001111110 0111111 00000000 00110001 E -- library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity helicopter is Port ( clk_50m : in STD_LOGIC; led_out : out STD_LOGIC; channel : in STD_LOGIC; yaw : in STD_LOGIC_VECTOR(6 downto 0) := (others => '0'); pitch : in STD_LOGIC_VECTOR(6 downto 0); throttle : in STD_LOGIC_VECTOR(6 downto 0); adjust : in STD_LOGIC_VECTOR(6 downto 0) ); end helicopter; architecture Behavioral of helicopter is -- The frame is assembled into this signal signal frame : STD_LOGIC_VECTOR(31 downto 0); -- The current frame being sent - acts as a shift register, loaded at start of frame signal frame_to_send : STD_LOGIC_VECTOR(31 downto 0) := (others => '0'); -- Counters for the various signal times signal carierCounter : STD_LOGIC_VECTOR(10 downto 0) := (others => '0'); signal frameCounter : STD_LOGIC_VECTOR(13 downto 0) := "10001000000000"; signal perBitCounter : STD_LOGIC_VECTOR( 6 downto 0) := (others => '0'); signal bitCounter : STD_LOGIC_VECTOR( 5 downto 0) := "111111"; -- current state of the carrier signal carrier : std_logic := '0'; -- Carrier frequency is 50,000,000 / 1314 = 38kHz, 50% duty cycle constant pulseLengthCarrier : natural := 645; -- Length of the zero and one tokens are different! constant pulseLengthZero : STD_LOGIC_VECTOR( 6 downto 0) := "0101110"; -- 46 constant pulseLengthOne : STD_LOGIC_VECTOR( 6 downto 0) := "1001101"; -- 77 constant pulseLengthHigh : STD_LOGIC_VECTOR( 6 downto 0) := "0010111"; -- 23 -- Length of the 'on' time for the Zero and One tokens. -- Details of when the frame is resent, and what the start token looks like constant frameLength : natural := 9206; constant frameStartHigh : natural := 152; constant frameStartLength : natural := 304; -- helper signals signal pulseLength : STD_LOGIC_VECTOR( 6 downto 0); begin frame <= '0' & yaw & '0' & pitch & channel & throttle & '0' & adjust; pulseLength <= pulseLengthZero when frame_to_send(31) = '0' else pulseLengthOne; process(clk_50m) begin if rising_edge(clk_50m) then led_out <= '0'; if bitCounter < 32 and perBitCounter < pulseLengthHigh then led_out <= carrier; end if; -- The start pulse (but we look at it as the end of the frame) if frameCounter >= frameLength-frameStartLength and frameCounter < frameLength-frameStartLength+frameStartHigh then led_out <= carrier; end if; -- Is this a rising edge of the carrier? if carierCounter = pulseLengthCarrier-1 then carrier <= not carrier; carierCounter <= (others => '0'); -- Time to send a new bit? if perBitCounter = pulseLength-1 then -- special note - we send the first '1' that gets appended to the frame_to_send (we send 33 bits!) frame_to_send <= frame_to_send(30 downto 0) & '1'; perBitCounter <= (others => '0'); if bitcounter < 33 then bitcounter <= bitcounter + 1; end if; else perBitCounter <= perBitCounter + 1; end if; -- Time to send a new frame? if frameCounter = frameLength-1 then frameCounter <= (others => '0'); bitcounter <= (others => '0'); perbitcounter <= (others => '0'); frame_to_send <= frame; else frameCounter <= frameCounter + 1; end if; else carierCounter <= carierCounter + 1; end if; end if; end process; end Behavioral;
Here's the top-level VHDL module I used for testing. It will be pretty easy to hook push buttons up to the yaw and pitch if you desired... or maybe interface to a UART.
library IEEE; use IEEE.STD_LOGIC_1164.ALL; entity helicopter_top is Port ( clk_50m : in STD_LOGIC; led_out : out STD_LOGIC; switches : in STD_LOGIC_VECTOR(6 downto 0)); end helicopter_top; architecture Behavioral of helicopter_top is COMPONENT helicopter PORT( clk_50m : IN std_logic; channel : IN std_logic; yaw : IN std_logic_vector(6 downto 0); pitch : IN std_logic_vector(6 downto 0); throttle : IN std_logic_vector(6 downto 0); adjust : IN std_logic_vector(6 downto 0); led_out : OUT std_logic ); END COMPONENT; begin Inst_helicopter: helicopter PORT MAP( clk_50m => clk_50m, led_out => led_out, channel => '0', yaw => "0111111", pitch => "0111111", throttle => switches, adjust => "0111111" ); end Behavioral;
The electronics for the IR output
For output I connected an IR LED to an I/O pin on the FPGA, with two 330 Ohm resistors in parallel. The IR LED used is not very bright and has a very directional beam on it, so the heli quickly moves out of reception. If you want to do this for a real project I recommend using multiple bright, wide angle LEDs with a NPN transistor switching them.
At the moment I've only hooked the throttle up to the switches. I should also hook the push-buttons to the left/right front/back. Then I could actually fly the thing!
For maximum reliability I would also like to play with the timings. This should be using a 38kHz carrier, but isn't. Does the helicopter have a very wide filter on its IR sensor so a precision clock isn't needed in the transmitter (keeping price down? Is my transmitter 2% or 3% our of spec? By playing with the constants could reception and range be improved?
What do the extra two bits do? They don't seem to be needed? Maybe they are for other models to control lights, guns or cameras?