[Embedded] Device Driver
디바이스 드라이버는 하드웨어를 추상화하는 소프트웨어 계층.. OS 다룬 내용이랑 유사할 수 밖에 없음.
Upper Half 계층과 HAL 두 계층으로 나뉜다.
// GPIO
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
// 통신 UART
HAL_UART_Transmit(&huart2, buf, len, 100);
// 초음파 TIMER
HAL_TIM_Base_Start_IT(&htim2);
// 모터 PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
사실 지금까지 자연스럽게 작성하던 HAL_* 함수가 모두 디바이스 드라이버임.
GPIO 레지스터를 직접 조작하지 않고 함수 호출만 했고, 내부에서는 디바이스 드라이버가 작동하던 것.


Upper Half 부분은 디바이스 독립적 API 부분으로 디바이스에 따라 바뀌지 않는다.
반면 Hardware Abstraction Layer는 디바이스마다 다르다.
같은 코드를 STM32F4에서 STM32H7로 옮길 때, 디바이스마다 UART 레지스터 위치와 클럭 구조가 다 다르다.
다만 Upper Half 부분은 그대로 두고 HAL 내부 구현만 고치면 되니 유용함.
임베디드용 RTOS는 드라이버와 커널이 같은 층이고, Standard OS는 드라이버가 커널 아래에 위치한다.
개념 정리를 더 하고 싶은데 CS가 그렇듯 항상 똑같아서 할 게 없다. OS에서 다룬 내용이 그대로 여기서도 적용됨.
추상화 + 표준인터페이스 + 계층화

프로젝트 폴더를 살펴보면 STM32F4xx_HAL_Driver를 찾을 수 있음. 이 폴더 전체가 디바이스 드라이버 모음집이다.
ppp는 peripheral의 약자임. uart, adc, gpio 버전도 있음.
main.c와 stm32f4xx_it.c 는 HAL 드라이버를 코드와 연결해주는 역할을 수행한다.
하드웨어 인터럽트가 발생하면 IRQHandler가 인식하고 HAL에서 내부 처리를 진행한다.
그리고 나서 내가 작성한 main.c에서 로직을 처리하는 순서임.
HAL이 로직의 대부분을 처리해주고 마지막 사용자 정의 부분만 콜백으로 외부로 던진다.
내가 작성한 콜백 함수가 디폴트 함수를 덮어쓰는 구조.
HAL 자료구조는 세 가지로 구성된다.
1. Peripheral Handle
2. Init / Config
3. 특정 작업을 위한 구조체 (&huart2, &htim3)
프로세서와 I/O 디바이스의 속도가 다르니 데이터 Sync를 맞추기 위해 둘 사이에 버퍼가 필요하다.
CPU와 메모리 사이에 캐시가 있어야 하고, 네트워크에는 송수신 버퍼가 있듯 CPU와 I/O 디바이스 사이에도 버퍼가 있어야 함.
속도 차이 문제를 버퍼로 해결하고, 공유 데이터 문제는 Semaphore와 Mutex로 해결한다.

프로세서는 병렬버스로 데이터에 접근하지만, 외부 디바이스는 직렬로 데이터에 접근한다.
그러니 변환 회로가 필요하고, 그 회로로 UART를 사용함. (Universial Asynchronous Receiver and Transmitter)
시스템 콜로 OS에게 출력을 부탁하고, UART 드라이버가 문자열을 받아 한 문자씩 UART로 전송한다.
이후 UART의 Shift Reg가 직렬로 바깥에 송출한다.
문자를 한 번에 한 개씩 UART로 전송하는게 포인트. 문자 단위로 루프가 실행된다.
while (*string != '\0')
printChar(*string++);
이렇게 짜면 간단하지만 while 루프는 UART가 문자를 전송하는 속도보다 훨씬 빠르게 실행되니 문제가 발생한다.
while (*string != '\0') {
if (UART_is_empty()) {
printChar(*string++);
}
}
이렇게 UART가 비어있을 때만 다음 문자를 쓰는 Busy Waiting 방식을 사용하면 너무 비효율적이다.
keyboard_ISR() {
ch = Read keyboard input register;
switch (ch) {
case 'b': startGame();
case 'x': doSomeProcessing();
...
}
}
이제 시점을 바꿔서 입력 쪽을 살펴보자.
startGame보다 doSomeProcessing이 훨씬 오래 걸리면 또 오류가 발생할 가능성이 높다. 다른 키를 누를 수 있으니까..
그러니 ISR 안에서는 절대 무거운 작업을 처리하면 안됨.
keyboard_ISR() {
*input_buffer++ = ch; // ← 버퍼에 저장만 (1줄!)
// 즉시 리턴
}
// 메인 루프 어딘가에서
while (!quit) {
if (*input_buffer) {
processCommand(*input_buffer); // 무거운 처리는 여기서
removeCommand(*input_buffer);
}
}
버퍼에 입력 문자를 일단 저장하고 ISR은 즉시 리턴한다. 무거운 처리는 메인 루프가 알아서 처리함.
버퍼는 문자의 입력속도와 프로세서의 처리속도간 차이를 분리하고, ISR 시간을 보장한다.
OS의 Producer - Consumer 패턴과 동일..
버퍼를 도입해서 속도 차이는 해결했지만, 그 버퍼에 여러 주체가 동시에 접근하면 또 문제가 발생한다.
Atomic Action과 Critical Section이 필요함.
; *input_buffer++ = ch 를 어셈블리 명령어로 보면...
LDR R0, =input_buffer ; ① input_buffer의 현재 주소를 R0에
LDR R1, [R0] ; ② R0가 가리키는 곳의 값(포인터)을 R1에
STR ch, [R1] ; ③ R1이 가리키는 메모리에 ch 저장
ADD R1, R1, #1 ; ④ R1을 1 증가 (포인터 ++)
STR R1, [R0] ; ⑤ 증가된 포인터를 다시 저장
5단계로 쪼개진다. 그런데 여기서 2번과 5번 사이에 인터럽트가 발생한다면?
첫 번째 ISR이 2번까지 실행하고 인터럽트가 발생해 새로운 ISR이 진입한다.
ISR이 할거 다하고 포인터를 1 증가시키고 원래 ISR로 돌아온다.
이러면 새로운 ISR이 증가시킨 포인터를 덮어쓰고 사라지게 됨. (Race Condition)
keyboard_ISR() {
MaskInterrupts(); // ① 인터럽트 비활성화
ch = Read keyboard input register;
*input_buffer++ = ch; // ② 이제 안전하게 기록
UnmaskInterrupts(); // ③ 다시 활성화
}
이렇게 아예 위험 구간에서의 인터럽트 자체를 막아버리는것도 가능.
MaskInterrupts()를 호출하면 CPU가 인터럽트를 받지 않는다.
문제는 해결되지만 다른 중요한 ISR까지 다 지연되니.. 꼭 필요한 곳에서만 사용해야 함.
// 메인 루프
while (!quit) {
if (*input_buffer) {
processCommand(*input_buffer); // ← 버퍼 읽기
removeCommand(*input_buffer); // ← 버퍼에서 삭제
}
}
// ISR (마스킹 처리 됨)
keyboard_ISR() {
MaskInterrupts();
ch = Read ACIA input register;
*input_buffer++ = ch;
UnmaskInterrupts();
}
InputBuffer에서 문자를 하나 삭제할 때 키보드 ISR이 발생하면 똑같은 문제가 발생한다.
removeCommand()도 내부적으로는 여러 명령어로 이루어지니 중간에 ISR이 끼어들면 버퍼가 또 깨진다.
while (!quit) {
if (*input_buffer) {
MaskInterrupts(); // ← 추가
processCommand(*input_buffer);
removeCommand(*input_buffer);
UnmaskInterrupts(); // ← 추가
}
}
그러니 같은 방식으로 보호해주자. MaskInterupts() 를 사용한다.
// 메인이 호출
printStr("this is a line");
void printStr(char *string) {
while (*string) {
outputBuffer[tail++] = *string++; // ← 진행 중
}
}
// 타이머 ISR
timer_ISR() {
clockTicks++;
printStr(convert(clockTicks)); // ← 같은 함수를 ISR도 호출!
}
이번엔 다른 문제가 발생한다. print 버퍼에 다른 ISR에서 접근하는 경우.
this is 까지만 버퍼에 기록하고 타이머 인터럽트가 발생해 clockTicks를 호출한다.
이러면 버퍼에 시간 문자열이 기록되고 버퍼가 오염된다.
본질은 printStr 함수가 메인과 ISR에서 동시에 실행되는 것.
그러니 함수가 재진입 가능하려면 모든 상태를 지역 변수로만 가지고 있어야 한다.
void printStr(char *string) {
MaskInterrupts(); // ┐
while (*string) { // │
outputBuffer[tail++] = *string++;// │ Critical Section
} // │
UnmaskInterrupts(); // ┘
}
이러면 문제가 해결된다.
마스킹으로 인터럽트 자체를 차단하는게 가장 단순하긴 하지만 모든게 지연되는게 단점이다.
Mutex는 인터럽트를 그대로 받지만 Task 간 동기화만 수행해줌.
Semaphore는 자원이 여러 개인 경우 Task간 신호를 전달할 때 사용한다.
'Computer Science > Embedded Software' 카테고리의 다른 글
| [Embedded] Embedded Operating System (0) | 2026.05.13 |
|---|---|
| [Embedded] Motor (0) | 2026.05.13 |
| [Embedded] Sensor (0) | 2026.05.13 |
| [Embedded] Communication Programming (0) | 2026.05.12 |
| [Embedded] Communication Protocol (1) (0) | 2026.05.12 |
댓글
이 글 공유하기
다른 글
-
[Embedded] Embedded Operating System
[Embedded] Embedded Operating System
2026.05.13 -
[Embedded] Motor
[Embedded] Motor
2026.05.13 -
[Embedded] Sensor
[Embedded] Sensor
2026.05.13 -
[Embedded] Communication Programming
[Embedded] Communication Programming
2026.05.12