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.