/** library to communicate using I²C as master, implemented in software * @file * @author King Kévin * @copyright SPDX-License-Identifier: GPL-3.0-or-later * @date 2021 * @note I implemented I²C in software because the hardware peripheral is hard to use, and buggy (I was not able to get rid of clock glitches corrupting the communication, undetected) * @note some methods copied from Wikipedia https://en.wikipedia.org/wiki/I%C2%B2C */ /* standard libraries */ #include // standard integer types #include // boolean types #include // general utilities /* own libraries */ #include "stm8s.h" // STM8S definitions #include "softi2c_master.h" // software I²C header and definitions // half period to wait for I²C clock static uint16_t period = 0; // port for data line #define SDA_PORT GPIO_PB // pin for data line #define SDA_PIN PB5 // port for clock line #define SCL_PORT GPIO_PB // pin for clock line #define SCL_PIN PB4 // operation timeout (in half period) #define TIMEOUT 10U // delay for half a period static void I2C_delay(void) { for (volatile uint16_t i = 0; i < period; i++); } // Return current level of SCL line, 0 or 1 static inline bool read_SCL(void) { return (SCL_PORT->IDR.reg & SCL_PIN); } // Return current level of SDA line, 0 or 1 static inline bool read_SDA(void) { return (SDA_PORT->IDR.reg & SDA_PIN); } // Do not drive SCL (set pin high-impedance) static inline void set_SCL(void) { SCL_PORT->ODR.reg |= SCL_PIN; } // Actively drive SCL signal low static inline void clear_SCL(void) { SCL_PORT->ODR.reg &= ~SCL_PIN; } // Do not drive SDA (set pin high-impedance) static inline void set_SDA(void) { SDA_PORT->ODR.reg |= SDA_PIN; } // Actively drive SDA signal low static inline void clear_SDA(void) { SDA_PORT->ODR.reg &= ~SDA_PIN; } bool softi2c_master_setup(uint16_t freq_khz) { // enforce minimal frequency if (0 == freq_khz) { freq_khz = 1; } // calculated period from frequency (hand tuned value using 16 MHz clock) period = 589 / (1 << CLK->CKDIVR.fields.HSIDIV) / (1 << CLK->CKDIVR.fields.CPUDIV) / freq_khz; // switch pins to open drain SCL_PORT->ODR.reg |= SCL_PIN; // ensure clock is high SCL_PORT->DDR.reg |= SCL_PIN; // switch pin to output SCL_PORT->CR1.reg &= ~SCL_PIN; // use in open-drain mode SDA_PORT->ODR.reg |= SDA_PIN; // ensure data is high SDA_PORT->DDR.reg |= SDA_PIN; // switch pin to output SDA_PORT->CR1.reg &= ~SDA_PIN; // use in open-drain mode I2C_delay(); // give time to get high return (read_SCL() && read_SDA()); // line is ready when the two lines are high } void softi2c_master_release(void) { SCL_PORT->DDR.reg &= ~SCL_PIN; // switch pin to input SDA_PORT->DDR.reg &= ~SDA_PIN; // switch pin to input } // if transaction has already started static bool started = false; bool softi2c_master_start(void) { if (started) { // if started, do a restart condition // set SDA to 1 set_SDA(); I2C_delay(); set_SCL(); uint8_t timeout = TIMEOUT; while (read_SCL() == 0 && timeout) { // Clock stretching I2C_delay(); timeout--; } if (0 == timeout) { return false; } // Repeated start setup time, minimum 4.7us I2C_delay(); } if (read_SDA() == 0) { return false; } // SCL is high, set SDA from 1 to 0. clear_SDA(); I2C_delay(); clear_SCL(); started = true; return true; } bool softi2c_master_stop(void) { // set SDA to 0 clear_SDA(); I2C_delay(); set_SCL(); uint8_t timeout = TIMEOUT; while (read_SCL() == 0 && timeout) { // Clock stretching I2C_delay(); timeout--; } if (0 == timeout) { return false; } I2C_delay(); // Stop bit setup time, minimum 4us // SCL is high, set SDA from 0 to 1 set_SDA(); I2C_delay(); if (read_SDA() == 0) { return false; } started = false; return true; } // Write a bit to I²C bus static bool softi2c_master_write_bit(bool bit) { // set data bit if (bit) { set_SDA(); } else { clear_SDA(); } I2C_delay(); // SDA change propagation delay set_SCL(); // Set SCL high to indicate a new valid SDA value is available I2C_delay(); // Wait for SDA value to be read by slave, minimum of 4us for standard mode uint8_t timeout = TIMEOUT; while (read_SCL() == 0 && timeout) { // Clock stretching I2C_delay(); timeout--; } if (0 == timeout) { return false; } // SCL is high, now data is valid if (bit && (read_SDA() == 0)) { // If SDA is high, check that nobody else is driving SDA return false; } clear_SCL(); // Clear the SCL to low in preparation for next change return true; } // Read a bit from I²C bus static bool softi2c_master_read_bit(void) { set_SDA(); // Let the slave drive data I2C_delay(); // Wait for SDA value to be written by slave, minimum of 4us for standard mode set_SCL(); // Set SCL high to indicate a new valid SDA value is available uint8_t timeout = TIMEOUT; while (read_SCL() == 0 && timeout) { // Clock stretching I2C_delay(); timeout--; } if (0 == timeout) { return false; } I2C_delay(); // Wait for SDA value to be written by slave, minimum of 4us for standard mode const bool bit = read_SDA(); // SCL is high, read out bit clear_SCL(); // Set SCL low in preparation for next operation return bit; } // Write a byte to I2C bus. Return true if ACK by the slave. static bool softi2c_master_write_byte(uint8_t byte) { for (uint8_t bit = 0; bit < 8; ++bit) { softi2c_master_write_bit((byte & 0x80) != 0); byte <<= 1; } const bool nack = softi2c_master_read_bit(); return !nack; } // Read a byte from I²C bus static uint8_t softi2c_master_read_byte(bool nack) { uint8_t byte = 0; for (uint8_t bit = 0; bit < 8; ++bit) { byte = (byte << 1) | softi2c_master_read_bit(); } softi2c_master_write_bit(nack); return byte; } bool softi2c_master_select_slave(uint8_t slave, bool write) { if (!softi2c_master_start()) { // send (re-)start condition return false; } const uint8_t byte = (slave << 1) | (write ? 0 : 1); // select slave, with read/write flag return softi2c_master_write_byte(byte); // select slave } bool softi2c_master_read(uint8_t* data, uint16_t data_size) { if (NULL == data || 0 == data_size) { // no data to read return false; } for (uint16_t i = 0; i < data_size; i++) { // read bytes IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog if (1 == (data_size - i)) { // last byte data[i] = softi2c_master_read_byte(true); // NACK after reading byte } else { data[i] = softi2c_master_read_byte(false); // ACK after reading byte } } return softi2c_master_stop(); } bool softi2c_master_write(const uint8_t* data, uint16_t data_size) { if (NULL == data || 0 == data_size) { // no data to read return true; // we don't indicate an error because the stop is done separately } // write data for (uint16_t i = 0; i < data_size; i++) { // write bytes if (!softi2c_master_write_byte(data[i])) { // write byte return false; } } return true; } bool softi2c_master_slave_read(uint8_t slave, uint8_t* data, uint16_t data_size) { if (NULL == data && data_size > 0) { // no data to read return false; } if (!softi2c_master_select_slave(slave, false)) { // select slave to read softi2c_master_stop(); return false; } if (NULL != data && data_size > 0) { // only read data if needed if (!softi2c_master_read(data, data_size)) { // read data (includes stop) return false; } } return true; } bool softi2c_master_slave_write(uint8_t slave, const uint8_t* data, uint16_t data_size) { if (NULL == data && data_size > 0) { // no data to read return false; } bool rc = false; if (!softi2c_master_select_slave(slave, true)) { // select slave to write goto error; } if (NULL != data && data_size > 0) { // write data only is some is available if (!softi2c_master_write(data, data_size)) { // write data goto error; } } rc = true; // all went well error: rc = softi2c_master_stop() && rc; // sent stop condition return rc; } bool softi2c_master_address_read(uint8_t slave, const uint8_t* address, uint16_t address_size, uint8_t* data, uint16_t data_size) { if (address_size > 0 && NULL == address) { return false; } if (data_size > 0 && NULL == data) { return false; } bool rc = false; rc = softi2c_master_select_slave(slave, true); // select slave to write if (!rc) { goto error; } // write address if (NULL != address && address_size > 0) { rc = softi2c_master_write(address, address_size); // send memory address if (!rc) { goto error; } } // read data if (NULL != data && data_size > 0) { rc = softi2c_master_select_slave(slave, false); // re-select slave to read if (!rc) { goto error; } rc = softi2c_master_read(data, data_size); // read memory (includes stop) if (!rc) { goto error; } } else { softi2c_master_stop(); // sent stop condition } rc = true; error: if (!rc) { // only send stop on error softi2c_master_stop(); // sent stop condition } return rc; } bool softi2c_master_address_write(uint8_t slave, const uint8_t* address, uint16_t address_size, const uint8_t* data, uint16_t data_size) { if (address_size > 0 && NULL == address) { return false; } if (data_size > 0 && NULL == data) { return false; } bool rc = false; rc = softi2c_master_select_slave(slave, true); // select slave to write if (!rc) { goto error; } if (address_size && address) { rc = softi2c_master_write(address, address_size); // send memory address if (!rc) { goto error; } } if (data_size && data) { rc = softi2c_master_write(data, data_size); // send memory data if (!rc) { goto error; } } rc = softi2c_master_stop(); // sent stop condition if (!rc) { return false; } rc = true; // all went fine error: if (!rc) { softi2c_master_stop(); // send stop on error } return rc; }