STM32F103C8T6으로 IR 신호를 다루어 보겠습니다. 사용하는 하드웨어는 그동안 사용해 오던 일명 blue pill 보드입니다. 생각보다 글이 길어져서 1편과 2편으로 나누겠습니다.
사용한 부품은 다음과 같습니다.
1. Blue pill 보드 1개
2. IR 수신 모듈(603LM) 1개
3. IR LED(부품명 모름) 1개
4. NPN Tr 1개
5. 저항 1㏀ 1개
6. 저항 22Ω 1개
부품 연결도입니다. 윈도우즈 10 그림판에서 대충 그렸습니다. IR led
1. 프로젝트 만들기
STM32F103C8T6(일명 blue pill, 이하 STM32F103) 보드로 리모콘의 IR 신호를 분석하여 그 결과를 PC로 보내고, PC로부터 명령을 전달 받아서 그에 해당하는 IR 신호를 송출하는 시스템을 구성해 보겠습니다. PC와 STM32F103의 통신은 STM32F103의 USB를 CDC(Communication Device Class)로 사용해서 시리얼 통신을 하도록 합니다. STM32F103의 USB를 CDC 모드로 프로그래밍하는 방법은 STM32F103 CDC 사용하기에서 자세히 다룬 바 있습니다. 이 글에서 다룬 방법대로 SM32F103의 CDC를 활성화 합니다. 다음은 앞의 링크 글에서 클럭을 설정한 상태입니다.
위에 링크한 글 STM32F103 CDC 사용하기에서 작성한 프로젝트에서 사용한 프로젝트를 그대로 활용하기로 합니다. uart.h uart.c 등을 이용해서 PC로부터 전달 받는 명령들은 ring형 queue로 처리합니다. 자세한 사항은 위의 링크한 글을 참조하시기 바랍니다.
위의 링크 글에 있던 클럭 설정 상황을 본 글에 다시 올린 이유는 본글에서 Timer1을 사용하기 때문입니다. STM32F103의 Timer1은 통상 TIM1으로 사용하며 APB2 버스의 클럭을 사용합니다. 위 그림의 아래쪽에 붉은 색으로 네4모 친 APB2 Prescaler (/16) 오른쪽에 4.5와 9가 기록되어 있습니다. 그림의 일부가 가려져서 다 보이지는 않습니다만, 4.5는 APB2 버스와 연결되어 있는 주변 장치에 제공하는 클럭이 4.5MHz라는 뜻입니다. 즉 본글에서 사용하는 Timer1은 9MHz로 동작합니다.
다음은 STM32CubeMX에서 Timer1을 설정한 화면입니다.
① Clocl Source를 Internal Clock으로 설정합니다. 위에서 언급한대로 Internal Clock은 9MHz로 동작합니다.
② Channel1을 Input Capture indirect mode로 설정합니다.
③ Channel2를 Input Capture direct mode로 설정합니다. 이렇게 하면 Input Capture Channle 2인 PA9으로 신호가 들어오면 이 신호가 Channel2로도 전달됩니다.
④ Parameter Settings 항목에서 Prescaler를 8, Count Period를 29999로 입력합니다. Timer1이 9MHz로 동작하는데 prescaler에 8을 입력하면 Timer1은 9MHz / (8 + 1)의 속도로 동작합니다. 즉 Timer1은 1micro second(1uS)에 1씩 증가합니다. Counter Period에 29999를 입력해 놓으면 Timer1은 (29999 + 1) uS마다 overflow가 발생합니다. TIM1 update interrupt가 발생하도록 설정했다면 30000uS 즉 30 mili second(mS)마다 TIM1 update interrupt가 발생합니다. Input Capture가 발생하다가 30mS 동안 입력이 없으면 일단 Input Capture가 종료한 것으로 간주하기로 합니다.
⑤ Pinout View에서 PA9을 오른쪽 마우스로 클릭한 다음 라벨을 IR_IN으로 입력합니다.
아래의 그림과 같이 Channel1과 Channel2의 인터럽트 시기를 정합니다. Channel1은 Falling Edge로 Channel2는 Rising Edge로 설정합니다.
리모콘에서 high 신호가 나오기 시작할 때에 603LM의 출력은 low로 변화합니다. 이 때에 TIM1의 Channel1에 Input Capture가 발생하도록 설정했습니다. 마찬가지로 IR 신호가 low로 변할 때에 603LM의 출력이 high가 되면서, 이 때에 TIM1의 Channel2에 Input Capture가 발생하도록 설정했습니다.
다음 그림에서와 같이 TIM1에서 update 인터럽트와 capture compare 인터럽트가 발생하도록 NVIC를 설정합니다.
IR 신호를 송출할 때에는 약 38KHz로 IR LED를 On/Off 해야 합니다. 이때에 TIM1의 Channel3을 PWM으로 동작시킬 예정입니다. 다음 그림에서와 같이 TIM1의 Channel3를 PWM Generation CH3로 설정하고, PA10의 라벨을 IR_OUT으로 설정합니다.
9000 / 38 = 약 237이므로 TIM1의 ARR(Auto Reload Register)에 237 - 1 = 236을 입력하면 약 38KHz의 주파수를 얻을 수 있습니다. 그러나, STM32CubeMX의 Timer1 Counter Period에 입력한 값이 TIM1의 ARR 값으로 입력됩니다. Timer1의 Counter Period는 위에서 IR 데이터를 수신할 때에 30mS을 계산하기 위해서 29999의 값을 써야 합니다. 따라서 TIM1의 ARR 값은 프로그램내에서 IR 송출할 때에 236으로 바꾸어 송출하고, 송출 후에는 다시 이전의 값인 29999의 값을 가지도록 하겠습니다. IR 신호를 송출할 때에 각 펄스의 duty비를 50%으로 하겠습니다. 어디선가 NEC 프로토콜은 duty비가 1/3이어야 한다고 본 듯한데, 50%로해도 동작하므로 50%로 사용하겠습니다. 38KHz 신호를 내보낼 때에 1 cycle의 길이가 237 클럭이므로 펄스의 폭은 (237 / 2 - 1)을 계산하여 117로 정합니다. 아래의 그림과 같이 Channel3를 PWM mode 1로 정하고 Pulse 값을 117로 입력합니다.
위와 같이 설정을 마치면 [Genrate Code] 버튼을 눌러 프로젝트를 생성합니다.
2. 코딩하기
먼저 TIM1의 Input Capture 인터럽트 서비스 루틴을 만듭니다. HAL 드라이버의 경우 Input Capture 인터럽트가 발생하면 HAL_TIM_IC_CaptureCallback 함수가 호출됩니다. 이 함수의 원형은 Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c 파일 안에 다음과 같이 정의되어 있습니다.
__weak void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { /* Prevent unused argument(s) compilation warning */ UNUSED(htim); /* NOTE : This function should not be modified, when the callback is needed, the HAL_TIM_IC_CaptureCallback could be implemented in the user file */ }
__weak로 정의되어 있으므로 일종의 가상함수입니다. 이 가상함수를 이용해서 IR_IN 핀으로 입력되는 신호의 길이를 측정하겠습니다. 먼저 IR 신호에 관한 정보를 담아 둘 데이터 큐를 만듭니다. 헤더 파일 infrared.h를 프로젝트에 추가하고 다음과 같이 새로운 데이터 타입을 만듭니다.
#define REMOCON_DATABITS 32 #define SAMSUNG_DATABITS 56 #define MAX_IR_QUEUESIZE (REMOCON_DATABITS * 5) #define IR_STANDBY 0 #define IR_DATA_IN 1 #define IR_NEC_LEADER 2 #define IR_SAMSUNG_LEADER 3 #define IR_TC9012_LEADER 4 #define IR_NO_MORE 5 typedef struct { uint8_t Bytes; uint8_t Code[16]; } REMOCONCODE; typedef struct { uint8_t Head, Tail, Data, State, Bits; uint16_t QueueHigh[MAX_IR_QUEUESIZE]; uint16_t QueueLow[MAX_IR_QUEUESIZE]; } IRQUEUE; extern REMOCONCODE RemoconCode; extern IRQUEUE IRQueue;
IR 신호에 관한 정보를 담아둘 데이터형은 IRQUEUE입니다. Head는 새로 들어오는 정보를 기록할 큐의 위치를 담는 변수이고, Tail은 꺼내갈 정보가 있는 큐상의 위치를 담는 변수입니다. Data는 현재 큐상에 있는 데이터 수를 담는 변수입니다. State는 IR 신호 상태를 나타내는 변수이고, Bits는 IR 정보를 분석할 때에 현재까지 파악된 비트 수를 담는 변수입니다.QueueHigh 배열과 QueueLow 배열에는 각 IR 신호의 high 시간과 low 시간의 길이를 uS 단위로 담아 놓을 예정입니다.
State 변수의 용도를 간단히 설명합니다. State 변수는 기본적으로 IR_STANDBY 값을 가지고 있습니다. 첫 IR 신호가 들어와서 HAL_TIM_IC_CaptureCallback 함수가 호출되면 State 변수 값을 IR_DATA_IN으로 바꿉니다. HAL_TIM_IC_CaptureCallback 함수에서 이 변수의 값이 IR_DATA_IN인 동안 들어오는 IR 신호의 값을 IRQueue에 담아 놓습니다. IR 신호가 들어온 후에 30mS이 경과하도록 추가 신호가 들어오지 않으면, TIM1의 update 인터럽트가 발생합니다. TIM1의 update 인터럽트 서비스 루틴에서는 State 변수의 값을 IR_NO_MORE 값으로 바꾸어 놓습니다. 실제로 리모콘에서 들어온 신호를 분석하는 일은 CheckRemocon 함수에서 처리합니다. CheckRemocon 함수에서는 State의 값이 IR_NO_MORE일 때에만 리모콘 신호를 분석하도록 프로그래밍할 것입니다.
프로젝트에 infra.c 파일을 추가하고 다음과 같이 HAL_TIM_IC_CaptureCallback 함수를 만듭니다.
REMOCONCODE RemoconCode; IRQUEUE IRQueue; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { uint32_t value = 0; if(htim->Instance == TIM1) { if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { value = TIM1->CCR1; TIM1->CNT = 0; if(IRQueue.State == IR_STANDBY) { IRQueue.State = IR_DATA_IN; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, RESET); return; } if(IRQueue.State != IR_DATA_IN) return; IRQueue.QueueHigh[IRQueue.Head] = value; // //printfCDC("%4d: %5d %5d\n", IRQueue.Head, IRQueue.QueueLow[IRQueue.Head], // IRQueue.QueueHigh[IRQueue.Head] - IRQueue.QueueLow[IRQueue.Head]); // IRQueue.Head = ((IRQueue.Head + 1) % MAX_IR_QUEUESIZE); IRQueue.Data++; } else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) { IRQueue.QueueLow[IRQueue.Head] = TIM1->CCR2; } } }
위 함수에 관하여 간단히 설명합니다. Input Caputer 인터럽트가 발생한 경우에 TIM1에서 발생했는지 확인하여(5행) TIM1이 아니면 아무 작업도 하지 않습니다. Input Capture 인터럽트가 발생한 채널이 Channel1인지 확인합니다(6행). Channel1인 경우는 Falling Edge에서 발생한 인터럽트이므로 새로운 IR 신호가 들어오기 시작한 것입니다. 이전 신호의 길이를 보관하기 위해서 Channel1 카운터 값을 읽고(7행), 카운터에 0을 기록합니다(8행). 이때의 IRQueue.State의 값이 IR_STNADBY이면(9행) 첫 신호가 들어온 상태입니다. 첫 신호가 들어왔을 때에는 이전 신호가 없으므로 이때의 카운터 값은 의미 없는 값입니다. 따라서 이 값은 IRQueue에 저장하지 않고 return합니다(12행). 다만 다음부터 들어 오는 값은 유의미한 값이므로 데이터를 IRQueue에 저장할 수 있도록 IRQueue.State의 값을 IR_DATA_IN으로 바꾸어 놓습니다(10행). 이와함께 IR 신호가 입력되고 있다는 것을 표시하기 위해서 blue pill 보드의 LED를 켭니다(11행).
IRQueue.State의 값이 IR_DATA_IN일 때에만 IR 신호를 IRQueue.QueueHigh에 넣습니다(14, 15행). 현재 카운터 값을 IRQueue.QueueHigh[IRQueue.Head]에 넣고(16행) Head와 Data를 1씩 증가 시킵니다(21, 22행). Head의 값이 큐 영역을 벗어나면 큐의 처음을 가리키도록 하는 조치도 함께합니다(21행).
Input Capture 인터럽트가 발생한 채널이 Channel 2이면(24행) Channel2 카운터 값을 읽어 IRQueue.QueueLow[IRQueue.Head]에 저장합니다(25행). 이 때에는 새로운 비트가 시작한 것이 아니라 603LM의 신호가 low에서 high로 바뀌기만 한 것이므로 Head나 Data 값은 변경시키지 않습니다. (항상 603LM의 출력이 low가 될 때부터 IR 신호가 시작됩니다.)
Channel1에서 인터럽트가 발생했을 때에 즉, 603LM의 신호가 low로 변할 때에 카운터 값을 IRQueue.QueueHigh에 저장하고 카운터 값을 0으로 설정합니다. Channel2에서 인터럽트가 발생했을 때에는 카운터를 0 으로 설정하지 않고 있습니다. 따라서 IRQueue.QueueHigh에는 이전 신호의 전체 길이 값이 들어가고, IRQueue.QueueLow에는 이전 신호의 low 신호 길이 값이 들어갑니다.
Channel1과 Channel2의 카운터 값을 가져오는 HAL 함수는 HAL_TIM_ReadCapturedValue입니다. 이 함수는 Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c에 정의되어 있습니다. 내용을 보면 TIME과 Channel을 매개 변수로 받아, 이 값에 따라 카운터 레지스터 CCRx의 값을 읽어 리턴하고 있습니다. 인터럽트 서비스 루틴은 가급적 빨리 마치는 것이 좋으므로 본 글에서는 이 함수를 사용하지 않고 바로 레지스터의 값을 읽어 왔습니다(7행, 25행)
위 소스프로그램 중 주석으로 처리한 17행부터 20행은 신호를 잘 받고 있는지 디버깅할 목적으로 넣어 두었습니다.
다음은 TIM1의 update 인터럽트를 처리합니다. Src/stm32f1xx_it.c 파일 안에 TIM1_UP_IRQHandler 함수가 있습니다. 만약 이 함수가 없다면 프로젝트를 만들 때에 NVIC 설정에서 update interrupt에 체크하지 않았는지 살펴 보아야 합니다. 이 함수를 다음과 같이 수정합니다.
/* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "infrared.h" /* USER CODE END Includes */ void TIM1_UP_IRQHandler(void) { /* USER CODE BEGIN TIM1_UP_IRQn 0 */ if (RemoconQueue.State == IR_DATA_IN) { IRQueue.State = IR_NO_MORE; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, SET); } /* USER CODE END TIM1_UP_IRQn 0 */ HAL_TIM_IRQHandler(&htim1); /* USER CODE BEGIN TIM1_UP_IRQn 1 */ /* USER CODE END TIM1_UP_IRQn 1 */ }
매크로가 정의되지 않았다는 오류를 방지하기 위해서 infrared.h를 포함시켰습니다. TIM1_UP_IRQHandler에서는 IRQueue.State의 값이 IR_DATA_IN인 상태에서 30mS가 경과하도록 IR 신호가 들어오지 않으면 신호 입력이 종료된 것으로 판단하여 IRQueue.State에 IR_NO_MORE 값을 입력합니다(11행). 이어서 IR 신호 입력이 종료되었다는 것을 알리기 위해서 blue pill 보드의 LED를 끕니다(12행).
HAL_TIM_IC_CaptureCallback 함수에서 IR 신호를 입력 받아 low 상태의 길이와 high 상태의 길이를 비롯하여 데이터 수 등을 IRQueue에 보관하였습니다. 30mS 동안 IR 신호가 추가로 들어오지 않으면 TIM1_UP_IRQHandler 함수에서 입력이 종료되었음을 표시합니다.
다음 글에서는 실제로 IRQueue에 있는 데이터들을 이용해서 리모콘 신호를 분석하고, PC로부터 명령을 받아 IR 신호를 송출하는 루틴들을 만들어 보겠습니다.
'STM32F103' 카테고리의 다른 글
STM32F103으로 ESP8266을 이용한 소켓 통신하기 - 제1편 (2) | 2020.01.11 |
---|---|
STM32F103으로 IR 신호 다루기(2편) (3) | 2019.11.20 |
STM32F103 CDC 사용하기 (0) | 2018.12.28 |
STM32F103으로 ESP8266 제어하기 - USART 프로그래밍 (0) | 2018.12.25 |
미세먼지 센서 SDS011 사용하기(개선편) (0) | 2018.12.18 |