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 SystemInitBIOS(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_RxCpltCallbackISR에서 데이터 처리를 수행하고, 필요 시 콜백 함수를 호출합니다. - 주석 처리된
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.cextern 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

