为什么STM32 I2C从设备会返回不想要的NACK或无限制的时钟延长?

4
我正在尝试将STM32作为I2C从设备编写,具有以下简单接口: enter image description here 因此,主设备每次都会发送一个注册地址,然后从该寄存器地址中读取或写入数据。
从设备需要始终接收一个字节的注册地址,然后,如果下一个操作是读取,则将该寄存器中的信息发送回主设备,如果主设备的下一个操作是另一个写入,则覆盖该寄存器。
但是,当我运行我的代码时,有些NACKS,其中应该是ACKS。
当主设备请求缓冲区时,以下是响应: 您可以在从设备完成发送最后一个字节后看到NACK 这有点麻烦,但主设备可以正常接收数据,所以我可以接受这种情况。 enter image description here 然而,当我尝试向从设备的寄存器写入数据时,出现以下问题:从设备接收到寄存器地址,然后接收到1个字节和ack,但在接收到第二个字节后,由于某种原因它就一直卡住了(我需要在这里使用时钟拉伸)。这不好,从设备不仅没有接收到所有数据,还锁定了线路以阻止进一步通信。为什么会出现这种情况?我已经思考了几个月了。 enter image description here 这是主设备代码,仅供参考(在简单的Arduino上运行),因为重点真正在于STM32从设备代码。
#include <Wire.h>

uint16_t read_register(int devAddr, unsigned char regAddr, unsigned char bytes, unsigned char * buffer){
  unsigned char i = 0;
  Wire.beginTransmission(devAddr);
  Wire.write(regAddr);
  Wire.endTransmission(false);
  Wire.requestFrom(devAddr, bytes , true);
  while(Wire.available()){
    buffer[i] = Wire.read();
    i++;
  }
  return true;
}

uint16_t write_register(int devAddr, unsigned char regAddr, unsigned char bytes, unsigned char * buffer){
  unsigned char i = 0;
  Wire.beginTransmission(devAddr);
  Wire.write(regAddr); // Reg to write
  for(i = 0; i < bytes; i++){
    Wire.write(buffer[i]);
  }
  Wire.endTransmission(true);
  return true;
}

void setup()
{
  Wire.begin();
  Wire.setClock(400);
  Serial.begin(9600);
  while (!Serial);             // Leonardo: wait for serial monitor
  Serial.println("Starting");
}


void loop()
{
  unsigned char buffSize = 4;
  unsigned char readBuff[buffSize];
  unsigned char writeBuff[5] = {0xFB, 0xE3, 0XE2, 0xE1, 0xE0};


  for (int i = 0; i < buffSize; i++) readBuff[i] = 0;
  read_register(0x1F, 251, buffSize, readBuff);
  Serial.print(readBuff[3], HEX);
  Serial.print(readBuff[2], HEX);
  Serial.print(readBuff[1], HEX);
  Serial.println(readBuff[0], HEX);
  
  write_register(0x1F, 0xFB, 5, writeBuff);
 
  delay(2000);
}

以下是STM32从设备的I2C代码部分:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    i2c.c
  * @brief   This file provides code for the configuration
  *          of the I2C instances.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "i2c.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

I2C_HandleTypeDef hi2c1;

/* I2C1 init function */
void MX_I2C1_Init(void)
{

  /* USER CODE BEGIN I2C1_Init 0 */
  // Get I2C address code from hardware jumpers
  // Address starts at I2C_ADDRESS_BASE and is offset by value read on jumpers array
  uint8_t I2C_Address = 0x0;
  I2C_Address = (I2C_ADDRESS_BASE + (
          (HAL_GPIO_ReadPin(AD0_GPIO_Port, AD0_Pin) << 0)|
          (HAL_GPIO_ReadPin(AD1_GPIO_Port, AD1_Pin) << 1)|
          (HAL_GPIO_ReadPin(AD2_GPIO_Port, AD2_Pin) << 2)|
          (HAL_GPIO_ReadPin(AD3_GPIO_Port, AD3_Pin) << 3)
  )) << 1;
  /* USER CODE END I2C1_Init 0 */

  /* USER CODE BEGIN I2C1_Init 1 */

  /* USER CODE END I2C1_Init 1 */
  hi2c1.Instance = I2C1;
  hi2c1.Init.Timing = 0x0000020B;
  hi2c1.Init.OwnAddress1 = I2C_Address;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_ENABLE;
  hi2c1.Init.OwnAddress2 = (I2C_ADDRESS_BASE + 16) << 1;
  hi2c1.Init.OwnAddress2Masks = I2C_OA2_NOMASK;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Analogue filter
  */
  if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Digital filter
  */
  if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2C1_Init 2 */

  /* USER CODE END I2C1_Init 2 */

}

void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(i2cHandle->Instance==I2C1)
  {
  /* USER CODE BEGIN I2C1_MspInit 0 */

  /* USER CODE END I2C1_MspInit 0 */

    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**I2C1 GPIO Configuration
    PB6     ------> I2C1_SCL
    PB7     ------> I2C1_SDA
    */
    GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* I2C1 clock enable */
    __HAL_RCC_I2C1_CLK_ENABLE();

    /* I2C1 interrupt Init */
    HAL_NVIC_SetPriority(I2C1_EV_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
  /* USER CODE BEGIN I2C1_MspInit 1 */

  /* USER CODE END I2C1_MspInit 1 */
  }
}

void HAL_I2C_MspDeInit(I2C_HandleTypeDef* i2cHandle)
{

  if(i2cHandle->Instance==I2C1)
  {
  /* USER CODE BEGIN I2C1_MspDeInit 0 */

  /* USER CODE END I2C1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_I2C1_CLK_DISABLE();

    /**I2C1 GPIO Configuration
    PB6     ------> I2C1_SCL
    PB7     ------> I2C1_SDA
    */
    HAL_GPIO_DeInit(GPIOB, GPIO_PIN_6);

    HAL_GPIO_DeInit(GPIOB, GPIO_PIN_7);

    /* I2C1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(I2C1_EV_IRQn);
    HAL_NVIC_DisableIRQ(I2C1_ER_IRQn);
  /* USER CODE BEGIN I2C1_MspDeInit 1 */

  /* USER CODE END I2C1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */

#define I2C_BUFFER_SIZE 8
uint8_t i2c_buffer[I2C_BUFFER_SIZE];
uint8_t reg_addr_rcvd = 0;
#define I2C_REG_ADD_SIZE        1
#define I2C_PAYLOAD_SIZE        4

extern void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode){
    UNUSED(AddrMatchCode);
    // If is master write, listen to necessary amount of bytes
    if(TransferDirection == I2C_DIRECTION_TRANSMIT){
        // First write request is always 1 byte of the requested reg address
        // Will saved it on the first position of I2C_buffer
        if(!reg_addr_rcvd){
            HAL_I2C_Slave_Sequential_Receive_IT(hi2c, (void*)i2c_buffer, I2C_REG_ADD_SIZE, I2C_FIRST_FRAME);

        } else {
            // If a subsequent write request is sent, will receve 4 bytes from master
            // Save it on the rest of the buffer
            HAL_I2C_Slave_Sequential_Receive_IT(hi2c, (void*)i2c_buffer, I2C_PAYLOAD_SIZE, I2C_NEXT_FRAME);
        }
    }
    else {
        // If a read request is sent by the master, return the value of the data in the requested register that was saved on 1st
        // position of the I2C buffer
        HAL_I2C_Slave_Sequential_Transmit_IT(hi2c, data_register[i2c_buffer[0]].mem_addr, data_register[i2c_buffer[0]].len, I2C_LAST_FRAME);
    }
    // Read address + data size. If it is a read command, data size will be zero
}


extern void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c){
    // This is called after a master 'write' request. first time around it will be a register.
    // Second time if its a write to register request, it will be a payload
    if(!reg_addr_rcvd){
        // If reg_addr_rcvd is false, means that it received a register
        reg_addr_rcvd = 1;
    } else {
        // If reg_addr_rcvd is set, means that this callback was returned after the payload data has been received
        reg_addr_rcvd = 0;
    }
    HAL_I2C_EnableListen_IT(hi2c);
    HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
}

extern void HAL_I2C_ListenCpltCallback (I2C_HandleTypeDef *hi2c){
    HAL_I2C_EnableListen_IT(hi2c);
    HAL_GPIO_TogglePin(LED_B_GPIO_Port, LED_B_Pin);
}

extern void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c){
    // Reset reg_addr_rcvd after finish sending requested register
    reg_addr_rcvd = 0;
    HAL_I2C_EnableListen_IT(hi2c);
}


extern void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    HAL_GPIO_TogglePin(LED_R_GPIO_Port, LED_R_Pin);
    //HAL_I2C_ERROR_NONE       0x00000000U    /*!< No error           */
    //HAL_I2C_ERROR_BERR       0x00000001U    /*!< BERR error         */
    //HAL_I2C_ERROR_ARLO       0x00000002U    /*!< ARLO error         */
    //HAL_I2C_ERROR_AF         0x00000004U    /*!< Ack Failure error  */
    //HAL_I2C_ERROR_OVR        0x00000008U    /*!< OVR error          */
    //HAL_I2C_ERROR_DMA        0x00000010U    /*!< DMA transfer error */
    //HAL_I2C_ERROR_TIMEOUT    0x00000020U    /*!< Timeout Error      */
    uint32_t error_code = HAL_I2C_GetError(hi2c);
    if (error_code != HAL_I2C_ERROR_AF){}
    HAL_I2C_EnableListen_IT(hi2c);
}
/* USER CODE END 1 */

以下是I2C从设备的cubeMX配置: 在此输入图像描述 在此输入图像描述 在此输入图像描述 非常感谢您提供的任何见解。 谢谢!
2个回答

4
您提出了两个问题,以下是针对第一个问题的回答:为什么在读取4个数据字节后会收到NACK?
这是完全正确和预期的行为,而且实施者是Arduino而不是STM32。
简要解释一下:在每个字节后的ack永远是没有发送该字节的一方负责生成。当主设备向从设备写入地址或数据时,如果从设备接收并准备好开始接收下一个字节,则从设备会生成一个ack。
当主设备从从设备读取数据时,从设备(即STM32)发送数据字节,此时主设备(即Arduino)有责任选择是发送ack还是nack。如果主设备发送ack,则表示“我已经接收到这个字节,请准备发送下一个字节”。如果主设备发送nack,则表示“我已经完成了从你那里获取数据”。
在I2C中,开始读取而不知道需要多少字节是完全合法的。在这种情况下,您将对每个字节进行ack,然后在读取足够的字节后发送停止条件。但在您的情况下,您事先告诉了Arduino它应该读取4个字节,因此它会ack前三个字节并nack第四个字节。
在某些情况下,这种行为可以节省从设备端的资源,因为从设备立即知道它不必准备第五个字节。

3
您提出了两个问题。这是对第二个问题的回答,即为什么STM32从设备不能ack更多写入的字节而是拉伸时钟?
在您的ADDR中断函数中,如果没有收到寄存器地址(reg_addr_rcvd为false),则启动接收一个字节。主设备(Arduino)发送此一个字节,并且可能会发生接收完成回调。
如果此时Arduino发送重新启动或停止-启动并再次发送从设备地址,则ADDR中断将再次发生,并在找到reg_addr_rcvd为true时开始接收4个字节,它们将全部被ack。
但是,Arduino不会发送重新启动,而是直接在寄存器地址后传输数据。这对于主设备来说非常正常和合理。您需要正确地处理两种情况。可能意味着一旦接收到寄存器地址,就在接收完成中断中启动数据接收。如果您不启动接收,则I2C外设将只拉伸时钟,因为它无处放置已缓冲的数据。
为编写健壮的生产质量软件,您还需要处理各种其他组合。例如:如果主设备发送超过4个数据字节,则会再次得到无限延长。如果主设备在少于4个字节后发送停止,则需要能够中止接收并返回到侦听等待状态。

开始在接收完成中断中接收数据解决了问题。现在读写都按预期工作。 感谢您准确的答案,比我读过的许多关于I2C的文章更有帮助,它们通常完全忽略从设备的行为。 正如您所提到的,我仍然需要处理边缘情况,可能会有后续问题,但现在,如果有人想要检查结果,我想在此处留下结果 https://github.com/wandrade/GISMo/blob/develop/Firmware/Core/Src/i2c.c - Werner Thomassen Andrade
@WernerThomassenAndrade:没问题!您能否在左侧用勾号标记此问题已解决。 - Tom V

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接