{{pfw:banner.png}} ====== I2C, The I2C protocol ====== **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! ===== The Idea of I2C ===== |{{https://user-images.githubusercontent.com/11397265/120920357-a63c7d00-c6be-11eb-8f43-5287f7f82a9c.jpg|i2c-logo}}|[[https://www.nxp.com/docs/en/user-guide/UM10204.pdf|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 [[https://www.i2c-bus.org/|I2C website]] or [[https://en.wikipedia.org/wiki/I%C2%B2C|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.** ---- ==== The low level I2C states for a single master are in short: ==== * Start condition * Address a device (for read or write) A device may stretch the clock cycle to allow for a slow response\\ When a device exists and is ready, it responds with an ACK * Read or write one or more data bytes After each byte an ACK is received, a NAK is received when it is the last byte * Stop condition {{https://user-images.githubusercontent.com/11397265/120920036-121de600-c6bd-11eb-9e0c-0ab8664f9c47.jpg|I2C logic analyzer tracks}} **A read and a write to a PCF8574** ---- ==== Pseudo code for low level bitbang I2C ==== 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 ---- ==== When looked to I2C from a higher level it's access is: ==== 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 ==== I2C pseudo code with high level factorisation ==== 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} ---- ==== Generic Forth low level part of bitbang example ==== 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. {{https://user-images.githubusercontent.com/11397265/123260134-83e79380-d4f5-11eb-86e8-8f3c6d46b4ba.jpg|Minimal forth example reading EEPROM}}\\ **Read byte from I2C EEPROM** %%**%%* ===== Generic Forth example ===== 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 ==== I2C implementation examples ==== 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' | ---- ==== Dedicated implementations ==== Have a look at the sub directories for implementations for different systems. * [[en:pfw:i2c_msp430|MSP430]], bitbang & hardware specific I2C implementations for MSP430 * [[en:pfw:i2c_gd32vf|GD32VF103]], bitbang & hardware specific I2C implementations for the Risc-V * [[https://github.com/project-forth-works/project-forth-works/tree/main/Communication-Protocols/I2C/Raspberry3B%2B|Raspberry3B+]], bitbang & hardware specific I2C implementation for BCM2835 * [[en:pfw:i2c_device-drivers|Generic device drivers]], for EEPROM, OLED, LCD, Clocks, etc.