10주차에는 크리스마스가 끼어있어 조금 여유로웠달까나요... ㅎㅎ
월요일에는 STM32을 활용한 실습을 진행하고 화,수 는 심화 실습을 진행했습니다.

실습은 차차 구현을 설명하기로 하고 그 전에 STM보드를 활용한 온도, 습도 인식에 대해 얘기해볼까 합니다.

DHT-11,DC Motor with STM32NucleoF401re

DC 모터를 사용하기 위해서는 트랜지스터가 필요합니다.

트랜지스터의 역할

  • 전기적 스위치
  • 증폭


위의 이미지에서 확인할 수 있듯이 Base에 어떤 값을 주느냐에 따라 트랜지스터의 사용법이 달라집니다.

  • PNP : 에미터(Emitter)의 전압보다 베이스(Base)의 전압이 낮을 때 전류가 흐른다
    • Low (0V) 입력 시: emitter (보통 3.3V나 5V 연결)와 base 사이에 전압 차이가 생겨 전류가 흐르고, 트랜지스터가 ON 됩니다.
    • High (3.3V/5V) 입력 시: emitter 와 base의 전압이 같아져서 전류가 흐르지 못함 → 트랜지스터 OFF
  • NPN : High(1)일 때 작동 (Active High)

DHT-11

	- return :  5byte { 1   1     1 1 1}
                          RH     T checksum 
                     상대습도 온도
온도,습도 모두 decimal , integral 로 두 바이트씩
	→ 7번 부터 날아온다. 
	연산 한계 때문에 소수부는 0으로 날아옴 
	실습에 사용한 DHT-11은 온도부는 소수점 한자리까지 측정 가능 .

→ DHT-22 와 통신 프로토콜의 통일을 위해 decimal bit 만들어둠
  • Commmunication Process

    • single wire 통신 : 별도의 클럭핀 없이 데이터 핀의 전압 레벨이 VCC와 GND 사이에서 바뀐다.
      기본 상태 : 풀업 저항 → high
      Data 핀의 high /low 지속 시간으로 구분 된다. ⇒ data 핀의 전압이 변화 open drain ⇒ 전압의 지속 시간으로 모든 것을 구분함
  • start signal

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

  • DHT Responses to MCU

    • Data 0

      26~28μs 동안 high (pull - up)상태 유지
    • Data 1

      70μs 동안 high (pull - up)상태 유지
      따라서 0과 1을 (27 +70)/2 의 값을 임계값으로 설정하여 구분한다 => datasheet의 배경

실제 STM32 구현

*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가 됨.
  • 타이밍 제어
    • HAL_Delay 또는 직접 busy loop / 타이머
  • 운영 체제 X → Bare-metal
  • 값 확인 → UART를 통해 printf 리다이렉션
      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 설치 필요)

  • delay_usec()
// 함수 내부
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에 많은 시간이 소요된다.

  • read_dht_data(void)
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");
    }
}
  1. delay_usec(19000, 1); 을 통해 초기 start signal을 인식

  2. 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배의 시간이 소요

  3. 데이터 유효성 검사 => 40bit 확인 + checksum 확인 (위의 함수에서는 생략)

    그렇다면 실제 Datasheet에 작성된 것처럼 40~50 사이를 임계값으로 설정하기 위해서는 어떻게 해야하는가 ?

  • 대안 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

1. 프로젝트 개요 (Project Overview)

이 프로젝트는 클라이언트-서버 모델을 기반으로 하며, 클라이언트가 원격으로 서버(라즈베리 파이)에 연결된 하드웨어 장치를 제어하는 시스템입니다. 단순한 제어를 넘어, 서버의 자동 모니터링 기능과 다중 클라이언트 동기화에 중점을 두었습니다.

주요 기능

  • LED: On/Off 및 PWM을 이용한 밝기 조절 (3단계)

  • Buzzer: On/Off 제어

  • CDS(조도센서): 빛 감지 시 LED 자동 제어 (서버 사이드 오토메이션)

  • 7-Segment: 숫자 표시 및 카운트 다운 타이머 (0이 되면 부저 울림)

하드웨어 연결 (Pin Map)

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)

2. 서버 (Server Architecture)

서버는 하드웨어 제어의 허브 역할을 하며, 유연한 확장성을 위해 동적 링킹(Dynamic Linking)을 구현했습니다.

특징 1: 동적 라이브러리 로딩 (dlopen/dlsym)

하드웨어 제어 코드를 서버 코드에 직접 포함하지 않고, .so (Shared Object) 파일로 분리했습니다. 서버 실행 시 dlopen을 통해 라이브러리를 로드하고, dlsym으로 함수 포인터를 가져와 실행합니다.

  • 장점: 장치 제어 로직이 변경되어도 서버를 재컴파일할 필요 없이 라이브러리 파일만 교체하면 됩니다.

  • 구현: DeviceLibs 구조체에 핸들과 함수 포인터를 매핑하여 관리.

특징 2: 멀티 스레드 및 동기화

  • CDS 모니터링 스레드: 별도의 스레드가 조도 센서 값을 주기적으로 폴링합니다. 빛이 감지되면 LED를 끄고, 어두워지면 켜는 로직이 서버 자체적으로 돌아갑니다.

  • 7-Segment 카운트다운 스레드: 클라이언트 요청 시 비동기로 카운트다운을 수행하며, 0이 되면 부저를 울립니다.

  • Broadcasting: 센서 값 변경이나 카운트다운 종료 시, 연결된 모든 클라이언트에게 상태 메시지를 전송합니다 (Linked List로 클라이언트 세션 관리 및 Mutex 보호).

3. 데몬 프로세스 (Daemon Implementation)

서버가 터미널 종료와 관계없이 백그라운드에서 상주하도록 Daemon 프로세스로 구현했습니다.

구현 이슈

  • 경로 문제
    일반적으로 daemon() 함수를 호출하면 프로세스의 작업 디렉토리(CWD)가
    / (루트)로 변경됩니다. 이로 인해 상대 경로로 지정된 라이브러리(.so)나 설정 파일을 찾지 못하는 문제가 발생했습니다.

해결

데몬 전환 전 get_exe_directory() 함수를 구현하여 /proc/self/exe를 통해 실행 파일의 절대 경로를 미리 확보했습니다.

확보된 절대 경로를 기준으로 라이브러리를 로드(dlopen)하도록 수정하여, 데몬 상태에서도 정상적으로 장치 제어 라이브러리를 링크할 수 있게 되었습니다.

4. 클라이언트 (Client Implementation)

사용자 편의성을 고려한 CLI(Command Line Interface) 환경을 구축했습니다.

  1. 수신/송신 스레드 분리
    서버로부터 오는 비동기 메시지(예: "LED를 켰습니다", "서버가 종료됩니다")를 실시간으로 처리하기 위해, 메시지 수신 전용 스레드를 분리했습니다. 이를 통해 사용자가 메뉴를 고르는 도중에도 서버의 알림을 즉시 화면에 출력할 수 있습니다.

  2. 시그널 핸들링 (Signal Handling)
    SIGINT (Ctrl+C): 핸들러를 등록하여 정상적인 자원 해제 후 종료되도록 유도했습니다. 이때 sigfillsetsigprocmask를 사용하여 종료 절차(5초 대기 등) 중 들어오는 다른 시그널을 Block(차단)하여 안전한 종료를 보장했습니다.
    ----> 프로그램의 안정성 증가

그 외 (SIGQUIT, SIGTERM 등): SIG_IGN을 통해 무시하도록 설정하여, 의도치 않은 종료를 방지했습니다.

5. 하드웨어 제어 (Hardware Control & Makefile)

프로젝트의 폴더 구조를 체계화하고 make를 통한 빌드 자동화를 구현했습니다.

폴더 구조


code/
  ├── client/      # 클라이언트 소스
  ├── server/      # 서버 소스
  └── device_control/ # 장치 제어 소스 (LED, Buzzer, etc.)
exec/
  ├── lib/         # 빌드된 .so 라이브러리 위치
  └── ...

계층적 Makefile

최상위 Makefile이 하위 디렉토리(code/device_control/)의 Makefile을 호출하는 구조를 가집니다.

  • libwiringLED.so, libwiringCDS.so 등 각 장치별로 독립적인 공유 라이브러리가 생성됩니다.

  • 서버는 컴파일 시점에 이 라이브러리들을 링크하지 않고, 런타임에 동적으로 불러옵니다.

6. 추가 기능 구현 (Additional Features)

퀴즈 기능

단순 제어 외에 서버와 상호 작용하는 퀴즈 기능을 추가했습니다

브로드캐스트

서버 종료 시(SIGINT/SIGTERM) 모든 클라이언트에게 SERVER_SHUTDOWN 메시지를 브로드캐스트하여 클라이언트가 이를 감지하고 "연결 끊김" 상태로 전환되도록(프로그램 강제 종료 대신) 처리했습니다.

7. 프로젝트 회고 및 개선 사항 (Reflections)

😥아쉬운 점

  1. I/O 버퍼링 문제
    클라이언트에서 퀴즈 기능과 일반 명령 모드를 오갈 때, scanf와 fgets 등의 입력 함수 간의 입력 버퍼(Stdin Buffer) 처리 문제로 인해 매끄럽지 않은 부분이 있었습니다.
  • 현상: 퀴즈 종료 후 메뉴 선택 시 Enter 키를 한 번 더 눌러야 입력이 인식되는 현상.

  • 원인: 퀴즈 스레드가 별도로 진행되면서 퀴즈 제한 시간 이후 bool 값이 변하는데 이때 enter키 입력시 스레드를 벗어나면서 변수 변경을 확인한다.

개선 방향: 퀴즈를 별도의 스레드를 생성하지 않고 Main loop에서 진행한다면 더 매끄러운 UX가 될 수 있습니다 .

  1. 서버 통신 및 소켓 동기화 이해 미흡
    소켓을 이용한한 TCP 통신에서 멀티 클라이언트 처리를 위해 mutex를 사용하여 클라이언트 - 서버 통신 구조를 구현했지만 전반적인 socket 통신 과정을 완전히 이해했다는 느낌을 받지 못했습니다. 추가적인 학습을 해야겠다는 피드백을 얻을 수 있었습니다.

심화 실습을 통해 리눅스 환경에서의 시스템 프로그래밍, 특히 자원 관리와 프로세스 간 통신에 대해 깊이 이해할 수 있었습니다. 자세한 코드는 상단의 GitHub 링크를 참고해주세요.

10주차 자투리

대.황.성.원 프로젝트는 이틀도 과하다. 깔끔하게 끝내버리고 풀 유튜브 자습 때리는 . 대.황.성.원.

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

10주차 맛집 탐방

또순이네

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

그러면 11주차 기록으로 돌아오겠습니다잇!

profile
세상의 어려운 문제를 해결하자

0개의 댓글