7-segment controller – Sequences and Serial protocol

A few days ago I finally started writing the LED controller code according to the specification of the init sequence that is described in another post here. One of the first tasks is to light up the LEDs and the display as a self-test on startup. Lighting up the display means that I should show the number ‘888’ that lights up all the segments (except for the decimal dot that I don’t use in this project). At this point I realised that the serial receiver part can receive only one byte – numbers up to 255. So I decided to spend a bit more time on the 7-segment controller and enhance it in two ways:

  1. Allow it to display sequences of patterns and not only numbers. The difference is this – when displaying a number the controller displays the same digits over and over again while a sequence shows a different pattern on every iteration through the main loop. The difference is a bit subtle because a number can be considered a sequence with just one state. Anyway, the youtube video below shows an example of a “sequence”
  2. Implement a serial input protocol by which it is possible to send commands to the controller. The protocol is explained below.

Example of a sequence

This youtube video shows an example of a sequence.

The serial protocol

The previous version of the controller listened on the serial port and expected to receive a single byte each time that it interpreted as an unsigned 1-byte number. The new version expects a “packet” with the following structure:

  • Type – 1 byte. Values are: 1 (1-byte number), 2 (2-byte number), 3 (4-byte number), 4 (sequence ID)
  • Length – 1 byte. The length of the value parameter. This is a forward looking field because right now the lengths of the values are completely determined by the type field. However, I am preparing for passing strings in the future.
  • Value – variable length according the the Length field. If the value is a number it is transmitted MSB first.
  • Delay – 1 byte. The delay is multiplied by 50 by the controller and is used for the time period in which digit is lit. The unit is the Timer0 rollover rate.
  • Stop byte – 1 byte – value 0xFF. This byte serves to indicate ‘end of packet’ and as a simple check sum. The controller verifies that it sees 0xFF at the expected end of the packet.

The code

#include <xc.h>

// CONFIG1
#pragma config FOSC = INTOSC // Oscillator Selection 
#pragma config WDTE = OFF // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = ON // Power-up Timer Enable (PWRT enabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select 
#pragma config CP = ON // Flash Program Memory Code Protection 
#pragma config CPD = ON // Data Memory Code Protection 
#pragma config BOREN = ON // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF // Clock Out Enable 
#pragma config IESO = ON // Internal/External Switchover 
#pragma config FCMEN = ON // Fail-Safe Clock Monitor Enable 

// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection 
#pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable 
#pragma config BORV = HI // Brown-out Reset Voltage Selection
#pragma config LVP = OFF // Low-Voltage Programming Enable 

#define STOP_BYTE 0xFF
#define BUFFER_SIZE 36
#define MAX_SEQUENCE_ID 1

// pin to segment assignment is:
// C0-C1-C2-C3-C4-C5-A4
// A -B -C -D -E -F -G

// Pin A1 used for Serial RX
// Pin A0 used for display pin 8 – digit 1 – ones
// Pin A2 used for display pin 9 – digit 2 – tens

// Serial input types
// 1 – 1 byte value
// 2 – 2 byte value
// 3 – 4 byte value
// 4 – sequence id

typedef struct {
    unsigned char *state;
    int length;
} Sequence;

int loopstate;
int overrun = 0;
unsigned char rxbuf[BUFFER_SIZE];
unsigned char rxoffset = 0;
unsigned char digit1;
unsigned char digit2;
unsigned char digit3;

The digits array maps each digit to its representation in 7-segments 

unsigned char digits[] = {
   0b01000000, // 0
    0b01111001, // 1
    0b00100100, // 2
    0b00110000, // 3
    0b00011001, // 4
    0b00010010, // 5
    0b00000010, // 6
    0b01111000, // 7
    0b00000000, // 8
   0b00011000 // 9
};

The sequences below describe special effects of lighting segments of the display. Each record (a triple) represents the state of the 3 digits. A zero (0) indicates that the corresponding segment it lit while a (1) indicates that the segment is off.

unsigned char state1[][3] = { // figure 8
   {0b01111110, 0b01111111, 0b01111111},
   {0b01111101, 0b01111111, 0b01111111},
   {0b01111011, 0b01111111, 0b01111111},
   {0b01111111, 0b01110111, 0b01111111},
   {0b01111111, 0b01111111, 0b01101111},
   {0b01111111, 0b01111111, 0b01011111},
   {0b01111111, 0b01111111, 0b01111110},
   {0b01111111, 0b01111111, 0b01111101},
   {0b01111111, 0b01111111, 0b01111011},
   {0b01111111, 0b01111111, 0b01110111},
   {0b01111111, 0b01111011, 0b01111111},
   {0b01111111, 0b01111101, 0b01111111},
   {0b01111111, 0b01111110, 0b01111111},
   {0b01111111, 0b01011111, 0b01111111},
   {0b01111111, 0b01101111, 0b01111111},
   {0b01110111, 0b01111111, 0b01111111},
   {0b01101111, 0b01111111, 0b01111111},
   {0b01011111, 0b01111111, 0b01111111}
};

unsigned char state2[][3] = { // run on edges
   {0b01111110, 0b01111111, 0b01111111},
   {0b01111111, 0b01111110, 0b01111111},
   {0b01111111, 0b01111111, 0b01111110},
   {0b01111111, 0b01111111, 0b01111101},
   {0b01111111, 0b01111111, 0b01111011},
   {0b01111111, 0b01111111, 0b01110111},
   {0b01111111, 0b01110111, 0b01111111},
   {0b01110111, 0b01111111, 0b01111111},
   {0b01101111, 0b01111111, 0b01111111},
   {0b01011111, 0b01111111, 0b01111111}
};

The sequence array describes the segments – a pointer to the array of triples and the length.

Sequence sequence[] = {
   {(unsigned char *) state1, 18},
   {(unsigned char *) state2, 10}
};

The following 5 variables control the loop that lights up segments.

// control variables with sensible initial values
int type = 4;
int state_index = 0;
int delay = 8;
unsigned int number = 256;
Sequence *seq = &(sequence[1]) ;

The set_digits function is called at the start of every “display” loop. It determines what should be displayed in the following iteration of the display loop. It could be a number where each display digit shows a digit of the number, or a different pattern of a sequence.
/****************************************
*
****************************************/
void set_digits()
{
   int remainder;
   unsigned char *s;

   if (type >= 1 && type <=3) { // number
      digit1 = digits[number / 100];
      remainder = number % 100;
      digit2 = digits[remainder / 10];
      digit3 = digits[remainder % 10];
   }
   else { // sequence
      s = &(seq->state[state_index]);
      digit1 = s[0];
      digit2 = s[1];
      digit3 = s[2];
      state_index += 3;
      if (state_index == seq->length * 3)
         state_index = 0;
      }
   }

}

The process_serial_input function is called by the ISR whenever it sees a stop byte (the value 0xFF). The stop value is a valid value therefore this function returns 0 to indicate that a full packet was processed and a -1 to indicate that the buffer does not contain a full packet and more bytes should be acquired.

/****************************************
*
****************************************/
int process_serial_input()
{
   int i;
   unsigned char ltype = 0;
   unsigned char len = 0;
   unsigned char ldelay = 0;
   unsigned int intvalue;
   unsigned char stopbyte = 0;
   unsigned char *buff = rxbuf;

   ltype = *buff++;
   len = *buff++;

   for (i = 0; i < len; i++) {
      intvalue = intvalue << 8;
      intvalue += *buff++;
   }
   ldelay = *buff++;
   stopbyte = *buff;

   // Check the input for errors
   if (buff[2 + len + 2] != STOP_BYTE)  // missing stop byte
      return -1;
   if (ltype > 4)
      return -1;
   if (ltype == 4 && intvalue > MAX_SEQUENCE_ID)
      return -1;

   type = ltype;
   delay = ldelay;
   if (ltype < 4)
      number = intvalue;
   else
      seq = &(sequence[intvalue]);

   return 0;
}

The interrupt service routine reads bytes from the serial connection and fills the input buffer.

/****************************************
*
****************************************/
void interrupt ISR()
{
   int rc;
   unsigned char invalue;

   if (PIR1bits.RCIF == 1) {
      if (FERR == 0) {
         invalue = RCREG;
         rxbuf[rxoffset] = invalue;
         rxoffset++;

         if (invalue == STOP_BYTE) {
            rc = process_serial_input();
            if (rc == 0)
               rxoffset = 0;
         }
     }

      if (OERR == 1) {
         overrun = 1;
         rxoffset = 0;
      }
      PIR1bits.RCIF = 0;
   }
}

The show_overrun_error displays a special pattern on the display to indicate an overrun error. No handling of this yet.

/****************************************
*
****************************************/
void show_overrun_error()
{
LATC0 = 0;
LATC1 = 1;
LATC2 = 1;
LATC3 = 1;
LATC4 = 1;
LATC5 = 1;
LATA4 = 1;
}

The show_number function displays  a digit.

/****************************************
*
****************************************/
void show_number(unsigned char number)
{
   LATC0 = ((number & 0x1) > 0) ? 1 : 0;
   LATC1 = ((number & 0x2) > 0) ? 1 : 0;
   LATC2 = ((number & 0x4) > 0) ? 1 : 0;
   LATC3 = ((number & 0x8) > 0) ? 1 : 0;
   LATC4 = ((number & 0x10) > 0) ? 1 : 0;
   LATC5 = ((number & 0x20) > 0) ? 1 : 0;
   LATA4 = ((number & 0x40) > 0) ? 1 : 0;
}

Main contains the initialisation and the display loop. It displays a single digit at any time. 

/****************************************
*
****************************************/
void main()
{
   int interval;

   OSCCON = 0xF0; // set internal osc to 32Mhz
   OPTION_REG = 0x08; // Prescaler not assigned to timer 0

   TRISC0 = 0;
   TRISC1 = 0;
   TRISC2 = 0;
   TRISC3 = 0;
   TRISC4 = 0;
   TRISC5 = 0;
   TRISA4 = 0;
   TRISA5 = 0;
   TRISA2 = 0;
   TRISA0 = 0;
   TRISA1 = 1;

   // configure the RX pin on A1
   RXDTSEL = 1;

   // configure EUSART BAUD rate: Fosc / 64(n+1)
   BRG16 = 0;
   BRGH = 0;

   // configure the EUSART receiver
   CREN = 1;
   SYNC = 0;
   SPEN = 1;

   // The analog bit must be cleared for RX to function
   ANSELAbits.ANSA1 = 0;

   loopstate = 0;

   // enabling EUSART rx interrupt
   PIE1bits.RCIE = 1;
   INTCONbits.PEIE = 1;
   INTCONbits.GIE = 1;

   while(1) {
      if (loopstate == 0) {
         interval = delay * 50;
         set_digits();
         LATA0 = 0;
         LATA2 = 1;
         LATA5 = 0;
         if (overrun == 1)
            show_overrun_error();
         else
            show_number(digit2);
      }
      if (loopstate == 1 * interval) {
         LATA0 = 1;
         LATA2 = 0;
         LATA5 = 0;
         if (overrun == 1)
            show_overrun_error();
         else
            show_number(digit3);
      }
      if (loopstate == 2 * interval) {
         LATA5 = 1;
         LATA2 = 0;
         LATA0 = 0;
         if (overrun == 1)
            show_overrun_error();
         else
            show_number(digit1);
      }
      if (loopstate >= 3 * interval)
         loopstate = 0;
      else
         loopstate++;
   };
}

An interesting bug

This code is not really debugged because of lack of time but it works. However, there is an interesting bug in the system that I don’t have time to fix. The problem is that every 256th number is shown as a zero (0).

I don’t know if the problem is in the controller, the protocol or the test app that send the numbers over the serial connection.

On second thought, this problem must be related to my use of the value 0xFF as a stop byte. I will have to research this a bit more.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s