[Intel] 펌웨어 프로그래밍

인텔 엣지 AI S/W 아카데미 4기 - '펌웨어 프로그래밍' 수업 정리
조수환's avatar
Apr 24, 2024
[Intel] 펌웨어 프로그래밍

개요

  • 저항과 캐퍼시터의 칩 통합
    • 저항 및 캐퍼시터는 회로 설계에서 필수적인 부품으로, 일부 칩 내부에 통합하여 설계 간소화 및 성능 향상을 도모할 수 있습니다.
  • 아두이노와 실제 제품 제작의 한계
    • 아두이노는 프로토타이핑 도구로 유용하지만, 실제 제품 제작에는 성능, 안정성, 확장성 측면에서 제한이 있어 적합하지 않습니다.

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 라이브러리의 주요 장점
      1. 이식성: 다양한 STM32 제품군 간 코드 재사용이 가능
      1. 유지보수성: 하드웨어 변경 시 코드 수정 최소화
      1. 생산성 향상: 복잡한 하드웨어 설정을 단순화

GPIO (General-Purpose Input/Output)

notion image
  • GPIO는 마이크로컨트롤러에서 가장 기본적인 입출력 인터페이스입니다.
  • GPIO 포트는 디지털 신호를 입력받거나 출력할 수 있으며, 다양한 주변 장치와의 상호작용에 사용됩니다.
  • 각 GPIO 핀은 독립적으로 설정할 수 있으며, 입력 또는 출력 모드로 동작할 수 있습니다.

2. 레지스터를 사용한 GPIO Port 제어

CubeMX Configuration

  1. 제어하고자 하는 Pin 이름에 대한 Port 이름과 번호를 회로도에서 찾는다. (ex. LD1 → PB0)
    1. notion image
  1. STM32CubeMX의 .ioc 파일에서 회로도에서 찾은 Port 이름과 번호를 GPIO로 설정한다.
    1. notion image
  1. 데이터시트에서 해당 Port와 Pin의 시작 주소를 찾는다. (ex. 0x4002 0400)
      • GPIO Port 시작 주소 찾기
        • notion image
          특성
          AHB (Advanced High-performance Bus)
          APB (Advanced Peripheral Bus)
          주 용도
          고속 데이터 전송
          저속 주변 장치 인터페이스
          속도
          고속
          저속
          복잡성
          복잡함 (파이프라인 방식)
          단순함 (비파이프라인 방식)
          클럭 에지
          단일 클럭 에지
          단일 클럭 사이클
          데이터 버스 폭
          32비트 또는 64비트
          8비트 또는 16비트
          전력 소비
          상대적으로 높음
          낮음
          마스터/슬레이브
          다중 마스터 지원
          단일 마스터
          notion image
      • GPIO Pin 레지스터 찾기
        • notion image
  1. 데이터시트에서 제어할 레지스터의 오프셋을 더한 후 직접 레지스터를 제어한다.
    1. #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)

  • 콜백 함수는 프로그램의 흐름을 제어하기 위해 다른 코드에 의해 호출되는 함수입니다.
  • 임베디드 시스템에서 특히 중요하게 사용되며, 비동기 이벤트 처리와 실시간 응답을 가능하게 합니다.
    • notion image
  • 콜백 함수의 특징
      1. 함수 포인터로 구현: 함수 포인터를 통해 전달되어 필요할 때 호출
      1. 비동기 처리: 비동기적인 이벤트 처리에 유용
      1. 플러그인 구조: 모듈 간의 의존성을 낮추고, 유연성을 높이는 플러그인 구조 구현 가능
  • 콜백 함수의 주요 응용
      1. 비동기 이벤트 처리
          • 다양한 외부 장치와 상호작용하며, 입력이나 상태 변화에 즉시 대응할 수 있습니다.
      1. 하드웨어 인터럽트 처리
          • 인터럽트 발생 시 호출되는 콜백 함수로 신속한 응답이 가능합니다.
      1. 낮은 CPU 사용률
          • 이벤트 발생 시에만 실행되어 대기 상태에서 CPU 사용률을 낮출 수 있습니다.
      1. 모듈화와 확장성
          • 모듈 간 결합도를 낮추어 새로운 기능 추가 시 콜백 함수만 변경하면 되므로 확장성이 높아집니다.
      1. 실시간 처리
          • 실시간 응답이 필요한 시스템에서 이벤트 발생 즉시 처리할 수 있습니다.
  • 일반 함수와 콜백 함수의 차이
    • 구분
      일반 함수
      콜백 함수
      호출 방식
      명시적으로 호출.
      특정 이벤트나 조건 발생 시 시스템에 의해 자동으로 호출.
      호출 시점
      개발자가 원하는 시점에 호출.
      시스템의 특정 이벤트 발생 시 호출.
      실행 흐름
      일반적인 함수 호출-실행-리턴 순서.
      외부 시스템에 의해 실행 흐름이 콜백 함수로 전달.
      종속성
      함수 간 종속성이 높음.
      콜백 함수와 호출 측 함수 간 종속성이 낮음.
      유연성
      결합도가 높아 변경이 어려움.
      결합도가 낮아 유연한 변경이 가능.

2. Edge Detection

  • 엣지는 신호가 변화하는 지점을 의미하며, 주로 두 가지 종류가 있습니다.
    • notion image
      1. Rising Edge(상승 엣지) : 신호가 낮은 상태 (Low)에서 높은 상태 (High)로 변화하는 지점.
      1. Falling Edge(하강 엣지) : 신호가 높은 상태 (High)에서 낮은 상태 (Low)로 변화하는 지점.

타이밍 다이어그램에서의 엣지

notion image
  • 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가 주기적으로 특정 조건이나 상태를 확인하여 원하는 이벤트가 발생했는지 확인하는 방식입니다.
    • notion image
  • 폴링 방식은 비블로킹(non-blocking) 방식으로, 다른 작업을 수행하면서 주기적으로 조건을 확인할 수 있습니다.
    • 구분
      HAL_Delay 방식
      Polling 방식
      블로킹 여부
      블로킹 (Blocking)
      비블로킹 (Non-blocking)
      CPU 사용 효율
      낮음 (지연 동안 CPU 유휴 상태)
      높음 (다른 작업과 병행 가능)
      구현 복잡도
      낮음 (간단한 사용)
      높음 (주기적인 상태 확인 로직 필요)
      응답성
      낮음 (지연 시간 동안 응답 없음)
      높음 (다른 이벤트에 즉시 반응 가능)
      전력 소비
      높음 (CPU 유휴 상태)
      낮음 (CPU가 다른 작업 수행 가능)
  • 현대 운영체제에서 사용하는 Interrupt를 사용한 방식이나 DMA를 이용한 방식에 비해서 비효율적인 방식입니다.

4. Polling 방식 버튼 감지 구현

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_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; }
    • ADC 값이 업데이트될 때 호출되어 값을 출력합니다.
    • 이 함수는 ADC 값이 변경되었을 때 적절한 처리를 수행합니다.

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가 경과하면, 스레드 구조체의 카운터를 업데이트합니다.
    • 작업 실행
      • 주기가 만료된 작업은 플래그를 설정하고, 설정된 플래그를 확인하여 해당 작업의 콜백 함수를 호출합니다.
    • 루프 반복
      • 모든 작업을 처리한 후, 루프를 반복하여 지속적으로 작업을 확인하고 실행합니다.

전체적인 구조 및 흐름

  1. 초기화
      • app.c에서 button_init()adc_init() 함수를 호출하여 버튼과 ADC 드라이버를 초기화합니다.
      • button_regcbf()adc_regcbf() 함수를 통해 버튼과 ADC의 콜백 함수를 등록합니다.
  1. 주기적인 체크
      • app.c의 메인 루프에서 주기적으로 button_check()adc_check() 함수를 호출하여 버튼과 ADC 값을 확인하고 업데이트합니다.
      • 각 함수는 버튼과 ADC의 상태를 확인하고, 등록된 콜백 함수를 호출하여 이벤트를 처리합니다.
  1. 이벤트 처리
      • 버튼 상태 변화와 ADC 값 변경 시 등록된 콜백 함수가 호출되어 해당 이벤트를 처리합니다.
      • 콜백 함수는 버튼이 눌리거나 떼어질 때, 그리고 ADC 값이 변경될 때 적절한 처리를 수행합니다.
  1. 반복 실행
      • 시스템은 메인 루프를 통해 지속적으로 버튼과 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

  1. 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)
  1. Stack Pointer 초기화
  • 초기화 코드는 스택 포인터를 초기화하여 스택이 사용할 메모리 공간을 설정합니다.
    • Reset_Handler: ldr sp, =_estack /* set stack pointer */
  1. 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
  1. 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
  1. C 런타임 환경 설정
  • C/C++ 프로그램이 올바르게 실행되기 위해 필요한 환경을 설정합니다.
    • 예를 들어, 전역 및 정적 생성자를 호출하거나, C++ 객체를 초기화합니다.
    • /* Call static constructors */ bl __libc_init_array
  1. 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)
  1. Main 함수 호출
  • 모든 초기화 작업이 완료되면, startup code는 메인 애플리케이션 코드의 시작점인 main() 함수를 호출합니다.
    • 이 시점부터 애플리케이션 코드가 실행을 시작합니다.
    • /* Call the application's entry point. */ bl main bx lr
  1. 기타 초기화 작업
  • MCU에 따라 추가적으로 필요한 초기화 작업이 있을 수 있습니다.
    • 예를 들어, 클럭 설정, GPIO 초기화, 주변 장치 설정 등이 있을 수 있습니다.
      • 이 코드에서는 SystemInit 함수가 클럭 설정 등의 시스템 초기화를 담당합니다.
      /* Call the clock system initialization function. */ bl SystemInit

BIOS(Basic Input/Output System) & 부트로더(Boot Loader)

notion image
  • BIOS는 컴퓨터가 켜졌을 때 가장 먼저 실행되며, 하드웨어 초기화 및 운영 체제를 로드하는 과정을 담당합니다.
  • 부트로더는 시스템 시작 시 가장 먼저 실행되며, 메인 애플리케이션 코드가 실행되기 전에 다양한 초기화 및 준비 작업을 수행합니다.

BIOS, 부트로더, Startup Code의 차이

항목
BIOS
부트로더(Bootloader)
Startup Code
목적
하드웨어 초기화 및 부트로더 실행
펌웨어 업데이트, 검증, 애플리케이션 선택 및 실행
기본 초기화 및 애플리케이션 실행 준비
위치
마더보드의 플래시 메모리
독립적인 메모리 섹션 (주로 플래시 메모리)
애플리케이션 코드의 시작 부분
초기화 작업
전반적인 하드웨어 초기화, POST 수행
기본 하드웨어 초기화, 클럭 설정, GPIO 설정 등
스택 포인터 초기화, 데이터/BSS 섹션 초기화, 인터럽트 벡터 테이블 설정
추가 기능
부팅 순서 설정, 기본 입출력 기능 제공
펌웨어 업데이트, 보안 부팅, 디버깅 등
없음
실행 시점
전원 켜짐 시, 가장 먼저 실행
MCU 리셋 또는 전원 켜짐 시, startup code 실행 전에
MCU 리셋 또는 전원 켜짐 시
종료 시점
부트로더 호출 후 종료
애플리케이션 코드로 점프 후 종료
main() 함수 호출 후 종료

2. 인터럽트(Interrupt)

  • 인터럽트(Interrupt)는 컴퓨터 시스템에서 중요한 개념으로, 현재 실행 중인 작업을 잠시 멈추고 다른 중요한 작업을 처리한 후 다시 원래 작업으로 복귀하도록 하는 메커니즘입니다.
  • 인터럽트는 하드웨어와 소프트웨어에서 모두 발생할 수 있으며, 시스템의 효율성과 반응성을 크게 향상 시킵니다.

인터럽트의 종류

  • 하드웨어 인터럽트
    • 키보드를 누르거나 마우스를 클릭하는 것, 네트워크 패킷 수신 등이 있습니다.
  • 소프트웨어 인터럽트
    • 시스템 호출이나 예외 상황(예: 분할 오류, 페이지 폴트) 등이 있습니다.

인터럽트 처리 과정

notion image
  1. 인터럽트 발생
      • 하드웨어 장치나 소프트웨어 명령이 인터럽트를 발생시킵니다.
  1. 현재 작업 중단
      • CPU는 현재 실행 중인 작업을 중단하고, 인터럽트 요청을 확인합니다.
  1. 인터럽트 벡터 테이블 참조
      • 인터럽트 처리 루틴의 주소를 저장한 인터럽트 벡터 테이블을 참조합니다.
  1. 인터럽트 서비스 루틴(ISR) 실행
      • 인터럽트 벡터 테이블에 정의된 ISR을 실행하여 인터럽트를 처리합니다.
  1. 원래 작업 복귀
      • ISR 실행이 완료되면 CPU는 원래의 작업으로 복귀합니다.

인터럽트와 폴링 비교

항목
인터럽트(Interrupt)
폴링(Polling)
작동 방식
이벤트가 발생할 때 시스템이 이를 처리하도록 중단
주기적으로 상태를 확인하여 이벤트를 처리
CPU 사용 효율성
높음 이벤트가 발생할 때만 CPU가 사용됨
낮음 CPU가 주기적으로 상태를 확인하며 자원 소모
반응 시간
빠름 이벤트 발생 시 즉시 처리
느림 폴링 주기에 따라 지연될 수 있음
구현 복잡성
비교적 복잡 인터럽트 처리기 작성 필요
비교적 단순 상태 확인 루프 작성 필요
시스템 부하
낮음 필요할 때만 처리
높음 주기적으로 상태를 확인해야 함
전력 소모
낮음 유휴 상태에서 대기 가능
높음 계속해서 상태를 확인해야 함
사용 예
실시간 시스템, 키보드 입력 처리, 네트워크 패킷 수신
간단한 장치 상태 확인, 주기적인 센서 데이터 수집

3. UART(Universal Asynchronous Receiver/Transmitter)

  • UART는 두 장치 간 비동기 직렬 통신을 지원하는 방식입니다.

주요 구성 요소

  1. 송신기 (Transmitter): 데이터를 직렬로 전송.
  1. 수신기 (Receiver): 직렬 데이터를 병렬 데이터로 변환하여 수신.

주요 특성

  • 비동기 통신: 클럭 신호를 공유하지 않으며, 시작 비트와 정지 비트로 동기화.
  • 데이터 프레임
    • 1비트의 시작 비트
    • 5-9비트의 데이터 비트
    • 선택적 패리티 비트
    • 1-2비트의 정지 비트
  • 패리티 비트: 오류 검출용 (홀수/짝수 패리티).
  • 보드레이트(Baud Rate): 초당 전송되는 비트 수, 송신기와 수신기가 동일하게 설정.
  • 전이중 방식(Full Duplex): 동시에 송수신 가능.
notion image

장점과 단점

  • 장점
    • 간단한 하드웨어 구성
    • 소프트웨어 구현 용이
    • 다양한 보드레이트 지원
  • 단점
    • 낮은 통신 속도
    • 긴 거리 통신에 부적합
    • 클럭 신호 부재로 인한 동기화 어려움

STM32 NUCLEO-F429XX

notion image
notion image

CubeMX Configuration

캡처 후 추가

4. 인터럽트 방식 스레드 구현

adc.c, io.c, button.c수정

io.c
  • EXTI 초기화 함수
    • void io_exti_init(void) { // EXTI 객체 배열 초기화 for (int i = 0; i < D_IO_EXTI_MAX; i++) { gIOExtiObjs[i].port = NULL; gIOExtiObjs[i].pin = 0; gIOExtiObjs[i].cbf = io_exti_dummy; // 기본 콜백 함수 할당 } // 사용자 버튼에 대한 EXTI 설정 gIOExtiObjs[13].port = USER_Btn_GPIO_Port; // 사용자 버튼 포트 설정 gIOExtiObjs[13].pin = USER_Btn_Pin; // 사용자 버튼 핀 설정 }
  • EXTI 콜백 함수 등록 함수
    • bool io_exti_regcbf(uint8_t idx, IO_CBF_T cbf) { if (idx >= D_IO_EXTI_MAX) return false; // 인덱스 범위 확인 gIOExtiObjs[idx].cbf = cbf; // 콜백 함수 등록 return true; }
  • GPIO EXTI 인터럽트 콜백 함수
    • void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { for (volatile uint16_t i = 0; i < D_IO_EXTI_MAX; i++) { // EXTI 객체 배열에서 핀 상태 읽기 volatile GPIO_PinState sts = HAL_GPIO_ReadPin(gIOExtiObjs[i].port, gIOExtiObjs[i].pin); // 인터럽트가 발생한 핀에 대한 콜백 함수 호출 if (GPIO_Pin & (0x01 << i)) { gIOExtiObjs[i].cbf((uint8_t)sts, (void *)&i); } } }
  • 더미 콜백 함수
    • static void io_exti_dummy(uint8_t rf, void *arg) { (void)rf; // 경고를 없애기 위해 사용되지 않는 인수들 명시 (void)arg; // 경고를 없애기 위해 사용되지 않는 인수들 명시 }
  • 변경점
      1. 폴링 → 인터럽트
          • 기존에는 polling_update 함수를 주기적으로 호출하여 GPIO 핀 상태를 확인했으나, 이제 인터럽트 방식으로 변경되어 필요 시에만 핀 상태를 확인합니다.
      1. 인터럽트 초기화 및 콜백 등록
          • EXTI 핀에 대한 초기 설정을 수행하고, 특정 핀에 대한 콜백 함수를 등록할 수 있습니다.
      1. GPIO 인터럽트 콜백 함수
          • GPIO 핀에 인터럽트가 발생했을 때, 등록된 콜백 함수가 호출되어 핀 상태에 따른 처리를 수행합니다.
button.c (adc.c)
  • 버튼 초기화 함수
    • void button_init(void) { prev_tick = HAL_GetTick(); // 현재 틱 값을 가져옴 io_exti_regcbf(D_USER_BTN_EXTI_NO, button_callback_13); // 버튼 콜백 함수 등록
  • 버튼 스레드 함수
    • void button_thread(void *arg) { if (flag == true) { flag = false; // 플래그 초기화 if (button_no == D_USER_BTN_EXTI_NO) { printf("rf:%d, no:%d\\r\\n", edge_rising_or_falling, button_no); // 버튼 상태 출력 } } }
  • 버튼 콜백 함수
    • static void button_callback_13(uint8_t rf, void *arg) { volatile uint32_t curr_tick = HAL_GetTick(); // 현재 틱 값을 가져옴 if (curr_tick - prev_tick > 120) { // 디바운스 처리 (120ms 이상) prev_tick = curr_tick; // 이전 틱 값 업데이트 edge_rising_or_falling = rf; // 상승 또는 하강 에지 기록 button_no = *(uint16_t *)arg; // 버튼 번호 기록 flag = true; // 상태 변화 플래그 설정 } }
  • 변경점
      1. 폴링 → 인터럽트
      1. 디바운스 처리
          • button_callback_13 함수에서 디바운스 처리를 통해 120ms 이상 간격으로만 버튼 상태 변화를 처리합니다.
      1. 전역 변수 사용
          • 버튼 상태 변화를 기록하는 전역 변수(flag, edge_rising_or_falling, button_no, prev_tick)를 사용하여 인터럽트 발생 시 상태를 기록하고, button_thread 함수에서 이를 출력합니다.
      1. 코드 단순화
          • 버튼 초기화 함수(button_init)와 콜백 함수 등록 함수(io_exti_regcbf)를 통해 버튼 상태 변화를 처리하는 구조로 단순화되었습니다.

Day01~02 app.cpolling.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_callbackbutton_callback2가 정의되어 카운터에 따라 콜백 함수가 변경됩니다.
      • ADC 콜백 함수 adc_callback이 추가되었습니다.
    • 스레드 구조체 정의 및 초기화
      • THR_T 구조체가 정의되고 gThrObjs 배열이 초기화되었습니다.
      • 스레드 배열에 주기, 카운트, 플래그 및 콜백 함수 포인터가 포함되었습니다.
    • 초기화 함수 추가
      • init 함수가 추가되어 버튼과 ADC를 초기화하고, 콜백 함수를 등록합니다.
    • 애플리케이션 메인 함수 변경
      • 무한 루프가 1ms 단위로 현재 시간을 체크하고, 주기가 만료된 작업을 확인합니다.
      • 스레드 배열을 순회하면서 각 스레드의 카운트를 업데이트하고, 주기가 다 된 스레드는 플래그를 설정합니다.
      • 플래그가 설정된 스레드의 콜백 함수를 호출하고, 다음 스레드로 이동합니다.

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); }
    • UART 버퍼 객체들을 초기화하고 인터럽트 기반 수신을 시작합니다.
      • 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; // 플래그 설정 } } } }
    • UART에서 데이터를 수신할 때 호출됩니다.
      • rxdata에 저장된 수신 데이터를 rxd 변수에 복사합니다.
      • 다음 1바이트를 계속 수신하기 위해 HAL_UART_Receive_IT 함수를 다시 호출합니다.
      • gBufObjs 배열의 첫 번째 버퍼 객체를 가리키는 포인터 p를 사용하여 수신된 데이터를 저장합니다.
      • 플래그가 설정되지 않은 경우에만 데이터를 버퍼에 저장합니다.
      • 버퍼 인덱스를 증가시키고, 수신한 데이터가 개행 문자일 경우 플래그를 설정하여 데이터 수신이 완료되었음을 나타냅니다.

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

  • 타이머는 클록 신호를 받아 카운터와 비교 레지스터 등을 이용해 주기적인 인터럽트를 발생시키는 장치입니다.

타이머의 주요 구성 요소 및 역할

notion image

Prescaler (PSC) - 16비트

  1. 역할 및 필요성
      • 타이머의 입력 클럭 주파수를 낮추는 레지스터
      • CPU의 클럭 주파수가 너무 높아서 전처리를 통해 클럭을 낮춰야 할 때 사용
      • 예를 들어, SYSCLK이 168MHz라면 168마이크로초마다 1번의 클럭을 발생시키는데, 이를 조정하기 위해 사용
  1. 동작 방식
      • 0에서 65535 사이의 값을 이용하여 입력 클럭을 나눔
      • 0으로 나눌 수 없으므로 초기값에 1이 더해짐. 예를 들어, 84로 나누기 위해서는 83을 입력
      • 이렇게 생성된 클럭이 카운터의 동작 클럭 (CK_CNT)이 됨
  1. 역할:
      • 타이머 카운터의 증가 속도를 조절
      • 타이머 주기 설정 범위를 확대
      • 타이머의 분해능(resolution)을 개선

Auto Reload Register (ARR) - 16비트 → 높낮이/명암 변화 주기

  1. 역할
      • 0에서 65535 사이의 값을 입력 가능
      • 업 카운터의 경우, CNT가 ARR 값에 도달하면 0으로 돌아가며 반복
      • 다운 카운터의 경우, CNT가 0에 도달하면 ARR 값까지 카운트하며 반복
      • 업-다운 카운터의 경우, CNT가 ARR 값에 도달하면 감소하고, 0에 도달하면 다시 증가하며 반복
  1. 업데이트 이벤트 발생 조건
      • 업 카운터: CNT가 0이 될 때 오버플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생
      • 다운 카운터: CNT가 ARR 값이 될 때 언더플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생
      • 업-다운 카운터: CNT가 0 또는 ARR 값이 될 때 오버플로우/언더플로우, 업데이트 이벤트(UEV), 업데이트 인터럽트 플래그(UIF) 발생

Capture/Compare Register (CCR) → 크기/밝기

  1. 카운터 모드: 출력 비교 모드(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 값이 영향이 없으면 일반적인 타이머와 동일
  1. 카운터 모드: 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 값)을 조절하여 다양한 장치의 속도, 각도, 밝기 등을 제어할 수 있습니다.
notion image

PWM 제어 원리

  1. PWM 제어의 의미
      • PWM을 제어한다는 것은 출력 신호의 HIGH 상태와 LOW 상태의 비율(duty cycle)을 조절하는 것을 의미합니다.
      • 이를 통해 모터의 속도, 각도, LED 밝기 등 다양한 애플리케이션에서 제어가 가능합니다.
  1. PWM 제어 방법
      • 주파수 설정 (ARR 값)
        • 타이머의 Auto-Reload Register(ARR) 값을 설정하여 PWM 신호의 주기를 결정합니다.
      • 듀티 사이클 설정 (CCR 값)
        • 타이머의 Capture/Compare Register(CCR) 값을 설정하여 PWM 신호의 듀티 사이클을 조절합니다.
        • 듀티 사이클은 CCR 값과 ARR 값의 비율로 결정됩니다. (duty cycle = CCR / ARR)

LED 밝기 조절 원리

  1. PWM 주기 설정
      • 타이머의 ARR 값을 설정하여 PWM 신호의 주기를 결정합니다.
      • 예를 들어, ARR 값을 999로 설정하면, PWM 주기는 1000 클럭 사이클이 됩니다.
  1. 듀티 사이클 조절
      • PWM 신호의 HIGH 상태 지속 시간(듀티 사이클)을 조절하여 LED 밝기를 변경합니다.
      • 타이머의 CCR 값을 설정하여 HIGH 상태 지속 시간을 조절합니다.
      • CCR 값이 클수록 HIGH 상태 지속 시간이 길어져 LED 밝기가 높아집니다.
  1. 전압 평균화
      • 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

notion image
  • TIM3 → Channel3 : PWM Generation CH3 → Clock Source : Internal Clock → Parameter Settings → Prescaler : 83, Counter Mode : Up, Counter Period : 999, Pulse : 500
notion image
  • 실제 각 타이머 레지스터 블록에 어떤 값들이 할당되어 있는지 Clock Configuration 메뉴를 통해 편하게 확인이 가능하다.

3. CLI을 이용한 디버깅 및 타이머 활용

ComportMaster를 이용한 디버깅

  • ComPortMaster는 직렬(시리얼) 통신 터미널 프로그램으로, USB-Serial(UART, RS-232, RS-422, RS-485 등) 컨버터를 사용하여 PC와 연결된 가상 COM 포트를 통해 데이터를 송수신할 수 있도록 합니다.
  • 이 프로그램은 포트 설정, 데이터 송신 및 수신, 로그 기록 등 직렬 통신을 위한 다양한 기능을 제공합니다.
notion image

CLI 및 타이머 활용

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 값을 설정합니다.
        1. 인자가 충분한지 확인합니다.
        1. 입력된 duty 값을 정수로 변환합니다.
        1. duty 값이 0~999 범위인지 확인합니다.
        1. 유효한 duty 값이면 timer.ctim_duty_set 함수를 호출하여 설정합니다.
    • cli_dump : 지정된 메모리 주소에서 메모리 덤프를 수행합니다.
        1. 인자가 충분한지 확인합니다.
        1. 주소와 길이를 16진수와 10진수로 변환합니다.
        1. 덤프할 최대 길이를 10으로 제한합니다.
        1. 지정된 주소와 길이만큼 메모리 내용을 16진수와 ASCII 형식으로 출력합니다.
    • cli_mode : 응용 프로그램 모드를 변경합니다.
        1. 인자가 충분한지 확인합니다.
        1. 입력된 모드를 정수로 변환합니다.
        1. app.capp_mode 함수를 호출하여 모드를 변경합니다.
    • cli_led : 지정된 LED의 상태를 변경합니다.
        1. 인자가 충분한지 확인합니다.
        1. LED 번호와 on/off 상태를 파싱합니다.
        1. led.cled_onoff 함수를 호출하여 LED 상태를 설정합니다.
    • cli_echo : 입력된 데이터를 에코 출력합니다.
        1. 인자가 충분한지 확인합니다.
        1. 입력된 데이터를 출력합니다.
    • cli_help : 사용 가능한 명령어와 설명을 출력합니다.
        1. 명령어 목록을 순회하여 각 명령어의 설명을 출력합니다.
  • 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)를 호출하고, argcargv를 전달합니다.
      • 일치하는 명령어가 없으면 "Unsupported Command" 메시지를 출력합니다.

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의 상태를 변경합니다.
        1. led_no가 유효한지 확인합니다. 유효하지 않으면 false를 반환합니다.
        1. gLedObjs 배열에서 해당 LED 객체의 포인터를 설정합니다.
        1. flag에 따라 핀 상태 (GPIO_PIN_SET 또는 GPIO_PIN_RESET)를 설정합니다.
        1. HAL_GPIO_WritePin 함수를 호출하여 LED의 상태를 변경합니다.
        1. LED 상태 변경이 성공하면 true를 반환합니다.

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 타이머 시작 }
    • 타이머와 PWM을 초기화하고 시작합니다.
        1. HAL_TIM_Base_Start 함수를 호출하여 기본 타이머를 시작합니다.
        1. 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 값 출력 }
    • PWM 타이머의 duty 값을 설정합니다.
        1. __HAL_TIM_SET_COMPARE 함수를 호출하여 타이머의 비교 값을 설정합니다.
        1. printf 함수를 사용하여 설정된 duty 값을 출력합니다.

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; } }
  • 변경점
      1. 함수 선언 및 정의 추가
          • app_mode(int mode) 함수가 추가되었습니다.
          • 내부 함수로 app_normal()app_diagnostic()가 추가되었습니다.
          • 함수 포인터 mode_func가 추가되어 모드를 전환할 수 있게 되었습니다.
      1. 애플리케이션 초기화 함수 확장
          • app_init() 함수에서 tim_init()cli_init()을 추가하여 타이머와 CLI 초기화를 수행합니다.
      1. 메인 루프에서 모드 전환 기능 추가
          • app() 함수에서 app_mode(1)을 호출하여 진단 모드를 기본 모드로 설정합니다.
          • 메인 루프에서 mode_func()를 호출하여 현재 모드에 따라 다른 작업을 수행합니다.
      1. 모드별 동작 추가
          • 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는 일반적인 운영 체제와는 다르게, 실시간성을 보장하기 위해 특정한 특성을 갖추고 있습니다.

특징

  1. Deterministic Timing (결정론적 시간성)
      • RTOS는 작업이 항상 정해진 시간 내에 완료되도록 보장합니다.
  1. Task Scheduling (태스크 스케줄링)
      • 우선순위 기반 스케줄링을 통해 중요한 작업이 우선적으로 처리되도록 합니다.
  1. Minimal Interrupt Latency (최소 인터럽트 지연)
      • 인터럽트 처리 시간과 태스크 전환 시간이 최소화되어야 합니다.
  1. Resource Sharing (자원 공유)
      • Mutex나 Semaphore와 같은 동기화 메커니즘을 사용하여 여러 태스크 간 자원 공유를 효율적으로 관리합니다.
  1. Small Footprint (작은 메모리 사용량)
      • 일반적으로 메모리 자원이 제한된 임베디드 시스템에서 사용되기 때문에, RTOS는 메모리 사용량이 작아야 합니다.

CMSIS (Cortex Microcontroller Software Interface Standard)

  • CMSIS는 ARM Cortex-M 프로세서를 기반으로 한 마이크로컨트롤러를 위한 표준 소프트웨어 인터페이스입니다.
  • ARM에서 개발한 CMSIS는 개발자가 하드웨어 종속적인 코드를 쉽게 작성하고, 표준화된 인터페이스를 통해 소프트웨어의 이식성과 재사용성을 높일 수 있도록 돕습니다.

특징

  1. 표준화된 API
      • CMSIS-RTOS는 표준화된 API를 제공하여, RTOS 구현체에 상관없이 일관된 인터페이스로 RTOS 기능을 사용할 수 있습니다.
  1. 이식성
      • 코드의 이식성을 높여, 다른 ARM Cortex-M 마이크로컨트롤러 플랫폼으로 쉽게 전환할 수 있습니다.
  1. 간편한 통합
      • 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)

notion image
  • 스케줄링은 시스템 자원(CPU 시간, 메모리 등)을 효율적으로 사용하기 위해, 여러 작업(Task 또는 Thread)을 어떻게 배치하고 실행할지 결정하는 과정입니다.
  • 스케줄러는 이 역할을 담당하는 소프트웨어 모듈로, 작업의 우선순위와 시스템 상태를 고려하여 실행 순서를 결정합니다.
notion image
  1. SysTick Handler
      • SysTick Handler는 시스템 타이머 (SysTick)에서 발생하는 인터럽트를 처리한다.
      • 주로 시스템의 타이밍과 타임 슬라이스를 관리하는 데 사용된다.
      • 주요 역할로는 주기적인 시스템 타이머 인터럽트를 처리하여 운영체제의 스케줄링 작업을 수행하거나, 타이머에 의한 시스템 틱을 증가시켜 시간 관련 작업을 수행한다.
  1. SVC (Supervisor Call) Handler
      • SVC Handler는 특권 모드에서 사용자 모드로 전환하여 특정 작업을 실행하기 위해 호출되는 핸들러이다.
      • 주로 운영체제의 서비스나 시스템 호출을 처리하는 데 사용된다.
      • SVC 인스트럭션은 특정 서비스를 요청하거나 특권 명령을 실행하기 위해 사용되며, 이를 처리하는 핸들러가 SVC Handler이다.
  1. PendSV (Pending Supervisor Call) Handler
      • PendSV Handler는 PendSV 인터럽트를 처리하는 핸들러이다.
      • PendSV는 소프트웨어에 의해 발생시키는 인터럽트로, 주로 스케줄러나 컨텍스트 스위칭을 구현하는 데 사용된다.
      • PendSV 인터럽트는 주로 스레드 간의 전환을 처리하거나, 프로세스 간의 상태 저장 및 복원을 수행한다. 특히, 멀티태스킹 환경에서 현재 실행 중인 프로세스의 상태를 저장하고 다음 프로세스의 상태를 로드하는 컨텍스트 스위칭에 사용된다.

주요 스케줄링 방식

  1. 선점형 스케줄링 (Preemptive Scheduling)
      • 높은 우선순위의 작업이 실행 중일 때, 낮은 우선순위의 작업을 중단하고 높은 우선순위의 작업을 실행합니다.
      • 응답 시간이 짧고, 실시간 시스템에서 주로 사용됩니다.
  1. 비선점형 스케줄링 (Non-preemptive Scheduling)
      • 작업이 완료될 때까지 CPU를 점유하며, 다른 작업이 CPU를 사용할 수 없습니다.
      • 응답 시간이 길어질 수 있지만, 간단한 구현이 가능합니다.
  1. 라운드 로빈 스케줄링 (Round Robin Scheduling)
      • 각 작업에 동일한 CPU 시간을 할당하고, 순환하며 작업을 실행합니다.
      • 공평한 자원 배분이 가능하지만, 우선순위를 고려하지 않기 때문에 실시간 시스템에는 부적합할 수 있습니다.
  1. 우선순위 기반 스케줄링 (Priority-based Scheduling)
      • 각 작업에 우선순위를 부여하고, 높은 우선순위의 작업을 먼저 실행합니다.
      • 실시간 시스템에서 자주 사용되며, 중요한 작업이 빠르게 처리될 수 있습니다.

TCB(Task Control Block)

 

3. Event Flags를 이용한 스레드간 통신 (메세지 패싱 방식)

  • CMSIS-RTOS2의 Event Flags는 스레드 간 통신을 위해 제공되는 방법입니다.
notion image

과정

  1. Event Flag Set 및 대기
      • 특정 스레드가 Event Flag를 Set하고 대기 상태로 전환됩니다. 이 작업은 주기적으로 수행됩니다.
  1. 다른 스레드의 Flag Wait
      • 다른 스레드는 Flag Wait를 수행하여, 특정 Event Flag가 Set될 때까지 기다립니다. 이 스레드는 이벤트가 발생할 때까지 Waiting 큐에 있습니다.
  1. Event 발생 및 문맥 교환
      • 첫 번째 스레드가 Event Flag를 Set하면, Waiting 큐에 있던 스레드가 Running 상태로 전환됩니다. 이때 주기적으로 Event Flag를 Set하던 스레드는 Waiting 큐로 이동하며, 문맥 교환이 발생합니다.
  1. 작업 완료 후 문맥 교환
      • Running 중이던 스레드의 작업이 끝나면, 해당 스레드는 다시 Event Flag를 Set하는 상태로 돌아가고, Waiting 큐에 있던 스레드와 문맥 교환이 일어납니다.

FreeRTOS에서의 OSEventFlags 사용법

  • osThreadNew(): 스레드 함수를 입력받아 활성화(Ready 상태)하는 함수
    • osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);
    • Parameters
      • func: 스레드 함수
      • argument: 스레드 함수에 전달되는 인수
      • attr: 스레드의 속성 (NULL 시 기본값 사용)
  • osEventFlagsNew(): 이벤트 플래그를 생성하는 함수
    • osEventFlagsId_t osEventFlagsNew(const osEventFlagsAttr_t *attr);
    • Parameters
      • attr: 이벤트 플래그의 속성
  • osEventFlagsWait(): 지정된 이벤트 플래그가 Set될 때까지 대기하는 함수
    • uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);
    • Parameters
      • ef_id: osEventFlagsNew로 얻은 이벤트 플래그 식별자
      • flags: 대기할 플래그
      • options: 플래그 옵션 (예: osFlagsWaitAny, osFlagsWaitAll)
      • timeout: 타임아웃 값 (예: osWaitForever)
  • osEventFlagsSet(): 지정된 이벤트 플래그를 Set하여 문맥 교환을 발생시키는 함수
    • uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags);
    • Parameters
      • ef_id: 이벤트 플래그 식별자
      • flags: Set할 플래그

CubeMX Configuration

notion image
  • Middleware Software Pack → FREERTOS → Interface : CMSIS_V2 → Advanced Setting Newlib: Enable

CMSIS-RTOS2 - Event Flags 구현

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; } } } }
  • 변경점
      1. uart_thread 함수 제거
          • uart_thread 함수는 콜백 함수 호출이 ISR 내부에서 직접 호출되도록 변경되었기 때문에 주석 처리되었습니다.
      1. 2HAL_UART_RxCpltCallback 함수 수정
          • 콜백 함수 호출 위치 변경
            • 이전에는 uart_thread 함수에서 콜백 함수를 호출했으나, 이제는 메시지 종료 문자가 수신되었을 때 ISR 내에서 직접 호출합니다.
            • 이를 통해 콜백 함수 호출이 즉시 이루어지며, 메시지 처리 지연이 줄어듭니다.
          • 버퍼 인덱스 및 플래그 초기화
            • 메시지 처리가 완료된 후 ISR 내에서 버퍼 인덱스와 플래그를 초기화합니다.
            • 이는 다음 메시지 수신을 준비하기 위한 것입니다.
          • ISR 내에서 데이터 수신 설정
            • HAL_UART_Receive_IT 함수 호출을 통해 다음 데이터를 수신할 수 있도록 계속해서 인터럽트를 설정합니다.
      1. uart_thread에서 불필요한 버퍼 재설정 제거
          • 버퍼 재설정 및 콜백 호출이 ISR 내부에서 처리되므로 uart_thread 함수에서 해당 로직이 더 이상 필요하지 않습니다.

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); }
  • 변경점
      1. CMSIS-RTOS2 사용
          • cmsis_os.h를 포함하여 RTOS 기능을 사용할 수 있도록 하였습니다.
          • RTOS 쓰레드 핸들(osThreadId_t)과 이벤트 플래그 핸들(osEventFlagsId_t)을 선언하였습니다.
      1. polling_init 함수
          • 이벤트 플래그 생성
            • osEventFlagsNew 함수를 사용하여 이벤트 플래그를 생성합니다. 성공 여부를 체크하여 적절한 메시지를 출력합니다.
          • 폴링 쓰레드 생성
            • osThreadNew 함수를 사용하여 폴링 쓰레드를 생성합니다. 성공 여부를 체크하여 적절한 메시지를 출력합니다.
              • polling_thread_attr로 쓰레드 속성을 정의하여 스택 크기와 우선 순위를 설정하였습니다.
      1. 이전 폴링 함수 제거
          • 이전의 polling_update 함수와 polling_thread 함수는 RTOS 기반 구현으로 대체되었습니다.
      1. 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.cgpio.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 수정

#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 수정

#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로 전송합니다.

Day06 : Message Queue를 이용한 Uart 데이터 입출력

1. Message Queue

메시지 큐 (Message Queue)

  • 메시지 큐는 실시간 운영체제 (RTOS)에서 태스크 간의 통신을 위해 사용되는 데이터 구조입니다.
  • 메시지 큐는 특히 다중 태스크 환경에서 데이터의 일관성과 동기화를 보장합니다.
      1. 데이터 버퍼링
          • 메시지를 큐에 저장하여 태스크 간에 데이터를 안전하게 주고받을 수 있습니다.
      1. 동기화
          • 생산자 태스크가 데이터를 큐에 추가하고, 소비자 태스크가 큐에서 데이터를 제거하는 방식으로 동기화를 보장합니다.
      1. 데이터 보호
          • 큐를 통한 데이터 전송은 데이터 보호와 무결성을 유지합니다.
  • 메시지 큐는 CMSIS-RTOS2와 같은 RTOS에서 주로 사용되며, osMessageQueue API를 통해 관리됩니다.

메모리 풀 (Memory Pool)

  • 메모리 풀은 고정 크기의 메모리 블록을 미리 할당해 두고 필요할 때마다 이 블록들을 재사용하는 방식으로 동작합니다.
  • 메모리 풀을 사용하면 동적 메모리 할당과 해제의 오버헤드를 줄일 수 있으며, 특히 메시지 큐와 같은 빈번한 메모리 할당이 필요한 경우 유용합니다.
      1. 고정 시간 메모리 할당/해제
          • 메모리 풀은 미리 할당된 블록을 사용하므로 메모리 할당과 해제에 소요되는 시간이 일정합니다.
      1. 메모리 단편화 방지
          • 메모리 풀은 고정 크기의 블록을 사용하여 메모리 단편화를 방지합니다.
      1. 효율적인 메모리 관리
          • 빈번한 메모리 할당과 해제에 효율적으로 대응합니다.

__attribute__((packed)) 사용

  • __attribute__((packed))는 구조체 멤버 변수를 최소한의 공간만 차지하도록 강제하는 컴파일러 속성입니다.
      1. 메모리 사용량 절감
          • 구조체의 메모리 사용량을 줄일 수 있습니다.
      1. 직접 전송 가능
          • 구조체를 네트워크 프로토콜이나 파일 포맷 등으로 직접 전송할 수 있습니다.
      1. 특정 메모리 위치에 매핑
          • 하드웨어 레지스터와 같은 특정 메모리 위치에 구조체를 매핑할 수 있습니다.
  • 이 속성은 특히 메모리 큐와 메모리 풀을 사용할 때 유용합니다.
    • 메모리 큐의 경우 구조체를 통해 데이터를 주고받을 때, 메모리 사용량을 최소화할 수 있기 때문입니다.

메시지 큐 + 메모리 풀

  • 메시지 큐와 메모리 풀을 결합하면 태스크 간의 통신을 더욱 효율적으로 관리할 수 있습니다.
  • 메시지를 전송할 때마다 메모리 풀에서 블록을 할당받아 사용하고, 메시지 처리가 끝나면 해당 블록을 다시 메모리 풀에 반환하는 방식입니다.
    • notion image

2. Message Queue를 이용한 Uart 데이터 입출력

button.c 수정

#include <stdbool.h> #include <stdio.h> #include "gpio.h" #include "button.h" #include "uart.h" #define D_BTN_BLUE_NO 13 #define D_BUTTON_MAX 2 static BUTTON_T gBtnObjs[D_BUTTON_MAX]; static void io_exti_btn_blue_callback(uint8_t rf, void *arg); static void uart_btn_callback(void *arg); void button_init(void) { for(int i = 0; i < D_BUTTON_MAX; i++){ gBtnObjs[i].no = 0; gBtnObjs[i].prev_tick = HAL_GetTick(); } io_exti_regcbf(D_BTN_BLUE_NO, io_exti_btn_blue_callback); uart_regcbf(E_UART_0, uart_btn_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[E_BTN_BLUE]; if (p->no == D_BTN_BLUE_NO) { printf("rf:%d, no:%d\r\n", p->edge, p->no); } } void button_proc_uart(void *arg) { BUTTON_T *p = &gBtnObjs[E_BTN_UART]; printf("%c:%d\r\n", p->no, 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[E_BTN_BLUE]; 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); // polling.c의 콜백 호출 } } static void uart_btn_callback(void *arg) // 동작 { BUF_T *pBuf = (BUF_T *)arg; BUTTON_T *p = &gBtnObjs[E_BTN_UART]; p->no = (uint16_t)pBuf->buf[0]; if (p->cbf != NULL) p->cbf((void *)p); }
  • 변경점
    • 새로운 인터페이스 추가 (UART 이벤트 처리)
      • 기존의 버튼 인터페이스 외에 UART 인터페이스도 추가되었습니다.
      • UART 이벤트 발생 시 콜백 함수 uart_btn_callback이 호출됩니다.
    • 버튼 구조체 초기화 개선
      • 기존 코드에서는 단일 버튼에 대해서만 초기화를 수행했습니다.
      • 변경된 코드에서는 두 개의 버튼(또는 인터페이스) 구조체를 초기화합니다.
    • 메시지 큐 추가
      • 메시지 큐를 통해 버튼 및 UART 이벤트를 관리합니다.
    • 콜백 함수 추가 및 수정
      • uart_btn_callback 콜백 함수가 추가되어 UART 이벤트 발생 시 데이터를 처리합니다.
      • button_proc_uart 함수가 추가되어 UART 데이터를 처리합니다.
    • 동작 개선
      • 버튼 및 UART 이벤트를 각각 별도의 처리 함수에서 처리합니다.
      • 콜백 함수 내에서 메시지 큐를 사용하여 데이터를 효율적으로 관리합니다.

cli.c 수정

/* * 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 "uart.h" #include "app.h" #include "led.h" #include "cli.h" #include "type.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 osMessageQId cli_msg_id; // 메시지 큐 핸들 static const osThreadAttr_t cli_thread_attr = { .stack_size = 128 * 8, .priority = (osPriority_t) osPriorityNormal, }; 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[]); static int cli_btn_uart(int argc, char *argv[]); const CMD_LIST_T gCmdListObjs[] = { { "btn", 2, cli_btn_uart, "button(uart)\r\n btn ['a'~'z']" }, { "duty", 2, cli_duty, "led 1 pwm duty\r\n duty [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 } }; //extern UART_HandleTypeDef huart2; static int cli_btn_uart(int argc, char *argv[]) { if (argc < 2) { printf("Err : Arg No\r\n"); return -1; } //HAL_UART_Transmit(&huart2, (uint8_t *)&argv[1][0], 1, 0xffff); printf("cli_btn:%c \r\n", argv[1][0]); return 0; } static int cli_duty(int argc, char *argv[]) { uint16_t duty; if (argc < 2) { printf("Err : Arg No\r\n"); return -1; } duty = (uint16_t)strtol(argv[1], NULL, 10); printf("tim_duty_set(%d)\r\n", duty); //tim_duty_set(duty); return 0; } 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; } 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; } 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; printf("led %ld, %d\r\n", no, onoff); //led_onoff((uint8_t)no, sts); return 0; } 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; } 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; } static void cli_parser(BUF_T *arg); //static void cli_event_set(void *arg); static void cli_msg_put(void *arg); static BUF_T gBufObj[1]; void cli_thread(void *arg) // event flag(상태 전달) -> message queue(데이터 전송) { //uint32_t flags; (void)arg; osStatus sts; MSG_T rxMsg,txMsg; txMsg.id = E_MSG_CLI_INIT; osMessageQueuePut(cli_msg_id, &txMsg,0, osWaitForever); while (1) { //flags = osEventFlagsWait(cli_evt_id, 0xffff, osFlagsWaitAny, osWaitForever); //if (flags & 0x0001) cli_parser(&gBufObj[0]); sts = osMessageQueueGet(cli_msg_id, &rxMsg, NULL, osWaitForever); if(sts == osOK){ switch (rxMsg.id){ case E_MSG_CLI_INIT:{ printf("CLI Thread Started...\r\n"); printf("Sizeof(MSG_T) = %d\r\n",sizeof(MSG_T)); uart_regcbf(E_UART_1, cli_msg_put); }break; case E_MSG_CLI:{ cli_parser((BUF_T *)rxMsg.body.vPtr); }break; } } } } void cli_init(void) { //cli_evt_id = osEventFlagsNew(NULL); cli_msg_id = osMessageQueueNew(3, sizeof(MSG_T), NULL); if (cli_msg_id != NULL) printf("CLI Message Queue Created...\r\n"); else { printf("CLI Message Queue Create File...\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); //} static void cli_msg_put(void *arg) { BUF_T *pBuf = (BUF_T *)arg; memcpy(&gBufObj[0], pBuf, sizeof(BUF_T)); MSG_T txMsg; txMsg.id = E_MSG_CLI; txMsg.body.vPtr = (void *)&gBufObj[0]; osMessageQueuePut(cli_msg_id, &txMsg, 0, 0); // <- ISR에서는 TImeout 0 } #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; //printf("rx:%s\r\n", (char *)arg); // token 분리 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; i<argc; i++) { // printf("%d:%s\r\n", i, argv[i]); // } 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"); }
  • 변경점
    • 메시지 큐 사용
      • 추가된 메시지 큐 핸들 (osMessageQId cli_msg_id):
        • 이벤트 플래그 대신 메시지 큐를 사용하여 데이터를 전송하고 처리합니다.
      • 메시지 큐 생성
        • cli_msg_id = osMessageQueueNew(3, sizeof(MSG_T), NULL);를 통해 메시지 큐를 생성합니다.
    • 메시지 구조체 (MSG_T)
      • 메시지 큐에서 사용되는 메시지 구조체를 정의합니다.
      • 각 메시지에는 ID와 바디가 포함됩니다.
    • 이벤트 플래그 제거
      • 기존 코드에서 사용된 이벤트 플래그 (osEventFlagsId_t cli_evt_id)가 제거되었습니다.
      • 이벤트 플래그를 사용한 데이터 전송 방식이 메시지 큐를 사용한 방식으로 대체되었습니다.
    • 콜백 함수 및 데이터 처리 함수 변경
      • cli_msg_put 함수 추가
        • UART 콜백에서 호출되며, 수신된 데이터를 메시지 큐에 저장합니다.
      • cli_event_set 함수 제거
        • 이벤트 플래그를 사용한 방식이 제거되었습니다.
    • CLI 스레드 변경
      • cli_thread 함수에서 이벤트 플래그를 대기하는 대신 메시지 큐에서 메시지를 대기합니다.
      • 수신된 메시지에 따라 적절한 처리를 수행합니다.

polling.c 수정

/* * polling.c * * Created on: Apr 15, 2024 * Author: iot00 */ #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); static void btn_uart_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(E_BTN_BLUE, btn_blue_callback); button_regcbf(E_BTN_UART, btn_uart_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__); } if (flags & 0x0008) { printf("%s[0x0008][%d]\r\n", __func__, __LINE__); button_proc_uart(NULL); } } } 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); // 스레드 모드로 전환 } static void btn_uart_callback(void *arg) { osEventFlagsSet(polling_evt_id, 0x0008); // 1-2-4-8 }
  • 변경점
    • UART 버튼 콜백 함수 추가
      • UART 버튼 이벤트를 처리하기 위한 btn_uart_callback 함수가 추가되었습니다.
    • 버튼 등록 및 처리
      • button_regcbf 함수 호출 시 E_BTN_UART에 대해 btn_uart_callback을 등록합니다.
      • 폴링 스레드 내에서 flags & 0x0008 플래그가 설정되면 button_proc_uart 함수를 호출하여 UART 버튼 이벤트를 처리합니다.
    • 이벤트 플래그 설정
      • btn_uart_callback에서 이벤트 플래그 0x0008을 설정하여 UART 버튼 이벤트가 발생했음을 폴링 스레드에 알립니다.

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; //extern UART_HandleTypeDef huart2; #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; // } // } //} // 인터럽트 서비스 루틴 (ISR) 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]; // p->buf[p->idx] = rxd; // // if (uart_cbf[E_UART_0] != NULL) uart_cbf[E_UART_0]((void *)&gBufObjs[E_UART_0]); // button.c로 올리기 // } if (huart == &huart3) { 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) { 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[E_UART_1] != NULL) uart_cbf[E_UART_1]((void *)&gBufObjs[E_UART_1]); p->idx = 0; p->flag = false; } } } }
  • 변경점
    • UART 포트 지원 확장
      • 다중 UART 포트를 지원하기 위해 E_UART_0E_UART_1 등 인덱스 기반의 포트 처리 방식으로 변경되었습니다.
    • 콜백 함수 배열 추가
      • uart_cbf 배열을 추가하여 각 UART 포트별로 콜백 함수를 등록하고 호출할 수 있게 하였습니다.
    • 버퍼 배열 추가
      • rxdatagBufObjs 배열을 추가하여 각 UART 포트별로 데이터를 독립적으로 처리할 수 있게 하였습니다.
    • UART 초기화 함수 변경
      • 각 UART 포트를 초기화하고, 인터럽트 기반으로 데이터를 수신하도록 설정하였습니다.
    • 인터럽트 서비스 루틴 (ISR) 변경
      • 다중 UART 포트를 처리하기 위해 조건문을 통해 각 포트별로 데이터를 처리하고, 적절한 콜백 함수를 호출하도록 수정되었습니다.

Day07 : SLIP 통신 패킹 프로토콜

1. SLIP (Serial Line Internet Protocol)

  • SLIP은 직렬 포트를 통해 데이터를 전송하기 위해 사용되는 간단한 통신 프로토콜입니다.
  • SLIP는 주로 IP 패킷을 직렬 연결을 통해 전송할 수 있도록 설계되었습니다.
    • 이는 주로 TCP/IP 프로토콜 스택이 직렬 회선을 통해 작동할 수 있도록 지원합니다.

특징과 작동 방식

  1. 간단한 프레임 구조
      • SLIP는 매우 간단한 프레임 구조를 가집니다. 패킷의 시작과 끝을 구분하기 위해 특별한 종료 문자를 사용합니다.
  1. 프레임 경계
      • 데이터 스트림에서 패킷의 끝을 표시하기 위해 0xC0 바이트를 사용합니다.
      • 만약 패킷 내에 0xC0 바이트가 포함되어 있어야 한다면, SLIP는 이를 회피하기 위해 슬립 이스케이프(SLIP escape) 메커니즘을 사용합니다.
        • 0xDB 바이트는 SLIP 이스케이프 바이트로 사용됩니다.
        • 0xC0는 0xDB 0xDC로 대체됩니다.
        • 0xDB는 0xDB 0xDD로 대체됩니다.
  1. 오버헤드 최소화
      • SLIP는 헤더나 트레일러가 없으므로 IP 패킷에 추가적인 오버헤드를 거의 주지 않습니다. 이는 프로토콜이 매우 가벼워야 하는 상황에서 유리합니다.
  1. 비신뢰성
      • SLIP는 오류 검출이나 수정 기능을 제공하지 않으므로, 신뢰성 있는 전송을 위해서는 상위 계층 프로토콜(TCP 등)에서 이러한 기능을 처리해야 합니다.
  1. 단순성
      • SLIP는 구현이 매우 단순하여 마이크로컨트롤러와 같은 리소스가 제한된 환경에서도 쉽게 사용할 수 있습니다.
notion image

인코딩 과정

  1. RAW DATA FRAME
      • 원래 데이터 프레임에는 여러 바이트가 포함될 수 있으며, 이 중 FEND (0xC0)나 FESC (0xDB)가 포함될 수 있습니다.
  1. ENCODED SLIP FRAME
      • 데이터 프레임을 SLIP 프로토콜로 인코딩할 때, FEND와 FESC는 특별한 시퀀스로 변환됩니다.
      • FEND (0xC0): 프레임의 끝을 나타내며, 데이터 안에 나타나면 안됩니다.
      • FESC + TFEND: 데이터 프레임 안에 FEND가 있으면, FESC(0xDB)와 TFEND(0xDC)로 대체됩니다.
      • FESC + TFESC: 데이터 프레임 안에 FESC가 있으면, FESC(0xDB)와 TFESC(0xDD)로 대체됩니다.

디코딩 과정

  1. SLIP 프레임 수신
      • 직렬 연결을 통해 수신된 데이터 스트림에서 SLIP 프레임을 식별합니다. 이는 0xC0 바이트를 통해 프레임의 시작과 끝을 구분하여 이루어집니다.
  1. 이스케이프 시퀀스 처리
      • 수신된 SLIP 프레임 내에서 FESC(0xDB) 바이트를 찾아 처리합니다.
      • FESC + TFEND(0xDB 0xDC) 시퀀스는 원래의 FEND(0xC0)로 디코딩됩니다.
      • FESC + TFESC(0xDB 0xDD) 시퀀스는 원래의 FESC(0xDB)로 디코딩됩니다.
  1. 원본 데이터 프레임 재구성
      • 이스케이프 시퀀스를 처리하여 원래의 데이터 프레임을 재구성합니다.
      • 재구성된 데이터 프레임에서 추가된 FEND(0xC0) 바이트를 제거하여 순수한 데이터만 추출합니다.
notion image

단점 및 대체 프로토콜

SLIP 프로토콜은 간단하고 효과적이지만, 몇 가지 단점도 있습니다. 예를 들어, 데이터 링크 계층의 오류 제어와 같은 기능이 없고, 여러 프로토콜을 구별하는 기능도 부족합니다. 이러한 이유로 SLIP는 PPP(Point-to-Point Protocol)와 같은 더 발전된 프로토콜로 대체되었습니다. PPP는 SLIP보다 많은 기능을 제공하며, 현대의 대부분의 시스템에서는 SLIP보다 PPP가 더 많이 사용됩니다.

WithRobot

  • WithRobot Packing 프로토콜은 Withrobot사에서 제공하는Comportmaster 소프트웨어에서 사용할수 있는 패킹 프로토콜입니다.
notion image
  • WithRobot 프로토콜 페이로드의 데이터 필드
    • CMD(2Bytes) : 데이터 명령 유형
    • Length(1Bytes) : 실제 전송 데이터 길이
    • Data(nBytes) : 실제 전송할려는 데이터
notion image

2. WithRobot - SLIP 프로토콜로 패킹하기

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 } } }
  • 변경점
      1. 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 프로토콜을 사용하여 데이터 패킷을 인코딩하고 전송하는 기능을 수행합니다.
      1. UART 수신 완료 콜백 함수 변경 (HAL_UART_RxCpltCallback):
          • UART2와 UART3에서 수신된 데이터를 처리하는 로직이 추가되었습니다. slip_decode 함수를 통해 수신된 데이터를 디코딩합니다.
      1. SLIP 디코딩 함수 추가 (slip_decode)
          • SLIP 디코딩을 수행하는 함수입니다. 수신된 바이트를 처리하여 SLIP 프로토콜에 따라 디코딩하고, 완전한 패킷이 수신되면 콜백 함수를 호출합니다.
            • pstate: 현재 디코딩 상태를 나타내는 변수입니다.
            • rxd: 수신된 바이트입니다.
            • p: 수신된 데이터를 저장하는 버퍼입니다.
            • uart_cbf: 데이터가 완전히 수신되었을 때 호출되는 콜백 함수입니다.

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_0E_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);

전체적인 구조 및 흐름

notion image
notion image

레이어

notion image

Day08 : LCD Device (HD44780U) + Mutex

1. LCD Device (HD44780U)

  • LCD(액정 표시 장치)는 액정(Liquid Crystal)을 사용하여 정보를 표시하는 디스플레이 장치입니다. LCD 디바이스는 일반적으로 휴대전화, 태블릿 컴퓨터, 디지털 시계, 모니터, 텔레비전 등 다양한 전자 제품에 사용됩니다.
    • notion image

HD44780 Datasheet

notion image
  • 입출력 버퍼(Input/Output Buffer)
    • 이곳에서 MCU와의 통신을 통해 데이터를 입력 받습니다.
  • 데이터 처리
    • 입력 받은 데이터는 폰트가 저장된 주소를 가리키는 위치 데이터를 받고, Character Generator 저장소(RAM&ROM)에서 해당 폰트에 접근하여 LCD로 출력됩니다.
notion image
  • 폰트 저장소: 사용자 저장 폰트는 64개, 기기 자체에 저장된 폰트는 최대 240개가 저장되어 있습니다.
  • 출력 처리: 실제 데이터가 입력되면 해당 데이터는 8비트 주소 데이터로 해당 위치의 폰트에 접근하여, Segment 데이터를 가져와 화면에 출력됩니다.

문자 패턴 출력

notion image
  • 문자 코드 비트 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)에 의해 개발되었으며, 저속 주변기기와의 간단한 통신을 위해 주로 사용됩니다.
notion image

주요 특징

  1. 양방향 데이터 전송: 두 개의 라인을 통해 양방향 데이터 전송이 가능.
  1. 단순한 하드웨어 구조: SDA(데이터)와 SCL(클럭) 두 개의 라인만 사용.
  1. 다중 슬레이브 지원: 여러 슬레이브 디바이스를 하나의 버스에 연결 가능.
  1. 주소 지정: 각 슬레이브 디바이스는 고유의 주소를 가짐.

구성 요소

  • 마스터(Master): 통신을 시작하고 클럭 신호를 생성하며, 슬레이브 디바이스를 선택.
  • 슬레이브(Slave): 마스터의 요청에 응답하는 디바이스.
  • SDA(Serial Data Line): 데이터 전송 라인.
  • SCL(Serial Clock Line): 클럭 신호 라인.

동작 방식

  1. 시작(Start) 조건: SDA가 SCL이 높은 상태에서 낮은 상태로 전환.
  1. 주소 전송: 마스터가 슬레이브의 주소를 전송하고, 슬레이브가 이를 인식하여 응답.
  1. 데이터 전송: 마스터와 슬레이브 간의 데이터 전송. 각 바이트 전송 후, 수신자는 ACK(인정) 비트를 보냅니다.
  1. 정지(Stop) 조건: SCL이 높은 상태에서 SDA가 높은 상태로 전환.

통신 단계

  1. 시작 조건(Start Condition): 통신의 시작을 알림. SDA가 SCL이 높은 상태에서 낮아짐.
  1. 주소 프레임(Address Frame): 마스터가 슬레이브 주소와 읽기/쓰기 비트를 전송.
  1. ACK/NACK: 슬레이브가 응답하여 ACK(인정) 또는 NACK(비인정) 비트를 전송.
  1. 데이터 프레임(Data Frame): 실제 데이터 전송. 각 바이트 전송 후 ACK/NACK 비트 교환.
  1. 정지 조건(Stop Condition): 통신의 끝을 알림. SCL이 높은 상태에서 SDA가 높아짐.

장점

  • 단순한 하드웨어 구조: 두 개의 와이어만 필요.
  • 확장성: 여러 슬레이브 디바이스를 쉽게 추가 가능.
  • 유연성: 다양한 속도(표준 모드, 고속 모드 등)로 동작 가능.

단점

  • 속도 제한: 상대적으로 낮은 속도(최대 3.4 Mbps).
  • 버스 충돌 가능성: 여러 디바이스가 동일한 버스를 공유하기 때문에 충돌 가능성이 존재.

3. Mutex

  • Mutex는 멀티스레드 환경에서 공유 자원에 대한 접근을 조율하기 위해 사용되는 상호 배제 메커니즘입니다.
  • LCD 디바이스와 같이 하나의 리소스를 여러 스레드가 동시에 접근할 경우, 데이터 일관성을 유지하고 충돌을 방지하기 위해 Mutex를 사용합니다.
notion image

장점

  • 데이터 일관성 보장: 공유 자원에 대한 동시 접근을 방지하여 데이터 일관성을 유지합니다.
  • 간단한 구현: 대부분의 프로그래밍 언어에서 라이브러리로 제공되어 쉽게 구현할 수 있습니다.

단점

  • 병목 현상: 하나의 스레드가 리소스를 독점하면 다른 스레드들이 대기해야 하므로 성능 저하가 발생할 수 있습니다.
  • 교착 상태: 잘못된 설계로 인해 교착 상태(데드락)가 발생할 수 있습니다.

세마포어 (Semaphore)

  • 세마포어는 Mutex와 유사하게 상호 배제를 제공하지만, 더 복잡한 시나리오를 처리할 수 있습니다.
  • 세마포어는 카운팅 메커니즘을 통해 여러 스레드가 공유 자원에 접근할 수 있도록 합니다.
notion image
  • 이진 세마포어: Mutex와 비슷하게 동작하며, 한 번에 하나의 스레드만 접근을 허용합니다.
  • 카운팅 세마포어: 특정 개수의 스레드가 동시에 자원에 접근할 수 있도록 허용합니다.
  • 장점
    • 동시 접근 허용: 카운팅 세마포어를 사용하면 여러 스레드가 동시에 자원에 접근할 수 있습니다.
  • 단점
    • 복잡성: 구현이 복잡하고, 잘못 사용하면 교착 상태나 기아 상태가 발생할 수 있습니다.

큐 (Queue)

  • 프로듀서-컨슈머 패턴에서 자주 사용되는 방법으로, 데이터가 준비될 때까지 대기할 수 있도록 합니다. 큐를 사용하여 데이터가 준비되면 이를 LCD로 전송합니다.
  • 장점
    • 비동기 처리: 프로듀서와 컨슈머가 독립적으로 동작할 수 있어 효율적입니다.
    • 버퍼링: 데이터가 일시적으로 버퍼에 저장되므로, 데이터 손실을 방지할 수 있습니다.
  • 단점
    • 메모리 사용: 큐의 크기에 따라 메모리 사용량이 증가할 수 있습니다.
    • 복잡성: 프로듀서-컨슈머 문제를 해결하기 위한 추가적인 동기화가 필요할 수 있습니다.

공유 메모리 (Shared Memory)

  • 여러 프로세스가 동일한 메모리 공간을 공유하도록 하여 데이터를 주고받는 방법입니다.
  • 속도가 빠르지만, 동기화 문제를 해결해야 합니다.
  • 장점
    • 고속 통신: 메모리 공간을 직접 공유하므로 속도가 매우 빠릅니다.
  • 단점
    • 동기화 필요: 데이터 일관성을 유지하기 위해 동기화 메커니즘이 필요합니다.
    • 보안 문제: 메모리를 공유하기 때문에 데이터 보호가 어려울 수 있습니다.

4. LCD Device 제어

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
RSSPowered by inblog