25 Ekim 2017 Çarşamba

Generating Sinewave using PWM

I found two references for this project Roman Black's 1kHz generator pages and Microchip app note AN1523. Both ideas are the same to create a PWM pulse train such that pulse width will be proportional to sinewave amplitude. A filter required to create the sine wave. I used Roman Black's simple filter instead of AN1523 Salen Key Filter.

I have 40 samples on a single 1kHz=1000us sinewave. so  sample period will be 1000us/40=25us. 25us will be TMR2 period.

Circuit works fine and creates a sinewave but the period was 6ms I used Roman Black table by changing TMR2 period to 20us  and 50 samples, but period was 9ms for this case. I checked 50usx20 samples and noticed that same pulse repeats 3 times then changes to new duty cycle. It means that during ISR processing (changing CCPR2L value) 3 to 6 cycle passes before changing the duty cycle. Maybe we can avoid this using higher freq external oscillator. 25MHz external clock decreased the period to 4ms.

Thus I decreased the frequency to 100Hz and 20 samples. It makes 500us timer2 period. I can reach to this range by using 1:16 prescaler. 20 sample values are the same as for 50usx20 table. Note that table values are related to number of samples and duty range you want to work. Finally, I modified the filter RC values to have 100Hz=1/(2*pi*R*C)


Proteus: PIC24k20 PWM output RC1 is connected to the filter.



1Khz was not achivable with this setup but 100Hz was ok with some ripples. We can play with the filter to reduce ripples.





MCC: Internal 16MHz oscillator was used. CCP2 is selected for PWM operation. CCP2 uses TMR2. Timer2 period is adjusted to 25us. 1:1 pre and post scalers are selected. we want to use PWM between 10-90% duty cycle. 




XC8: First we will create a sinevalue array for 40 samples. I have prepared using an excel file. I added +1 to have positive sinewave amplitude:

1+sin(2.pi.f.t) 
f=1000hz  
t=0,25us,50us...975us 


we can check maximum value of CCPR2L by setting duty cycle 100% and press GENERATE, check pwm2.c CCPRL value. For our example it was 99. Thus we can use the table as it is. I changed duty cycle by setting CCPR2L. Another method is to use PWM2_LoadDutyValue() function but I noticed that it created some spikes probably due to processing of another function inside ISR. We have to finish our job inside ISR as quick as possible to catch next interrupt otherwise we may miss some cycles.   I also checked with AN1523 sine table which has different numbers and it works fine, too. 

Roman Black changes duty cycle inside main.c within a while loop. This did not work for me. Thus I put this function inside timer2 ISR tmr2.c  



void TMR2_DefaultInterruptHandler(void){
    // add your TMR2 interrupt custom code
    // or set custom function using TMR2_SetInterruptHandler()

     const unsigned char sinevalue[20] = {
        50,62,74,82,88,90,88,82,74,62,50,38,26,18,12,10,12,18,26,38
    };
   
    static unsigned char pwmstep=0;   

    CCPR2L=sinevalue[pwmstep];
    pwmstep=(pwmstep+1) % 20;
       
}

download the example

23 Ekim 2017 Pazartesi

IRIGB Time Code Display

Years ago, I have designed an IRIGB decoder with PIC16F88  using CCS compiler. I updated the code for XC8.

Proteus: First stage is a comparator to remove sine waves below 1.2V (two reference diodes). Second stage is a "missing pulse detector" to create train of pulses with 2ms, 5ms and 8ms DC level shift modulated (DCLS) IRIGB code. Once we have pulses we can measure pulse width using a PIC18F24K20 CCP2 and TIMER1.


MCC: 8MHz internal oscillator. CCP2 rising edge and interrupt. Timer1 works with CCP2,  no need for timer interrupt. 



XC8: inside ccp2.c we can add our code to measure pulse width. transition from rising to falling works only if I disable with CCP2CON=0x00; ccp_delta is pulse width. 

main.c has a switch statement counts hours, minutes and seconds according to pulse widths in IRIGB format.


-------ccp2.c--------------

void CCP2_CallBack(uint16_t capturedValue)
{
    // Add your code here
static int t1_rising_edge;  //define static to keep the value

if(CCP2CON==0x05)
  {
    t1_rising_edge = capturedValue;
    CCP2CON=0x00;
    CCP2CON=0x04;
   
  }
else
  {
   ccp_delta = capturedValue - t1_rising_edge;
   got_pulse_width = 1;
   CCP2CON=0x00;
   CCP2CON = 0x05;   
  }
    
}


download the example hex file and irigb.wav to feed the circuit. make 3volt amplitude.

17 Ekim 2017 Salı

Introduction to the C Programming Language

I found presentations of Masters 2013 conference at here and followed Introduction to the C Programming Language Class and LAB Manual prepared for XC16. Very useful reference. Collected and modified source codes of LAB subjects into a single XC8 project file. You can follow below subjects by commenting out related part inside main.c


  • LAB01: variables
  • LAB02: constants
  • LAB03: printf()
  • LAB04: operators
  • LAB05: if
  • LAB06: switch
  • LAB07: loops
  • LAB08: functions
  • LAB09: header files
  • LAB10: array
  • LAB11: pointer
  • LAB12: pointer arrays
  • LAB13: function pointers
  • LAB15: structure
  • LAB16: bitfields



16 Ekim 2017 Pazartesi

Learning C using UART1 Output Window

We can practice C code programming using UART1 output window of MPLAB.  http://microchipdeveloper.com/xc8:console-printing proposes the way to do it. I simplified this using MCC.

MCC: Start a new project for PIC18F24K20. add EUSART peripheral. enable transmit. redirect STDIO to USART.  that's all, 1MHz internal osc selected.

Project Properties-> Simulator -> Enable UART1 IO

these settings will be enough to redirect stdio to UART1 window. press GENERATE to create mcc files and write your first code inside main.c

printf("Hello World!\n");

press the debug button. it will run simulation, open UART1 Output and will print Hello World!

use cmd-L to clear the screen for the next run.









13 Ekim 2017 Cuma

Temperature Logger

We will combine thermometer and SD card examples to create a temperature logger.

Proteus: SD card is connected to SPI pins and we will use RA0 for ADC to read temperature from LM35.


MCC: Add MSSP and ADC peripherals. no interrupts. Select RA0 at pin manager for ADC. 







XC8:  add headers of SD card as in previous example.  First read the value convert  and save the result "t" string. Writing to SD card is the same as in SD card example. Just be careful on number of bytes, it must be same to the length of string "Measurement 000:000.0\r\n" which is 23 bytes, otherwise you will get garbage characters in your "temp_log.txt" file.  We use the same 2Mbytes image file "sdcard.ima" that we created in SD card example. "temp_log.txt" will be created if you run the simulation in Proteus. Dont forget to select "sdcard.ima" file in SD card properties.







#include "mcc_generated_files/mcc.h"
#include <stdio.h>
#include "ff.h"


FATFS FatFs; /* FatFs work area needed for each volume */
FIL Fil;  /* File object needed for each open file */


...


    char t[20];
    uint16_t cV = 0,i=0;
    float  tm;

    while (1)
    {
        // Add your application code
        i++;
        ADC_StartConversion(channel_AN0);
        while(!ADC_IsConversionDone());
        cV = ADC_GetConversionResult();
        tm=cV*500.0/65536;
        sprintf(t, "Measurement %3d:%3.1f \r\n", i,tm);
        
    UINT bw;

    if (f_mount(&FatFs, "", 1) == FR_OK) { /* Mount SD */

    if (f_open(&Fil, "temp_log.txt", FA_OPEN_ALWAYS | FA_READ | FA_WRITE) == FR_OK) { /* Open or create a file */

    if ((Fil.fsize != 0) && (f_lseek(&Fil, Fil.fsize) != FR_OK))  goto endSD; /* Jump to the end of the file */

        f_write(&Fil, t, 23, &bw); /* Write data to the file */
           
     endSD: f_close(&Fil);        /* Close the file */
    }
    }
    
    

    __delay_ms(1000);
    }
}

download the example

Adjustable Clock DS1307

We learned how to read and set DS1307 clock in our previous examples. We will develop an adjustable clock in this example. There are three buttons SET, UP, DOWN as you see on commercial digital clocks. SET button will select and blink  hour and minutes sequentially.  UP,DOWN buttons will increment or decrement hours or minutes.

Proteus: DS1307 with a crystal. LCD is connected to PORTB. RA0, RA1,RA2 are used for setting buttons.


MCC: nothing special. RA0,RA1,RA2 is set for input. select MSSP, I2C master, 1Khz. 1Mhz internal oscillator.





XC8: Enable global and peripheral interrupts. First set clock to 03:02:01 we will not use day, month year but I set them, too. read DS1307 and print to LCD. In case of SET button pressed, set seconds to zero and check UP/DOWN buttons. If up/down buttons are pressed change hour/minutes. Second SET button will switch to minutes and third SET button will set the clock.

I converted DS1307 BCD values to decimal for math operations and turned back to BCD again for register setting.  

    
    unsigned char s1[10], s2[10];
    uint8_t hr[2]={1,0}, readValue[7], writeValue[8]={0,1,2,3,4,5,6,7};
    uint8_t c,ho,mi,se,da,mo,ye;
    I2C_MESSAGE_STATUS    I2C_status;
    
    I2C_MasterWrite( &writeValue, 8, 0b1101000, &I2C_status); //sets pointer address and write data
    while (I2C_MESSAGE_PENDING  == I2C_status ); 

    
    OpenXLCD(FOUR_BIT & LINES_5X7);
    WriteCmdXLCD(DON&CURSOR_OFF&BLINK_OFF); 
    

    while (1)
    {
        // Add your application code
        
    I2C_MasterWrite( 0, 1, 0b1101000, &I2C_status); //set pointer address to zero
    while (I2C_MESSAGE_PENDING  == I2C_status);
    I2C_MasterRead( &readValue, 7, 0b1101000, &I2C_status); // read 7 bytes starting from zero
    while (I2C_MESSAGE_PENDING  == I2C_status );

    sprintf(s1, "%02X:%02X:%02X", readValue[2],readValue[1],readValue[0]);             
        
    SetDDRamAddr(0x01); // clear LCD          
    SetDDRamAddr(0x00); // first line 
    putrsXLCD(s1);        
    SetDDRamAddr(0x40); // goto second line
    putrsXLCD(s2);
    
    writeValue[0]=0;  //pointer address of zero 
    writeValue[1]=readValue[0]; //sec
    writeValue[2]=readValue[1]; //min
    writeValue[3]=readValue[2]; //hour
    writeValue[4]=readValue[3]; //weekday
    writeValue[5]=readValue[4]; //day
    writeValue[6]=readValue[5]; //month
    writeValue[7]=readValue[6]; //year
        
    ho=readValue[2]; mi=readValue[1]; se=readValue[0];
    da=readValue[4]; mo=readValue[5]; ye=readValue[6];
    
    c=0;
    if (IO_RA0_GetValue()==1) { //set pressed
    c=(c+1) % 3;   // set value 0=no-set,1=hour,2=minutes
    
    while (c==1){ //set hours    

        
    writeValue[1]=0; //set seconds to zero
    
    ho=(ho>>4)*10+(ho&0x0F); //bcd2dec
    if (IO_RA1_GetValue()==1) ho=(ho+1) % 24; //up
    if (IO_RA2_GetValue()==1) ho=(ho+23) % 24; //down
    ho=((ho/10)<<4)+(ho%10); //dec2bcd
    writeValue[3]=ho;
    
    sprintf(s1, "%02X:%02X:%02X", ho,mi,0);             
    SetDDRamAddr(0x01); // clear LCD          
    SetDDRamAddr(0x00); // first line 
    putrsXLCD(s1);        
    __delay_ms(100);
    
    sprintf(s1, "  :%02X:%02X",mi,0);             
    SetDDRamAddr(0x01); // clear LCD          
    SetDDRamAddr(0x00); // first line 
    putrsXLCD(s1);        
    __delay_ms(100);
    
    
    if (IO_RA0_GetValue()==1) { //set pressed
        c=(c+1) % 3;
        I2C_MasterWrite( &writeValue, 8, 0b1101000, &I2C_status); //sets pointer address and write data
        while (I2C_MESSAGE_PENDING  == I2C_status ); 
    }
    }

    while (c==2){ //set minutes       
    mi=(mi>>4)*10+(mi&0x0F); //bcd2dec
    if (IO_RA1_GetValue()==1) mi=(mi+1) % 60; //up
    if (IO_RA2_GetValue()==1) mi=(mi+59) % 60; //down
    mi=((mi/10)<<4)+(mi%10); //dec2bcd
    writeValue[2]=mi; 
    
    sprintf(s1, "%02X:%02X:%02X", ho,mi,0);             
    SetDDRamAddr(0x01); // clear LCD          
    SetDDRamAddr(0x00); // first line 
    putrsXLCD(s1);        
    __delay_ms(100);
    
    sprintf(s1, "%02X:  :%02X",ho,0);             
    SetDDRamAddr(0x01); // clear LCD          
    SetDDRamAddr(0x00); // first line 
    putrsXLCD(s1);        
    __delay_ms(100);
    
   if (IO_RA0_GetValue()==1){ 
        c=(c+1) % 3;
        I2C_MasterWrite( &writeValue, 8, 0b1101000, &I2C_status); //sets pointer address and write data
        while (I2C_MESSAGE_PENDING  == I2C_status ); 

    }

    }    
    }
        
        __delay_ms(1000);
    }
    



9 Ekim 2017 Pazartesi

SD Card Example

We will create a text file in an SD card and write some text inside. Microchip MDD legacy library did not work for XC8 as expected. It is for XC16 and XC32. I tried to modify header files but I gave up. I followed the example at http://www.studentcompanion.net/en/interfacing-sd-card-with-pic-microcontroller-xc8/  which is using  Chan's FatFs file system. The example was for 18F45K22, my old proteus was not supporting this PIC thus I used PIC18F45K20. It needs a little modification but working well.

Proteus: PIC18F45K20 connected to an SD card using RC3, RC4, RC5 ports and RC0 is for ChipSelect CS.  To create image file for SD card I used winimage. Select custom format, FAT16, 4096 sectors for 2MByte. Save the image give a name "sdcard2M.ima" and select this image in Proteus. If you run the simulation. "test.txt" will be created inside the image. Stop the simulation and open the image file again. You will see the "test.txt file",  we extract and look inside.






MCC: 1MHz internal oscillator as usual. select MSSP peripheral for SPI Master. No interrupts. Rename RC0 as SD_CS; this is important and used in header files. 





XC8: There are some modifications at diskio.h
#define _SD_SPI 0  // we are using PIC18F45K20 and this has one SPI module

#else

#define sd_init() SPI_Initialize()
#define sd_open() SPI_Initialize()  //SPI_Open() does not exist, thus change it to SPI_Initialize()
#define sd_tx(d) SPI_Exchange8bit(d)
#define sd_rx() SPI_Exchange8bit(0xFF)

there are no changes at other files.  main.c is as below, creates "test.txt" file and writes "Hello world! ..." sentence to this file.

#include "mcc_generated_files/mcc.h"
#include "ff.h"

FATFS FatFs; /* FatFs work area needed for each volume */
FIL Fil; /* File object needed for each open file */


/*
                         Main application
 */
void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();
    
        
    UINT bw;

    if (f_mount(&FatFs, "", 1) == FR_OK) { /* Mount SD */

if (f_open(&Fil, "test.txt", FA_OPEN_ALWAYS | FA_READ | FA_WRITE) == FR_OK) { /* Open or create a file */

if ((Fil.fsize != 0) && (f_lseek(&Fil, Fil.fsize) != FR_OK)) goto endSD; /* Jump to the end of the file */

f_write(&Fil, "Hello world! This is text message written to sd card\r\n", 54, &bw); /* Write data to the file */
           
endSD: f_close(&Fil); /* Close the file */
}
}


     

    while (1)
    {
        // Add your application code
    }
}




4 Ekim 2017 Çarşamba

Measure Period and Duty Cycle using CCP2 and TIMER1

We will use the same circuit to measure duty cycle together with the period.  Period is the time difference between two rising edges (t3-t1). Duty cycle is the difference between the first rising edge and next falling edge (t2-t1).


MCC: MCC settings are the same as previous example, 1MHz internal osc, 1:1 prescaler, CCP interrupt enabled for every rising edge.

XC8:   while(!CCP2IF) statement to capture the interrupt case  inside the main.c did not work (there are many examples of it in internet). Thus I put conditions inside ccp2.c ISR  such as
...
extern uint16_t  t1,t2,t3;
int c=0;

void CCP2_CallBack(uint16_t capturedValue)
{
    // Add your code here

       
    if ( c==2) {
      t3=capturedValue;  
      CCP2CON=0x00;   //second rising edge
      c=0;        
    } else if (c==1) {
      t2=capturedValue; // falling edge
      CCP2CON=0x00;
      CCP2CON=0x05;
      c=2;
    } else {    
      t1=capturedValue;  //first rising edge
      CCP2CON=0x00;
      CCP2CON=0x04;
      c=1;        
    }
   
}

transition from rising to falling edge did not work. Thus I disabled interrupt first and enabled with new state. t1,t2,t3 are defined as extern to use in main.c, c integer is local for toggling between cases.

statement inside main.c enables rising edge, waits for interrupt stop then calculated duty and period, prints on LCD

        CCP2CON=0x05;  
        while(CCP2CON!=0);
        d=t2-t1;
        if (d<0) d=d+65536;
        d=d*4.0/1000.0;
        f=t3-t1;
        if (f<0) f=f+65536;
        f=f*4.0/1000.0;
        
        sprintf(s1,"%3.3fms", f); 
        sprintf(s2,"%3.3fms", d); 

This code was able to measure between 1-262 ms with 1MHZ oscillator. Below 1ms yields multiple captured values and wrong results. We can increase the osc frequency (8MHZ or 16MHz) or use 16th rising edge etc for lower values. we can add timer1 overflow interrupt to measure values above 262ms, too. 


2 Ekim 2017 Pazartesi

Measure period using CCP2 and TIMER1

In this example, we will measure period of a pulse signal. In theory it is simple, measure the time difference between two rising/falling edges. I saw two approaches; first to do this within main.c, like in http://www.electronicwings.com/pic/pic18f4550-timer-capture. But it did not work for me specially in high frequencies=low periods. If you measure high frequency a lot of CCP interrupt occurs and it seems blocking the PIC.


while(1) { while(!(PIR1bits.CCP1IF)); /* Wait for interrupt flag which is generated when edge is detected*/ data1=CCPR1; /* Copy count of 1st edge detected*/ PIR1bits.CCP1IF=0; while(!(PIR1bits.CCP1IF)); /* Wait for interrupt flag which is generated when edge is detected*/ data2=CCPR1; /* Copy count of 2nd edge detected*/ PIR1bits.CCP1IF=0;


Thus, I wanted to capture only two rising edge, not more, at every 1 second. I
handled it within interrupt routine.

Proteus: Connect a pulse generator to the RC1/CCP2A. I opened the debugger to see
how many CCP event occurs.


MCC:  Add TIMER and CCP peripherals. Select rising edge. CCP2 interrupt automatically selected. 1MHZ internal osc was used. Use 1:1 prescaler at timer. no timer interrupt.





XC8:   added the code inside ISR. I used c=0, c=1 tokens to take only two samples, then disable the CCPCON=0x00;  interrupt. I enable within main.c again. Period is measured inside main.c. Be careful, you have to define t1 variable as extern inside ISR and take them to top at main.c  

It is working good between 500us-262ms (timer1 range without overflow). but again below 500us, more than two CCP interrupts occurs and measurement may give wrong results. I could not solve it yet. Any comments are welcome. obviously using higher freq oscillator will decrease this value.

------ccp2.c---------
#include <xc.h>
#include "ccp2.h"

extern double t1;
int c;
...

void CCP2_CaptureISR(void)
{
    CCP_PERIOD_REG_T module;

    // Clear the CCP2 interrupt flag
    PIR2bits.CCP2IF = 0;
    
    // Copy captured value.
    module.ccpr2l = CCPR2L;
    module.ccpr2h = CCPR2H;
    
    // Return 16bit captured value
    CCP2_CallBack(module.ccpr2_16Bit);
}

void CCP2_CallBack(uint16_t capturedValue)
{
    // Add your code here
    

    if ( c==1) {        
        t2=capturedValue;    //second  rising edge
        CCP2CON=0x00;    //disable after second rising edge
        c=0;        
    } else  {    
        t1=capturedValue;    //first rising edge
        c=1;
    }


    }


-----main.c-------

#include "mcc_generated_files/mcc.h"
#include "delays.h"
#include "myxlcd.h"
#include <stdio.h>

double  pr,t1,t2;
int c;
/*
                         Main application
 */
void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable();


    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable();

   unsigned char s1[10], s2[10];

   
    OpenXLCD(FOUR_BIT & LINES_5X7);
    WriteCmdXLCD(DON&CURSOR_OFF&BLINK_OFF); 
    
    
    while (1)
    {
        // Add your application code


         CCP2CON=0x05;  //enable CCP interrupt, which will be disabled inside ccp2.c        
        while (CCP2CON ==0x05); // wait for the end of interrupt
        pr=t2-t1;   //time difference
        if (pr<0) pr=pr+65536;  //in case of timer1 overflow
        pr=pr*4.0/1000.0;
     
        sprintf(s1,"%3.3fms", pr);   
  

        while(BusyXLCD());
        SetDDRamAddr(0x01); // clear LCD        
        putrsXLCD("P=");putrsXLCD(s1);
        SetDDRamAddr(0x40); // goto second line
        while(BusyXLCD());
        putrsXLCD("D=");putrsXLCD(s2);
        __delay_ms(1000);
        
    }
}

I checked with measurement inside main.c using (8MHz clock, thus 0.5 multiplier) this works fine upto 10ms but gives double or wrong values below. I noticed that  while (PIR2bits.CCP2IF==0);  may not work well because interrupts flowing continuously at debugger which causes high CPU load.

   while (1)
    {
        // Add your application code


        while (PIR2bits.CCP2IF==0); //wait until interrupt
        t1=CCPR2H*256+CCPR2L;

        while (PIR2bits.CCP2IF==0);
        t2=CCPR2H * 256 + CCPR2L;
     
        pr=t2-t1;
        if (pr<0) pr=pr+65536;
        pr=pr*0.5/1000.0;

I also checked the algorithm at https://web.sonoma.edu/users/f/farahman/sonoma/courses/es310/lectures/chapter_11_timers.pdf but it did not work for me also. I read some comments that resetting timer for period measurement will not give accurate results, instead measure time difference between two CCP rising edge interrupt advised as a better approach