softi2c_master: add software implementation I²C library

This commit is contained in:
King Kévin 2021-08-17 11:30:53 +02:00
parent 158d1899b9
commit 04901a6ce1
2 changed files with 460 additions and 0 deletions

377
softi2c_master.c Normal file
View File

@ -0,0 +1,377 @@
/** library to communicate using I²C as master, implemented in software
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @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 <stdint.h> // standard integer types
#include <stdbool.h> // boolean types
#include <stdlib.h> // 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;
}

83
softi2c_master.h Normal file
View File

@ -0,0 +1,83 @@
/** library to communicate using I²C as master, implemented in software
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @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);