29 April 2008

14. Of Ports and Pins and SFRs

Up until now, I’ve conveniently skipped over how the PicPack libray handles the read-before-write problem on low to mid range pics, or even what this issue is really all about. It provides a simple solution to a complicated problem, but it bears explaining since you’ll learn how these devices work under the covers.

On Microchip micocontrollers, like many similar devices, allow you to make a given pin either an input, or an output. And you can change this programmatically at runtime as well – handy talking to some devices over a bi-directional data line. There’s some circuitry that can detect if there’s a high or low value present on a pin and also what’s known as a data latch attached to the same pins. The data latch is an output device that holds the value you give it until you change it.

The problem with Microchip pic devices prior to the 18f range is two fold – firstly that the output latch isn’t readable and secondly that all outputs are done at the port (not pin) level. What this means is that when you just want to change one output pin in a port from one value to another, the chip doesn’t know what the current values on the other pins are. Say you’ve set pin 1 to high previously, and now you want to change pin 2 to a high as well. It doesn’t have any record of what the previous value of pin 1 was set to (remember, the data latch isn’t readable) and all outputs to the latch happen at a port level (so it has to write values to all the pins in a port at once). The way the chip gets around this is called “read before write”. It reads the value on all the pins in the port (that is, the actual value present on the physical pins of the pin), then changes the pin you’re interested in, and then writes the whole port back again.

So, in our example, it should read a high value on pin 1 (since this is what the latch is outputting), change the value on pin 2 to high and write them both (and the other pins in the port of course) back to the latch, resulting in pin 1 and pin 2 set to high values.

That all sounds fair enough. It’s clearly a bit of a pain, but surely the output of the latch is the same as what you told it to output, so what’s the problem? Well, in theory that’s perfectly true – but in practice, it doesn’t work like that. Imagine that as part of your circuit you have a capacitor connected between your output pin and ground. The reason why you’d do this isn’t important right now. You can imagine that when you set that pin high, it’s going to take some actual real life time to get to the point of actually reading at a high level. Maybe just microseconds, but still, some time. Now imagine if you set this pin high then, very quickly, set another pin high. Your capacitor laden pin is going to be read and the chip may decide that in fact that pin is low at the moment since the capacitor hasn’t charged to the high level.

Eek! That means that the pin is going to get written back as a low! Disaster! In fact, it doesn’t just happen if you have something capacitor-like attached to the pin, it can happen even with a set of leds and resistors. What to do?

The answer is to simulate the readable latch that we don’t have. The PicPack library provides a “shadow” port that it writes changes to before copying it to the whole port. Of course this results in slightly more code – instead of a one instruction bit-set you end up with three. As I’ve learnt with microcontrollers, everything is a compromise. Presumably Microchip saved some money by not having a readable latch in these devices, but it costs you an extra couple of instructions to guarantee the value on an individual pin. The 18f devices (and onwards) have a readable and writable data latch that makes this problem redundant, but without needing to make any chances, the PicPack library makes your code transparently portable between these devices even though 16f devices don’t have a readable data latch.

At the end of the day you can use the PicPack routines without having to worry about what’s happening under the covers – and this makes your code more portable and readable as well. It’s worth explaining how PicPack achieves this without taking too many instructions, and even without having to tell it how many ports your device has.

Open up the include file for the chip you’re working on at the moment. This example uses the 16f88, which for me, is located at c:\Program Files\SourceBoost\include\pic16f88.h.

Pics have a memory space for RAM which includes clumps of “magic” memory locations that do things when you read to them or write to them. These locations are called Special Function Registers or SFRs. You write to a port, for example, by writing a value to a SFR that results in the port being changed.

BoostC include files define the address of these SFRs, and then define a variable that is located at that address. That means that when you set that variable, you are writing data to that memory address – which means writing data to the SFR which causes the chip to do something.

I’m being long-winded about this because it is actually quite confusing until you get your head around it.

Have a look at the include file. You’ll find the definition of our port locations:
#define PORTA                 0x0005
#define PORTB 0x0006
Notice that the PORTA and PORTB are in capitals. Later on, you’ll see in the same include file:
volatile char porta                  @PORTA;
volatile char portb @PORTB;
So, here we define two global variables, porta and portb that happen to reside at the right place that if you read or write to the variable, you end up reading or write the similarly named SFR. Do you see how the variables use lower case letters, but the actual address is in capitals? Thankfully SourceBoost provide these pre-made include files for just about every pic out there. They’re choice of capital vs lower case is somewhat in contrast to other pic compilers, and sometimes the variable names and address locations aren’t even the same as what is in the data sheet for the same pic, just by way of warning. As I’ve said, portability is relative these days, even if we’re all talking C.

Now, all the “bits” of the SFRs are defined in here as well – so you can, for example, set the timer 0 interrupt enable bit of the intcon SFR by using:
intcon.TMR0IE = 1;
This is bit setting notation is handy for single pins, but I prefer the more portable macro that SourceBoost provides:
set_bit(intcon, TMR0IE);
It results in the same code generated, but it means you could port to a different compiler (or microcontroller vendor if need be) more easily.

Now, you can see here we’re setting bit 5 of the intcon variable (actually memory location 0x000b). This is all okay, since SFRs beyond ports don’t have real-life outputs and hence don’t suffer from the read-before-write problem.

In order to make sure we generate as little code as possible, PicPack sets up an array at the same location as the port and tris SFRs in the pic_utils.h file:
volatile uns8 port_array[NUMBER_PORTS] @PORTA;
volatile uns8 tris_array[NUMBER_PORTS] @TRISA;
The tris SFRs are used to set whether a pin is an input (1) or an output (0). “Tris” comes from the (copyrighted) word tri-state, since a pin can be an output at high, an output at low, or an input (and high impedance). You’ll see shortly that you don’t even need to worry about this tris malarkey either with the pic_utils library.

These declarations don’t use any memory up – they just make it handy when we’re moving things around. We also declare the shadow array:
extern uns8 port_shadow[NUMBER_PORTS];

The NUMBER_PORTS define has already been calculated for you, earlier in pic_utils.h. The actual useful functions are defined like this:
#define set_pin(port, pin) \
 set_bit(port_shadow[port - PORTA], pin); \
 port_array[port - PORTA] = port_shadow[port - PORTA];
So our set_pin routine just works out which shadow location it needs to change, then copies the whole byte to the port at the right location. Through a great deal of experimentation, I’ve found that this results in the smallest amount of generated code. Even though it looks like we have lots of calculations to do, if we’re using known ports and pins then the compiler does what’s called “constant folding” and all the calculations are done as the code is compiled, not at run time.

So, to set bit 5 of porta, you need to:
set_pin(PORTA, 5);
Note that this is set_pin, and not set_bit, which is the generic macro for manipulating any byte. Set_pin and its friends are only for actions on ports.

This is why you’ll see these sort of #defines that you provide in your config.h:
#define i2c_scl_port PORTC
#define i2c_sda_port PORTC
#define i2c_scl_pin 3
#define i2c_sda_pin 4
You can see the PORTC in capitals here. Once you’ve defined it, you can use i2c_scl_port and i2c_scl_pin and it’s much more readable form the code what you’re talking about. In addition, if you decide to move where the hardware’s connected to, all you need to is update it in one place in the config.h.

The routines that are provided are:
set_pin(port_sfr_addr, pin)
clear_pin(port_sfr_addr, pin)
toggle_pin(port_sfr-addr, pin)
These work exactly as you’d expect, all buffered and safe from read-before-write problems. There are a couple of other handy functions as well:
test_pin(port_sfr_addr, pin)
returns the value of the pin (given it’s an input) allowing you to use the same naming convention for your input ports as well as outputs, eg:
set_pin(PORTA, 1);
if (test_pin(PORTA, 2) {
    // do something
}
You can also change a pin to a given value, eg,
change_pin(port_sfr_addr, pin, value)
like:
#define MY_VALUE 1
  change_pin(PORTA, 2, MY_VALUE);
Now, because of the way code is generated for pics, when you’re using constants for the pin and port values, these #defines give you nice, simple code. When you’re using variables, however, it does end up with more code. At that point, you’re generally better off using these set of functions:
set_pin_var(port_sfr_addr, pin)
clear_pin_var(port_sfr_addr, pin)
toggle_pin_var(port_sfr_addr, pin)
change_pin_var(port_sfr_addr, pin, value)
These functions actually use subroutines instead of inline code, but will result in less code overall if you want to do things like:
uns8 my_pin;
  my_pin = 5;
set_pin_var(PORTA, my_pin);
It’s a subtle difference, and it other environments, (eg programming for PCs) you wouldn’t even bother make the distinction. When every byte counts though, it’s worth making the effort. Of course, this uses another stack level – so you may wish to swap code size for stack level and use the set_pin version nevertheless.

As a general note, I’m pretty sure none of the PicPack library actually uses the set_pin_var family of functions. You’ll probably find that you generally know what pin you need to change well in advance – ie, at compile time.

Finally, these routines also make it easy (and beautifully clear in your code) to make pins either inputs or outputs:
make_input(port_sfr_addr, pin)
make_output(port_sfr_addr, pin)

Again, this allows you to do things like this:
make_output(PORTA, 3);
set_pin(PORTA, 3);
Notice how you don’t even need to know anything about tris bits and it is quite obvious what’s meant to be going on.

By way of summary, for the imaginary rt373 device, define your ports and pins in your config.h like this:
#define tr373_data_port PORTB
#define tr373_data_pin 1
#define tr373_clk_port PORTB
#define tr373_clk_pin 1
And then you can do things like this:
#include “pic_utils.h”
  // setup ports and pins
make_input(tr373_data_port, tr373_data_pin);
make_output(tr373_clk_port, tr373_clk_pin);
  // clock in the first value
clear_pin(tr373_clk_port, tr373_clk_pin);
set_pin(tr373_clk_port, tr373_clk_pin);
my_value = test_pin(tr373_data_port, tr373_data_pin);
…and so on. Easy! Note that while the PicPack library currently doesn’t do anything differently for PIC18 devices, the routines are perfectly portable between the devices without any code or even config changes.

2 comments:

Jesito said...

Hi Ian, I've just discovered your blog and I'm deligthed with these nice tutorials, thanks for sharing!.

Ian Harris said...

Thanks Jesito, I hope you enjoy them!