/* HCS PICDIO Link v1.3 (c) 1998-2001 Creative Control Concepts Change History ============== v1.3 1/15/2001 Fixed major problem with printing & new networking code v1.2 8/21/2000 Added code to latch short input pulses until next query Moved to ring serial buffer so multiple commands to be cached Updated main procedure to handle new serial buffer v1.1 4/25/2000 Added Network RESET command (R) Changed Serial In routine to handle node matching internally Changed Clock Freq. to get 0% error at 9600 baud Switched from array based buffer to direct mapped RAM access Added watchdog and brownout protection Added Serial Error, RS485 In, and Parallel Out LED status lines Change Node Addr Set so a jumper means a 1 not a 0 v1.0 Original Release The HCS PIC-DIO is a cost effective alternative to the existing Comm-Link and AnswerMan Jr Digital IO nodes for the HCS Home Automation System. HCS PICDIO-Link is 100% backward compatible with the existing HCS DIO-Link commands. This includes support for a Centronics printer which can be used with the LPT()= XPRESS command ALWAYS MAKE SURE IF YOU ADD VARIABLES THAT THEY DON'T OVERLAP THE BUFFER AREA! The PIC IO lines are assigned as follows: RA0-2 Address Set (0-7) RA3 Printer Strobe RA4 Printer Busy In RB0-7 Data Bits RC5 Serial Enable RC6 Serial Out RC7 Serial In */ #INCLUDE <16c63.h> #FUSES XT, WDT, PROTECT, PUT, BROWNOUT #USE DELAY (CLOCK=3686400, RESTART_WDT) // Allows for <4MHz clock with 0% err @9600 baud #USE FAST_IO(A) #USE FAST_IO(B) #USE FAST_IO(C) // Setup hardware serial port #USE RS232(baud=9600, xmit=PIN_C6, rcv=PIN_C7, enable=PIN_C5, bits=8, parity=n, restart_wdt) #ZERO_RAM // Saves us any trouble! // Reserve Buffer Space #RESERVE 0x60:0x7F, 0xA0:0xFF #define BUFFER_SIZE 128 // We use a routine to access the RAM buffer directly #define LED_ON 0 #define LED_OFF 1 #define HCS_RXBUFF_START 0 #define HCS_RXBUFF_END 0x7F #define NODE_ADDR_LENGTH 4 #define REAL_DATA_STARTS 5 // Offset for space and node addr = 5 #define START_CHECKSUM 8 // Start saving checksum values after 9 chars // Register assignments #byte porta = 0x05 #byte portb = 0x06 #bit STROBE = 0x05.3 #bit BUSY = 0x05.4 #bit S_ERR = 0x07.0 #bit LAN_IN = 0x07.1 #bit P_OUT = 0x07.2 #Bit RCIF = 0x0C.5 #Bit CREN = 0x18.4 #Bit FERR = 0x18.2 #Bit OERR = 0x18.1 #Byte FSR = 0x04 #Byte INDF = 0x00 #Byte PIR1 = 0x0C #Byte RCREG = 0x1A int check_val, node_total, last_buff_char; char thisnode[4]; boolean s_idle; // When TRUE, we are NOT receiving a packet boolean checksum; // Buffer Counters int hcs_packet_ctr, hcs_ring_ptr, hcs_ring_read, hcs_ring_end; boolean bad_hcs_packet; char checksum_in[2]; // BUFFER ACCESS ROUTINES // Our buffer is 128 characters long allowing for up to 3 lines of text on a 40 col printer // The compiler arrays are a bit of a hassle so we'll use the upper section of Bank 0 and // all of Bank 1 // 0-1F Bank 0 60-7F (32 bytes) // 20-7F Bank 1 A0-FF (96 bytes) // // We can easily handle the bank switching since bits 6 & 7 of the buffer addr correspond // to the proper bank set bits and bit 0-6 will bring us directly to the proper offset in // each bank. byte read_buffer(int addr) { if (addr > 0x1F) { // Bank 1 FSR = addr + 0x80; // We don't subtract 80 to keep the FSR bit 7 set } else { // Bank 0 FSR = addr + 0x60; } return INDF; } void write_buffer(int addr, int b_data) { // Prevent a bogus address value if (addr > 0x7F) { return; } if (addr > 0x1F) { // Bank 1 FSR = addr + 0x80; } else { // Bank 0 FSR = addr + 0x60; } INDF = b_data; } // This routine is used to convert characters for checking the checksum // It simply converts an ASCII hex value into true decimal form // ASCII allows for a quick and dirty conversion routine! byte gethexbyte(char digit) { if(!BIT_TEST(digit,6)) // Its a number if bit 6 is 0 return(digit & 0x0F); // Simple way to convert 0-9 else return((digit & 0x0F) + 9); } // Serial routine - handles buffering of data which is tricky due to memory paging. #INT_RDA void serial_in() { char data_in; int packet_idx; // Lets read the buffer until it is empty in case we have more than one char here while (bit_test(PIR1, 5)) { // We don't use getc since it does a needless bit check to see if data is avail. restart_wdt(); data_in = RCREG; // We use the HW UART here for simplicity sake! // That's it! Lets act on the data... // We are looking for the start of a packet. // Otherwise we stay in this state and ignore the incoming char if (s_idle) { if ((data_in == '!') || (data_in == '#')) { // New packet - lets init the receive routine LAN_IN = LED_ON; // Turn on LAN LED hcs_packet_ctr = 0; check_val = node_total; // Clear checksum buffer using default beginning bad_hcs_packet = FALSE; // Reset this flag on each packet s_idle = FALSE; // Indicate we are actively receiving a packet // Lets get the correct offset for the address/node name if (data_in == '#') { packet_idx = 3; checksum = TRUE; } else { packet_idx = 1; checksum = FALSE; } } // if ((data_in == '!') || (data_in == '#')) } else { // Lets ensure we haven't caught up with unprocessed data if (hcs_ring_ptr == hcs_ring_read) { // We caught up since the ring ptr points to the read ptr. bad_hcs_packet = TRUE; // This gets us back to the right buffer ptr S_ERR = LED_ON; // Indicate the error } else { // Lets process the character // Check for the end of the packet if ((data_in == '\r') || (data_in == '\n')) { // Check the checksum if we are supposed to // If the bit counter is too small, it can't be a valid packet if (hcs_packet_ctr <= (REAL_DATA_STARTS + packet_idx)) { bad_hcs_packet = TRUE; } else { if (checksum) { // Lets check the checksum to ensure a valid packet check_val += (gethexbyte(checksum_in[0]) << 4) | gethexbyte(checksum_in[1]); if (check_val != 0) { // Bad Checksum! bad_hcs_packet = TRUE; S_ERR = LED_ON; } // if (check_val != 0) } // if (checksum) if (!bad_hcs_packet) { // We have a valid packet. Lets update the end ptr // This indicates to the main loop that new data is available // First we store 0x0d which is used as an end of packet flag write_buffer(hcs_ring_ptr, 0x0D); // Store data in ring hcs_ring_end = hcs_ring_ptr; if (hcs_ring_ptr == HCS_RXBUFF_END) { hcs_ring_ptr = HCS_RXBUFF_START; } else { hcs_ring_ptr++; } S_ERR = LED_OFF; // Turn off any HCS serial error } // if (!bad_hcs_packet) } // if (hcs_packet_ctr < (REAL_DATA_STARTS + packet_idx)) s_idle = TRUE; LAN_IN = LED_OFF; } else { // We have a regular char - lets process it. // Lets store the checksum bytes or validate the node address if necessary if (hcs_packet_ctr < 2 && checksum) { checksum_in[hcs_packet_ctr] = data_in; } else { if ((hcs_packet_ctr >= packet_idx) && (hcs_packet_ctr < (packet_idx + NODE_ADDR_LENGTH))) { // Its an node address character - lets check it if (thisnode[(hcs_packet_ctr-packet_idx)] != data_in) { // It isn't our packet - break out and wait for next one bad_hcs_packet = TRUE; } } } // if (hcs_packet_ctr < 2 && checksum) // Lets add the character into the checksum value if necessary if (checksum) { // Lets add the char to the checksum value if needed if (hcs_packet_ctr >= START_CHECKSUM) { check_val += data_in; } } // Save the data if we need to if (!bad_hcs_packet && (hcs_packet_ctr >= (REAL_DATA_STARTS + packet_idx))) { write_buffer(hcs_ring_ptr, data_in); // Store data in ring // Update ring pointers and loop if necessary if (++hcs_ring_ptr > HCS_RXBUFF_END) { // Lets loop hcs_ring_ptr = HCS_RXBUFF_START; } } // if (!bad_hcs_packet && (hcs_packet_ctr >= (REAL_DATA_STARTS + packet_idx)) hcs_packet_ctr++; // Increment the bit counter } // if ((data_in == '\r') || (data_in == '\n')) } // if (hcs_ring_ptr == hcs_ring_read) if (bad_hcs_packet) { // Lets move the pointer back to the 1st byte after hcs_end_ptr if (hcs_ring_end == HCS_RXBUFF_END) { // Loop hcs_ring_ptr = HCS_RXBUFF_START; } else { hcs_ring_ptr = hcs_ring_end + 1; } s_idle = TRUE; LAN_IN = LED_OFF; } // if (bad_hcs_packet) } // if (s_idle) } // while (bit_test(PIR1, 5)) } // // byte_to_ascii // // This routine converts an 4-bit hex value into an associated ASCII character // // Data to be converted is passed in (int) and the ascii value is returned byte byte_to_ascii(byte tdata) { if (tdata <= 9) { return (tdata | 0x30); } else { return ((tdata - 9) | 0x40); } } // // send_prt_data // // This routine accepts a byte and sends it to a parallel printer or // other connected device. It also handles all necessary handshaking boolean send_prt_data(byte printer_data) { // Sending the data is easy: // 1-Ensure the busy line is clear // 1a - If not, start a timeout timer // - If timer exceeds 25ms, dump buffer and return to wait loop // 2-Put the data on the bus // 3-Assert Strobe for 250us // 4-Loop to next character restart_wdt(); if (BUSY) { // We are busy - set timer and wait set_rtcc(0); while(BUSY) { // Check RTCC and if it exceeds 25ms, dump buffer restart_wdt(); if (get_rtcc() >= 196) { // Dump buffer return 0; } } } // Printer is ready for data! Send it! portb = printer_data; delay_cycles(1); // Let data stabilize STROBE = 0; delay_us(25); // Protocol says this can be as low as 1us! STROBE = 1; return 1; } // // This routine sends a byte back out the serial port UART // We have to wrap putc here because compiler serial out doesn't restart WDT // NOTE!!! This may have been fixed in latest compiler!!!!! #INLINE void send_uart_serial(byte char_in) { // We have to do our own serial send routine to get the WDT reset restart_wdt(); putc(char_in); } // // get_next_char // // This routine will return the next character from the ring buffer // If we reach the end of the packet, the ptr will wrap to the front // of the buffer. If we hit a 0x0D, we've reached the end of the // current command and this routine simply returns 0x0D without // incrementing the ptr. If we hit the ring_end ptr, we've finished // processing data and this routine simply returns the same character // char get_next_char() { if ((hcs_ring_read != hcs_ring_end) && (last_buff_char != 0x0D)) { if (++hcs_ring_read > HCS_RXBUFF_END) { hcs_ring_read = HCS_RXBUFF_START; } last_buff_char = read_buffer(hcs_ring_read); // Get the next character } return last_buff_char; } void main () { int data, last_hcs_data, scratch, scratch2, idx, last_query; int byte_change; boolean byte_query; char data_char[2]; PORT_B_PULLUPS(TRUE); // Since we tri state a High on port B set_tris_b(0b11111111); // User will determine the directions setup_counters(RTCC_INTERNAL, RTCC_DIV_256); // For BUSY timeout set_tris_a(0b11010111); // These shouldn't change strobe = 1; // Init printer port last_hcs_data = 0xFF; // Assume all inputs from HCS to start set_tris_c(0b10011000); // Serial Port IO // Turn off LEDs S_ERR = LED_OFF; LAN_IN = LED_OFF; P_OUT = LED_OFF; // Load the configuration thisnode[0] = 'D'; thisnode[1] = 'I'; thisnode[2] = 'O'; // Grab address from A0-A2 thisnode[3] = (~porta & 0x07) + 48; // Lets total up the set parts of the checksum (#, spaces, dummy checksum, and nodename) // This saves us time during each packet node_total = '#' + '0' + '0' + ' ' + 'D' + 'I' + 'O' + ' '; node_total += thisnode[3]; // Initial values for serial and buffer handling s_idle = TRUE; // Setup ring pointers hcs_ring_ptr = HCS_RXBUFF_START; hcs_ring_read = 0xFF; // Prevent initial overflow error hcs_ring_end = 0xFF; // When read = end, no new data last_query = portb; // Prime the query input buffer; byte_change = 0x00; // Lets enable the interrupts! disable_interrupts(GLOBAL); enable_interrupts(INT_RDA); enable_interrupts(GLOBAL); while (TRUE) { // Endless packet processing loop // Lets wait until the buffer has a valid packet for our node while(hcs_ring_read == hcs_ring_end) { // All we do here is restart the watchdog restart_wdt(); // Lets ensure that no serial error has disabled the serial input if (OERR) { S_ERR = LED_ON; CREN = 0; delay_cycles(1); CREN = 1; } // If a short pulse comes in between queries, the HCS-II will never // see it because we were sending back the port state at the time the // query was processed. Users have asked that the inputs be 'buffered' // in such a way that changes are returned to the HCS-II for one query. // The best we can do is to flip a bit if it CHANGES between queries // Thus we can catch one pulse but no more. For example, if a bit // is high at the last query, drops low and then returns high, the // next query it will be returned as LOW. The query after that will // cause it to be HIGH again unless it pulses again // The best way to do this is to keep a byte who's bits indicate a // change of state. We check portb against what we sent last time // Using an XOR. We take that result and OR it with the existing // flag byte. Result gets stored in flag byte. During query, we // XOR old query byte with flag byte to get new response byte_change |= (portb ^ last_query); // Turn on bits for inputs that have changed } // Okay, lets process the packet restart_wdt(); if (hcs_ring_read == 0xFF) { // This is our first packet hcs_ring_read = HCS_RXBUFF_START; } else { // Skip off the 0x0D to the first packet character if (++hcs_ring_read > HCS_RXBUFF_END) { hcs_ring_read = HCS_RXBUFF_START; } } // Command handling // The PIC DIO watches for the following commands: // Byte Set: DIO# S DP=(hex byte) // Bit Set: DIO# SDP.(bit num 0-7)=0 or 1 // Byte Read: DIO# QDP // Bit Read: DIO# QDP.(bit num 0-7) // Print Byte String DIO# TDP=string // Reset: DIO# R // We use a direct grab here since we already skipped off the 0x0D scratch = read_buffer(hcs_ring_read); // Lets get the command last_buff_char = 0x00; // Reset get_next_char lock (ie = 0x0D) // Lets allow a space between the command letter and DP if (get_next_char() == ' ') { get_next_char(); } // At this point we will be pointing to the D switch(scratch) { case('Q'): // Its a query! Read the byte or bit and return it // Note we DON'T touch last_hcs_data because a low input would be reasserted during the // next bit twiddle. // Lets check the port one last time byte_change |= (portb ^ last_query); // Turn on bits for inputs that have changed data = last_query ^ byte_change; // This buffers any short pulses last_query = portb; // Reset for next loop byte_change = 0x00; // Clear change flags // We need to figure out if they wanted a bit or byte returned get_next_char(); // The 'P' if (get_next_char() == '.') { // The '.' means a bit query byte_query = FALSE; // Lets prevent bogus bit numbers idx = get_next_char(); scratch2 = idx; // Need to save bit number for reply if ((idx > '7') || (idx < '0')) { break; } for (idx; idx >= '0'; idx--) { // Lets shift out the bit into a byte variable scratch = shift_right(&data, 1, 0); } // The requested bit is now in position 1 - lets wipe out the rest scratch &= 0b00000001; data_char[0] = scratch + 48; // Convert to ASCII 0 or 1 // Neat trick - we won't use this to return data, but we can use it to add the .# // to the computed checksum. Slick! data_char[1] = '.' + scratch2; } else { // They want a byte returned // Lets convert data into a 2 char ASCII string byte_query = TRUE; data_char[0] = byte_to_ascii(data >> 4); data_char[1] = byte_to_ascii(data & 0x0F); } // Should we send back a checksum? if (checksum) { // Lets build the checksum. We'll cheat and use some predetermined values here scratch = '$' + '0' + '0' + ' ' + 'D' + 'I' + 'O' + ' ' + 'D' + 'P' + '='; // In 8 bits only! scratch += thisnode[3] + data_char[0] + data_char[1]; // Now lets take the 2's complement of the checksum subtotal result scratch ^= 0xFF; scratch++; } // Turn of Serial Rx in case of RS-485 echo CREN = 0; // Lets send the response delay_ms(50); send_uart_serial('$'); if (checksum) { printf(send_uart_serial, "%2X", scratch); } printf(send_uart_serial, " DIO%c DP", thisnode[3]); if (!byte_query) { // Lets include the .# for the queried bit printf(send_uart_serial, ".%c=%c\n", scratch2, data_char[0]); } else { printf(send_uart_serial, "=%c%c\n", data_char[0], data_char[1]); } // Turn Rx back on CREN = 1; break; case('R'): // Lets do a hard reset - use the watchdog timer disable_interrupts(GLOBAL); S_ERR = LED_ON; LAN_IN = LED_ON; P_OUT = LED_ON; delay_ms(2000); while(TRUE) {} // This trips the watchdog reset break; case('S'): // Its a set command, bit or byte P_OUT = LED_ON; get_next_char(); // This will skip to the next char (P) if (get_next_char() == '=') { // Its a byte set - easy! // Convert the ASCII hex into a real hex value last_hcs_data = (gethexbyte(get_next_char()) * 16) + gethexbyte(get_next_char()); // Because we need open collector ports, we use the Tris command to set highs // and a normal port output for lows SET_TRIS_B(last_hcs_data); // Highs are high impedence portb = last_hcs_data; // Assert the lows - highs already there due to pullups last_query = last_hcs_data; // Override any buffered pulses byte_change = 0x00; } else { // Its a bit set - little more complicated // Use the last data sent by the HCS to maintain proper Tristate settings // We need this since a low input would reassert and flip the tri causing a possible short // if we read the port, flipped the bit, and sent it back to the port idx = 1; // Lets prevent bogus bit numbers scratch = get_next_char(); if ((scratch > '7') || (scratch < '0')) { break; } // Quick and dirty way to get 2^x for (scratch = (scratch & 0x0F); scratch != 0; scratch--) { idx <<= 1; // Shift the bit once } // If we set a bit, lets clear any pulse buffer bit byte_change &= (idx ^ 0xFF); get_next_char(); // Skip = if (get_next_char() == '0') { // Lets twiddle the bit using the last HCS data // Thus we won't grab inputs and re assert them last_hcs_data &= (idx ^ 0xFF); // Twiddle the bit from HCS data - maintain inputs last_query &= (idx ^ 0xFF); // Clear bit in last_query byte portb = last_hcs_data; // Set latches BEFORE changing TRIS SET_TRIS_B(last_hcs_data); // Now assert the low already set in latches } else { last_hcs_data |= idx; // Set the specific bit last_query |= idx; // Set bit in last_query buffer SET_TRIS_B(last_hcs_data); // No need to set port. Pull-ups take care of that } } // Pulse the strobe on a port change in case the user needs it delay_cycles(1); // Let data stabilize strobe = 0; delay_us(250); // Pulse strobe low in case they need it strobe = 1; break; case('T'): // Printer output. // Here we simply cycle through the buffer (after the = sign) and output // the data to the port using the Strobe and Busy signals for handshaking // We'll timeout after 1 second and dump the buffer if we get stuck on a busy P_OUT = LED_ON; get_next_char(); // Returns the P set_tris_b(0x00); // All outputs on PORT_B get_next_char(); // The = sign scratch2 = get_next_char(); // The first real character or 0x0D if nothing after = while(scratch2 != 0x0D) { // Lets scan the buffer and output to the printer // First lets check for an escape sequence (\) restart_wdt(); // We need to store the data to output separate from the actual data // variable. If we send \n, data gets set to 0x0D which will cause // us to break out prematurely if (scratch2 == '\\') { switch (get_next_char()) { case 'n': // New Line (linefeed + carriage return) scratch = 13; break; case 'r': // Carriage Return only scratch = 10; break; case 'x': // Next two bytes are a hex number! // convert ascii to hex byte scratch = (gethexbyte(get_next_char()) << 4) | gethexbyte(get_next_char()); break; case 'f': scratch = 12; break; case 'e': scratch = 27; break; case 't': scratch = 9; break; case 'c': // Control Code - simply subtract 64 or 96 from next character and send scratch = get_next_char(); if (scratch > 95) { // Its lower case scratch -= 96; } else { // Its upper case scratch -= 64; } break; case '\\': scratch = '\\'; break; default: scratch = get_next_char(); // unknown command letter - ignore backslash } } else { scratch = scratch2; } // Send the data - dump buffer if busy timeout if (!send_prt_data(scratch)) { scratch2 = 0x0D; // We'll break out here } else { scratch2 = get_next_char(); // Grab the next character } } // while(scratch2 != 0x0D) break; default: // Do Nothing break; } // switch(scratch) // Now lets ensure we are pointing at the end of the packet // This ensures any extra data at the end is skipped // Every packet will have 0x0D at the end // get_next_char will NOT skip past 0x0D if we are already there while (get_next_char() != 0x0D) {} P_OUT = LED_OFF; } while (TRUE); }