PIC internals

This chapter covers main and widely used PIC microcontroller internals (also referred as PIC peripherals in datasheets), like PWM, ADC, etc... For each section, you'll find some basic theory explaining how things works, then a real-life example.

PWM Intro - Pulse Width Modulation

Image icon pwm_freq.png1.24 KB
Image icon pwm_duty.png1.5 KB
Author: Sébastien Lelong | Jallib Group

In the following tutorials, we're going to (try to) have some fun with PWM. PWM stands for Pulse Width Modulation, and is quite weird when you first face this (this was at least my first feeling). So here's a brief explanation of what it is about.

How does PWM look like ?...

PWM is about switching one pin (or more) high and low, at different frequencies and duty cycles. This is a on/off process. You can either vary:
  • the frequency,
  • or the duty cycle, that is the proportion where the pin will be high
Figure 1. PWM: same duty cycle, different frequencies.

Both have a 50% duty cycle (50% on, 50% off), but the upper one's frequency is twice the bottom

Figure 2. PWM: same frequency, different duty cycles

Three different duty cycle (10%, 50% and 90%), all at the same frequency

But what is PWM for ? What can we do with it ? Many things, like:
  • producing variable voltage (to control DC motor speed, for instance)
  • playing sounds: duty cycle is constant, frequency is variable
  • playing PCM wave file (PCM is Pulse Code Modulation)
  • ...

That said, we're now goind to experiment these two major properties.

PWM (Part 1) - Dimming a led with PWM

One PWM channel + one LED = fun

For now, and for this first part, we're going to see how to control the brightness of a LED. If simply connected to a pin, it will light at its max brightness, because the pin is "just" high (5V).

Now, if we connect this LED on a PWM pin, maybe we'll be able to control the brightness: as previously said, PWM can be used to produce variable voltages. If we provide half the value (2.5V), maybe the LED will be half its brightness (though I guess the relation between voltage and brightness is not linear...). Half the value of 5V. How to do this ? Simply configure the duty cycle to be 50% high, 50% low.

But we also said PWM is just about switching a pin on/off. That is, either the pin will be 0V, or 5V. So how will we be able to produce 2.5V ? Technically speaking, we won't be able to produce a real 2.5V, but if PWM frequency is high enough, then, on the average, and from the LED's context, it's as though the pin outputs 2.5V.

Building the circuit

Enough theory, let's get our hands dirty. Connecting a LED to a PWM pin on a 16f88 is quite easy. This PIC has quite a nice feature about PWM, it's possible to select which pin, between RB0 and RB3, will carry the PWM signals. Since I use tinybootloader to upload my programs, and since tiny's fuses are configured to select the RB0 pin, I'll keep using this one (if you wonder why tinybootloader interferes here, read this post).

Figure 1. Connecting a LED to a PWM pin

On a breadboard, this looks like this:

The connector brings +5V on the two bottom lines (+5V on line A, ground on line B).

LED is connected to RB0

Writing the software

For this example, I took one of the 16f88's sample included in jallib distribution (16f88_pwm_led.jal), and just adapt it so it runs at 8MHz, using internal clock. It also select RB0 as the PWM pin.

So, step by step... First, as we said, we must select which pin will carry the PWM signals...

pragma target CCP1MUX      RB0     -- ccp1 pin on B0

and configure it as output

var volatile bit pin_ccp1_direction is pin_b0_direction
pin_ccp1_direction = output
-- (simply "pin_b0_direction = output" would do the trick too)

Then we include the PWM library.

include pwm_hardware

Few words here... This library is able to handle up to 10 PWM channels (PIC using CCP1, CCP2, CCP3, CCP4, ... CCP10 registers). Using conditional compilation, it automatically selects the appropriate underlying PWM libraries, for the selected target PIC.

Since 16f88 has only one PWM channel, it just includes "pwm_ccp1" library. If we'd used a 16f877, which has two PWM channels, it would include "pwm_ccp1" and "pwm_ccp2" libraries. What is important is it's transparent to users (you).

OK, let's continue. We now need to configure the resolution. What's the resolution ? Given a frequency, the number of values you can have for the duty cycle can vary (you could have, say, 100 different values at one frequency, and 255 at another frequency). Have a look at the datasheet for more.

What we want here is to have the max number of values we can for the duty cycle, so we can select the exact brightness we want. We also want to have the max frequency we can have (ie. no pre-scaler).


If you read the jalapi documentation for this, you'll see that the frequency will be 7.81kHz (we run at 8MHz).

PWM channels can be turned on/off independently, now we want to activate our channel:


Before we dive into the forever loop, I forgot to mention PWM can be used in low or high resolution. On low resolution, duty cycles values range from 0 to 255 (8 bits). On high resolution, values range from 0 to 1024 (10 bits). In this example, we'll use low resolution PWM. For high resolution, you can have a look at the other sample, 16f88_pwm_led_highres.jal. As you'll see, there are very few differences.

Now let's dive into the loop...

forever loop
   var byte i
   i = 0
   -- loop up and down, to produce different duty cycle
   while i < 250 loop
      i = i + 1
   end loop
   while i > 0 loop
      i = i - 1
   end loop
   -- turning off, the LED lights at max.

end loop

Quite easy right ? There are two main waves: one will light up the LED progressively (0 to 250), another will turn it off progressively (250 to 0). On each value, we set the duty cycle with pwm1_set_dutycycle(i) and wait a little so we, humans, can see the result.

About the result, how does this look like ? See this video: http://www.youtube.com/watch?v=r9_TfEmUSf0

"I wanna try, where are the files ?"

To run this sample, you'll need latest jallib pack (at least 0.2 version). You'll also find the exact code we used here.

PWM (Part 2) - Sound and Frequency with Piezo Buzzer

Image icon pwm_sound.jpg23.19 KB
Image icon pwm_sound_piezo.jpg24.73 KB

In previous tutorial, we had fun by controlling the brightness of a LED, using PWM. This time, we're going to have even more fun with a piezo buzzer, or a small speaker.

If you remember, with PWM, you can either vary the duty cycle or the frequency. Controlling the brightness of a LED, ie. produce a variable voltage on the average, can be done by having a constant frequency (high enough) and vary the duty cycle. This time, this will be the opposite: we'll have a constant duty cycle, and vary the frequency.

What is a piezo buzzer ?

It's a "component" with a material having piezoelectric ability. Piezoelectricity is the ability for a material to produce voltage when it get distorted. The reverse is also true: when you produce a voltage, the material gets distorted. When you stop producing a voltage, it gets back to its original shape. If you're fast enough with this on/off voltage setting, then the piezo will start to oscillate, and will produce sound. How sweet...

Constant duty cycle ? Why ?

So we now know why we need to vary the frequency. This will make the piezo oscillates more and less, and produces sounds at different levels. If you produce a 440Hz frequency, you'll get a nice A3.

But why having a constant duty cycle ? What is the role of the duty cycle in this case ? Remember: when making a piezo oscillate, it's not the amount of volts which is important, it's how you turn the voltage on/off1:
  • when setting the duty cycle to 10%: during a period, piezo will get distorted 10% on the time, and remain inactive 90% on the time. The oscillation proportion is low.
  • when setting the duty cycle to 50%: the piezo is half distorted, half inactive. The oscillation proportion is high, because the piezo oscillates at the its maximal amplitude, it's half and equally distorted and inactive.
  • when setting the duty cycle to 90%: the piezo will get distorted during 90% of a period, then nothing. The oscillation proportion is low again, because the proportion between distortion and inactivity is not equal.

So, to summary, what is the purpose of the duty cycle in our case ? The volume ! You can vary the volume of the sound by modifying the duty cycle. 0% will produce no sounds, 50% will be the max volume. Between 50% and 100% is the same as between 0% and 50%. So, when I say when need a constant duty cycle, it's not that true, it's just that we'll set it at 50%, so the chances we hear something are high :)

Let's produce sounds !

The schematics will use is exactly the same as on the previous post with the LED, except the LED is replaced with a piezo buzzer, like this:

By the way, how to observe the "duty cycle effect" on the volume ? Just program your PIC with the previous experiment one, which control the brightness of a LED, and power on the circuit. I wanted to show a video with sounds, but the frequency is too high, my camera can't record it...

Anyway, that's a little bit boring, we do want sounds...

Writing the software

The software part has a lot of similarities with the previous experiment. The initialization is the same, I let you have a look. Only the forever loop has changed:
var dword counter = 0
forever loop

  for 100_000 using counter loop
     -- Setting @50% gives max volume
     -- must be re-computed each time the frequency
     -- changes, because it depends on PR2 value
  end loop

end loop
Quite straightforward:
  • we "explore" frequencies between 0 and 100 000 Hz, using a counter
  • we use pwm_set_frequency(counter) to set the frequency, in Hertz. It takes a dword as parameter (ie. you can explore a lot of frequencies...)
  • finally, as we want a 50% duty cycle, and since its value is different for each frequency setting, we need to re-compute it on each loop.
Note: jallib's PWM libraries are coming from a "heavy refactoring" of Guru Stef Mientki's PWM library. While integrating it to jallib, we've modified the library so frequencies can be set and changed during program execution. This wasn't the case before, because the frequency was set as a constant.

So, how does this look like ? Hope you'll like the sweet melody :)


"Where can I download the files ?"

As usual, you'll need the latest jallib pack (at least 0.2 version). You'll also find the exact code we used here.

1 I guess this is about energy or something like that. One guru could explain the maths here...

ADC - Analog-to-Digital Conversion

Analog-to-Digital Conversion is yet another nice feature you can get with a PIC. It's basically used to convert a voltage as an analog source (continuous) into a digital number (discrete).

ADC with water...

To better understand ADC, imagine you have some water going out of a pipe, and you'd like to know how many water it goes outside. One approach would be to collect all the water in a bucket, and then measure what you've collected. But what if water flow never ends ? And, more important, what if water flow isn't constant and you want to measure the flow in real-time ?

The answer is ADC. With ADC, you're going to extract samples of water. For instance, you're going to put a little glass for 1 second under the pipe, every ten seconds. Doing the math, you'll be able to know the mean rate of flow.

The faster you'll collect water, the more accurate the rate will be. That is, if you're able to collect 10 glasses of water each second, you'll have a better overview of the rate of water than if you collect 1 glass each ten seconds. This is the process of making a continous flow a discrete, finite value. And this is about resolution, one important property of ADC (and this is also about clock speed...). The higher the resolution, the more accurate the results.

Now, what if the water flow is so high that your glass gets filled before the end of the sample time ? You could use a bigger glass, but let's assume you can't (scenario need...). This means you can't measure any water flow, this one has to be scaled according to your glass. On the contrary, the water flow may be so low samples you extract may not be relevant related to the glass size (only few drops). Fortunately, you can use a smaller glass (yes, scenario need) to scale down your sample. That is about voltage reference, another important property.

Leaving our glass of water, many PICs provide several ADC channels: pins that can do this process, measuring voltage as input. In order to use this peripheral, you'll first have to configure how many ADC channels you want. Then you'll need to specify the resolution, usually using 8 bits (0 to 255), 10 bits (0 to 1024) or even 12 bits (0 to 4096). Finally, you'll have to setup voltage references depending on the voltage spread you plan to measure.

ADC with jallib...

As usual, Microchip PICs offers a wide choice configuring ADC:
  • Not all PICs have ADC module (...)
  • Analog pins are dispatched differently amongst PICs, still for user's sake, they have to be automatically configured as input. We thus need to know, for each PIC, where analog pins are...
  • Some PICs have their analog pins dependent from each other, and some are independent (more on this later)
  • Clock configuration can be different
  • As previously stated, some PICs have 8-bits low resolution ADC module, some have 10-bits high resolution ADC module1
  • Some PICs can have two external voltage references (VRef+ and VRef-), only one voltage reference (Vref+ or Vref-) and some can't handle external voltage references at all
  • (and probably other differences I can't remember :)...

Luckily most of these differences are transparent to users...

Dependent and independent analog pins

OK, let's write some code ! But before this, you have to understand one very important point: some PICs have their analog pins dependent from each other, some PICs have their analog pins independent from each other. "What is this suppose to mean ?" I can hear...

Let's consider two famous PICs: 16F877 and 16F88. 16F877 datasheet explains how to configure the number of analog pins, and vref, setting PCFG bits:

Figure 1. 16F877 ADC channels are controlled by PCFG bits

Want 6 analog pins, no Vref ? Then PCFG bits must be set to 0b1001. What will then be the analog pins ? RA0, RA1, RA2, RA3, RA5 and RE0. "What if I want 7 analog pins, no Vref ?" You can't because you'll get a Vref pin, no choice. "What if I want 2 analog pins being RE1 and RE2 ?" You can't, because there's no such combination. So, for this PIC, analog pins are dependent from each other, driven by a combination. In this case, you'll have to specify:
  • the number of ADC channels you want,
  • and amongst them, the number of Vref channels

Now, let's consider 16F88. In this case, there's no such table:

Figure 2. 16F88 ADC channels are controlled by ANS bits

Mmmh... OK, there are ANS bits, one for each analog pins. Setting an ANS bit to 1 sets the corresponding pin to analog. This means I can set whatever pin I want to be analog. "I can have 3 analog pins, configured on RA0, RA4 and RB6. Freedom !"

Analog pins are independent from each other in this case, you can do what you want. As a consequence, since it's not driven by a combination, you won't be able to specify the number of ADC channels here. Instead, you'll use set_analog_pin() procedure, and if needed, the reverse set_digital_pin() procedure. These procedures takes a analog pin number as argument. Say analog pin AN5 is on pin RB6. To turn this pin as analog, you just have to write set_analog_pin(5), because this is about analog pin AN5, and not RB6.

Remember: as a consequence, these procedures don't exist when analog pins are dependent as in our first case.
it's not because there are PCFG bits that PICs have dependent analog pins. Some have PCFG bits which act exactly the same as ANS bits (like some of recent 18F)
Tip: how to know if your PIC has dependent or independent pins ? First have a look at its datasheet, if you can a table like the one for 16F877, there are dependent. Also, if you configure a PIC with dependent pins as if it was one with independent pins (and vice-versa), you'll get an error. Finally, if you get an error like: "Unable to configure ADC channels. Configuration is supposed to be done using ANS bits but it seems there's no ANS bits for this PIC. Maybe your PIC isn't supported, please report !", or the like, well, this is not a normal situation, so as stated, please report !

Once configured, using ADC is easy. You'll find adc_read() and adc_read_low_res() functions, for respectively read ADC in high and low resolution. Because low resolution is coded on 8-bits, adc_read() returns a byte as the result. adc_read_low_res() returns a word.

Example with 16F877, dependent analog pins

The following examples briefly explains how to setup ADC module when analog pins are dependent from each other, using PIC 16F877.

The following diagram is here to help knowing where analog pins (blue) are and where Vref pins (red) are:

Figure 3. Analog pins and Vref pins on 16F877

Example 1: 16F877, with only one analog pin, no external voltage reference

-- beginning is about configuring the chip
-- this is the same for all examples for about 18F877
include 16f877
-- setup clock running @20MHz
pragma target OSC HS
pragma target clock 20_000_000
-- no watchdog
pragma target WDT  disabled
pragma target LVP  disabled
include delay

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware

-- ok, now let's configure ADC
-- we want to measure using low resolution
-- (that's our choice, we could use high resolution as well)
const bit ADC_HIGH_RESOLUTION = false
-- we said we want 1 analog channel...
const byte ADC_NCHANNEL = 1
-- and no external voltage reference
-- now we can include the library
-- note it's now named "adc", not "adc_hardware" anymore
include adc
-- and run the initialization step

-- will periodically send those chars
var byte measure
forever loop
   -- get ADC result, on channel 0
   -- this means we're currently reading on pin RA0/AN0 !
   measure = adc_read_low_res(0)
   -- send it back through serial

   -- and sleep a litte to prevent flooding serial...
end loop

Example 2: 16F877, with 5 analog pins, 1 external voltage reference, that is, Vref+

This is almost the same as before, except we now want 5 (analog pins) + 1 (Vref) = 6 ADC channels (yes, I consider Vref+ pin as an ADC channel).

The beginning is the same, here's just the part about ADC configuration and readings:

const bit ADC_HIGH_RESOLUTION = false
-- our 6 ADC channel
const byte ADC_NCHANNEL = 6
-- and one Vref pin
-- the two parameters could be read as:
-- "I want 6 ADC channels, amongst which 1 will be
-- reserved for Vref, and the 5 remaining ones will be
-- analog pins"
include adc

-- will periodically send those chars
var byte measure
forever loop
   -- get ADC result, on channel 0
   -- this means we're currently reading on pin RA0/AN0 !
   measure = adc_read_low_res(0)
   -- send it back through serial

   -- same for pin RA1/AN1
   measure = adc_read_low_res(1)

   -- same for pin RA2/AN2
   measure = adc_read_low_res(2)

   -- pin RA3/AN3 can't be read, since it's Vref+

   -- same for pin RA5/AN4
   -- 4 is from from "AN4" !
   measure = adc_read_low_res(4)

   -- same for pin RE10/AN5
   measure = adc_read_low_res(5)

   -- and sleep a litte to prevent flooding serial...
end loop 

Example with 16F88, independent analog pins

The following example is about setting up ADC module with PIC 16F88, where analog pins are independent from each other.

The following diagram is here to help knowing where analog pins (blue) are and where Vref pins (red) are:

Figure 4. Analog pins and Vref pins on 16F88

Example 1: 16F88, analog pins on RA0/AN0, RA4/AN4 and RB6/AN5. No external voltage reference.

-- beginning is about configuring the chip
include 16f88
-- We'll use internal oscillator. It work @ 8MHz
pragma target CLOCK     8_000_000
pragma target OSC       INTOSC_NOCLKOUT
OSCCON_IRCF = 0b_111
pragma target WDT       disabled

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware

-- now configure ADC
const bit ADC_HIGH_RESOLUTION = false
-- we can't specify a number of ADC channel here,
-- or we'll get an error !
include adc
-- now we declare the pin we want as analog !
set_analog_pin(0)  -- RA0/AN0
set_analog_pin(4)  -- RA4/AN4
set_analog_pin(5)  -- RB6/AN5

-- reading is then the same
var byte measure
forever loop

   measure = adc_read_low_res(0)

   measure = adc_read_low_res(4)

   measure = adc_read_low_res(5)

end loop

Whether you would want to turn RB6/AN5 into a digital pin again, you'd just call:

1 and some have 12-bits, those aren't currently handled by jallib ADC libraries, That's a restriction.

I²C (Part 1) - Building an I²C slave + Theory

Author: Sébastien Lelong | Jallib Group

i2c is a nice protocol: it is quite fast, reliable, and most importantly, it's addressable. This means that on a single 2-wire bus, you'll be able to plug up to 128 devices using 7bits addresses, and even 1024 using 10bits address. Far enough for most usage... I won't cover i2c in depth, as there are plenty resources on the Web (and I personally like this page).

A few words before getting our hands dirty...

i2c is found in many chips and many modules. Most of the time, you create a master, like when accessing an EEPROM chip. This time, in this three parts tutorial, we're going to build a slave, which will thus respond to master's requests.

The slave side is somewhat more difficult (as you may have guess from the name...) because, as it does not initiate the talk, it has to listen to "events", and be as responsive as possible. You've guessed, we'll use interrupts. I'll only cover i2c hardware slave, that is using SSP peripheral1. Implementing an i2c software slave may be very difficult (and I even wonder if it's reasonable...).

There are different way implementing an i2c slave, but one seems to be quite common: defining a finite state machine. This implementation is well described in Microchip AppNote AN734. It is highly recommended that you read this appnote, and the i2c sections of your favorite PIC datasheet as well (I swear it's quite easy to read, and well explained).

Basically, during an i2c communication, there can be 5 distinct states:
  1. Master writes, and last byte was an address: to sum up, master wants to talk to a specific slave, identified by the address, it wants to send data (write)
  2. Master writes, and last byte was data: this time, master sends data to the slave
  3. Master read, and last byte was an address: almost the same as 1., but this time, master wants to read something from the salve
  4. Master read, and last byte was data: just the continuation of state 3., master has started to read data, and still wants to read more data
  5. Master sends a NACK: basically, master doesn't want to talk to the slave anymore, it hangs up...
Note: in the i2c protocol, one slave has actually two distinct addresses. One is for read operations, and it ends with bit 1. Another is for write operations, and it ends with bit 0.

Example: consider the following address (8-bits long, last bit is for operation type)

0x5C => 0b_0101_1100 => write operation

The same address for read operation will be:

0x93 => 0b_0101_1101 => read operation

Note: jallib currently supports up to 128 devices on a i2c bus, using 7-bits long addresses (without the 8th R/W bits). There's currently no support for 10-bits addresses, which would give 1024 devices on the same bus. If you need it, please let us know, we'll modify libraries as needed !

OK, enough for now. Next time, we'll see how two PICs must be connected for i2c communication, and we'll check the i2c bus is fully working, before diving into the implementation.

1 some PICs have MSSP, this means they can also be used as i2c hardware Master

I²C (Part 2) - Setting up and checking an I²C bus

In previous tutorial, we saw a basic overview of how to implement an i2c slave, using a finite state machine implementation. This time, we're going to get our hands a little dirty, and starts connecting our master/slave together.

Checking the hardware and the i2c bus...

First of all, i2c is quite hard to debug, especially if you don't own an oscilloscope (like me). So you have to be accurate and rigorous. That's why, in this second part of this tutorial, we're going to setup the hardware, and just make sure the i2c bus is properly operational.

Connecting two PIC together through i2c is quite easy from a hardware point of view. Just connect SDA and SCL together, and don't forget pull-ups resistors. There are many differents values for these resistors, depending on how long the bus is, or the speed you want to reach. Most people use 2.2K resistors, so let's do the same ! The following schematics is here to help:

In this circuit, both PIC have a LED connected, which will help us understand what's going on. On a breadboard, this looks like that:

The master is on the right side, the slave on the left. I've put the two pull-ups resistors near the master:

Green and orange wires connect the two PICs together through SDA and SCL lines:

The goal of this test is simple: check if the i2c bus is properly built and operational. How ? PIC 16F88 and its SSP peripheral is able to be configured so it triggers an interrupts when a Start or Stop signal is detected. Read this page (part of an nice article on i2c, from previous tutorial's recommandations).

How are we gonna test this ? The idea of this test is simple:
  1. On power, master will blink a LED a little, just to inform you it's alive
  2. On the same time, slave is doing the same
  3. Once master has done blinking, it sends a i2c frame through the bus
  4. If the bus is properly built and configured, slave will infinitely blink its LED, at high speed

Note master will send its i2c frame to a specific address, which don't necessarily need to be the same as the slave one (and I recommand to use different addresses, just to make sure you understand what's going on).

What about the sources ? Download latest jallib pack, and check the following files (either in lib or sample directories):

The main part of the slave code is the way the initialization is done. A constant is declared, telling the library to enable Start/Stop interrupts.

const SLAVE_ADDRESS = 0x23 -- whatever, it's not important, and can be
                           -- different from the address the master wants
                           -- to talk to
-- with Start/Stop interrupts
const bit i2c_enable_start_stop_interrupts = true
-- this init automatically sets global/peripherals interrupts

And, of course, the Interrupt Service Routine (ISR):

procedure i2c_isr() is
   pragma interrupt
   if ! PIR1_SSPIF then
   end if
   -- reset flag
   PIR1_SSPIF = false
   -- tmp store SSPSTAT
   var byte tmpstat
   tmpstat = SSPSTAT
   -- check start signals
   if (tmpstat == 0b_1000) then
      -- If we get there, this means this is an SSP/I2C interrupts
      -- and this means i2c bus is properly operational !!!
      while true loop
         led = on
         led = off
      end loop
   end if
end procedure
The important thing is to:
  • check if interrupt is currently a SSP interrupts (I2C)
  • reset the interrupt flag,
  • analyze SSPSTAT to see if Start bit is detected
  • if so, blinks 'til the end of time (or your battery)

Now, go compile both samples, and program two PICs with them. With a correct i2c bus setting, you should see the following:


On this next video, I've removed the pull-ups resistors, and it doesn't work anymore (slave doesn't high speed blink its LED).


Next time (and last time on this topic), we'll see how to implement the state machine using jallib, defining callback for each states.

I²C (Part 3) - Implementing an I²C Slave

In previous parts of this tutorial, we've seen a little of theory, we've also seen how to check if the i2c bus is operational, now the time has come to finally build our i2c slave. But what will slave will do ? For this example, slave is going to do something amazing: it'll echo received chars. Oh, I'm thinking about something more exciting: it will "almost" echo chars:
  • if you send "a", it sends "b"
  • if you send "b", it sends "c"
  • if you send "z", it sends "{"1

Building the i2c master

Let's start with the easy part. What will master do ? Just collect characters from a serial link, and convert them to i2c commands. So you'll need a PIC to which you can send data via serial. I mean you'll need a board with serial com. capabilities. I mean we won't do this on a breadboard... There are plenty out there on the Internet, pick your choice. If you're interested, you can find one on my SirBot site: dedicated to 16f88, serial com. available, and i2c ready (pull-ups resistors).

It looks like this:

Two connectors are used for earch port, PORTA and PORTB, to plug daughter boards, or a breadboard in our case.

The i2c initialization part is quite straight forward. SCL and SDA pins are declared, we'll use a standard speed, 400KHz:

-- I2C io definition
var volatile bit i2c_scl            is pin_b4
var volatile bit i2c_scl_direction  is pin_b4_direction
var volatile bit i2c_sda            is pin_b1
var volatile bit i2c_sda_direction  is pin_b1_direction
-- i2c setup
const word _i2c_bus_speed = 4 ; 400kHz
const bit _i2c_level = true   ; i2c levels (not SMB)
include i2c_software

We'll also use the level 1 i2c library. The principle is easy: you declare two buffers, one for receiving and one for sending bytes, and then you call procedure specifying how many bytes you want to send, and how many are expected to be returned. Joep has written a nice post about this, if you want to read more about this. We'll send one byte at a time, and receive one byte at a time, so buffers should be one byte long.

const single_byte_tx_buffer = 1 -- only needed when length is 1
var byte i2c_tx_buffer[1]
var byte i2c_rx_buffer[1]
include i2c_level1

What's next ? Well, master also has to read chars from a serial line. Again, easy:

const usart_hw_serial = true
const serial_hw_baudrate = 57_600
include serial_hardware
-- Tell the world we're ready !

So when the master is up, it should at least send the "!" char.

Then we need to specify the slave's address. This is a 8-bits long address, the 8th bits being the bit specifying if operation is a read or write one (see part 1 for more). We then need to collect those chars coming from the PC and sends them to the slave.

The following should do the trick (believe me, it does :))

var byte icaddress = 0x5C   -- slave address

forever loop
   if serial_hw_read(pc_char)
      serial_hw_write(pc_char)  -- echo
      -- transmit to slave
      -- we want to send 1 byte, and receive 1 from the slave
      i2c_tx_buffer[0] = pc_char
      var bit _trash = i2c_send_receive(icaddress, 1, 1)
      -- receive buffer should contain our result
      ic_char = i2c_rx_buffer[0]
   end if
end loop

The whole program is available on jallib SVN repository here.

Building the i2c slave

So this is the main part ! As exposed on first post, we're going to implement a finite state machine. jallib comes with a library where all the logic is already coded, in a ISR. You just have to define what to do for each state encountered during the program execution. To do this, we'll have to define several callbacks, that is procedures that will be called on appropriate state.

Before this, we need to setup and initialize our slave. i2c address should exactly be the same as the one defined in the master section. This time, we won't use interrrupts on Start/Stop signals; we'll just let the SSP module triggers an interrupts when the i2c address is recognized (no interrupts means address issue, or hardware problems, or...). Finally, since slave is expected to receive a char, and send char + 1, we need a global variable to store the results. This gives:

include i2c_hw_slave

const byte SLAVE_ADDRESS = 0x5C

-- will store what to send back to master
-- so if we get "a", we need to store "a" + 1
var byte data
Before this, let's try to understand how master will talk to the slave (italic) and what the slave should do (underlined), according to each state (with code following):
  • state 1: master initiates a write operation (but does not send data yet). Since no data is sent, slave should just do... nothing (slave just knows someone wants to send data).
    procedure i2c_hw_slave_on_state_1(byte in _trash) is
       pragma inline
       -- _trash is read from master, but it's a dummy data
       -- usually (always ?) ignored
    end procedure
  • state 2: master actually sends data, that is one character. Slave should get this char, and process it (char + 1) for further sending.
    procedure i2c_hw_slave_on_state_2(byte in rcv) is
       pragma inline
       -- ultimate data processing... :)
       data = rcv + 1
    end procedure
  • state 3: master initiates a read operation, it wants to get the echo back. Slave should send its processed char.
    procedure i2c_hw_slave_on_state_3() is
       pragma inline
    end procedure
  • state 4: master still wants to read some information. This should never occur, since one char is sent and read at a time. Slave should thus produce an error.
    procedure i2c_hw_slave_on_state_4() is
       pragma inline
       -- This shouldn't occur in our i2c echo example
    end procedure
  • state 5: master hangs up the connection. Slave should reset its state.
    procedure i2c_hw_slave_on_state_5() is
       pragma inline
       data = 0
    end procedure

Finally, we need to define a callback in case of error. You could do anything, like resetting the PIC, and sending log/debug data, etc... In our example, we'll blink forever:

procedure i2c_hw_slave_on_error() is
   pragma inline
   -- Just tell user user something's got wrong
   forever loop
      led = on
      led = off
   end loop
end procedure

Once callbacks are defined, we can include the famous ISR library.

include i2c_hw_slave_isr
So the sequence is:
  1. include i2c_hw_slave, and setup your slave
  2. define your callbacks,
  3. include the ISR
The full code is available from jallib's SVN repository:

All those files and other dependencies are also available in latest jallib-pack (see jallib downloads)

Connecting and testing the whole thing...

As previously said, the board I use is ready to be used with a serial link. It's also i2c ready, I've put the two pull-ups resistors. If your board doesn't have those resistors, you'll have to add them on the breadboard, or it won't work (read part 2 to know and see why...).

I use a connector adapted with a PCB to connect my main board with my breadboard. Connector's wires provide power supply, 5V-regulated, so no other powered wires it required.

Connector, with power wires

Everything is ready...

Crime scene: main board, breadboard and battery pack

Once connected, power the whole and use a terminal to test it. When pressing "a", you'll get a "a" as an echo from the master, then "b" as result from the slave.

What now ?

We've seen how to implement a simple i2c hardware slave. The ISR library provides all the logic about the finite state machine. You just have to define callbacks, according to your need.

i2c is a widely used protocol. Most of the time, you access i2c devices, acting as a master. We've seen how to be on the other side, on the slave side. Being on the slave side means you can build modular boards, accessible with a standard protocol. For instance, I've built a DC motor controller daughter board using this. It's a module, a unit on its own, just plug, and send/receive data, with just two wires.

1 why "{" ? According to ASCII, "z" is the character for position 122. 123 is... "{")

SPI Introduction

Image icon spi_intro_data_transfer.jpg21.04 KB
Author: Matthew Schinkel | Jallib Group

Introduction to SPI - Serial Peripheral interface

What is SPI?

SPI is a protocol is simply a way to send data from device to device in a serial fashion (bit by bit). This protocol is used for things like SD memory cards, MP3 decoders, memory devices and other high speed applications.

We can compare SPI to other data transfer protocols:

Table 1. Protocol Comparison Chart
  SPI RS-232 I2C
PINS 3 + 1 per device 2 2
Number Of Devices unlimited 2 1024
Bits in one data byte transfer 8 10 (8 bytes + 1 start bit + 1 stop bit) 9 (8 bytes + 1 ack bit)
Must send one device address byte before transmission No No Yes
Clock Type Master clock only Both device clocks must match Master Clock that slave can influence
Data can transfer in two directions at the same time (full-duplex) Yes Yes No

As you can see SPI sends the least bit's per data byte transfer byte and does not need to send a device address before transmission. This makes SPI the fastest out of the three we compared.

Although SPI allows "unlimited" devices, and I2C allows for 1024 devices, the number of devices that can be connected to each of these protocol's are still limited by your hardware setup. This tutorial does not go into detail about connecting a large number of devices on the same bus. When connecting more devices, unrevealed problems may appear.

How does SPI work?

Firstly, SPI works in a master/slave setup. The master is the one that sends the clock pulses. At each pulse, data will be sent and received.

SPI has a chip select pin. Every device will share the "SDI", "SDO" and "Clock" pins, but each device will have it's own chip select pin (also known as slave select). This means we can have a virtually unlimited number of devices on the same SPI bus. You should also note that the chip select pin can be active high or active low depending on the device.

For some devices, the chip select pin must stay enabled throughout the transmission, and others require a change in the chip select line before the next transmission.

SPI is Dual-Duplex. This means data can be sent and received at the same time. If you wish to send data and not receive any, the PIC will receive data anyways. You may ignore the return byte.

Here's a diagram showing the way in which SPI sends & receives data:

SPI Modes

If you are using a device that does not yet have a Jallib library, you will need to get the devices SPI mode. Some device datasheets tell you the SPI mode, and some don't. Your device should tell you the clock idle state and sample edge, with this information, you can find the SPI mode. SPI devices can be set to run in 4 different modes depending on the clock's idle state polarity & data sample rising or falling edge.

The image above is SPI mode 1,1. See if you can understand why.

Clock Polarity (CKP) - Determines if the clock is normally high or normally low during it's idle state.

If CKP = 1 - the clock line will be high during idle.

If CKP = 0 - the clock will be low during idle.

Data Clock Edge (CKE) - The edge that the data is sampled on (rising edge or falling edge)

If CKP = 0, CKE = 0 - Data is read on the clocks rising edge (idle to active clock state)

If CKP = 0, CKE = 1 - Data is read on the clocks falling edge (active to idle clock state)

If CKP =1, CKE = 0 - Data is read on the clocks falling edge (idle to active clock state)

If CKP = 1, CKE = 1 - Data is read on the clocks rising edge (active to idle clock state)

We can put this in a chart to name the modes:

0,0 0 1
0,1 0 0
1,0 1 1
1,1 1 0
Note: I noticed the mode numbers & mode table on Wikipedia is different then the table in the Microchip PDF. I am going by the Microchip PDF, as well as the tested and working PIC Jallib library + samples. Wikipedia also names these registers CPOL/CPHA instead of CKP/CKE.

Using The Jallib Library

At the moment, there is only a SPI master hardware library, therefore any device you wish to control must be connected to the PIC's SDI, SDO, SCK pins. The chip select pin can be any digital output pin.

The library requires you to set the pin directions of the SDI, SDO, SCK lines as follows:

-- setup SPI
include spi_master_hw         -- first include the library

-- define SPI inputs/outputs
pin_sdi_direction = input    -- spi data input
pin_sdo_direction = output   -- spi data output
pin_sck_direction = output   -- spi data clock

You only need to set the pin direction of the chip select pin, the PIC will set the direction of the SDI, SDO & SCK for you. You will Alias this chip select pin as required by the device's jallib library.

If you are using more then one device in your circuit, you will need to declare your chip select pin near the beginning of your program. If you do not do this at the beginning of your program, some of your devices may receive data because their chip select pin could be enabled during init procedures of other devices on the SPI bus.

-- choose your SPI chip select pin
-- pin_SS is the PIC's slave select (or chip select) pin.
ALIAS device_chip_select_direction   is pin_SS_direction
ALIAS device_chip_select             is pin_SS
device_chip_select_direction = output    -- chip select/slave select pin
device_chip_select = low                -- disable the device

Now the last step in setting up the SPI library is to use the init procedure.

Use the SPI mode name chart to get your SPI mode. The modes can be any of the following:





You will also need to set the spi bus speed. Here is a list of the speeds you may choose from:

SPI_RATE_FOSC_4 -- oscillator / 4

SPI_RATE_FOSC_16 -- oscillator / 16

SPI_RATE_FOSC_64 -- oscillator / 64

SPI_RATE_TMR -- PIC's internal timer

You will use the following init procedure with your custom values entered:

spi_init(SPI_MODE_11,SPI_RATE_FOSC_16) -- choose spi mode and speed

Now your ready to use the procedures to send and receive data. First you must enable the device with the chip select line:

device_chip_select = high -- enable the device

You can use the pseudo variable spi_master_hw to send and receive data as follows:

-- send decimal 50 to spi bus
spi_master_hw = 50

Or receive data like this:

-- receive data from the spi port into byte x
var byte x
x = spi_master_hw

You can also send and receive data at the same time with the spi_master_hw_exchange procedure. here's an example:

-- send decimal byte 50 and receive data into byte x
var byte x
x = spi_master_hw_exchange (50)

When your done transmitting & receiving data, don't forget to disable your device

device_chip_select = low -- enable the device

Alright, now you should be able to implement SPI into any of your own devices. If you need assistance, contact us at the Jallist Support Group or at Jallib Group.


The Jallib spi_master_hw library - Written by William Welch

Microchip Technology SPI Overview - http://ww1.microchip.com/downloads/en/devicedoc/spi.pdf

Wikipedia - http://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus