You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NUCLEO F401RE的HAL_I2C_Mem_Read_DMA多读取问题及优化咨询

基于STM32和LIS2DE加速度传感器的DMA读取问题

我正在做一个基于STM32和LIS2DE加速度传感器的项目,需求是每秒读取传感器寄存器数据并通过UART发送到Arduino IDE。但现在遇到问题:连续三次调用HAL_I2C_Mem_Read_DMA时,后两次无法正常工作。删除第一次调用后,只有第二次能运行,检查返回值是HAL_BUSY。

目前HAL_I2C_Master_Transmit_DMA和HAL_I2C_Master_Receive_DMA能正常工作,但教授建议用HAL_I2C_Mem_Read_DMA实现自动收发,所以想尝试这个方式。想请教两个问题:

  1. 分开用Transmit和Receive,还是用HAL_I2C_Mem_Read_DMA更好?原因是什么?
  2. 有没有方法一次性获取三轴数据?查 datasheet 看到了自动递增功能,但没找到示例。

已配置1秒定时器,开启了UART_TX、I2C_TX、I2C_RX的DMA,以及UART、TIM、I2C的事件中断,相关代码如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){
    HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_X_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)&acc[0], 1);
    HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_Y_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)&acc[1], 1);
    HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_Z_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)&acc[2], 1);
}

void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef* hi2c){
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        if(count==2){    // 每次Mem_Read触发一次中断,第三次回调后通过UART发送数据
        count = 0;

        len = snprintf(string, sizeof(string), "X: %+.3fg   Y: %+.3fg   Z: %+.3fg\n",
                acc[0] / 256.0 * 4.0,
                acc[1] / 256.0 * 4.0,
                acc[2] / 256.0 * 4.0);
        HAL_UART_Transmit_DMA(&huart2, (unsigned char*)string, len);
         }
         else
                count++;
}

更新1

现在我改成了在TIM中断启动第一次读取,后续读取由前一次的完成回调触发,最后一次触发打印,用全局变量跟踪步骤。这个方案能工作,但感觉不够简洁,不过必须使用带DMA的I2C。代码如下:

#if VERSION == 2
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){
    HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_X_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(acc+count), 1);
    count++;
}

void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef* hi2c){
    switch(count){
        case 1:
            HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_Y_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(acc+count), 1);
            count++;
            break;
        case 2:
            HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_Z_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(acc+count), 1);
            count++;
            break;
        case 3:
            count=0;

            len = snprintf(string, sizeof(string), "X: %+.3fg   Y: %+.3fg   Z: %+.3fg, (Tot:%+.3fg | %+.3fm/s²)\n",
                    acc[0] / 256.0 * 4.0,
                    acc[1] / 256.0 * 4.0,
                    acc[2] / 256.0 * 4.0,
                    sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0,
                    sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0 * 9.80665);
            HAL_UART_Transmit_DMA(&huart2, (unsigned char*)string, len);
    }
}
#endif

更新2

以下是main函数,其他代码为自动生成,加上了建议的回调函数:

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART2_UART_Init();
  MX_I2C1_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */

  if (HAL_I2C_Master_Transmit_DMA(&hi2c1, ACCEL_ADD, (uint8_t*)CTRL_REG1, sizeof(CTRL_REG1)) == HAL_OK)
      len = snprintf(string, sizeof(string), "LIS2DE found!\n");
  else{
      ACCEL_ADD = LIS2DE12_ADD;
      if (HAL_I2C_Master_Transmit_DMA(&hi2c1, ACCEL_ADD, (uint8_t*)CTRL_REG1, sizeof(CTRL_REG1)) == HAL_OK)
          len = snprintf(string, sizeof(string), "LIS2DE12 found!\n");
      else
          len = snprintf(string, sizeof(string), "Accelerator error!\n");
  }
  HAL_UART_Transmit_DMA(&huart2, (unsigned char*)string, len);

  HAL_I2C_Master_Transmit_DMA(&hi2c1, ACCEL_ADD, (uint8_t*)CTRL_REG2, sizeof(CTRL_REG2));
  HAL_I2C_Master_Transmit_DMA(&hi2c1, ACCEL_ADD, (uint8_t*)CTRL_REG4, sizeof(CTRL_REG4));

  TIM2->SR &= ~TIM_SR_UIF;          // or __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE); or HAL_Delay(500);
  HAL_TIM_Base_Start_IT(&htim2);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
#if VERSION == 3
      while (!timer_elapsed){/*do nothing*/}

      for (int i=0; i<3; i++){
          dma_complete = false;
          HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, regs[i], I2C_MEMADD_SIZE_8BIT, (uint8_t*)(acc+i), 1);

          while (!dma_complete){/*do nothing*/}
      }

      len = snprintf(string, sizeof(string), "X: %+.3fg   Y: %+.3fg   Z: %+.3fg (Tot:%+.3fg | %+.3fm/s²)\n",
              acc[0] / 256.0 * 4.0,
              acc[1] / 256.0 * 4.0,
              acc[2] / 256.0 * 4.0,
              sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0,
              sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0 * 9.80665);
      HAL_UART_Transmit_DMA(&huart2, (unsigned char*)string, len);

      timer_elapsed = false;
#endif
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

解答

1. HAL_I2C_Mem_Read_DMA vs 分开使用Transmit+Receive

HAL_I2C_Mem_Read_DMA是STM32 HAL库封装好的“先发送寄存器地址,再接收数据”的组合操作,内部已经处理了I2C传输的起始、停止等时序,比手动分开调用HAL_I2C_Master_Transmit_DMA和HAL_I2C_Master_Receive_DMA更简洁,也减少了手动维护传输状态的出错概率。

关键注意点:DMA模式下,同一I2C外设的DMA操作不能并发执行——这就是你一开始连续三次调用HAL_I2C_Mem_Read_DMA返回HAL_BUSY的原因:第一次调用还在DMA传输中,外设处于忙状态,后续调用自然失败。你的更新1方案(回调链式触发下一次读取)是正确的处理方式,这是DMA异步操作的标准流程,虽然代码量稍多,但逻辑严谨。

如果用分开的Transmit+Receive,你同样需要处理两次DMA操作的衔接(Transmit完成后再触发Receive),代码复杂度更高,所以优先选HAL_I2C_Mem_Read_DMA更符合教授的建议,也更合理。

2. 一次性读取三轴数据(利用自动递增功能)

LIS2DE的自动递增功能(CTRL_REG1的第7位auto_inc,置1后读取寄存器时地址会自动+1)开启后,你可以一次性读取3个字节的X、Y、Z轴数据,无需分三次调用:

实现步骤

  1. 初始化传感器时,确保CTRL_REG1的auto_inc位被置1(检查你的main代码中发送的CTRL_REG1配置值)。
  2. 简化TIM中断和回调函数:
// TIM中断回调中只需一行代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){
    HAL_I2C_Mem_Read_DMA(&hi2c1, ACCEL_ADD, OUT_X_ADD, I2C_MEMADD_SIZE_8BIT, (uint8_t*)acc, 3);
}

// 接收完成回调直接处理数据发送
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef* hi2c){
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    len = snprintf(string, sizeof(string), "X: %+.3fg   Y: %+.3fg   Z: %+.3fg, (Tot:%+.3fg | %+.3fm/s²)\n",
            acc[0] / 256.0 * 4.0,
            acc[1] / 256.0 * 4.0,
            acc[2] / 256.0 * 4.0,
            sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0,
            sqrt(pow(acc[0],2)+pow(acc[1],2)+pow(acc[2],2))/64.0 * 9.80665);
    HAL_UART_Transmit_DMA(&huart2, (unsigned char*)string, len);
}

这样既实现了单次DMA读取三轴数据,代码更简洁,还减少了I2C总线的起始/停止次数,传输效率更高。

3. 关于你的更新1方案

这个方案是可行的,是处理多步DMA异步操作的常规手段,但利用自动递增功能后可以进一步简化,不需要维护count变量和链式回调,代码逻辑更清晰。

4. 额外注意事项

  • 确保传感器的自动递增位配置正确,否则一次性读取3个字节会得到重复的同一寄存器数据。
  • DMA传输的目标缓冲区(acc数组)要保证在传输过程中不被修改,建议定义为全局变量或静态变量。
  • 使用UART_DMA发送时,要确保上一次UART传输完成后再发送新数据,避免HAL_BUSY错误(可以在HAL_UART_TxCpltCallback中设置标志位,或者检查huart2.State是否为HAL_UART_STATE_READY)。

内容的提问来源于stack exchange,提问作者Rob99

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 13:28:10