Mini Proejct1 - Socket Driver Arcade
Socket Driver Arcade Project
# 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 출력으로 시각화
✅ 시스템 콜 / 하드웨어 통신 / 커맨드 제어 모두 아우른 종합 설계