호타리 2025. 6. 27. 09:07

Socket Driver Arcade Project

 

SOCKET_DRIVER_ARCADE.pdf
0.73MB

 

# Socket_Driver_Arcade

실행 방법

Ubuntu 환경
1. vim lcd1602.c
2. vim Makefile
3. make -j12 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
4. scp lcd1602.ko pi@ip주소:/home/pi

Raspi 환경
1. sudo insmod lcd1602.ko
2. sudo chmod 666 /dev/lcd1602
3. dmesg | tail로 lcd1602 register 확인
4. sudo mknod /dev/lcd1602 c $MAJOR $MINOR (dmesg | tail로 확인)
5. vim server_final.c
6. gcc server_final.c -o server_final
7. vim client_final.c
8. gcc client_final.c -o client_final
9. ./server_final
10. ./client_final ip주소 - 플레이어 2명 접속
11. 게임 실행

# 멀티플레이어 터미널 미니 게임, LCD1602 & LED 하드웨어 피드백

이 프로젝트는 C로 구현한 간단한 멀티플레이어 미니게임 서버·클라이언트 프로그램과, Raspberry Pi의 하드웨어(LED 및 I2C LCD1602 디스플레이)를 이용해 플레이어 점수 및 라운드 승자를 시각적으로 출력하는 예제입니다.

## 목차

* [주요 기능](#주요-기능)
* [하드웨어 구성](#하드웨어-구성)
* [소프트웨어 요구사항](#소프트웨어-요구사항)
* [프로젝트 구조](#프로젝트-구조)
* [빌드 방법](#빌드-방법)
* [사용법](#사용법)
* [코드 개요](#코드-개요)

## 주요 기능

* **3가지 미니게임**: 가위바위보, 연산 대결, 반응 속도 게임
* **두 명 동시 플레이**: TCP 클라이언트 2명이 각 라운드에 참여
* **자동 점수 집계**: 3라운드 종료 후 승/패 결과 요약
* **하드웨어 피드백**:

  * **LED**: 각 라운드별 승리 시 해당 라운드 LED(총 3개) 점등
  * **LCD1602**: 최종 점수 요약을 I2C LCD1602에 표시

## 하드웨어 구성

* **LED (GPIO 제어)**

  * 라운드1→GPIO17 (LED0)
  * 라운드2→GPIO27 (LED1)
  * 라운드3→GPIO22 (LED2)
  * 각 LED는 330Ω 저항과 함께 Raspberry Pi GND에 연결
* **I2C LCD1602 디스플레이**

  * SDA → GPIO2
  * SCL → GPIO3
  * 모듈 주소: 0x27
  * 4.7kΩ 풀업 저항 (보드 내장 또는 외부)

## 소프트웨어 요구사항

* Raspberry Pi OS (Raspbian 등)
* `gcc`, `make`, `pthread` 라이브러리
* `i2c-tools` (I2C 버스 확인 및 디버깅)
* `raspi-gpio` 유틸리티 (GPIO 제어)
* I2C 활성화:

  ```bash
  sudo raspi-config nonint do_i2c 0
  ```

## 프로젝트 구조

```
project/
├── server_final.c   # 게임 서버 및 LCD/LED 제어 (라운드별 LED + LCD)
├── client_final.c        # 게임 클라이언트 (터미널 인터페이스)
├── lcd1602.c           # I2C LCD1602 커널 모듈
└── Makefile            # 빌드 스크립트
```

## 빌드 방법

1. 프로젝트 디렉터리를 Raspberry Pi로 복사
2. 의존성 설치:

   ```bash
   sudo apt update
   sudo apt install build-essential i2c-tools raspi-gpio
   ```
3. 빌드:

   ```bash
   make -j12 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
   ```

   → `server_final`, `client_final`, `lcd1602.ko` 생성

## 사용법

### 1. LCD 커널 모듈 로드

```bash
sudo insmod lcd1602.ko
sudo chmod 666 /dev/lcd1602
sudo mknod /dev/lcd1602 c $MAJOR $MINOR (dmesg | tail에서 번호 확인)
ls -l /dev/lcd1602
```

### 2. 서버 실행

```bash
sudo ./server_final
```

* 포트 10000에서 두 명의 클라이언트 연결을 대기

### 3. 클라이언트 접속

두 개의 터미널에서:

```bash
./client_final <서버_IP>
```

### 4. 하드웨어 피드백 확인

* **라운드별 LED**: 각 라운드 종료 시 플레이어1이 이긴 라운드 LED만 점등
* **LCD1602**: 3라운드 종료 후 다음 내용 표시:

  ```
  P1: X win Y lose
  P2: A win B lose
  ```

## 코드 개요

### `server_final.c`

1. **네트워크**: TCP 소켓 생성, 포트 10000 바인딩, 최대 2명 접속
2. **미니게임 로직**

   * `play_rps()`, `play_math()`, `play_react()` 함수
3. **라운드별 LED 제어**

   * `round_winners[3]`에 각 라운드 승자(0 또는 1) 저장
   * `led_per_round()` 함수에서 승자 배열 참조, `raspi-gpio`로 GPIO17/27/22 제어
4. **LCD 제어**

   * `/dev/lcd1602`에 write하여 I2C LCD1602 출력
5. **결과 전송 및 정리**

### `client_final.c`

* 서버 연결 및 게임 입력/출력 처리

### `lcd1602.c`

* I2C LCD1602 커널 모듈 (dynamic char device)

---

## 데모 동영상

아래 링크에서 프로젝트 실행 모습을 확인할 수 있습니다:

* [https://youtu.be/a2IlBgpWruA](https://youtu.be/a2IlBgpWruA)
* [https://youtube.com/shorts/zeM5-n1U8Fc](https://youtube.com/shorts/zeM5-n1U8Fc)
* [https://youtube.com/shorts/pw4d3U6k4-s](https://youtube.com/shorts/pw4d3U6k4-s)

---

 

핵심코드
server.c
🔹 1. TCP 서버 초기화 및 클라이언트 연결 대기
int sock = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in addr = { AF_INET, htons(PORT), INADDR_ANY };
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, MAX_CLIENTS);

while (cnt < MAX_CLIENTS) {
    int cfd = accept(sock, NULL, NULL);
    client_info_t *ci = malloc(sizeof(*ci));
    ci->sockfd = cfd; ci->player_id = cnt;
    clients[cnt++] = ci;
    pthread_create(&tid, NULL, client_thread, ci);
    pthread_detach(tid);
}
​
🗣️ 설명:
TCP 소켓 생성 후 바인딩 → 최대 2명 클라이언트 접속 대기
접속마다 개별 스레드 할당
pthread로 병렬 처리
🔹 2. 클라이언트 스레드 진입 처리: client_thread()
void *client_thread(void *arg) {
    client_info_t *ci = arg;
    char buf[BUF_SIZE];
    snprintf(buf, sizeof(buf), "[서버] Player %d 입장\n", ci->player_id+1);
    send(ci->sockfd, buf, strlen(buf), 0);

    while (1) {
        pthread_mutex_lock(&game.lock);
        if (clients[0] && clients[1]) {
            pthread_mutex_unlock(&game.lock);
            break;
        }
        pthread_mutex_unlock(&game.lock);
        usleep(100000);
    }
    return NULL;
}
​
구성 요소
설명
client_info_t
각 클라이언트의 소켓 FD와 플레이어 ID 정보 저장
send()
접속 확인 메시지 전송 (Player 1 입장, Player 2 입장)
pthread_mutex
두 클라이언트 모두 연결될 때까지 락을 걸고 조건 확인
usleep(100000)
바쁜 대기를 피하기 위한 0.1초 sleep, CPU 낭비 최소화
비동기 입장 처리
→ 각 클라이언트 입장 후 별도 스레드에서 대기
Race condition 방지
→ pthread_mutex_lock()을 이용해 clients[] 접근 시 동기화
게임 진행 제어점
→ 두 명 모두 입장해야 게임 진행 가능 (main thread에서 round 시작)
클라이언트 스레드는 입장 알림 및 진입 동기화 역할만 수행
이후 게임 로직은 main thread가 직접 관리
멀티플레이 환경에서 정확한 입장 타이밍 제어를 위한 핵심 함수
🔹 3. 게임 제어 및 통신 루프 (핵심 로직)
int (*games[3])(client_info_t*, client_info_t*) = { play_rps, play_math, play_react };
while (game.current_round < 3) {
    int r = game.current_round;
    int w = games[r](clients[0], clients[1]);
    round_winners[r] = w;
    game.scores[w]++;
    game.current_round++;
    for (int i = 0; i < MAX_CLIENTS; i++) {
        send(clients[i]->sockfd,
             i == w ? "WIN\n" : "LOSE\n",
             i == w ? 4 : 5, 0);
    }
}
​
🗣️ 설명:
가위바위보, 연산, 반응 속도 게임을 차례로 진행
각 라운드마다 승자를 판단하고 점수 반영
클라이언트에게 WIN/LOSE 결과 전송
🔹 4. 디바이스 연동: LCD + LED 제어
// LCD 출력용 메시지 생성
snprintf(line1, sizeof(line1), "P1:%d win %d lose", p1, 3-p1);
snprintf(line2, sizeof(line2), "P2:%d win %d lose", p2, 3-p2);
memcpy(out, line1, 16);
memcpy(out + 16, line2, 16);
lcd_write(out);  // /dev/lcd1602에 결과 출력

// LED 피드백
for (int i = 0; i < 3; i++) {
    snprintf(cmd, sizeof(cmd), "raspi-gpio set %d op %s",
             led_pins[i], round_winners[i] == 0 ? "dh" : "dl");
    system(cmd);
}

​
🗣️ 설명:
/dev/lcd1602를 통해 LCD에 최종 점수 출력
각 라운드 결과를 GPIO LED로 표시 (승자에 따라 High/Low 제어)
raspi-gpio 명령어로 핀 출력 설정
client.c
🔹  1. 서버 연결 초기화
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv = {AF_INET, htons(PORT)};
inet_pton(AF_INET, argv[1], &serv.sin_addr);
if(connect(sockfd, (struct sockaddr*)&serv, sizeof(serv)) < 0) {
    perror("connect"); exit(1);
}
​
🗣️ 설명:
TCP 소켓 생성 (SOCK_STREAM)
서버 주소 설정 및 connect()로 연결
IP는 사용자 인자(argv[1])로 입력
🔹 2. 게임 루프 구조 및 메시지 처리
while(fgets(buf, BUF_SIZE, fp)) {
    if(strncmp(buf, "WIN\n", 4)==0) { printf("[결과] 승리!\n"); continue; }
    if(strncmp(buf, "LOSE\n", 5)==0) { printf("[결과] 패배.\n"); continue; }
    if(strncmp(buf, "TIE\n", 4)==0) { printf("[결과] 무승부!\n"); continue; }
    ...
​
🗣️ 설명:
서버에서 오는 문자열 메시지를 기준으로 분기 처리
결과 메시지 (WIN, LOSE, TIE)는 단순 출력
🔹 3. 미니게임 입력 처리 (예: 가위바위보)
if(strncmp(buf, "RPS", 3)==0) {
    printf("[게임: 가위바위보] rock/paper/scissors 입력: ");
    char in[BUF_SIZE];
    fgets(in, BUF_SIZE, stdin);
    in[strcspn(in,"\n")] = '\0';
    fprintf(fp, "%s\n", in);
    fflush(fp);
}
​
🗣️ 설명:
서버의 지시에 따라 입력 요청
사용자 입력을 줄바꿈 제거 후 서버로 전송
구조는 MATH, REACT도 유사
i2c_lcd.c
🟦 1. 프로젝트 내 LCD 드라이버의 역할
📌 목적: 게임 서버 결과를 하드웨어 LCD에 출력

🎯 위치: server 에서 write("/dev/lcd1602") 호출
→ LCD 디바이스 드라이버가 이를 처리하여 실제 I2C 전송 수행

✅ 주요 기술요소
- 문자 디바이스 드라이버
- I2C client 장치 생성
- 데이터시트 기반 LCD 제어 (4bit 모드)
​
🟦 2. 드라이버 구조 개요
[사용자 공간] → echo "P1:1 P2:2" > /dev/lcd1602
                   ↓ write() 호출

[커널 공간]   → lcd_write() 함수 진입
                   ↓
                I2C 통신 (i2c_master_send)
                   ↓
[실제 하드웨어] LCD1602 
​
📌 구조 설명:
문자 디바이스 등록
유저 write() → 문자 → 커맨드 변환 → LCD로 전송
🟦 3. 문자 디바이스 등록 과정
alloc_chrdev_region(&lcd_dev, 0, 1, "lcd1602");
cdev_init(&lcd_cdev, &lcd_fops);
cdev_add(&lcd_cdev, lcd_dev, 1);
​
📌 설명:
alloc_chrdev_region()으로 major/minor 동적 할당
cdev_add()로 write 시스템콜 연결
결과: /dev/lcd1602 자동 생성됨
🟦 4. I2C 클라이언트 장치 등록
struct i2c_board_info info = {
    .type = "lcd1602",
    .addr = 0x27,
};
adap = i2c_get_adapter(1);
lcd_client = i2c_new_client_device(adap, &info);
​
📌 설명:
Raspberry Pi의 I2C-1 버스에서 주소 0x27의 장치 등록
이후 i2c_master_send()로 해당 장치에 데이터 전송 가능
lcd_client가 하드웨어 포인터 역할(I2C 디바이스를 나타내는 커널 구조체 포인터)
🟦 5. LCD 초기화 시퀀스 (HD44780, 4bit)
lcd_cmd(0x33);  // 초기화
lcd_cmd(0x32);  // 4bit 모드 설정
lcd_cmd(0x28);  // 2줄, 5x8도트
lcd_cmd(0x0C);  // 화면 ON, 커서 OFF
lcd_cmd(0x06);  // 자동 커서 이동
lcd_cmd(0x01);  // 화면 clear
​
📌 설명:
HD44780의 초기화 순서를 정확히 따름
LCD1602는 실제로 4bit만 사용되므로 두 번에 나눠 전송
🟦 6. 명령/데이터 전송 구조
void lcd_send(u8 val, u8 rs) {
    write4(val, 0);         // 상위 4비트
    write4(val << 4, rs);   // 하위 4비트
}

void write4(u8 nibble, u8 ctrl) {
    u8 data = (nibble & 0xF0) | ctrl | LCD_BACKLIGHT;
    i2c_master_send(lcd_client, &data, 1);
    pulse_enable(data);
}
​
📌 설명:
한 글자를 4bit 단위로 나눠 두 번 전송
RS=0: 커맨드, RS=1: 데이터
LCD Enable 펄스는 latch 역할
🟦 7. write() 시스템콜 구현
char kbuf[33];
copy_from_user(kbuf, buf, len);  // 사용자 버퍼 복사

lcd_cmd(0x01); // 화면 clear
for (i = 0; i < len && i < 16; i++)
    lcd_data(kbuf[i]);
if (len > 16) {
    lcd_cmd(0xC0); // 둘째 줄로 이동
    for (i = 16; i < len && i < 32; i++)
        lcd_data(kbuf[i]);
}
​
📌 설명:
한 줄 16글자 기준
2줄(32자)까지 출력 가능
write() → 커널 복사 → i2c 전송
🟦 8. insmod → 동작 예시 흐름
# 드라이버 삽입
$ sudo insmod lcd1602.ko

# 장치 생성 확인
$ ls /dev/lcd1602

# dmesg 로그 확인
lcd1602: char device registered (major=245, minor=0)
lcd1602: LCD initialized
​
📌 설명:
실제 리눅스 환경에서 사용자가 어떻게 접근하는지 명확히 제시
🟦 9. 커널 모듈 exit 및 정리
void __exit lcd_exit_module(void) {
    i2c_unregister_device(lcd_client);
    cdev_del(&lcd_cdev);
    unregister_chrdev_region(lcd_dev, 1);
}
​
📌 설명:
rmmod lcd1602 시 안전하게 I2C 장치 제거
문자 디바이스 및 major 번호 반납
🟦 10. 요약: LCD 드라이버 설계의 핵심
✅ 문자 디바이스와 I2C 통신을 커널 모듈에서 직접 구현
✅ 사용자 공간 ↔ 커널 ↔ 하드웨어 구조 완전 이해
✅ 게임 서버 결과 → 실시간 LCD 출력으로 시각화
✅ 시스템 콜 / 하드웨어 통신 / 커맨드 제어 모두 아우른 종합 설계