
10주차에는 크리스마스가 끼어있어 조금 여유로웠달까나요... ㅎㅎ
월요일에는 STM32을 활용한 실습을 진행하고 화,수 는 심화 실습을 진행했습니다.
실습은 차차 구현을 설명하기로 하고 그 전에 STM보드를 활용한 온도, 습도 인식에 대해 얘기해볼까 합니다.
DC 모터를 사용하기 위해서는 트랜지스터가 필요합니다.

위의 이미지에서 확인할 수 있듯이 Base에 어떤 값을 주느냐에 따라 트랜지스터의 사용법이 달라집니다.
- return : 5byte { 1 1 1 1 1}
RH T checksum
상대습도 온도
온도,습도 모두 decimal , integral 로 두 바이트씩
→ 7번 부터 날아온다.
연산 한계 때문에 소수부는 0으로 날아옴
실습에 사용한 DHT-11은 온도부는 소수점 한자리까지 측정 가능 .
→ DHT-22 와 통신 프로토콜의 통일을 위해 decimal bit 만들어둠
Commmunication Process
start signal

mcu sends out Start Signal : ⇒ 최소 18ms 동안 pull-down voltage → DHT11에서 탐지
=> start signal 이후 40bit 데이터 전송
DHT Responses to MCU


*GIPIO Setting
1. clock and pins configuration at cubeMX
clock 84MHz. so, TIM3 connects to APB1 == 84MHz
PA0 as signal pin of DHT11
TIM3 as usec resolution timer
PSC= 84-1 // 1us로 클럭 카운트 세팅
Counter Mode= Up
Autoreload = 0xFFFF
internal clock division = No
auto-reload preload = Disable
PSC = timer의 count를 몇 초로 할 것인가 ?
APB1 Timer Clock ──▶ Prescaler ──▶ Counter (CNT)
PSC를 설정하기에 따라 값이 달라진다.
```c
htim3.Init.Prescaler = 84-1;
```
Prescaler를 83으로 설정 ⇒ 타이머 클럭 : APB1 * 2 = 84MHZ
PSC = (타이머 입력 클럭 / 원하는 CNT 주파수) - 1
84 - 1 은 / 1,000,000을 한 값 ⇒ 원하는 주파수 1,000,000
1 마이크로초(µs) ⇒ clk 의 한 tic이 1µs가 됨.
int _write(int file, char *ptr, int len){
// &huart2를 통해 ptr이 가리키는 문자열을 len만큼 전송합니다.
if (HAL_UART_Transmit(&huart2, (uint8_t*)ptr, len, HAL_MAX_DELAY) == HAL_OK)
{
return len;
}
return -1;
}해당 코드를 통해 stdout 버퍼의 값을 리다이렉션하여 Vscode serial monitor을 이용하여 값을 확인합니다 . (extension 설치 필요)
// 함수 내부
void delay_usec(int us, int loop)
{
#if 0
while (loop--)
{
TIM3->CR1 = 0; // Control = disable
TIM3->ARR = us; // Auto-reload value
// TIM3->CNT = us; // no way! not gonna work
TIM3->CR1 = 1 << 0; // Control = enable
while (1)
{
int s = TIM3->SR & (1 << 0); // UIF: update interrupt flag
if (s == 1)
{
TIM3->SR = ~0x00000001; // clear UIF (rc_w0)
break;
}
}
} // 코드가 더 부정확 arr 방식은 오버플로우 구조라 zitter가 더 크다 .
#else
//해당 코드로 실행했을 때 threshold 20에서 값 출력
while (loop--)
{
__HAL_TIM_SET_COUNTER(&htim3, 0); //tim3의 카운터 값을 받아온다
//0으로 설정 후 1만큼 증가하면 끝.
HAL_TIM_Base_Start(&htim3);
while (__HAL_TIM_GET_COUNTER(&htim3) < us);
HAL_TIM_Base_Stop(&htim3);
}
#endif
return;
}
delay_usec(1,1)을 사용 -> 1us를 측정하여 0과 1을 구분하는 기준으로 사용
하지만 해당 코드를 실행했을 때는 타이머 카운트를 사용하여 1을 증가시키지만 함수 호출 및 return에 많은 시간이 소요된다.
void read_dht_data(void) {
unsigned laststate = HIGH;
unsigned counter = 0;
unsigned j = 0, i, k;
// 데이터 배열 초기화
data[0] = data[1] = data[2] = data[3] = data[4] = 0;
/* MCU에서 센서로 시작 신호 전송 (최소 18ms 동안 LOW 유지)
-> 최소 보장이라 오차가 있어도 넘기만 하면 괜찮다 .
*/
pinMode(DHT_PIN, OUTPUT);
digitalWrite(DHT_PIN, LOW);
delay_usec(19000, 1);
/* 데이터 읽기 준비를 위해 입력 모드로 전환 */
pinMode(DHT_PIN, INPUT);
/* 핀의 상태 변화를 감지하여 40비트 데이터 수신 */
for (i = 0; i < MAX_TIMINGS; i++) {
counter = 0;
while (digitalRead(DHT_PIN) == laststate) {
counter++;
delay_usec(1, 1);
if (counter == 500) {
break;
}
}
laststate = digitalRead(DHT_PIN);
if (counter > 400) {
break;
}
/* 초기 3번의 신호 변화(Response 신호)는 무시하고 실제 데이터(4번째부터)만 추출 */
if ((i >= 4) && (i % 2 == 0)) {
data[j / 8] <<= 1;
// 카운터 값(High 유지 시간)이 일정 기준 이상이면 비트 1로 간주
if (counter > 20) {
data[j / 8] |= 1;
}
j++;
}
}
// 수신된 비트 수와 데이터 확인을 위한 디버그 출력
printf("j = %d , %d %d %d %d %d \r\n", j, data[0], data[1], data[2], data[3], data[4]);
/* * 데이터 유효성 검사: 40비트 모두 읽었는지 + 체크섬이 일치하는지 확인
*/
if ((j >= 40) && (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF))) {
// 습도 계산
unsigned h = ((data[0] << 8) + data[1]);
if (h > 1000) {
h = data[0] * 10; // DHT11용 예외 처리
}
// 온도 계산
unsigned c = (((data[2] & 0x7F) << 8) + data[3]);
if (c > 1250) {
c = data[2] * 10; // DHT11용 예외 처리
}
// 음수 온도 처리 (최상위 비트가 1인 경우)
if (data[2] & 0x80) {
c = -c;
}
printf("Humidity = %d.%d %% Temperature = %d.%d *C \r\n", h / 10, h % 10, c / 10, c % 10);
} else {
printf("Data not good, skip\r\n");
}
}
delay_usec(19000, 1); 을 통해 초기 start signal을 인식
counter threshold를 20으로 설정
```c
/* 초기 3번의 신호 변화(Response 신호)는 무시하고 실제 데이터(4번째부터)만 추출 */
if ((i >= 4) && (i % 2 == 0)) {
data[j / 8] <<= 1;
// 카운터 값(High 유지 시간)이 일정 기준 이상이면 비트 1로 간주
if (counter > 20) {
data[j / 8] |= 1;
}
j++;
```
왜 20인가 ? delay_usec()함수 호출 과정에서 실제로 1us의 거의 2배의 시간이 소요
데이터 유효성 검사 => 40bit 확인 + checksum 확인 (위의 함수에서는 생략)
대안 1 : 함수 내에서 바로 timer count 읽기
uint16_t t;
data[0] = data[1] = data[2] = data[3] = data[4] = 0;
/* Start signal */
pinMode(DHT_PIN, OUTPUT);
digitalWrite(DHT_PIN, LOW);
delay_usec(19000, 1); // 최소 18ms 대기
pinMode(DHT_PIN, INPUT);
/* 데이터 수신 루프 */
for (i = 0; i < MAX_TIMINGS; i++)
{
// 타이머 시작 및 카운터 초기화
HAL_TIM_Base_Start(&htim3);
__HAL_TIM_SET_COUNTER(&htim3, 0);
// 핀의 상태가 변할 때까지 대기
while (digitalRead(DHT_PIN) == laststate)
{
// 타임아웃 체크 (200us 이상 지연 시 중단)
if (__HAL_TIM_GET_COUNTER(&htim3) > 200)
break;
}
// 경과 시간 기록 및 타이머 중지
t = __HAL_TIM_GET_COUNTER(&htim3);
HAL_TIM_Base_Stop(&htim3);
// 마지막 핀 상태 업데이트
laststate = digitalRead(DHT_PIN);
```
대안 2 : interrput polling
연결된 signal 핀의 전압이 바뀔 때 interrupt를 발생시켜 정확한 시간을 측정
이는 추후에 직접 구현해보고 코드를 업로드하도록 하겠습니다 .
10주차에 진행된 실습에서는 리눅스 시스템 프로그래밍(LSP)을 통해 TCP/IP 기반의 원격 하드웨어 제어 시스템을 개발했습니다. 라즈베리 파이의 GPIO 장치를 제어하며, 멀티 프로세스/스레드, IPC, 시그널 처리, 데몬 프로세스, 동적 라이브러리(Shared Library) 등 리눅스 프로그래밍의 핵심 개념들을 모두 적용해보는 것이 주요 과제였습니다.
전체 소스 코드: https://github.com/physical-100/VEDA_Project2
이 프로젝트는 클라이언트-서버 모델을 기반으로 하며, 클라이언트가 원격으로 서버(라즈베리 파이)에 연결된 하드웨어 장치를 제어하는 시스템입니다. 단순한 제어를 넘어, 서버의 자동 모니터링 기능과 다중 클라이언트 동기화에 중점을 두었습니다.
LED: On/Off 및 PWM을 이용한 밝기 조절 (3단계)
Buzzer: On/Off 제어
CDS(조도센서): 빛 감지 시 LED 자동 제어 (서버 사이드 오토메이션)
7-Segment: 숫자 표시 및 카운트 다운 타이머 (0이 되면 부저 울림)
LED: GPIO 12 (WiringPi 26)
Buzzer: GPIO 21 (WiringPi 29)
CDS: GPIO 11 (WiringPi 14)
7-Segment (7447 Decoder):
A: GPIO 14 (WP 15), B: GPIO 15 (WP 16)
C: GPIO 18 (WP 1), D: GPIO 23 (WP 4)
서버는 하드웨어 제어의 허브 역할을 하며, 유연한 확장성을 위해 동적 링킹(Dynamic Linking)을 구현했습니다.
dlopen/dlsym)하드웨어 제어 코드를 서버 코드에 직접 포함하지 않고, .so (Shared Object) 파일로 분리했습니다. 서버 실행 시 dlopen을 통해 라이브러리를 로드하고, dlsym으로 함수 포인터를 가져와 실행합니다.
장점: 장치 제어 로직이 변경되어도 서버를 재컴파일할 필요 없이 라이브러리 파일만 교체하면 됩니다.
구현: DeviceLibs 구조체에 핸들과 함수 포인터를 매핑하여 관리.
CDS 모니터링 스레드: 별도의 스레드가 조도 센서 값을 주기적으로 폴링합니다. 빛이 감지되면 LED를 끄고, 어두워지면 켜는 로직이 서버 자체적으로 돌아갑니다.
7-Segment 카운트다운 스레드: 클라이언트 요청 시 비동기로 카운트다운을 수행하며, 0이 되면 부저를 울립니다.
Broadcasting: 센서 값 변경이나 카운트다운 종료 시, 연결된 모든 클라이언트에게 상태 메시지를 전송합니다 (Linked List로 클라이언트 세션 관리 및 Mutex 보호).
서버가 터미널 종료와 관계없이 백그라운드에서 상주하도록 Daemon 프로세스로 구현했습니다.
daemon() 함수를 호출하면 프로세스의 작업 디렉토리(CWD)가/ (루트)로 변경됩니다. 이로 인해 상대 경로로 지정된 라이브러리(.so)나 설정 파일을 찾지 못하는 문제가 발생했습니다.데몬 전환 전 get_exe_directory() 함수를 구현하여 /proc/self/exe를 통해 실행 파일의 절대 경로를 미리 확보했습니다.
확보된 절대 경로를 기준으로 라이브러리를 로드(dlopen)하도록 수정하여, 데몬 상태에서도 정상적으로 장치 제어 라이브러리를 링크할 수 있게 되었습니다.
사용자 편의성을 고려한 CLI(Command Line Interface) 환경을 구축했습니다.
수신/송신 스레드 분리
서버로부터 오는 비동기 메시지(예: "LED를 켰습니다", "서버가 종료됩니다")를 실시간으로 처리하기 위해, 메시지 수신 전용 스레드를 분리했습니다. 이를 통해 사용자가 메뉴를 고르는 도중에도 서버의 알림을 즉시 화면에 출력할 수 있습니다.
시그널 핸들링 (Signal Handling)
SIGINT (Ctrl+C): 핸들러를 등록하여 정상적인 자원 해제 후 종료되도록 유도했습니다. 이때 sigfillset과 sigprocmask를 사용하여 종료 절차(5초 대기 등) 중 들어오는 다른 시그널을 Block(차단)하여 안전한 종료를 보장했습니다.
----> 프로그램의 안정성 증가
그 외 (SIGQUIT, SIGTERM 등): SIG_IGN을 통해 무시하도록 설정하여, 의도치 않은 종료를 방지했습니다.
프로젝트의 폴더 구조를 체계화하고 make를 통한 빌드 자동화를 구현했습니다.
폴더 구조
code/
├── client/ # 클라이언트 소스
├── server/ # 서버 소스
└── device_control/ # 장치 제어 소스 (LED, Buzzer, etc.)
exec/
├── lib/ # 빌드된 .so 라이브러리 위치
└── ...
최상위 Makefile이 하위 디렉토리(code/device_control/)의 Makefile을 호출하는 구조를 가집니다.
libwiringLED.so, libwiringCDS.so 등 각 장치별로 독립적인 공유 라이브러리가 생성됩니다.
서버는 컴파일 시점에 이 라이브러리들을 링크하지 않고, 런타임에 동적으로 불러옵니다.
단순 제어 외에 서버와 상호 작용하는 퀴즈 기능을 추가했습니다
서버 종료 시(SIGINT/SIGTERM) 모든 클라이언트에게 SERVER_SHUTDOWN 메시지를 브로드캐스트하여 클라이언트가 이를 감지하고 "연결 끊김" 상태로 전환되도록(프로그램 강제 종료 대신) 처리했습니다.
현상: 퀴즈 종료 후 메뉴 선택 시 Enter 키를 한 번 더 눌러야 입력이 인식되는 현상.
원인: 퀴즈 스레드가 별도로 진행되면서 퀴즈 제한 시간 이후 bool 값이 변하는데 이때 enter키 입력시 스레드를 벗어나면서 변수 변경을 확인한다.
개선 방향: 퀴즈를 별도의 스레드를 생성하지 않고 Main loop에서 진행한다면 더 매끄러운 UX가 될 수 있습니다 .
심화 실습을 통해 리눅스 환경에서의 시스템 프로그래밍, 특히 자원 관리와 프로세스 간 통신에 대해 깊이 이해할 수 있었습니다. 자세한 코드는 상단의 GitHub 링크를 참고해주세요.

+다소 늦었지만 2025.12.25 크리스마스 자투리

10주차 맛집 탐방
https://naver.me/5ZJxE6jr
평점: ⭐️⭐️⭐️⭐️ + 0.8
서울 레전드 된찌 맛집.
점심 -> 식사만 가능 된찌 1인 7000원
달래향 그득한 구수한 된찌를 먹고 싶다면 여기로옷!