The PFW universal I2C drivers The I2C protocol is a must-have for any microcontroller. It opens up access to IO-expanders, real-time clocks, more memory and much much more. But starting with I2C in Forth might seem a daunting task. To help you use, and understand, the I2C protocol, PFW has developed a universal set of drivers for 3 different hardware platforms (MSP430, Risc-V, Raspberry/ARM). At the top level the drivers are the same for each platform. For instance, a device-driver for a LCD-display developed for the MSP430 will also work on the other platforms. This makes developing new high-level drivers simple as there are always examples available to base your new driver on. The drivers are structured into 3 classical layers. First a small hardware specific layer for each of the platforms (both a bitbang and internal version for each). On top of this sits a universal high level abstraction layer. And on top of this sit the actual device drivers. Amazingly the hardware specific layer is very small, a few definitions is all that is needed for each platform. And this although the internal I2C-hardware for the 3 platforms is very different.
With these drivers, the use of I2C should not be a problem for anyone. Have fun exploring and using I2C-devices!
![]() | I2C is a synchronous serial protocol with two lines (SDA & SCL). It is used for clocks, memory, I/O-expanders, sensors, etc. An in-depth protocol description of the I2C signals can be found on the I2C website or on wikipedia. |
| SDA | Serial Data Line |
| SCL | Serial Clock Line |
Note that the I2C-protocol uses 7-bits addresses and a read/write bit, but in some cases an 8-bit address is mentioned. Even the original designer of the protocol, Philips, sometimes falls into this trap.
A read and a write to a PCF8574
This pseudo code is without the use of clock stretching. This is only necessary when you use a multi master system or I2C slaves implemented on slow microcontrollers. Note that the used example works on on a chip with a push/pull output ports
Reserve RAM memory cells named: DEV SUM NACK?
Function: WAIT ( -- )
Delay for about 5 µsec.
Function: I2START ( -- )
Clock line high, wait, generate low flank on data line, wait
Function: I2ACK ( -- )
Clock line low, data line low, wait,
clock line high, wait
Function: I2NACK ( -- )
Clock line low, data line high, wait,
clock line high, wait
Function: I2ACK@ ( -- )
Clock line low, data line high, wait, clock line high, wait
Read status of data line, store true in NACK? if it is a nack, otherwise false
Function: BUS! ( x -- )
8 loop
clock line low
write bit-7 level of x to data line, wait
clock line high, wait
shift x left
Discard byte, perform i2ack@
Function: {I2C-ADDR ( +n -- )
store +n + 1 in SUM, perform i2start
read dev, perform bus!
Higher level I2C access, hides internal details!
Function: I2C-ON ( -- )
Setup I/O-bits for two bidirectional
open collector lines with pull-up
Function: I2C} ( -- )
Clock line high, wait, generate high flank on data line, wait
Function: BUS@ ( -- y )
Initialise y at zero
8 loop
shift y left
Clock line low, data line high, wait, clock line high, wait
read data line to bit-0 position of y
Decrease SUM
Sum not zero IF perform i2ack ELSE perform i2nack THEN
Function: DEVICE! ( dev -- )
Multiply dev by 2, AND result with 0xFE and store in DEV
Function: {I2C-WRITE ( +n -- )
Discard +n, perform i2start, read DEV, (bus!
Read nack? issue error message when true
Function: {I2C-READ ( +n -- )
Store +n in SUM, perform i2start, read DEV and set lowest bit,
Perform bus!, read nack? issue error message when true
Function: {DEVICE-OK?} ( -- fl ) \ Flag 'fl' is true when an ACK is received
Perform {i2c-addr, perform i2c}
Read nack?, leave true when result is zero
\ Waiting for an EEPROM write to succeed is named acknowledge polling.
Function: {POLL} ( -- ) Start loop {device-ok?} leave when ACK received
Function: {I2C-OUT ( dev +n -- ) Store dev in DEV perform {i2c-write
Function: {I2C-IN ( dev +n -- ) Store dev in DEV perform {i2c-read
Function: BUS!} ( b -- ) Perform bus!, perform i2c}
Function: BUS@} ( -- b ) Perform bus@, perform i2c}
Function: BUS-MOVE ( a u -- ) Sent string of 'u' bytes from 'a' over the I2C-bus
1) Simple write action - Open I2C-bus for write access to bus-address and output one byte to I2C-bus - Close I2C-bus access
2) Multiple write action to a devices register or address: - Open I2C-bus for write access to bus-address and output one byte to I2C-bus - Output one ore more byte(s) to I2C-bus (with auto increment) - Close I2C-bus access
3) A read action from a devices register or address: - Open I2C-bus for write access to bus-address and output the address byte to I2C-bus - Open I2C-bus for reading (Repeated start) - Read one byte from I2C-bus - Close I2C-bus access
4) Multiple read action from a devices register or address: - Open I2C-bus for write access to bus-address and output the address byte to I2C-bus - Open I2C-bus for reading (Repeated start) - Read one or more byte(s) from I2C-bus (with auto increment) - Close I2C-bus access
Function: >PCF8574 ( byte dev-addr -- )
perform device! 1 perform {i2c-write perform bus! perform i2c}
Function: PCF8574> ( dev-addr -- byte )
perform device! 1 perform {i2c-read perform bus@ perform i2c}
This example has the I2C interface pins connected like this.
SDA (Serial DAta line) = bit-7 SCL (Serial CLock line) = bit-6
The used addresses are for port-1 of the MSP430G2553:
Note that the MSP430 controller series does not have the easiest I/O structure to implement a bitbang version of I2C! This is because it only has push/pull outputs and I2C needs an open collector (or open drain) output. So this example code mimics open collector ports.
Extra words: ABORT" TUCK
Words with hardware dependencies:
: *BIS ( mask addr -- ) tuck c@ or swap c! ;
: *BIC ( mask addr -- ) >r invert r@ c@ and r> c! ;
: BIT* ( mask addr -- b ) c@ and ;
20 constant P1IN \ Port-1 input register
21 constant P1OUT \ Port-1 output register
22 constant P1DIR \ Port-1 direction register
26 constant P1SEL \ Port-1 function select
27 constant P1REN \ Port-1 resistor enable (pullup/pulldown)
42 constant P1SEL2 \ Port-1 function select-2
40 constant SCL \ I2C clock line
80 constant SDA \ I2C data line
SCL SDA or constant IO \ I2C bus lines
: WAIT ( -- ) \ Delay of 5 µsec. must be trimmed!
( true drop ) ;
: I2START ( -- )
scl p1out *bis scl p1dir *bic wait
sda p1dir *bis sda p1out *bic wait ;
: I2ACK ( -- )
scl p1out *bic scl p1dir *bis
sda p1out *bic sda p1dir *bis wait
scl p1out *bis scl p1dir *bic wait ;
: I2NACK ( -- )
scl p1out *bic scl p1dir *bis
sda p1out *bis sda p1dir *bic wait
scl p1out *bis scl p1dir *bic wait ;
: I2ACK@ ( -- flag )
scl p1out *bic scl p1dir *bis
sda p1out *bis sda p1dir *bic wait
scl p1out *bis scl p1dir *bic wait
sda p1in bit* nack? ! ;
: BUS! ( byte -- )
8 0 do
scl p1out *bic scl p1dir *bis
dup 80 and if
sda p1out *bis sda p1dir *bic
else
sda p1out *bic sda p1dir *bis
then
wait 2*
scl p1out *bis scl p1dir *bic wait
loop drop i2ack@ ;
: {I2C-ADDR ( +n -- )
drop i2start dev @ bus! ; \ Start I2C write with address in DEV
\ Higher level I2C access, hides internal details!
\ Note that this setup is valid for an MSP430 with external pull-up resistors attached!
\ On hardware which is able to use an open collector (or open source) with pull-up
\ resistor, you should initialise this mode!
: I2C-ON ( -- )
io p1ren *bic \ Deactivate pull-up/pull-down resistors
io p1dir *bic \ SDA & SCL are inputs
io p1out *bis \ Which start high
io p1sel *bic \ Guarantee normal i/o on MSP430
io p1sel2 *bic ;
: BUS@ ( -- byte )
0 8 0 do
2*
scl p1out *bic scl p1dir *bis
sda p1out *bis sda p1dir *bic wait
sda p1in bit* 0= 0= 1 and or
scl p1out *bis scl p1dir *bic wait
loop -1 sum +!
sum @ if i2ack else i2nack then ;
: I2C} ( -- )
scl p1out *bic scl p1dir *bis
sda p1out *bic sda p1dir *bis wait
scl p1out *bis scl p1dir *bic wait
sda p1out *bis sda p1dir *bic ;
: DEVICE! ( dev -- ) 2* FE and dev ! ;
: {DEVICE-OK?} ( -- f ) 0 {i2c-addr i2c} nack? @ 0= ; \ 'f' is true when an ACK was received
: {I2C-WRITE ( +n -- ) {i2c-addr nack? @ abort" Ack error" ; \ Start I2C write with device in DEV
: {I2C-READ ( +n -- ) \ Start read to device in DEV
sum ! i2start dev @ 1+ bus! \ Used for repeated start
nack? @ abort" Ack error" ;
\ Waiting for an EEPROM write to succeed is named acknowledge polling.
: {POLL} ( -- ) begin {device-ok?} until ; \ Wait until ACK received
: {I2C-OUT ( dev +n -- ) swap device! {i2c-write ;
: {I2C-IN ( dev +n -- ) swap device! {i2c-read ;
: BUS!} ( b -- ) bus! i2c} ;
: BUS@} ( -- b ) bus@ i2c} ;
: BUS-MOVE ( a u -- ) bounds ?do i c@ bus! loop ; \ Send string of bytes from 'a' with length 'u
This example is for an 8-bit PCF8574 like I/O-expander:
: >PCF8574 ( byte dev-addr -- )
device! 1 {i2c-write bus! i2c} ;
: PCF8574> ( dev-addr -- byte )
device! 1 {i2c-read bus@ i2c} ;
More examples can be found in the file i2c-examples.f, for the EEPROM code you may adjust the address constant #EEPROM. Note that the programming page size is different between EEPROM sizes. More info in the file.
See the list of example words below.
| Word | Stack | Description |
|---|---|---|
I2C? | ( dev – ) | Show or device 'dev' is present on the bus |
SCAN-I2C | ( – ) | Show a grid with all device addresses found on the bus |
>PCF8574 | ( b dev – ) | Write data to 8-bit I/O-extender with 'dev' address |
PCF8574> | ( dev – b ) | Read data from 8-bit I/O-extender with 'dev' address |
EC@ | ( addr – b ) | Fetch byte from address in EEPROM |
EC! | ( b addr – ) | Store byte to address in EEPROM |
EDMP | ( addr – ) | Dump EEPROM memory from EEPROM 'addr' onward |
WRITE-PAGE | ( sa1 ta1 dev +n – sa2 ta2 ) | Write '+n' bytes data from 'sa1' to 'ta1' in 'dev' etc. |
WRITE-MEMORY | ( sa ta u – ) | Write 'u' bytes data from 'sa' to 'ta' |
EEFILL | ( a u b – ) | Fill 'u' EEPROM bytes from address 'a' with byte 'b' |
Have a look at the sub directories for implementations for different systems.