Let's Make Robots!

I2C IO expander/servo controller using a picaxe 20x2

20x2 picaxeI needed analog ports and servo control for the RPi in my R2D2 project that can be controlled with I2C. Because I have a few spare Picaxe ICs I decided to make one using a Picaxe 20X2. Currently I only need a single servo and a single analog input, but I don’t want to reprogram the Picaxe every time I add another sensor so I wrote the attached code to turn the 20X2 into a general purpose IO expander. 

This blog is a description of how it works and what its limitations are. I hope some of you may benefit from this or even better, come up with improvements.

NOTE: I am still testing this and finding typos and bugs. the code is in alpha stage and i'll post updates as i iron out the bugs.

Addressing

A Picaxe 20X2 in I2C slave mode is controlled just like an EEPROM. You read from or write to one of the 128 scratchpad ram addresses and every time you finish a write operation, an interrupt is fired so the program can act on changes to those values.

This program uses 3 bytes for every available pin. One byte to make the IO-pin an input, analog input, output, servo output or PWM output. The other two bytes for a pin are used to set the output value or to read the input data.

Other addresses are used for settings or special commands, like changing the slave-address.

address : function

0 : Pin B.0 mode

1 : Pin B.0 Data1 (LSB)

2 : pin B.0 Data2 (MSB)

...

3-41 : Pins B1-C.7

48 : set this to a non-zero value to process pin configuration changes. it will be set to 0 when pin modes are set

64 : this byte will give you the state of the inputs on the B-port. In Picaxe language this holds the pinsB variable

65 : this byte will give you the state of the inputs on the C-port. In Picaxe language this holds the pinsC variable

99 : write special commands to this address to set the frequency of the picaxe or write the pin settings to ROM

100 : loop delay: the value of this address determines how long the program pauses after processing all inputs and outputs.

101 : setting this address to a non-zero value will enable the internal pull-up resistors on inputs C.0, C.6, C.7, B.0, B.1, B.6 

102 : loop increments. Set this to a value of 1 to 50 to change how much the loopcounter is incremented every loop

 

All the default settings are stored in the EEPROM of the 20X2. If you change the pin functions or other settings, you can use a special command to write the state of the scratchpad to the EEPROM, so when you reset the picaxe, all settings are preserved. 

 

Pin functions

Not all pins on the 20X2 can perform all functions. Pin C.6 (picaxe numbering) for example can only be used as an input. Also, the serial out pin (A.0) is not used in the program, although it will not be hard to change the code to use that pin as well. So only the B-port and C-port pins are used and two of those (B.5 and B.7) are used for I2C communication.

That leaves 14 pins you can use. Only the pins marked ADC in this picture can be used as analog inputs.

If you change the function of a pin from input to output or vice versa, the change is not immediate. You have to issue the set-mode command (address 48) before the program changes the pin settings. 

All pins are set to input mode when you power-up the picaxe.

 

Input functions

Set the pin mode = 0 to make it a digital input. Don’t forget to set byte 48 to process the change. When the input is processed, the state of the pin will be written to the first databyte.  

Example: to make pin C.7 a digital input. Set byte 39 to 0 and then set byte 48 (set mode) to 1 or anything but 0. Then read the input value (high=255 or low=0) from byte 40.

 

Set the pin mode = 2 to make the pin an 8-bit analog input. Read the input value from the first databyte.

Example: to make pin C.7 analog input. Set byte 39 to 2 and then set byte 48 (set mode) to 1 or anything but 0. Then read the input value (0-255) from byte 40.

 

Set the pin mode = 3 to make the pin a 10-bit analog input. Read the input value as a 16-bit value from data1 and data2

Example: to make pin C.7 a 10-bit analog input. Set byte 39 to 3 and then set byte 48 (set mode) to 1 or anything but 0. Then read the input value (0-1024) from bytes 40 and 41. Note: because the value is written as LSB, MSB, you can read the value als a 16-bit word in one read.

 

Output functions

Set the pin mode = 1 to make the pin a digital output. Don’t forget to set byte 48 to process the change. Write data1 to anything non-zero to make the output high or to 0 to make it low.

Example: to make pin C.7 a digital output. Set byte 39 to 1 and then set byte 48 (set mode) to 1 or anything but 0. Write to byte 40 to make the out 0(low) or 1(high).

 

set the pin mode = 5 to make the pin a servo output. Write the position of the servo to data1. Note that you should set an initial value to data1 before you write to 48. Otherwise the program might send too high or too low values to the servo and that might damage it.

The data1 values correspond with the Picaxe basic servo command. A value of 150 (1.5ms) for middle position. These values are valid even if you change the frequency of the Picaxe, so at 64MHz a value of 150 is still 1.5Ms.

 

set the pin mode = 6 to make the pin a (software) PWM output. Send the duty value to data1 as a value from 0-100(%). Every value greater than 100 will be also seen as 100%.

 

Note that PWM is simply expressed as the number of loops that the pin will be held high. When you use servos in combination with PWM, the duration of the loop will vary due to the different durations of the output pulses to the servos. This will make the PWM somewhat unpredictable: especially if you have serval servos that change position often. 

To change the period of the PWM, you can change the loop delay (address 100) to make the loops longer. You can also change the default (8MHz) frequency of the picaxe to 4, 8, 16, 32 or 64 MHz. The loop delays effected by the frequency in the same way the Picaxe pause command is. A loop delay of 5(default) is 5ms at 8Mhz, but will be 2.5ms when you run the chip at 16Mhz

update: I timed the PWM and it seems 100 loops takes about 2 full seconds at 8Mhz. That is way to slow for use with PWM so  another command is added. You can now change how much the loop counter is incremented every loop. Setting it to 10 and running the CPU at 64Mhz will give you usable PWM but only with 10 different speeds.

How the program works

The program initially sets all the pins to inputs. All pull-up resistors are disabled. After that all the settings are read by copying the first 128 bytes of the EEPROM to the scratchpad. Then the set mode command is issued by calling the interrupt subroutine to process all the settings.

The main loop of the program simply checks every pin; one at a time. The pin mode is read from the scratchpad and if it is an input, the state of the pin is written to the data bytes of that pin. If the pin is an output, the pins data byte is read and the output is set.

There is a small delay at the end of the loop that can be changed by the I2C master.

 

Loops are counted from 1 to 100. The value of the loop counter is used in the PWM command. If the loop counter is higher than the value that is set in data1 of a PWM pin, the pin will be set low or 0. Otherwise the pin will be set high or 1. The period of the PWM is therefor 100 x loop-duration. Tweak the frequency and the loop-delay to influence the PWM-period. 

Servos are controlled using the Picaxe basic pulsout command. I2C and the servo command don’t go well together on a picaxe and the 20X2 normally only supports servo commands on the B-port only. This program should be able to handle 13 servo’s simultaneously, although I haven’t tried that out. Make sure you keep the loop time between 6ms and 30ms otherwise the servo will not respond properly.

 

Every time the I2C master writes to the Picaxe, the interrupt subroutine is called. This routine checks the modes off all the pins and makes the necessary changes like setting a pin to input or output or issuing the ADCsetup command. Commands are also handled in the interrupt. 

I have done very little testing on the PWM functions, so if you are going to try this out, please let me know how it goes.

All the addresses, modes and commands are described in the attached code. 

AttachmentSize
20X2_IO_expander_V0.4.bas14.68 KB

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I admit I did not read the whole thing, but maybe this would be of any help :http://letsmakerobots.com/content/arduino-i2c-picaxe

It turns out 100 loops at 8Mhz take more than 2 full seconds. Those picaxe programs run really really slow! If I speed everything up to make PWM anywhere near the point where you could use it for driving motors, the loops run really fast and the servo's get confused because the time between pulses gets too short. 

For usable PWm you need a period of maximum 100mS (10Hz). At 100 loops per period, that means 1 loop should take maximum 1mS, which is way too fast for servo's as a single pulse takes between 0.7 and 2.3mS.

So I added a method of changing the loop increment. Setting it to 10 will make 1 period only 10 loops. That might just give you a nice 10mS loop to run the servo's and use a 10 speed 10Hz PWM at the same time.

PicAxe's are great little chips for projects within their capabilites.

I've found some limits when trying to control both PWM motors and servos with the 20M2, but they are great for a ton of projects.

I really like this.  I've wanted to do something similar for a long time but have never gotten around to it.  I really like the way you've implemented it.  It should be VERY useful.  I really like the way you can define by I2C command what each pin function will be and save that in eeprom.    I suspect you will get a lot of mileage out of this.  Others will to.

I do have one concern.  The fact that you default on startup to having the pins as outputs.  If they are connected to some other device that is an output without protections and without being set up first, the chip could be damaged.  My personal preference would be to default to inputs as most microcontrollers do.

Great project.  Thanks for posting it here.  I got some great ideas from it.

Actually all pins turned out to be default inputs after a few mS, because as soon as the probram starts it reads the memory, which contains zero's, and sets all pins to input mode.

The Picaxe program loader writes zero to all EEPROM locations you haven't defined in the program. That caused another problem because the program tried to become a I2C slave at address 0x00 and that makes the picaxe unreachable from I2C and the programming cable. It took me a while to figure out what went wrong ;)

If you keep all pins as inputs and haven't connected them, that might become a possible source of problems because you have all pins floating: i.e. not being pulled up or down.

Anyway. i took your advice and changed the code. Thanks for the feedback!