/* UTC_Clk v2, January 2012 By bertrand Zauhar, VE2ZAZ ve2zaz@amsat.org http://ve2zaz.net TARGET device: PIC18F1220 Compiler: SourceBoost's BoostC for PIC v7 IDE: SourceBoost IDE This code is written to work with a pair of Sure Electronics 3208 (32 x 8) LED array displays mounted side by side. CS1 and CS2 are used to address the two Holtek HT1632C display controller chips. Resulting display : DDD- v2: Fixes the end of year rollover problem, where the display would show 366-00:00:00 instead of 001-00:00:00 V1: Initial Release. */ #include // Probably redundant #include #include #include #define SINGLE_PORT_MODE // Required by uart_driver.h to specify a single port usage #include // #include // Only needed if real time debugging using ICD2 is required // GLOBALS volatile char led_tris @TRISA; //Port A wired to the LED boards volatile char led_port @PORTA; //Port A wired to the LED boards volatile char ctl_tris @TRISB; //Port B wired to the external control signals volatile char ctl_port @PORTB; //Port B wired to the external control signals // USART Variables used by uart_driver.h unsigned char txBuffer[2]; // Adjust to bigger size if used. unsigned char txHead; unsigned char txTail; unsigned char rxBuffer[2]; // Requirement is small because buffer is emtied as soon as data is received and partial NMEA sentenses are rejected unsigned char rxHead; unsigned char rxTail; unsigned char rxCnt; // unsigned char rxdata[76]; // Requirement is 74 characters, incl. CR and LF. unsigned char intr_ctr; bool rxstring_ready; // Used to flag that the Received string is complete and ready unsigned char a; // variable used as counter unsigned char b; // variable used as counter unsigned char c; // variable used as counter char tempbuff[3]; // Temporary buffer used for string to numerical conversions unsigned char rxdata_ctr; // Used to point to current position oin the rxdata buffer bool pps_detected; // Used to flag when the PPS pulse gets detected unsigned short day_of_year; // 1 to 366 (day of year). 16 bits unsigned unsigned char hrs; // Hours numerical value unsigned char mins; // Minutes numerical value unsigned char secs; // Seconds numerical value bool init_display; // Used to initialize the display at power up bool leap_year; // Used to flag that it is a leap year rom char *gprmc_preamble = "$GPRMC"; // Beginning of the NMEA string used to extract time and date rom char *digit_data_table = { // 5 x 7 dot digits + additional spacing column (bottom row reserved for walking seconds dot) 0x7C,0x82,0x82,0x82,0x7C,0x00, // 0 0x00,0x42,0xFE,0x02,0x00,0x00, // 1 0x42,0x86,0x8A,0x92,0x62,0x00, // 2 0x84,0x82,0xA2,0xD2,0x8C,0x00, // 3 0x18,0x28,0x48,0xFE,0x08,0x00, // 4 0xE4,0xA2,0xA2,0xA2,0x9C,0x00, // 5 0x3C,0x52,0x92,0x92,0x0C,0x00, // 6 0x80,0x8E,0x90,0xA0,0xC0,0x00, // 7 0x6C,0x92,0x92,0x92,0x6C,0x00, // 8 0x60,0x92,0x92,0x94,0x78,0x00, // 9 0x00,0x10,0x10,0x10,0x00,0x00, // - 0x28,0x00,0x00,0x00,0x00,0x00, // : This one is only one colummn plus spacing column 0x01,0x00,0x00,0x00,0x00,0x00 // . Also one column + spacing column. }; rom char *column_data_table = { // This is good only for 5 column-wide digits on a 64-column display 0, // Day of year: digit #1 6, // Day of year: digit #2 12, // Day of year: digit #3 18, // '-' 24, // Hours: digit #1 30, // Hours: digit #2 36, // ':' 38, // Minutes: digit #1 44, // Minutes: digit #2 50, // ':' 52, // Seconds: digit #1 58 // Seconds: digit #2 }; rom char *days_in_months = { // Lists the number of days in each month. Used to calculate the day number. 31,28,31,30,31,30,31,31,30,31,30,31 }; #define digit_width 6 // The width in pixels (number of columns) of each character, including the spacing column /* Alternate Digit size rom char *digit_data_table = { // 3 x 5 dot digits + additional spacing column 0x3E,0x22,0x3E,0x00, // 0 0x22,0x3E,0x02,0x00, // 1 0x2E,0x2A,0x3A,0x00, // 2 0x2A,0x2A,0x3E,0x00, // 3 0x38,0x08,0x3E,0x00, // 4 0x3A,0x2A,0x2E,0x00, // 5 0x3E,0x2A,0x2E,0x00, // 6 0x20,0x20,0x3E,0x00, // 7 0x3E,0x2A,0x3E,0x00, // 8 0x3A,0x2A,0x3E,0x00, // 9 0x08,0x08,0x08,0x00, // - 0x14,0x00,0x00,0x00 // : This one is only one colummn plus spacing column }; rom char *column_data_table = { // This is good only for 3 column-wide digits on a 64-column display 0, // Day of year: digit #1 4, // Day of year: digit #2 8, // Day of year: digit #3 12, // '-' 16, // Hours: digit #1 20, // Hours: digit #2 24, // ':' 26, // Minutes: digit #1 30, // Minutes: digit #2 34, // ':' 36, // Seconds: digit #1 40 // Seconds: digit #2 }; #define digit_width 4 // The width in pixels (number of columns) of each character, including the spacing column */ // CONFIGURATION BYTES #pragma DATA _CONFIG1H, _INTIO2_OSC_1H & _FSCM_OFF_1H & _IESO_OFF_1H #pragma DATA _CONFIG2L, _PWRT_OFF_2L & _BORV_27_2L & _BOR_ON_2L #pragma DATA _CONFIG2H, _WDT_OFF_2H #pragma DATA _CONFIG3H, _MCLRE_OFF_3H #pragma DATA _CONFIG4L, _DEBUG_OFF_4L & _LVP_OFF_4L & _STVR_OFF_4L #pragma DATA _CONFIG5L, _CP0_OFF_5L & _CP1_OFF_5L #pragma DATA _CONFIG5H, _CPB_OFF_5H & _CPD_OFF_5H #pragma DATA _CONFIG6L, _WRT0_OFF_6L & _WRT1_OFF_6L #pragma DATA _CONFIG6H, _WRTC_OFF_6H & _WRTB_OFF_6H & _WRTD_OFF_6H #pragma DATA _CONFIG7L, _EBTR0_OFF_7L & _EBTR1_OFF_7L #pragma DATA _CONFIG7H, _EBTRB_OFF_7H #pragma CLOCK_FREQ 8000000 // Tells the compiler what clock frequency is used. Important for delay functions // CONSTANTS AND MASKS DEFINITION // Definitions required by uart_driver.h #define uart1Init rs232Init #define uart1TxInterruptHandler rs232TxInterruptHandler #define uart1RxInterruptHandler rs232RxInterruptHandler #define uart1Rx rs232Rx #define uart1Tx rs232Tx //Other definitions #define int_ena_bit 7 // The bit position for intcon register that controls the high priority interrupts. 1 is enabled. #define cs1 0 // The bit position for CS1 on the LED controller port. #define cs2 1 // The bit position for CS2 on the LED controller port. #define data 2 // The bit position for the DATA line on the LED controller port. #define wr 3 // The bit position for the WR line on the LED controller port. #define pps_pin 0 // The bit position for the PPS signal into the control port. // Interrupt Service Routine void interrupt( void ) // High priority interrupt { clear_bit(intcon,int_ena_bit); // disable interrupts if(uart1RxInterruptHandler()) //interrupt is EUSART validated. { rxdata[rxdata_ctr++] = uart1Rx(); // Read character from rxbuffer if (rxdata[rxdata_ctr-1] == '\n') // If character detected... { rxdata_ctr = 0; // ... clear rxdata position counter, rxstring_ready = true; // This signals that a NMEA sentence is available in rxdata } clear_bit(pir1,5); // clear the USART rx interrupt request bit } set_bit(intcon,int_ena_bit); // enable interrupts } // End of interrupt routine // void interrupt_low( void ) // Low priority interrupt //{ //} // End of interrupt routine // EXECUTED CODE STARTS HERE char check_walking_dot(char y) //This function checks if the walking dot lines up with the LED column being updated { if ((secs + 2) == y) return 1; // Range is column 2 to column 61. else return 0; } // Procedure that sends the LED data ( to the display controller) for one of the 64 LED columns void send_led_column(char column_num,char column_data) { // LOCAL VARIABLES volatile char x; // Used as local counter volatile char y; // Used as local counter volatile char z; // Used as local counter // INSTRUCTIONS // Here we toggle the proper CS signal based on the column number received if (column_num < 32) { clear_bit(led_port,cs1); // Clear CS1 (chip Select active for display #1) } else { clear_bit(led_port,cs2); // Clear CS2 (chip Select active for display #2) column_num = column_num - 32; // This brings the column number back to 0-31 for Display #2 } // for (x = 0; x <= 1; x++) // 8 LEDs for each column = 2 display controller addresses (4 LEDs per address x) // { // Here we send "101", the write command set_bit(led_port,data); // Write "1" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" clear_bit(led_port,data); // Write "0" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" set_bit(led_port,data); // Write "1" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" // Here we send the address (A6 to A0 bits) for (z = 7; z > 0; z--) { if (test_bit(column_num * 2 , z-1)) // Case when sent data bit is high { set_bit(led_port,data); // Write "1" } else //Case when sent data bit is low { clear_bit(led_port,data); // Write "0" } set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width on display controller. clear_bit(led_port,wr); // Transition clock to "0" } // Here we send the LED data for one column (D0 to D3 bits x 2 contiguous addresses) for (z = 8; z > 0; z--) { if (test_bit(column_data, z-1)) // Case when sent data bit is high { set_bit(led_port,data); // Write "1" } else //Case when sent data bit is low { clear_bit(led_port,data); // Write "0" } set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" } // } set_bit(led_port,cs1); // Set CS1 (chip Select inactive for display #1) set_bit(led_port,cs2); // set CS2 (chip Select inactive for display #2) } // Procedure that sends one full character to the LED display controller void send_led_digit(char digit_pos,char digit_data) { // LOCAL VARIABLES volatile char x; // Used as local counter volatile char y; // Used as local counter volatile char z; // Used as local counter volatile char table_index; // Pointer in the digit table // INSTRUCTIONS z = 0; // z is the digit data table field offset switch(digit_data) // Select the right LED data based on the character received { case '0': // Cases of digits case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': x = digit_data - 48; // Converts the ascii value to numerical table_index = x * digit_width; // Initialises to the right character in the table for (y = digit_pos ; y <= digit_pos + 5 ; y++) // y is the LED column { send_led_column(y,digit_data_table[table_index + z] + check_walking_dot(y)); // Send one column data to controller z++; // Increment the digit data table field offset } break; // Leave switch section case '-': // Case of "-" table_index = 10 * digit_width; // Initialises to the right character in the table for (y = digit_pos ; y <= digit_pos + 5 ; y++) // y is the LED column { send_led_column(y,digit_data_table[table_index + z] + check_walking_dot(y)); // Send one column data to controller z++; // Increment the digit data table field offset } break; // Leave switch section case ':': table_index = 11 * digit_width; // Initialises to the right character in the table for (y = digit_pos ; y <= digit_pos + 1 ; y++) // y is the LED column. ':' is only two columns wide { send_led_column(y,digit_data_table[table_index + z] + check_walking_dot(y)); // Send one column data to controller z++; // Increment the digit data table field offset } break; // Leave switch section case '.': table_index = 12 * digit_width; // Initialises to the right character in the table for (y = digit_pos ; y <= digit_pos + 1 ; y++) // y is the LED column. '.' is only two columns { send_led_column(y,digit_data_table[table_index + z] + check_walking_dot(y)); // Send one column data to controller z++; // Increment the digit data table field offset } break; // Leave switch section default: // Will generate a " " (space) for (y = digit_pos ; y <= digit_pos + 5 ; y++) // y is the LED column { send_led_column(y,0x00 + check_walking_dot(y)); // no LEDs on except walking dot z++; // Increment the digit data table field offset } } } void send_command(char command) { // LOCAL VARIABLES volatile char y; // Used as local counter volatile char z; // Used as local counter // INSTRUCTIONS for (y = 0; y<=1; y++) // Two passes to update the two display controllers { clear_bit(led_port,cs1 + y); // Clear CSx to select proper display (chip Select active low) // The data write starts with "100....." set_bit(led_port,data); // Write "1" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" clear_bit(led_port,data); // Write "0" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" clear_bit(led_port,data); // Write "0" set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" for (z = 8; z > 0; z--) // Then send command bits 8 to 1 { if (test_bit(command, z-1)) // Case when sent data bit is high { set_bit(led_port,data); // Write "1" } else //Case when sent data bit is low { clear_bit(led_port,data); // Write "0" } set_bit(led_port,wr); // Transition clock to "1" nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" } set_bit(led_port,wr); // Transition clock to "1". Command bit 0 is a "no care" but must be sent, thus an extra clock toggle. nop(); // Required to meet the minimum pulse width. clear_bit(led_port,wr); // Transition clock to "0" set_bit(led_port,cs1 + y); // Release CSx (chip Select inactive for display) } } char asciidec_2_dec(char buffer10,char buffer1) //This function converts two-character decimal representation into a decimal value. { // INSTRUCTIONS return ((buffer10 - 48) * 10) + (buffer1 - 48); } void main(void) { // Initialization stage rcon = 0b10000000; // enable interrupt priorities (high/low) intcon = 0b00000000; // Disable high priority ints, disable low priority ints. osccon = 0b01110010; // select internal oscillator and set it to 8MHz (this speed is required for 9600 bps USART) intcon2 = 0b00000000; // enable weak pullups on portb intcon3 = 0b00000000; // Don't care if external peripheral interrupts not used pie1 = 0b00100000; // Disable all interrupts. uart1Init takes care of the USART interrupt enables. pie2 = 0b00000000; // Disable all interrupts. ipr1 = 0b00100000; // Set USART Tx and Rx priority levels to low priorities. ipr2 = 0b00000000; // Don't care for LED board. t0con = 0b00000111; // Disable T0. t2con = 0b00000011; // Disable T2. adcon0 = 0; // disables A/d converter adcon1 = 0xFF; // Set RA<7:0> as digital I/O pins txsta = 0b00000000; // Set EUSART TX to high speed, asynchronous and enable it for 8 bit operation rcsta = 0b10010000; // Set EUSART RX for 8 bit operation and enable it baudctl = 0b00001000; // Set EUSART RX for 8 bit operation, 8 bit baud rate generator and enable it spbrgh = 0; // Sets 9600 bauds when SYNC = 0, BRGH = 0 and BRG16 = 1 spbrg = 51; // " portb = 0b00000000; // Clear portB trisb = 0b11110001; // PORTB set as follows: // 7-Not used. Has internal pull-up. // 6-Not used. Has internal pull-up. // 5-Not used. Has internal pull-up. // 4-USART Asynchronous Receive. // 3-Not used. Has internal pull-up. // 2-Not used. Has internal pull-up. // 1-USART Asynchronous Transmit. // 0-PPS signal input. Has internal pull-up. porta = 0b00000011; // Chip Selects placed high, write clock placed low. trisa = 0b11110000; // PORTA first disabled, but assigned as follows: // 7-Not used. // 6-Not used. // 5-Not used. Can be assigned to MCLR input. // 4-Not used. Open Drain when set as output. // 3-WR output to LED board // 2-Data output to LED board // 1-CS2 output to LED board // 0-CS1 output to LED board rxdata_ctr = 0; // Positions the rxdata buffer counter to 0 pps_detected = false; // PPS pulse not detected txHead = 0; txTail = 0; rxHead = 0; rxTail = 0; rxCnt = 0; //Initialize the two display controllers send_command(0b00000000); // System Disable send_command(0b00100000); // Common mode: N-MOS open drain output and 8 COM option send_command(0b00011000); // Master mode and clock source from on-chip RC osc., system clock output to OSC pin and sync. signal output to SYN pin send_command(0b00000001); // System Enable send_command(0b10100000 + 7); // PWM intensity from 0 to 15 send_command(0b00000011); // LEDs on init_display = true; // This forces an initial update of the display rxstring_ready = true; // This forces an initial update of the display with the test string uart1Init(); //Initialize uart driver pie1 = 0b00100000; // Disable USART Tx interrupt enables (not using Tx) set_bit(intcon,int_ena_bit); // enable low priority interrupts while (true) // Main loop { if (((!pps_detected) && (ctl_port.pps_pin == 1)) || init_display) // If PPS pulse detected for the first time since it got reset (or if at power up) { init_display = false; // Do not allow display init entry into the loop anymore pps_detected = true; // Set the PPS pulse detected flag if (rxstring_ready) { // Expected string format is: $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh rxstring_ready = false; if (strncmp(rxdata,gprmc_preamble,6) == 0) //Compare first 6 characters of received string { day_of_year = 0; hrs = asciidec_2_dec(rxdata[7],rxdata[8]); //Convert the time into numeric values mins = asciidec_2_dec(rxdata[9],rxdata[10]); secs = asciidec_2_dec(rxdata[11],rxdata[12]); if (++secs == 60) //Increase the seconds by 1 (NMEA lags the PPS by 1) and treat the seconds rollover { secs = 0; if (++mins == 60) //Treat the minutes rollover { mins = 0; if (++hrs == 24) hrs = 0; //Treat the hours rollover } } uitoa_dec(tempbuff,hrs,2); // Convert the numerical Hours to ascii characters send_led_digit(column_data_table[4],tempbuff[0]); //Hours fields sent to display send_led_digit(column_data_table[5],tempbuff[1]); send_led_digit(column_data_table[6],':'); uitoa_dec(tempbuff,mins,2); // Convert the numerical Minutes to ascii characters send_led_digit(column_data_table[7],tempbuff[0]); //Minutes fields sent to display send_led_digit(column_data_table[8],tempbuff[1]); send_led_digit(column_data_table[9],':'); uitoa_dec(tempbuff,secs,2); // Convert the numerical Seconds to ascii characters send_led_digit(column_data_table[10],tempbuff[0]); //seconds fields sent to display send_led_digit(column_data_table[11],tempbuff[1]); // Now calculating the day of the year b = 0; // used as comma counter c = 0; // used as character counter leap_year = false; do { if (rxdata[c++] == ',') b++; }while (b < 9); //date is located right after the ninth comma character for (a = 0; a < (asciidec_2_dec(rxdata[c+2],rxdata[c+3]) - 1);a++) // Compute the number of days in the previous completed months { day_of_year = day_of_year + days_in_months[a]; // Adds up the days in the previous completed months } day_of_year = day_of_year + asciidec_2_dec(rxdata[c],rxdata[c+1]); // Adds the days in the current month if ((asciidec_2_dec(rxdata[c+4],rxdata[c+5])%4 == 0)) leap_year = true; // Check if leap year. if (leap_year && (a > 1)) day_of_year++; // Add leap year day after February 29th. if (!(hrs | mins | secs)) // Check if day rollover happens { day_of_year++; // Increment the day of year when time rolls over to 00:00:00, otherwise will only happen at 00:00:01 if ((!leap_year && (day_of_year == 366)) || (leap_year && (day_of_year == 367))) day_of_year = 1; // Treat the year rollover impact of day of year. } uitoa_dec(tempbuff,day_of_year,3); // Convert the numerical day of the year to ascii characters for (a = 0; a <= 2 ;a++) send_led_digit(column_data_table[a],tempbuff[a]); //day of the year sent to display send_led_digit(column_data_table[3],'-'); // Add a field separator between day and time } } else // PPS pulse detected but no NMEA string available, or wrong NMEA string preamble { for (a = 0; a <= 11; a++) send_led_digit(column_data_table[a],'-'); // Fill the display with dashes } } else if (ctl_port.pps_pin == 0) // If PPS pulse clears, reset the PPS pulse detected flag { pps_detected = false; // Reset the PPS pulse detected flag } } } // End of main function code