7SDevice part4

From Pinguino
Jump to: navigation, search
Language: English


PART 4

Device C Coding

Now we have to implement the real code that will run inside the PIC16F84A. The first thing to do is to set the processor configuration bits. We will disable code protection, watch dog timer, and power startup timer and declare the use of the XT oscillator. This is accomplished by in the file configuration_bits.c


__CONFIG(FOSC_XT & WDTE_OFF & PWRTE_OFF & CP_OFF);


Then in the user.h header we will put all port pin definitions, we will declare the receiver machine states in an enum type, and we will create some variables and functions prototypes.


/******************************************************************************/
/* User Level #define Macros                                                  */
/******************************************************************************/

#define MISO    RA2
#define MOSI    RA3
#define SCK     RA1
#define DSELECT RA0

#define WDT_TIMEOUT 30000   

/******************************************************************************/
/* User Function Prototypes                                                   */
/******************************************************************************/

volatile uint8_t DigitA,DigitB,out;

typedef enum APP_STATO {
            Init,
            Idle,
            WaitForClockDown,
            WaitForClockUp,
            UpdateDigit,
            TEST,
            ERROR
} stato ;

stato App_stato;
volatile uint8_t dgt=0;
volatile uint16_t App_WDT=0;
volatile uint8_t  reading_bit=0;
volatile uint8_t  buffer;

void InitApp(void);         /* I/O and Peripheral Initialization */
void App_Task(void);


Note that most variables are volatile. We declare an object as "volatile" when its value could be changed or modified in a way that is not apparent to the compiler, so we want a consistent and explicit storage model without hidden optimization. DigitA and DigitB will store the digit to display. The Dgt variable will be used to select the digit that the receiver state machine has to update. Buffer variable is the incoming byte from the host.

App_WDT and WDT_TIMEOUT will be explained later on.

In the function InitApp() in user.c we will initialize Ports and data direction as follows:

void InitApp(void)
{
   
  // RA0 : Output  (DigitSelect)
  // RA1 : Input   (SPI Protocol SCK pin from Master
  // RA2 : Input   (SPI Protocol MISO)
  // RA3 : Output  (SPI Protocol MOSI)
  // RA4 : Dont'care

     TRISA = 0b00010110;
     RA0 = 0;
     RA3 = 1;

  // RB0-RB7 Data bus to 7 Segment displays
     TRISB = 0;
     PORTB = 0;

  // Pull Up enabled on port B
  // timer0 Clock source internal
  // Prescaler Assigment to timer0
  // Prescaler set to 1/32
     
     OPTION_REG = 0b00000100;

  // Global Interrupt Enable
  // Timer0 Overflow Interrupt Enable

     INTCON = 0b10100100;

  // Timer0 overflow at a rate of 8.192 msec @ 4Mhz

     App_stato = Init;
}


Here we will also set timer0 clock source to be internal and prescaler to 1/32 and assign to timer0. From the datasheet we know that clock for internal counter is Fclock/4 so in our case is 1 Mhz. We set prescaler to 1/32 so the counter will increment at a rate of (Fclock/4)/32 that is 31250 hz. So our 8 bit counter will be incremented every 1/31250 sec and the counter will overflow every 256/31250 = 0.008192. That is 8.192 msec as we've seen in part 1. We enable interrupts and set the beginning receiver machine state to Init. From now on the interrupt routine will run forever displaying each digit in turn.

In user.c there are also the compiler directive for storing the ledcode table we seen in part 2 in the internal EEPROM with these instructions


/******************************************************************************/
/* INTERNAL EEPROM 7 Segment Display Table Preloading
/* The table starts at address 0 and list the "abcdefgh" code for each digit                                                             */
/******************************************************************************/

// Digit from 0 to 7
__EEPROM_DATA( 0x7B,0x60,0x37,0x75,0x6C,0x5D,0x5F,0x70 );

// Digit from 8 to F
__EEPROM_DATA( 0x7F,0x7C,0x7E,0x4F,0x1B,0x67,0x1F,0x1E );

// Digit H P L t - OFF
__EEPROM_DATA( 0x6E,0x3E,0x0B,0x0F,0x04,0x00,0x00,0x00 );


Let's now see the interrupt C routine:

void interrupt isr(void)
{
    uint8_t ledcode;
    // If timer0 overflows update displays in chopper mode
    if(T0IF)
    {
        if (out==0) {
            PORTB = 0;
            ledcode = eeprom_read(DigitB);
            DSELECT = 1;
            PORTB = ledcode;
            out=1;
        }
        else {
            PORTB = 0;
            ledcode = eeprom_read(DigitA);
            DSELECT = 0;
            PORTB = ledcode;
            out=0;
        }
        T0IF=0; /* Clear Interrupt Flag 1 */
    }
    else {} // ALL other interrupts un-handled
}


Here we check for interrupt overflow flag T0IF and if the mutex variable “out” is 0 we select digit B, if 1 we select digit A with DSELECT signal (RA0).

We switch off the leds first and then we retrieve the ledcode according the data digit displacement passed. The compiler function eeprom_read(displacement) will return the eeprom content at the address “Eeprom base + displacement” so for example when the host wants to display the digit 5 it passes the uint8 number 5 to the device and the device will retrieve the corresponding ledcode accessing the location Eeprom base + 5 that in our case is 0x5D. We put that value on port B and invert the mutex variable “out” then we reset the overflow flag.

I want to now talk about the blocking code. Even if we aren't writing a multitasking application it is a good idea to write the firmware code in a form of multiple state machines running at the same time.

With this approach the main code is composed of a unique super-loop. At every iteration each state machine task is called. Each state machine maintains its unique inner state variables and according to the environmental change events it updates variables and changes state if necessary. With this approach you can guarantee that many tasks can run at the same time and that the superloop execution time can be identified with a certain accuracy. If a state machine has the need to have a loop this is accomplished checking some state variable at each superloop iteration and changing state only when a certain condition is met.

In the part 3 we've seen that each machine state will be waiting for the other part to became ready or to complete its work. So we have to think about what happen if for some reason this information never appears. The state machine will hang in a loop we said. As a result the device would probably be unable to work again. For this reason we have to implement a mechanism to abort the current operation and revert to a known state if something goes wrong.

In every processor there is at least a system WatchDog Timer. This is a counter that increments on its own. When a specific value is reached the WatchDog timer resets the processor program counter and the code starts again as if the part was just powered on.

So the programmer has the responsibility of clearing the watchdog timer in each part of the code he wants assuring that is in a normal working condition and the watchdog timer trigger value is never reached. With this concept, should the firmware hang in a loop for some reason after a max amount of time, the processor will be surely reset.

We'll introduce the same concept for each state machine. So if the machine that handles the communication protocol hangs in a loop at every iteration of the superloop an application Watch Dog Timer will be incremented. This App_WDT will be reset during normal algorithm sequence.

Note that is possible to have many different Application WatchDog timers running at the same time (App_WDT_1, App_WDT_2 ecc) so we can model a specific application behavior according the expiration of a specific Application Watch Dog timer. In this way instead of resetting the whole processor we can reset a single task or even a specific part of a task within the superloop if needed.

Before we see the actual state machine device C Code implementation we have to say that it will be useful to have a mechanism to test if the hardware board just assembled is working properly before connecting any host. To do so, I implemented a self test in the code. When device is powered on and finds both clock and MOSI line down, it will enter in a blocking forever test state and will display a two digit counter running. This way you can check that every pcb track is ok and that the hardware is good. Let's now see the application code.


APP_Task(void) {

    switch(App_stato) {
        case Init:
            for (int dgt=0;dgt<30000;dgt++);
            if ((MISO == 0) && (SCK == 0)) App_stato = TEST;
            else App_stato = Idle;
            break;
        case Idle:
            buffer = 0;
            reading_bit = 0;
            MOSI = 1;
            App_stato = WaitForClockDown;
            break;
        case WaitForClockDown:
            App_WDT++;
            if (SCK == 0) {
            App_WDT = 0;
            MOSI = 0;
            App_stato = WaitForClockUp;
            }
            if (App_WDT>WDT_TIMEOUT) App_stato = Idle;
            break;
        case WaitForClockUp:
            App_WDT++;
            if (SCK == 1) {
                App_WDT = 0;
              if (MISO == 1) buffer |= 0x01;
              else buffer &= 0xFE;
              reading_bit++;
              if (reading_bit == 8) App_stato = UpdateDigit;
              else {
                  buffer = buffer <<1;
                  App_stato = WaitForClockDown;
              }
              MOSI = 1; 
            }
            if (App_WDT>WDT_TIMEOUT) App_stato = Idle;
            break;
        case UpdateDigit:
            if (dgt==0) DigitA = buffer;
            else DigitB = buffer;
            dgt = !dgt;
            MOSI = 1;
            App_stato = Idle;
            break;
        case TEST :
            for (int buffer=0;buffer<22;buffer++){
            DigitA = buffer;
            for (int dgt=0;dgt<22;dgt++) {
                DigitB = dgt;
                for (App_WDT = 0;App_WDT < 15000;App_WDT++) {}
            }
            }
    }
}


The state machine starts in the init state and waits for the power to stabilize. Then it checks if a test condition is matched. If this is the case the next state is set at TEST. On the next superloop iteration it will enter in the TEST state and will stay there for ever until power off. The wait for power stabilizing time and the test state time are the unique part of the code that are blocking since in this context the application isn't started yet.

The state machine implementation is quite similar to what we saw in the java simulator. Here the blocking code we see has been substituted. Let's analyze this code:

        case WaitForClockDown:
            App_WDT++;
            if (SCK == 0) {
            App_WDT = 0;
            MOSI = 0;
            App_stato = WaitForClockUp;
            }
            if (App_WDT>WDT_TIMEOUT) App_stato = Idle;
            break;


We can be in this state for two reasons ... we are at the beginning of a incoming byte or we are waiting for the n-th bit of the byte. Since we have to wait until clock is down we, at every iteration, increment the application watch dog timer and check if the clock is down.

If this condition is held for too much time something has probably gone wrong with the host. So when the watchdog timer passes a boundary threshhold value WDT_TIMEOUT, the application state machine is reset in the Idle state, clearing the receive bit number and the incoming byte buffer. We set WDT_TIMEOUT to 30000. if we count the longest path within the superloop we'll see that the number of instructions really executed in a superloop is limited (take a look to the assembly listing of the code) compared with 30000. We set this instruction number to around 15.

So 30000 App_WDT increments can be approximated as if there are 30000 *15 RISC instruction to be executed. Since each instruction is executed at Fclock/4 this timer will be expiring not less than 4/Fclock * 30000 * 15 = 450 msec @4 Mhz. So after at least 450 msec without any host activity the machine state will be reset in the idle state.

  • (Some experiments after the writing of this article shown that is safe to reduce WDT_TIMEOUT to 1000 to respond faster to interrupt events. In this case the device application reset is about 15 msec)

On receiving the right criteria (clock down in this case) the App_WDT is reset to 0. So in normal operation wdt is never triggered. When the application state machine is waiting for a byte coming from the host the state will be constantly ping-ponging from idle state to the WaitForClockDown state.

Now it's time to see the implementation of the host state machine. We will start with the simple Pinguino IDE implementation. First we should see how to connect the device board to the PIC32_DIY board. Look at this drawing:


7s 10.jpg


In the Pinguino IDE example given the pin assignment are as follow

  • Green Serial Clock from Host
  • Red +3.3 Power connection
  • Yellow MOSI data line from Host to Device
  • Orange MISO data line from Device to Host
  • White GND

In the Pinguino32 DIY layout it would be nice to have each connector with a spare +vcc and gnd pin available. This would make it much easier to connect two boards together. But for now we have to use what we have. However you can choose any pin you like on the host side. On the Device side the connector is, numbering from top to bottom, as in picture:

  • 1 – GND
  • 2 – MISO
  • 3 – MOSI
  • 4 – VCC
  • 5 – SCK

Pinguino IDE Host Routine

/*
*  7SDevice Demo Code - Host blocking implementation
*  Fabio Capozzi 2014  f.capozzi@tin.it
*
*  Hardware  : Pinguino PIC32_DIY  
*  Processor : PIC32MX250F128B
*  7S Device : See article on pinguino wiki
*              http://wiki.pinguino.cc/index.php/7SDevice
*
*  This code show how to use the SPI_Send custom routine to pass digit to 
*  2x7 segment display device  
*
*/

#define SDI 8		
#define SDO 7		
#define CLK 6		
 
  
void setup()
{
    pinMode(CLK,OUTPUT);       // Master Clock 
    pinMode(SDO,OUTPUT);       // Device MISO signal
    pinMode(SDI,INPUT);        // Device MOSI Signal
    pinMode(USERLED, OUTPUT); 
    digitalWrite(USERLED,LOW);
    
}

void Display_Send(unsigned char a, unsigned char b) {
 SPI_Send(a);
 SPI_Send(b);
}

void SPI_Send(unsigned char b) {
   typedef enum APP_STATO {
        Idle,
        WaitForDeviceReady,
        WaitForDeviceAck,
        WaitForReceiveOK,
        eot
   } t_stato ;
   t_stato stato;
   
   int Bit;
   unsigned char ch,aux;
   
   
   stato = Idle;
   Bit=0;
   ch = b;
   while(stato != eot) {
      switch(stato) {
         case Idle:
           digitalWrite(CLK,HIGH);
           while(digitalRead(SDI)!=HIGH);
           stato = WaitForDeviceReady;
           break;
         case WaitForDeviceReady:
           digitalWrite(CLK,LOW);
           while(digitalRead(SDI)!=LOW);
           aux = (ch << Bit) & 0x80;
           if (aux == 0x80) digitalWrite(SDO,HIGH);
           else digitalWrite(SDO,LOW);
           Bit++;
           digitalWrite(CLK,HIGH);
           stato = WaitForDeviceAck;
           break;
         case WaitForDeviceAck:
           while(digitalRead(SDI)!=HIGH);
           if (bit==8) stato = eot;
           else stato=WaitForDeviceReady;
           break;
         case eot:
           break;
      }
   }
}

void loop()
{ 
 
   int i,j;
   int flag=0;
   for (i=0;i<22;i++) 
    for (j=0;j<22;j++)
    { 
     Display_Send(i,j);
     delay(50);
   }
   toggle(USERLED);			// alternate ON and OFF
}

In most applications its enough to include in your project these two routines that are implemented as blocking code:

  • SPI_Send implement the host protocol to transfer a digit to device
  • Display_Send simply call SPI_Send for both digit passed.

As you can see there are no delays or other special attention for talking to the device.

A more complex Demo Host example

Should you want to use this device as a debug unit for displaying error codes probably the host protocol implementation has to be non blocking, because most other tasks running inside the main application code could be time sensitive and more important, so the display process should be a non intrusive low priority task. So I have written this tiny example as a guide to show you how to implement the non blocking host routine.

The code is written for PIC32_DIY PIC32MX250F128B

The conceptual idea is:

  • The timer1 is driven by the Peripheral Clock divided by 64 to generate an overflow every 100 usec. When overflow occurs the Display Interrupt Handler is called. This implements step by step the Host-Device Protocol State Machine that is scattered among multiple interrupt callback executions. At each interrupt call the state machine proceeds and changes state if the specific protocol conditions are met.
  • After what we've seen in the last section you might be able to read and understand the code on your own. Look to the processor datasheet for explanation of the register used. Here you will find the Application WatchDog timer I introduced before and a start-up timer to have a clean sync start. I made also the USERLED (RA0) gently flashing during operation.

This code is written in MPLAB-X. I included in the project tree the .ld file to allow .hex loading through a bootloader. You will find it in the .zip provided archive. To load the code with bootloader use:

  • ubw32 -w <file.hex> -n -r


/* 
 * File:   main.c
 * Author: Fabio Capozzi
 *
 * Created on 25 febbraio 2014, 10.52
 */

#include <stdio.h>
#include <stdlib.h>
#include <p32xxxx.h>
#include <plib.h>
#include <inttypes.h>

// PIC32MX250F128B Configuration Bit Settings

#include <xc.h>

// DEVCFG3
// USERID = No Setting
#pragma config PMDL1WAY = OFF           // Peripheral Module Disable Configuration (Allow multiple reconfigurations)
#pragma config IOL1WAY = OFF            // Peripheral Pin Select Configuration (Allow multiple reconfigurations)
#pragma config FUSBIDIO = ON            // USB USID Selection (Controlled by the USB Module)
#pragma config FVBUSONIO = ON           // USB VBUS ON Selection (Controlled by USB Module)

// DEVCFG2
#pragma config FPLLIDIV = DIV_2         // PLL Input Divider (2x Divider)
#pragma config FPLLMUL = MUL_20         // PLL Multiplier (20x Multiplier)
#pragma config UPLLIDIV = DIV_2         // USB PLL Input Divider (2x Divider)
#pragma config UPLLEN = OFF             // USB PLL Enable (Disabled and Bypassed)
#pragma config FPLLODIV = DIV_2         // System PLL Output Clock Divider (PLL Divide by 2)

// DEVCFG1
#pragma config FNOSC = PRIPLL           // Oscillator Selection Bits (Primary Osc w/PLL (XT+,HS+,EC+PLL))
#pragma config FSOSCEN = OFF            // Secondary Oscillator Enable (Disabled)
#pragma config IESO = OFF               // Internal/External Switch Over (Disabled)
#pragma config POSCMOD = HS             // Primary Oscillator Configuration (HS osc mode)
#pragma config OSCIOFNC = OFF           // CLKO Output Signal Active on the OSCO Pin (Disabled)
#pragma config FPBDIV = DIV_2           // Peripheral Clock Divisor (Pb_Clk is Sys_Clk/2)
#pragma config FCKSM = CSDCMD           // Clock Switching and Monitor Selection (Clock Switch Disable, FSCM Disabled)
#pragma config WDTPS = PS1048576        // Watchdog Timer Postscaler (1:1048576)
#pragma config WINDIS = OFF             // Watchdog Timer Window Enable (Watchdog Timer is in Non-Window Mode)
#pragma config FWDTEN = OFF             // Watchdog Timer Enable (WDT Disabled (SWDTEN Bit Controls))
#pragma config FWDTWINSZ = WISZ_25      // Watchdog Timer Window Size (Window Size is 25%)

// DEVCFG0
#pragma config JTAGEN = OFF             // JTAG Enable (JTAG Disabled)
#pragma config ICESEL = ICS_PGx2        // ICE/ICD Comm Channel Select (Communicate on PGEC2/PGED2)
#pragma config PWP = OFF                // Program Flash Write Protect (Disable)
#pragma config BWP = OFF                // Boot Flash Write Protect bit (Protection Disabled)
#pragma config CP = OFF                 // Code Protect (Protection Disabled)

#define SCK   LATBbits.LATB5
#define MISO  PORTBbits.RB4
#define MOSI  LATAbits.LATA4

#define DISPLAY_TIMEOUT 65
#define START_TIMER 1000

typedef enum  {
       Init,
       Idle,
       WaitForDeviceReady,
       WaitForDeviceAck,
       WaitForReceiveOK,
       eot
    } t_stato;

volatile uint32_t startup, counter=0;

volatile struct {
    uint8_t digitA;
    uint8_t digitB;
    t_stato stato;
    uint8_t Bit;
    uint8_t dgt;    // 0 for sending A  1 for sending B
    uint32_t Disp_WDT;
} Display;


void SystemInit() {

    // Preload Structure
      Display.digitA = 0x0F;
      Display.digitB = 0x0F;
      Display.dgt = 0;
      Display.Bit = 0;
      Display.stato = Init;
      Display.Disp_WDT = DISPLAY_TIMEOUT;
      startup = 0;

    // Disable Analog Peripheral
       ANSELA = 0;
       ANSELB = 0;

    // Set port A direction RA0, RA4(MOSI)output;
       LATA = 0x0010;
       TRISACLR = 0x0011;

    // Set port B direction RB5(SCK) output   RB4(MISO) input
       TRISBCLR = 0x0020;
       TRISBSET = 0x0010;
       LATB = 0x0030;

    // Configure timer1
    // PBCLOCK is 20 Mhz - timer1 increment at PBCLOCK/64 = 3.2 usec
    // Interrupt flag at 32 * 3.2 usec = 102.4 usec
       T1CONbits.ON = 0;          // Disable timer 1
       T1CONbits.TCS = 0;         // Set clock source to be PBCLCK
       T1CONbits.TCKPS = 2;       // Set prescaler to 1/64
       T1CONbits.TGATE = 0;       // Set Gated Timer accumulation disable
       PR1 = 32;                  // Preload timer compare register
       T1CONbits.ON = 1;          // Enable timer 1

       INTEnableSystemMultiVectoredInt();

    // Configure interrupts for Timer 1   IRQ 4   Vector 4
    // IECO<4> IFS0<4>  Pri IPC1<4:2>   SubPri IPC1<0:1>
       IEC0CLR = 0x0010;          // Disable interrupts for timer1
       IPC1CLR = 0x001C;          // Clear priority and subpriority
       IPC1SET = 0x0008;          // Set priority 2 Subpri 0 for timer 1
       IFS0CLR = 0x0010;          // Clear flag for IRQ4
       IEC0SET = 0x0010;          // Enable Interrupts for timer1
}


void __ISR(4,ipl2) Timer1_Display_Handler(void) {

    uint8_t ch,aux;
    if (Display.dgt) ch = Display.digitB;
    else ch = Display.digitA;
    switch(Display.stato) {
        case Init:
            startup++;
            if (startup > START_TIMER) Display.stato = Idle;
            break;
        case Idle :
            Display.Disp_WDT++;
            SCK = 1;
            if (MISO == 1)  { 
                Display.Disp_WDT = 0;
                Display.stato = WaitForDeviceReady;
            }
            if (Display.Disp_WDT > DISPLAY_TIMEOUT) {
                Display.Bit = 0;
                Display.dgt = 0;
                Display.stato = Idle;
                Display.Disp_WDT = 0;
            }
            break;
        case WaitForDeviceReady :
            Display.Disp_WDT++;
            SCK = 0;
            if (MISO == 0) {
                Display.Disp_WDT = 0;
                aux = (ch << Display.Bit) & 0x80;
                if (aux == 0x80) MOSI = 1;
                else MOSI = 0;
                Display.Bit++;
                SCK = 1;
                Display.stato = WaitForDeviceAck;
            }
            if (Display.Disp_WDT > DISPLAY_TIMEOUT) {
                Display.Bit = 0;
                Display.dgt = 0;
                Display.stato = Idle;
                Display.Disp_WDT = 0;
            }
            break;
        case WaitForDeviceAck :
            Display.Disp_WDT++;
            if (MISO == 1) {
                Display.Disp_WDT = 0;
                if (Display.Bit == 8) Display.stato = eot;
                else Display.stato = WaitForDeviceReady;
            }
            if (Display.Disp_WDT > DISPLAY_TIMEOUT) {
                Display.Bit = 0;
                Display.dgt = 0;
                Display.stato = Idle;
                Display.Disp_WDT = 0;
            }
            break;
        case eot:
            Display.Bit = 0;
            Display.dgt = !Display.dgt;
            Display.stato = Idle;
            Display.Disp_WDT = 0;
            break;
    }

    counter++;
    if (counter == 750) {
        LATAINV = 0x0001;
        counter = 0;
    }

    IFS0CLR = 0x0010;
}

int main(int argc, char** argv) {

    int i,j,k;
    SystemInit();
    while (1) {
        for (i=0;i<10;i++)
        for (j=0;j<10;j++) {
             Display.digitA = i;
             Display.digitB = j;
             for (k=0;k<500000;k++);
        }
    }

    return (EXIT_SUCCESS);
}

As a final part let now do some testing and measures.

All parts of this article