I2C IO expander/servo controller using a picaxe 20x2

20X2_IO_expander_V0.4.bas (15028Bytes)


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


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. 

Well done!

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.