Home | Job | Pinball | Photo Album | Automotive | Press/Awards | Contact |
NINJA CR14A Wireless Remote
Control
by
Dennis Hawkins
(Email:n4mwdATamsat.org)
This document attempts to explain how the X-10 Ninja CR14A remote control works. As built, the remote control only operates within 20 feet line of sight. It is hoped that by exposing the inner workings of this unit, someone will figure out a way to extend the range to something more useful. It should be noted that X-10 did not supply any information used in this document. Everything was obtained by reverse engineering the hardware and also with help from other people who have done the same.
The NINJA wireless remote control consists of a keypad, a 16C54 PIC microprocessor, a small transmitter circuit and other associated components. The schematic in PDF format is here. I will be referring to it throughout the remainder of this document so it would be a good idea to print it out.
Power Supply
The CR14A is powered by 4 alkaline AAA cells giving a total of 6V. The transmitter section of the remote is powered through LED D1 and R2 and also through inductor/antenna L1. The CPU is powered through D2 and D3. Since these two diodes are in series, they serve as the voltage regulator and reduce the supply voltage from 6 volts to 5 volts as required by the CPU.
L1 also serves to shield the power supply from RF as much as possible. Decoupling capacitors C1 and C2 smooth any noise on the power supply to the CPU that may have come from the RF or other sources.
Keypad
The main keypad consists of a matrix of rubber buttons with a conductive rubber section on the bottom. Pressing a button causes contacts that have been printed on the circuit board to short. This combination of the conductive rubber buttons and the circuit board effectively form a pushbutton switch matrix.
The matrix is formed by 3 columns with pushbuttons connecting them to one of 6 rows. The columns are connected to the CPU at pins RA0, RA1, and RA2. The rows are connected to RB0, RB1, RB2, RB3, RB5 and RB6.
When the CPU is scanning the keypad matrix, it sets all but one of the aforementioned RB lines high (logic '1') in a bit chasing pattern. That is, first RB0 goes low, then RB0 goes high and then RB1 goes low, etc. This will sequentially force one of the 6 rows low while the others remain high. If a button is pressed on the row that is currently low, it will also force the corresponding column low as well. Since the CPU knows which row was being held low, it can easily determine which button was pressed by comparing to the column that is also low.
The keypad also serves to wake up the CPU from SLEEP mode. Normal keypad scanning does not occur when the CPU is sleeping. See the CPU section for details.
Sleeping CPU
The CPU is a Microchip PIC 16C54A. This CPU is ideal for use in remote controls. The CPU handles keypad/switch scanning and also generates the proper digital code for the transmitter.
The CPU clock oscillator is a 4MHz ceramic resonator. Because the function of this unit does not require a precise timebase, a ceramic resonator is good enough.
When the CPU is first powered up, it sets the I/O lines on the RA port to high impedance and the I/O lines on the RB port all low. Then it immediately goes into sleep mode. With this CPU, the state of the I/O lines are maintained during sleep mode.
Because all of the RB lines are low, all of the rows to the keypad are also all simultaneously low. When a button is pressed, one of the corresponding columns is also pulled low. Because the CPU is in SLEEP mode, it cannot normally respond to this. However, diodes D4, D5 and D6 together with resistor bridge R6 and R7 act as a passive AND gate.
Because all of the inputs to the passive AND gate are normally pulled high by the pullup resistors R3, R4 and R5, pressing a key pulls one of the AND gate inputs low and causes the CPU reset line MCLR to be pulled low as well. This wakes up the CPU by resetting it. As soon as the CPU responds to the reset condition, it's internal circuitry sets all of its ports to high impedance. This allows the columns to be pulled back high by the pullup resistors R3, R4 and R5 and prevents the passive AND gate from continuously pulling the MCLR line low. Consequently, this allows the CPU to start normally.
As soon as the CPU wakes up from SLEEP mode, it immediately changes RA3 from high impedance to output high. This overrides the passive AND gate and forces the MCLR line high which prevents the CPU from being reset again by a keypress. The RA3 line is held high until the CPU has completed transmitting its code and goes back into sleep mode.
Other Switches
The CR14A also has two other switches. These are the Normal/Program switch and the BCD house code switch. These are scanned in a different way than the keypad.
When the CR14A detects a key that has been pressed on the keypad, it immediately turns on the transmitter by pulling RB7 high and setting RB1, RB3, RB4, RB5 and RB6 to inputs. In the RF transmission protocol, there is a short start pulse that gets sent before any actual data.
During this interval, the CPU reads the inputs RB1, RB3, RB4, RB5 and RB6 and decides the status of these switches. The diodes D7, D8, D9, D10 and D12 prevent interference with the keypad scanning which takes place when RB7 is low and the transmitter is off. In actuality, the CPU doesn't know what code it will be transmitting until after it has already started transmitting.
The BCD house code switch is rather interesting. It is a printed copper foil pattern combined with a circular wiper brush. The foil pattern consists of three concentric circular patterns. The wiper brush selectively connects three of the patterns on the right side with two of the patterns on the outer left side. The three patterns are laid out in such a way that a unique BCD code is output depending on the position of the circular wiper brush.
The above diagram shows the basic layout of the foil pattern used by the house code switch. The house code letter is drawn around the circumference for clarity. The COM section is common to all codes and is connected to RB7 through resistor R11.
The code that is output is not true BCD, but rather, a unique code generated by the contacts. The table below shows the relationship between the house code and the code generated by the contacts:
Switch A B C D Hex Code |
The RF Transmitter
The RF section of the CR14A is rather interesting as well. It transmits at approximately 310 MHz. European version may be using a different frequency. There are three components that are missing in the US version which may be present in the European version.
The RF transmitter is basically just a little UHF oscillator that gets keyed on and off by RB7 on the CPU. The transistor IC2 is a KSP10 NPN transistor. This forms an oscillator circuit together with C6, C7, C8, T1 and L2.
Transformer T1 and inductor L2 form a resonant circuit that keeps the transmitter mostly at 310 MHz. Transformer T1 is a printed foil pattern on the circuit board which is common when working with UHF frequencies.
Inductor L1 is a wound coil that serves two purposes. First, it acts as a small antenna. Second, it acts as a choke which prevents the RF from interfering with the rest of the circuit through the power supply.
The transmitter only draws power when it is actually transmitting. Since it gets power through D1, and D1 is an LED, it will glow when the unit is transmitting.
The missing components appear to be used to achieve better frequency stability. They may also be used in the European version to change the transmit frequency.
Ninja Transmission Protocol
The CR14A remote transmits two different binary protocols. One is the standard X10 Firecracker protocol. It is used to switch the four possible cameras on and off. It is not discussed here. The other protocol is the Ninja protocol. That is what makes the Ninja move.
The Ninja control code consists of 5 nibbles (2 1/2 bytes or 20 bits). The value of these nibbles depends on the button pressed, the position of the house code switch and the normal/program switch selection. They are encoded using the BCD value from the house code switch and the button function code.
The button function code is a value representing the function sent by the remote. The following table lists out the functions and their corresponding codes:
CODE BUTTON FUNCTION |
The buttons P1, P2, P3, P4, Center and Sweep will make a different code depending on the position of the Normal/Program switch. All codes above come from the Normal position unless specified otherwise. |
The house code switch will output a unique 4 bit binary code based on the position of the switch. The binary code does not have a linear relationship with the house code switch position, instead, the following table is used to convert between the two:
ENCRYPTED HOUSE CODE -> 6 7 4 5 8 9 A B E F C D 0 1 2 3 |
To encode the 5 nibbles that are sent by the remote to the Ninja, the following method is used. First, the encrypted house code binary value and the button function value are determined using the above tables. Here, I have numbered the nibbles A, B, C, D and E. Next, these values are calculated according to the following formulas:
AAAA BBBB CCCC DDDD EEEE
Byte AAAABBBB = ((ENCRYPTED HOUSE CODE + 3) * 16) + BUTTON
FUNCTION
CODE
+ 5
Nibble CCCC = 6
Nibble DDDD = BUTTON FUNCTION CODE
Nibble EEEE = ENCRYPTED HOUSE CODE
Nibbles A and B are combined to form byte AB due to their close relationship. Because of the redundant encoding, the receiver can easily determine if a bad code has been received. Once the nibbles have been encoded, they are transmitted in order MSB first.
The bitstream that is actually transmitted to the Ninja consists of a start bit, the 5 nibbles (MSB first) and a stop bit. The manner in which the bits are encoded are shown below:
Start Bit - At least 2.6ms logic LOW followed by 2.6ms logic
HIGH
followed
by 1.6ms logic LOW.
Zero Bit - 0.6ms logic HIGH followed by 0.6 ms logic LOW.
One Bit - 0.6ms logic HIGH followed by 1.6ms logic LOW.
Stop Bit - 0.6ms logic HIGH followed by 1.6ms logic LOW.
After the start bit, the length of the HIGH pulses is not terribly significant. They should not be allowed to approach the length of a start bit, but they don't have to be exactly 0.6ms. These HIGH pulses are clock bits. The LOW level durations are the data bits in this serial datastream. The final stop bit is required to be able to determine the duration of the LOW period of the final data bit.
Sample C Code
The following are examples of routines used to receive/generate the Ninja codes. They were developed using an 8051 protoboard. The wire on the test Ninja that connects the little receiver board to the motor control board was split and the two ends were sent to the protoboard. The protoboard was able to successfully send and receive Ninja commands.
// Ninja function codes
#define NINJA_LEFT 0
#define NINJA_RIGHT 1
#define NINJA_UP 2
#define NINJA_DOWN 3
#define NINJA_P1 4
#define NINJA_PROGRAM_P1 5
#define NINJA_P2 6
#define NINJA_PROGRAM_P2 7
#define NINJA_P3 8
#define NINJA_PROGRAM_P3 9
#define NINJA_P4 10
#define NINJA_PROGRAM_P4 11
#define NINJA_CENTER 12
#define NINJA_PROGRAM_CENTER 13
#define NINJA_SWEEP 14
#define NINJA_PROGRAM_SWEEP 15
// Wait for code and return the code sent.
// Returned code is packed in the returned byte. The MSN contains the
// House code ('A' = 0) and the LSN contains the Function code.
BYTE NinjaReceiveCode(void)
{
BYTE b, n[5];
static BYTE const code HouseTable[16] =
{
0xC0, 0xD0, 0xE0, 0xF0,
0x20, 0x30, 0x00, 0x10,
0x40, 0x50, 0x60, 0x70,
0xA0, 0xB0, 0x80, 0x90
};
TryAgain:
// Wait for the start pulse - 2.6ms zero -> 2.6ms one -> 1.6ms zero
do
{
b = GetNinjaTransition();
} while (b < 0xA6); // min start pulse 2ms high followed by 1.2ms low
// read in nibbles
for (b = 0; b < 5; b++)
{
n[b] = GetNinjaNibble();
if (n[b] == 0xFF) goto TryAgain; // error
}
// Receive the Stop Bit
b = GetNinjaTransition();
if (b > 0x40 || b < 0x20) goto TryAgain; // error on stop bit
// Validate the received nibbles
b = ((n[4] + 3) << 4) + n[3] + 5; // calculate AB nibble Pair
n[1] = (n[0] << 4) | n[1]; // combine n[0] and n[1] to form nibble pair
if (n[1] != b) goto TryAgain; // error if not the same
if (n[2] != 6) goto TryAgain; // nibble should always be 6
// We should have valid nibbles if here
return(HouseTable[n[4]] | n[3]);
}
// Calculate and send 5 nibbles to Ninja
// HouseCode is the House Code number (A = 0).
void NinjaSendCode(char HouseCode, BYTE FunctionCode)
{
static BYTE const code HouseTable[16] =
// A B C D E F G H I J K L M N O P
{ 6, 7, 4, 5, 8, 9, 0xA, 0xB, 0xE, 0xF, 0xC, 0xD, 0, 1, 2, 3 };
// Convert HouseCode number to Encrypted HouseCode number.
HouseCode = HouseTable[HouseCode & 0xF]; // get from table
FunctionCode &= 0xF; // Normalize code
// Send Start Bit
// The start sequence is a 2.6ms space
// followed by a 2.6ms pulse followed by a 1.6ms space.
DATA_OUT = LOW;
Wait(26); // delay 2.6ms
DATA_OUT = HIGH;
Wait(26); // delay 2.6 ms
DATA_OUT = LOW;
Wait(16); // delay 1.6 ms
// calculate first nibble pair and send it
NinjaSendNibble(((HouseCode + 3) << 4) + FunctionCode + 5, TRUE);
NinjaSendNibble(6, FALSE); // Always 6
NinjaSendNibble(FunctionCode, FALSE); // Send function code
NinjaSendNibble(HouseCode, FALSE); // Send Housecode
NinjaSendBit(1); // Send Stop Bit
}
BYTE GetNinjaTransition(void)
{
BYTE highcount, lowcount;
// Look for high pulses
highcount = 0;
while (DATA_IN == HIGH)
{
if (highcount < 15) highcount++;
Wait(2); // wait in 0.2ms increments
}
if (highcount == 0) return(0); // data not ready
// look for low pulses
lowcount = 0;
while (DATA_IN == LOW && lowcount < 10)
{
lowcount++;
Wait(2); // wait in 0.2ms increments
}
// pack and return counts
return((highcount << 4) | lowcount);
}
// Get a nibble. Return nibble or 0xFF on error.
BYTE GetNinjaNibble(void)
{
BYTE bitcount, b, nib;
nib = 0;
// read in bits
for (bitcount = 0; bitcount < 4; bitcount++)
{
b = GetNinjaTransition();
if (b == 0 || b > 0x50) return(0xFF); // error - no clock pulse or clock pulse too fat
// zero pulse width greater than 1ms is considered a ONE bit, else ZERO bit
nib = (nib << 1) | ((bit)((b & 0x0F) > 5));
}
return(nib);
}
// A 1-bit is a 0.6ms pulse followed by a 1.6ms space.
// A 0-bit is a 0.6ms pulse followed by a 0.6ms pace.
void NinjaSendBit(bit b)
{
DATA_OUT = HIGH;
Wait(6); // delay 0.6 ms
DATA_OUT = LOW;
Wait(b ? 16 : 6); // delay 1.6ms if b = 1 or 0.6ms if 0
}
void NinjaSendNibble(BYTE nib, bit IsByte)
{
BYTE bitno;
bitno = 8;
if (!IsByte)
{
bitno = 4; // set up for nibble
nib <<= 4;
}
do
{
NinjaSendBit((bit)(nib & 0x80)); // send MSB first
nib <<= 1; // shift next bit into MSB position
} while(--bitno); // do all bits
}