diff --git a/softi2c_master.c b/softi2c_master.c new file mode 100644 index 0000000..0fd5df0 --- /dev/null +++ b/softi2c_master.c @@ -0,0 +1,377 @@ +/** 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; + +#define SDA_PORT GPIO_PB +#define SDA_PIN PB5 +#define SCL_PORT GPIO_PB +#define SCL_PIN PB4 + +// 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()); +} + +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(); + while (read_SCL() == 0); // Clock stretching + + // 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(); + while (read_SCL() == 0); // Clock stretching + + 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 + while (read_SCL() == 0); // Clock stretching + // 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 + while (read_SCL() == 0); // Clock stretching + 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; +} diff --git a/softi2c_master.h b/softi2c_master.h new file mode 100644 index 0000000..0cd8c27 --- /dev/null +++ b/softi2c_master.h @@ -0,0 +1,83 @@ +/** 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 + */ +#pragma once + +/** setup I²C peripheral + * @param[in] freq_khz desired clock frequency, in kHz + * @return if I²C bus is ready + */ +bool softi2c_master_setup(uint16_t freq_khz); +/** release I²C peripheral */ +void softi2c_master_release(void); +/** send start condition + * @return if start sent (else arbitration lost) + */ +bool softi2c_master_start(void); +/** sent stop condition + * @param[in] i2c I²C base address + * @return if stop sent (else arbitration lost) + */ +bool softi2c_master_stop(void); +/** select I²C slave device + * @param[in] slave I²C address of slave device to select + * @param[in] write this transaction will be followed by a read (false) or write (true) operation + * @return if slave ACKed + * @note includes (re-)start condition + */ +bool softi2c_master_select_slave(uint8_t slave, bool write); +/** read data over I²C + * @param[out] data array to store bytes read + * @param[in] data_size number of bytes to read + * @return if read succeeded (else arbitration lost) + * @warning the slave device must be selected before this operation + * @note includes sending stop (after having NACKed last received byte) + */ +bool softi2c_master_read(uint8_t* data, uint16_t data_size); +/** write data over I²C + * @param[in] data array of byte to write to slave + * @param[in] data_size number of bytes to write + * @return if write succeeded (else data has been NACKed) + * @warning the slave device must be selected before this operation + * @note no stop condition is sent at the end, allowing multiple writes + */ +bool softi2c_master_write(const uint8_t* data, uint16_t data_size); +/** read data from slave device + * @param[in] slave I²C address of slave device to select + * @param[out] data array to store bytes read + * @param[in] data_size number of bytes to read + * @return if read succeeded (else arbitration has been lost) + * @note start and stop conditions are included + */ +bool softi2c_master_slave_read(uint8_t slave, uint8_t* data, uint16_t data_size); +/** write data to slave device + * @param[in] slave I²C address of slave device to select + * @param[in] data array of byte to write to slave + * @param[in] data_size number of bytes to write + * @return if write succeeded + * @note start and stop conditions are included + */ +bool softi2c_master_slave_write(uint8_t slave, const uint8_t* data, uint16_t data_size); +/** read data at specific address from an I²C memory slave + * @param[in] slave I²C address of slave device to select + * @param[in] address memory address of slave to read from + * @param[in] address_size address size in bytes + * @param[out] data array to store bytes read + * @param[in] data_size number of bytes to read + * @return if read succeeded + * @note start and stop conditions are included + */ +bool softi2c_master_address_read(uint8_t slave, const uint8_t* address, uint16_t address_size, uint8_t* data, uint16_t data_size); +/** write data at specific address on an I²C memory slave + * @param[in] slave I²C address of slave device to select + * @param[in] address memory address of slave to write to + * @param[in] address_size address size in bytes + * @param[in] data array of byte to write to slave + * @param[in] data_size number of bytes to write + * @return if write succeeded + * @note start and stop conditions are included + */ +bool softi2c_master_address_write(uint8_t slave, const uint8_t* address, uint16_t address_size, const uint8_t* data, uint16_t data_size);