Chapter 6: Examples with PIC Integrated Peripherals


Introduction

It is commonly said that microcontroller is an “entire computer on a single chip”, which implies that it has more to offer than a single CPU (microprocessor). This additional functionality is actually located in microcontroller’s subsystems, also called the “integrated peripherals”. These (sub)devices basically have two major roles: they expand the possibilities of the MCU making it more versatile, and they take off the burden for some repetitive and “dumber” tasks (mainly communication) from the CPU.

Every microcontroller is supplied with at least a couple of integrated peripherals – commonly, these include timers, interrupt mechanisms and AD converters. More powerful microcontrollers can command a larger number of more diverse peripherals. In this chapter, we will cover some common systems and the ways to utilize them from BASIC programming language.

6.1 Interrupt Mechanism

Interrupts are mechanisms which enable instant response to events such as counter overflow, pin change, data received, etc. In normal mode, microcontroller executes the main program as long as there are no occurrences that would cause an interrupt. Upon interrupt, microcontroller stops the execution of main program and commences the special part of the program which will analyze and handle the interrupt. This part of program is known as the interrupt (service) routine.

In BASIC, interrupt service routine is defined by procedure with reserved name interrupt. Whatever code is stored in that procedure, it will be executed upon interrupt.

First, we need to determine which event caused the interrupt, as PIC microcontroller calls the same interrupt routine regardless of the trigger. After that comes the interrupt handling, which is executing the appropriate code for the trigger event.

Here is a simple example:

In the main loop, program keeps LED_run diode on and LED_int diode off. Pressing the button T causes the interrupt – microcontroller stops executing the main program and starts the interrupt procedure.

program testinterrupt

symbol LED_run = PORTB.7                  ' LED_run is connected to PORTB pin 7
symbol LED_int = PORTB.6                  ' LED_int is connected to PORTB pin 6


sub procedure interrupt                   ' Interrupt service routine

  if INTCON.RBIF = 1 then                 ' Changes on RB4-RB7 ?
    INTCON.RBIF = 0

  else if INTCON.INTF = 1 then            ' External interupt (RB0 pin) ?
          LED_run = 0
          LED_int = 1
          Delay_ms(500)
          INTCON.INTF = 0

       else if INTCON.T0IF = 1 then       ' TMR0 interrupt occurred ?
               INTCON.T0IF = 0

            else if INTCON.EEIF = 1 then  ' Is EEPROM write cycle finished ?
                    INTCON.EEIF = 0

                 end if
            end if
        end if
  end if
end sub


main:

  TRISB = %00111111        ' Pins RB6 and RB7 are output
  OPTION_REG = %10000000   ' Turn off pull-up resistors
                           '     and set interrupt on falling edge
                           '     of RB0 signal
  INTCON = %10010000       ' Enable external interrupts
  PORTB = 0                ' Initial value on PORTB

eloop:                     ' While there is no interrupt, program runs in endless loop:
  LED_run = 1              '     LED_run is on
  LED_int = 0              '     LED_int is off
goto eloop

end.

Now, what happens when we push the button? Our interrupt routine first analyzes the interrupt by checking flag bits with couple of if..then instructions, because there are several possible interrupt causes. In our case, an external interrupt took place (pin RB0/INT state changes) and therefore bit INTF in INTCON register is set. Microcontroller will change LED states, and provide a half second delay for us to actually see the change. Then it will clear INTF bit in order to enable interrupts again, and return to executing the main program.

In situations where microcontroller must respond to events unrelated to the main program, it is very useful to have an interrupt service routine. Perhaps, one of the best examples is multiplexing the seven-segment display – if multiplexing code is tied to timer interrupt, main program will be much less burdened because display refreshes in the background.

6.2 Internal AD Converter

A number of microcontrollers have built in Analog to Digital Converter (ADC). Commonly, these AD converters have 8-bit or 10-bit resolution allowing them voltage sensitivity of 19.5mV or 4.8mV, respectively (assuming that default 5V voltage is used).

The simplest AD conversion program would use 8-bit resolution and 5V of microcontroller power as referent voltage (value which the value "read" from the microcontroller pin is compared to). In the following example we measure voltage on RA0 pin which is connected to the potentiometer (see the figure below).

Potentiometer gives 0V in one terminal position and 5V in the other – since we use 8-bit conversion, our digitalized voltage can have 256 steps. The following program reads voltage on RA0 pin and displays it on port B diodes. If not one diode is on, result is zero and if all of diodes are on, result is 255.

program ADC_8

main:

TRISA = %111111      ' Port A is input
PORTD = 0
TRISD = %00000000

ADCON1 = %1000010    ' Port A is in analog mode,
                     '   0 and 5V are referent voltage values,
                     '   and the result is aligned right
                     '   (higher 6 bits of ADRESH are zero).

ADCON0 = %11010001   ' ADC clock is generated by internal RC
                     '   circuit; voltage is measured on RA2 and
                     '   allows the use of AD converter

Delay_ms (500)       ' 500 ms pause

eloop:
  ADCON0.2 = 1        ' Conversion starts

wait:

' wait for ADC to finish
Delay_ms(5)
if ADCON0.2 = 1 then
   goto wait
end if

PORTD = ADRESH       ' Set lower 8 bits on port D
Delay_ms(500)        ' 500 ms pause
goto eloop           ' Repeat all
end.                 ' End of program.

First, we need to properly initialize registers ADCON1 and ADCON0. After that, we set ADCON0.2 bit which initializes the conversion and then check ADCON0.2 to determine if conversion is over. If over, the result is stored into ADRESH and ADRESL where from it can be copied.

Former example could also be carried out via ADC_Read instruction. Our following example uses 10-bit resolution:

program ADC_10

dim AD_Res as word


main:
TRISA  = %11111111       ' PORTA is input

TRISD  = %00000000       ' PORTD is output

ADCON1 = %1000010        ' PORTA is in analog mode, 
                         '   0 and 5V are referent voltage values,
                         '   and the result is aligned right
eloop:
  AD_Res = ADC_read(2)   ' Execute conversion and store result
                         '   in variable AD_Res.

PORTD = Lo(AD_Res)       ' Display lower byte of result on PORTD
Delay_ms(500)            ' 500 ms pause
goto eloop               ' Repeat all
end.                     ' End of program

As one port is insufficient, we can use LCD for displaying all 10 bits of result. Connection scheme is below and the appropriate program follows. For more information on LCD routines, check Chapter 5.2: Library Routines.

program ADC_on_LCD

dim AD_Res as word
dim dummyCh as char[6]

main:

TRISA  = %1111111          ' PORTA is input
TRISB  = 0                 ' PORTB is output (for LCD)

ADCON1 = %10000010         ' PORTA is in analog mode,
                           '   0 and 5V are referent voltage values,
                           '   and the result is aligned right.

Lcd_Init(PORTB)            ' Initialize LCD
Lcd_Cmd(LCD_CLEAR)         ' Clear LCD
Lcd_Cmd(LCD_CURSOR_OFF)    '   and turn the cursor off

eloop:

AD_Res = ADC_Read(2)       ' Execute conversion and store result
                           '   to variable AD_Res
LCD_Out(1, 1, "      ")    ' Clear LCD from previous result
WordToStr(AD_Res, dummyCh) ' Convert the result in text,
LCD_Out(1, 1, dummyCh)     '   and print it in line 1, char 1

Delay_ms(500)              ' 500 ms pause
goto eloop                 ' Repeat all
end.                       ' End of program

6.3 TMR0 Timer

TMR0 timer is an 8-bit special function register with working range of 256. Assuming that 4MHz oscillator is used, TMR0 can measure 0-255 microseconds range (at 4MHz, TMR0 increments by one microsecond). This period can be increased if prescaler is used. Prescaler divides clock in a certain ratio (prescaler settings are made in OPTION_REG register).

Our following program example shows how to generate 1 second using TMR0 timer. For visual purposes, program toggles LEDs on PORTB every second.

Before the main program, TMR0 should have interrupt enabled (bit 2) and GIE bit (bit 7) in INTCON register should be set. This will enable global interrupts.

program Timer0_1sec

dim cnt as byte
dim  a as byte
dim  b as byte

sub procedure interrupt
  cnt = cnt + 1        ' Increment value of cnt on every interrupt
  TMR0   = 96
  INTCON = $20         ' Set T0IE, clear T0IF
end sub

main:

a = 0
b = 1
OPTION_REG = $84      ' Assign prescaler to TMR0
TRISB  =   0          ' PORTB as output
PORTB  = $FF          ' Initialize PORTB
cnt =   0             ' Initialize cnt
TMR0   =  96
INTCON = $A0          ' Enable TMRO interrupt

' If cnt is 200, then toggle PORTB LEDs and reset cnt
do
  if cnt = 200 then
    PORTB  = not(PORTB)
    cnt = 0
  end if
loop until 0 = 1

end.

Prescaler is set to 32, so that internal clock is divided by 32 and TMR0 increments every 31 microseconds. If TMR0 is initialized at 96, overflow occurs in (256-96)*31 us = 5 ms. We increase cnt every time interrupt takes place, effectively measuring time according to the value of this variable. When cnt reaches 200, time will total 200*5 ms = 1 second.

6.4 TMR1 Timer

TMR1 timer is a 16-bit special function register with working range of 65536. Assuming that 4MHz oscillator is used, TMR1 can measure 0-65535 microseconds range (at 4MHz, TMR1 increments by one microsecond). This period can be increased if prescaler is used. Prescaler divides clock in a certain ratio (prescaler settings are made in T1CON register).

Before the main program, TMR1 should be enabled by setting the zero bit in T1CON register. First bit of the register defines the internal clock for TMR1 – we set it to zero. Other important registers for working with TMR1 are PIR1 and PIE1. The first contains overflow flag (zero bit) and the other is used to enable TMR1 interrupt (zero bit). With TMR1 interrupt enabled and its flag cleared, we only need to enable global interrupts and peripheral interrupts in the INTCON register (bits 7 and 6, respectively).

Our following program example shows how to generate 10 seconds using TMR1 timer. For visual purposes, program toggles LEDs on PORTB every 10 seconds.

program Timer1_10sec

dim cnt as byte

sub procedure interrupt
  cnt = cnt + 1
  pir1.0 = 0      ' Clear TMR1IF
end sub

main:

TRISB = 0
T1CON = 1
PIR1.TMR1IF = 0         ' Clear TMR1IF
PIE1  =   1             ' Enable interrupts
PORTB = $F0
cnt =   0               ' Initialize cnt
INTCON = $C0

' If cnt is 152, then toggle PORTB LEDs and reset cnt
  do
    if cnt = 152 then
      PORTB  = not(PORTB)
      cnt = 0
    end if
  loop until 0 = 1

end.

Prescaler is set to 00 so there is no dividing the internal clock and overflow occurs every 65.536 ms. We increase cnt every time interrupt takes place, effectively measuring time according to the value of this variable. When cnt reaches 152, time will total 152*65.536 ms = 9.96 seconds.

6.5 PWM Module

Microcontrollers of PIC16F87X series have one or two built-in PWM outputs (40-pin casing allows 2, 28-pin casing allows 1). PWM outputs are located on RC1 and RC2 pins (40-pin MCUs), or on RC2 pin (28-pin MCUs). Refer to PWM library (Chapter 5.2: Library Routines) for more information.

The following example uses PWM library for getting various light intensities on LED connected to RC2 pin. Variable which represents the ratio of on to off signals is continually increased in the loop, taking values from 0 to 255. This results in continual intensifying of light on LED diode. After value of 255 has been reached, process begins anew.

program PWM_LED_Test

dim j as byte

main:

TRISB = 0             ' PORTB is output
PORTB = 0             ' Set PORTB to 0
j     = 0
TRISC = 0             ' PORTC is output
PORTC = $FF           ' Set PORTC to $FF
PWM_Init(5000)        ' Initialize PWM module
PWM_Start             ' Start PWM

while true            ' Endless loop
  Delay_ms(10)        ' Wait 10ms
  j = j + 1           ' Increment j
  PWM_Change_Duty(j)  ' Set new duty ratio
  PORTB =  CCPR1L     ' Send value of CCPR1L to PORTB
wend

end.

6.6 Hardware UART module (RS-232 Communication)

The easiest way to transfer data between microcontroller and some other device, e.g. PC or other microcontroller, is the RS-232 communication (also referred to as EIA RS-232C or V.24). RS232 is a standard for serial binary data interchange between a DTE (Data terminal equipment) and a DCE (Data communication equipment), commonly used in personal computer serial ports. It is a serial asynchronous 2-line (Tx for transmitting and Rx for receiving) communication with effective range of 10 meters.

Microcontroller can establish communication with serial RS-232 line via hardware UART (Universal Asynchronous Receiver Transmitter) which is an integral part of PIC16F87X microcontrollers. UART contains special buffer registers for receiving and transmitting data as well as a Baud Rate generator for setting the transfer rate.

This example shows data transfer between the microcontroller and PC connected by RS-232 line interface MAX232 which has role of adjusting signal levels on the microcontroller side (it converts RS-232 voltage levels +/- 10V to TTL levels 0-5V and vice versa).

Our following program example illustrates use of hardware serial communication. Data received from PC is stored into variable dat and sent back to PC as confirmation of successful transfer. Thus, it is easy to check if communication works properly. Transfer format is 8N1 and transfer rate is 2400 baud.

program USART_Echo

dim dat as byte

main:

USART_Init(2400)                  ' Initialize USART module
while true
  if USART_Data_Ready = 1 then    ' If data is received
    dat = USART_Read              ' Read the received data
    USART_Write(dat)              ' Send data via USART
  end if
wend

end.

In order to establish the communication, PC must have a communication software installed. One such communication terminal is part of mikroBasic IDE. It can be accessed by clicking Tools > Terminal from the drop-down menu. Terminal allows you to monitor transfer and to set all the necessary transfer settings. First of all, we need to set the transfer rate to 2400 to match the microcontroller's rate. Then, select the appropriate communication port by clicking one of the 4 available (check where you plugged the serial cable).

After making these adjustments, clicking Connect starts the communication. Type your message and click Send Message – message will be sent to the microcontroller and back, where it will be displayed on the screen.

Note that serial communication can also be software based on any of 2 microcontroller pins – for more information, check the Chapter 9: Communications.