From b64b6e6186d0999d3318c0e7999b0e7d00ccc4b2 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Thu, 23 May 2024 23:12:10 -0700 Subject: [PATCH] LPC1768: Rewrite I2CSlave HAL to match datasheet way of doing it (#280) * LPC1768: Rewrite I2CSlave HAL to match datasheet way of doing it * Style and doc fixes --- drivers/include/drivers/I2CSlave.h | 13 +- hal/include/hal/i2c_api.h | 2 +- targets/TARGET_NXP/TARGET_LPC176X/i2c_api.c | 151 ++++++++++++++------ 3 files changed, 122 insertions(+), 44 deletions(-) diff --git a/drivers/include/drivers/I2CSlave.h b/drivers/include/drivers/I2CSlave.h index 1eec996e9b9..b6624210fe7 100644 --- a/drivers/include/drivers/I2CSlave.h +++ b/drivers/include/drivers/I2CSlave.h @@ -165,7 +165,10 @@ class I2CSlave { */ int receive(void); - /** Read bytes transmitted to this MCU from an I2C master. + /** + * @brief Read bytes transmitted to this MCU from an I2C master. + * + * Call this function only once \c receive() returns WriteAddressed or WriteGeneral. * * @param data Pointer to the buffer to read data into. * @param length Maximum number of bytes to read. @@ -180,7 +183,11 @@ class I2CSlave { */ int read(void); - /** Write to an I2C master. + /** + * @brief Write to an I2C master which is addressing this slave device. + * + * Call this function only once \c receive() returns ReadAddressed. + * This will send the given data bytes to the master and then send a NACK to end the read transaction. * * @param data Pointer to the buffer containing the data to be sent. * @param length Number of bytes to send. @@ -204,7 +211,7 @@ class I2CSlave { /** Set the I2C slave address. * - * @param address The address to set for the slave (least significant bit is ignored). + * @param address The 8-bit address to set for the slave (least significant bit is ignored). * * @note If address is set to 0, the slave will only respond to the * general call address. diff --git a/hal/include/hal/i2c_api.h b/hal/include/hal/i2c_api.h index 1ea34ac05d8..ff0a8f6978f 100644 --- a/hal/include/hal/i2c_api.h +++ b/hal/include/hal/i2c_api.h @@ -331,7 +331,7 @@ int i2c_slave_read(i2c_t *obj, char *data, int length); * @param obj The I2C object * @param data The buffer for sending * @param length Number of bytes to write - * @return non-zero if a value is available + * @return number of bytes actually written to the master, or negative value on error. */ int i2c_slave_write(i2c_t *obj, const char *data, int length); diff --git a/targets/TARGET_NXP/TARGET_LPC176X/i2c_api.c b/targets/TARGET_NXP/TARGET_LPC176X/i2c_api.c index 5054ee61eb4..bc2b3241e90 100644 --- a/targets/TARGET_NXP/TARGET_LPC176X/i2c_api.c +++ b/targets/TARGET_NXP/TARGET_LPC176X/i2c_api.c @@ -382,56 +382,127 @@ int i2c_slave_receive(i2c_t *obj) { } int i2c_slave_read(i2c_t *obj, char *data, int length) { + int count = 0; - int status; - - do { - i2c_clear_SI(obj); - i2c_wait_SI(obj); - status = i2c_status(obj); - if((status == 0x80) || (status == 0x90)) { - data[count] = I2C_DAT(obj) & 0xFF; + + if(i2c_status(obj) != 0x60 && i2c_status(obj) != 0x70) { + return -1; // I2C peripheral not in setup-write-to-slave mode + } + + if(i2c_status(obj) == 0x70) { + // When addressed with the general call address, per the manual we can only receive a max of 1 byte. + if(length > 1) { + length = 1; } - count++; - } while (((status == 0x80) || (status == 0x90) || - (status == 0x060) || (status == 0x70)) && (count < length)); - - // Clear old status and wait for Serial Interrupt. - i2c_clear_SI(obj); - i2c_wait_SI(obj); - - // Obtain new status. - status = i2c_status(obj); - - if(status != 0xA0) { - i2c_stop(obj); } - + + i2c_conset(obj, 0, 0, 0, length > 0); // Set AA flag to acknowledge write as long as we have buffer space to store a byte in i2c_clear_SI(obj); - - return count; + + // This is implemented as a state machine according to section 19.10.8 in the reference manual. + while(true) { + + // Wait until the I2C peripheral has an event for us + i2c_wait_SI(obj); + +#if LPC1768_I2C_DEBUG + printf("i2c_slave_read(): in state 0x%hhx\n", i2c_status(obj)); +#endif + + switch(i2c_status(obj)) { + case 0x68: + case 0x78: + // Arbitration Lost event. I'm a bit confused about how this can happen as a slave device but the manual says it can... + i2c_conclr(obj, 1, 0, 0, 1); // Clear start and ack + i2c_clear_SI(obj); + + // Reset the I2C state machine. This doesn't actually send a STOP condition in slave mode. + i2c_stop(obj); + return -2; // Arbitration lost + + case 0x80: + case 0x90: + // Received another data byte from master. ACK has been returned. + data[count++] = I2C_DAT(obj) & 0xFF; + + if(count >= length) { + // Out of buffer space, NACK the next byte + i2c_conclr(obj, 0, 0, 0, 1); + i2c_clear_SI(obj); + } + else { + // Ack the next byte + i2c_conset(obj, 0, 0, 0, 1); + i2c_clear_SI(obj); + } + break; + + case 0x88: + case 0x98: + // Master wrote us a byte and we NACKed it. Slave receive mode has been exited. + i2c_conset(obj, 0, 0, 0, 1); // Set AA flag so that we go back to idle slave state + i2c_clear_SI(obj); + return count; + + case 0xA0: + // Stop condition received. Slave receive mode has been exited. + i2c_conset(obj, 0, 0, 0, 1); // Set AA flag so that we go back to idle slave state + i2c_clear_SI(obj); + return count; + } + } } int i2c_slave_write(i2c_t *obj, const char *data, int length) { - int count = 0; - int status; - - if(length <= 0) { - return(0); - } - - do { - status = i2c_do_write(obj, data[count], 0); - count++; - } while ((count < length) && (status == 0xB8)); - - if ((status != 0xC0) && (status != 0xC8)) { - i2c_stop(obj); + + int next_byte_idx = 0; + + if(i2c_status(obj) != 0xA8) { + return -1; // I2C peripheral not in setup-read-from-slave mode } + // Write first data byte. Note that there's no way for the slave to NACK a read-from-slave event, so if we run out of bytes, + // just send zeroes. + I2C_DAT(obj) = (next_byte_idx < length) ? data[next_byte_idx] : 0; + ++next_byte_idx; + + i2c_conset(obj, 0, 0, 0, 1); // Set AA flag to acknowledge read as long as we have something to transmit i2c_clear_SI(obj); - - return(count); + + // This is implemented as a state machine according to section 19.10.9 in the reference manual. + while(true) { + + // Wait until the I2C peripheral has an event for us + i2c_wait_SI(obj); + + switch(i2c_status(obj)) { + case 0xB0: + // Arbitration Lost event. I'm a bit confused about how this can happen as a slave device but the manual says it can... + i2c_conclr(obj, 1, 0, 0, 1); // Clear start and ack + i2c_clear_SI(obj); + + // Reset the I2C state machine. This doesn't actually send a STOP condition in slave mode. + i2c_stop(obj); + return -2; // Arbitration lost + + case 0xB8: + // Prev data byte has been transmitted. ACK has been received. Write the next data byte. + I2C_DAT(obj) = (next_byte_idx < length) ? data[next_byte_idx] : 0; + ++next_byte_idx; + + i2c_conset(obj, 0, 0, 0, 1); // Set AA flag to acknowledge read as long as we have something to transmit + i2c_clear_SI(obj); + break; + + case 0xC0: + case 0xC8: + // Last data byte transmitted (ended either by us not setting AA, or by the master NACKing) + i2c_conset(obj, 0, 0, 0, 1); // Set AA flag so that we go back to idle slave state + i2c_clear_SI(obj); + + return next_byte_idx; + } + } } void i2c_slave_address(i2c_t *obj, int idx, uint32_t address, uint32_t mask) {