Electronics
PIC tutorial 1 - Counter
- Details
- Category: PIC Tutorials
- Hits: 3154
Fig. 1 Board 3 with 8 leds output
Description:
This is the first tutorial in the serie, which covers a few basics.
It covers basic output, delay, timers and interrupts. You will need a processor board, a power supply and the hexadecimal display or 8 leds output board on port B. On some processor boards, you could use another port. (Port A doesn't always have the 8 bits available).
The tutorial is split in 3 parts:
- basic delay routine (built-in with CCS compiler), which also covers the libraries for the processor boards.
- use of timer and interruption for the delays.
- use of a time symbol library, that can be used with any project where timing needs to be managed.
The boards can be powered by one of the PSU boards (powered from USB port), or between 2.5 to 5V.
Requirements:
Processor boardAny board (board 3 and 4 used in examples)
Extension(s)8 leds output
or hexadecimal display
Power supplyPSU 1, 2 or 3. Depends on board used.
LanguageCCS C
ProgrammerPicKIT 2
Program:
Version A
This version setup the time to wait using the CCS compiler delay routines, determined with the crystal frequency.
#define board_id 2 // Variable board_id needs to be defined with the board number used for the tutorial.
#include "../lib/Boards/mainboard.c" // Load board module
BYTE leds; // variable to store data to output on port B
void main() {
boot_up(); // Initialise the board
leds = 0; // reset counter to 0
while (true) { // main loop
output_b(leds); // output variable led on port b
leds ++; // increment it
delay_ms(250); // wait 250ms
}
}
This is a very small program. The variables are declared using types defined in a specific file, so don't look like the classic CCS types. This is to make the program easier to port to other compilers. The program loads some configuration files for the selected processor board. The variable leds hold the counter to output on port B.
The main program initialise the board with the function boot_up, reset the counter, and enter a loop that output the counter on port B, increments the counter and wait 250ms before looping again.
The function delay_ms is a built-in function of the CCS compiler that waits for a number of ms before returning. To be accurate, the compiler needs to know the clock frequency. This is defined in the mainboard file. It is easy to change the time between increments by changing the value passed to the delay_ms function. Each board having different clock speed and processors, they each have their own 'library' file. The variable board_id tells the compiler which library to use.
This program introduce the define feature of the CCS compiler. It is used to define variable, and can be used in various places in the program. When the program is compiled, the variable is replaced with its definition. The definition can be a macro for example. It is a very useful feature.
The program define the variable board_id. It tells the compiler which processor board is used to run the project. It is used to tell the program how to configure the delay functions. It is setup as 3, because I use board 3 to run the tutorial.
To use a different board, just change the board_id value and recompile. That's it.
It then loads the ../lib/Boards/mainboard.c file. This file loads the appropriate definition file for the processor board used. Depending of the board_id value, it loads a file that configure the hardware used, and loads a boot_up function that will be used later to initialise the board.
#ifndef board_id // Board ID not defined, throw error
#error "variable board_id not defined"
#else
#if (board_id == 1 ) // Board 1 library (PIC 16F819)
#include "../lib/Boards/mb16F819.c"
#elif (board_id == 2) // Board 2 library (PIC 16F628)
#include "../lib/Boards/mb16F628.c"
#elif (board_id == 3) // Board 3 library (PIC 16F873A)
#include "../lib/Boards/mb16F87x.c"
#elif (board_id == 4) // Board 4 library (PIC 18F2320)
#include "../lib/Boards/mb18F2320.c"
#elif (board_id == 5) // Board 5 library (PIC 18F26J11)
#include "../lib/Boards/mb18F26J11.c"
#elif (board_id == 6) // Board 6 library (PIC 18F26J50)
#include "../lib/Boards/mb18F2xJ50.c"
#else // Board ID unknown, throw error with board not supported
#error "Board board_id not supported"
#endif
#include "../lib/Common/TypeDefs.c"
#include "../lib/Common/PIC.h"
#endif
Mainboard.c checks that board_id is defined, else it throws an error. Depending on board_id, it loads the corresponding file for the specific processor. If the board id is not supported, it throws an error. It also loads a library that defines data types, and a library with PIC registers details. It make porting to other compiler (like Microchip C18).
The board used for this tutorial is board 3, with a PIC 16F873A processor. This is the file I will concentrate on: mb16F87x.c.
#include <16F873A.h>
#device = *=16 adc=8
#define CRYSTAL_FREQ 16000000
#if ( ( CRYSTAL_FREQ < 4000000) || ( CRYSTAL_FREQ > 20000000 ) )
#error "CRYSTAL FREQ" not defined between 4000000 and 20000000
#endif
#FUSES HS, PUT, NOWDT, BROWNOUT, NOLVP, NOCPD, NODEBUG
#ifdef PROTECT_CODE
#FUSES PROTECT
#else
#FUSES NOPROTECT // Code not protected from reading
#endif
#use delay ( clock = CRYSTAL_FREQ )
#use standard_io ( A )
#use standard_io ( B )
#use standard_io ( C )
void boot_up() {
setup_adc_ports(NO_ANALOGS);
setup_adc(ADC_OFF);
setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
setup_timer_1(T1_DISABLED);
setup_timer_2(T2_DISABLED,0,1);
setup_comparator(NC_NC_NC_NC);
setup_vref(FALSE);
}
Firstly, the program loads a file to define the device used for the project, 16F873A in this case.
It defines pointers as 16 bits variables, and the return value from analog to digital conversion to an 8 bits value.
The crystal oscillator frequency is then set, and checked that it is within acceptable values for that device (Between 4 and 20 MHz).
The fuses to be set in the processor are then defined: high speed crystal oscillator, no watch dog timer... Each processor will have its own set of fuses.
We then setup the delay functionnality: it tells the compiler the speed of the clock, to configure the built-in timing functions. This first version uses a built-in function (delay_ms) to wait between each increment. It waits a number of millisecond before returning. It is critical to define properly the clock so the timing is accurate.
We then setup the way the compiler will process inputs/outputs.
The boot_up() function is defined for every processor board. It is called in the main program to initialise it properly. It sets up various components of the processor.
The files for each processor are very similar. Using different ones allows to setup specific processor functionnality, without changing the main program.
Compile the project and program the target with a PicKIT 2 programmer for example. Connect the processor board to the 8 leds output on port B and power up the boards.
The leds will start blinking every 250ms, or 4 times a second, following a binary counter pattern.
If you use the hexadecimal display, it will display the value of the counter as an hex value.
This way works, but the processor spends a lot of time just counting... To make it more efficient, lets look at timers and interrupts in version B.
Fig. 2 Board 4 with hexa display
Version B
This version will use a timer to generate an interrupt repeatedly. I will then increment the counter and output its value on port B.
Between interrupts from the timer, the processor can go into idle mode to save energy, depending on the processor, or do other tasks.
In this version, the processor is not busy simply counting, it uses one of its hardware counter to do it, it is then free to do any other task.
For this version, I use processor board 4, with a PIC 18F2320, just to show how easy it is to compile a tutorial program on another processor board.
The most important documentation for the tutorials is the datasheet of the various processors used. Some components of a processor can be implemented a bit differently from one processor to another. The variable board_id in the program can be used to determine how to implement a component based on the processor board.
//* define CPU board used
//* 1: 16F819, 2: 16F628, 3: 16F873A, 4: 18F2320, 5: 18F26J11
#define board_id 4 // Variable board_id needs to be defined with the board number used for the tutorial.
#include "../lib/Boards/mainboard.c" // Load board module
#define inc_delay 250 // Delay in ms between each increment
#define int_count 10 // Number of interrupts between increments of counter
#define Timer1_Reload (65536-((CRYSTAL_FREQ/(4*8*1000))*inc_delay/int_count)) // (Timer 1 incremented every 1.6uS) 49910 for 25ms
BYTE leds; // variable to store data to output on port B
BYTE countInt=0; // count number of interrupt and initialise it to 0
#priority timer1 // set interrupt from timer 1 with a high priority
#int_timer1 // routine executed when an interrupt from timer 1 occurs
void Timer1Interrupt ( void ) {
set_timer1(Timer1_Reload); // reload timer 1 with predetermined value
countInt ++; // increment interrupt counter
if (countInt == int_count) { // if desired number of interrupt counted
leds ++; // increment counter to output
countInt = 0; // reset interrupt counter
}
}
void main() {
boot_up(); // Initialise the board
leds = 0; // reset counter to 0
setup_timer_1 ( T1_INTERNAL | T1_DIV_BY_8 ); // setup timer 1 to use internal clock with prescaler to 8
set_timer1(Timer1_Reload); // setup timer 1 value with precalculated value
enable_interrupts ( INT_TIMER1 ); // enable Timer1 interrupt
enable_interrupts ( GLOBAL ); // enable all interrupts
while (true) { // main loop
output_b(leds); // output variable led on port b
#if defined(__PCH__) // if 18F family
setup_oscillator(OSC_IDLE_MODE); // set idle bit
sleep(); // go to idle mode (power save)
#endif
}
}
Timer 1 is used to generate interrupts. It is a 16 bits counter, available on all processors. Timer 1 can be setup to be incremented by the internal clock (Crystal frequency / 4) through a prescaler. It counts until 0xFFFF, and overflows to 0, which can generate an interrupt. We can preset the value of the counter for timer 1 so we know exactly how long it will take between interrupts.
The internal clock is at crystal frequency / 4, which is 5MHz with a 20MHz oscillator. If we setup the prescaler to 8 (timer 1 incremented every 8 internal clock signal), we increment timer 1 at a rate of 625KHz, or every 1.6 us.
The formula to determine the time between every increment for timer 1 is: T = (4*PRESCALER)/CRYSTAL_FREQ
The formula for another timer might be different (Timer 2 on the PIC 18F2320 has a pre and post scaler for example)
We want to increment the counter to ouput on port B every 250 ms. Timer 1 would need to count up to 250 000/1.6 = 156250. Timer 1 is a 16 bits counter, which can only count up to 65535, or 104.8576ms. We need a bit of help with the software: we will generate an interrupt every 1/10th of the required time, and increment the counter every 10 interrupts.
In the program, I defined values for the delay between the increments, and the number of interrupts to generate between them. Timer1_Reload is defined as a macro, which is the formula to calculate the value to preset timer 1 to generate the interrupt at the right time, just to show how practical the #define command can be.
Version C
This version will use a time symbol library. It is very helpfull when timing needs to be managed. Like version B, it uses a timer to generate interrupts, but also add functions to manage timing.
For this version, I use processor board 4 again, with a PIC 18F2320, but the library works with both 16 or 18 family.
The library is based on a library from Microchip, provided as part of the Microchip Application Library. It is changed to compile properly on the CCS compiler and adds support for the 16 family.
#define board_id 4 // Variable board_id needs to be defined with the board number used for the tutorial.
#include "../lib/Boards/mainboard.c" // Load board module
#include "../lib/Common/SymbolTime.c"
BYTE leds; // variable to store data to output on port B
TICK_T time1, time2;
void main() {
boot_up(); // Initialise the board
InitSymbolTimer();
enable_interrupts(global);
leds = 0; // reset counter to 0
output_b(leds);
time1 = TickGet();
while (true) { // main loop
time2 = TickGet();
if (TickGetDiff(time2, time1) > ONE_SECOND/4) {
time1 = time2;
leds ++; // increment it
output_b(leds); // output variable led on port b
}
}
}
The program is fairly short too, like version A. The heart of it is actually the library SymbolTime.c.
The library uses timer 0 to generate interrupts, and counts "ticks". The program checks the difference between to reads of ticks, and update the counter if it is the desired intervall.
SymbolTime.c implements a counter, incremented at every interrupts from timer 0. It is a 4 bytes (32 bits) value, incremented every tick (or timer 0 interrupt). The interrupt would typically occurs every 16 us, which gives the ability to measure time of up to 19 hours (16us * 2^32).
The side effect of using the library is that timer 0 is used for time management, and not available for other tasks. Global interrupt needs to be enabled.
#ifndef _SYMBOL_TIME_LIB
#define _SYMBOL_TIME_LIB
#include "../lib/Common/SymbolTime.h"
volatile BYTE timerExtension1,timerExtension2;
#if (defined(__PCM__))
volatile BYTE timerExtension3;
#endif
void InitSymbolTimer() {
#if (defined(__PCH__) || defined(__PCM__)) // 18F or 16F familly
setup_timer_0 (RTCC_INTERNAL | CLOCK_DIVIDER_SETTING);
#priority rtcc
clear_interrupt(INT_RTCC);
enable_interrupts(INT_RTCC);
timerExtension1 = 0;
timerExtension2 = 0;
#if (defined(__PCM__))
timerExtension3 = 0;
#endif
#else
#error "Symbol timer implementation required for stack usage."
#endif
}
TICK_T TickGet(void) {
TICK_T currentTime;
#if defined(__PCM__)
BYTE failureCounter;
disable_interrupts(INT_RTCC);
currentTime.bytes.b1 = 0; // copy the byte extension
currentTime.bytes.b2 = 0;
currentTime.bytes.b3 = 0;
failureCounter = 0;
do { // read the timer value
currentTime.bytes.b0 = get_rtcc();
} while( currentTime.bytes.b0 == 0xFF && failureCounter++ < 3 );
//if an interrupt occured after IE = 0, then we need to figure out if it was before or after we read TMR0L
if(TMR0IF) { //if we read TMR0 after the rollover that caused the interrupt flag then we need to increment the 2nd byte
currentTime.bytes.b1++;
if(timerExtension1 == 0xFF) {
currentTime.bytes.b2++;
}
}
currentTime.bytes.b1 += timerExtension1; // copy the byte extension
currentTime.bytes.b2 += timerExtension2;
currentTime.bytes.b3 += timerExtension3;
enable_interrupts(INT_RTCC); // enable the timer interrupts
#elif defined(__PCH__)
BYTE failureCounter;
disable_interrupts(INT_RTCC);
currentTime.bytes.b2 = 0; // copy the byte extension
currentTime.bytes.b3 = 0;
failureCounter = 0;
do { // read the timer value
currentTime.words.w0 = get_rtcc();
} while( currentTime.words.w0 == 0xFFFF && failureCounter++ < 3 );
//if an interrupt occured after IE = 0, then we need to figure out if it was before or after we read TMR0L
if(TMR0IF) { //if we read TMR0L after the rollover that caused the interrupt flag then we need to increment the 3rd byte
currentTime.bytes.b2++;
if(timerExtension1 == 0xFF) {
currentTime.bytes.b3++;
}
}
currentTime.bytes.b2 += timerExtension1; // copy the byte extension
currentTime.bytes.b3 += timerExtension2;
enable_interrupts(INT_RTCC);
#else
#error "Symbol timer implementation required for stack usage."
#endif
return currentTime;
}
#if (defined(__PCH__) || defined(__PCM__))
#int_rtcc void interruptSymbolTimer(void) {
if(TMR0IF) { // check to see if the symbol timer overflowed
clear_interrupt(INT_RTCC);
timerExtension1++;
if(timerExtension1 == 0) {
timerExtension2++;
#if (defined(__PCM__))
if(timerExtension2 == 0) {
timerExtension3++;
}
#endif
}
}
}
#endif
#endif
The library first load its header file: SymbolTime.h. This file defines various variables based on oscillator speed. It also defines the numbers of ticks (interrupts from timer 0) that would be in a set time (1 second, 1 ms, ...)
The 3 functions available are:
- InitSymbolTimer: this function initisliase timer 0, the variables and interrupts used by the library. It needs to be called before any other function can be active.
- TickGet: returns the time tick value, as the data type TICK_T, which is a 32 bits value.
- TickGetDiff: this macro returns the difference between 2 time readings.
The interrupt handler for timer 0 (or RTCC) is also defined, and increment counters.
This library makes it very easy to manage timing, and time events, or difference between them.
Files and links:
Tutorial 1 source files.Processor board 3.
Processor board 4.
8 leds output board.
Hexa decimal display board.
PIC tutorial 2 - SPI DAC
- Details
- Category: PIC Tutorials
- Hits: 4002
Fig. 1 Board 4 with Dual 8 bits DAC
Description:
For the second tutorial, we will cover basic SPI communication, and how to connect SPI devices to a PIC board.
It ouputs a analog voltage on a digital to analog converter, using the Dual 8 bits DAC, which has an SPI interface.
The SPI interface across all tutorials is the same, and connects to boards with 8 pins: power supply, Master In Slave Out, Master Out Slave In, Clock and 3 Chip Select lines. The 3 CS lines can be used to connect up to 3 SPI devices on the bus, or allow a device to use external interrupts.
The tutorial will update the output voltage of both DAC on the board at every interrupt by timer 0.
The boards can be powered by one of the PSU boards (powered from USB port), or between 2.5 to 5V.
Requirements:
Processor boardAny board (board 1 in example)
Ideally with hardware SPI interface
Extension(s)Dual 8 bits DAC
Power supplyPSU 1, 2 or 3. Depends on board used.
LanguageCCS C
ProgrammerPicKIT 2
Program:
#define board_id 4
#define CS_DAC CS_SPI0 // Chip Select line used/configured on the DAC board
#include "../lib/Boards/mainboard.c"
#include "../lib/Analog/MAX549.c" // load MAX549 library
#priority RTCC
BYTE DAC1=0,DAC2=0; // Values for DAC A and DAC B
#int_rtcc // timer 0 (or RTCC) interrupt
void Timer0Interrupt ( void ) {
DAC1++;
DAC2--;
}
void main() {
boot_up(); // Initialise board
Init_SPI(); // Initialise SPI interface
setup_timer_0 ( RTCC_INTERNAL|RTCC_DIV_256|RTCC_8_BIT );
enable_interrupts ( INT_TIMER0 ); // enable Timer0 interrupts
enable_interrupts ( GLOBAL ); // enable all interrupts
while (TRUE) { // main loop
DAC_write (0, DAC1); // update dac 1 with DAC1 byte
DAC_write (1, DAC2); // and dac 2 with DAC2 byte
}
}
#define board_id 4 // Variable board_id needs to be defined with the processor board number used for the tutorial
#include "lib\mainboard.c"
#include "lib\spi_lib.c"
#include "lib\MAX549_lib.c"
#priority RTCC
char DAC1=0,DAC2=0; // Values for DAC 1 and DAC 2
#int_rtcc
void Timer0Interrupt ( void ) {
DAC1++;
DAC2--;
}
void main() {
boot_up();
Init_SPI(); // Init SPI interface
setup_timer_0 ( RTCC_INTERNAL|RTCC_DIV_256|RTCC_8_BIT );
enable_interrupts ( INT_RTCC ); // enable Timer0 interrupt
enable_interrupts ( GLOBAL ); // enable all interrupts
while (TRUE) {
DAC_write (0, DAC1);
DAC_write (1, DAC2);
}
}
As usual, the program defined the processor board used, and loads library files.
The MAX549 library defines functions to communicate and control the SPI DAC chip.
Timer0Interrupt runs for every interrupt generated by timer 0. It simply update the values to output on each DAC.
The main program initialise the board with the function boot_up, initialise the SPI interface, setup timer 0 and interrupts, and enter a loop that update each DAC of the chip.
Timer 0 is setup with a predivider set to 8. On the PIC 18F family, timer 0 is an 16 bits counter (can be setup as 8 bits), while it is an 8 bits counter on the 16F processors.
Timer 0 is incremented by the internal clock (crystal clock /4). Every time timer 0 overflows, it generates an interrupts, which updates the variables to send to the DAC.
The fornula to determines the time between interrupts is: T = (4*PRESCALER*2^n)/CRYSTAL_FREQ. n is the resolution of timer 0 (8 or 16 bits)
On 18F processor, the default resolution is 16 bits. To set it to 8 bits, you need to specify it when setting up timer 0 with RTCC_8_BIT.
In 8 bits mode, 256 prescaler and 20MHz clock, the values are updated every 13ms. The DAC values will cycle every 3.3s (256 x 13ms)
The board used for this tutorial is board 4, with a PIC 18F2320 processor. The libraries for each main/processor board are supplied in the zip file, with the source code.
The MAX549 library loads the various functions to control the chip.
#ifndef _MAX549_LIB // make sure the library hasn't been already loaded
#define _MAX549_LIB // if it was, just skip it
#include "../lib/Common/spi.c" // load SPI generic library
#ifndef CS_DAC // define default CS line of not defined earlier
#define CS_DAC CS_SPI0 // CS pin for DAC module to CS0 if not already defined
#endif
void DAC_write (BOOLEAN dac, BYTE output) { // function to update DAC output value
BYTE cmd;
if (dac) { // DAC B is selected if dac is not 0
cmd = 0x0A;
} else { // DAC A if dac is 0
cmd = 0x09;
}
output_low(CS_DAC); // Enable DAC with Chip Select
SPI_out(cmd); // Sends command byte
SPI_out(output); // Then sends DAC value
output_high(CS_DAC); // Release chip
}
#endif
First, it loads the SPI generic library, to have the basic SPI routines (read, write, setup SPI interface...)
Then it defines a default value for the Chip Select signal for the DAC. It default to CS_SPI0, which is defined in the SPI library, and depends on the mainboard used.
The function DAC_write is the function used to update the output value of the DAC. It takes 2 input variables:
- dac: is a boolean (1 bit) value. If true, it will set DAC B. Else it will work with DAC A.
- output: is a byte, which is the value to send to the selected DAC channel.
It does what is typically done for SPI chips: select the chip by setting its Chip Select signal low, sends 2 bytes (command + value) and release the chip by setting Chip Select high. The MAX549 datasheet explains in detail how to communicate with the chip.
One simple function is implemented, which allows to update individually each DAC channel.
The MAX549 frst loaded the generic SPI library, which is explained below:
#ifndef _SPI_LIB // make sure library is not already loaded
#define _SPI_LIB
#include "../lib/Common/SPI.h"
void Init_SPI (void) { // Function to initialise SPI interface
output_high(MOSI);
output_high(SCK);
input(MISO);
#if defined(HARDWARE_SPI) // if HARDWARE_SPI variable defined, use built-in SPI function available for device with hardware SPI interface
setup_spi(SPI_MASTER | SPI_MODE_0_0 | SPI_CLK_DIV); // Setup SPI as master, mode 0 and speed defined in SPI.h
#else
#use spi(DI=MISO, DO=MOSI, CLK=SCK, BITS=8, MODE=0) // same as above, but for software emulated SPI interface
#endif
#ifdef CS_SPI0 // Set each Chip Select line to high (chip not selected) if defined
output_high(CS_SPI0);
#endif
#ifdef CS_SPI1
output_high(CS_SPI1);
#endif
#ifdef CS_SPI2
output_high(CS_SPI2);
#endif
}
void SPI_out(BYTE dataout) { // output byte on SPI
#if defined(HARDWARE_SPI) // again, use hardware function if possible
spi_write(dataout);
#else
spi_xfer(dataout);
#endif
}
BYTE SPI_in(BYTE dataout) { // reads a byte from SPI
BYTE datain;
#if defined(HARDWARE_SPI)
datain = spi_read(dataout);
#else
datain = spi_xfer(dataout);
#endif
return datain;
}
void SPI_out16(WORD dataout) { // functions for 16 bits value
SPI_out(dataout>>8);
SPI_out(dataout&0xff);
}
WORD SPI_in16(WORD dataout) {
WORD datain=0;
datain = spi_in(dataout >> 8);
datain <<= 8;
datain |= spi_in(dataout & 0xff);
return datain;
}
#endif
The SPI library defines basic functions to use the SPI interface of a board. Typical communication with SPI devices is on 8 or 16 bits. The basic functions are the 8 bits ones, which implement hardware SPI if the variable HARDWARE_SPI is defined. Else it will use software function to emulate the SPI bus.
- Init_SPI: this initialise the SPI port as a master.
- SPI_out: send a byte on the SPI interface
- SPI_in: read a byte from the SPI interface, while clocking out the byte dataout
- SPI_out16: send a 16 bits value on the SPI interface
- SPI_in16: read a 16 bits value from the SPI interface while clocking the 16 bits dataout
The library loaded a header file, which defines variables based on the processor board used. It is worth noticing that the board_id variable needs to be defined, so the library knows how to implement the SPI interface, as each is wired differently on different board.
It is easy to add yor own board_id value, and base the varaible definition on an existing board. It makes adding your own projects/board very easy, without modifying the library files.
It is possible to define the variables BEFORE loading the header, in the main program for example. The values would then not be overwritten by the header values.
#ifndef _SPI_H
#define _SPI_H
#include "../lib/Common/TypeDefs.c" // Load custom data types
#define SPI_MODE_0_0 (SPI_L_TO_H | SPI_XMIT_L_TO_H) // defines various SPI modes
#define SPI_MODE_0_1 (SPI_L_TO_H)
#define SPI_MODE_1_0 (SPI_H_TO_L)
#define SPI_MODE_1_1 (SPI_H_TO_L | SPI_XMIT_L_TO_H)
#ifdef board_id // 1: 16F819, 2: 16F628, 3: 16F873A, 4: 18F2320, 5: 18F26J11
#if (board_id == 1 ) // Board 16F819
#ifndef MISO // Master SDI, Slave SDO
#define MISO PIN_B1
#endif
#ifndef MOSI // Master SDO, Slave SDI
#define MOSI PIN_B2
#endif
#ifndef SCK // SPI clock signal
#define SCK PIN_B4
#endif
#ifndef CS_SPI0
#define CS_SPI0 PIN_B5 // SPI CS0 (SS Slave Select signal, to be able to use Slave mode of board)
#endif
#ifndef CS_SPI1
#define CS_SPI1 PIN_B0 // SPI CS1 (Interrupt signal, so SPI device could trigger external interrupts on the board)
#endif
#ifndef CS_SPI2
#define CS_SPI2 PIN_B3 // SPI CS2
#endif
#define SPI_CLK_DIV SPI_CLK_DIV_4 // define SPI clock speed in master mode
#ifndef HARDWARE_SPI
#define HARDWARE_SPI
#endif
#elif (board_id == 2) // Board 16F628 (no hardware SPI interface
#ifndef MISO
#define MISO PIN_B1
#endif
#ifndef MOSI
#define MOSI PIN_B2
#endif
#ifndef SCK
#define SCK PIN_B4
#endif
#ifndef CS_SPI0
#define CS_SPI0 PIN_B5
#endif
#ifndef CS_SPI1
#define CS_SPI1 PIN_B0
#endif
#ifndef CS_SPI2
#define CS_SPI2 PIN_B3
#endif
#elif (board_id == 3) // Board 16F873A
#ifndef MISO
#define MISO PIN_C4
#endif
#ifndef MOSI
#define MOSI PIN_C5
#endif
#ifndef SCK
#define SCK PIN_C3
#endif
#ifndef CS_SPI0
#define CS_SPI0 PIN_C0
#endif
#ifndef CS_SPI1
#define CS_SPI1 PIN_C1
#endif
#ifndef CS_SPI2
#define CS_SPI2 PIN_C2
#endif
#define SPI_CLK_DIV SPI_CLK_DIV_16
#ifndef HARDWARE_SPI
#define HARDWARE_SPI
#endif
#elif (board_id == 4) // Board 18F2320
#ifndef MISO
#define MISO PIN_C4
#endif
#ifndef MOSI
#define MOSI PIN_C5
#endif
#ifndef SCK
#define SCK PIN_C3
#endif
#ifndef CS_SPI0
#define CS_SPI0 PIN_A5
#endif
#ifndef CS_SPI1
#define CS_SPI1 PIN_B0
#endif
#ifndef CS_SPI2
#define CS_SPI2 PIN_C2
#endif
#define SPI_CLK_DIV SPI_CLK_DIV_64
#ifndef HARDWARE_SPI
#define HARDWARE_SPI
#endif
#elif (board_id == 5) // Board 18F26J11
#ifndef MISO
#define MISO PIN_C4
#endif
#ifndef MOSI
#define MOSI PIN_C5
#endif
#ifndef SCK
#define SCK PIN_C3
#endif
#ifndef CS_SPI0
#define CS_SPI0 PIN_A5
#endif
#ifndef CS_SPI1
#define CS_SPI1 PIN_B0
#endif
#ifndef CS_SPI2
#define CS_SPI2 PIN_C0
#endif
#define SPI_CLK_DIV SPI_CLK_DIV_16
#ifndef HARDWARE_SPI
#define HARDWARE_SPI
#endif
#else
#error "Board board_id not supported"
#endif
#endif
#if !defined(MISO) || !defined(MOSI) || !defined(SCK)
#error MISO, MOSI and SCK must all be defined
#endif
#ifdef HARDWARE_SPI
#if !defined(SPI_CLK_DIV)
#define SPI_CLK_DIV SPI_CLK_DIV_16
#endif
#endif
// function prototypes
void Init_SPI (void);
void SPI_out(BYTE dataout);
BYTE SPI_in(BYTE dataout);
void SPI_out16(WORD dataout);
WORD SPI_in16(WORD dataout);
#endif
Compile the project and program the target with a PicKIT 2 programmer for example. Connect the processor board to the Dual 8 bits DAC board on the SPI connector. The CS line is defined to CS_SPI0, so make sure CS line on the DAC board is setup up on CS0 with JP1.
Power up the boards.
The output voltage on DAC A will increment, and decrement on DAC B. You can check this with a multimeter or oscilloscope.
Files and links:
Tutorial 2 source files.Processor boards.
Dual 8 bits DAC.
PIC tutorial 7 - GPS Receiver
- Details
- Category: PIC Tutorials
- Hits: 3359
Fig. 1a Board 3 with GPS receiver and graphic LCD Display
Description:
Now we can display things on a graphical or alphanumeric LCD display, we'll use tutorial 7 to introduce serial communication and GPS data stream processing. We will build a library to get serial communication going and another one to manage the GPS receiver. A serial GPS receiver from Fastrax is used in this tutorial, but the tutorial can easily be adapted to other receiver.
The tutorial will display date, time, longitude, latitude, number of satellite used in calculation, fix mode, speed and altitude on the LCD screen if the custom sentence is selected, or the selected NMEA sentence.
The GPS library read a NMEA sentence from the receiver, or build a custom sentence with: Time,latitude,N/S,longitude,E/W,fix,number of satellites, altitude, speed, course and date (by default).
The graphic display shows:
| Graphic LCD | Alphanumeric LCD | |
|---|---|---|
|
17/05/10 17:50 |
17/05/10 17:50 |
The boards can be powered by one of the PSU boards (powered from USB port), or between 3.3 to 5V, depending on the display used.
Requirements:
Processor boardAny board with 4K or more (board 3 in example)
Extension(s)GPS Receiver
graphic or alphanumeric LCD Display
Power supplyPSU 1, 2 or 3.
LanguageCCS C
ProgrammerPicKIT 2 2
Program:
#define board_id 3
#define _NMEACode 0 // Set the string to receive to the custom sentence
#include "lib\mainboard.c"
#if (board_id == 4 ) // Include alphanumeric LCD library if board 4
#include "lib\lcd_lib.c"
#else // else use graphic display
#include "lib\nok3310_lib.c"
#endif
#include "lib\rs232_lib.c" // Load serial library
#include "lib\GPS_lib.c" // Load GPS library
#include <stdlib.h> // Load standard C library
int1 blinkFlag=0; // Bit with second flag (used for displaying blinking ':' in time)
void display_GPS () { // Displays custom sentence on display one field after the other
float tempf; // Temporary float
int8 fieldLength;
fieldLength = GPS_GetField(); // get first field of sentence (time)
display_gotoxy(9,0); // Go to time position on display
if (fieldLength > 0) {
display_putc(GPS_ctrl.Word[0]); // Display 2 digits of hours
display_putc(GPS_ctrl.Word[1]);
if (blinkFlag == 0) {display_putc(0x20); blinkFlag = 1;} // if second flag not set, display space
else {display_putc(0x3a); blinkFlag = 0;} // else display ':'
display_putc(GPS_ctrl.Word[2]); // Display 2 digits of time
display_putc(GPS_ctrl.Word[3]);
} else {
printf(display_putc, "**:**");
}
fieldLength = GPS_GetField(); // get 2nd field (latitude)
display_gotoxy(0,1);
if (fieldLength > 0) {
printf(display_putc, GPS_ctrl.Word);
} else {
printf(display_putc, "%d", fieldLength);
}
display_putc(' ');
fieldLength = GPS_GetField(); // get 3rd field (latitude N/S)
if (fieldLength > 0) {
display_putc(GPS_ctrl.word[0]);
} else {
printf(display_putc, "* ");
}
fieldLength = GPS_GetField(); // get 4th field (longitude)
display_gotoxy(0,2);
if (fieldLength > 0) {
printf(display_putc, GPS_ctrl.Word);
} else {
printf(display_putc, "%d", fieldLength);
} display_putc(' ');
fieldLength = GPS_GetField(); // get 5th field longitude (E/W)
if (fieldLength > 0) {
display_putc(GPS_ctrl.word[0]);
} else {
printf(display_putc, "* ");
}
fieldLength = GPS_GetField(); // get 6th field (fix)
display_gotoxy(13,2);
if (fieldLength > 0) {
display_putc(GPS_ctrl.word[0]);
} else {
display_putc('*');
}
fieldLength = GPS_GetField(); // get 7th field (nb satellites)
display_gotoxy(12,1);
if (fieldLength > 1) {
display_putc(GPS_ctrl.word[0]);
display_putc(GPS_ctrl.word[1]);
} else if (fieldLength > 0) {
display_putc(' ');
display_putc(GPS_ctrl.word[0]);
} else {
printf(display_putc, " *");
}
fieldLength = GPS_GetField(); // get 8th field (altitude)
if (fieldLength > 0) {
display_gotoxy(13-fieldLength,3);
printf(display_putc, GPS_ctrl.Word);
display_putc('m');
} else {
display_gotoxy(8,3);
display_putc('*');
}
fieldLength = GPS_GetField(); // get speed
display_gotoxy(0,3);
if (fieldLength > 0) {
tempf = atof(GPS_ctrl.Word);
#if (GPS_SPEED_UNIT == 2 )
printf(display_putc, "%3LuKm/h", (int16) (tempf*1.852));
#elif (GPS_SPEED_UNIT == 3 )
printf(display_putc, "%3Lum/h", (int16) (tempf*1.151));
#else
printf(display_putc, "%3LuKnot", (int16) (tempf));
#endif
} else {
printf(display_putc, " **.*");
}
GPS_SkipField(1); // skip course
fieldLength = GPS_GetField(); // get date
display_gotoxy(0,0);
if (fieldLength > 0) {
display_putc(GPS_ctrl.Word[0]);
display_putc(GPS_ctrl.Word[1]);
display_putc(0x2f);
display_putc(GPS_ctrl.Word[2]);
display_putc(GPS_ctrl.Word[3]);
display_putc(0x2f);
display_putc(GPS_ctrl.Word[4]);
display_putc(GPS_ctrl.Word[5]);
} else {
printf(display_putc, "**/**/**");
}
}
void main() {
boot_up();
Init_display();
Init_GPS(); // Initialise GPS receiver
printf(display_putc, "Waiting GPS");
InitGPSBuffer(_NMEACode);
while (TRUE) {
if (GPS_ctrl.State == GPS_STATE_IDLE) { // If buffer ready to receive
InitGPSBuffer(_NMEACode); // set code to receive (0 to get custom sentence)
Read_GPS();
}
if (GPS_ctrl.State == GPS_STATE_READY) { // Sentence in buffer
Clear_display(); // clear screen
#if (_NMEACode == 0 ) display_GPS(); // use function to display custom sentence
#else printf(display_putc, GPS_ctrl.Buffer); // or just diaplsy the sentence
#endif
GPS_ctrl.State = GPS_STATE_IDLE;
}
}
}
The program defines the processor board used, and loads the library files. If board 4 is used, it will compile to use the alphanumeric LCD display, else it will use the graphic display.
We then load the serial, gps and standard C libraries.
The blinkFlag bit is used to be able to have the ':' character blinking in the time displayed.
The display_GPS function simply displays each field of the custom sentence on the screen, at a given position. The GPS_GetField function get the current field from the GPS buffer in the GPS_ctrl.Word array. It is then displayed. This introduces various standard C functions (atof...). We check the length of the value returned before displaying it, to make sure we have a relevant value.
The main program initialise the board with the function boot_up and initialise the display and GPS receiver.
It then enters a loop that sets the GPS buffer to read the custom GPS sentence and reads the GPS stream to build it. Once received, it clears the screen and displays the various data on the display.
It is easy to receive in the GPS buffer another NMEA sentence by changing the value of _NMEACode variables to one of the value available in the GPS library (75, 86, 85, 66). The value is the 5 characters of the NMEA header hashed. When receiving a sentence, it is checked against the message received to see if it is the once requested.
Now if this value is changed, then the sentence is simply displayed on the screen.
Note: on the alphanumeric display, the displays fills line 1, then 3, then 2 and eventually 4. The NMEA sentence might be displayed incorrectly if it has more than twice the number of characters/per line for the display.
The board used for this tutorial is board 3, with a PIC 16F873A processor.
Now, the serial library. The serial port is implemented differently on different processor. Some have hardware UART interface, some don't, and would use a software emulation.
#ifndef _RS232_LIB
#define _RS232_LIB
#ifndef board_id
#error "variable board_id not defined"
#endif
// 1: 16F819, 2: 16F628, 3: 16F873A, 4: 18F2320
#if (board_id == 1 )
#define RXD PIN_B1
#define TXD PIN_B2
#use rs232 ( baud=9600, xmit=TXD, rcv=RXD, PARITY=N, BITS=8 )
#elif (board_id == 2)
#define RXD PIN_B1
#define TXD PIN_B2
#use rs232 ( baud=9600, UART1, PARITY=N, BITS=8, ERRORS )
#elif (board_id == 3)
#define RXD PIN_C7
#define TXD PIN_C6
#use rs232 ( baud=9600, UART1, PARITY=N, BITS=8, ERRORS )
#elif (board_id == 4)
#define RXD PIN_C7
#define TXD PIN_C6
#use rs232 ( baud=9600, UART1, PARITY=N, BITS=8, ERRORS )
#else
#error "Board board_id not supported"
#endif
#endif
The library checks that it knows what processor board is used so it knows how to configure the serial port.
If it is, it defines the RX and TX pins and setup the serial port using the built-in function in CCS: use rs232. If the board has a hardware interface, then it uses it, else it uses software emulation.
The library setup the serial interface at 9600 bauds, no parity and 8 bits. The serial function of CCS are now available, and it is fairly easy to implement. The GPS library will use it to communicate with the receiver, mainly with the function getc, which receives a character from the serial port.
#ifndef _RS232_LIB
#error "RS232 library not loaded"
#else
#ifndef _GPS_LIB
#define _GPS_LIB
#ifndef DEBUG_MODE
#define DEBUG_MODE 0
#endif
#ifndef GPS_BAUDRATE
#define GPS_BAUDRATE 9600
#endif
#define GPS_BUFFER_SIZE 70
#ifndef DEGREE
#define DEGREE 0xdf
#endif
#define GPRMC_CODE 75 // NMEA messages code XORed
#define GPGGA_CODE 86
#define GPGSV_CODE 85
#define GPGSA_CODE 66
#ifndef _NMEACode
#define _NMEACode 0
#endif
#define GPS_STATE_IDLE 0 // GPS Buffer ready to receive
#define GPS_STATE_READY 255 // GPS Buffer has sentence
typedef struct {
int8 State; // Status of GPS module
int8 Buffer[GPS_BUFFER_SIZE]; // The actual buffer data
int8 *BufferPtr; // Points to the buffer
int8 MsgTypeReceived; // Hashed message code received
int8 MsgTypeDesired; // Hashed message code desired
int8 ByteCnt; // Number of bytes in the buffer
int8 rmcPtr; // point to byte number of start of GGA data in buffer
int8 Word[10]; // Holds fieds (one GPS data)
} GPS_control_t;
GPS_control_t GPS_ctrl;
// function prototypes
void Init_GPS(void); // Initialize serial port for GPS receiver
void InitGPSBuffer(int8 NMEACode); // Initialize GPS buffer to receive NMEACode sentence
void Read_GPS(void); // put NMEA sentence defined by _NMEACode into GPS buffer
void GPS_SkipField (int8 Cnt); // skip Cnt GPS field(s) in GPS buffer
int8 GPS_GetChar(void); // Get character from GPS buffer
int8 GPS_GetField(void); // Get GPS field from GPS buffer into GPSword array
void Init_GPS() {
#use rs232(baud=GPS_BAUDRATE, xmit=TXD, rcv=RXD, PARITY=N, BITS=8, stream=GPSstream)
GPS_Ctrl.rmcPtr = 0;
//printf("$PMTK104*37\r\n"); // Reset GPS receiver (check with datasheet)
}
void InitGPSBuffer (int8 NMEACode) {
GPS_ctrl.BufferPtr = GPS_ctrl.buffer;
GPS_ctrl.ByteCnt = 0;
GPS_ctrl.State = GPS_STATE_IDLE;
GPS_ctrl.MsgTypeDesired = NMEACode;
if (GPS_Ctrl.rmcPtr == 0)
memset(GPS_ctrl.Buffer, 0, GPS_BUFFER_SIZE); // reset buffer content
}
void Read_GPS() {
int8 ch, i;
int1 skip=0;
do {
ch=getc(); // get caracter from serial port
if (ch == '$') { // if start of NMEA0183 message
GPS_ctrl.ByteCnt = 0; // reset byte count
GPS_ctrl.MsgTypeReceived = 0; // set hashed value to null
GPS_ctrl.State ++; // next state
for ( i=0; i<5; i++ ) { // the next 5 caracters are the NMEA header
ch=getc();
GPS_ctrl.MsgTypeReceived ^= ch; // hash in msg type
}
if (GPS_ctrl.MsgTypeDesired == 0) { // Process data to build default string (hhmmss,lat,N/S,long,E/W,fix,nbSat,altitude,speed,course,date)
ch=getc(); // skip comma after message type
if (GPS_ctrl.MsgTypeReceived == GPGGA_CODE) { // we received GGA
for ( i=0; i<9; i++) { // process 9 fields from GGA sentence
do {
ch=getc();
if (i==0 && ch=='.') skip = 1; // skip decimal of time (time is first field)
if (!skip && i!=7) { // do not store if skip or 7th field (Horizontal dilution)
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt] = ch;
GPS_ctrl.ByteCnt++;
}
} while (ch != ','); // Store data until ',' is received (end of field)
if (skip) {
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt] = ','; // Add separator to received sentence
GPS_ctrl.ByteCnt++;
}
skip = 0;
}
GPS_ctrl.rmcPtr = GPS_ctrl.ByteCnt; // GGA data received, we will receive RMC data next.
} else if (GPS_ctrl.MsgTypeReceived == GPRMC_CODE) { // we received RMC
GPS_ctrl.ByteCnt = GPS_ctrl.rmcPtr;
for ( i=0; i<9; i++) { // process 9 fields from RMC sentence
do {
ch=getc();
if (i>5) { // start recording from 6th field (Speed)
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt] = ch;
GPS_ctrl.ByteCnt++;
}
} while (ch != ',');
}
GPS_ctrl.rmcPtr = 0; // reset pointer for received data
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt-1] = 0; // Set end of data caracter
GPS_ctrl.State = GPS_STATE_READY; // Buffer is now ready
}
} else if (GPS_ctrl.MsgTypeReceived == GPS_ctrl.MsgTypeDesired) { // if it is the NMEA sentence requested
ch=getc(); // skip comma after message type
do {
ch=getc();
if (ch != '*') { // record characters received until the end character '*'
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt] = ch;
GPS_ctrl.ByteCnt++;
}
} while (ch != '*');
GPS_Ctrl.rmcPtr = 0;
GPS_ctrl.Buffer[GPS_ctrl.ByteCnt+1] = 0;
GPS_ctrl.State = GPS_STATE_READY;
}
}
} while(GPS_ctrl.State != GPS_STATE_READY); // Read messages until the sentence is built
}
void GPS_SkipField (int8 Cnt) {
int8 X;
for ( X = 0; X < Cnt; X++ ) {
while (GPS_GetChar() != ',');
}
}
int8 GPS_GetChar (void) { // Get the next available byte in the recv fifo.
int8 Value;
if (GPS_ctrl.State == GPS_STATE_READY) {
Value = 0;
if (GPS_ctrl.ByteCnt > 0) { // For safety, check if there is any data
Value = *GPS_ctrl.BufferPtr++; // Read byte from fifo
if (GPS_ctrl.BufferPtr == (GPS_ctrl.Buffer + GPS_BUFFER_SIZE)) { // Did tail ptr wrap ?
GPS_ctrl.BufferPtr = GPS_ctrl.Buffer; // If so, reset it to start of buffer
}
GPS_ctrl.ByteCnt--; // Decrement byte count
} return (Value);
}
}
int8 GPS_GetField (void) {
int8 X=0, Index=0;
while (true) {
X = GPS_GetChar();
if ((X == ',') || (X == 13) || (X == 10) || (X == 0)) {
break;
}
GPS_ctrl.Word[Index++] = X;
}
GPS_ctrl.Word[Index] = 0x00;
return (Index); // return number of characters in field
}
#endif
#endif
Fig. 1b Board 3 with GPS receiver and alphanumeric LCD Display
First, we need to check if the serial library is loaded. A few variables are then defined, like the baud rate of the GPS module, the buffer size, the default NMEA message wanted. The NMEA message can be one of the values above (GPRMC_CODE, GPGGA_CODE, GPGSV_CODE, GPGSA_CODE ) to receive the equivalent NMEA message. If set to 0, then a custom message is built in the following format: Time,latitude,N/S,longitude,E/W,fix,number of satellites, altitude, speed, course and date.
A structure holds all the variables used in managing the GPS module: receive buffer, various pointers, status, counters...
A set of functions is then defined to manage communication with the GPS module.
- Init_GPS: initialize the GPS module and serial port at the right speed. The PMTK104 string reset the module from any custom configuration. The initialisation string might be different with other GPS module.
- InitGPSBuffer: initialise the control structure and sets which NMEA sentence needs to be received.
- Read_GPS: parse NMEA sentence to fill the GPS receive buffer. It reads the NMEA sentence defined when calling InitGPSBuffer. If it was 0, then the function parses several NMEA sentences to build the custom sentence. The code is commented a bit. It basically parse each character received and store it or not, depending whether it is wanted or not.
- GPS_SkipField: Once a sentence has been received in the buffer, a set of function allows to retrieve the various data from the sentence. Skip field will skip n fields from the received sentence. The pointer is pointing at the next data in the sentence.
- GPS_GetField: It puts the data of the current field in the received sentence in an array (GPS_ctrl.Word) and returns the length of it. It is the function used to retrieve the data from the pointed field.
- GPS_GetChar: This function just returns the character pointed to in the receive buffer.
Compile the project and program the target with a PicKIT 2 programmer for example. Connect the processor board to the LCD display board on port B and the GPS module on port C, making sure the jumpers on the GPS board match your processor board for the Txd and Rxd signals.
To use the alphanumeric display, either use another processor board (4 in the example), or change the display library loaded. Recompile and program the board. That's it.
Power up the boards.
The GPS module will need to get a fix, and the time it takes depends on the chip used and location. Position the receiver outside or near a window to get a quicker fix. The end of line 3 will display a value greater than 0 once a valid fix is acquired. You'll also be able to see the number of satellites in view. Usually, the date and time will become available before a valid fix. It takes only 1 satellites to get it, but at least 3 to get a proper position fix.
Files and links:
Tutorial 7 source files.Processor boards.
Graphic LCD Display board.
GPS Receiver board.
PIC tutorial 3 - LCD Display
- Details
- Category: PIC Tutorials
- Hits: 2315
Fig. 1 Board 3 with LCD Display
Description:
For the third tutorial, we will start looking at LCD displays. We will build a library to drive generic alphanumeric LCD display, of different type: 1, 2 or 4 lines, and up to 40 caracters per line.
The tutorial will update the display with the value of a counter, displayed in hexadecimal and decimal, updated every 250 ms.
The boards can be powered by one of the PSU boards (powered from USB port), or between 3.3 to 5V, depending on the display used.
Requirements:
Processor boardAny board (board 3 in example)
Extension(s)LCD Display
Power supplyPSU 1, 2 or 3. Depends on board used.
LanguageCCS C
ProgrammerPicKIT 2
Program:
#define board_id 3
#include "lib\mainboard.c" // Load board module
#include "lib\lcd_lib.c"
int8 counter;
void main() {
boot_up();
counter = 0;
init_display();
while (true) {
printf(display_putc, "\fH=%X, D=%u", counter, counter);
counter ++;
delay_ms(250);
}
}
The program defined the processor board used, and loads the library files.
We then load the LCD library, which defines functions to communicate with the LCD display.
The main program initialise the board with the function boot_up, reset the counter and initialise the display.
It then enters a loop that display the counter in Hexadecimal and decimal using the built-in function printf. Printf is told to use the display_putc function to output each caracter. Display_putc sends a caracter to the LCD display at the current position of the cursor. The counter is then incremented and the programs counts for 250ms before looping again.
The board used for this tutorial is board 3, with a PIC 16F873A processor. The library for the board is similar to the one in tutorial 2 and is included in the zip file.
Now, the LCD library. The LCD display is implemented differently (port A or B) on different processor boards.
#ifndef _LCD_LIB
#define _LCD_LIB
#ifndef DEBUG_MODE
#define DEBUG_MODE 0
#endif
#ifndef lcd_type
#define lcd_type 2 // 0=5x7, 1=5x10, 2=2 lines
#endif
#ifndef lcd_line_two
#define lcd_line_two 0x40 // LCD RAM address for the second line
#endif
#ifndef lcd_char_line
#define lcd_char_line 20 // number of characters per line
#endif
struct lcd_pin_map { // This structure is overlayed
int8 data : 4; // on to an I/O port to gain
int1 rs; // access to the LCD pins.
int1 unused; // The bits are allocated from
int1 rw; // low order up.
int1 enable;
} lcd_port;
// 1: 16F819, 2: 16F628, 3: 16F873A, 4: 18F2320
#if (board_id==1)
#locate lcd_port = pa // LCD on port A
#define set_tris_lcd(x) set_tris_a(x)
#elif (board_id==2)
#locate lcd_port = pa // LCD on port A
#define set_tris_lcd(x) set_tris_a(x)
#elif (board_id==3)
#locate lcd_port = pb // LCD on port B
#define set_tris_lcd(x) set_tris_b(x)
#elif (board_id==4)
#locate lcd_port = pb // LCD on port B
#define set_tris_lcd(x) set_tris_b(x)
#else
#error "Board board_id not supported"
#endif
int8 const LCD_INIT_STRING[4] = {0x20 | (lcd_type << 2), 0xc, 1, 6}; // These bytes need to be sent to the LCD to start it up.
struct lcd_pin_map const LCD_WRITE = {0,0,0,0,0}; // For write mode all pins are out
struct lcd_pin_map const LCD_READ = {15,0,0,0,0}; // For read mode data pins are in
// function prototypes
void Init_LCD(void);
void lcd_send_nibble( int8 n );
void lcd_send_byte(int1 address, int8 n);
int8 lcd_read_byte(void);
void lcd_gotoxy(int8 x, int8 y);
void lcd_putc(int8 c);
int8 lcd_getc( int8 x, int8 y);
// Library functions
void Init_LCD() {
int8 i;
set_tris_lcd(LCD_WRITE);
lcd_port.rs = 0;
lcd_port.rw = 0;
lcd_port.enable = 0;
delay_ms(15);
for(i=1;i<=3;++i) {
lcd_send_nibble(3);
delay_ms(5);
}
lcd_send_nibble(2);
for(i=0;i<=3;++i)
lcd_send_byte(0,LCD_INIT_STRING[i]);
}
#define Init_display Init_LCD // Init function for screen
void lcd_send_nibble( int8 n ) {
lcd_port.data = n;
delay_cycles(4);
lcd_port.enable = 1;
delay_us(2);
lcd_port.enable = 0;
}
void lcd_send_byte(int1 address, int8 n ) { // Send byte as 2 4bits word (address: 0=cmd, 1=data)
lcd_port.rs = 0;
while ( bit_test(lcd_read_byte(),7) ); // Wait for display to be ready
lcd_port.rs = address;
delay_cycles(4);
lcd_port.rw = 0;
delay_cycles(4);
lcd_port.enable = 0;
lcd_send_nibble(n >> 4);
lcd_send_nibble(n & 0xf);
}
int8 lcd_read_byte() { // Reads byte from display
int8 low,high;
set_tris_lcd(LCD_READ);
lcd_port.rw = 1;
delay_cycles(4);
lcd_port.enable = 1;
delay_cycles(4);
high = lcd_port.data;
lcd_port.enable = 0;
delay_cycles(4);
lcd_port.enable = 1;
delay_us(1);
low = lcd_port.data;
lcd_port.enable = 0;
set_tris_lcd(LCD_WRITE);
return( (high<<4) | low);
}
void lcd_gotoxy( int8 x, int8 y) { // Position cursor (0,0 top left corner)
int8 address;
switch(y) {
case 0 : address=0;
break;
case 1 : address=lcd_line_two;
break;
case 2 : address=lcd_char_line;
break;
case 3 : address=lcd_line_two+lcd_char_line;break;
}
address+=x;
lcd_send_byte(0,0x80|address);
}
#define display_gotoxy lcd_gotoxy // gotoxy function for screen
void lcd_putc( int8 c) { // Send character to display
switch (c) {
case '\f' : lcd_send_byte(0,1); // Clear screen
delay_ms(20);
break;
case '\n' : lcd_gotoxy(0,1); // Goto to second line
break;
case '\b' : lcd_send_byte(0,0x10); // Shift screen
break;
default : lcd_send_byte(1,c); // Send character as data
break;
}
}
#define display_putc lcd_putc // Putc function for output to screen
int8 lcd_getc( int8 x, int8 y) { // Reads data from display at address x,y
int8 value;
lcd_gotoxy(x,y);
while ( bit_test(lcd_read_byte(),7) ); // wait until busy flag is low
lcd_port.rs=1;
value = lcd_read_byte();
lcd_port.rs=0;
return(value);
}
void lcd_print_degree(void) { // Writes degree character to display
lcd_putc('ß');
}
#define display_print_degree lcd_print_degree // Putc function for output to screen
#endif
The library initialise some variables to default values if not already defined in the main program. It defines the type of display, the number of lines and the number of caracters per line. The variables can be defined in the main program to suit the LCD display used, or in the library to change the default display type. The standard LCD displays are organised as 2 lines in memory. Even if the display has 4 lines, it is still organised as 2 lines of caracters in memory: once the first line is full, the next bytes would be displayed on line 3 of the display, but the caracters belong to the memory area of line 1. lcd_line_two defined the start address of line 2 in the display memory (0x40 by default).
The library then defines how the display is connected to the processor. The LCD display board is connected to an 8 bits port of the processor. The library defines the lcd_pin_map structure to fit the connection to the display: bits 0-3 are the data lines, then bit 4 is rs, 5 not used, 6 rw and 7 enable. It is easy to match the connection of any LCD board to that structure. The structure is named lcd_port. Next, it needs to be maped to a processor port.
Using the board_id variable, the library maps lcd_port to port A or port B of the processor.
A set of functions is then defined to control the display. They are not all needed, but the main ones are:
- Init_LCD initialize the LCD display. It is the first function to call when using the display.
- lcd_gotoxy set the cursor position to X and Y (Top left corner is position 0,0).
- lcd_putc sends a caracter to the display. Special caracters have special functions: \f clears the screen, \n go to the second line,...
- lcd_print_degree prints a degree symbol to the display.
The main functions are also defined with a generic name, used by all display libraries (Alphanumeric or graphical LCD display. It makes the main program the same, whatever display is used).
For example, the main program uses the init_display function, and not Init_LCD. The same program could be used with the graphic LCD display. We would just need to load the graphic LCD library instead of the alphanumeric one.
Compile the project and program the target with a PicKIT 2 programmer for example. Connect the processor board to the LCD display board on port B.
Power up the boards.
The display will display un incrementing counter in hexadecimal and decimal format.
Files and links:
Tutorial 3 source files.Processor boards.
LCD Display board.
LCD Display caracters table.
PIC tutorial 4 - I2C temperature
- Details
- Category: PIC Tutorials
- Hits: 2496
Fig. 1a Board 3 with LCD display and I2C temperature sensor 1
Description:
In the 4th tutorial, we'll cover some I2C communication with temperature sensors, and data conversion.
You will need a processor board, a power supply, the LCD display and one of the I2C temperature sensor.
The tutorial will work with an I2C and Temperature sensor libraries. The sensor library will be able to manage either type of sensor (MCP9803 or DS1631)
The boards can be powered by one of the PSU boards (powered from USB port), or between 2.7 to 5V.
Requirements:
Processor boardAny board (board 3 in example)
Extension(s)LCD Display
I2C temperature sensor 1 or 2
Power supplyPSU 1, 2 or 3. Depends on board used.
LanguageCCS C
ProgrammerPicKIT 2
Program:
#define board_id 4 // define processor board used
#define I2C_Temp_Chip 1 // 1 for MCP9803 or 2 for DS1631
#include "I2C_Temp_on_LCD.h" // load header and libraries
signed int16 temp; // varaible to stoer temperature
void main() {
boot_up();
init_display(); // initialise LCD display
init_I2C_temp(); // initialise temperature sensor (resolution...)
#if (I2C_Temp_Chip == 1 ) // display text depending on sensor used
printf(display_putc, " Temp MCP9803 ");
#else
printf(display_putc, " Temp DS1631");
#endif
while (true) { // main looptemp = readt_I2C_temp(); display_gotoxy(0,1); printf(display_putc, "%3.1w", temp); display_print_degree(); display_putc('C'); display_putc(' '); delay_ms(1000); } }
#define board_id 3
#define I2C_Temp_Chip 1 // Set I2C temperature chip to sensor 1 (MCP9803)
#include "lib\mainboard.c"
#include "lib\lcd_lib.c"
#include "lib\i2c_lib.c"
#include "lib\I2C_Temp_lib.c"
signed int16 temp;
void main() {
boot_up();
init_display();
init_I2C_temp();
while (true) {
temp = readt_I2C_temp();
#if (I2C_Temp_Chip == 1 )
printf(display_putc, "\fTemp MCP9803: %3.1w", temp);
#else
printf(display_putc, "\fTemp DS1631: %3.1w", temp);
#endif
display_print_degree();
display_putc('C');
delay_ms(1000);
}
}
The program defines the processor board used and the type of I2C temperature sensor used (1 or 2 for MCP9803 or DS1631). It is defined intially as 1, so we'll use an MCP9803 sensor.
We then loads the header file, which also loads the libraries for the processor, lcd display and I2C sensors.
Once all libraries are loaded, we declare a signed integer to store the temperature in celsius. The processor board, LCD display and temperature sensor are initialised..
The main loop is then entered: we read the temperature from the sensor in celsius, and display it on the LCD display. The temperature is displayed using 3 digits, and 1 decimal. Depending on the sensor type, the message displayed is slightly different. The \f caracter forces the display to blank before displaying the rest.
We finish with the degree sign, a C charater for Celsius and a delay of 1s, before starting the loop again.
This is the header file:
#ifndef _CONFIG_APP_H
#define CONFIG_APP_H
// following codes defines the platforms as well as the hardware configuration
#if ((board_id == 1) | (board_id == 2)) // if using board 1 or 2, use alphanumeric display
#define DISPLAY_TYPE "Alpha"
#else // else use the nokia graphic LCD
#define DISPLAY_TYPE "3310"
#endif
#include "../lib/Boards/mainboard.c" // load boards library
#include "../lib/Display/lcd_display.c" // load LCD display library
#include "../lib/Sensors/I2C_Temp.c" // Load I2C temperature sensor
#endif
It defines which LCD display to use based on the processor board and loads the required libraries.
Now the I2C temperature sensor library:
#ifndef _I2C_temp_LIB // make sure library is not already loaded
#define _I2C_temp_LIB
#include "../lib/Common/I2C.c" // load I2C generic library
#ifndef I2C_Temp_Chip // set chip used if not defined
#define I2C_Temp_Chip 1 // 1 for MCP9803, 2 for DS1631
#endif
void config_I2C_temp(BYTE data) { // senda byte to config register of sensor
i2c_start(); // start I2C communication
#if (I2C_Temp_Chip == 1 ) // point to config register
i2c_write(I2C_temp); // on sensor 1
i2c_write(0x01);
#else
i2c_write(I2C_temp2); // or sensor 2
i2c_write(0xac);
#endif
i2c_write(data); // and send data
i2c_stop(); // stop I2C communication
}
void init_I2C_temp() { // Initialise the temperature sensor
#if (I2C_Temp_Chip == 1 ) // Setup DAC to 12 bits
config_I2C_temp(0x60);
#else
config_I2C_temp(0x0c);
i2c_start(); // and start conversions for DS1631
i2c_write(I2C_temp2);
i2c_write(0x51);
i2c_stop();
#endif
}
WORD read_I2C_temp() { // reads temperature register from sensor
BYTE temph, templ;
signed int16 datat;
i2c_start();
#if (I2C_Temp_Chip == 1 )
i2c_write(I2C_temp);
i2c_write(0x00); // point to temperature register
i2c_start(); // sends a restart on the I2C bus
i2c_write(I2C_temp+1); // address sensor in read mode
#else
i2c_write(I2C_temp2);
i2c_write(0xAA); // point to temperature register
i2c_start(); // sends a restart on the I2C bus
i2c_write(I2C_temp2+1); // address sensor in read mode
#endif
temph=i2c_read(); // reads 2 bytes (msb first)
templ=i2c_read(0);
i2c_stop(); // stop I2C communication
datat=(signed int16) temph; // combine msb and lsb in 16 bits value
datat = datat<<4;
datat=datat + (templ >> 4); // the lsb of the value is on the upper 4 bits of the second read.
return(datat);
}
signed int16 readt_I2C_temp() { // Returns temperature in Celsius
WORD datat;
datat = read_I2C_temp();
return (float)datat*0.625;
}
#endif
It loads the generic I2C library, checks that a chip has been selected (default to MCP9803 if not) and declares some functions to control the chips:
- config_I2C_temp: sends a byte to the configuration register of the sensor
- init_I2C_temp: configure the sensor in 12 bits mode, and start the conversions
- read_I2C_temp: returns the temperature register of the sensor as a 16 bits value
- readt_I2C_temp: returns the temperature from the sensor in celsius
The I2C generic library is detailed below:_lib.c first check that it knows what processor board is used. It then loads a list of slave I2C addresses stored in I2C_Addr.h.
Depending on the processor, it then configure the I2C interface. For each processor, we defines the pin used for the SCL and SDA signals, and setup the I2C interface.
The I2C_present function returns 1 if the slave at the address specified is present on the I2C bus, or 0 if not.
#ifndef _I2C_LIB // load it only if hasn't been loaded already
#define _I2C_LIB
#include "../lib/Common/I2C.h" // load header file
BYTE I2C_present(BYTE addr) {
BYTE ack,present;
i2c_start();
ack = i2c_write(addr);
i2c_stop();
switch (ack) {
case 0:
present = 1;
break;
case 1:
present = 0;
break;
default:
present = 2;
break;
}
return present;
}
void i2c_poll(BYTE addr) {
while(true) {
i2c_start();
if (i2c_write(addr) == 0) {
break;
}
}
i2c_stop();
}
#endif
The header file defines how the I2C interface is configured, and load sone predefined I2C addresses for the tutorials.
It also defines 2 functions:
- I2C_present: checks if a component with a given address (addr) is on the I2C bus. It returms a byte with the following values: 0: device not detected (No Ack received), 1: device present (Ack received), 2: collision on the bus (In multimaster mode)
- i2c_poll: waits undefinitely until the device at address addr returns an Ack.
The header file of the I2C library defines how I2C is setup and configured on the processor board.
#ifndef _I2C_H
#define _I2C_H
#include "../lib/Common/I2C_Addr.h" // load I2C components addresses
#ifndef board_id
#error "variable board_id not defined"
#else //* 1: 16F819, 2: 16F628, 3: 16F873A, 4: 18F2320, 5: 18F26J11
#if (board_id == 1 )// Board 16F819
#ifndef P_SDA
#define P_SDA PIN_B1
#endif
#ifndef P_SCL
#define P_SCL PIN_B4
#endif
#use I2C(master, sda=P_SDA, scl=P_SCL, stream = I2C_stream)
#elif (board_id == 2) // Board 16F628
#ifndef P_SDA
#define P_SDA PIN_B1
#endif
#ifndef P_SCL
#define P_SCL PIN_B4
#endif
#use I2C(master, sda=P_SDA, scl=P_SCL)
#elif (board_id == 3) // Board 16F873A
#ifndef P_SDA
#define P_SDA PIN_C4
#endif
#ifndef P_SCL
#define P_SCL PIN_C3
#endif
#use I2C(master, sda=P_SDA, scl=P_SCL, FORCE_HW, stream = I2C_stream)
#elif (board_id == 4) // Board 18F2320
#ifndef P_SDA
#define P_SDA PIN_C4
#endif
#ifndef P_SCL
#define P_SCL PIN_C3
#endif
#use I2C(master, sda=P_SDA, scl=P_SCL, stream = I2C_stream)
#elif (board_id == 5) // Board 18F26J11
#ifndef P_SDA
#define P_SDA PIN_C4
#endif
#ifndef P_SCL
#define P_SCL PIN_C3
#endif
#use I2C(master, sda=P_SDA, scl=P_SCL, stream = I2C_stream)
#else
#error "Board board_id not supported"
#endif
#endif
// function prototypes
BYTE I2C_present(BYTE addr);
void i2c_poll(BYTE addr);
#endif
I2C_Addr.h simply defines I2C components with their slave I2C address. The address can usually be configured by microswitches on the boards.
#ifndef _I2C_ADDR_H
#define _I2C_ADDR_H
// Boards Addresses
#define I2C_clock 0xA0 // I2C clock (PCF8583)
#define I2C_clock2 0xD0 // I2C clock2 (DS1307)
#define I2C_eeprom 0xA2 // I2C EEPROM (24C64)
#define I2C_temp 0x90 // I2C Temperature sensor (MCP9803)
#define I2C_temp2 0x92 // I2C Temperature sensor 2 (DS1631)
#define I2C_keyb 0x40 // I2C keyboard
#define I2C_8bits 0x42 // I2C 8 Bits (PCF8574)
#define I2C_IOexp 0x44 // I2C IO extender (MCP23016)
#define I2C_Accel 0x3A // I2C Accelerometer (LIS302)
#define I2C_GLCD 0x7A // I2C/SPI graphic LCD display
#endif
Fig. 1b Board 3 with LCD display and I2C temperature sensor 2
Compile the project and program the target with a PicKIT 2 programmer for example. Connect the processor board to the LCD display board on port B and the I2C temperature sensor on the I2C connector, making sure the jumpers on the board match the slave address defined in I2C_Addr.h.
Power up the boards.
The temperature from the sensor will be displayed on the first line of the display.
To change sensor type, just define I2C_Temp_Chip as 2 and recompile. The program is then set for a DS1631 sensor.
Files and links:
Tutorial 4 source files.Processor board 3.
LCD Display board.
I2C temperature sensor 1.
I2C temperature sensor 2.

