이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.

 

인터넷 상에 STM32를 이용한 시리얼 통신에 관한 정보는 넘쳐날 정도로 많습니다. 그 글들 중에서 상당 부분의 글들은 STM32CubeMx를 사용하지 않는 방법을 다루고 있어서 좀 복잡하고 코딩할 내용이 많습니다. STM32CubeMx로 만들었어도 시간이 오래 되어 적절하지 않은 경우도 있고, 너무나 당연한 내용이라고 생각해서인지 설명을 하지 않아서 UART 통신을 이해하는데 어려움이 많았습니다.

 

본 글에서는 비록 초보적인 내용이지만, 필자가 터득한 내용을 이해하기 쉽도록 서술할 예정입니다. 마이크로프로세서를 전혀 다루지 않았던 사람들도 쉽게 이해할 수 있도록 기술할 예정입니다. 필자처럼 AVR을 다루다가 STM32를 익히고자 하는 경우에는 더 쉽게 이해할수 있을 것으로 예상합니다.

아울러 필자도 처음 STM32를 배우기 시작한 초보자이기 때문에 정확하지 못한 표현 등이 있을 수 있음을 다시 한 번 밝힙니다. 혹시 잘못된 내용이나 정확하지 못한 부분을 발견하시면, 지적해 주시기 바랍니다. 학습하여 수정하도록 하겠습니다.

 

본 글에서는 이전 글에서 만든 프로젝트에 코딩하는 작업을 하겠습니다. STM32CubeMx를 이용해서 프로젝트를 작성하면, 복잡한 초기화를 STM32CubeMx가 다 해주기 때문에 실제로 코딩할 내용은 많지 않습니다.

 

 

 

 

1. 보드와 ESP8266 연결

 

 

 

본 프로그램에서는 ESP8266-12F를 STM32F407의 USART2에 연결합니다. ESP8266-12F를 시리얼 Wifi로 사용하기 위해서 다음 그림에 녹색으로 표시한 4 곳을 연결합니다.

 

 

 

 

 

STM32F407과 ESP8266 모두 3.3V를 사용하기 때문에 Rx와 Tx를 직접 연결해도 되지만, AVR과 연결하려고 1kΩ 저항을 연결해 둔 것이 있어서 그냥 사용했습니다. 보드와 ESP8266을 연결한 사진입니다. Vcc(3.3V)와 GND를 연결하였고, ESP8266의 TxD는 STM32F407의 USART2 RxD(PA3)에, ESP8266의 RxD는 STM32F407 USART2 TxD(PA2)에 연결합니다.

 

 

 

 

 

 

 

2. 보드와 PC 연결

 

 

 

PC와 시리얼 통신을 하기 위해서 STM32F407의 USART3에 USB-TTL 장치를 연결합니다. 가급적 배선을 줄이려고 USB-TTL의 전원선인 Vcc와 GND는 연결하지 않았습니다. 보드의 전원을 PC의 USB로부터 받기 때문에 전원을 연결하지 않아도 통신이 가능합니다. USB-TTL의 TxD는 STM32F407의 USART3 RxD(PB11), USB-TTL의 RxD는 STM32F407의 USART3 TxD(PB10)에 연결합니다. 다음은 보드에 ST-LINK/V2와 ESP8266-12F, USB-TTL을 연결한 사진입니다.

 

 

 

 

 

 

 

3. 코딩 준비

 

 

다음 순서에 따라 코딩 작업을 준비합니다.



① True Studio를 실행합니다.

② 이전에 작업하던 프로젝트가 있으면 프로젝트 창에서 프로젝트을 우측 마우스로 클릭한 다음에 [Delete] 메뉴를 클릭하여 지웁니다. 현재 열려 있는 파일들이 있으면 파일명 옆의 [X]를 눌러서 모두 닫습니다.

 

 

프로젝트를 지울 때에 다음과 같은 팝업 창이 나오는데, 빨간 색으로 표시한 부분을 체크하고 [OK] 버튼을 누르면, 프로젝트가 하드디스크에서도 완전히 지워져서 다시 복구하기 어려워진다는 점에 유의해야 합니다.

 




③ [File] 메뉴에서 [Open Projects From File System] 항목을 클릭합니다.

 


④ [Directory...] 버튼을 눌러 앞의 글에서 만든 STM32F407Uart가 있는 폴더를 선택합니다. 필자의 경우는 [D:\Users\Jeong\Documents\MyProjects\STM32\STM32F4\STM32F407Uart] 폴더에 만들었습니다. [Import source]의 항목이 정확하게 선택한 후에, [Folder] 항목에서 STM32F407Uart의 체크 박스를 선택하고, [Finish] 버튼을 클릭합니다.

 



⑤ [Project Explorer] 창에서 [Src] 폴더 안에 있는 [main.c]를 더블 클릭하여 엽니다.

 

 

 

 

main.c의 43행에 다음과 같은 내용이 있습니다.

/* USER CODE BEGIN Includes */ 
/* USER CODE END Includes */ 
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;
UART_HandleTypeDef huart3;


위 코드를 보면 STM32CubeMx가 USART2와 USART3를 다루기 위해 변수 husart2와 husart3를 정의한 것을 알 수 있습니다. 본 프로젝트에서 입력할 코드는 비교적 짧으므로 모든 코드를 main.c에 입력해도 되지만, 여러 이점이 있으므로 USART를 헤더 파일 uart.h와 소스 파일 uart.c에 코드를 입력하겠습니다.

STM32CubeMx가 만든 코드에 사용자가 프로그래밍 코드를 입력할 때에는 /* USER CODE BEGIN ….*/과 /* USER CODE END … */ 사이에 입력해야 합니다. STM32CubeMx로 프로젝트를 수정하여 코드를 재생성할 때에 이 부분 이외의 영역에 입력한 코드는 사라집니다. 위 코드의 2행에 uart.h를 추가하여 다음과 같이 되도록 합니다.

/* USER CODE BEGIN Includes */
#include "uart.h"
/* USER CODE END Includes */ 
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;
UART_HandleTypeDef huart3;


⑥ uart.h와 uart.c 파일을 프로젝트에 추가합니다. 헤더 파일을 [Inc] 폴더에 추가하기 위해 다음과 같이합니다. [Project Explorer] 창의 [STM32F407Uart] 프로젝트 아래의 [Inc] 폴더를 우측 마우스 버튼으로 클릭한 후에 [New], [Header File]을 클릭합니다.



[Header file] 항목에 uart.h를 입력하고 [Finish] 버튼을 누릅니다.




위와 같은 방법으로 [Src] 폴더를 우측 마우스 버튼으로 클릭한 후에 [Source File]을 선택하여 [uart.c] 파일을 추가합니다.



4. 코딩




(1) UART 수신 인터럽트 사용



본격적으로 STM32의 USART 데이터 수신 인터럽트에 관하여 살펴 보고 코딩 작업을 하겠습니다.

USART 데이터 수신만 인터럽트로 처리하는 경우에 STM32 HAL 드라이버에서 언급해야할 함수는 3개입니다.

ⓐ 데이터 수신 인터럽트를 설정하는 함수 : HAL_UART_Receive_IT()
ⓑ 데이터 수신 인터럽트가 발생했을 때 HAL 드라이버가 호출하는 callback 함수 : HAL_UART_TxCpltCallback()
ⓒ USART로 데이터를 송신하는 함수 : HAL_UART_Transmit()

각각의 함수에 대한 설명은 관련된 내용이 나올 때에 자세히 설명하겠습니다. 이 중 ⓐ와 ⓒ는 HAL 드라이버에서 제공하는 함수로 필요할 때에 호출하는 함수이고, ⓑ는 프로그램의 용도에 따라 작성해야 합니다. 본 프로젝트에서는 USART2로 부터 수신한 데이터는 USART3로 전송하고, USART3로부터 받은 데이터는 USART2로 전송하도록 할 예정입니다.

AVR은 어셈블러로 프로그래밍하는 경우, 인터럽트 벡터 주소에 인터럽트 서비스 루틴의 주소를 기록해 놓으면 데이터 하나가 수신될 때마다 자동으로 인터럽트 서비스 루틴이 실행되었습니다. STM32CubeMx는 STM32 HAL 드라이브를 사용합니다. STM32의 HAL 드라이버의 USART 데이터 수신 인터럽트 서비스 처리 방식은 AVR에서와 많이 다릅니다.

첫째, 데이터가 하나 수신되었을 때마다 인터럽트 서비스 루틴이 호출되지 않는다.
다음은 HAL 드라이버에서 데이터가 입력될 경우 인터럽트 서비스를 부르라고 설정하는 함수로서 프로젝트의 [Drivers\STM32F4xx_HAL_Driver\Inc\stm32f4xx_hal_uart.h]에 정의되어 있습니다.

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);


첫 매개 변수 *huart는 데이터 수신 인터럽트를 발생시킬 USART 포트입니다. 위의 main.c에서 보았던 huart2, huart3 변수가 위치하는 번지수를 보내면 됩니다. 둘째 매개 변수 *pData는 입력된 데이터를 기록할 위치의 주소값입니다. 세번째 매개 변수 Size는 입력 받아야할 데이터 개수입니다. AVR은 데이터 하나가 입력되면 인터럽트 서비스 루틴이 실행되지만, STM32 HAL 드라이버의 경우는 Size로 정한 개수의 데이터가 입력되어야 비로소 인터럽트 서비스루틴이 호출됩니다. 들어오는 데이터의 길이가 일정한 경우는 STM32 HAL 드라이버의 방식이 편리하겠지만, 그렇지 않은 경우는 세번째 매개 변수 Size를 1로 정하여 데이터 하나가 도착할 때마다 호출하도록 해야 합니다.

 

둘째, 수신 인터럽트 서비스 루틴이 호출된 후에, 계속하여 데이터 수신 인터럽트가 발생하도록 하려면 데이터 수신 인터럽트를 다시 설정해야 합니다.
AVR의 경우는 한 번 인터럽트를 설정하면 프로그램에서 해제하지 않는한 데이터 도달시 인터럽트가 계속하여 발생합니다. 그러나, STM32 HAL 드라이버는 데이터 수신 인터럽트 서비스 루틴이 호출된 뒤에, 데이터 수신 인터럽트를 다시 설정하지 않으면 더 이상 데이터 수신 인터럽트가 발생하지 않습니다.

결론적으로, STM32 HAL 드라이버에서의 USART 인터럽트는 다음과 같은 과정으로 처리합니다.

ⓐ 수신 인터럽트가 발생하도록 설정합니다. HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
ⓑ 데이터 수신 콜백 함수에서 데이터를 처리한다. 데이터 인터럽트가 발생하면 HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); 함수가 호출됩니다. 이 함수는 [Drivers\STM32F4xx_HAL_Driver\Inc\stm32f4xx_hal_uart.c]에 다음과 같이 정의되어 있습니다.

__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
   /* Prevent unused argument(s) compilation warning */
   UNUSED(huart);
   /* NOTE: This function Should not be modified, when the callback is needed,
     the HAL_UART_TxCpltCallback could be implemented in the user file
   */
}


위 함수는 가상함수인데, 본 프로젝트에서는 uart.c에 작성 것입니다.

ⓒ ⓐ에서 사용한 HAL_StatusTypeDef HAL_UART_Receive_IT() 함수를 다시 호출하여 데이터 수신 인터럽트가 발생하도록 설정합니다.

 

 

 

 

(2) Queue 사용

 

 

본 프로젝트에서는 STM32F407이 PC와 ESP8266 간의 데이터 전송을 중계하려 합니다. 양쪽의 데이터가 일정한 길이를 가지고 있는 경우가 아니므로, PC등 한 쪽으로부터 데이터를 하나 수신하면 그 데이터를 다른쪽에게 전송하는 방식으로 운영해야 합니다. 이럴 경우 인터럽트 서비스 루틴이 다 수행되기 전에 또 다른 데이터가 수신되면 나중의 데이터를 놓치기 쉽습니다. 그래서 데이터가 들어오면 일단 메모리에 저장해 놓고 시간적 여유가 있을 때에 데이터를 처리하는 방식을 사용할 필요가 있습니다.

 

이렇게 데이터를 일단 메모리에 저장해 놓고 나중에 처리할 때에 사용하는 일반적인 방법으로는 스택(stack)과 큐(Queue)가 있습니다. 주로 stack은 후입선출(LIFO:Last In First Out) 방식을 사용하고 queue는 선입선출(FIFO:First In First Out) 방식을 사용합니다. Usart 데이터 수신 인터럽트 처리는 FIFO 방식이 적절하므로 queue를 사용하기로 합니다.

 

uart.h에 UARTQUEUE 데이터 형을 다음과 같이 정의합니다.

#define QUEUE_BUFFER_LENGTH 1024
typedef struct {
  int head, tail, data;
  uint8_t Buffer[QUEUE_BUFFER_LENGTH];
} UARTQUEUE, *pUARTQUEUE;


프로그램 초기에는 queue의 구성 요소 head는 수신한 데이터를 넣을 Buffer 상의 위치를 가리키며, tail은 꺼내갈 데이터의 Buffer 상 위치를 가리키는 용도로 사용합니다. 구성 요소 data는 현재 queue에 있는 유효한 데이터 수를 가지고 있도록 합니다. data가 0이면 현재 수신된 데이터가 없는 상태이고, data가 QUEUE_BUFFER_LENGTH와 같으면 버퍼가 가득 찬 상태입니다. 프로그램이 시작할 때 또는 Buffer를 모두 비워야 할 때에는 queue의 구성 요소 head, tail, data 모두 0의 값을 가져야 합니다. uart.c에 queue를 초기화 하는 함수 InitUartQueue() 함수를 다음과 같이 만듭니다.

void InitUartQueue(pUARTQUEUE pQueue)
{
  pQueue->data = pQueue->head = pQueue->tail = 0;
}


데이터가 하나 수신되면 queue에서는 다음과 같은 작업을 해야 합니다.

ⓐ head를 1증가 시킨다.
ⓑ head가 Buffer의 끝이면 head에 0을 대입한다.
ⓒ data 값을 1증가 시킨다.
ⓓ 데이터 수신 인터럽트가 발생하도록 설정한다.


다음은 앞에서 언급한 STM32 HAL 드라이버에서 데이터 수신 인터럽트가 발생했을 때 호출되는 callback 함수 HAL_UART_RxCpltCallback()을 작성한 것입니다. 이 함수도 uart.c에 저장합니다. WifiQueue는 ESP8266이 연결된 USART2에서 사용할 queue이고, MonitorQueue는 PC와 연결된 USART3에서 사용할 queue입니다.

 

UARTQUEUE WifiQueue;
UARTQUEUE MonitorQueue;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  pUARTQUEUE pQueue;
  pQueue = (huart->Instance == USART2 ? &WifiQueue:&MonitorQueue); // usart2 면 WifiQueue를 아니면 MonitorQueue를 대상으로
  pQueue->head++;                                                  //ⓐ head를 1증가시키고
  if (pQueue->head == QUEUE_BUFFER_LENGTH) pQueue->head = 0;       //ⓑ head가 Buffer의 끝이면
  pQueue->data++;                                                  //ⓒ data 값을 1증가
  if (pQueue->data == QUEUE_BUFFER_LENGTH)                           //queue가 full이면 하나 처리하고
  GetDataFromUartQueue(huart);
  HAL_UART_Receive_IT(huart, pQueue->Buffer + pQueue->head, 1);   //ⓓ 데이터 수신 인터럽트 설정
}



다음으로 Queue에서 데이터를 하나 꺼내어 처리하는 함수를 만듭니다. 본 프로젝트에서는 ESP8266으로부터 받은 데이터는 PC로 전달하고, PC로부터 받은 데이터는 ESP8266에게 전달합니다. 이를 위하여 GetDataFromUartQueue() 함수를 uart.c에 다음과 같이 만듭니다. USART에 한 바이트를 전송하는 함수는 HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)이며, 이 함수는 [Drivers\STM32F4xx_HAL_Driver\Inc\stm32f4xx_hal_uart.c]에 정의 되어 있습니다.

#define hWifi    huart2 
#define hMonitor huart3
void GetDataFromUartQueue(UART_HandleTypeDef *huart)
{
  UART_HandleTypeDef *dst = (huart->Instance == USART2 ? &hMonitor:&hWifi);       //USART2이면 출력 대상은 USART3, 아니면 USART2
  pUARTQUEUE pQueue = (huart->Instance == USART2 ? &WifiQueue:&MonitorQueue);     //USART2이면 WifiQueue의 데이터를, 아니면 MonitorQueue의 데이터를
  HAL_UART_Transmit(dst, pQueue->Buffer + pQueue->tail, 1, 3000);                   //한 바이트 전송
  pQueue->tail++;                                                                   //tail 1증가
  if (pQueue->tail == QUEUE_BUFFER_LENGTH) pQueue->tail = 0;                        //Buffer의 끝이면 0을
  pQueue->data--;                                                                   //data 1감소
  HAL_Delay(1);                                                                        //시간 지연
}


매크로 상수 hWifi와 hMonitor는 USART2에 ESP8266를 연결했는지 PC를 연결했는지를 굳이 기억하지 않고 프로그램을 작성하기 위해 정의하였습니다. 실제로는 이 매크로는 uart.h에서 정의해서 STM32CubeMx가 자동으로 생성하는 부분이 아닌 모든 부분에서는 husart2나 husart3를 사용하지 않도록 프로그램을 작성하였습니다.

다음은 모든 코드가 입력된 uart.h 파일입니다.

#ifndef UART_H_
#define UART_H_ 
#define hWifi huart2
#define hMonitor huart3
#define QUEUE_BUFFER_LENGTH 1024
typedef struct {
  int head, tail, data;
  uint8_t Buffer[QUEUE_BUFFER_LENGTH];
} UARTQUEUE, *pUARTQUEUE;

extern UART_HandleTypeDef huart2;
extern UART_HandleTypeDef huart3;
extern UARTQUEUE WifiQueue;
extern UARTQUEUE MonitorQueue;

void InitUartQueue(pUARTQUEUE pQueue);
void GetDataFromUartQueue(UART_HandleTypeDef *huart);

#endif /* UART_H_ */

다음은 모든 코드가 입력된 uart.c 파일입니다.

#include "stm32f4xx_hal.h"
#include "uart.h"

UARTQUEUE WifiQueue;
UARTQUEUE MonitorQueue;

void InitUartQueue(pUARTQUEUE pQueue)
{
  pQueue->data = pQueue->head = pQueue->tail = 0;
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  pUARTQUEUE pQueue;
  pQueue = (huart->Instance == USART2 ? &WifiQueue:&MonitorQueue);
  pQueue->head++;
  if (pQueue->head == QUEUE_BUFFER_LENGTH)
    pQueue->head = 0;
  pQueue->data++;
  if (pQueue->data == QUEUE_BUFFER_LENGTH)
    GetDataFromUartQueue(huart);
  HAL_UART_Receive_IT(huart, pQueue->Buffer + pQueue->head, 1);
}

void GetDataFromUartQueue(UART_HandleTypeDef *huart)
{
  UART_HandleTypeDef *dst = (huart->Instance == USART2 ? &hMonitor:&hWifi);
  pUARTQUEUE pQueue = (huart->Instance == USART2 ? &WifiQueue:&MonitorQueue);
  HAL_UART_Transmit(dst, pQueue->Buffer + pQueue->tail, 1, 3000);
  pQueue->tail++;
  if (pQueue->tail == QUEUE_BUFFER_LENGTH)
    pQueue->tail = 0;
  pQueue->data--;
  HAL_Delay(1);
}

main.c 파일에 추가해야할 내용은 다음과 같습니다. STM32CubeMx가 만든 주석 내용을 잘 보고 적절한 위치에 입력해야 합니다. 설명은 주석으로 대신합니다.

/* USER CODE BEGIN WHILE */
  InitUartQueue(&WifiQueue);                              //ESP8266 queue 초기화
  InitUartQueue(&MonitorQueue);                           //PC측 queue 초기화
  HAL_UART_Receive_IT(&hWifi, WifiQueue.Buffer, 1);       //ESP8266 측 데이터 수신 인터럽트 설정
  HAL_UART_Receive_IT(&hMonitor, MonitorQueue.Buffer, 1); //PC측 데이터 수신 인터럽트 설정
  while (1)   {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    while (WifiQueue.data > 0) GetDataFromUartQueue(&hWifi);       //ESP8266에서 온 데이터가 있으면 처리
    while (MonitorQueue.data > 0) GetDataFromUartQueue(&hMonitor); //PC에서 온 데이터가 있으면 처리
  }
  /* USER CODE END 3 */




5. 컴파일 및 다운로드




(1) 컴파일



[Project] 메뉴에서 [Build Project]를 클릭하여 실행파일을 만듭니다.





(2) 다운로드 및 디버깅



[Run] 메뉴에서 [Debug Configurations...]을 클릭하여 다음의 그림과 같이 설정한 후에, 맨 아래의 [Debug] 버튼을 누릅니다. [Debug] 버튼을 누르면 실행파일이 STM32F407로 다운로드 된 후에 디버깅 모드로 들어 갑니다. [Run] 메뉴의 [Terminate]를 클릭하거나 툴박스의 [Terminate] 아이콘을 누르면 디버그 모드가 종료됩니다. 이후로는 [Run] 메뉴의 [Debug]를 클릭하면 바로 다운로드 및 디버깅이 시작됩니다.

 



 

 

 

 

 

다음은 PC에서 STM32F407 보드를 경유하여 ESP8266을 제어하는 화면을 캡쳐한 그림입니다.

 

 

 

 

직접 코딩하는 내용은 별로 많지 않은데, 자세하게 설명하느라 생각보다 글이 길어졌습니다. 다음 글에서는 STM32CubeMx가 생성한 코드들을 분석해 보도록 하겠습니다. AVR과 달리 초기화하는 과정이 상당히 복잡한 듯합니다. 하나씩 차근차근 파헤쳐볼 생각입니다.

 

프로젝트에 사용된 파일들을 첨부합니다. 첨부한 파일은 에러 발생시를 가정하여 약간의 코드를 추가하였습니다.

 

STM32F407Uart.zip
다운로드

 

 

 

블로그 이미지

엠쿠스

Microprocessor(STM32, AVR)로 무엇인가를 만들어 보고자 학습 중입니다.

,