2025-02-14 07:12 AM
As mentioned in the topic subject, I would like to implement I2C Slave device which will simulate the BQ769x2 device. For development I'm using STM32F4 Discovery board, but the final product will be deployed on STM32L0.
Master device is first trying to detect if slave device is present and after that some functions will be called in no exact order nor the functions have same length. Therefore I was thinking interrupt mode would be appropriate to implement here for greater control.
For testing purpose I have written small Master device program for Arduino (running on Arduino MKRZERO).
With this example one can send scan or probe command over serial to run the procedure.
#include <Wire.h>
#include <stdarg.h>
#define PRINTF_BUF 80 // define the tmp buffer size (change if desired)
String inputData;
void setup() {
Wire.begin();
Wire.setClock(100000);
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(300 * 1000 /* us */, true /* reset_on_timeout */);
#else
Wire.setTimeout(500);
#endif
Serial.begin(115200);
while(!Serial)
delay(10);
Serial.println("\nI2CScanner started");
}
void loop() {
// wait for user input
while(Serial.available() == 0){}
inputData = Serial.readString();
inputData.trim();
if(inputData.equals("scan"))
{
Serial.println(inputData);
i2cdetect();
}
else if(inputData.startsWith("probe"))
{
uint8_t err = probe(0x48);
if(!err)
{
Serial.println("No error...");
}
else
{
Serial.print("\nError probing with error: ");
Serial.println(err);
}
}
}
uint8_t probe(uint8_t address)
{
Wire.beginTransmission(address);
Wire.write(0x3E);
Wire.write(0x01);
Wire.write(0x00);
uint8_t err = Wire.endTransmission(1);
return err;
}
void i2cdetect()
{
uint8_t first = 1,
last = 127;
uint8_t i, j, address, error;
// header
Serial.print(" ");
for (i = 0; i < 16; i++) {
printf("%3x", i);
}
Serial.println("");
// body
for (j = 0; j < 8; j++) {
_printf("%02d:", j*10);
for (i = 0; i < 16; i++) {
address = i + j*16;
if (address >= first && address <= last) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error) {
Serial.print(" --");
} else {
_printf(" %02x", address);
}
}
else {
Serial.print(" ");
}
}
Serial.println("");
}
}
void _printf(const char *format, ...)
{
char buf[PRINTF_BUF];
va_list ap;
va_start(ap, format);
vsnprintf(buf, sizeof(buf), format, ap);
for(char *p = &buf[0]; *p; p++) // emulate cooked mode for newlines
{
if(*p == '\n')
Serial.write('\r');
Serial.write(*p);
}
va_end(ap);
}
This is (with different address) what Master should create:
As you can see, here the example shows clock stretching - I have to work without this functionality.
The problem is, that my STM32 program is not working properly. The code for I2C slave device is provided below.
If HAL_Delay is called, the ACK is sent, but HAL_I2C_ListenCpltCallback is not called. If delay is not present the NACK is generated and Master can not proceed as desired.
This is what I can see on my oscilloscope after probe procedure is called on the master in case of no delay in the STM32 code and bellow in case when delay is present in STM32 code.
But as said, if ACK is sent STM does not call HAL_I2C_ListenCpltCallback (as it should?) and in case of NACK master can't go on (in real life situation).
Here are parts of my STM32F4 code:
/*
* STM32_I2C_Slave.c
*/
/* include */
#include "I2C_Slave.h"
#include "registers.h"
#include "protocol.h"
#include "stdio.h"
#include "string.h"
volatile i2c_t I2C_slave_obj;
uint8_t buff[2] = {0x00, 0x00};
uint32_t maxBusyDelay = 150;
uint32_t maxCMDDelay = 500; // delay in MS
uint32_t lastAddrCallback = 0;
uint8_t readRegAddress = 1;
uint8_t allowTX = 0;
uint8_t indexRA = 0;
void i2c_slave_clear(void){
I2C_slave_obj.reg_address = 0;
I2C_slave_obj.reg_addr_rcvd = 0;
I2C_slave_obj.payload = 0;
I2C_slave_obj.curr_idx = NONE;
}
void i2c_slave_init(I2C_HandleTypeDef *hi2c)
{
I2C_slave_obj.i2c_handler = hi2c;
i2c_slave_clear();
HAL_I2C_EnableListen_IT(hi2c);
}
void i2c_slave_check_timeout(void)
{
static int rx_busy_counter = 0;
HAL_I2C_StateTypeDef status = HAL_OK;
status = HAL_I2C_GetState(I2C_slave_obj.i2c_handler);
if (status == HAL_I2C_STATE_BUSY_RX_LISTEN) rx_busy_counter++;
else rx_busy_counter = 0;
if (rx_busy_counter > 150)
{
HAL_I2C_DisableListen_IT(I2C_slave_obj.i2c_handler);
HAL_I2C_DeInit(I2C_slave_obj.i2c_handler);
HAL_I2C_Init(I2C_slave_obj.i2c_handler);
HAL_I2C_EnableListen_IT(I2C_slave_obj.i2c_handler);
rx_busy_counter = 0;
}
if(lastAddrCallback + maxCMDDelay > HAL_GetTick())
{
readRegAddress = 0;
lastAddrCallback = 0;
HAL_GPIO_WritePin(LD5_GPIO_Port, LD5_Pin, GPIO_PIN_RESET);
}
else
{
readRegAddress = 1;
}
}
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode){
if(AddrMatchCode == I2C_slave_obj.i2c_handler->Init.OwnAddress1)
lastAddrCallback = HAL_GetTick();
if(TransferDirection == I2C_DIRECTION_TRANSMIT)
{
// MASTER / Controller wants to write
// can write CMD or just I2CDetect probing
if(readRegAddress)
{
//HAL_I2C_Slave_Receive_IT(hi2c, (uint8_t*)&I2C_slave_obj.reg_addr_rcvd, 3);
if(HAL_I2C_Slave_Seq_Receive_IT(hi2c, (uint8_t*)&I2C_slave_obj.reg_addr_rcvd, 1, I2C_FIRST_FRAME) != HAL_OK)
{
HAL_I2C_ErrorCallback(hi2c);
}
else
{
HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
}
}
}
else
{
// MASTER / Controller wants to read
// we will need to send appropriate data to MASTER / Controller
HAL_I2C_Slave_Seq_Receive_IT(hi2c, buff, 2, I2C_LAST_FRAME);
HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
}
HAL_I2C_EnableListen_IT(hi2c);
}
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c){
if(!I2C_slave_obj.reg_addr_rcvd)
{
// If reg_addr_rcvd = 0 its probably I2CDetect probing
readRegAddress = 1;
}
else
{
switch(I2C_slave_obj.reg_addr_rcvd)
{
case 0x3E:
//HAL_I2C_Slave_Receive_IT(hi2c, &buff[0], 2);
if(HAL_I2C_Slave_Seq_Receive_IT(hi2c, &buff[indexRA], 1, (!indexRA) ? I2C_NEXT_FRAME : I2C_LAST_FRAME) != HAL_OK)
{
HAL_I2C_ErrorCallback(hi2c);
}
else
{
indexRA++;
if(indexRA==2)
{
HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
}
HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
// HAL_Delay(20);
// this delay allows to make ACK ?... but ListenCpltCallback still not called
}
break;
default:
readRegAddress = 1;
indexRA = 0;
lastAddrCallback = 0;
}
}
if(indexRA == 2)
{
readRegAddress = 1;
indexRA = 0;
lastAddrCallback = 0;
}
HAL_I2C_EnableListen_IT(hi2c);
}
void HAL_I2C_AbortCpltCallback (I2C_HandleTypeDef *hi2c){
HAL_I2C_EnableListen_IT(hi2c);
}
void HAL_I2C_ListenCpltCallback (I2C_HandleTypeDef *hi2c)
{
HAL_GPIO_TogglePin(LD6_GPIO_Port, LD6_Pin);
HAL_GPIO_WritePin(LD3_GPIO_Port, LD3_Pin, GPIO_PIN_RESET);
HAL_I2C_EnableListen_IT(hi2c);
}
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c){
I2C_slave_obj.reg_addr_rcvd = 0;
I2C_slave_obj.reg_address = 0;
HAL_I2C_EnableListen_IT(hi2c);
}
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
HAL_GPIO_WritePin(LD5_GPIO_Port, LD5_Pin, GPIO_PIN_SET);
uint32_t error_code = HAL_I2C_GetError(hi2c);
uint32_t BERR = (error_code & HAL_I2C_ERROR_BERR);
uint32_t ARLO = (error_code & HAL_I2C_ERROR_ARLO);
uint32_t AF = (error_code & HAL_I2C_ERROR_AF);
uint32_t OVR = (error_code & HAL_I2C_ERROR_OVR);
uint32_t DMAE = (error_code & HAL_I2C_ERROR_DMA);
uint32_t TOUT = (error_code & HAL_I2C_ERROR_TIMEOUT);
uint32_t SIZE = (error_code & HAL_I2C_ERROR_SIZE);
uint32_t DMA = (error_code & HAL_I2C_ERROR_DMA_PARAM);
uint32_t STR = (error_code & HAL_I2C_WRONG_START);
if (BERR)
{
HAL_GPIO_TogglePin(LD5_GPIO_Port, LD5_Pin);
HAL_I2C_DisableListen_IT(hi2c);
HAL_I2C_DeInit(hi2c);
HAL_I2C_Init(hi2c);
HAL_I2C_EnableListen_IT(hi2c);
}
else if (AF)
{
// I2CDetect was called most probably
__HAL_I2C_CLEAR_FLAG(hi2c, I2C_FLAG_AF);
readRegAddress = 1;
lastAddrCallback = 0;
I2C_slave_obj.reg_addr_rcvd = 0;
memset(buff, 0, sizeof(buff));
}
HAL_I2C_EnableListen_IT(hi2c);
}
/////// this is from main.c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2S3_Init();
MX_USB_HOST_Init();
MX_I2C1_Init();
//Init i2c-slave and call HAL_I2C_EnableListen_IT inside
i2c_slave_init(&hi2c1);
while (1)
{
// check for timeouts
i2c_slave_check_timeout();
// all other code for I2C is handled in I2C_Slave
MX_USB_HOST_Process();
}
}
// and init functions for clk and i2c
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
static void MX_I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 144;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_ENABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
}
Looks like I'm handling something wrong or some flags are not cleared as they should be... maybe __disable_irq/__enable_irq at some point... using interrupts only for setting flags and implement logic in main program... I don't know anymore :)
Also I'm not sure if HAL_I2C_EnableListen_IT should be really called in all those places.
I hope I was clear with my question - head is allover the place right now.
Basically - I would like to support i2cdetect (in a way that STM32 will not stuck) and presented command in first picture. Right now - or slave or master don't work as desired.
Thanks for help in advance.
2025-02-14 07:23 AM
Start from an example that works and use that code as a template.
> HAL_I2C_DeInit(hi2c);
> HAL_I2C_Init(hi2c);
It is not good to have stuff like this in your code. If anything, it will silently hide the error happening. Solve the error instead.