softi2c_master: add software implementation I²C library
This commit is contained in:
parent
158d1899b9
commit
04901a6ce1
|
@ -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;
|
||||
}
|
|
@ -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);
|
Loading…
Reference in New Issue