본문 바로가기
# Semiconductor/- Semicon Academy

[Harman 세미콘 아카데미] 48일차 - ARM 및 RTOS 활용(RTOS, UART_rx, UART의 여러 가지 방식)

by Graffitio 2023. 9. 1.
[Harman 세미콘 아카데미] 48일차 - ARM 및 RTOS 활용(RTOS, UART_rx, UART의 여러 가지 방식)
728x90
반응형
[RTOS 기본 개념]

 

아래 링크 참조

RTOS의 개념

 

임베디드 시스템의 핵심 기술, RTOS란? (기본 개념 및 특징, FreeRTOS, OSEK/VDX, 용어 정리-Task, Deadline

[OS - Operating System] OS란? Operating System의 약자로, 운영체제라고 부르기도 한다. 응용프로그램이 요청하는 시스템 자원을 효율적으로 분배하고 관리하는 기능을 하며, 사용자가 컴퓨터를 편리하고

rangvest.tistory.com

 

Firmware

: 임베디드 하드웨어를 동작시키며 특정 기능의 수행을 목표로 하는 프로그램

  유저의 개입이 거의 없다는 의미로 봐도 무방하다.

  하드웨어에 기본적으로 내장된, 전원이 꺼지더라도 다시 전원이 공급되면

  저장된 내용이 실행 가능하도록 하는 소프트웨어

  ROM이나 PROM에 저장되어 있다.

 


 

[FreeRTOS 추가]

 

📌 ioc Setting

 

지원되는 명령어 차이, 큰 차이는 안 보이니까 CMSIS-V1 클릭

 

안 해주면 자꾸 Enable하라고 에러 뜸

 

Code Generation

 

그냥 Generation하면 이러한 Warning 뜸

RTOS는 Systick보다는 HAL 타임 베이스 소스를 사용하는 것이 좋다는 의미

 

보통 뒷 번호의 TIM을 쓰는데 우리는 그냥 TIM3 사용할 것이다.

 

경고없이 정상 동작

 

새로운 파일이 두 개가 생긴 것을 볼 수 있다.

이전까지는 컴파일러 진입점을 main() 함수로 사용했었다.

하지만 FreeRTOS를 사용하게 되면, FreeRTOS를 진입점으로 삼게 된다.

 


 

📌 Task 생성

 

다시 Setting으로 돌아와서 설정 시작

Task, Thread, 작업 다 비슷한 말들이니, 너무 용어에 치중하지 않아도 된다.

 

OK 클릭

 

Task 생성됨
Code Generation

 

Task 등록 시, 위와 같은 함수들이 추가된다.

 

Task는 각각 개별적으로 구동되고,

Task를 등록하게 되면, 먼저 Task를 정의하고 세부적으로 설계하는 작업이 필요하다.

Task는 가능하면 간단하고(적은 메모리 사용) 시간을 짧게 가져가도록 설계하는 것이 좋다.

 

Task는 각각 실행되는 것이 아니고, osKernelStart()에 의해 한 번에 실행된다.

 

Task 자체의 main 함수

    ✅ for(;;) = while(1) 

          무한루프 돌리는 함수

 

    ✅ osDelay(1) = HAL_Delay(1)

           Task에서는 HAL_Delay 쓰면 충돌이 일어나므로

           osDelay 함수를 써야 한다.

 

Setting 변경

 

변경된 Setting 값이 적용됨.

 


 

📌 Task coding

 

Task 1개 설계

 

// LED Toggle하는 Task 설계
void Test_Task(void const * argument)
{
  /* USER CODE BEGIN Test_Task */
  /* Infinite loop */
  for(;;)
  {
	HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    osDelay(200);
  }
  /* USER CODE END Test_Task */
}

 

 

Task 3개 운용 설계

 

Pin Configuration

 

Task 생성

 

각각의 Task들이 Thread로 선언됨.

 

// LED_Red
void Task_LED_R(void const * argument)
{
  for(;;)
  {
	HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    osDelay(200);
  }
}

// LED_Green
void Task_LED_G(void const * argument)
{
  for(;;)
  {
	HAL_GPIO_TogglePin(LED_Green_GPIO_Port, LED_Green_Pin);
    osDelay(100);
  }
}

// LED_Blue
void Task_LED_B(void const * argument)
{
  for(;;)
  {
	  HAL_GPIO_TogglePin(LED_Blue_GPIO_Port, LED_Blue_Pin);
	  osDelay(50);
  }
}

 

 

 

Kernel Start

  osKernelStart(); // Kernel Start 함수

  while (1)
  {
	  HAL_GPIO_TogglePin(LED_main_GPIO_Port, LED_main_Pin); // main 함수의 while문
  }
}

 

커널 스타트가 되면, 그 다음으로 넘어가지 않는다.

따라서 main 함수의 while문 안에 코드를 작성해도 실행되지 않고,

RTOS는 Task들만 시분할하여 구동시킨다.

스케쥴링에 따라서 하나씩 구동되며, 그 속도가 매우 빨라 동시에 실행되는 것처럼 보인다.

 

cf)

ARM은 Single core이다.

따라서 Task를 아무리 세분화시켜서 잘 만들어도 한계가 있다.

 

Task로 설정된 LED만 동작한다.

 


 

[UART-Rx]

 

📌 UART_Input

✅ putchar : argument를 받아 출력하는 함수

✅ getchar : 입력을 받는 함수

     하나의 패킷단위로 불러들여온다.(패킷 : 짧게는 1byte 길게는 1k 또는 1M까지)

     키보드에 있는 Enter : Scanf의 리턴값( = \n)

 

✅ Echo

     : 터미널에서 입력된 값이 PC로 던져지게 되고,

       (터미널 입장에서는 tx, PC 입장에서는 rx)

       PC에서 다시 터미널로 돌려주어 이 값이 작성되었다고 출력시킴.

 

#include "main.h"
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
int __io_putchar(int ch)
{
	HAL_UART_Transmit(&huart2, &ch, 1, 10);
	return ch;
}
int __io_getchar(void)
{
	int ch;
	while(1)
	{
		if(HAL_UART_Receive(&huart2, &ch, 1, 10) == HAL_OK)
		break;
	}
	HAL_UART_Transmit(&huart2, &ch, 1, 10);

	return ch;
}
/* USER CODE END PFP */


int main(void)
{

  SystemClock_Config();

  MX_GPIO_Init();
  MX_USART2_UART_Init();
  printf("Program Start! \r\n");

  while (1)
  {
	int i;
	printf("Press numeric key and [Enter]....\r\n"); // 숫자 키를 입력하라고 프롬포트 내보내 줌.
	scanf("%d", &i);
	printf("You Pressed [%d] value.\r\n", i);
  }
}

 

scanf 함수 사용 시에는

setbuf를 쓰지 않으면 제대로 작동하지 않는다.

 

stdio.h 선언
setbuf 선언

#include "main.h"
#include <stdio.h>
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
int __io_putchar(int ch)
{
	HAL_UART_Transmit(&huart2, &ch, 1, 10);
	return ch;
}
int __io_getchar(void)
{
	int ch;
	while(1)
	{
		if(HAL_UART_Receive(&huart2, &ch, 1, 10) == HAL_OK)
		break;
	}
	HAL_UART_Transmit(&huart2, &ch, 1, 10);

	return ch;
}
/* USER CODE END PFP */


int main(void)
{

  SystemClock_Config();

  MX_GPIO_Init();
  MX_USART2_UART_Init();
  printf("Program Start! \r\n");
  setbuf(stdin, NULL);

  while (1)
  {
	int i;
	printf("Press numeric key and [Enter]....\r\n"); // 숫자 키를 입력하라고 프롬포트 내보내 줌.
	scanf("%d", &i);
	printf("You Pressed [%d] value.\r\n", i);
  }
}

정상적으로 동작한다.

 

문제는 왼쪽 화살표 또는 Backspace 키와 같이 숫자가 아닌 다른 키를 눌렀을 경우에,

아래와 같이 난리가 난다. (왼쪽 화살표 = \b이기 때문에)

 

 


 

📌 Coding

cMode 값에 따라 LED 상태가 변환되는 기능 구현

cMode 값은 UART를 통해 입력 받으며, scanf를 쓰지 않고 구현할 것.

(Task에서 scanf 함수 사용 시, 오류 발생)

 

#include "main.h"
#include "cmsis_os.h"

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;

osThreadId defaultTaskHandle;
osThreadId Task_LED_RedHandle;
osThreadId Task_LED_GreenHandle;
osThreadId Task_LED_BlueHandle;
/* USER CODE BEGIN PV */
char rxbuf[100];
int cMode; // LED Control : 1, 2, 3(R, G, Y) / 4(All)
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
void StartDefaultTask(void const * argument);
void Task_LED_R(void const * argument);
void Task_LED_G(void const * argument);
void Task_LED_B(void const * argument);

/* USER CODE BEGIN PFP */
int __io_putchar(int ch)
{
	HAL_UART_Transmit(&huart2, &ch, 1, 10);
	return ch;
}
int __io_getchar(void)
{
	int ch;
	while(1)
	{
		if(HAL_UART_Receive(&huart2, &ch, 1, 10) == HAL_OK)
		break;
	}
	if (ch == '\b' || ch == 0x7f)
	{
		HAL_UART_Transmit(&huart2, "\b \b", 3, 10); // \b \b : 총 세 글자이기때문에 3
	}
	else if(ch == '\n')
	{
		HAL_UART_Transmit(&huart2, "\r\n", 2, 10);
	}
	else HAL_UART_Transmit(&huart2, &ch, 1, 10);

	return ch;
}

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_USART2_UART_Init();

  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* definition and creation of Task_LED_Red */
  osThreadDef(Task_LED_Red, Task_LED_R, osPriorityIdle, 0, 128);
  Task_LED_RedHandle = osThreadCreate(osThread(Task_LED_Red), NULL);

  /* definition and creation of Task_LED_Green */
  osThreadDef(Task_LED_Green, Task_LED_G, osPriorityIdle, 0, 128);
  Task_LED_GreenHandle = osThreadCreate(osThread(Task_LED_Green), NULL);

  /* definition and creation of Task_LED_Blue */
  osThreadDef(Task_LED_Blue, Task_LED_B, osPriorityIdle, 0, 128);
  Task_LED_BlueHandle = osThreadCreate(osThread(Task_LED_Blue), NULL);

  /* Start scheduler */
  osKernelStart();

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

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

 

void StartDefaultTask(void const * argument)
{
  for(;;)
  {
	printf("    <LED Control>    \r\n");
	printf("cMode = 1 ----> Red\r\n");
	printf("cMode = 2 ----> Green\r\n");
	printf("cMode = 3 ----> Yellow\r\n");
	printf("cMode = 4 ----> All\r\n");
	printf("Press cMode + [Enter]...\r\n");
	cMode = __io_getchar()-48; // __io_getchar()는 문자로 값을 받아 온다. 우리가 1을 넣으면, 아스키 코드값인 49가 반환되므로 실제로 1을 사용하기 위해서는 0의 아스키코드값인 48을 뺴주면 된다.
	printf("LED Mode : %d\r\n", cMode);
	printf("\r\n");
	printf("\r\n");
    osDelay(200);
  }
}

void Task_LED_R(void const * argument)
{
  for(;;)
  {
	if(cMode == 1 || cMode == 4) HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    osDelay(100);
  }
}

void Task_LED_G(void const * argument)
{
  for(;;)
  {
	if(cMode == 2 || cMode == 4) HAL_GPIO_TogglePin(LED_Green_GPIO_Port, LED_Green_Pin);
    osDelay(100);
  }
}

void Task_LED_B(void const * argument)
{
  for(;;)
  {
	  if(cMode == 3 || cMode == 4) HAL_GPIO_TogglePin(LED_Blue_GPIO_Port, LED_Blue_Pin);
	  osDelay(100);
  }
}

 

 

__io_getchar()는 문자로 값을 받아 온다.

우리가 1을 넣으면, 아스키 코드값인 49가 반환되므로

실제로 1을 사용하기 위해서는 0의 아스키코드값인 48( = 0x30)을 뺴주면 된다.

  

 

Task와 Task를 연결하는 작업을 하게 될 경우,

공통적인 변수를 이용하여 Main Task에서 컨트롤해줄 수 있도록 구성하는 것이 좋다.

 


 

📌 Task 점유에 관한 내용(중요❗❗)

// Task의 기본형
void StartDefaultTask(void const * argument)
{
  for(;;)
  {
    osDelay(1);
  }
}

 

처음 Task를 생성하면 기본적으로 위와 같이 코드가 생성되는데,

별로 중요해 보이지 않았던 Delay가 사실, 아주 중요한 기능을 가지고 있다.

그 기능은 바로 다른 Task가 실행될 수 있도록 기회를 주는 것이며,

해당 프로세스가 clock의 상당한 부분을 점유하여 다른 Task가 동작할 기회가 없어지고 

이로 인해 Task 간의 연계될 수 있는 타이밍이 형성되지 않게 된다.

(한 놈만 미친듯이 돌고 있음.)

 

따라서 다른 Task가 정상적으로 동작할 수 있도록 적당한 Delay를 주는 것이 좋다.

 


 

[UART의 여러가지 사용 방식]

 

UART도 ADC처럼 여러 가지 방식으로 사용될 수 있다.  ① 수동(위 예제와 같이)  ② 인터럽트  ③ DMA     가장 안정적인 방식은 DMA     (타이밍이 쫑날 가능성이 거의 없다.)

 


 

📌 DMA 방식

데이터가 언제 들어올 지 모르니까데이터가 들어오면 DMA에 쌓이도록 rx로 DMA 설정

DMA Setting
✅ Circular로 하지 않으면, 정해진 범위에서 누적되어 계속 쌓인다.     Circular를 사용하면, 누적되어 끝까지 가면 다시 처음부터 쌓인다.

 

Peripheral to Memory : 주변 기기에서부터 메모리로     cf) Tx로 DMA를 설정할 경우에는,  Memory to Pefipheral로 설정          예시 : 저속 출력 장치로 대용량의 데이터를 출력할 때(프린터 출력)                   스풀이라는 임시 저장 공간에 저장해놓고 하나씩 출력

 

 

__io_getchar() 는 수동 입력 대기 함수이다.따라서 입력이 들어올 때까지 무한 대기 상태가 된다.

 

하지만 우리는 DMA 방식을 사용할 것이므로아무때나 입력되고 입력된 값은 DMA에 저장되어 있게 되므로Task 부하를 덜어주게 된다.

 


 

📌 DMA 방식으로 Coding

 

<main()>

int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  // HAL_UART_Receive_DMA(&huart2, rxbuf, 100);
  // DMA가 두 가지 방식이므로 Receive, Circular DMA에 방식이므로 100까지 쌓이다가 처음부터 다시 쌓임
  // 근데 이렇게 하면 별도로 DMA를 관리하는 기능이 추가로 필요하다.
  // Circular mode이므로 내가 최종적으로 받은 데이터가 어디에 위치하는지 잘 모르게 됨.
  // 그래서 그냥 심플하게 size를 1로 하면 편함
  HAL_UART_Receive_DMA(&huart2, &rxbuf, 1);
  /* USER CODE END 2 */

  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* definition and creation of Task_LED_Red */
  osThreadDef(Task_LED_Red, Task_LED_R, osPriorityIdle, 0, 128);
  Task_LED_RedHandle = osThreadCreate(osThread(Task_LED_Red), NULL);

  /* definition and creation of Task_LED_Green */
  osThreadDef(Task_LED_Green, Task_LED_G, osPriorityIdle, 0, 128);
  Task_LED_GreenHandle = osThreadCreate(osThread(Task_LED_Green), NULL);

  /* definition and creation of Task_LED_Blue */
  osThreadDef(Task_LED_Blue, Task_LED_B, osPriorityIdle, 0, 128);
  Task_LED_BlueHandle = osThreadCreate(osThread(Task_LED_Blue), NULL);
  
  /* Start scheduler */
  osKernelStart();

  /* We should never get here as control is now taken by the scheduler */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

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

 

<Task Coding>

/* USER CODE BEGIN 4 */
int state = 0; // Input state : 0 / ready , 1 / data
/* USER CODE END 4 */

/* USER CODE BEGIN Header_StartDefaultTask */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
	 if(state == 0)
	 {
		 printf("    <LED Control>    \r\n");
		 printf("cMode = 1 ----> Red\r\n");
		 printf("cMode = 2 ----> Green\r\n");
		 printf("cMode = 3 ----> Yellow\r\n");
		 printf("cMode = 4 ----> All\r\n");
		 printf("Press cMode + [Enter]...\r\n");
		 state = 1;
	 }
	 else if (rxbuf[0] != 0) // rxbuf 에 어떤 값이라도 들어와 있다면?
	 {
		 cMode = rxbuf[0]-48; // __io_gethar() 대신 rxbuf[0]에 저장되므로
		 state = 0;
		 printf("LED Mode : %d\r\n", cMode);
		 rxbuf[0] = 0; // 다시 0을 넣어줘야 무한반복되지 않는다.
		 // cMode는 이전 rxbuf[0] 값이 들어가기 때문에 LED 출력은 유지된다.
//		 printf("\r\n");
//		 printf("\r\n");
//		 printf("\r\n");
	 }
    osDelay(200);
  }
  /* USER CODE END 5 */
}

/* USER CODE BEGIN Header_Task_LED_R */
/**
* @brief Function implementing the Task_LED_Red thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Task_LED_R */
void Task_LED_R(void const * argument)
{
  /* USER CODE BEGIN Task_LED_R */
  /* Infinite loop */
  for(;;)
  {
	if(cMode == 1 || cMode == 4) HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    osDelay(100);
  }
  /* USER CODE END Task_LED_R */
}

/* USER CODE BEGIN Header_Task_LED_G */
/**
* @brief Function implementing the Task_LED_Green thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Task_LED_G */
void Task_LED_G(void const * argument)
{
  /* USER CODE BEGIN Task_LED_G */
  /* Infinite loop */
  for(;;)
  {
	  if(cMode == 2 || cMode == 4) HAL_GPIO_TogglePin(LED_Green_GPIO_Port, LED_Green_Pin);
    osDelay(100);
  }
  /* USER CODE END Task_LED_G */
}

/* USER CODE BEGIN Header_Task_LED_B */
/**
* @brief Function implementing the Task_LED_Blue thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Task_LED_B */
void Task_LED_B(void const * argument)
{
  /* USER CODE BEGIN Task_LED_B */
  /* Infinite loop */
  for(;;)
  {
	  if(cMode == 3 || cMode == 4) HAL_GPIO_TogglePin(LED_Blue_GPIO_Port, LED_Blue_Pin);
	  osDelay(100);
  }
  /* USER CODE END Task_LED_B */
}

 

내가 입력했던 데이터는 rxbuf[0]에 저장되어 있으니, 터미널로 계속해서 출력된다.

따라서 rxbuf[0]에 0을 넣어주면, cMode는 이전 값이 저장되어 있어 LED 출력은 유지되고

프롬포트 상 출력은 무한반복되지 않는다.

 

 

 

여담이지만, 이전에 내가 작성했던 코드들은 웬만하면 지우지 말고 주석처리해놓으면 좋다

코드가 길어져서 일 많이한 것처럼 보이기도 하고,

내가 어떤 생각 플로우로 이 코드를 짰는 지도 기억해낼 단서가 된다.

 


 

728x90
반응형