Receiving X10 Commands with the PSC05/TW523

Overview:

Since the "dark ages" the Arduino IDE has included the X10 library and examples. Although the documentation says that "This library enables you to send and receive X10 commands from an Arduino module." in reality, the library only allows you to send commands. This article will give you the ability to receive commands. Together, you can now do some pretty interesting stuff like make an X10 "controller" with "macros" or log X10 signals. (Or in my case, count how many times the cat door opens!)

[3/29/2010 Here is a beta library with the combined Send/Receive X10 code: https://docs.google.com/leaf?id=0B5Sg6E9g_zOXMzQxZmVkYjktNjQwZi00MjgxLTk4YzQtNGIwYzI0ZjA0Njg3&hl=en_US]

Perhaps someone will want to fold this into the X10 library at some point ([3/30/2010 BH] - Someone did! See link above.) Alternatively, I've tested this receive code with the X10 library's send code and they coexist well. (Note that the interrupt used here should be temporarily detached when sending.)

The receive capability requires the PSC05/TW523 X10 module. (The PL513/TW513 can only send.) Also note that PSC05/TW523 uses a slightly different connection from the one shown in the X10 library documentation.

For those of you in 220V and/or 50Hz land, during my search I found low cost ways to modify the much much cheaper 115V modules to your voltage and frequency. You will find instructions in the best X10 source I've come across -http://www.idobartana.com/hakb/.

Below is a description of how it works and the source code for a simple example. Please feel free to use it more cleverly and to improve it, . . . and to share your improvements.

Background:

You've asked "What time is it?" and I'm going to tell you how to build a clock! So if you don't care how it works, you can skip this part and grab the working example below. I'm writing this because there's not much on how to receive from the PSC05/TW523, and nothing for the AVR. There is very good info on what the PSC05/TW523 sees on the power line, but relating that to what the PSC05/TW523 outputs, involves some challenges. Besides, it's nice to have it all in one place.

So I'll start by describing what takes place on the power line when X10 commands are sent, and then describe how the outputs of the PSC05/TW523 relate to that. (The best source on the power line signals is http://www.hometoys.com/htinews/feb99/articles/kingery/kingery13.htm ) This will help you understand what the Check_Rcvr() function does.

Suppose an A1-ON button is pressed. In the X10 world, this actually transmits two commands – (#1) The house code 'A' and the unit code '1' and (#2) The house code 'A' (again) and the command code "ON". A "Start Code" (SC) is also added before each frame. In addition, the data for each command is sent twice, in two identical "frames". So what you have on the line is:

SC…HOUSE-A…UNIT-1 / SC…HOUSE-A…UNIT-1 | SC…HOUSE-A…ON / SC…HOUSE-A…ON

                            Command 1 | Command 2
          Frame 1 / Frame 2           |      Frame 1  / Frame 2

The bits in frame are transmitted by sending a 1 ms burst of 120KHz at the "zero crossings" of the AC waveform. (These bursts actually start about 100us after the zero crossing.)

OK, one last level of complexity and your done with this part! After the Start Code (SC) – which is 4 bits (1110), each of the rest of the bits is followed by a parity bit. (So a bit '1' will send '1 0' and a bit '0' will send '0 1'.)

The Start Code is 4 bits with no parity. The House Code is 4 bits with parity – so 8 bits. The Unit Code and the Command Code is 5 bits with parity – so 10 bits. So a "frame" will consist of 22 bits but you will only care about 13 of them.

(I lied a little. There's really one more level of complexity but it doesn’t concern us - all the data bits described above is repeated 3 times / 1/2 cycle for power lines that use 3 Phase AC. The PSC05/TW523 isolates us from this.)

With the above background in place, let's talk about what you get out of the PSC05/TW523. (About the only reference on this is a few paragraphs and a diagram from X10 - http://www.x10pro.com/pro/pdf/technote.pdf )

For receiving were concerned about 2 outputs: (#1) RJ11 pin 1 (BLK) -> Zero Crossing (#2) RJ11 pin 3 (GRN) -> Receive

(Pin 2 is ground and must be connected to Arduino's ground. Pin 4 is Transmit and this can be used by the X10.lib.)

Both the Zero Crossing and Receive outputs are "low active" and should use the internal pullup resistors. The Zero Crossing will go LOW for each zero crossing and must be connected to an interrupt pin (2 or 3) and the interrupts should be triggered on CHANGE. The Receive output will go LOW for each '1' bit and can be connected to any pin.

Remember that the data for each frame is sent twice on the power line? Well the PSC05/TW523 only sends the data from the second set. It does send the parity bits, however, and it sends all bits MSB first. (This is the Rosetta Stone.)

So putting this all together and using the actual bits for the house code and unit code/command, A1-ON looks like:

1 1 1 0 01 10 10 01 01 10 10 01 01 | 1 1 1 0 01 10 10 01 01 01 10 01 10

SC=1110/House=0110 / Unit=01100 | SC=1110/House=0110 / Cmnd=00101

I have shown the parity bits right next to the actual bit, but in reality each bit (including the parity) will be sent at the zero crossing – which is when the ISR fires. Remember that a '1' bit is a LOW on the Receive pin.

The Check_Rcvr() function does not record the parity bits, so it counts the zero crossings and after the first 4 bits of the start code, it only records every odd bit. The rest of the function is easy to understand. One last note is that a command can be distinguished from a unit code because commands end in a '1' bit and units end in a '0' bit.

Example Code:

The comments in the example code below should provide additional help in understanding the X10 receive process. It also includes transmitting using the X10.lib to show how they can coexist.

  1. /* Arduino Interface to the PSC05 X10 Receiver.                       BroHogan 3/24/09
  2.  * SETUP: X10 PSC05/TW523 RJ11 to Arduino (timing for 60Hz)
  3.  * - RJ11 pin 1 (BLK) -> Pin 2 (Interrupt 0) = Zero Crossing
  4.  * - RJ11 pin 2 (RED) -> GND
  5.  * - RJ11 pin 3 (GRN) -> Pin 4 = Arduino receive
  6.  * - RJ11 pin 4 (YEL) -> Pin 5 = Arduino transmit (via X10 Lib)
  7.  * NOTES:
  8.  * - Must detach interrup when transmitting with X10 Lib
  9.  */
  10.  
  11. #include "WProgram.h"                  // this is needed to compile with Rel. 0013
  12. #include <x10.h>                       // X10 lib is used for transmitting X10
  13. #include <x10constants.h>              // X10 Lib constants
  14. #define RPT_SEND 2                     // how many times transmit repeats if noisy set higher
  15.  
  16. #include "PSC05.h"                     // constants for PSC05 X10 Receiver
  17. #define TRANS_PIN      5               // YEL pin 4 of PSC05
  18. #define RCVE_PIN       4               // GRN pin 3 of PSC05
  19. #define ZCROSS_PIN     2               // BLK pin 1 of PSC05
  20. #define LED_PIN        13              // for testing
  21.  
  22. volatile unsigned long mask;           // MSB first - bit 12 - bit 0
  23. volatile unsigned int X10BitCnt = 0;   // counts bit sequence in frame
  24. volatile unsigned int ZCrossCnt = 0;   // counts Z crossings in frame
  25. volatile unsigned long rcveBuff;       // holds the 13 bits received in a frame
  26. volatile boolean X10rcvd = false;      // true if a new frame has been received
  27. boolean newX10 = false;                // both the unit frame and the command frame received
  28. byte houseCode, unitCode, cmndCode;    // current house, unit, and command code
  29. byte startCode;                        // only needed for testing - sb B1110 (14)
  30.  
  31. x10 SendX10= x10(ZCROSS_PIN,TRANS_PIN);// set up a x10 library instance:
  32.  
  33. void setup() {
  34.   attachInterrupt(0,Check_Rcvr,CHANGE);// (pin 2) trigger zero cross
  35.   Serial.begin(9600);
  36.   pinMode(LED_PIN,OUTPUT);             // onboard LED
  37.   pinMode(RCVE_PIN,INPUT);             // receive X10 commands - low = 1
  38.   pinMode(ZCROSS_PIN,INPUT);           // zero crossing - 60 Hz square wave
  39.   digitalWrite(RCVE_PIN, HIGH);        // set 20K pullup (low active signal)
  40.   digitalWrite(ZCROSS_PIN, HIGH);      // set 20K pullup (low active signal)
  41. }
  42.  
  43. void loop(){
  44.  
  45.   if (newX10){                         // received a new command
  46.     X10_Debug();                       // print out the received command
  47.     newX10 = false;
  48.  
  49.     if (unitCode == 1){
  50.       detachInterrupt(0);                  // must detach interrupt before sending
  51.       SendX10.write(D,UNIT_5,RPT_SEND);              
  52.       if(cmndCode == ON) SendX10.write(D,ON,RPT_SEND);  
  53.       if(cmndCode == OFF) SendX10.write(D,OFF,RPT_SEND);  
  54.       attachInterrupt(0,Check_Rcvr,CHANGE);// re-attach interrupt
  55.     }
  56.   }
  57. }
  58.  
  59. void Check_Rcvr(){    // ISR - called when zero crossing (on CHANGE)
  60.   if (X10BitCnt == 0) {                // looking for new frame
  61.     delayMicroseconds(OFFSET_DELAY);   // wait for bit
  62.     if(digitalRead(RCVE_PIN)) return;  // still high - no start bit - get out
  63.     digitalWrite(LED_PIN, HIGH);       // indicate you got something
  64.     rcveBuff = 0;
  65.     mask = 0x1000;                     // bitmask with bit 12 set
  66.     rcveBuff = rcveBuff | mask;        // sets bit 12 (highest)
  67.     mask = mask >> 1;                  // move bit down in bit mask
  68.     X10BitCnt = 1;                     // inc the bit count
  69.     ZCrossCnt = 1;                     // need to count zero crossings too
  70.     return;
  71.   }
  72.   // Begins here if NOT the first bit . . .
  73.   ZCrossCnt++;                         // inc the zero crossing count
  74.   // after SC (first 4 bits) ignore the pariety bits - so only read odd crossings
  75.   if (X10BitCnt < 5 || (ZCrossCnt & 0x01)){ // if it's an odd # zero crossing
  76.     delayMicroseconds(OFFSET_DELAY);   // wait for bit
  77.     if(!digitalRead(RCVE_PIN)) rcveBuff = rcveBuff | mask;  // got a 1 set the bit, else skip and leave it 0
  78.     mask = mask >> 1;                  // move bit down in bit mask
  79.     X10BitCnt++;
  80.  
  81.     if(X10BitCnt == 13){               // done with frame after 13 bits
  82.       for (byte i=0;i<5;i++)delayMicroseconds(HALF_CYCLE_DELAY); // need this
  83.       X10rcvd = true;                  // a new frame has been received
  84.       digitalWrite(LED_PIN, LOW);
  85.       X10BitCnt = 0;
  86.       Parse_Frame();                   // parse out the house & unit code and command
  87.     }
  88.   }
  89. }
  90.  
  91. void Parse_Frame() {   // parses the receive buffer to get House, Unit, and Cmnd
  92.   if(rcveBuff & 0x1){                  // last bit set so it's a command
  93.     cmndCode = rcveBuff & 0x1F;        // mask 5 bits 0 - 4 to get the command
  94.     newX10 = true;                     // now have complete pair of frames
  95.   }
  96.   else {                               // last bit not set so it's a unit
  97.     unitCode = rcveBuff & 0x1F;        // mask 5 bits 0 - 4 to get the unit
  98.     newX10 = false;                    // now wait for the command
  99.     for (byte i=0; i<16; i++){         // use lookup table to get the actual unit #
  100.       if (Unit[i] == unitCode){
  101.         unitCode = i+1;                // this gives Unit 1-16
  102.         break;                         // stop search when found!
  103.       }
  104.     }
  105.   }
  106.   rcveBuff = rcveBuff >> 5;            // shift the house code down to LSB
  107.   houseCode = rcveBuff & 0x0F;         // mask the last 4 bits to get the house code
  108.   for (byte i=0; i<16; i++){           // use lookup table to get the actual command #
  109.     if (House[i] == houseCode){
  110.       houseCode = i+65;                // this gives House 'A' - 'P'
  111.       break;                           // stop search when found!
  112.     }
  113.   }
  114.   rcveBuff = rcveBuff >> 4;            // shift the start code down to LSB
  115.   startCode = rcveBuff & 0x0F;         // mask the last 4 bits to get the start code
  116.   X10rcvd = false;                     // reset status
  117. }
  118.  
  119. void X10_Debug(){
  120.   Serial.print("SC-");
  121.   Serial.print(startCode,BIN);
  122.   Serial.print(" HOUSE-");
  123.   Serial.print(houseCode);
  124.   Serial.print(" UNIT-");
  125.   Serial.print(unitCode,DEC);
  126.   Serial.print(" CMND");
  127.   Serial.print(cmndCode,DEC);
  128.   if(cmndCode == ON)Serial.print(" (ON)");
  129.   if(cmndCode == OFF)Serial.print(" (OFF)");
  130.   Serial.println("");
  131. }

Now make another tab in the IDE named PSC05.h and add the following . . .

  1. // Defines and constants for PSC05 receiving
  2.  
  3. #define OFFSET_DELAY     500    // uS from zero cross to center of bit (sugg 500-700 us)
  4. #define HALF_CYCLE_DELAY 8334   // Calculated 8334 uS between bit repeats in a half-cycle
  5.  
  6. #ifndef ON                      // use same defines from x10constants.h for rcvd cmnds
  7. #define ON   B00101             // these are examples
  8. #endif
  9. #ifndef OFF
  10. #define OFF  B00111
  11. #endif
  12.  
  13. byte House[16] = {              // Lookup table for House Code
  14.   B0110,  // A
  15.   B1110,  // B
  16.   B0010,  // C
  17.   B1010,  // D
  18.   B0001,  // E
  19.   B1001,  // F
  20.   B0101,  // G
  21.   B1101,  // H
  22.   B0111,  // I
  23.   B1111,  // J
  24.   B0011,  // K
  25.   B1011,  // L
  26.   B0000,  // M
  27.   B1000,  // N
  28.   B0100,  // O
  29.   B1100,  // P
  30. };
  31.  
  32. byte Unit[16] = {               // Lookup table for Unit Code
  33.   B01100,  // 1
  34.   B11100,  // 2
  35.   B00100,  // 3
  36.   B10100,  // 4
  37.   B00010,  // 5
  38.   B10010,  // 6
  39.   B01010,  // 7
  40.   B11010,  // 8
  41.   B01110,  // 9
  42.   B11110,  // 10
  43.   B00110,  // 11
  44.   B10110,  // 12
  45.   B00000,  // 13
  46.   B10000,  // 14
  47.   B01000,  // 15
  48.   B11000,  // 16
  49. };
  50.  

Notes & Limitations:

  • Because a new transmission can cause an interrupt at any time you should process the command as quickly as possible. Adding an input queue or the like might be a good idea.
  • According to the datasheet, the PSC05/TW523 should receive the same command it has just sent. However, this doesn't happen in the example.
  • The PSC05/TW523 itself does not support all of the "Extended Commands" (see it's datasheet). However, having control over what is sent and what is received should give some options. For example commands for a certain house code could be interpreted as 2 4 bit values.
  • I've received some very good feedback from someone who is using this in the 50Hz world. To convert from 60Hz to 50Hz:
  1. define OFFSET_DELAY from 500 to 800
  2. define HALF_CYCLE_DELAY from 8334 to 10000
  • A corresponding change can be made to the X10.lib in X10constants.h:
  1. define BIT_DELAY from 1778 to 2133
  2. define BIT_LENGTH from 800 to 900
  • I have a blog that describes Arduino X10 projects I've made using this, as well as my interface to the "Fire Cracker" (CM17A). It's at http://brohogan.blogspot.com/

Boy this is long! That's all I know. BroHogan

Share