soft i2c work

added a check to soft i2c master for clock stretch on write.

mostly implemented soft i2c slave.
This commit is contained in:
true 2024-10-19 19:14:33 -07:00
parent 904fcf65b4
commit 16ffde1077
5 changed files with 283 additions and 54 deletions

View File

@ -10,7 +10,8 @@
*
* quirks and features:
* - this implementation is blocking.
* - partial clock stretching is supported (only during read phase).
* - partial clock stretching is supported
* (only during start of write, and within read phase).
* - timings likely haven't been tested so long as this message exists.
*/
@ -152,6 +153,9 @@ uint8_t i2cm_wr(uint8_t dat)
uint8_t x;
uint8_t ack;
SCL_IN_HI();
while (!SCL_GET()); // clock stretch
for (x = 8; x; x--) {
if (dat & 0x80) { SDA_IN_HI(); }
else { SDA_OUTLO(); }

View File

@ -4,7 +4,6 @@
* hastily written. likely has bugs.
*
* known quirks:
* - STOP is never checked for
* - a fast repeated start may not work as this code
* does not check for START after the beginning
*/
@ -16,10 +15,13 @@
#include <stdint.h>
#define I2CSS_ERR_TIMEOUT 4 // timeout in bit-times before i2c slave releases control
#define I2CSS_IDLE_TIMEOUT 2 // idle timeout required in bit-times before i2c slave can be addressed again
#define I2CSS_ERR_TIMEOUT 6 // timeout in bit-times before i2c slave releases control
#define I2CSS_IDLE_TIMEOUT 3 // idle timeout required in bit-times before i2c slave can be addressed again
// todo: use with struct instead of hard-coding to allow for multiple slaves, but make it fast too
// note: will have issues with non-responsive slaves with more than one, so is it necessary?
#define SDA_PIN 13
#define SDA_BIT (1 << (SDA_PIN % 8))
#define SDA_DIR_REG R8_PA_DIR_0
@ -47,54 +49,51 @@
static uint16_t err_timeout;
static uint16_t bit_time;
static uint16_t idle_timeout = 0;
static inline inline void i2css_reload_timeout(uint16_t bit_time)
static int8_t i2css_wait_for_scl_hi()
{
err_timeout = I2CSS_ERR_TIMEOUT * bit_time;
int16_t timeout = I2CSS_ERR_TIMEOUT * bit_time;
while (!SCL_GET()) { if (!timeout--) return I2CSS_SCL_TIMEOUT; }
return I2CSS_OK;
}
static inline int8_t i2css_wait_for_scl_hi()
{
while (!SCL_GET()) { if (!err_timeout--) return -1; }
return 0;
}
static inline int8_t i2css_wait_for_scl_lo()
static int8_t i2css_wait_for_scl_lo()
{
uint8_t old_data = SDA_GET();
int16_t timeout = I2CSS_ERR_TIMEOUT * bit_time;
err_timeout--; // compensation
timeout--; // compensation
while (SCL_GET()) {
if (err_timeout <= 1) return -1;
if (SDA_GET() != old_data) return -2;
err_timeout -= 2;
if (timeout <= 1) return I2CSS_SCL_TIMEOUT;
if (SDA_GET() != old_data) return I2CSS_START_STOP;
timeout -= 2;
}
return 0;
return I2CSS_OK;
}
static inline int8_t i2css_ack(uint16_t bit_time)
{
uint8_t ret;
SDA_OUTLO();
i2css_reload_timeout(bit_time);
if (i2css_wait_for_scl_hi()) return I2CSS_ACK_ERROR;
i2css_reload_timeout(bit_time);
if (i2css_wait_for_scl_lo()) return I2CSS_ACK_TIMEOUT;
ret = i2css_wait_for_scl_lo();
SDA_IN_HI();
return 0;
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_ACK_TIMEOUT;
return ret;
}
// blocking implementation of i2c slave, with 8-bit address register.
// basically, simulates a 24C02 EEPROM.
// externally kicked off by a low transition on SCL.
int8_t i2css_idle_init(struct I2CSoftSlave *slave)
{
// set up a hardware timer to tick our idle timer
@ -120,63 +119,206 @@ int8_t i2css_idle_check(struct I2CSoftSlave *slave)
int8_t i2css_read(struct I2CSoftSlave *slave)
{
return 0;
int8_t i;
int8_t ret;
uint8_t ack = 0;
uint8_t dat = 0;
// with no read function configured, we can't do anything
if (!slave->rd_cb) {
SCL_IN_HI();
return I2CSS_NO_READ_FUNCTION;
}
// clock low for first bit / stretch, get data to send to master
SCL_OUTLO();
dat = slave->rd_cb();
// release clock, wait for clock to be low by the master
SCL_IN_HI();
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_READ_TIMEOUT;
if (ret) return ret;
// send data to master
for (i = 8; i; i--) {
if (dat &= 0x80) { SDA_IN_HI(); }
else { SDA_OUTLO(); }
dat <<= 1;
if (i2css_wait_for_scl_hi()) return I2CSS_READ_ERROR;
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_READ_TIMEOUT;
if (ret) return ret;
}
// get (n)ack from master
if (i2css_wait_for_scl_hi()) return I2CSS_READ_ERROR;
ack = SDA_GET();
return ack ? I2CSS_NACK : I2CSS_OK;
}
int8_t i2css_write(struct I2CSoftSlave *slave)
{
int8_t i;
int8_t ret;
uint8_t dat = 0;
uint8_t ack;
// release clock for master
SCL_IN_HI();
// with no read function configured, we can't do anything
if (!slave->wr_cb) {
return I2CSS_NO_WRITE_FUNCTION;
}
// get data from master
for (i = 8; i; i--) {
if (i2css_wait_for_scl_hi()) return I2CSS_WRITE_ERROR;
dat |= SDA_GET() ? 1 : 0;
dat <<=1;
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_WRITE_TIMEOUT;
if (ret) return ret;
}
// stretch clock, write our data with callback
SCL_OUTLO();
ack = slave->wr_cb(dat);
// send (n)ack to master
if (ack) { SDA_IN_HI(); } // nack
else { SDA_OUTLO(); } // ack
// release clock and wait
SCL_IN_HI();
if (i2css_wait_for_scl_hi()) return I2CSS_WRITE_ERROR;
// wait for clock to go low
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_WRITE_TIMEOUT;
if (ret) return ret;
// then stretch
SCL_OUTLO();
return 0;
}
int8_t i2css_blocking(struct I2CSoftSlave *slave)
// blocking implementation of i2c slave, with 8-bit address register.
// basically, simulates a 24C02 EEPROM.
// externally kicked off by a low transition on SCL.
static inline int8_t i2css_process(struct I2CSoftSlave *slave)
{
int8_t i;
int32_t i;
int8_t ret;
uint8_t rx_addr = 0;
SCL_IN_HI();
// wait for SCL low to indicate start condition
i2css_reload_timeout(slave->bit_time);
while (!SCL_GET()) { if (!err_timeout--) return I2CSS_START_SCL_NOT_LOW; }
// wait for SCL low to indicate data has begun
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_START_SCL_NOT_LOW;
if (ret) return ret;
// read address from bus
for (i = 0; i < 7; i++) {
i2css_reload_timeout(slave->bit_time);
for (i = 7; i; i--) {
if (i2css_wait_for_scl_hi()) return I2CSS_ADDR_READ_ERROR;
rx_addr |= SDA_GET() ? 1 : 0;
rx_addr <<=1;
i2css_reload_timeout(slave->bit_time);
if (i2css_wait_for_scl_lo()) return I2CSS_ADDR_READ_TIMEOUT;
ret = i2css_wait_for_scl_lo();
if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_ADDR_READ_TIMEOUT;
if (ret) return ret;
}
// is this us?
slave->addr_last_seen = rx_addr;
if (slave->addr != rx_addr) return I2CSS_ADDR_NO_MATCH;
// seems so. get the read/write bit
i2css_reload_timeout(slave->bit_time);
if (i2css_wait_for_scl_hi()) return I2CSS_ADDR_READ_LASTBIT_TIMEOUT;
rx_addr |= SDA_GET() ? 1 : 0;
// let the host know we're ready to serve
i = i2css_ack(slave->bit_time);
if (i) return i;
ret = i2css_ack(slave->bit_time);
if (ret) return ret;
// stretch clock while preparing slave to receive
SCL_OUTLO();
if (slave->start_cb) slave->start_cb();
switch (rx_addr & 1) {
case 0: { // writing
while (!i2css_write(slave));
case 0: { // master is writing to us
do {
ret = i2css_write(slave);
} while (!ret);
break;
}
case 1: { // reading
while (!i2css_read(slave)) {
i = i2css_ack(slave->bit_time);
if (i) return i;
}
case 1: { // master is reading from us
do {
// stretch clock
SCL_OUTLO();
ret = i2css_read(slave);
} while (!ret);
break;
}
}
return 0;
return ret;
}
int8_t i2css_blocking(struct I2CSoftSlave *slave)
{
int8_t ret;
int8_t start;
do {
ret = i2css_process(slave);
start = 0;
if (ret == I2CSS_START_STOP) {
// determine which
// high = stop, low = start
start = SDA_GET() ? 0 : 1;
}
} while (start);
return ret;
}
// call this function with SDA falling low interrupt.
int8_t i2css_start(struct I2CSoftSlave *slave)
{
uint8_t scl;
// idle our clock and check for start/stop
SCL_IN_HI();
scl = SCL_GET();
// module will use this bit time for this communication
bit_time = slave->bit_time;
// ensure SDA pin will go low when set as an output
SDA_SET_LO();
// if SCL is high, this should be a start condition.
if (scl) {
return i2css_blocking(slave);
}
// if it isn't, there must be some other communication going on, and it doesn't involve us
return I2CSS_BUS_BUSY;
}

View File

@ -15,16 +15,19 @@
typedef struct I2CSoftSlave {
uint8_t addr;
uint8_t addr2;
uint8_t addr_last_seen;
uint16_t bit_time;
uint8_t (*rd_cb)(uint8_t addr, uint8_t *data); // return 0 to ACK
void (*wr_cb)(uint8_t addr, uint8_t data);
void (*start_cb)(); // call on any new read/write operation
uint8_t (*rd_cb)(); // returns data
int8_t (*wr_cb)(uint8_t data); // return 0 to ACK, all other NACK
} I2CSoftSlave;
enum I2CSoftSlave_Error {
// standard responses
I2CSS_OK,
I2CSS_STOP,
I2CSS_NACK,
I2CSS_ADDR_NO_MATCH,
// errors
I2CSS_START_SCL_NOT_LOW = -1,
@ -33,11 +36,22 @@ enum I2CSoftSlave_Error {
I2CSS_ADDR_READ_LASTBIT_TIMEOUT = -4,
I2CSS_ACK_ERROR = -5, // could be timeout, start or stop condition detected
I2CSS_ACK_TIMEOUT = -6,
I2CSS_READ_ERROR = -7,
I2CSS_READ_TIMEOUT = -8,
I2CSS_WRITE_ERROR = -9,
I2CSS_WRITE_TIMEOUT = -10,
I2CSS_NO_READ_FUNCTION = -32,
I2CSS_NO_WRITE_FUNCTION = -33,
I2CSS_SCL_TIMEOUT = -126,
I2CSS_BUS_BUSY = -127,
I2CSS_START_STOP = -128
};
int8_t i2css_blocking(struct I2CSoftSlave *slave);
// call this function with SDA falling low interrupt.
void i2css_init();
int8_t i2css_start(struct I2CSoftSlave *slave);

View File

@ -107,5 +107,5 @@ void ch32sub_write_1b(uint16_t reg, uint8_t dat)
void ch32sub_rgb_hwen(uint8_t en)
{
i2c_addr(SUB_I2C_ADDR, 1);
ch32sub_write_1b(REG_RGB_HWEN, en);
}

View File

@ -28,11 +28,75 @@
struct I2CSoftSlave gat_slave;
enum {
GAT_MODE_STARTING,
GAT_MODE_READ,
GAT_MODE_WRITE
};
uint8_t gat_state;
uint16_t gat_data_idx;
uint8_t gat_data[GAT_DATA_SIZE];
uint8_t gat_i2c_read_cb()
{
uint8_t ret;
switch (gat_state) {
case GAT_MODE_STARTING: {
gat_state = GAT_MODE_READ;
// no break
}
case GAT_MODE_READ: {
// can't read past end of buffer; just send 0xff
if (gat_data_idx == GAT_DATA_SIZE) {
ret = 0xff;
} else {
ret = gat_data[gat_data_idx & 0xff];
if (gat_data_idx <= GAT_DATA_SIZE) gat_data_idx++;
}
return ret;
}
default: {
return 0;
}
}
}
int8_t gat_i2c_write_cb(uint8_t dat)
{
switch (gat_state) {
case GAT_MODE_STARTING: {
// master wrote our address
gat_data_idx = dat;
gat_state = GAT_MODE_WRITE;
break;
}
case GAT_MODE_WRITE: {
if (gat_data_idx >= GAT_DATA_SIZE) {
return I2CSS_NACK;
} else {
gat_data[gat_data_idx++] = dat;
}
return I2CSS_OK;
}
default: {
return I2CSS_NACK;
}
}
}
void gat_i2c_start_cb()
{
gat_state = GAT_MODE_STARTING;
}
void gat_i2c_init()
@ -43,11 +107,16 @@ void gat_i2c_init()
// roughly guess bit time length for expected speed
gat_slave.bit_time = GetSysClock() / GAT_I2C_SPEED / 4;
// set up callbacks
gat_slave.start_cb = gat_i2c_start_cb;
gat_slave.rd_cb = gat_i2c_read_cb;
gat_slave.wr_cb = gat_i2c_write_cb;
// enable interrupt on SDA pin to detect start condition
// this assumes GPIO has already been configured
}
void gat_i2c_irq_cb()
{
i2css_blocking(&gat_slave);
i2css_start(&gat_slave);
}