« Back to home

Digital IO (PIC16F628A)

This is another article in the PIC Tutorial series. In this post we cover the basics of digital IO on the PIC 16F628A chip as well as covering the basics of using the timer and storing persistent data in the EEPROM.

Introduction

This part of the PIC tutorial introduces digital input and output. We have used some digital output previously (the first post simply toggled an output pin to flash an LED) but we are going to look at it in a little more detail this time around.

We are also going to look at some more advanced features of the PIC chip such as the built in EEPROM and the timer peripheral.

Digital Tutorial

The project we are building is a Knight Rider style display using 8 LED's. The pattern is defined as a sequence of bytes (stored in the EEPROM) where each bit controls one LED. The timer module will be used to provide a constant delay between each step in the pattern. A push button is used to trigger a full cycle of the pattern and an additional output is used to indicate if the pattern is currently active or not.

The full set of project files (sources, MPLAB project and circuit diagrams) can be downloaded here.

PIC Digital IO Architecture

The Two IO Ports

The 16F628A has two digital IO ports (port A and port B) each consisting of eight IO pins where each pin can be configured as either an input or an output. The state of each port is modified or read through the port register for each port (registers PORTA and PORTB). The direction of each pin is controlled through two additional registers (TRISA and TRISB) and there are additional flag registers to enable and disable pin specific features.

Changing the state of an output (or reading the state of an input) is accomplished either by using the bit manipulation operations on the port registers to change individual pin states or by reading or writing the entire port register as an entire byte.

Not all IO pins are exactly the same, some have multiple purposes and will not be available for general use when certain peripherals are enabled (for example the USART serial controller will use bits 1 and 2 of PORTB for reception and transmission respectively). Other pins have special features that can be enabled if needed.

While the majority of the IO pins operate as standard CMOS pins there is one (RA4) that operates as an open drain. This needs to be treated differently and is probably best to avoid at this stage.

A number of pins have an internal weak pull up resistor that can be enabled. When configured as inputs with the pull up resistor enabled the input will be held at a logic '1' until the external device pulls the pin to ground. This is useful for input devices (such as push buttons or switches) that can hold the input in a floating or unconnected state.

Other pins have a Schmidt trigger on the input which helps alleviate issues such as contact bounce or unstable signal input.

In this project we are not going to explicitly use any of these additional features - we are only going to use standard CMOS level IO. The input pin we use for the push button does have a Schmidt Trigger on the input which will alleviate bounce but in this case the software is resistant to multiple triggers that could be caused by bounce so it makes little difference.

To simplify the code we are going to use all 8 bits of PORTB as outputs to drive the LED's, this will allow us to update the pattern with a single write to the output register. We will use RA0 as an active low input for the trigger and RA1 as the pattern active output.

Output Load Current

Each output pin is capable of driving up to a 25mA load with a total simultaneous load of 200mA. If we wire up the LED's directly, so they pull all their operating current from the output pin, using a 150 Ohm current limiting resistor we will pull around 20mA from each pin when the LED is on. We are using 9 LED's (8 for the pattern display and one more to show the status of the 'pattern active' output) so in the worst case scenario (all LED's are on) we will pull a total of 180mA through the PIC which is approaching the total current limit of 200mA for all pins.

Indirect Driving

In order to keep the circuit simple we are directly driving the LED's through the output pins and in this case we can get away with it. If we were to add any more outputs though (or use super bright LED's that pull more current) we would need to take some precautions to limit the current draw. One option would be to drive the output through an NPN BJT Transistor as shown in the circuit to the right. A fuller description is available in the article Driving LEDs.

The Circuit

Circuit Diagram

Once again I've used Fritzing to draw up the circuit diagram and breadboard layout. The circuit itself (shown to the right) is fairly simple - each pin of output port B is used to drive an LED. We use pin 1 of port A to indicate a pattern is playing and this also drives an LED.

The trigger input is active low and is driven by a push button. We use a pull up resistor to hold the input high until the button is pressed and provides a path to ground, pulling the input low.

Breadboard Layout

The breadboard layout (shown to the left) is a little bit tricky for this one given the number of LED's and driver resistors involved. Note that the red wires are overlaying some of the connections for the LED's (mainly the connections to ground). You will need to take this into account when wiring up the breadboard and be double check all your connections carefully.

The power connector is provided by the two pins in the upper left of the breadboard, the positive input is the pin to the left, the negative (ground) input is the one to the right. You can use power from a USB charger (using a modified USB cable as detailed in my tips and tricks post).

You will definitely need to use a standalone USB charger to power this circuit as it the it pulls might be pushing the limit of what a standard USB port on a computer can provide.

Actual Layout

I cheated a little bit and used by breadboard workstation with it's built in power supply and the LED breadboard module I made earlier. This made building it up on a breadboard much easier.

The Software

The full software listing is available ``` ;============================================================================ ; Button triggered 'Knight Rider' animation for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 20-OCT-2012 shaneg ; ; Use 8 LED's to emulate the 'Knight Rider' animation that appears on the ; front of KITT. ;============================================================================

             list P = 16F628A     ; Identify the chip to use                 include  P16F628A.INC

             ; Set the configuration bits for this application                 __config _WDT_OFF & _BOREN_OFF & _INTOSC_OSC_NOCLKOUT

;---------------------------------------------------------------------------- ; Constants and definitions ;----------------------------------------------------------------------------

PATMAXBYTES equ 0x20 ; Maximum bytes in a pattern PATFACTIVE equ 1 ; Bit to use as the 'active' flag

; Registers (internal state) TMRCOUNT equ 0x20 ; Used as an addition / 256 prescaler PATCURRENT equ 0x21 ; The current index into the pattern PAT_FLAGS equ 0x22 ; Flags to control pattern operation

; Registers (scratch/working) SCRATCH1 equ 0x23 ; Scratchpad register SCRATCH2 equ 0x24 ; Scratchpad register

; Registers (State information for interrupt handler - in shared memory area) SAVEDW equ 0x70 SAVEDSTATUS equ 0x71

; Registers (pattern data) PATLENGTH equ 0x2E ; Register to hold length (from EEPROM) PATDATA equ 0x2F ; Registers containing pattern data

;---------------------------------------------------------------------------- ; EEPROM data ;----------------------------------------------------------------------------

             org     0x2100

; Pattern information ; ; This data is programmed into the EEPROM at programming time. The first byte ; defines the number of bytes in the pattern, subsequent bytes specify the ; pattern to emit at each cycle. This program supports a maximum of 32 bytes ; of pattern data.

; This pattern does a Knight Rider style pattern (edge to center and back) ;EEPROM de 0x0C ; de 0x00, 0x81, 0xC3, 0x66, 0x3C, 0x18 ; de 0x18, 0x3C, 0x66, 0xC3, 0x81, 0x00

; This pattern is like a 'Cylon' eye (left to right and back) EEPROM de 0x13 de 0x00, 0x80, 0xC0, 0x60, 0x30, 0x18 de 0x0C, 0x06, 0x03, 0x01, 0x03, 0x06 de 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80 de 0x00

; This pattern starts empty and expands to fill the bar ;EEPROM de 0x05 ; de 0x00, 0x18, 0x3C, 0x7E, 0xFF

;---------------------------------------------------------------------------- ; Interrupt vector table ;----------------------------------------------------------------------------

             ; Reset vector                 org     0x0000                 goto    program

             ; Interrupt vector                 org     0x0004                 goto    interrupt

;---------------------------------------------------------------------------- ; Functions and routines ;----------------------------------------------------------------------------

;---------------------------------------------------------------------------- ; Interrupt handler ;----------------------------------------------------------------------------

timerinterrupt: ; This handler deals with interrupts from Timer0 ; Clear the timer interrupt banksel INTCON bcf INTCON, T0IF ; Update our internal prescaler banksel TMRCOUNT incf TMRCOUNT, F btfss STATUS, Z goto timerdone ; Do we need to update the output ? btfss PATFLAGS, PATFACTIVE goto timerdone ; Output the current pattern value movlw PATDATA movwf FSR movfw PATCURRENT addwf FSR, F movfw INDF movwf PORTB ; Move to the next value incf PATCURRENT, F ; Have we hit the end ? ;incf PATLENGTH, W movfw PATLENGTH subwf PATCURRENT, W btfss STATUS, Z goto timerdone ; We've hit the end, flag the sequence as complete bcf PATFLAGS, PATFACTIVE bcf PORTA, 1 timer_done: return

interrupt: ; Save current state movwf SAVEDW ; Save W register first movf STATUS, W ; Move the STATUS register to W movwf SAVEDSTATUS ; And save that as well ; Process and clear outstanding interrupts banksel INTCON btfsc INTCON, T0IF ; Check for timer interrupt call timerinterrupt ; Restore state movf SAVEDSTATUS, W movwf STATUS swapf SAVEDW, F ; Loads saved W without changing STATUS swapf SAVEDW, W retfie

;---------------------------------------------------------------------------- ; Main program ;----------------------------------------------------------------------------

program: ; Configure IO pins banksel PORTA clrf PORTA ; Clear outputs clrf PORTB banksel TRISA clrf TRISA bsf TRISA, 0 ; RA0 is the button input pin clrf TRISB ; All of PORTB is output ; Configure the timer banksel OPTIONREG movlw 0xC0 andwf OPTIONREG, W movwf SCRATCH1 ; Save the option bytes we do not change movlw 0x11 iorwf SCRATCH1, W ; Timer mode, prescaler (/4), internal movwf OPTIONREG ; Read the number of bytes in the pattern banksel EEADR clrw movwf EEADR banksel EECON1 bsf EECON1, RD banksel EEDATA movfw EEDATA banksel PATLENGTH movwf PATLENGTH movlw PATMAXBYTES + 1 subwf PATLENGTH, W btfss STATUS, C goto readpattern ; Specified byte range is too large, trim it movlw PATMAXBYTES movwf PATLENGTH readpattern: ; Set up loop to read pattern data from EEPROM movlw PATDATA movwf FSR ; Base pointer for memory pattern banksel EEADR movlw 1 movwf EEADR ; Base EEPROM address banksel PATLENGTH movfw PATLENGTH movwf SCRATCH1 ; Loop counter readloop: movfw SCRATCH1 btfsc STATUS, Z goto endofloop decf SCRATCH1, F ; Read the next entry from the EEPROM banksel EECON1 bsf EECON1, RD banksel EEDATA movfw EEDATA movwf INDF ; Step forward by one byte incf FSR, F banksel EEADR incf EEADR, F banksel SCRATCH1 goto readloop endofloop: ; Enable interrupts banksel INTCON bsf INTCON, T0IE ; Enable Timer0 interrupt bsf INTCON, GIE ; Enable global interrupts mainloop: banksel PATFLAGS ; See if a pattern is active btfsc PATFLAGS, PATFACTIVE goto mainloop ; Check for pattern trigger btfsc PORTA, 0 goto mainloop ; Set flags to trigger pattern running clrw movwf PATCURRENT bsf PATFLAGS, PATFACTIVE bsf PORTA, 1 ; Set the 'active' output high goto mainloop

             end ```

and, once again, it is based on my ``` ;============================================================================ ; Template program for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 28-SEP-2012 shaneg ; ; Use this file as a template for new projects. Sets up a common code layout ; and basic coding style. ;============================================================================

             list P = 16F628A     ; Identify the chip to use                 include  P16F628A.INC

             ; Set the configuration bits for this application                 __config _WDT_OFF & _BOREN_OFF & _INTOSC_OSC_NOCLKOUT

;---------------------------------------------------------------------------- ; Constants and definitions ;----------------------------------------------------------------------------

;---------------------------------------------------------------------------- ; Interrupt vector table ;----------------------------------------------------------------------------

             ; Reset vector                 org     0x0000                 goto    program

             ; Interrupt vector                 org     0x0004                 goto    interrupt

;---------------------------------------------------------------------------- ; Functions and routines ;----------------------------------------------------------------------------

;---------------------------------------------------------------------------- ; Interrupt handler ;----------------------------------------------------------------------------

interrupt: ; Save current state ; Process and clear outstanding interrupts ; Restore state retfie

;---------------------------------------------------------------------------- ; Main program ;----------------------------------------------------------------------------

program: ; Configure periphials and IO mainloop: ; Main program loop (runs forever) goto mainloop

             end ``` . The remainder of this post will go through the salient parts of the code to illustrate how they work. You might find it helpful to keep the full source listing open in another browser window to follow along.

Setting up the IO Pins

One of the first things we do in the program is to set up the IO pins in the state we want. There are two ports available on the PIC, each consisting of 8 IO pins that can be configured as either input or output. The direction of each of the pins is controlled by the TRISA and TRISB registers (for port A and port B respectively). Setting the appropriate pin to high (1) in the register designates the pin as an input, setting it to low (0) designates the pin as an output. As we only have a single input pin (RA0) our set up is fairly simple:

 When selecting what pins to use for your own projects you need to be careful. Many of the pins have alternative functions and are tied to specific peripherals on board the chip such as a UART, PWM module, etc. When these modules are active those pins will not be available for general IO.


## Using the EEPROM

 The 16F628A has 128 bytes of on-board EEPROM. This is memory that keeps whatever is stored on it through power cycles, think of it as a very small hard disk. This memory is not accessed in the same way as the RAM registers are, you need to read and write it through SFRs (**S**pecial **F**unction **R**egisters). The **EEADR** register is used to specify which byte of the EEPROM to access, for the 16F628A the valid range is from 0 to 127 inclusive. The **EECON1** register controls the operation to be performed (a read or a write).

 The contents of the EEPROM can also be set when you burn your program - there is a small trick to this though. If you define byte data starting at address 0x2100 this will be written into EEPROM memory instead of into the program memory. You use an **org** statement in the assembly file to define the start address for the next statements. Be sure to use another **org** statement before your actual assembly code to ensure it is written to the right spot. The following snippet of code shows how we define the pattern (I've included a number of different patterns as examples, simply uncomment the one you want to be displayed before assembling the program and burning the chip).
                 org     0x2100

 ; Pattern information     ;     ; This data is programmed into the EEPROM at programming time. The first byte     ; defines the number of bytes in the pattern, subsequent bytes specify the     ; pattern to emit at each cycle. This program supports a maximum of 32 bytes     ; of pattern data.

 ; This pattern does a Knight Rider style pattern (edge to center and back)     ;EEPROM          de      0x0C     ;                de      0x00, 0x81, 0xC3, 0x66, 0x3C, 0x18     ;                de      0x18, 0x3C, 0x66, 0xC3, 0x81, 0x00

 ; This pattern is like a 'Cylon' eye (left to right and back)     EEPROM          de      0x13                     de      0x00, 0x80, 0xC0, 0x60, 0x30, 0x18                     de      0x0C, 0x06, 0x03, 0x01, 0x03, 0x06                     de      0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80                     de      0x00

 ; This pattern starts empty and expands to fill the bar     ;EEPROM          de      0x05     ;                de      0x00, 0x18, 0x3C, 0x7E, 0xFF

In our case we save the pattern sequence in EEPROM as a byte count followed by a one byte pattern for each step in the sequence. When we initialise the our program we read that data from the EEPROM into normal RAM registers to work with (reading from the EEPROM at every step would be a bit time consuming and is not really necessary). I have set an upper limit of 32 bytes per pattern just so we know the maximum number of registers we need to keep reserved to store them in. In the code shown below we read the number of bytes, make sure it is within range, and then read the data bytes into the registers.

 Once the values are read into RAM we don't need to access the EEPROM any more.


## Using the Timer

 I'm only going to briefly describe the operation of the timer module here, I'll cover it in more detail in the next post in the series where we will be using it to generate timed pulses for PWM generation.

 The 16F628A has three timers - 2 which are 8 bit and another which is 16 bit. We are using TMR0, one of the 8 bit timers for simplicity. The timer is essentially a counter that increments once per clock pulse, when the counter rolls over (goes from 255 to 0 again) an interrupt is generated. This interrupt can be used to provide accurate timing of periodic operations.

 The clock pulse that is fed into the timer is generated from but not exactly the same as the main clock speed. The base rate is the instruction cycle which is one quarter of the main clock rate - for an 8MHz main clock the timer input will be 2MHz.

 The timer has a prescaler than can be applied to this input as well allowing you to further divide it by 2, 4, 8, etc up to 256 to stretch out the duration between the interrupts. In this code we use our own internal counter to stretch the period out even further so we change the state of the display about eight times a second. Our interrupt handler for the timer interrupt does this first whenever it is invoked:
                 ; Clear the timer interrupt                     banksel INTCON                     bcf     INTCON, T0IF                     ; Update our internal prescaler                     banksel TMR_COUNT                     incf    TMR_COUNT, F                     btfss   STATUS, Z                     goto    timer_done                     ; Rest of code goes here     timer_done:

Putting it Together

Rather than continuously display the pattern we use the input from a button to trigger the start of the cycle and only run through it once. We use a bit in a working register as a flag to signal if a pattern is active or not.

The remainder of the timer interrupt simply walks through the stored pattern values and outputs them to PORTB. The code looks like this:

``` ; Do we need to update the output ? btfss PATFLAGS, PATFACTIVE goto timerdone ; Output the current pattern value movlw PATDATA movwf FSR movfw PATCURRENT addwf FSR, F movfw INDF movwf PORTB ; Move to the next value incf PATCURRENT, F ; Have we hit the end ? movfw PATLENGTH subwf PATCURRENT, W btfss STATUS, Z goto timerdone ; We've hit the end, flag the sequence as complete bcf PATFLAGS, PATFACTIVE bcf PORTA, 1 timerdone: return

Overall it's a fairly simple operation but something that a PIC is very well suited for.

What Comes Next?

I've tried to design this project so it can be used as a standalone project to add some bling to a hardware project. You can use the push button input as a trigger and the 'pattern active' output to determine when it should be triggered again. Some slight changes to the code and you can have it repeat the pattern endlessly without need of a trigger. There are also enough spare pins to add a 'programming mode' and allow new patterns to be loaded into the EEPROM over a serial connection (or via I2C).

In the next post in the series we'll start looking at something a bit more interesting than flashing lights and learn how to generate PWM output using a PIC which will use a lot of the techniques we've covered in this post.

This article has taken a very long time to complete and has wound up far longer than I would have preferred and yet contains far less detail than I had hoped. For future articles I'm going to try and concentrate on smaller, more digestible chunks even if it means stretching out the tutorial series over a larger number of articles.

I hope this series has been interesting and useful so far - I look forward to hearing your feedback.