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.
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.

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

Three different duty cycle (10%, 50% and 90%), all at the same frequency
That said, we're now goind to experiment these two major properties.
| Attachment | Size |
|---|---|
| pwm_freq.png | 1.24 KB |
| pwm_duty.png | 1.5 KB |
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.
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).

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
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).
pwm_max_resolution(1)
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:
pwm1_on()
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
pwm1_set_dutycycle(i)
_usec_delay(10000)
i = i + 1
end loop
while i > 0 loop
pwm1_set_dutycycle(i)
_usec_delay(10000)
i = i - 1
end loop
-- turning off, the LED lights at max.
_usec_delay(500000)
pwm1_off()
_usec_delay(500000)
pwm1_on()
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
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.
| Attachment | Size |
|---|---|
| pwm_led_schematics.png | 7.2 KB |
| pwm_led_breadboard.jpg | 67.52 KB |
| pwm_led_details.jpg | 27.37 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.
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...
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.
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 :)
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...
var dword counter = 0
forever loop
for 100_000 using counter loop
pwm_set_frequency(counter)
-- Setting @50% gives max volume
-- must be re-computed each time the frequency
-- changes, because it depends on PR2 value
pwm1_set_percent_dutycycle(50)
end loop
end loop
So, how does this look like ? Hope you'll like the sweet melody :)
http://www.youtube.com/watch?v=xZ9OhQUKGtQ
As usual, you'll need the latest jallib pack (at least 0.2 version). You'll also find the exact code we used here.
| Attachment | Size |
|---|---|
| pwm_sound.jpg | 23.19 KB |
| pwm_sound_piezo.jpg | 24.73 KB |
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).
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.
Luckily most of these differences are transparent to users...
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:
Now, let's consider 16F88. In this case, there's no such table:
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.
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.
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:
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 enable_digital_io() include delay -- ok, now setup serial, we'll use this -- to get ADC measures const serial_hw_baudrate = 19_200 include serial_hardware serial_hw_init() -- 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 const byte ADC_NVREF = ADC_NO_EXT_VREF -- now we can include the library -- note it's now named "adc", not "adc_hardware" anymore include adc -- and run the initialization step adc_init() -- 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 serial_hw_write(measure) -- and sleep a litte to prevent flooding serial... delay_1ms(200) 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 const byte ADC_NVREF = ADC_VREF_POS -- 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 adc_init() -- 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 serial_hw_write(measure) -- same for pin RA1/AN1 measure = adc_read_low_res(1) serial_hw_write(measure) -- same for pin RA2/AN2 measure = adc_read_low_res(2) serial_hw_write(measure) -- 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) serial_hw_write(measure) -- same for pin RE10/AN5 measure = adc_read_low_res(5) serial_hw_write(measure) -- and sleep a litte to prevent flooding serial... delay_1ms(200) end loop
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:
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 enable_digital_io() -- ok, now setup serial, we'll use this -- to get ADC measures const serial_hw_baudrate = 19_200 include serial_hardware serial_hw_init() -- now configure ADC const bit ADC_HIGH_RESOLUTION = false const byte ADC_NVREF = ADC_NO_EXT_VREF -- we can't specify a number of ADC channel here, -- or we'll get an error ! include adc adc_init() -- 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) serial_hw_write(measure) measure = adc_read_low_res(4) serial_hw_write(measure) measure = adc_read_low_res(5) serial_hw_write(measure) end loop
Whether you would want to turn RB6/AN5 into a digital pin again, you'd just call:
set_digital_pin(5)
| Attachment | Size |
|---|---|
| adc_16f877_table.jpg | 90.6 KB |
| adc_16f88_ans.jpg | 40.05 KB |
| adc_16f877_diagram.jpg | 61.86 KB |
| adc_16f88_diagram.jpg | 54.68 KB |
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).
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).
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
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.
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.
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).
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).
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
i2c_hw_slave_init(SLAVE_ADDRESS)
And, of course, the Interrupt Service Routine (ISR):
procedure i2c_isr() is
pragma interrupt
if ! PIR1_SSPIF then
return
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
_usec_delay(100000)
led = off
_usec_delay(100000)
end loop
end if
end procedure
Now, go compile both samples, and program two PICs with them. With a correct i2c bus setting, you should see the following:
http://www.youtube.com/watch?v=NalAkRhFP-s
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).
http://www.youtube.com/watch?v=cNK_cCgWctY
Next time (and last time on this topic), we'll see how to implement the state machine using jallib, defining callback for each states.
| Attachment | Size |
|---|---|
| i2c_check_bus_schematic.png | 11.36 KB |
| i2c_breadboard_top.jpg | 23.4 KB |
| i2c_pullups.jpg | 24.04 KB |
| i2c_wires.jpg | 22.98 KB |
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 i2c_initialize()
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
serial_hw_init()
-- Tell the world we're ready !
serial_hw_write("!")
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)
then
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]
serial_hw_write(ic_char)
end if
end loop
The whole program is available on jallib SVN repository here.
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 i2c_hw_slave_init(SLAVE_ADDRESS) -- will store what to send back to master -- so if we get "a", we need to store "a" + 1 var byte 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
procedure i2c_hw_slave_on_state_2(byte in rcv) is pragma inline -- ultimate data processing... :) data = rcv + 1 end procedure
procedure i2c_hw_slave_on_state_3() is pragma inline i2c_hw_slave_write_i2c(data) end procedure
procedure i2c_hw_slave_on_state_4() is pragma inline -- This shouldn't occur in our i2c echo example i2c_hw_slave_on_error() end procedure
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
_usec_delay(200000)
led = off
_usec_delay(200000)
end loop
end procedure
Once callbacks are defined, we can include the famous ISR library.
include i2c_hw_slave_isr
All those files and other dependencies are also available in latest jallib-pack (see jallib downloads)
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.

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.
| Attachment | Size |
|---|---|
| i2c_seb_mainboard_up.jpg | 62.82 KB |
| i2c_seb_mainboard_facade.jpg | 49.32 KB |
| i2c_connector.jpg | 50.09 KB |
| i2c_details.jpg | 53.32 KB |
| i2c_crimescene.jpg | 52.12 KB |
| i2c_pseudoecho.png | 6.29 KB |
Introduction to SPI - Serial Peripheral interface
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:
| 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.
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:
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:
| MODE NAME | CKP | CKE |
|---|---|---|
| 0,0 | 0 | 1 |
| 0,1 | 0 | 0 |
| 1,0 | 1 | 1 |
| 1,1 | 1 | 0 |
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:
SPI_MODE_00
SPI_MODE_01
SPI_MODE_10
SPI_MODE_11
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
| Attachment | Size |
|---|---|
| spi_intro_data_transfer.jpg | 21.04 KB |