NUCLEO F401RE的HAL_I2C_Mem_Read_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实现自动收发,所以想尝试这个方式。想请教两个问题:
- 分开用Transmit和Receive,还是用
HAL_I2C_Mem_Read_DMA更好?原因是什么? - 有没有方法一次性获取三轴数据?查 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轴数据,无需分三次调用:
实现步骤
- 初始化传感器时,确保CTRL_REG1的
auto_inc位被置1(检查你的main代码中发送的CTRL_REG1配置值)。 - 简化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

