본문 바로가기

Firmware/stm32

STM32 SPI Slave로 Interrupt DMA Transmit/Receive 구현하기

STM32cubeMX : 5.6.1
STM32cubeIDE : 1.3.1

Code Lib Version: STM32Cube_FW_L1_V1.9.0

MCU:STM32L151VET

 

SPI는 양방향 Serial 통신으로 Master or Slave로 동작을 할 수 있다.

 

이번에 Slave로 구현을 해보자

 

CubeMX 설정

Mode를 Slave로 설정

Hardware NSS Signal은 Hardware NSS Input Signal로 설정한다.

 

다음은 DMA 및 Interrupt를 설정한다.

 

DMA란 Direct Memory Acess로 DMA를 OFF 할 경우 MCU에서 SPI통신을 할 때 다 Adminstrate를 해 줘야 하지만 

 

DMA로 연결해 놓을 경우 Hardware 단에서 관리해 하므로 Application 단에는 SPI 통신을 통해 데이터를 써야 할 경우 또는 데이터를 읽어야 할 경우 DMA 에 써 놓기만 하고 할거 하면 된다.

 

용도에 따라 다르겠지만 경험상 DMA로 쓸수 있으면 쓰는게  훨씬 퀘적(?) 하다.

 

 

DMA Setting

DMA Setting 중 가장 중요한 Setting은 Mode이다

 

Mode 설정에는 Normal 과 Circular 이 있는데 만들고자 하는 Application 특성에 따라 선택해야 한다.

 

이렇게 설정 후 Code Generate를 하게되면 DMA initialize까지 있고  SPI_TX, SPI_RX DMA를 Enable 해주는 코드는 없다

 

따라서 다음과 같이 코드를 추가 해야한다.

 

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 */
  SPI_MISO_EN(APP_HIGH);
  SPI_MOSI_EN(APP_HIGH);
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  /* USER CODE BEGIN 2 */
  
  HAL_SPI_Transmit_DMA(&hspi1, tx_buf, size); //Transmit DMA Enable 코드 추가
  HAL_SPI_Receive_DMA(&hspi1, rx_buf, size);  //Receive DMA Enable  코드 추가
  
  }

이렇게  HAL_SPI_Transmit_DMA, HAL_SPI_Receive_DMA를 추가해서 잘 동작하면 좋겠지만 

 

DMA Enable 함수를 연속으로 호출하게 되면 함수 버그로 인해 아래 DMA가 Enable 되지 않는다.

 

HAL_SPI_Transmit_DMA(&hspi1, tx_buf, size);  //DMA Enable

HAL_SPI_Receive_DMA(&hspi1, rx_buf, size);  //Bug로 인해 DMA 가 Enable 되지 않는다. 

 

원인은 처음 DMA를 Enable 한 후 SPI State를 BUSY로 하고 빠져나와서 발생한 문제이다 BUSY 상태로 다음 DMA를 Enable 시도 하여, Enable 하지 못하게 된다.

 

따라서 DMA를 Enable 한 후 STATE 상태를 READY로 바꿀 수 있게 DMA_STATE_CHANGE 함수를 만든 후 DMA 사이에 넣어 준다.

  HAL_SPI_Transmit_DMA(&hspi1, tx_buf, size);  //HAL_SPI_STATE_BUSY_TX
  HAL_DMA_STATE_CHANGE(&hspi1); //HAL_SPI_STATE_BUSY_TX -> HAL_SPI_STATE_READY
  HAL_SPI_Receive_DMA(&hspi1, rx_buf, size); //HAL_SPI_STATE_BUSY_RX
  HAL_DMA_STATE_CHANGE(&hspi1); //HAL_SPI_STATE_BUSY_RX -> HAL_SPI_STATE_READY


}

/**@brief Change SPI State to READY
*/
void HAL_DMA_STATE_CHANGE(SPI_HandleTypeDef *hspi)
{
  hspi->State = HAL_SPI_STATE_READY;
}

 

DMA 설정

Normal Mode

말 그대로 일반적으로 쓰이는 모드이다

만약 데이터 Transmit or receive 할 Buffer 크기를 계속 가변 해야 한다면 Normal Mode로 동작 해야한다.

DMA를 동작 시키고 나면 DMA가 꺼지므로 다시 Enable 해줘야 한다. 

다시 ENABLE 할 때 데이터 크기, 받고자 하는 버퍼를 정할 수 있다.

    

 

Circular Mode

Circular Mode는 연속으로 데이터를 처리 할 때 사용하는 Mode이다.

항상 Enable 되어 있고 Ring Buffer를 통해 데이터를 Receive/Transmit 하므로 Receive Pointer, Transmit Pointer가 있다.

 

 

처음 개발 할때 매번 DMA를 켜야할 경우를 피하기위해 Circular Mode를 사용하였다.

 

하지만 Circular Mode를 사용할 경우 Master에서 보내는 데이터 갯수가 바뀌는데 이때 Circular Mode를 사용하면 

문제가 생긴다.

 

DMA Circular Buffer 구조

DMA Circular Buffer 구조는 Ring Buffer 처럼 움직이는데 Master 에서 보내는 SPI 데이터

개수가 변경되다 보니 변경 될때 마다 Deinit -> init 형식으로 바꿔 줘야한다.

 

ex)  Deinit 후 다시 Init 하는 코드

void reInit_DMA() // Circular Mode에서 SPI Size 변경을 하기 위해서는 Deinit -> init 필요
{
    HAL_DMA_DeInit(&hdma_spi1_rx);
    HAL_DMA_DeInit(&hdma_spi1_tx);
    HAL_DMA_Init(&hdma_spi1_rx);
    HAL_DMA_Init(&hdma_spi1_tx);
    
    HAL_SPI_Transmit_DMA(&hspi1, tx_buf, size);  
    HAL_DMA_STATE_CHANGE(&hspi1);
    HAL_SPI_Receive_DMA(&hspi1, rx_buf, size);
    HAL_DMA_STATE_CHANGE(&hspi1);
    
  }
    

 

그런데 Deinit 후 다시 init 하게 되면 앞에 1Byte가 밀려서 들어오게 된다.

 

ex) Master에서 Transmit Byte -> 0x1 0x2 0x3 0x4 0x5

     Slave 에서  Receive  Byte -> 0x5 0x1 0x2 0x3 0x4

 

원인은 Transmit Pointer 와 Receive Pointer가 초기화 하면서 1Byte 씩 오차가 생겨 발생하는 것으로 보인다.

-> 문제를 해결 하기 위해 Circular Mode 에서 Normal Mode로 변경하였다. 

 

 

코드에서 HAL_SPI_Receive_DMA(&hspi1, rx_buf, size) 로 선언 했으므로 CallBack 함수를 통해 rx_buf를 확인 하면  

데이터가 정상적으로 들어 왔는지 확인 할 수 있다.

 

/** SPI Receive 가 완료 되면 CallBack 함수 호출 된다
    spi는 하나만 쓰므로 따로 구별하는 코드를 넣진 않음
  */
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
  spi_title = rx_buf[0];

}



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 */
  SPI_MISO_EN(APP_HIGH);
  SPI_MOSI_EN(APP_HIGH);
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  /* USER CODE BEGIN 2 */
  
  HAL_SPI_Transmit_DMA(&hspi1, tx_buf, size);
  HAL_DMA_STATE_CHANGE(&hspi1);
  HAL_SPI_Receive_DMA(&hspi1, rx_buf, size);
  HAL_DMA_STATE_CHANGE(&hspi1);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    if (spi_title == 0xf1)
    {
      test_val = ++test;
      tx_buf[0] = test_val;
      tx_buf[1] = test_val;
      tx_buf[2] = test_val;
      tx_buf[3] = test_val;
      tx_buf[4] = get_checksum(tx_buf, 4);
      spi_title = -1;  //data dummy receive = 0
      response_ack(0, 5);  //Response
    }
  }

 

 

 

 

 

번외) CPU에서 DMA 에서 받은 데이터가 깨진 경우 다음과 같은 문제 일 수도 있다.

 

 

MIPS에서는 다음과 같이 해결한다.