Contents
개요Day01 : HAL함수를 이용한 GPIO Port 제어1. 개요2. 레지스터를 사용한 GPIO Port 제어3. HAL 함수를 통해 GPIO Port 제어하기+헤더파일 구조Day02 : 주기적 버튼 신호 감지(Polling) + Systick1. 콜백 함수2. Edge Detection3. 폴링(Polling)4. Polling 방식 버튼 감지 구현Day03 : 인터럽트 방식 스레드 구현 + UART1. Startup Code2. 인터럽트(Interrupt)3. UART(Universal Asynchronous Receiver/Transmitter)4. 인터럽트 방식 스레드 구현adc.c, io.c, button.c수정Day01~02 app.c → polling.cuart.capp.cDay04 : CLI을 이용한 디버깅 + Timer1. Timer2. PWM3. CLI을 이용한 디버깅 및 타이머 활용cli.cled.ctimer.capp.c 수정Day05 : 프로그램 FREERTOS 상에 올리기1. RTOS (Real-Time Operating System)2. Task & Thread3. Event Flags를 이용한 스레드간 통신 (메세지 패싱 방식)Day06 : Message Queue를 이용한 Uart 데이터 입출력1. Message Queue2. Message Queue를 이용한 Uart 데이터 입출력Day07 : SLIP 통신 패킹 프로토콜1. SLIP (Serial Line Internet Protocol)2. WithRobot - SLIP 프로토콜로 패킹하기Day08 : LCD Device (HD44780U) + Mutex1. LCD Device (HD44780U)2. I2C 통신3. Mutex4. LCD Device 제어실습 코드목차
개요
- 저항과 캐퍼시터의 칩 통합
- 저항 및 캐퍼시터는 회로 설계에서 필수적인 부품으로, 일부 칩 내부에 통합하여 설계 간소화 및 성능 향상을 도모할 수 있습니다.
- 아두이노와 실제 제품 제작의 한계
- 아두이노는 프로토타이핑 도구로 유용하지만, 실제 제품 제작에는 성능, 안정성, 확장성 측면에서 제한이 있어 적합하지 않습니다.
1. 개발 환경 설정
- STM32CubeIDE와 Eclipse
- STM32CubeIDE는 STM32 마이크로컨트롤러 개발을 위한 통합 개발 환경으로, Eclipse 기반이지만 무거울 수 있습니다.
- NUCLEO-F429ZI 보드 선택
- NUCLEO-F429ZI는 STM32F4 시리즈의 기능을 활용할 수 있는 개발 보드로, 다양한 애플리케이션에 적합합니다.
2. 산업 분야 적용
- 항공, 군수: 고신뢰성 요구
- 자율주행: 실시간 처리 및 안정성
- 휴대폰: 저전력 및 고성능 요구
3. 마이크로프로세서와 마이크로컨트롤러
- 마이크로프로세서: Multi CPU
- 복잡한 연산을 처리하는 다중 CPU 기반 시스템으로 고성능이 요구되는 애플리케이션에 사용됩니다.
- 마이크로컨트롤러: Single CPU
- 단순한 제어 작업에 사용되며, 주로 가전제품과 같은 임베디드 시스템에 사용됩니다.
4. 설계 및 생산 고려사항
- 부품 생산 기간
- 부품의 생산 기간을 고려하여 설계를 진행합니다.
- 클럭 기준 동작
- 시스템의 모든 동작은 클럭 신호에 의해 제어됩니다.
- 실무 계산 및 장비 활용
- 실무에서는 대부분 장비를 사용하여 측정하며, 해석 능력은 필수입니다.
5. 참고 문서
- Reference manual와 Data sheet 차이
- Reference manual: 마이크로컨트롤러의 상세 기능 설명
- Data sheet: 전자 부품의 기술 사양 제공
6. 버스 시스템
- DMA2D
- Direct Memory Access를 통한 고속 데이터 전송
- 버스 매트릭스와 버스 아비터
- 버스 매트릭스: 다중 마스터-슬레이브 연결
- 버스 아비터: 버스 사용 권한 중재
7. 시리얼 통신
- SPI(Serial Peripheral Interface)
- 마이크로컨트롤러와 주변장치 간의 시리얼 통신 프로토콜
- High Impedance
- 저항이 매우 높은 상태로, 회로 절연을 의미
8. 소프트웨어 개발
- HAL API(Layer)
- 하드웨어 추상화 계층을 통해 하드웨어 독립적인 소프트웨어 개발 가능
- 부트로더
- 시스템 시작 시 펌웨어를 로드하고 실행
__attribute__((weak))
의 사용- 재정의 가능한 함수 정의에 사용
- CMSIS
- Cortex-M 마이크로컨트롤러의 표준 소프트웨어 프레임워크
assert_param
매크로- 매개변수 검증 및 오류 발생 시 프로그램 중단
- DMA(Direct Memory Access)
- CPU 개입 없이 메모리 간 데이터 전송
9. 메모리 관리
- 메모리 할당(Heap 영역 사용)
- 임베디드 시스템에서 직접 메모리 할당 함수 구현
- 부동소수점 설정
- 단정도 계산 설정 필요
10. 논리 회로
- NAND + NOT = AND
- 기본 논리 게이트 조합
11. 디바이스 드라이버 개발
- BSP에서의 개발
- Board Support Package를 통한 하드웨어 제어
- 데이터 시트 활용
- 필요한 부분 신속히 참조
- 표준 프로토콜에 맞춘 코딩
- 프로토콜 준수하여 안정성 확보
12. 기타
- CUDA
- GPU를 활용한 병렬 컴퓨팅 기술
Day01 : HAL함수를 이용한 GPIO Port 제어
1. 개요
HAL (Hardware Abstraction Layer) 함수
- HAL(Hardware Abstraction Layer) 함수는 하드웨어 독립적인 소프트웨어 개발을 가능하게 하는 계층입니다.
- HAL 라이브러리는 STM32CubeMX와 함께 사용되어 코드의 이식성을 높이고, 다양한 STM32 마이크로컨트롤러를 위한 공통 인터페이스를 제공합니다.
- HAL 함수는 하드웨어 레지스터를 직접 조작하는 대신, 고수준의 API를 통해 하드웨어 기능을 제어할 수 있도록 합니다.
- HAL 라이브러리의 주요 장점
- 이식성: 다양한 STM32 제품군 간 코드 재사용이 가능
- 유지보수성: 하드웨어 변경 시 코드 수정 최소화
- 생산성 향상: 복잡한 하드웨어 설정을 단순화
GPIO (General-Purpose Input/Output)
- GPIO는 마이크로컨트롤러에서 가장 기본적인 입출력 인터페이스입니다.
- GPIO 포트는 디지털 신호를 입력받거나 출력할 수 있으며, 다양한 주변 장치와의 상호작용에 사용됩니다.
- 각 GPIO 핀은 독립적으로 설정할 수 있으며, 입력 또는 출력 모드로 동작할 수 있습니다.
2. 레지스터를 사용한 GPIO Port 제어
CubeMX Configuration
- 제어하고자 하는 Pin 이름에 대한 Port 이름과 번호를 회로도에서 찾는다. (ex. LD1 → PB0)
- STM32CubeMX의 .ioc 파일에서 회로도에서 찾은 Port 이름과 번호를 GPIO로 설정한다.
- 데이터시트에서 해당 Port와 Pin의 시작 주소를 찾는다. (ex. 0x4002 0400)
- GPIO Port 시작 주소 찾기
- GPIO Pin 레지스터 찾기
특성 | AHB (Advanced High-performance Bus) | APB (Advanced Peripheral Bus) |
주 용도 | 고속 데이터 전송 | 저속 주변 장치 인터페이스 |
속도 | 고속 | 저속 |
복잡성 | 복잡함 (파이프라인 방식) | 단순함 (비파이프라인 방식) |
클럭 에지 | 단일 클럭 에지 | 단일 클럭 사이클 |
데이터 버스 폭 | 32비트 또는 64비트 | 8비트 또는 16비트 |
전력 소비 | 상대적으로 높음 | 낮음 |
마스터/슬레이브 | 다중 마스터 지원 | 단일 마스터 |
- 데이터시트에서 제어할 레지스터의 오프셋을 더한 후 직접 레지스터를 제어한다.
#define PORTB_BSRR_BASE_ADDR 0x40020418 #define LD1_BIT 0 #define LD2_BIT 7 #define LD3_BIT 14 #define MAX_LED 3 // ODR을 통한 제어 if(pin_num == LD1_BIT){ uint32_t bsrr_addr = PORTB_BSRR_BASE_ADDR; uint32_t bit = on_flag ? (0x1 << LD1_BIT) : (0x1 << (16 + LD1_BIT)); *(uint32_t*)bsrr_addr = bit; } else if(pin_num == LD2_BIT){ uint32_t bsrr_addr = PORTB_BSRR_BASE_ADDR; uint32_t bit = on_flag ? (0x1 << LD2_BIT) : (0x1 << (16 + LD2_BIT)); *(uint32_t*)bsrr_addr = bit; } else if(pin_num == LD3_BIT){ uint32_t bsrr_addr = PORTB_BSRR_BASE_ADDR; uint32_t bit = on_flag ? (0x1 << LD3_BIT) : (0x1 << (16 + LD3_BIT)); *(uint32_t*)bsrr_addr = bit; } else return; 0x00001 == 0x1 << 0; (uint32_t*)(PORTB_BASE + ODR_OFFSET) |= (0x1 << 0); (uint32_t*)(PORTB_BASE + ODR_OFFSET) |= (0x1 << 7); (uint32_t*)(PORTB_BASE + ODR_OFFSET) |= (0x1 << 14); // BSRR을 통한 제어 + 구조체,포인터로 간결하게 typedef struct{ uint32_t* bsrr_addr; uint16_t on; uint16_t off; } LED_T; const LED_T gLedObjs[MAX_LED]={ {(uint32_t*)PORTB_BSRR_BASE_ADDR, LD1_BIT, 16 + LD1_BIT}, {(uint32_t*)PORTB_BSRR_BASE_ADDR, LD2_BIT, 16 + LD2_BIT}, {(uint32_t*)PORTB_BSRR_BASE_ADDR, LD3_BIT, 16 + LD3_BIT} }; if(led_num >= MAX_LED) return; LED_T *p; p = (LED_T *)&gLedObjs[led_num]; *p->bsrr_addr = on_flag ? (0x1 << p->on) : (0x1 << p->off);
3. HAL 함수를 통해 GPIO Port 제어하기
HAL_GPIO_WritePin
if (on_flag){ HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); // LED 켜기 } else{ HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); // LED 끄기 }
HAL_GPIO_WritePin
함수는 특정 GPIO 포트와 핀의 출력 상태를 설정하거나 리셋하는 역할을 합니다.
main.c
에서 GPIO 포트와 핀 번호를 초기화해야 합니다.- 이를 위해
MX_GPIO_Init
함수를 호출하면 됩니다.
main.h
에 GPIO 포트와 핀 번호를 extern 변수로 선언해야 합니다.
HAL_GPIO_TogglePin
HAL_GPIO_TogglePin(LED_PORT, LED_PIN); // LED 상태 반전
HAL_GPIO_TogglePin
함수는 주어진 GPIO 포트와 핀 번호에 해당하는 핀의 상태를 반전시킵니다.
+헤더파일 구조
#include <stdbool.h> // 헤더 파일 중복 포함 방지를 위한 가드 #ifndef SRC_IO_H_ #define SRC_IO_H_ // C++ 컴파일러에서 해당 헤더 파일을 사용할 때를 대비 #ifdef __cplusplus extern "C" { #endif // 함수 프로토타입 선언 int __io_putchar(int ch); void led_onoff(bool on_flag, uint8_t led_num); void led_onoff2(bool on_flag, uint8_t led_num); // + 콜백 함수, 구조체 추가 // C++ 컴파일러에서 사용할 때를 대비한 닫는 중괄호 #ifdef __cplusplus } #endif // 헤더 파일 포함 가드의 닫는 부분 #endif /* SRC_IO_H_ */
Day02 : 주기적 버튼 신호 감지(Polling) + Systick
1. 콜백 함수
콜백 함수(Callback Function)
- 콜백 함수는 프로그램의 흐름을 제어하기 위해 다른 코드에 의해 호출되는 함수입니다.
- 임베디드 시스템에서 특히 중요하게 사용되며, 비동기 이벤트 처리와 실시간 응답을 가능하게 합니다.
- 콜백 함수의 특징
- 함수 포인터로 구현: 함수 포인터를 통해 전달되어 필요할 때 호출
- 비동기 처리: 비동기적인 이벤트 처리에 유용
- 플러그인 구조: 모듈 간의 의존성을 낮추고, 유연성을 높이는 플러그인 구조 구현 가능
- 콜백 함수의 주요 응용
- 비동기 이벤트 처리
- 다양한 외부 장치와 상호작용하며, 입력이나 상태 변화에 즉시 대응할 수 있습니다.
- 하드웨어 인터럽트 처리
- 인터럽트 발생 시 호출되는 콜백 함수로 신속한 응답이 가능합니다.
- 낮은 CPU 사용률
- 이벤트 발생 시에만 실행되어 대기 상태에서 CPU 사용률을 낮출 수 있습니다.
- 모듈화와 확장성
- 모듈 간 결합도를 낮추어 새로운 기능 추가 시 콜백 함수만 변경하면 되므로 확장성이 높아집니다.
- 실시간 처리
- 실시간 응답이 필요한 시스템에서 이벤트 발생 즉시 처리할 수 있습니다.
- 일반 함수와 콜백 함수의 차이
구분 | 일반 함수 | 콜백 함수 |
호출 방식 | 명시적으로 호출. | 특정 이벤트나 조건 발생 시 시스템에 의해 자동으로 호출. |
호출 시점 | 개발자가 원하는 시점에 호출. | 시스템의 특정 이벤트 발생 시 호출. |
실행 흐름 | 일반적인 함수 호출-실행-리턴 순서. | 외부 시스템에 의해 실행 흐름이 콜백 함수로 전달. |
종속성 | 함수 간 종속성이 높음. | 콜백 함수와 호출 측 함수 간 종속성이 낮음. |
유연성 | 결합도가 높아 변경이 어려움. | 결합도가 낮아 유연한 변경이 가능. |
2. Edge Detection
- 엣지는 신호가 변화하는 지점을 의미하며, 주로 두 가지 종류가 있습니다.
- Rising Edge(상승 엣지) : 신호가 낮은 상태 (Low)에서 높은 상태 (High)로 변화하는 지점.
- Falling Edge(하강 엣지) : 신호가 높은 상태 (High)에서 낮은 상태 (Low)로 변화하는 지점.
타이밍 다이어그램에서의 엣지
- ADC_CLK
- ADC 클럭 신호입니다. 이 신호는 ADC 변환을 동기화합니다. 클럭 신호가 일정한 주기로 변화하며, 이를 통해 ADC가 변환 타이밍을 잡습니다.
- ADON
- ADC가 켜지는 신호입니다. 이 신호가 High로 변할 때 ADC가 활성화되며, 이는 Rising Edge로 나타납니다.
- SWSTART/JSWSTART
- 소프트웨어 시작 신호입니다. 이 신호가 Low에서 High로 변할 때 (Rising Edge), ADC는 첫 번째 변환을 시작합니다.
- ADC
- ADC 변환이 진행되는 동안의 신호입니다. SWSTART의 Rising Edge 이후, ADC 변환이 시작됩니다.
- EOC (End of Conversion)
- 변환이 완료되었음을 나타내는 신호입니다. 이 신호가 High로 변할 때 (Rising Edge), 변환이 완료된 것으로 간주됩니다.
3. 폴링(Polling)
- 폴링 방식은 CPU가 주기적으로 특정 조건이나 상태를 확인하여 원하는 이벤트가 발생했는지 확인하는 방식입니다.
- 폴링 방식은 비블로킹(non-blocking) 방식으로, 다른 작업을 수행하면서 주기적으로 조건을 확인할 수 있습니다.
구분 | HAL_Delay 방식 | Polling 방식 |
블로킹 여부 | 블로킹 (Blocking) | 비블로킹 (Non-blocking) |
CPU 사용 효율 | 낮음 (지연 동안 CPU 유휴 상태) | 높음 (다른 작업과 병행 가능) |
구현 복잡도 | 낮음 (간단한 사용) | 높음 (주기적인 상태 확인 로직 필요) |
응답성 | 낮음 (지연 시간 동안 응답 없음) | 높음 (다른 이벤트에 즉시 반응 가능) |
전력 소비 | 높음 (CPU 유휴 상태) | 낮음 (CPU가 다른 작업 수행 가능) |
- 현대 운영체제에서 사용하는 Interrupt를 사용한 방식이나 DMA를 이용한 방식에 비해서 비효율적인 방식입니다.
4. Polling 방식 버튼 감지 구현
button.c
button.c
- 구조체와 함수 정의
- BUTTON_T 구조체
- 버튼의 GPIO 포트와 핀, 폴링 주기, 카운터, 현재 및 이전 상태, 콜백 함수 등을 저장
- BUTTON_STS 구조체: 버튼의 상태(눌림, 떼짐)를 저장
- FUNC_CBF 타입: 콜백 함수 포인터 타입
- 초기화와 콜백 함수 등록
// 버튼 객체 배열 static BUTTON_T gBtnObjs[] = { {USER_Btn_GPIO_Port, USER_Btn_Pin, 80, 0, 0, 0, button_dummy, {true, 0}}, {NULL , 0 , 0 , 0, 0, 0, NULL , {true, 0}} }; // 버튼 드라이버 초기화 함수 void button_init(void) { // 초기화 할 내용이 없음 } // 버튼 이벤트 콜백 함수 등록 함수 void button_regcbf(uint16_t idx, FUNC_CBF cbf) { gBtnObjs[idx].cbf = cbf; }
- 버튼 상태 체크 함수
void button_check(void) { BUTTON_T *p = &gBtnObjs[0]; // 버튼 객체 배열 순회 for(uint8_t i=0; p->cbf != NULL; i++){ p->count++; // 버튼 체크 카운터 증가 p->count %= p->period; // 주기로 나눈 나머지 저장 // 주기(period)마다 버튼 상태 체크 if(p->count == 0){ // 현재 버튼 상태 읽기 p->curr = HAL_GPIO_ReadPin(p->port, p->pin); // 버튼 상태 변화 감지 if(p->prev == 0 && p->curr == 1){ // 눌림 이벤트 p->sts.edge = true; p->sts.pushed_count = 0; p->cbf((void *)&(p->sts)); // 콜백 함수 호출 } else if(p->prev == 1 && p->curr == 0){ // 떼짐 이벤트 p->sts.edge = false; p->cbf((void *)&(p->sts)); } else if(p->prev == 1 && p->curr == 1){ // 누르고 있음 if(p->sts.pushed_count < 100) p->sts.pushed_count++; } else p->sts.pushed_count = 0; p->prev = p->curr; // 이전 상태 업데이트 } p++; // 다음 버튼 객체로 이동 } } // 빈 콜백 함수 static void button_dummy(void *) { return; }
- 주기적으로 버튼의 현재 상태를 읽고, 이전 상태와 비교하여 변화가 있는지 확인
- 버튼이 눌리거나 떼어질 때, 콜백 함수를 호출하여 해당 이벤트를 처리합니다.
- 눌림, 떼짐, 누르고 있는 상태를 모두 처리합니다.
adc.c
adc.c
- 구조체와 함수 정의
- ADC_T 구조체: ADC 값과 콜백 함수 포인터를 포함합니다.
- ADC_CBF 타입: ADC 콜백 함수 포인터 타입입니다.
- 초기화와 콜백 함수 등록
#include <stdio.h>#include "main.h"#include "adc.h"#include <stdbool.h>static void adc_dummy(void *); // 경고 없애기 위해 static ADC_T gAdcObjs[] = { {.cbf = adc_dummy}, {.cbf = NULL } }; // ADC 드라이버 초기화 함수 void adc_init(void) { // 초기화 할 내용이 없음 } // ADC 이벤트 콜백 함수 등록 함수 void adc_regcbf(uint16_t idx, ADC_CBF cbf) { gAdcObjs[idx].cbf = cbf; }
- ADC 값 체크 함수
void adc_check(void) { static uint16_t value = 100; for(int i = 0; gAdcObjs[i].cbf != NULL; i++){ value += 100; // ADC 값 증가 (테스트용) gAdcObjs[i].value = value; // ADC 객체의 값 업데이트 gAdcObjs[i].cbf((void *)&gAdcObjs[i].value); // 콜백 함수 호출 } } // 빈 콜백 함수 static void adc_dummy(void *) { return; }
app.c
app.c
- 콜백 함수 정의
// 버튼 콜백 함수 1 void button_callback(void *arg) { // 정적 카운터 변수 static uint8_t count = 0; // 버튼 상태 정보 구조체 포인터 BUTTON_STS *pSts = (BUTTON_STS *)arg; // 버튼이 눌렸을 때(Rising) if (pSts->edge == true) printf("Rising!\r\n"); // 버튼이 떼어졌을 때(Falling) else if (pSts->edge == false) printf("Falling! : period = %d\r\n", pSts->pushed_count); // 카운터 값 출력 printf("1. count = %d\r\n", count); // 카운터 증가 count++; count %= 10; // 카운터가 10이 되면 if (count == 10) { // 콜백 함수를 button_callback2로 변경 button_regcbf(0, button_callback2); printf("cbf changed to callback2!\r\n"); } } // 버튼 콜백 함수 2 void button_callback2(void *arg) { // 정적 카운터 변수 static uint8_t count = 0; // 버튼 상태 정보 구조체 포인터 BUTTON_STS *pSts = (BUTTON_STS *)arg; // 버튼이 눌렸을 때(Rising) if (pSts->edge == true) printf("Rising!\r\n"); // 버튼이 떼어졌을 때(Falling) else if (pSts->edge == false) printf("Falling! : period = %d\r\n", pSts->pushed_count); // 카운터 값 출력 printf("2. count = %d\r\n", count); // 카운터 증가 count++; count %= 10; // 카운터가 10이 되면 if (count == 10) { // 콜백 함수를 button_callback으로 변경 button_regcbf(0, button_callback); printf("cbf changed to callback!\r\n"); } } // ADC 콜백 함수 void adc_callback(void *arg) { printf("adc value = %d\r\n",*(uint16_t *)arg); }
- 스레드 구조체 정의와 초기화
typedef struct{ uint32_t period, count; // 주기 및 카운트 변수 bool flag; // 플래그 변수 void (*cbf)(void); // 콜백 함수 포인터 } THR_T; // 스레드 객체 배열 초기화 THR_T gThrObjs[] = { {.period = 1, .count = 0, .flag = false, .cbf = button_check}, {.period = 500, .count = 0, .flag = false, .cbf = adc_check }, {.period = 0, .count = 0, .flag = false, .cbf = NULL } }; // 초기화 함수 static void init(void) { // 버튼 드라이버 초기화 button_init(); // 버튼 콜백 함수 등록 (button_callback) button_regcbf(0, button_callback); // ADC 초기화 및 콜백 함수 등록 adc_init(); adc_regcbf(0, adc_callback); }
period
: 작업이 실행되는 주기.count
: 현재 카운트 값.flag
: 작업이 실행될 때 true로 설정되는 플래그.cbf
: 작업이 실행될 때 호출되는 콜백 함수 포인터.- 애플리케이션 메인 함수
void app(void) { uint32_t thr_idx = 0; // 스레드 인덱스 변수 uint32_t tick_prev, tick_curr; // 시간 측정 변수 // 부팅 메시지 출력 printf("booting\r\n"); // 초기화 함수 호출 init(); // 시간 측정 변수 초기화 tick_prev = tick_curr = HAL_GetTick(); // 무한 루프 while (1) { // 현재 시간 측정 tick_curr = HAL_GetTick(); // 1ms 단위로 시간 체크 if (tick_curr - tick_prev >= 1){ // 스레드 객체 배열 순회 for(int i = 0; gThrObjs[i].cbf != NULL; i++){ gThrObjs[i].count++; // 카운터 증가 // 주기로 나눈 나머지 저장 gThrObjs[i].count %= gThrObjs[i].period; // 주기 도달 시 플래그 설정 if(gThrObjs[i].count == 0) gThrObjs[i].flag = true; } // 이전 시간 업데이트 tick_prev = tick_curr; } // 현재 스레드의 플래그가 설정되었는지 확인 if(gThrObjs[thr_idx].flag == true){ gThrObjs[thr_idx].flag = false; // 플래그 초기화 gThrObjs[thr_idx].cbf(); // 콜백 함수 호출 } // 다음 스레드로 이동 thr_idx++; // 마지막 스레드 이후 첫 번째 스레드로 이동 if(gThrObjs[thr_idx].cbf == NULL) thr_idx = 0; } }
- 1ms 단위로 현재 시간을 체크하여, 주기가 만료된 작업이 있는지 확인합니다.
- 현재 시간 (
tick_curr
)과 이전 시간 (tick_prev
)을 비교하여 1ms가 경과하면, 스레드 구조체의 카운터를 업데이트합니다.
- 주기가 만료된 작업은 플래그를 설정하고, 설정된 플래그를 확인하여 해당 작업의 콜백 함수를 호출합니다.
- 모든 작업을 처리한 후, 루프를 반복하여 지속적으로 작업을 확인하고 실행합니다.
전체적인 구조 및 흐름
- 초기화
app.c
에서button_init()
과adc_init()
함수를 호출하여 버튼과 ADC 드라이버를 초기화합니다.button_regcbf()
와adc_regcbf()
함수를 통해 버튼과 ADC의 콜백 함수를 등록합니다.
- 주기적인 체크
app.c
의 메인 루프에서 주기적으로button_check()
와adc_check()
함수를 호출하여 버튼과 ADC 값을 확인하고 업데이트합니다.- 각 함수는 버튼과 ADC의 상태를 확인하고, 등록된 콜백 함수를 호출하여 이벤트를 처리합니다.
- 이벤트 처리
- 버튼 상태 변화와 ADC 값 변경 시 등록된 콜백 함수가 호출되어 해당 이벤트를 처리합니다.
- 콜백 함수는 버튼이 눌리거나 떼어질 때, 그리고 ADC 값이 변경될 때 적절한 처리를 수행합니다.
- 반복 실행
- 시스템은 메인 루프를 통해 지속적으로 버튼과 ADC 상태를 체크하고, 주기적으로 작업을 실행합니다.
- 이를 통해 버튼과 ADC 이벤트를 실시간으로 처리하고, 다른 작업들도 주기적으로 실행될 수 있도록 관리합니다.
레이어
+---------------------+ | Application Layer | | (app.c) | +---------------------+ | Driver Layer | | (button.c, adc.c) | +---------------------+ | HAL Layer | | (main.h, hal_gpio.h| | hal_adc.h, etc.) | +---------------------+ | Hardware Layer | +---------------------+
Day03 : 인터럽트 방식 스레드 구현 + UART
1. Startup Code
- startup code는 MCU가 전원을 켜거나 리셋될 때 실행되는 초기화 코드를 말합니다.
- 이 코드는 MCU의 초기 상태를 설정하고 메인 애플리케이션 코드가 실행되기 전에 필요한 모든 준비 작업을 수행합니다.
startup_stm32f429zitx.s - Startup code
- Reset Vector
- MCU가 리셋될 때 실행을 시작하는 주소입니다. 일반적으로 이 주소에는 startup code의 시작 지점을 가리키는 포인터가 저장됩니다.
.section .isr_vector,"a",%progbits .type g_pfnVectors, %object g_pfnVectors: .word _estack .word Reset_Handler ; ... (other exception and interrupt vectors)
- Stack Pointer 초기화
- 초기화 코드는 스택 포인터를 초기화하여 스택이 사용할 메모리 공간을 설정합니다.
Reset_Handler: ldr sp, =_estack /* set stack pointer */
- Data Section 초기화
- 초기화되지 않은 변수(Zero-initialized data)와 초기화된 변수(Data)가 저장될 메모리 영역을 설정합니다.
- 초기화된 변수는 플래시 메모리에서 RAM으로 복사되고, 초기화되지 않은 변수는 0으로 설정됩니다.
/* Copy the data segment initializers from flash to SRAM */ ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: adds r4, r0, r3 cmp r4, r1 bcc CopyDataInit
- BSS Section 초기화
- BSS 영역은 초기화되지 않은 전역 및 정적 변수를 저장하는 메모리 공간입니다.
- 이 영역은 초기화 코드에서 0으로 채워집니다.
/* Zero fill the bss segment. */ ldr r2, =_sbss ldr r4, =_ebss movs r3, #0 b LoopFillZerobss FillZerobss: str r3, [r2] adds r2, r2, #4 LoopFillZerobss: cmp r2, r4 bcc FillZerobss
- C 런타임 환경 설정
- C/C++ 프로그램이 올바르게 실행되기 위해 필요한 환경을 설정합니다.
- 예를 들어, 전역 및 정적 생성자를 호출하거나, C++ 객체를 초기화합니다.
/* Call static constructors */ bl __libc_init_array
- Interrupt Vector Table 설정
- 인터럽트 벡터 테이블을 설정하여 각 인터럽트가 발생할 때 실행될 핸들러 주소를 지정합니다.
- 이 테이블은 일반적으로 고정된 위치에 저장되며, 각 인터럽트 요청에 해당하는 핸들러를 지정합니다.
.section .isr_vector,"a",%progbits .type g_pfnVectors, %object g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler ; ... (other exception and interrupt vectors)
- Main 함수 호출
- 모든 초기화 작업이 완료되면, startup code는 메인 애플리케이션 코드의 시작점인
main()
함수를 호출합니다. - 이 시점부터 애플리케이션 코드가 실행을 시작합니다.
/* Call the application's entry point. */ bl main bx lr
- 기타 초기화 작업
- MCU에 따라 추가적으로 필요한 초기화 작업이 있을 수 있습니다.
- 예를 들어, 클럭 설정, GPIO 초기화, 주변 장치 설정 등이 있을 수 있습니다.
- 이 코드에서는
SystemInit
함수가 클럭 설정 등의 시스템 초기화를 담당합니다.
/* Call the clock system initialization function. */ bl SystemInit
BIOS(Basic Input/Output System) & 부트로더(Boot Loader)
- BIOS는 컴퓨터가 켜졌을 때 가장 먼저 실행되며, 하드웨어 초기화 및 운영 체제를 로드하는 과정을 담당합니다.
- 부트로더는 시스템 시작 시 가장 먼저 실행되며, 메인 애플리케이션 코드가 실행되기 전에 다양한 초기화 및 준비 작업을 수행합니다.
BIOS, 부트로더, Startup Code의 차이
항목 | BIOS | 부트로더(Bootloader) | Startup Code |
목적 | 하드웨어 초기화 및 부트로더 실행 | 펌웨어 업데이트, 검증, 애플리케이션 선택 및 실행 | 기본 초기화 및 애플리케이션 실행 준비 |
위치 | 마더보드의 플래시 메모리 | 독립적인 메모리 섹션 (주로 플래시 메모리) | 애플리케이션 코드의 시작 부분 |
초기화 작업 | 전반적인 하드웨어 초기화, POST 수행 | 기본 하드웨어 초기화, 클럭 설정, GPIO 설정 등 | 스택 포인터 초기화, 데이터/BSS 섹션 초기화, 인터럽트 벡터 테이블 설정 |
추가 기능 | 부팅 순서 설정, 기본 입출력 기능 제공 | 펌웨어 업데이트, 보안 부팅, 디버깅 등 | 없음 |
실행 시점 | 전원 켜짐 시, 가장 먼저 실행 | MCU 리셋 또는 전원 켜짐 시, startup code 실행 전에 | MCU 리셋 또는 전원 켜짐 시 |
종료 시점 | 부트로더 호출 후 종료 | 애플리케이션 코드로 점프 후 종료 | main() 함수 호출 후 종료 |
- 컴퓨터의 부팅 과정
2. 인터럽트(Interrupt)
- 인터럽트(Interrupt)는 컴퓨터 시스템에서 중요한 개념으로, 현재 실행 중인 작업을 잠시 멈추고 다른 중요한 작업을 처리한 후 다시 원래 작업으로 복귀하도록 하는 메커니즘입니다.
- 인터럽트는 하드웨어와 소프트웨어에서 모두 발생할 수 있으며, 시스템의 효율성과 반응성을 크게 향상 시킵니다.
인터럽트의 종류
- 하드웨어 인터럽트
- 키보드를 누르거나 마우스를 클릭하는 것, 네트워크 패킷 수신 등이 있습니다.
- 소프트웨어 인터럽트
- 시스템 호출이나 예외 상황(예: 분할 오류, 페이지 폴트) 등이 있습니다.
인터럽트 처리 과정
- 인터럽트 발생
- 하드웨어 장치나 소프트웨어 명령이 인터럽트를 발생시킵니다.
- 현재 작업 중단
- CPU는 현재 실행 중인 작업을 중단하고, 인터럽트 요청을 확인합니다.
- 인터럽트 벡터 테이블 참조
- 인터럽트 처리 루틴의 주소를 저장한 인터럽트 벡터 테이블을 참조합니다.
- 인터럽트 서비스 루틴(ISR) 실행
- 인터럽트 벡터 테이블에 정의된 ISR을 실행하여 인터럽트를 처리합니다.
- 원래 작업 복귀
- ISR 실행이 완료되면 CPU는 원래의 작업으로 복귀합니다.
인터럽트와 폴링 비교
항목 | 인터럽트(Interrupt) | 폴링(Polling) |
작동 방식 | 이벤트가 발생할 때 시스템이 이를 처리하도록 중단 | 주기적으로 상태를 확인하여 이벤트를 처리 |
CPU 사용 효율성 | 높음
이벤트가 발생할 때만 CPU가 사용됨 | 낮음
CPU가 주기적으로 상태를 확인하며 자원 소모 |
반응 시간 | 빠름
이벤트 발생 시 즉시 처리 | 느림
폴링 주기에 따라 지연될 수 있음 |
구현 복잡성 | 비교적 복잡
인터럽트 처리기 작성 필요 | 비교적 단순
상태 확인 루프 작성 필요 |
시스템 부하 | 낮음
필요할 때만 처리 | 높음
주기적으로 상태를 확인해야 함 |
전력 소모 | 낮음
유휴 상태에서 대기 가능 | 높음
계속해서 상태를 확인해야 함 |
사용 예 | 실시간 시스템, 키보드 입력 처리, 네트워크 패킷 수신 | 간단한 장치 상태 확인, 주기적인 센서 데이터 수집 |
3. UART(Universal Asynchronous Receiver/Transmitter)
- UART는 두 장치 간 비동기 직렬 통신을 지원하는 방식입니다.
주요 구성 요소
- 송신기 (Transmitter): 데이터를 직렬로 전송.
- 수신기 (Receiver): 직렬 데이터를 병렬 데이터로 변환하여 수신.
주요 특성
- 비동기 통신: 클럭 신호를 공유하지 않으며, 시작 비트와 정지 비트로 동기화.
- 데이터 프레임
- 1비트의 시작 비트
- 5-9비트의 데이터 비트
- 선택적 패리티 비트
- 1-2비트의 정지 비트
- 패리티 비트: 오류 검출용 (홀수/짝수 패리티).
- 보드레이트(Baud Rate): 초당 전송되는 비트 수, 송신기와 수신기가 동일하게 설정.
- 전이중 방식(Full Duplex): 동시에 송수신 가능.
장점과 단점
- 장점
- 간단한 하드웨어 구성
- 소프트웨어 구현 용이
- 다양한 보드레이트 지원
- 단점
- 낮은 통신 속도
- 긴 거리 통신에 부적합
- 클럭 신호 부재로 인한 동기화 어려움
STM32 NUCLEO-F429XX
CubeMX Configuration
캡처 후 추가
4. 인터럽트 방식 스레드 구현
Day01~02 app.c
→ polling.c
app.c
→ polling.c
- 콜백 함수 정의
// ADC 콜백 함수 1 static void adc_callback(void *arg) { printf("adc[0] value = %d\\r\\n", *(uint16_t *)arg); } // ADC 콜백 함수 2 static void adc_callback_2(void *arg) { printf("adc[1] value = %d\\r\\n", *(uint16_t *)arg); }
- 스레드 구조체 정의와 초기화
typedef struct { uint32_t period; // 주기 uint32_t count; // 카운트 bool flag; // 플래그 void (*cbf)(void *); // 콜백 함수 포인터 } THR_T; // 스레드 타입 volatile THR_T gThrObjs[] = { { .period = 500, .count = 0, .flag = false, .cbf = adc_callback }, { .period = 1500, .count = 0, .flag = false, .cbf = adc_callback_2 }, { .period = 0, .count = 0, .flag = false, .cbf = NULL } }; // 초기화 함수 void polling_init(void) { adc_init(); adc_regcbf(0, adc_callback); }
- 폴링 스레드 함수
void polling_thread(void *arg) { static uint16_t thr_idx = 0; if (gThrObjs[thr_idx].flag == true) { gThrObjs[thr_idx].flag = false; gThrObjs[thr_idx].cbf(NULL); } thr_idx++; if (gThrObjs[thr_idx].cbf == NULL) thr_idx = 0; }
- 폴링 업데이트 함수
// io.c 파일 HAL_IncTick() 함수에서 호출 // HAL_IncTick() 함수는 systick irq handler에서 호출됨(인터럽트 서비스 루틴임) // 1ms 마다 호출됨 void polling_update(void) { for (int i = 0; gThrObjs[i].cbf != NULL; i++) { gThrObjs[i].count++; gThrObjs[i].count %= gThrObjs[i].period; if (gThrObjs[i].count == 0) gThrObjs[i].flag = true; } }
- 변경점
- 콜백 함수 등록 및 변경
- 버튼 콜백 함수
button_callback
과button_callback2
가 정의되어 카운터에 따라 콜백 함수가 변경됩니다. - ADC 콜백 함수
adc_callback
이 추가되었습니다. - 스레드 구조체 정의 및 초기화
THR_T
구조체가 정의되고gThrObjs
배열이 초기화되었습니다.- 스레드 배열에 주기, 카운트, 플래그 및 콜백 함수 포인터가 포함되었습니다.
- 초기화 함수 추가
init
함수가 추가되어 버튼과 ADC를 초기화하고, 콜백 함수를 등록합니다.- 애플리케이션 메인 함수 변경
- 무한 루프가 1ms 단위로 현재 시간을 체크하고, 주기가 만료된 작업을 확인합니다.
- 스레드 배열을 순회하면서 각 스레드의 카운트를 업데이트하고, 주기가 다 된 스레드는 플래그를 설정합니다.
- 플래그가 설정된 스레드의 콜백 함수를 호출하고, 다음 스레드로 이동합니다.
uart.c
uart.c
- 주요 구조체 및 정의
#include <stdbool.h> #include <stdio.h> #include "uart.h" extern UART_HandleTypeDef huart3; // 외부 UART 핸들러 선언 static uint8_t rxdata[1]; // 수신 데이터 버퍼 #define D_BUF_OBJ_MAX 3 // 최대 버퍼 객체 수 #define D_BUF_MAX 100 // 버퍼 크기 typedef struct { uint8_t buf[D_BUF_MAX + 1]; // 데이터 저장 버퍼 (+1은 NULL 문자 저장 공간) uint8_t idx; // 현재 버퍼 인덱스 uint8_t flag; // '\\r' 또는 '\\n' 수신 여부 플래그 } BUF_T; static BUF_T gBufObjs[D_BUF_OBJ_MAX]; // 버퍼 객체 배열 static void (*uart_cbf)(void *); // UART 콜백 함수 포인터
- UART 초기화 함수
void uart_init(void) { // 모든 버퍼 객체 초기화 for (int i = 0; i < D_BUF_OBJ_MAX; i++) { gBufObjs[i].idx = 0; gBufObjs[i].flag = false; } // 인터럽트 방식으로 1바이트 수신 시작 HAL_UART_Receive_IT(&huart3, (uint8_t *)&rxdata[0], 1); }
gBufObjs
배열의 모든 인덱스와 플래그를 초기화합니다.HAL_UART_Receive_IT
함수를 호출하여 UART의 인터럽트 기반 1바이트 수신을 시작합니다.
- UART 콜백 함수 등록 함수
void uart_regcbf(void (*cbf)(void *)) { uart_cbf = cbf; // UART 콜백 함수 등록 }
- UART 스레드 함수
void uart_thread(void *arg) { // 모든 버퍼 객체를 순회하며 플래그가 설정된 객체를 찾음 for (int i = 0; i < D_BUF_OBJ_MAX; i++) { if (gBufObjs[i].flag == true) { // 플래그가 설정된 버퍼 객체에 대해 콜백 함수 호출 if (uart_cbf != NULL) uart_cbf((void *)&gBufObjs[i]); gBufObjs[i].idx = 0; // 인덱스 초기화 gBufObjs[i].flag = false; // 플래그 초기화 } } }
gBufObjs
배열을 순회하여 플래그가 설정된 버퍼 객체를 찾습니다.- 플래그가 설정된 버퍼 객체에 대해 등록된 콜백 함수
uart_cbf
를 호출합니다. - 콜백 함수 호출 후 해당 버퍼 객체의 인덱스와 플래그를 초기화합니다.
- UART 인터럽트 서비스 루틴
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { volatile uint8_t rxd; if (huart == &huart3) { rxd = rxdata[0]; // 수신된 데이터 저장 // 다음 데이터 수신 준비 HAL_UART_Receive_IT(huart, (uint8_t *)&rxdata[0], 1); BUF_T *p = (BUF_T *)&gBufObjs[0]; // 첫 번째 버퍼 객체 사용 if (p->flag == false) { p->buf[p->idx] = rxd; // 수신된 데이터 버퍼에 저장 if (p->idx < D_BUF_MAX) p->idx++; // 인덱스 증가 // 수신된 데이터가 '\\r' 또는 '\\n'일 경우 if (rxd == '\\r' || rxd == '\\n') { p->buf[p->idx] = 0; // 버퍼에 NULL 문자 추가 p->flag = true; // 플래그 설정 } } } }
rxdata
에 저장된 수신 데이터를rxd
변수에 복사합니다.- 다음 1바이트를 계속 수신하기 위해
HAL_UART_Receive_IT
함수를 다시 호출합니다. gBufObjs
배열의 첫 번째 버퍼 객체를 가리키는 포인터p
를 사용하여 수신된 데이터를 저장합니다.- 플래그가 설정되지 않은 경우에만 데이터를 버퍼에 저장합니다.
- 버퍼 인덱스를 증가시키고, 수신한 데이터가 개행 문자일 경우 플래그를 설정하여 데이터 수신이 완료되었음을 나타냅니다.
app.c
app.c
- 여러 인터럽트 처리를 위한 스레드 함수 분리
/* * app.c * * Created on: Apr 11, 2024 * Author: IOT */ #include <stdio.h> #include "main.h" #include "polling.h" #include "button.h" #include "adc.h" #include "io.h" #include "uart.h" // 애플리케이션 초기화 함수 void app_init(void); // 애플리케이션 메인 함수 void app(void) { // 부팅 메시지 출력 printf("booting\r\n"); // 애플리케이션 초기화 함수 호출 app_init(); // 메인 루프 시작 while (1) { button_thread(NULL); // 버튼 스레드 함수 호출 polling_thread(NULL); // 폴링 스레드 함수 호출 uart_thread(NULL); // UART 스레드 함수 호출 // 이벤트는 인터럽트 핸들러에서 처리됩니다 } } // 애플리케이션 초기화 함수 void app_init(void) { io_exti_init(); // EXTI(외부 인터럽트) 초기화 polling_init(); // 폴링 초기화 button_init(); // 버튼 초기화 uart_init(); // UART 초기화 }
전체적인 구조 및 흐름
- 초기화
app.c
에서io_exti_init()
,polling_init()
,button_init()
,uart_init()
함수를 호출하여 EXTI, 폴링, 버튼, UART 드라이버를 초기화합니다.- 각 초기화 함수에서 필요한 콜백 함수들을 등록합니다.
- 주기적인 체크
app.c
의 메인 루프에서 주기적으로button_thread()
,polling_thread()
,uart_thread()
함수를 호출하여 버튼, ADC, UART 상태를 확인하고 업데이트합니다.- 각 스레드 함수는 주기적으로 상태를 확인하고, 설정된 조건에 따라 콜백 함수를 호출하여 이벤트를 처리합니다.
- 이벤트 처리
- 버튼 상태 변화, ADC 값 변경, UART 데이터 수신 시 등록된 콜백 함수가 호출되어 해당 이벤트를 처리합니다.
- 콜백 함수는 버튼이 눌리거나 떼어질 때, ADC 값이 변경될 때, UART 데이터가 수신될 때 적절한 처리를 수행합니다.
- 반복 실행
- 시스템은 메인 루프를 통해 지속적으로 버튼, ADC, UART 상태를 체크하고, 주기적으로 작업을 실행합니다.
- 이를 통해 버튼, ADC, UART 이벤트를 실시간으로 처리하고, 다른 작업들도 주기적으로 실행될 수 있도록 관리합니다.
Day04 : CLI을 이용한 디버깅 + Timer
1. Timer
- 타이머는 클록 신호를 받아 카운터와 비교 레지스터 등을 이용해 주기적인 인터럽트를 발생시키는 장치입니다.
타이머의 주요 구성 요소 및 역할
Prescaler (PSC) - 16비트
- 역할 및 필요성
- 타이머의 입력 클럭 주파수를 낮추는 레지스터
- CPU의 클럭 주파수가 너무 높아서 전처리를 통해 클럭을 낮춰야 할 때 사용
- 예를 들어, SYSCLK이 168MHz라면 168마이크로초마다 1번의 클럭을 발생시키는데, 이를 조정하기 위해 사용
- 동작 방식
- 0에서 65535 사이의 값을 이용하여 입력 클럭을 나눔
- 0으로 나눌 수 없으므로 초기값에 1이 더해짐. 예를 들어, 84로 나누기 위해서는 83을 입력
- 이렇게 생성된 클럭이 카운터의 동작 클럭 (CK_CNT)이 됨
- 역할:
- 타이머 카운터의 증가 속도를 조절
- 타이머 주기 설정 범위를 확대
- 타이머의 분해능(resolution)을 개선
Auto Reload Register (ARR) - 16비트 → 높낮이/명암 변화 주기
- 역할
- 0에서 65535 사이의 값을 입력 가능
- 업 카운터의 경우, CNT가 ARR 값에 도달하면 0으로 돌아가며 반복
- 다운 카운터의 경우, CNT가 0에 도달하면 ARR 값까지 카운트하며 반복
- 업-다운 카운터의 경우, CNT가 ARR 값에 도달하면 감소하고, 0에 도달하면 다시 증가하며 반복
- 업데이트 이벤트 발생 조건
- 업 카운터: CNT가 0이 될 때 오버플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생
- 다운 카운터: CNT가 ARR 값이 될 때 언더플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생
- 업-다운 카운터: CNT가 0 또는 ARR 값이 될 때 오버플로우/언더플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생
Capture/Compare Register (CCR) → 크기/밝기
- 카운터 모드: 출력 비교 모드(Output Compare: OC)
- CNT의 출력 값이 CCR(1~4)와 일치할 때 출력 발생
- 종류: active, inactive, toggle, forced(active/inactive), timing
- active: CNT = CCR이면 OC = High
- inactive: CNT = CCR이면 OC = Low
- toggle: CNT = CCR이면 OC 값이 토글
- forced: CNT 값과 상관없이 High 또는 Low
- CCR 값이 영향이 없으면 일반적인 타이머와 동일
- 카운터 모드: PWM 출력 모드
- PWM mode 1
- 업 카운팅: CNT가 CCR보다 작으면 active, 크거나 같으면 inactive
- 다운 카운팅: CNT가 CCR보다 작거나 같으면 active, 크면 inactive
- PWM mode 2
- 업 카운팅: CNT가 CCR보다 작으면 inactive, 크거나 같으면 active
- 다운 카운팅: CNT가 CCR보다 작거나 같으면 inactive, 크면 active
2. PWM
- PWM은 출력 신호의 active와 inactive 비율을 조절하여 모터 속도, 각도, LED 밝기 등을 제어하는 기술입니다.
- PWM을 통해 주파수(ARR 값)와 듀티 사이클(CCR 값)을 조절하여 다양한 장치의 속도, 각도, 밝기 등을 제어할 수 있습니다.
PWM 제어 원리
- PWM 제어의 의미
- PWM을 제어한다는 것은 출력 신호의 HIGH 상태와 LOW 상태의 비율(duty cycle)을 조절하는 것을 의미합니다.
- 이를 통해 모터의 속도, 각도, LED 밝기 등 다양한 애플리케이션에서 제어가 가능합니다.
- PWM 제어 방법
- 주파수 설정 (ARR 값)
- 타이머의 Auto-Reload Register(ARR) 값을 설정하여 PWM 신호의 주기를 결정합니다.
- 듀티 사이클 설정 (CCR 값)
- 타이머의 Capture/Compare Register(CCR) 값을 설정하여 PWM 신호의 듀티 사이클을 조절합니다.
- 듀티 사이클은 CCR 값과 ARR 값의 비율로 결정됩니다. (duty cycle = CCR / ARR)
LED 밝기 조절 원리
- PWM 주기 설정
- 타이머의 ARR 값을 설정하여 PWM 신호의 주기를 결정합니다.
- 예를 들어, ARR 값을 999로 설정하면, PWM 주기는 1000 클럭 사이클이 됩니다.
- 듀티 사이클 조절
- PWM 신호의 HIGH 상태 지속 시간(듀티 사이클)을 조절하여 LED 밝기를 변경합니다.
- 타이머의 CCR 값을 설정하여 HIGH 상태 지속 시간을 조절합니다.
- CCR 값이 클수록 HIGH 상태 지속 시간이 길어져 LED 밝기가 높아집니다.
- 전압 평균화
- LED는 전압이 가해질 때만 빛을 발하므로, PWM 신호의 평균 전압이 LED에 인가됩니다.
- 평균 전압이 높을수록 LED 밝기가 밝아집니다.
- 예를 들어, ARR 값이 999이고 CCR 값이 500인 경우, PWM 신호의 듀티 사이클은 50%가 됩니다. 이때 LED에 인가되는 평균 전압은 공급 전압의 50%가 되어 LED 밝기가 절반이 됩니다.
예시
- ARR = 999, CCR = 500
- 듀티 사이클 = 500 / 999 ≈ 50%
- LED에 인가되는 평균 전압은 공급 전압의 50%가 되어 LED 밝기가 절반이 됩니다.
- ARR = 999, CCR = 750
- 듀티 사이클 = 750 / 999 ≈ 75%
- LED에 인가되는 평균 전압은 공급 전압의 75%가 되어 LED 밝기가 더 밝아집니다.
- ARR = 999, CCR = 250
- 듀티 사이클 = 250 / 999 ≈ 25%
- LED에 인가되는 평균 전압은 공급 전압의 25%가 되어 LED 밝기가 더 어두워집니다.
CubeMX Configuration
- TIM3 → Channel3 : PWM Generation CH3 → Clock Source : Internal Clock → Parameter Settings → Prescaler : 83, Counter Mode : Up, Counter Period : 999, Pulse : 500
- 실제 각 타이머 레지스터 블록에 어떤 값들이 할당되어 있는지 Clock Configuration 메뉴를 통해 편하게 확인이 가능하다.
3. CLI을 이용한 디버깅 및 타이머 활용
ComportMaster를 이용한 디버깅
- ComPortMaster는 직렬(시리얼) 통신 터미널 프로그램으로, USB-Serial(UART, RS-232, RS-422, RS-485 등) 컨버터를 사용하여 PC와 연결된 가상 COM 포트를 통해 데이터를 송수신할 수 있도록 합니다.
- 이 프로그램은 포트 설정, 데이터 송신 및 수신, 로그 기록 등 직렬 통신을 위한 다양한 기능을 제공합니다.
CLI 및 타이머 활용
cli.c
cli.c
- 주요 구조체 및 정의
#include <string.h> #include <stdlib.h> #include <ctype.h> #include <stdio.h> #include "uart.h" #include "app.h" #include "led.h" #include "cli.h" #include "timer.h" typedef struct { char *cmd; // 명령어 uint8_t no; // 인자 최소 갯수 int (*cbf)(int, char **); // 명령어 처리 함수 포인터 char *remark; // 설명 } CMD_LIST_T; static int cli_led(int argc, char **argv); static int cli_echo(int argc, char *argv[]); static int cli_help(int argc, char *argv[]); static int cli_mode(int argc, char *argv[]); static int cli_dump(int argc, char *argv[]); static int cli_duty(int argc, char *argv[]); const CMD_LIST_T gCmdListObjs[] = { {"duty", 2, cli_duty, "change duty\\r\\n duty [0~999]"}, {"dump", 3, cli_dump, "memory dump\\r\\n dump [address:hex] [length:max:10 lines]"}, {"mode", 2, cli_mode, "change application mode\\r\\n mode [0/1]"}, {"led", 3, cli_led, "led [1/2/3] [on/off]"}, {"echo", 2, cli_echo, "echo [echo data]"}, {"help", 1, cli_help, "help"}, {NULL, 0, NULL, NULL} }; static void cli_parser(void *arg);
- CLI 명령어 함수들
// cli_duty (duty 값을 변경하는 함수) static int cli_duty(int argc, char *argv[]) { if (argc < 2) { printf("Err : Arg No\\r\\n"); return -1; } long duty = strtol(argv[1], NULL, 10); if (duty > 999) { printf("Err: Range 0~999\\r\\n"); return -1; } else { tim_duty_set((uint16_t)duty); } return 0; } // cli_dump (메모리 덤프를 수행하는 함수) static int cli_dump(int argc, char *argv[]) { uint32_t address, length, temp; if (argc < 3) { printf("Err : Arg No\\r\\n"); return -1; } if (strncmp(argv[1], "0x", 2) == 0) { address = (uint32_t)strtol(&argv[1][2], NULL, 16); } else { address = (uint32_t)strtol(&argv[1][0], NULL, 16); } length = (uint32_t)strtol(argv[2], NULL, 10); if (length > 10) length = 10; printf("address %08lX, length = %ld\\r\\n", address, length); for (int i = 0; i < length; i++) { printf("\\r\\n%08lX : ", (uint32_t)address); temp = address; for (int j = 0; j < 16; j++) { printf("%02X ", *(uint8_t *)temp); temp++; } temp = address; for (int j = 0; j < 16; j++) { char c = *(char *)temp; c = isalnum(c) ? c : (char)' '; printf("%c", c); temp++; } address = temp; } printf("\\r\\n"); return 0; } // cli_mode (응용 모드를 변경하는 함수) static int cli_mode(int argc, char *argv[]) { if (argc < 2) { printf("Err : Arg No\\r\\n"); return -1; } long mode = strtol(argv[1], NULL, 10); app_mode((int)mode); return 0; } // cli_led (LED 상태를 변경하는 함수) static int cli_led(int argc, char *argv[]) { if (argc < 3) { printf("Err : Arg No\\r\\n"); return -1; } long no = strtol(argv[1], NULL, 10); int onoff = strcmp(argv[2], "off"); if (onoff != 0) onoff = 1; bool sts = onoff ? true : false; led_onoff((uint8_t)no, sts); return 0; } // cli_echo (에코 데이터를 출력하는 함수) static int cli_echo(int argc, char *argv[]) { if (argc < 2) { printf("Err : Arg No\\r\\n"); return -1; } printf("%s\\r\\n", argv[1]); return 0; } // cli_help (도움말을 출력하는 함수) static int cli_help(int argc, char *argv[]) { for (int i = 0; gCmdListObjs[i].cmd != NULL; i++) { printf("%s\\r\\n", gCmdListObjs[i].remark); } return 0; }
cli_duty
: 사용자가 입력한 duty 값을 설정합니다.- 인자가 충분한지 확인합니다.
- 입력된 duty 값을 정수로 변환합니다.
- duty 값이 0~999 범위인지 확인합니다.
- 유효한 duty 값이면
timer.c
의tim_duty_set
함수를 호출하여 설정합니다.
cli_dump
: 지정된 메모리 주소에서 메모리 덤프를 수행합니다.- 인자가 충분한지 확인합니다.
- 주소와 길이를 16진수와 10진수로 변환합니다.
- 덤프할 최대 길이를 10으로 제한합니다.
- 지정된 주소와 길이만큼 메모리 내용을 16진수와 ASCII 형식으로 출력합니다.
cli_mode
: 응용 프로그램 모드를 변경합니다.- 인자가 충분한지 확인합니다.
- 입력된 모드를 정수로 변환합니다.
app.c
의app_mode
함수를 호출하여 모드를 변경합니다.
cli_led
: 지정된 LED의 상태를 변경합니다.- 인자가 충분한지 확인합니다.
- LED 번호와 on/off 상태를 파싱합니다.
led.c
의led_onoff
함수를 호출하여 LED 상태를 설정합니다.
cli_echo
: 입력된 데이터를 에코 출력합니다.- 인자가 충분한지 확인합니다.
- 입력된 데이터를 출력합니다.
cli_help
: 사용 가능한 명령어와 설명을 출력합니다.- 명령어 목록을 순회하여 각 명령어의 설명을 출력합니다.
- CLI 초기화 및 스레드 함수
// cli_init (CLI 초기화 함수) void cli_init(void) { uart_regcbf(cli_parser); } // cli_thread (CLI 스레드 함수) void cli_thread(void *arg) { (void)arg; }
- CLI 파서 함수
#define D_DELIMITER " ,\\r\\n" // cli_parser (명령어 파싱 및 실행 함수) static void cli_parser(void *arg) { int argc = 0; char *argv[10]; char *ptr; char *buf = (char *)arg; // 문자열 토큰 분리 ptr = strtok(buf, D_DELIMITER); if (ptr == NULL) return; while (ptr != NULL) { argv[argc] = ptr; argc++; ptr = strtok(NULL, D_DELIMITER); } for (int i = 0; gCmdListObjs[i].cmd != NULL; i++) { if (strcmp(gCmdListObjs[i].cmd, argv[0]) == 0) { gCmdListObjs[i].cbf(argc, argv); return; } } printf("Unsupported Command\\r\\n"); }
argc
: 명령어와 인자 개수를 저장하는 변수입니다.argv
: 명령어와 인자들을 저장하는 배열입니다.buf
: 입력된 명령어 문자열을 저장하는 포인터입니다.
strtok
함수를 사용하여 입력된 문자열을 토큰으로 분리합니다.- 토큰들은 공백, 쉼표, 개행 문자 등을 기준으로 분리됩니다.
- 첫 번째 토큰은 명령어로 간주하고, 나머지 토큰들은 인자로 간주합니다.
- 토큰을
argv
배열에 저장하고argc
값을 증가시킵니다.
- 등록된 명령어 목록
gCmdListObjs
를 순회하며 입력된 명령어와 일치하는 항목을 찾습니다. - 일치하는 명령어가 있으면 해당 명령어의 처리 함수(
cbf
)를 호출하고,argc
와argv
를 전달합니다. - 일치하는 명령어가 없으면 "Unsupported Command" 메시지를 출력합니다.
led.c
led.c
- 주요 구조체 및 정의
#include "led.h" #define LED_MAX 3 // 최대 LED 수 typedef struct { GPIO_TypeDef *port; // GPIO 포트 uint16_t pin; // GPIO 핀 번호 } LED_T; // LED 객체 배열 정의 const LED_T gLedObjs[LED_MAX] = { { LD2_GPIO_Port, LD2_Pin }, { LD2_GPIO_Port, LD2_Pin }, { LD3_GPIO_Port, LD3_Pin } };
- LED 제어 함수
// led_onoff (LED 상태를 변경하는 함수) bool led_onoff(uint8_t led_no, bool flag) { LED_T *p; GPIO_PinState sts; if (led_no > LED_MAX) return false; // 유효한 LED 번호인지 확인 p = (LED_T *)&gLedObjs[led_no]; // LED 객체 포인터 설정 sts = flag ? GPIO_PIN_SET : GPIO_PIN_RESET; // flag에 따른 핀 상태 설정 HAL_GPIO_WritePin(p->port, p->pin, sts); // 핀 상태 변경 return true; // 상태 변경 성공 }
led_no
가 유효한지 확인합니다. 유효하지 않으면false
를 반환합니다.gLedObjs
배열에서 해당 LED 객체의 포인터를 설정합니다.flag
에 따라 핀 상태 (GPIO_PIN_SET
또는GPIO_PIN_RESET
)를 설정합니다.HAL_GPIO_WritePin
함수를 호출하여 LED의 상태를 변경합니다.- LED 상태 변경이 성공하면
true
를 반환합니다.
timer.c
timer.c
- 주요 구조체 및 정의
#include "timer.h" #include <stdio.h> void tim_duty_set(uint16_t duty); extern TIM_HandleTypeDef htim3; // 외부에서 선언된 타이머 핸들러
TIM_HandleTypeDef
구조체는 HAL 라이브러리에서 제공하는 타이머 핸들러입니다. 이 핸들러를 사용하여 타이머를 제어합니다.- 타이머 초기화 함수
// tim_init (타이머 초기화 함수) void tim_init(void) { HAL_TIM_Base_Start(&htim3); // 기본 타이머 시작 HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // PWM 타이머 시작 }
HAL_TIM_Base_Start
함수를 호출하여 기본 타이머를 시작합니다.HAL_TIM_PWM_Start
함수를 호출하여 PWM 타이머를 시작합니다.
- 타이머 스레드 함수
// tim_thread (타이머 스레드 함수) void tim_thread(void *arg) { // 현재는 아무 작업도 수행하지 않습니다. }
- duty 설정 함수
// tim_duty_set (duty 값을 설정하는 함수) void tim_duty_set(uint16_t duty) { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, duty); // 비교 값 설정 printf("change duty : %d\\r\\n", duty); // 설정된 duty 값 출력 }
__HAL_TIM_SET_COMPARE
함수를 호출하여 타이머의 비교 값을 설정합니다.printf
함수를 사용하여 설정된 duty 값을 출력합니다.
app.c
수정
app.c
수정// void app_init(void); // void app_mode(int mode); // static : 내부용 함수 static void app_normal(void); static void app_diagnostic(void); void app_init(void); // static : 내부 변수 static void (*mode_func)(void); // 함수 포인터 변수 void app(void) { printf("System Started.....!\r\n"); app_mode(1); // diagnostic mode is default. app_init(); while (1) { mode_func(); } } void app_init(void) { io_exti_init(); polling_init(); button_init(); tim_init(); uart_init(); cli_init(); } static void app_normal(void) { polling_thread(NULL); button_thread(NULL); tim_thread(NULL); uart_thread(NULL); cli_thread(NULL); } static void app_diagnostic(void) { tim_thread(NULL); uart_thread(NULL); cli_thread(NULL); } void app_mode(int mode) { if (mode == 0) { printf("Mode : Normal \r\n"); mode_func = app_normal; } else { printf("Mode : Diagnostic \r\n"); mode_func = app_diagnostic; } }
- 변경점
- 함수 선언 및 정의 추가
app_mode(int mode)
함수가 추가되었습니다.- 내부 함수로
app_normal()
과app_diagnostic()
가 추가되었습니다. - 함수 포인터
mode_func
가 추가되어 모드를 전환할 수 있게 되었습니다. - 애플리케이션 초기화 함수 확장
app_init()
함수에서tim_init()
와cli_init()
을 추가하여 타이머와 CLI 초기화를 수행합니다.- 메인 루프에서 모드 전환 기능 추가
app()
함수에서app_mode(1)
을 호출하여 진단 모드를 기본 모드로 설정합니다.- 메인 루프에서
mode_func()
를 호출하여 현재 모드에 따라 다른 작업을 수행합니다. - 모드별 동작 추가
app_normal()
과app_diagnostic()
함수에서 각 모드에 맞는 스레드 함수를 호출합니다.app_normal()
:polling_thread
,button_thread
,tim_thread
,uart_thread
,cli_thread
호출app_diagnostic()
:tim_thread
,uart_thread
,cli_thread
호출
전체적인 구조 및 흐름
- 초기화
app_init()
함수에서 모든 모듈을 초기화합니다.- 각 모듈은 하드웨어 설정을 수행하고 필요한 경우 콜백 함수를 등록합니다.
- 모드 설정 및 부팅 메시지
app_mode()
함수를 통해 초기 모드를 설정합니다.- 부팅 메시지를 출력합니다.
- 주기적인 체크 및 이벤트 처리
- 메인 루프에서 현재 설정된 모드 함수(
mode_func
)를 반복 호출합니다. - 각 스레드 함수는 주기적으로 상태를 확인하고 업데이트합니다.
- CLI 명령어 파싱 및 실행
cli_parser()
함수는 사용자 입력을 파싱하고 해당 명령어를 실행합니다.
- LED 제어
led_onoff()
함수는 LED의 상태를 제어합니다.
- 타이머 제어
tim_init()
함수는 타이머와 PWM을 초기화하고 시작합니다.tim_duty_set()
함수는 PWM 타이머의 duty 값을 설정합니다.
Day05 : 프로그램 FREERTOS 상에 올리기
1. RTOS (Real-Time Operating System)
- 실시간 시스템은 일정한 시간 내에 작업을 완료해야 하는 시스템으로, 주로 임베디드 시스템에서 사용됩니다.
- RTOS는 일반적인 운영 체제와는 다르게, 실시간성을 보장하기 위해 특정한 특성을 갖추고 있습니다.
특징
- Deterministic Timing (결정론적 시간성)
- RTOS는 작업이 항상 정해진 시간 내에 완료되도록 보장합니다.
- Task Scheduling (태스크 스케줄링)
- 우선순위 기반 스케줄링을 통해 중요한 작업이 우선적으로 처리되도록 합니다.
- Minimal Interrupt Latency (최소 인터럽트 지연)
- 인터럽트 처리 시간과 태스크 전환 시간이 최소화되어야 합니다.
- Resource Sharing (자원 공유)
- Mutex나 Semaphore와 같은 동기화 메커니즘을 사용하여 여러 태스크 간 자원 공유를 효율적으로 관리합니다.
- Small Footprint (작은 메모리 사용량)
- 일반적으로 메모리 자원이 제한된 임베디드 시스템에서 사용되기 때문에, RTOS는 메모리 사용량이 작아야 합니다.
CMSIS (Cortex Microcontroller Software Interface Standard)
- CMSIS는 ARM Cortex-M 프로세서를 기반으로 한 마이크로컨트롤러를 위한 표준 소프트웨어 인터페이스입니다.
- ARM에서 개발한 CMSIS는 개발자가 하드웨어 종속적인 코드를 쉽게 작성하고, 표준화된 인터페이스를 통해 소프트웨어의 이식성과 재사용성을 높일 수 있도록 돕습니다.
특징
- 표준화된 API
- CMSIS-RTOS는 표준화된 API를 제공하여, RTOS 구현체에 상관없이 일관된 인터페이스로 RTOS 기능을 사용할 수 있습니다.
- 이식성
- 코드의 이식성을 높여, 다른 ARM Cortex-M 마이크로컨트롤러 플랫폼으로 쉽게 전환할 수 있습니다.
- 간편한 통합
- CMSIS는 STM32CubeMX와 같은 도구와 통합되어, 프로젝트 생성 및 RTOS 설정을 간편하게 할 수 있습니다.
2. Task & Thread
Task
- Task는 일반적으로 실시간 운영 체제(RTOS)나 임베디드 시스템에서 사용되는 용어로, 수행해야 할 일이나 작업을 의미합니다.
- Task는 독립적으로 실행될 수 있는 코드의 단위로, 특정한 기능을 수행하기 위해 설계됩니다.
- RTOS에서 Task는 스케줄러에 의해 관리되며, 여러 Task가 동시에 실행되는 것처럼 보이게 합니다.
Thread
- Thread는 멀티스레딩 운영 체제(예: Windows, Linux)에서 사용되는 용어로, 프로세스 내에서 실행되는 독립적인 실행 흐름을 의미합니다.\
- 프로세스는 메모리 공간을 가지며, 여러 Thread가 이 메모리 공간을 공유합니다.
- Thread는 프로세스의 자원을 공유하며, 독립적으로 실행될 수 있습니다.
차이점
구분 | Task | Thread |
사용 환경 | 주로 RTOS 및 임베디드 시스템 | 멀티스레딩 운영 체제 (Windows, Linux 등) |
메모리 관리 | 독립적인 메모리 공간을 가질 수 있음 | 프로세스의 메모리 공간을 공유 |
스케줄링 | RTOS 스케줄러에 의해 관리됨 | 운영 체제 스케줄러에 의해 관리됨 |
우선순위 | 우선순위 기반 스케줄링을 주로 사용 | 우선순위 기반 스케줄링 가능, 라운드 로빈 등 다양한 방식 |
상호작용 | 다른 Task와 메시지 큐, 세마포어 등을 통해 상호작용 | 다른 Thread와 메모리 및 자원을 공유하며 상호작용 |
실시간성 | 실시간성을 보장하기 위해 설계됨 | 실시간성을 반드시 보장하지 않음 |
사용 목적 | 특정 기능을 독립적으로 수행하도록 설계됨 | 프로세스 내에서 병렬 처리를 위해 설계됨 |
예제 사용 | 임베디드 시스템, RTOS (FreeRTOS 등) | 멀티스레딩 응용 프로그램 (웹 서버, 게임 등) |
복잡도 | 상대적으로 간단한 구조와 낮은 자원 소모 | 더 복잡한 구조와 높은 자원 소모 |
스케줄링(Scheduling)
- 스케줄링은 시스템 자원(CPU 시간, 메모리 등)을 효율적으로 사용하기 위해, 여러 작업(Task 또는 Thread)을 어떻게 배치하고 실행할지 결정하는 과정입니다.
- 스케줄러는 이 역할을 담당하는 소프트웨어 모듈로, 작업의 우선순위와 시스템 상태를 고려하여 실행 순서를 결정합니다.
- SysTick Handler
- SysTick Handler는 시스템 타이머 (SysTick)에서 발생하는 인터럽트를 처리한다.
- 주로 시스템의 타이밍과 타임 슬라이스를 관리하는 데 사용된다.
- 주요 역할로는 주기적인 시스템 타이머 인터럽트를 처리하여 운영체제의 스케줄링 작업을 수행하거나, 타이머에 의한 시스템 틱을 증가시켜 시간 관련 작업을 수행한다.
- SVC (Supervisor Call) Handler
- SVC Handler는 특권 모드에서 사용자 모드로 전환하여 특정 작업을 실행하기 위해 호출되는 핸들러이다.
- 주로 운영체제의 서비스나 시스템 호출을 처리하는 데 사용된다.
- SVC 인스트럭션은 특정 서비스를 요청하거나 특권 명령을 실행하기 위해 사용되며, 이를 처리하는 핸들러가 SVC Handler이다.
- PendSV (Pending Supervisor Call) Handler
- PendSV Handler는 PendSV 인터럽트를 처리하는 핸들러이다.
- PendSV는 소프트웨어에 의해 발생시키는 인터럽트로, 주로 스케줄러나 컨텍스트 스위칭을 구현하는 데 사용된다.
- PendSV 인터럽트는 주로 스레드 간의 전환을 처리하거나, 프로세스 간의 상태 저장 및 복원을 수행한다. 특히, 멀티태스킹 환경에서 현재 실행 중인 프로세스의 상태를 저장하고 다음 프로세스의 상태를 로드하는 컨텍스트 스위칭에 사용된다.
주요 스케줄링 방식
- 선점형 스케줄링 (Preemptive Scheduling)
- 높은 우선순위의 작업이 실행 중일 때, 낮은 우선순위의 작업을 중단하고 높은 우선순위의 작업을 실행합니다.
- 응답 시간이 짧고, 실시간 시스템에서 주로 사용됩니다.
- 비선점형 스케줄링 (Non-preemptive Scheduling)
- 작업이 완료될 때까지 CPU를 점유하며, 다른 작업이 CPU를 사용할 수 없습니다.
- 응답 시간이 길어질 수 있지만, 간단한 구현이 가능합니다.
- 라운드 로빈 스케줄링 (Round Robin Scheduling)
- 각 작업에 동일한 CPU 시간을 할당하고, 순환하며 작업을 실행합니다.
- 공평한 자원 배분이 가능하지만, 우선순위를 고려하지 않기 때문에 실시간 시스템에는 부적합할 수 있습니다.
- 우선순위 기반 스케줄링 (Priority-based Scheduling)
- 각 작업에 우선순위를 부여하고, 높은 우선순위의 작업을 먼저 실행합니다.
- 실시간 시스템에서 자주 사용되며, 중요한 작업이 빠르게 처리될 수 있습니다.
TCB(Task Control Block)
3. Event Flags를 이용한 스레드간 통신 (메세지 패싱 방식)
- CMSIS-RTOS2의 Event Flags는 스레드 간 통신을 위해 제공되는 방법입니다.
과정
- Event Flag Set 및 대기
- 특정 스레드가 Event Flag를 Set하고 대기 상태로 전환됩니다. 이 작업은 주기적으로 수행됩니다.
- 다른 스레드의 Flag Wait
- 다른 스레드는 Flag Wait를 수행하여, 특정 Event Flag가 Set될 때까지 기다립니다. 이 스레드는 이벤트가 발생할 때까지 Waiting 큐에 있습니다.
- Event 발생 및 문맥 교환
- 첫 번째 스레드가 Event Flag를 Set하면, Waiting 큐에 있던 스레드가 Running 상태로 전환됩니다. 이때 주기적으로 Event Flag를 Set하던 스레드는 Waiting 큐로 이동하며, 문맥 교환이 발생합니다.
- 작업 완료 후 문맥 교환
- Running 중이던 스레드의 작업이 끝나면, 해당 스레드는 다시 Event Flag를 Set하는 상태로 돌아가고, Waiting 큐에 있던 스레드와 문맥 교환이 일어납니다.
FreeRTOS에서의 OSEventFlags 사용법
- osThreadNew(): 스레드 함수를 입력받아 활성화(Ready 상태)하는 함수
osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);
func
: 스레드 함수argument
: 스레드 함수에 전달되는 인수attr
: 스레드의 속성 (NULL 시 기본값 사용)
- osEventFlagsNew(): 이벤트 플래그를 생성하는 함수
osEventFlagsId_t osEventFlagsNew(const osEventFlagsAttr_t *attr);
attr
: 이벤트 플래그의 속성
- osEventFlagsWait(): 지정된 이벤트 플래그가 Set될 때까지 대기하는 함수
uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);
ef_id
: osEventFlagsNew로 얻은 이벤트 플래그 식별자flags
: 대기할 플래그options
: 플래그 옵션 (예:osFlagsWaitAny
,osFlagsWaitAll
)timeout
: 타임아웃 값 (예:osWaitForever
)
- osEventFlagsSet(): 지정된 이벤트 플래그를 Set하여 문맥 교환을 발생시키는 함수
uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags);
ef_id
: 이벤트 플래그 식별자flags
: Set할 플래그
CubeMX Configuration
- Middleware Software Pack → FREERTOS → Interface : CMSIS_V2 → Advanced Setting Newlib: Enable
CMSIS-RTOS2 - Event Flags 구현
uart.c
수정
uart.c
수정#define D_BUF_OBJ_MAX 3 static BUF_T gBufObjs[D_BUF_OBJ_MAX]; static void (*uart_cbf)(void *); void uart_init(void) { for (int i = 0; i < D_BUF_OBJ_MAX; i++) { gBufObjs[i].idx = 0; gBufObjs[i].flag = false; } // 인터럽트 방식 수신 시작 : 1바이트 HAL_UART_Receive_IT(&huart3, (uint8_t *)&rxdata[0], 1); } void uart_regcbf(void (*cbf)(void *)) { uart_cbf = cbf; } // void uart_thread(void *arg) // { // for (int i = 0; i < D_BUF_OBJ_MAX; i++) { // if (gBufObjs[i].flag == true) { // if (uart_cbf != NULL) uart_cbf((void *)&gBufObjs[i]); // gBufObjs[i].idx = 0; // gBufObjs[i].flag = false; // } // } // } // 인터럽트 서비스 루틴 (ISR) void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { volatile uint8_t rxd; if (huart == &huart3) { rxd = rxdata[0]; HAL_UART_Receive_IT(huart, (uint8_t *)&rxdata[0], 1); BUF_T *p = (BUF_T *)&gBufObjs[0]; if (p->flag == false) { p->buf[p->idx] = rxd; if (p->idx < D_BUF_MAX) p->idx++; if (rxd == '\r' || rxd == '\n') { p->buf[p->idx] = 0; // '\0'; p->flag = true; if (uart_cbf != NULL) uart_cbf((void *)&gBufObjs[0]); p->idx = 0; p->flag = false; } } } }
- 변경점
uart_thread
함수 제거uart_thread
함수는 콜백 함수 호출이 ISR 내부에서 직접 호출되도록 변경되었기 때문에 주석 처리되었습니다.2HAL_UART_RxCpltCallback
함수 수정- 콜백 함수 호출 위치 변경
- 이전에는
uart_thread
함수에서 콜백 함수를 호출했으나, 이제는 메시지 종료 문자가 수신되었을 때 ISR 내에서 직접 호출합니다. - 이를 통해 콜백 함수 호출이 즉시 이루어지며, 메시지 처리 지연이 줄어듭니다.
- 버퍼 인덱스 및 플래그 초기화
- 메시지 처리가 완료된 후 ISR 내에서 버퍼 인덱스와 플래그를 초기화합니다.
- 이는 다음 메시지 수신을 준비하기 위한 것입니다.
- ISR 내에서 데이터 수신 설정
HAL_UART_Receive_IT
함수 호출을 통해 다음 데이터를 수신할 수 있도록 계속해서 인터럽트를 설정합니다.uart_thread
에서 불필요한 버퍼 재설정 제거- 버퍼 재설정 및 콜백 호출이 ISR 내부에서 처리되므로
uart_thread
함수에서 해당 로직이 더 이상 필요하지 않습니다.
polling.c
수정
polling.c
수정#include <stdio.h> #include "cmsis_os.h" #include "button.h" #include "polling.h" static osThreadId_t polling_thread_hnd; // 쓰레드 핸들 static osEventFlagsId_t polling_evt_id; // 이벤트 플래그 핸들 static const osThreadAttr_t polling_thread_attr = { .stack_size = 128 * 8, .priority = (osPriority_t) osPriorityNormal, }; void polling_thread_init(void); static void btn_blue_callback(void *arg); #define D_BTN_BLUE 0 //static BUTTON_T gBtnBlue; static void polling_thread(void *arg) { uint32_t flags; (void)arg; printf("Polling Thread Started...\r\n"); button_init(); button_regcbf(D_BTN_BLUE, btn_blue_callback); while (1) { flags = osEventFlagsWait(polling_evt_id, 0xffff, osFlagsWaitAny, osWaitForever); if (flags & 0x0001) { printf("\r\n%s[0x0001][%d]\r\n", __func__, __LINE__); osEventFlagsSet(polling_evt_id, 0x0002); button_proc_blue(NULL); } if (flags & 0x0002) { printf("%s[0x0002][%d]\r\n", __func__, __LINE__); osEventFlagsSet(polling_evt_id, 0x0004); } if (flags & 0x0004) { printf("%s[0x0004][%d]\r\n", __func__, __LINE__); } } } void polling_init(void) { polling_evt_id = osEventFlagsNew(NULL); if (polling_evt_id != NULL) printf("Polling Event Flags Created...\r\n"); else { printf("Polling Event Flags Create File...\r\n"); while (1); } polling_thread_hnd = osThreadNew(polling_thread, NULL, &polling_thread_attr); if (polling_thread_hnd != NULL) printf("Polling Thread Created...\r\n"); else { printf("Polling Thread Create Fail...\r\n"); while (1); } } static void btn_blue_callback(void *arg) { // BUTTON_T *p; // if (arg == NULL) return; // p = (BUTTON_T *)arg; // gBtnBlue.edge = p->edge; // gBtnBlue.no = p->no; osEventFlagsSet(polling_evt_id, 0x0001); }
- 변경점
- CMSIS-RTOS2 사용
cmsis_os.h
를 포함하여 RTOS 기능을 사용할 수 있도록 하였습니다.- RTOS 쓰레드 핸들(
osThreadId_t
)과 이벤트 플래그 핸들(osEventFlagsId_t
)을 선언하였습니다. polling_init
함수- 이벤트 플래그 생성
osEventFlagsNew
함수를 사용하여 이벤트 플래그를 생성합니다. 성공 여부를 체크하여 적절한 메시지를 출력합니다.- 폴링 쓰레드 생성
osThreadNew
함수를 사용하여 폴링 쓰레드를 생성합니다. 성공 여부를 체크하여 적절한 메시지를 출력합니다.polling_thread_attr
로 쓰레드 속성을 정의하여 스택 크기와 우선 순위를 설정하였습니다.- 이전 폴링 함수 제거
- 이전의
polling_update
함수와polling_thread
함수는 RTOS 기반 구현으로 대체되었습니다. polling_thread
함수polling_thread
함수는 이벤트 플래그를 대기하고 적절한 콜백 함수들을 호출합니다.- 이벤트 플래그 대기
osEventFlagsWait
함수는 이벤트 플래그를 대기합니다. 여기서는 모든 플래그(0xffff)를 기다리며, 플래그가 설정될 때까지 무한 대기(osWaitForever
)합니다.- 플래그 처리
flags & 0x0001
: 플래그 0x0001이 설정된 경우, 이를 처리하고 다음 플래그 0x0002를 설정합니다. 이 과정에서button_proc_blue
함수가 호출됩니다.flags & 0x0002
: 플래그 0x0002가 설정된 경우, 이를 처리하고 다음 플래그 0x0004를 설정합니다.flags & 0x0004
: 플래그 0x0004가 설정된 경우, 이를 처리합니다.- 버튼 이벤트를 처리하기 위해
btn_blue_callback
함수를 사용합니다.
io.c
→ gpio.c
io.c
수정
io.c
수정/* * io.c * * Created on: Apr 11, 2024 * Author: iot00 */ #include <stdbool.h> #include "io.h" extern UART_HandleTypeDef huart3; //int __io_putchar(int ch) //{ // HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xffff); // return ch; //} int _write(int file, char *ptr, int len) { (void)file; //int DataIdx; HAL_UART_Transmit(&huart3, (uint8_t *)ptr, len, 0xffff); return len; }
- 변경점
- UART 전송 함수 변경
__io_putchar
함수가 주석 처리되었습니다.- 대신
_write
함수가 도입되어 문자열을 UART로 전송합니다. - 단일 문자를 전송하는
__io_putchar
대신 문자열을 한 번에 전송할 수 있는_write
함수를 사용하여 효율성을 높였습니다. _write
함수는 파일 시스템에서 사용하는 함수로, 임베디드 시스템에서 표준 출력 함수의 재정의로 자주 사용됩니다.
cli.c
수정
cli.c
수정#include <string.h> #include <stdlib.h> #include <ctype.h> #include <stdio.h> #include <stdbool.h> #include "cmsis_os.h" #include "uart.h" #include "app.h" #include "tim.h" #include "led.h" #include "cli.h" typedef struct { char *cmd; // 명령어 uint8_t no; // 인자 최소 갯수 int (*cbf)(int, char **); // int argc, char *argv[] char *remark; // 설명 } CMD_LIST_T; static osThreadId_t cli_thread_hnd; // 쓰레드 핸들 static osEventFlagsId_t cli_evt_id; // 이벤트 플래그 핸들 static const osThreadAttr_t cli_thread_attr = { .stack_size = 128 * 8, .priority = (osPriority_t) osPriorityNormal, }; // ... (CLI 명령어 함수들) ... static void cli_parser(BUF_T *arg); static void cli_event_set(void *arg); static BUF_T gBufObj[1]; void cli_thread(void *arg) { uint32_t flags; (void)arg; printf("CLI Thread Started...\r\n"); uart_regcbf(cli_event_set); while (1) { flags = osEventFlagsWait(cli_evt_id, 0xffff, osFlagsWaitAny, osWaitForever); if (flags & 0x0001) cli_parser(&gBufObj[0]); } } void cli_init(void) { cli_evt_id = osEventFlagsNew(NULL); if (cli_evt_id != NULL) printf("CLI Event Flags Created...\r\n"); else { printf("CLI Event Flags Create Fail...\r\n"); while (1); } cli_thread_hnd = osThreadNew(cli_thread, NULL, &cli_thread_attr); if (cli_thread_hnd != NULL) printf("CLI Thread Created...\r\n"); else { printf("CLI Thread Create Fail...\r\n"); while (1); } } static void cli_event_set(void *arg) { BUF_T *pBuf = (BUF_T *)arg; memcpy(&gBufObj[0], pBuf, sizeof(BUF_T)); osEventFlagsSet(cli_evt_id, 0x0001); } #define D_DELIMITER " ,\r\n" static void cli_parser(BUF_T *arg) { int argc = 0; char *argv[10]; char *ptr; char *buf = (char *)arg->buf; ptr = strtok(buf, D_DELIMITER); if (ptr == NULL) return; while (ptr != NULL) { argv[argc] = ptr; argc++; ptr = strtok(NULL, D_DELIMITER); } for (int i=0; gCmdListObjs[i].cmd != NULL; i++) { if (strcmp(gCmdListObjs[i].cmd, argv[0]) == 0) { gCmdListObjs[i].cbf(argc, argv); return; } } printf("Unsupported Command\r\n");
- 변경점
- RTOS 적용
cmsis_os.h
헤더 파일을 포함하여 RTOS 기능을 사용합니다.- CLI 처리를 위한 쓰레드 핸들(
cli_thread_hnd
)과 이벤트 플래그 핸들(cli_evt_id
)이 추가되었습니다. - CLI 쓰레드 속성을 정의하는
osThreadAttr_t
구조체가 추가되었습니다. cli_thread
함수가 실제로 쓰레드로 동작하도록 변경되었습니다.osEventFlagsWait
함수로 이벤트 플래그를 대기하고 설정하는 로직이 추가되었습니다.- 함수 변경
cli_parser
함수는void *arg
에서BUF_T *arg
로 매개변수 타입이 변경되었습니다.cli_event_set
함수가 추가되어 UART 콜백에서 이벤트를 설정합니다.
button.c
수정
button.c
수정#include <stdbool.h> #include <stdio.h> #include "gpio.h" #include "button.h" #define D_BTN_BLUE_NO 13 #define D_BUTTON_MAX 1 static BUTTON_T gBtnObjs[D_BUTTON_MAX]; static void io_exti_btn_blue_callback(uint8_t rf, void *arg); void button_init(void) { gBtnObjs[0].no = 0; gBtnObjs[0].prev_tick = HAL_GetTick(); io_exti_regcbf(D_BTN_BLUE_NO, io_exti_btn_blue_callback); } bool button_regcbf(uint16_t idx, BUTTON_CBF cbf) { if (idx > D_BUTTON_MAX) return false; gBtnObjs[idx].cbf = cbf; return true; } void button_proc_blue(void *arg) { BUTTON_T *p = &gBtnObjs[0]; if (p->no == D_BTN_BLUE_NO) { printf("rf:%d, no:%d\r\n", p->edge, p->no); } } static void io_exti_btn_blue_callback(uint8_t rf, void *arg) { volatile uint32_t curr_tick = HAL_GetTick(); BUTTON_T *p = &gBtnObjs[0]; if (curr_tick - p->prev_tick > 120) { p->prev_tick = curr_tick; p->edge = rf; p->no = *(uint16_t *)arg; if (p->cbf != NULL) p->cbf((void *)p); } }
- 변경점
- 구조체 사용
- 새로운 코드에서는
BUTTON_T
구조체를 사용하여 버튼 관련 데이터를 관리합니다. 이 구조체는 버튼 번호, 이전 틱, 엣지 정보 및 콜백 함수를 포함합니다. - 원래 코드에서는 별도의 전역 변수를 사용하여 버튼 상태를 관리했습니다.
- 콜백 함수 변경
- 원래 코드에서는
button_callback_13
라는 단일 콜백 함수를 사용하여 EXTI 이벤트를 처리했습니다. - 새로운 코드에서는
io_exti_btn_blue_callback
라는 콜백 함수를 사용하고, 구조체를 통해 버튼 정보를 업데이트합니다. - 추가된 기능
button_regcbf
함수가 추가되어 특정 버튼에 대한 사용자 정의 콜백 함수를 등록할 수 있게 되었습니다. 이 함수는 버튼 번호가 유효한지 확인한 후, 콜백 함수를 설정합니다.button_proc_blue
함수가 추가되어, 버튼이 눌렸을 때 처리할 로직을 정의할 수 있게 되었습니다.- 버튼 처리 로직
- 원래 코드에서는 버튼이 눌렸을 때 플래그를 설정하고,
button_thread
함수에서 해당 플래그를 검사하여 출력하는 방식이었습니다. - 새로운 코드에서는 콜백 함수에서 바로 버튼 상태를 업데이트하고, 필요 시 등록된 콜백 함수를 호출하는 방식으로 변경되었습니다.
전체적인 구조 및 흐름
- RTOS 초기화 및 스레드 관리
- 시스템이 시작되면
osKernelInitialize
함수를 호출하여 RTOS 커널을 초기화하고,osKernelStart
함수를 통해 커널이 시작됩니다. - 각 기능별로 독립적인 스레드가 생성되며, 이벤트 플래그를 통해 스레드 간의 동기화가 이루어집니다.
- UART 통신 관리 (
uart.c
) - UART 초기화와 인터럽트 기반 데이터 수신을 관리합니다.
- 데이터 수신이 완료되면
HAL_UART_RxCpltCallback
ISR에서 데이터 처리를 수행하고, 필요 시 콜백 함수를 호출합니다. - 주석 처리된
uart_thread
함수는 더 이상 사용되지 않으며, 대신 ISR 내에서 직접 데이터를 처리합니다.
- 버튼 인터페이스 (
button.c
) - 버튼 초기화와 EXTI (External Interrupt) 인터페이스를 관리합니다.
- 버튼 상태를
BUTTON_T
구조체로 관리하며, 버튼 이벤트 발생 시 콜백 함수를 호출합니다. - 이벤트 플래그를 사용하여 버튼 이벤트를 처리합니다.
- CLI (Command Line Interface) (
cli.c
) - CLI 초기화와 명령어 처리를 관리합니다.
- UART로 수신된 데이터를 기반으로 CLI 명령어를 파싱하고 실행합니다.
- RTOS 이벤트 플래그를 사용하여 CLI 이벤트를 처리합니다.
- 폴링 인터페이스 (
polling.c
) - 폴링 초기화와 이벤트 기반 처리를 관리합니다.
- RTOS 이벤트 플래그를 사용하여 폴링 이벤트를 처리하며, 버튼 이벤트를 처리하기 위한 콜백 함수가 포함됩니다.
- IO 처리 (
io.c
) - UART 출력:
_write
함수에서 문자열을 UART로 전송합니다.
Day07 : SLIP 통신 패킹 프로토콜
1. SLIP (Serial Line Internet Protocol)
- SLIP은 직렬 포트를 통해 데이터를 전송하기 위해 사용되는 간단한 통신 프로토콜입니다.
- SLIP는 주로 IP 패킷을 직렬 연결을 통해 전송할 수 있도록 설계되었습니다.
- 이는 주로 TCP/IP 프로토콜 스택이 직렬 회선을 통해 작동할 수 있도록 지원합니다.
특징과 작동 방식
- 간단한 프레임 구조
- SLIP는 매우 간단한 프레임 구조를 가집니다. 패킷의 시작과 끝을 구분하기 위해 특별한 종료 문자를 사용합니다.
- 프레임 경계
- 데이터 스트림에서 패킷의 끝을 표시하기 위해 0xC0 바이트를 사용합니다.
- 만약 패킷 내에 0xC0 바이트가 포함되어 있어야 한다면, SLIP는 이를 회피하기 위해 슬립 이스케이프(SLIP escape) 메커니즘을 사용합니다.
- 0xDB 바이트는 SLIP 이스케이프 바이트로 사용됩니다.
- 0xC0는 0xDB 0xDC로 대체됩니다.
- 0xDB는 0xDB 0xDD로 대체됩니다.
- 오버헤드 최소화
- SLIP는 헤더나 트레일러가 없으므로 IP 패킷에 추가적인 오버헤드를 거의 주지 않습니다. 이는 프로토콜이 매우 가벼워야 하는 상황에서 유리합니다.
- 비신뢰성
- SLIP는 오류 검출이나 수정 기능을 제공하지 않으므로, 신뢰성 있는 전송을 위해서는 상위 계층 프로토콜(TCP 등)에서 이러한 기능을 처리해야 합니다.
- 단순성
- SLIP는 구현이 매우 단순하여 마이크로컨트롤러와 같은 리소스가 제한된 환경에서도 쉽게 사용할 수 있습니다.
인코딩 과정
- RAW DATA FRAME
- 원래 데이터 프레임에는 여러 바이트가 포함될 수 있으며, 이 중 FEND (0xC0)나 FESC (0xDB)가 포함될 수 있습니다.
- ENCODED SLIP FRAME
- 데이터 프레임을 SLIP 프로토콜로 인코딩할 때, FEND와 FESC는 특별한 시퀀스로 변환됩니다.
- FEND (0xC0): 프레임의 끝을 나타내며, 데이터 안에 나타나면 안됩니다.
- FESC + TFEND: 데이터 프레임 안에 FEND가 있으면, FESC(0xDB)와 TFEND(0xDC)로 대체됩니다.
- FESC + TFESC: 데이터 프레임 안에 FESC가 있으면, FESC(0xDB)와 TFESC(0xDD)로 대체됩니다.
디코딩 과정
- SLIP 프레임 수신
- 직렬 연결을 통해 수신된 데이터 스트림에서 SLIP 프레임을 식별합니다. 이는 0xC0 바이트를 통해 프레임의 시작과 끝을 구분하여 이루어집니다.
- 이스케이프 시퀀스 처리
- 수신된 SLIP 프레임 내에서 FESC(0xDB) 바이트를 찾아 처리합니다.
- FESC + TFEND(0xDB 0xDC) 시퀀스는 원래의 FEND(0xC0)로 디코딩됩니다.
- FESC + TFESC(0xDB 0xDD) 시퀀스는 원래의 FESC(0xDB)로 디코딩됩니다.
- 원본 데이터 프레임 재구성
- 이스케이프 시퀀스를 처리하여 원래의 데이터 프레임을 재구성합니다.
- 재구성된 데이터 프레임에서 추가된 FEND(0xC0) 바이트를 제거하여 순수한 데이터만 추출합니다.
단점 및 대체 프로토콜
SLIP 프로토콜은 간단하고 효과적이지만, 몇 가지 단점도 있습니다. 예를 들어, 데이터 링크 계층의 오류 제어와 같은 기능이 없고, 여러 프로토콜을 구별하는 기능도 부족합니다. 이러한 이유로 SLIP는 PPP(Point-to-Point Protocol)와 같은 더 발전된 프로토콜로 대체되었습니다. PPP는 SLIP보다 많은 기능을 제공하며, 현대의 대부분의 시스템에서는 SLIP보다 PPP가 더 많이 사용됩니다.
WithRobot
- WithRobot Packing 프로토콜은 Withrobot사에서 제공하는Comportmaster 소프트웨어에서 사용할수 있는 패킹 프로토콜입니다.
- WithRobot 프로토콜 페이로드의 데이터 필드
- CMD(2Bytes) : 데이터 명령 유형
- Length(1Bytes) : 실제 전송 데이터 길이
- Data(nBytes) : 실제 전송할려는 데이터
2. WithRobot - SLIP 프로토콜로 패킹하기
uart.c
uart.c
/* * uart.c * * Created on: Apr 11, 2024 * Author: iot00 */ // 지금은 cli에만 사용 중 #include <stdbool.h> #include <stdio.h> #include "uart.h" extern UART_HandleTypeDef huart3; // idx = 1 extern UART_HandleTypeDef huart2; // idx = 0 #define D_BUF_OBJ_MAX 3 static uint8_t rxdata[D_BUF_OBJ_MAX]; static BUF_T gBufObjs[D_BUF_OBJ_MAX]; //static void (*uart_cbf[D_BUF_OBJ_MAX])(void *); //typedef void (*UART_CBF)(void *); static UART_CBF uart_cbf[D_BUF_OBJ_MAX]; void uart_init(void) { for (int i=0; i<D_BUF_OBJ_MAX; i++) { gBufObjs[i].idx = 0; gBufObjs[i].flag = false; } // 인터럽트 방식 수신 시작 : 1바이트 HAL_UART_Receive_IT(&huart2, (uint8_t *)&rxdata[E_UART_0], 1); HAL_UART_Receive_IT(&huart3, (uint8_t *)&rxdata[E_UART_1], 1); } // 직접 가르켜 주려면 전부 extern을 붙여야 함 // 등록 함수에 bool uart_regcbf(uint8_t idx, UART_CBF cbf) { if(idx > D_BUF_OBJ_MAX) return false; uart_cbf[idx] = cbf; return true; } //void uart_thread(void *arg) //{ // for (int i=0; i<D_BUF_OBJ_MAX; i++) { // if (gBufObjs[i].flag == true) { // if (uart_cbf != NULL) uart_cbf((void *)&gBufObjs[i]); // gBufObjs[i].idx = 0; // gBufObjs[i].flag = false; // } // } //} #define FEND 0xC0 #define TFEND 0xDC #define TFESC 0xDD #define FESC 0xDB // 인터럽트 서비스 루틴 (ISR) static void slip_decode(uint8_t *pstate, uint8_t rxd, BUF_T *p, UART_CBF uart_cbf) { switch (*pstate) { case 0: { if (rxd == FEND) { (*pstate)++; p->idx = 0; } } break; case 1: { if (rxd == FEND) { if (p->idx == 0) { (*pstate) = 0; } else { p->flag = true; if (uart_cbf != NULL) uart_cbf((void *)p); p->flag = false; (*pstate) = 0; } } else if (rxd == FESC) { (*pstate)++; } else { p->buf[p->idx++] = rxd; } } break; case 2: { if (rxd == TFEND) { p->buf[p->idx++] = FEND; (*pstate)--; } else if (rxd == TFESC) { p->buf[p->idx++] = FESC; (*pstate)--; } else { (*pstate) = 0; } } break; } } bool slip_encode(const uint8_t *pRaw, uint16_t rawLen, uint8_t *pEncode, uint16_t *pEncodeLen) { if (pRaw == NULL || pEncode == NULL || pEncodeLen == NULL) return false; uint16_t idx = 0; pEncode[idx++] = FEND; for (uint16_t i=0; i<rawLen; i++) { if (pRaw[i] == FEND) { pEncode[idx++] = FESC; pEncode[idx++] = TFEND; } else if (pRaw[i] == FESC) { pEncode[idx++] = FESC; pEncode[idx++] = TFESC; } else { pEncode[idx++] = pRaw[i]; } } pEncode[idx++] = FEND; pEncodeLen[0] = idx; // *pEncodeLen = idx; return true; } // 인터럽트 서비스 루틴 (ISR) static uint8_t state[2] = {0, }; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { volatile uint8_t rxd; if (huart == &huart2) { // idx = 0 rxd = rxdata[E_UART_0]; HAL_UART_Receive_IT(huart, (uint8_t *)&rxdata[E_UART_0], 1); BUF_T *p = (BUF_T *)&gBufObjs[E_UART_0]; if (p->flag == false) { slip_decode(&state[E_UART_0], rxd, p, uart_cbf[E_UART_0]); } } if (huart == &huart3) { // idx = 1 rxd = rxdata[E_UART_1]; HAL_UART_Receive_IT(huart, (uint8_t *)&rxdata[E_UART_1], 1); BUF_T *p = (BUF_T *)&gBufObjs[E_UART_1]; if (p->flag == false) { slip_decode(&state[E_UART_1], rxd, p, uart_cbf[E_UART_1]); #if 0 // switch (state) { // case 0: { // if (rxd == FEND) { state++; p->idx = 0; } // } break; // // case 1: { // if (rxd == FEND) { // if (p->idx == 0) { state = 0; // } else { // p->flag = true; // if (uart_cbf[1] != NULL) uart_cbf[1]((void *)p); //&gBufObjs[E_UART_1]); // p->flag = false; // state = 0; // } // } else if (rxd == FESC) { state++; // } else { p->buf[p->idx++] = rxd; } // } break; // // case 2: { // if (rxd == TFEND) { p->buf[p->idx++] = FEND; state--; // } else if (rxd == TFESC) { p->buf[p->idx++] = FESC; state--; // } else { state = 0; } // } break; // } #endif #if 0 p->buf[p->idx] = rxd; //p->idx++; //p->idx %= D_BUF_MAX; if (p->idx < D_BUF_MAX) p->idx++; if (rxd == '\r' || rxd == '\n') { p->buf[p->idx] = 0; //'\0'; p->flag = true; if (uart_cbf[1] != NULL) uart_cbf[1]((void *)&gBufObjs[E_UART_1]); p->idx = 0; p->flag = false; } #endif } } }
- 변경점
- UART 초기화 함수 변경 (
uart_init
): - UART2와 UART3 모두 인터럽트 방식을 사용하여 1바이트씩 데이터를 수신합니다.
- UART3 (E_UART_1): CLI 입출력
- UART3는 CLI를 통해 명령을 입력받고, 그 결과를 출력하는 용도로 사용됩니다.
- 코드에서
E_UART_1
으로 정의되어 있으며, CLI 명령을 처리하는 콜백 함수에 연결됩니다. - UART2 (E_UART_0): 노드 간 통신
- UART2는 다른 노드와의 통신을 위해 사용됩니다.
- 코드에서
E_UART_0
으로 정의되어 있으며, SLIP 프로토콜을 사용하여 데이터 패킷을 인코딩하고 전송하는 기능을 수행합니다. - UART 수신 완료 콜백 함수 변경 (
HAL_UART_RxCpltCallback
): - UART2와 UART3에서 수신된 데이터를 처리하는 로직이 추가되었습니다.
slip_decode
함수를 통해 수신된 데이터를 디코딩합니다. - SLIP 디코딩 함수 추가 (
slip_decode
) - SLIP 디코딩을 수행하는 함수입니다. 수신된 바이트를 처리하여 SLIP 프로토콜에 따라 디코딩하고, 완전한 패킷이 수신되면 콜백 함수를 호출합니다.
pstate
: 현재 디코딩 상태를 나타내는 변수입니다.rxd
: 수신된 바이트입니다.p
: 수신된 데이터를 저장하는 버퍼입니다.uart_cbf
: 데이터가 완전히 수신되었을 때 호출되는 콜백 함수입니다.
cmd.c
cmd.c
/* * cmd.c * * Created on: Apr 17, 2024 * Author: iot00 */ /* * cli.c * * Created on: Apr 12, 2024 * Author: iot00 */ #include <string.h> #include <stdlib.h> #include <ctype.h> #include <stdio.h> #include <stdbool.h> #include "cmsis_os.h" #include "type.h" #include "uart.h" #include "app.h" #include "cmd.h" #include "mem.h" #include "cli.h" static osThreadId_t cmd_thread_hnd; // 쓰레드 핸들 static osMessageQueueId_t cmd_msg_id; //메시지큐 핸들 static const osThreadAttr_t cmd_thread_attr = { .stack_size = 128 * 8, .priority = (osPriority_t) osPriorityNormal, }; static void cmd_msg_put_0(void *arg); // 추가 static void cmd_msg_put(void *arg); void cmd_thread(void *arg) { (void)arg; osStatus sts; MSG_T rxMsg; uart_regcbf(E_UART_0, cmd_msg_put_0); // UART2 콜백 등록 uart_regcbf(E_UART_1, cmd_msg_put); // UART3 콜백 등록 while (1) { sts = osMessageQueueGet(cmd_msg_id, &rxMsg, NULL, osWaitForever); if (sts == osOK) { switch (rxMsg.id) { case E_MSG_CMD_RX_0: { MEM_T *pMem = (MEM_T *)rxMsg.body.vPtr; PKT_T *pRxPkt = (PKT_T *)pMem->buf; printf("E_MSG_CMD_RX_0\\r\\n"); printf("cmd=%04x, len=%d ", pRxPkt->cmd, pRxPkt->len); for (int i = 0; i < pRxPkt->len; i++) { printf("%02x ", pRxPkt->ctx[i]); } printf("\\r\\n"); mem_free(pMem); } break; case E_MSG_CMD_RX: { MEM_T *pMem = (MEM_T *)rxMsg.body.vPtr; PKT_T *pRxPkt = (PKT_T *)pMem->buf; printf("E_MSG_CMD_RX\\r\\n"); switch (pRxPkt->cmd) { case E_CMD_LED: { printf("LED command\\r\\n"); printf("cmd=%04x, len=%d ", pRxPkt->cmd, pRxPkt->len); for (int i = 0; i < pRxPkt->len; i++) { printf("%02x ", pRxPkt->ctx[i]); } printf("\\r\\n"); bool res = false; uint16_t enc_len; uint8_t enc_data[200]; res = slip_encode(pMem->buf, pRxPkt->len + 3, enc_data, &enc_len); if (res == true) { for (uint16_t i = 0; i < enc_len; i++) { printf("%02x ", enc_data[i]); } printf("\\r\\n"); } extern UART_HandleTypeDef huart2; HAL_UART_Transmit(&huart2, enc_data, enc_len, 0xffff); mem_free(pMem); } break; case E_CMD_CLI: { printf("CLI Command\\r\\n"); pRxPkt->ctx[pRxPkt->len] = 0; // '\\0' cli_msg_put((void *)pMem); } break; } } break; } } } } void cmd_init(void) { cmd_msg_id = osMessageQueueNew(3, sizeof(MSG_T), NULL); if (cmd_msg_id != NULL) { printf("CMD Message Queue Created...\\r\\n"); } else { printf("CMD Message Queue Create Fail...\\r\\n"); while (1); } cmd_thread_hnd = osThreadNew(cmd_thread, NULL, &cmd_thread_attr); if (cmd_thread_hnd != NULL) { printf("CMD Thread Created...\\r\\n"); } else { printf("CMD Thread Create Fail...\\r\\n"); while (1); } } // ISR static void cmd_msg_put_0(void *arg) { BUF_T *pBuf = (BUF_T *)arg; MEM_T *pMem = mem_alloc(100, 0); if (pMem != NULL) { memcpy(pMem->buf, pBuf, pBuf->idx); Q_PUT(cmd_msg_id, E_MSG_CMD_RX_0, pMem, 0); } } static void cmd_msg_put(void *arg) { BUF_T *pBuf = (BUF_T *)arg; MEM_T *pMem = mem_alloc(100, 0); // timeout=0 if (pMem != NULL) { memcpy(pMem->buf, pBuf, pBuf->idx); // pMem->buf는 주소 Q_PUT(cmd_msg_id, E_MSG_CMD_RX, pMem, 0); } }
cmd_thread(void *arg)
: 명령을 처리하는 메인 스레드.uart_regcbf(E_UART_0, cmd_msg_put_0)
과uart_regcbf(E_UART_1, cmd_msg_put)
을 통해 UART2와 UART3의 인터럽트 콜백 함수 등록.- 메시지 큐에서 메시지를 꺼내어 적절한 처리 수행 (
E_MSG_CMD_RX_0
과E_MSG_CMD_RX
).
cmd_init(void)
: 명령 처리 스레드와 메시지 큐를 초기화.osMessageQueueNew
로 메시지 큐 생성.osThreadNew
로 명령 처리 스레드 생성.
cmd_msg_put_0(void *arg)
: UART2 (E_UART_0)에서 수신된 데이터를 메시지 큐에 저장.mem_alloc
으로 메모리 할당.memcpy
로 수신된 데이터 복사.Q_PUT
으로 메시지 큐에 데이터 삽입.
cmd_msg_put(void *arg)
: UART3 (E_UART_1)에서 수신된 데이터를 메시지 큐에 저장.
SLIP 인코딩 및 전송
- SLIP 인코딩: 데이터를 SLIP 프로토콜을 통해 인코딩.
bool res = slip_encode(pMem->buf, pRxPkt->len + 3, enc_data, &enc_len);
- 데이터 전송: 인코딩된 데이터를 UART를 통해 전송.
extern UART_HandleTypeDef huart2; HAL_UART_Transmit(&huart2, enc_data, enc_len, 0xffff);
전체적인 구조 및 흐름
레이어
Day08 : LCD Device (HD44780U) + Mutex
1. LCD Device (HD44780U)
- LCD(액정 표시 장치)는 액정(Liquid Crystal)을 사용하여 정보를 표시하는 디스플레이 장치입니다. LCD 디바이스는 일반적으로 휴대전화, 태블릿 컴퓨터, 디지털 시계, 모니터, 텔레비전 등 다양한 전자 제품에 사용됩니다.
HD44780 Datasheet
- 입출력 버퍼(Input/Output Buffer)
- 이곳에서 MCU와의 통신을 통해 데이터를 입력 받습니다.
- 데이터 처리
- 입력 받은 데이터는 폰트가 저장된 주소를 가리키는 위치 데이터를 받고, Character Generator 저장소(RAM&ROM)에서 해당 폰트에 접근하여 LCD로 출력됩니다.
- 폰트 저장소: 사용자 저장 폰트는 64개, 기기 자체에 저장된 폰트는 최대 240개가 저장되어 있습니다.
- 출력 처리: 실제 데이터가 입력되면 해당 데이터는 8비트 주소 데이터로 해당 위치의 폰트에 접근하여, Segment 데이터를 가져와 화면에 출력됩니다.
문자 패턴 출력
- 문자 코드 비트 0부터 2까지는 CGRAM 주소 비트 3부터 5까지에 대응합니다. (3비트: 8종류).
- CGRAM 주소 비트 0부터 2까지는 문자 패턴 라인 위치를 지정합니다. 8번째 라인은 커서 위치이며, 그 표시는 커서와 논리적 OR로 형성됩니다. 커서 표시를 유지하기 위해 커서 표시 위치에 해당하는 8번째 라인 데이터를 0으로 유지해야 합니다. 8번째 라인 데이터가 1이면, 커서의 존재와 관계없이 1 비트가 8번째 라인을 점등합니다.
- 문자 패턴 행 위치는 CGRAM 데이터 비트 0부터 4에 해당합니다. (비트 4는 왼쪽에 위치합니다).
- 문자 코드 비트 4부터 7이 모두 0인 경우 CGRAM 문자 패턴이 선택됩니다. 문자 코드 비트 3은 효과를 주지 않습니다.
2. I2C 통신
- I2C(Inter-Integrated Circuit)는 단일 마스터와 여러 슬레이브 간의 통신을 위해 설계된 양방향 두 개의 와이어 통신 프로토콜입니다.
- 이 프로토콜은 Philips Semiconductor(현 NXP Semiconductors)에 의해 개발되었으며, 저속 주변기기와의 간단한 통신을 위해 주로 사용됩니다.
주요 특징
- 양방향 데이터 전송: 두 개의 라인을 통해 양방향 데이터 전송이 가능.
- 단순한 하드웨어 구조: SDA(데이터)와 SCL(클럭) 두 개의 라인만 사용.
- 다중 슬레이브 지원: 여러 슬레이브 디바이스를 하나의 버스에 연결 가능.
- 주소 지정: 각 슬레이브 디바이스는 고유의 주소를 가짐.
구성 요소
- 마스터(Master): 통신을 시작하고 클럭 신호를 생성하며, 슬레이브 디바이스를 선택.
- 슬레이브(Slave): 마스터의 요청에 응답하는 디바이스.
- SDA(Serial Data Line): 데이터 전송 라인.
- SCL(Serial Clock Line): 클럭 신호 라인.
동작 방식
- 시작(Start) 조건: SDA가 SCL이 높은 상태에서 낮은 상태로 전환.
- 주소 전송: 마스터가 슬레이브의 주소를 전송하고, 슬레이브가 이를 인식하여 응답.
- 데이터 전송: 마스터와 슬레이브 간의 데이터 전송. 각 바이트 전송 후, 수신자는 ACK(인정) 비트를 보냅니다.
- 정지(Stop) 조건: SCL이 높은 상태에서 SDA가 높은 상태로 전환.
통신 단계
- 시작 조건(Start Condition): 통신의 시작을 알림. SDA가 SCL이 높은 상태에서 낮아짐.
- 주소 프레임(Address Frame): 마스터가 슬레이브 주소와 읽기/쓰기 비트를 전송.
- ACK/NACK: 슬레이브가 응답하여 ACK(인정) 또는 NACK(비인정) 비트를 전송.
- 데이터 프레임(Data Frame): 실제 데이터 전송. 각 바이트 전송 후 ACK/NACK 비트 교환.
- 정지 조건(Stop Condition): 통신의 끝을 알림. SCL이 높은 상태에서 SDA가 높아짐.
장점
- 단순한 하드웨어 구조: 두 개의 와이어만 필요.
- 확장성: 여러 슬레이브 디바이스를 쉽게 추가 가능.
- 유연성: 다양한 속도(표준 모드, 고속 모드 등)로 동작 가능.
단점
- 속도 제한: 상대적으로 낮은 속도(최대 3.4 Mbps).
- 버스 충돌 가능성: 여러 디바이스가 동일한 버스를 공유하기 때문에 충돌 가능성이 존재.
3. Mutex
- Mutex는 멀티스레드 환경에서 공유 자원에 대한 접근을 조율하기 위해 사용되는 상호 배제 메커니즘입니다.
- LCD 디바이스와 같이 하나의 리소스를 여러 스레드가 동시에 접근할 경우, 데이터 일관성을 유지하고 충돌을 방지하기 위해 Mutex를 사용합니다.
장점
- 데이터 일관성 보장: 공유 자원에 대한 동시 접근을 방지하여 데이터 일관성을 유지합니다.
- 간단한 구현: 대부분의 프로그래밍 언어에서 라이브러리로 제공되어 쉽게 구현할 수 있습니다.
단점
- 병목 현상: 하나의 스레드가 리소스를 독점하면 다른 스레드들이 대기해야 하므로 성능 저하가 발생할 수 있습니다.
- 교착 상태: 잘못된 설계로 인해 교착 상태(데드락)가 발생할 수 있습니다.
세마포어 (Semaphore)
- 세마포어는 Mutex와 유사하게 상호 배제를 제공하지만, 더 복잡한 시나리오를 처리할 수 있습니다.
- 세마포어는 카운팅 메커니즘을 통해 여러 스레드가 공유 자원에 접근할 수 있도록 합니다.
- 이진 세마포어: Mutex와 비슷하게 동작하며, 한 번에 하나의 스레드만 접근을 허용합니다.
- 카운팅 세마포어: 특정 개수의 스레드가 동시에 자원에 접근할 수 있도록 허용합니다.
- 장점
- 동시 접근 허용: 카운팅 세마포어를 사용하면 여러 스레드가 동시에 자원에 접근할 수 있습니다.
- 단점
- 복잡성: 구현이 복잡하고, 잘못 사용하면 교착 상태나 기아 상태가 발생할 수 있습니다.
큐 (Queue)
- 프로듀서-컨슈머 패턴에서 자주 사용되는 방법으로, 데이터가 준비될 때까지 대기할 수 있도록 합니다. 큐를 사용하여 데이터가 준비되면 이를 LCD로 전송합니다.
- 장점
- 비동기 처리: 프로듀서와 컨슈머가 독립적으로 동작할 수 있어 효율적입니다.
- 버퍼링: 데이터가 일시적으로 버퍼에 저장되므로, 데이터 손실을 방지할 수 있습니다.
- 단점
- 메모리 사용: 큐의 크기에 따라 메모리 사용량이 증가할 수 있습니다.
- 복잡성: 프로듀서-컨슈머 문제를 해결하기 위한 추가적인 동기화가 필요할 수 있습니다.
공유 메모리 (Shared Memory)
- 여러 프로세스가 동일한 메모리 공간을 공유하도록 하여 데이터를 주고받는 방법입니다.
- 속도가 빠르지만, 동기화 문제를 해결해야 합니다.
- 장점
- 고속 통신: 메모리 공간을 직접 공유하므로 속도가 매우 빠릅니다.
- 단점
- 동기화 필요: 데이터 일관성을 유지하기 위해 동기화 메커니즘이 필요합니다.
- 보안 문제: 메모리를 공유하기 때문에 데이터 보호가 어려울 수 있습니다.
4. LCD Device 제어
app.c
app.c
extern TIM_HandleTypeDef htim14; #define TIM_HND htim14 extern UART_HandleTypeDef huart3;
- 외부 변수 선언
- htim14: 타이머 핸들.
- huart3: UART 핸들.
uint16_t get_time(void) { return (uint16_t)__HAL_TIM_GET_COUNTER(&TIM_HND); } void set_time(uint16_t time) { __HAL_TIM_SET_COUNTER(&TIM_HND, time); }
- 타이머 관련 함수
- get_time: 타이머 카운터 값을 읽어오는 함수.
- set_time: 타이머 카운터 값을 설정하는 함수.
extern inline void pin_high(void) { GPIOE->BSRR = (1<<0); // 0x00000001; } void pin_low(void) { GPIOE->BSRR = (1<<(16+0)); //0x00010000; } uint8_t pin_get(void) { return (uint8_t)((GPIOE->IDR & 0x0001) >> 0); }
- GPIO 제어 함수
- pin_high: PE0 핀을 높은 상태(High)로 설정하는 함수.
- pin_low: PE0 핀을 낮은 상태(Low)로 설정하는 함수.
- pin_get: PE0 핀의 현재 상태를 읽어오는 함수.
void pin_out_wait(uint16_t time) { volatile uint16_t start, curr; start = get_time(); while (1) { curr = get_time(); if ((uint16_t)(curr - start) > time) break; } }
- 딜레이 함수
- pin_out_wait: 주어진 시간 동안 대기하는 함수. 타이머를 사용하여 정확한 시간 지연을 구현.
int8_t pin_get_change(uint16_t *time) { volatile uint8_t pin_prev; volatile uint16_t start; pin_prev = pin_get(); // 현재 핀 상태 저장 start = get_time(); // 시작하는 시간 저장 while (1) { if (pin_prev != pin_get()) { // 핀 상태가 변하는가? *time = get_time() - start; // 변했을 때 핀의 상태가 유지된 시간 break; } else { if (get_time() - start > 150) return -1; // 150us 이상 변화가 없으면 타임아웃 } } return !pin_get(); // 핀 상태 리턴 }
- 핀 상태 변화 감지 함수
- pin_get_change: 핀 상태가 변화하는지 감지하고, 변화가 발생하면 그 시간을 기록.
typedef struct { int8_t sts; uint16_t time; } PIN_T;
- DHT11 데이터 읽기 구조체 정의
- PIN_T: DHT11 센서의 신호 상태와 시간을 저장하는 구조체.
void app(void) { PIN_T pin_sts[100]; // DHT11에서 출력되는 시그널을 저장하기 위한 변수 uint8_t data[5]; // pin_sts에 저장된 시그널을 분석해서 바이트 단위로 저장하기 위한 변수 uint8_t i, j, k, l; int8_t err; uint8_t checksum; printf("System Start!\\n"); HAL_TIM_Base_Start(&TIM_HND); // 타이머 16비트, 1us 단위로 설정 while (1) { if (getkey() == 1) { memset(data, 0, 5); err = 0; pin_low(); pin_out_wait(18000); // 18ms 대기 pin_high(); pin_out_wait(40); // 40us 대기 for (i = 0; i < 83; i++) { // 83개 신호 읽기 pin_sts[i].sts = pin_get_change(&pin_sts[i].time); if (pin_sts[i].sts == -1) { err = -1; // 센서 응답 끝이나 응답이 없을 때 break; } } printf("err code = %d\\n", err); printf("i = %d\\n", i); if (i < 83) { printf("read bit error....\\n"); } else { for (j = 0; j < i; j++) { printf("%2d, %2d, %6d\\n", j, pin_sts[j].sts, pin_sts[j].time); } l = 0; k = 0; for (j = 3; j < i; j += 2) { if (pin_sts[j].time > 50) { data[l] |= (0x80 >> k); } k++; k %= 8; // 8비트 단위 if (k == 0) { // 다음 바이트로 l++; if (l >= 5) break; // 5바이트 넘으면 끝. } } printf("result------\\n"); for (i = 0; i < l; i++) { printf("[%3d]%3d,%02x\\n", i, data[i], data[i]); } checksum = 0; for (i = 0; i < 4; i++) { checksum += data[i]; } if (checksum != data[4]) { printf("Checksum error\\n"); } else { printf("Checksum ok!\\n"); printf("Humidity:%d.%d%%\\n", data[0], data[1]); printf("Temperature:%d.%dC\\n", data[2], data[3]); } } } } }
- 주요 애플리케이션 함수
- app: 메인 애플리케이션 함수로, DHT11 센서로부터 데이터를 읽고, 온도와 습도를 계산하여 출력합니다.
- pin_sts: DHT11 센서로부터 읽어들인 신호 상태와 시간을 저장.
- data: 신호를 분석하여 5바이트 단위로 저장.
- getkey: 버튼 입력 확인.
- pin_low, pin_high: DHT11 센서와 통신을 시작하고, 데이터를 읽어들임.
- pin_get_change: 핀 상태 변화 감지.
- Checksum: 데이터의 무결성 확인.
#if 1 // CubeIDE int _write(int file, char *ptr, int len) { (void)file; HAL_UART_Transmit(&huart3, (uint8_t *)ptr, len, 0xffff); return len; } #else // Keil int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 5); return ch; } #endif
- UART 출력 함수
- _write: CubeIDE 환경에서 UART로 데이터를 전송하는 함수.
- fputc: Keil 환경에서 UART로 데이터를 전송하는 함수.
실습 코드
Share article