리눅스 오픈소스 랜섬웨어(Ransomware) CryptoTrooper 분석 환경 구축
CryptoTrooper는 리눅스 기반의 오픈소스 랜섬웨어(Ransomware)입니다. 간단히 랜섬웨어 실습을 해보고 싶을 때 사용할 수 있습니다.
※ Victim 서버용 OS 구축하기 ※
가장 먼저 Guest OS를 구축합니다. 저는 Ubuntu 16.04 Server용 이미지를 이용해 Guest OS를 설치했습니다. 일단 Host OS에서 Guest에 손쉽게 접속할 수 있도록 Guest 입장에서 SSH를 설치합니다.
이제 Guest는 Network 설정을 진행합니다. 기본적으로 네트워크 어댑터를 NAT에 연결합니다. 또한 [Connect Network Adapter]에 정상적으로 체크되어 있는지 확인합니다. 이 과정에서 Guest의 아이피 주소를 확인하기 위해 ifconfig 명령어를 사용해 볼 필요가 있습니다. 결과적으로 다음과 같이 포트포워딩을 진행합니다. 간단히 웹 서버도 같이 개방하고자 합니다.
※ Guest OS에 필요한 라이브러리 설치하기 ※
Guest에서는 apache2 웹 서버와 MySQL을 설치합니다.
sudo apt-get install apache2 nginx-common mysql-server postgresql
이후에 MySQL에 접속하여 간단히 설치된 내용을 확인합니다.
mysql -u root -p
use mysql;
select host, user, authentication_string from user;
실행 결과는 다음과 같습니다.
이후에 간단히 하나의 테이블을 만들고 예시 데이터를 입력합니다.
CREATE TABLE CUSTOMER (
id int(10) NOT NULL,
name varchar(20) DEFAULT NULL,
PRIMARY KEY(id)
);
INSERT INTO CUSTOMER VALUES (1, 'Johnson');
SELECT * FROM CUSTOMER;
실행 결과는 다음과 같습니다.
※ Host에서 Guest OS의 웹 서버에 접속해보기 ※
포트포워딩을 통해 개방된 웹 서버에 접속해 볼 수 있습니다.
※ Guest OS에서 CryptoTrooper를 사용하기 ※
이제 Guest OS에서 CryptoTrooper를 사용해 랜섬웨어에 감염되어 봅시다. 먼저 전체 소스코드를 다운로드 받습니다. 그리고 소스코드가 존재하는 경로에 있는 모든 파일들을 루트 디렉토리(/)로 옮깁시다.
git clone https://github.com/jdsecurity/CryptoTrooper
cd /
sudo mv $CRYPTOTROOPER_SOURCE_DIRECTORY/* .
실행 결과는 다음과 같습니다.
이후에 랜섬웨어를 동작시키는 cipher.sh 스크립트를 실행합니다.
sudo /cipher.sh &
실행 결과는 다음과 같습니다. 기본적으로 접근 가능한 디렉토리에 있는 파일들을 모두 감염시킵니다.
※ Host에서 감염된 Guest OS의 웹 서버에 접속해보기 ※
웹 사이트에 접속하면 다음과 같이 랜섬 노트(Ransom Note)를 확인할 수 있습니다.
※ 감염된 Guest OS에서 복호화 해보기 ※
Guest OS에서는 whiteDecipher.sh 스크립트를 실행하여 암호된 파일들을 암호 해제해 볼 수 있습니다.
sudo ./whiteDecipher.sh
sudo /decipher.sh
실행 결과는 다음과 같습니다.
이어서 호스트가 다시 접속하면 다음과 같이 복호화 된 것을 확인할 수 있습니다.
※ Host에서 예시 웹 사이트 만들어 Guest로 옮기기 ※
커스텀 웹 사이트가 랜섬웨어에 감염된 것을 실습하는 방법입니다.
▶ index.html
<html>
<head>
<title>Demo Website</title>
</head>
<body>
<h2>Demo Website</h2>
<p>This is a demo website for demonstrating ransomware infection.</p>
<h3>
<a href="./link1.html">Link 1</a>
</h3>
<h3>
<a href="./link2.html">Link 2</a>
</h3>
</body>
</html>
▶ link1.html
<html>
<head>
<title>Demo Website</title>
</head>
<body>
<h2>Link 1</h2>
<p>This is link 1.</p>
</body>
</html>
▶ link2.html
<html>
<head>
<title>Demo Website</title>
</head>
<body>
<h2>Link 2</h2>
<p>This is link 2.</p>
</body>
</html>
이제 Host OS에서 Guest OS로 파일을 옮깁시다. Host OS 입장에서는 127.0.0.1:5678이 Guest 의 주소가 됩니다. 결과적으로 Guest의 /home/dongbin/web 이라는 경로에 파일들을 옮기는 것입니다.
sudo scp -r -P 5678 /home/dongbin/Downloads/demo/ dongbin@127.0.0.1:/home/dongbin/web
실행 결과는 다음과 같습니다.
만약에 SSH 접속을 많이 해서 Host Identification이 변경되었다는 내용이 출력되는 경우 /root/.ssh/known_hosts에 작성된 내용을 제거할 수 있도록 합니다.
sudo rm /root/.ssh/known_hosts
결과적으로 Guest는 다음과 같이 웹 파일을 받게 됩니다.
이후에 전달 받은 경로인 /home/dongbin/web을 Apache의 루트 디렉토리로 설정합니다.
sudo nano /etc/apache2/apache2.conf
대략 다음과 같은 형태로 작성된 부분이 있습니다.
<Directory /var/www/>
...
</Directory>
이걸 자신의 경로로 바꿉니다.
<Directory /home/dongbin/web/>
...
</Directory>
예를 들어 다음과 같이 작성됩니다.
이어서 sites-available 경로에 있는 default.conf 파일도 변경합니다.
sudo nano /etc/apache2/sites-available/000-default.conf
여기에 적혀 있는 DocumentRoot의 경로를 마찬가지로 변경해 주겠습니다.
DocumentRoot /home/dongbin/web/
다음과 같이 변경됩니다.
이제 웹 서버를 재부팅합니다.
sudo service apache2 restart
이어서 다시 호스트 입장에서 접속하면 다음과 같습니다.
참고로 만약 호스트에서 접속했을 때 403 Forbidden 오류가 나온다면, 다음과 같이 루트 디렉토리에 있는 파일들에 대하여 권한을 변경합니다.
cd /home/dongbin/web/
sudo chmod o+x ~
이어서 Guest OS에서 다시 CryptoTrooper를 받아서 랜섬웨어 감염을 진행합시다.
cd /home/dongbin
sudo rm -rf CryptoTrooper
git clone https://github.com/jdsecurity/CryptoTrooper
cd /
sudo mv $CRYPTOTROOPER_SOURCE_DIRECTORY/* .
sudo /cipher.sh &
실행 결과는 다음과 같습니다.
그러면 각종 웹 파일들의 확장자가 .enc로 변경되며 암호화됩니다.
마찬가지로 다시 복호화를 진행하면 정상적으로 호스트에서 파일을 확인할 수 있습니다.
'기타' 카테고리의 다른 글
킬디스크(Killdisk) 랜섬웨어 분석 환경 구축 (2) | 2020.11.15 |
---|---|
Ubuntu 16.04 호스트(Host) PC에 Oracle VM VirtualBox를 설치하여 Ubuntu 16.04 게스트(Guest) 설치하기 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용한 Serial 통신 파일 전송(File Transfer) 속도 분석 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용해 2개의 USB Configuration을 재부팅마다 교체하기 (0) | 2020.11.15 |
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용한 Serial 통신 파일 전송(File Transfer) 속도 분석
1. 라즈베리파이에서 Host PC로 다량의 데이터 전송
Host PC 입장에서는 다음과 같이 코드를 작성할 수 있다. Serial 통신을 이용해 계속해서 데이터를 전달 받아 파일에 쓰게 된다. 또한 프로그램이 동작하는 과정에서 주기적으로 통신 속도를 출력하도록 해보았다.
import time
import serial
s = serial.Serial("COM7", 115200)
total = 0
start_time = time.time()
last_time = 0
while True:
# 가져올 데이터가 존재하는 경우에만 처리
len = s.inWaiting()
if len == 0:
continue
# 파일을 열어 로그를 작성
f = open("log.dat", "ab")
data = s.read(len)
f.write(data)
f.close()
total += len
# 주기적으로 출력
passed = time.time() - start_time
if passed > last_time + 1:
print(f"total length: {total} bytes")
print(f"average speed: {total / (1024 ** 2 * passed)} MB/s")
last_time = passed
s.close()
라즈베리파이에서는 다음과 같이 코드를 작성할 수 있다. 크기가 4096 bytes인 배열에 데이터를 담아서 반복적으로 Serial 통신을 통해 데이터를 전송한다.
#include <stdio.h>
int main(void) {
FILE* file;
char buf[4096];
for (int i = 0; i < sizeof(buf); i++) {
buf[i] = i % 256;
}
while (1) {
file = fopen("/dev/ttyGS1", "r+");
fwrite(buf, 1, sizeof(buf), file);
fclose(file);
}
return 0;
}
단순히 데이터를 전달받는 것만 작업을 했더니 약 3MB/s 정도의 전송 속도가 나오는 것을 확인할 수 있다.
다만 필자의 경우 처음에 데이터의 유실이 발생했다. 일단 내가 라즈베리파이에서 데이터를 쓸 때 파일을 "r+" 옵션으로 열었는데, b를 붙여 주는 것을 깜빡 잊어 byte stream으로 데이터를 작성하지 않고 있었다. 그래서 "rb+" 옵션으로 바꾸어주면 해결될 줄 알았다. 하지만 여전히 데이터의 유실이 발생했다.
정확히는 라즈베리파이가 \b10을 보내려고 하면 \b13\b10이 보내지는 것이었다. 다시 말해 \n을 보내려고 하면, 자동으로 앞에 \r가 붙어버리는 문제가 발생했다. 이때 CR(Carriage Return)은 \r을 의미한다. (아스키코드 13번) 또한 LF(Line Feed)는 New Line와 같은 말로 \n을 의미한다. (아스키코드 10번) 콘솔에서는 CRLF 한 쌍을 줄 바꿈으로 인식하는 경우가 많아서, 시리얼로 LF만 보내도 자동으로 앞에 CR를 붙여 버리는 것이다.
따라서 라즈베리파이에서 다음과 같은 명령어를 입력한 뒤에 문제가 해결되었다. raw 데이터 자체를 보내도록 하는 것이다.
sudo stty -F /dev/ttyGS1 raw
이렇게 raw 모드로 바꾼 이후에는 속도를 확인한 결과 다음과 같이 5MB/s 정도의 전송 속도가 나오는 것을 확인할 수 있었다. 속도에 영향을 미치는 부분으로 raw 모드만 작용했는지는 명확하지는 않은데, 아무튼 이 정도 속도까지는 뽑을 수 있다.
아무튼 속도가 빠르기 때문에 3분이면 1GB 정도의 데이터를 보낼 수 있었다. 추가적으로 데이터의 유실이 발생하는지 여부를 체크하는 코드를 짜보자. 데이터를 받는 쪽에서 이용하면 된다.
f = open("log.dat", "rb")
data = f.read()
for i in range(len(data) - 1):
if (data[i] + 1) % 256 != data[i + 1]:
print("(Invalid) 데이터가 부정확합니다.")
if i % (1024 ** 2) == 0:
print(f"{i // (1024 ** 2)}MB checked.")
실행 결과 약 800MB 정도의 데이터를 전달받았으나, 한 치의 오차도 없이 데이터가 모두 전송된 것을 확인할 수 있었다.
2. Host PC에서 라즈베리파이로 다량의 데이터 전송
Host PC 입장에서도 Serial 통신을 이용해 대량의 파일을 라즈베리파이에게 전송할 수 있다.
'기타' 카테고리의 다른 글
Ubuntu 16.04 호스트(Host) PC에 Oracle VM VirtualBox를 설치하여 Ubuntu 16.04 게스트(Guest) 설치하기 (0) | 2020.11.15 |
---|---|
리눅스 오픈소스 랜섬웨어(Ransomware) CryptoTrooper 분석 환경 구축 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용해 2개의 USB Configuration을 재부팅마다 교체하기 (0) | 2020.11.15 |
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용해 2개의 USB Configuration을 재부팅마다 교체하기
※ 기초 설정 ※
/boot/config.txt 파일을 열어서 제일 마지막 줄에 다음의 코드를 추가한다.
dtoverlay=dwc2
/etc/modules의 마지막 줄에 다음 두 개의 커널 모듈을 넣어주자.
dwc2
libcomposite
이후에 Mass Storage Class의 기능을 위해 하나의 블록 디바이스(block device)를 생성한다.
mkdir -p /home/pi/images
# 1Gb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk1.img bs=1048576 count=1024
mkdosfs /home/pi/images/usbdisk1.img
※ Configuration 1 ※
첫 번째 Configuration인 /usr/bin/my_usb1을 생성해 보자.
sudo touch /usr/bin/my_usb1 # 파일 생성
sudo chmod +x /usr/bin/my_usb1 # 실행할 수 있도록 만들기
sudo nano /usr/bin/my_usb1 # 스크립트 작성하기
구체적인 스크립트는 다음과 같다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
################# Bulk Transfer 1 #################
mkdir -p functions/acm.usb0
ln -s functions/acm.usb0 configs/c.1/
################# Bulk Transfer 2 #################
mkdir -p functions/acm.usb1
ln -s functions/acm.usb1 configs/c.1/
################# Mass Storage 1 #################
# 첫 번째 이미지 지정하기
FILE1=/home/pi/images/usbdisk1.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE1/img/d}
mount -o loop,ro, -t vfat $FILE1 ${FILE1/img/d} # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
echo 0 > functions/mass_storage.usb0/lun.0/ro # 호스트 입장에서는 쓰기 가능
echo 0 > functions/mass_storage.usb0/lun.0/nofua
echo $FILE1 > functions/mass_storage.usb0/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb0 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
※ Configuration 2 ※
두 번째 Configuration인 /usr/bin/my_usb2를 생성해 보자.
sudo touch /usr/bin/my_usb2 # 파일 생성
sudo chmod +x /usr/bin/my_usb2 # 실행할 수 있도록 만들기
sudo nano /usr/bin/my_usb2 # 스크립트 작성하기
구체적인 스크립트는 다음과 같다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
################# Bulk Transfer 1 #################
mkdir -p functions/acm.usb0
ln -s functions/acm.usb0 configs/c.1/
################# Bulk Transfer 2 #################
mkdir -p functions/acm.usb1
ln -s functions/acm.usb1 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
※ 스크립트 자동 실행 ※
스크립트를 부팅 시 자동 실행하도록 만들기 위해 /etc/rc.local을 수정한다.
sudo nano /etc/rc.local
이후에 exit 0 위쪽 라인에 /usr/bin/my_usb1을 추가하자. 재부팅 이후에 USB device가 잡히는 것을 확인할 수 있다.
※ 선택적으로 USB Configuration 변경하기 ※
이제 /etc/rc.local에 내용을 더 추가하자.
cat /dev/ttyGS0 >> /home/pi/test.dat &
기본적으로 test.dat은 첫 번째 USB Configuration 정보를 가지도록 한다.
echo A > /home/pi/test.dat
이후에 /etc/rc.local을 아래 내용을 넣는 방식으로 바꾼다. test.dat의 가장 마지막 문자열이 A일 때는 첫 번째 Configuration, B일 때는 두 번째 Configuration을 이용한다.
var=$(cat /home/pi/test.dat)
flag="${var: -1}"
if [ $flag == "A" ]
then
/usr/bin/my_usb1
else
/usr/bin/my_usb2
fi
만약 정상적으로 동작이 안 된다면 동일한 스크립트를 작성한 뒤에 sudo bash 명령으로 실행해보자. 호스트 쪽에서 신호를 보낼 때는 다음과 같이 한다.
import serial
s = serial.Serial("COM6", 115200)
s.write(b'A\n')
s.close()
'기타' 카테고리의 다른 글
리눅스 오픈소스 랜섬웨어(Ransomware) CryptoTrooper 분석 환경 구축 (0) | 2020.11.15 |
---|---|
Raspberry Pi Zero W를 이용한 Serial 통신 파일 전송(File Transfer) 속도 분석 (0) | 2020.11.15 |
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제 (0) | 2020.11.15 |
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기 (1) | 2020.11.15 |
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법
미디어파이어(MediaFire)는 무료 파일 공유 서비스다. 일단 무료로 사용할 수 있으며 정말 편리하게 파일을 올려서 바로 링크를 공유할 수 있기 때문에 필자는 매우 자주 사용한다.
▶ MediaFire 회원가입 경로: www.mediafire.com/upgrade/
사이트에 접속하면 다음과 같이 회원가입 플랜이 등장한다. 간단히 BASIC으로 다운로드를 진행할 수 있다.
이후에 다음과 같이 회원가입을 진행한다. 참고로 2020년 11월을 기준으로 별도로 이메일 인증은 존재하지 않는다.
이제 로그인을 하고 나면 다음과 같이 [UPLOAD] 버튼을 눌러서 바로 파일 업로드를 진행할 수 있다.
파일은 그냥 드래그 앤 드롭(Drag & Drop)해서 올리면 된다.
이후에 다음과 같이 [Copy Link]를 눌러 링크를 복사할 수 있다.
이후에 링크에 접속하면 다운로드(Download) 버튼이 등장한다. 참고로 무료 계정의 경우 광고(ad)가 다수 등장할 수 있기 때문에 다운로드를 받는 입장에서는 유의할 필요가 있다.
'기타' 카테고리의 다른 글
Raspberry Pi Zero W를 이용한 Serial 통신 파일 전송(File Transfer) 속도 분석 (0) | 2020.11.15 |
---|---|
Raspberry Pi Zero W를 이용해 2개의 USB Configuration을 재부팅마다 교체하기 (0) | 2020.11.15 |
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제 (0) | 2020.11.15 |
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기 (1) | 2020.11.15 |
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법 (2) | 2020.11.09 |
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제
Raspberry Pi Zero W는 Serial 통신 목적의 USB 디바이스로 사용할 수 있다. 한 번 2개의 Serial 인터페이스를 제공하는 USB 디바이스를 만들어 보았다. 기본적인 환경 설정 파일은 다음과 같다. 경로는 /usr/bin/my_usb로 하였다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# 하나의 Serial 통신 기능 추가
mkdir -p functions/acm.usb0
ln -s functions/acm.usb0 configs/c.1/
# 하나의 Serial 통신 기능 추가
mkdir -p functions/acm.usb1
ln -s functions/acm.usb1 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
이로써 두 개의 Serial 인터페이스를 호스트에 제공하게 된다. 기본적인 이름은 /dev/ttyGS0과 /dev/ttyGS1에 매핑된다. 추가하는 횟수에 따라서 인덱스가 증가하게 된다.
1. 단순히 Serial (Bulk Transfer)로 "A" X 4096을 반복적으로 보내는 프로그램
그냥 계속해서 데이터를 이어 붙여 보내기 때문에 r+ 옵션을 사용한다.
#include <stdio.h>
int main(void) {
FILE* file;
char buf[4096];
for (int i = 0; i < sizeof(buf); i++) {
buf[i] = 'A';
}
while (1) {
file = fopen("/dev/ttyGS0", "r+");
fwrite(buf, 1, sizeof(buf), file);
fclose(file);
}
return 0;
}
간단히 transfer1라는 이름으로 실행 파일을 만들었다.
sudo gcc 소스코드 -o transfer1
2. 랜덤한 64bits (8 bytes) 데이터를 생성하여 반복적으로 보내는 프로그램
이 경우 파일 내용은 매번 새로 써서 8 bytes 씩 보내는 것이므로 하므로 w+ 옵션을 이용한다. (근데 이렇게 해도 Bulk Transfer의 특성상 가능한 최대한 많은 양의 데이터를 보낸다.)
#include <stdio.h>
#include <stdlib.h> // rand()
#include <time.h> // time()
#include <stdint.h> // uint32_t
uint32_t rand_uint32(void) {
uint32_t r = 0;
for (int i = 0; i < 32; i++) {
r = r * 2 + rand() % 2;
}
return r;
}
// 4바이트를 읽어 버퍼의 특정 인덱스에 삽입
void val32_to_buf(char* buf, uint32_t val, uint32_t index) {
buf[index] = val;
buf[index + 1] = val >> 8;
buf[index + 2] = val >> 16;
buf[index + 3] = val >> 24;
}
int main(void) {
FILE* file;
char buf[8];
srand(time(NULL));
while (1) {
uint32_t r1 = rand_uint32();
uint32_t r2 = rand_uint32();
val32_to_buf(buf, r1, 0);
val32_to_buf(buf, r2, 4);
file = fopen("/dev/ttyGS1", "w+");
fwrite(buf, 1, sizeof(buf), file);
fclose(file);
}
return 0;
}
이어서 transfer2라는 이름으로 실행 파일을 만들었다.
sudo gcc 소스코드 -o transfer2
※ Serial 통신 프로그램 자동 실행 ※
이후에 /etc/rc.local를 수정한다. 두 프로그램을 백그라운드에서 실행할 수 있도록 한다.
stty -F /dev/ttyGS0 raw
stty -F /dev/ttyGS1 raw
/home/pi/transfer1 &
/home/pi/transfer2 &
이후에 sudo reboot을 해서 재부팅하자. 그러면 재부팅 이후에 호스트(host) PC 입장에서는 다음과 같이 나온다.
※ 호스트 쪽 프로그램 작성 ※
호스트 쪽 프로그램은 다음과 같이 pyserial 라이브러리를 이용하여 작성할 수 있다. 그냥 USB 디바이스로부터 데이터를 받아 출력하는 코드다. 윈도우(Windows) OS에서는 Serial USB 장치가 꽂힌 경우 COM 포트로 인식하기 때문에 이를 이용하면 된다.
import time
import serial
s = serial.Serial("COM8", 115200)
total = 0
start_time = time.time()
last_time = 0
while True:
# 가져올 데이터가 존재하는 경우에만 처리
len = s.inWaiting()
if len == 0:
continue
data = s.read(len)
print(data)
time.sleep(1)
s.close()
프로그램을 실행하면 다음과 같이 'A'를 계속해서 받는 것을 알 수 있다.
※ (추가) Guest가 데이터를 받아서 단순히 파일에 기록할 때 ※
USB device가 호스트(host)로부터 데이터를 받아서 저장할 때는 다음과 같이 한다. 그냥 /dev/ttyGS1에서 파일 읽기를 수행하여 특정한 파일에 쓰는 것(이어쓰기)이다.
cat /dev/ttyGS1 >> /home/pi/test.dat
결과적으로 다음과 같이 /etc/rc.local 파일을 수정하면 된다. 기본적으로 cat과 >> 를 이용하여 파일의 뒤쪽에 쭉 이어 붙여 데이터를 쓸 수 있도록 한다.
stty -F /dev/ttyGS0 raw
stty -F /dev/ttyGS1 raw
/home/pi/transfer1 &
cat /dev/ttyGS1 >> /home/pi/test.dat
이제 리부팅 진행한 뒤에 호스트에서 다음과 같은 프로그램을 작성하여 보내보자.
import serial
s = serial.Serial("COM6", 115200)
s.write(b'Hello\n')
s.close()
프로그램을 여러 번 실행하면 라즈베리파이에는 다음과 같이 파일의 내용이 쓰인다.
'기타' 카테고리의 다른 글
Raspberry Pi Zero W를 이용해 2개의 USB Configuration을 재부팅마다 교체하기 (0) | 2020.11.15 |
---|---|
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법 (0) | 2020.11.15 |
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기 (1) | 2020.11.15 |
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법 (2) | 2020.11.09 |
카카오톡 오픈채팅 단체 카톡방 만드는 방법 (0) | 2020.11.09 |
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기
C언어를 이용해 랜덤한 데이터를 생성해 출력하는 방법은 다양하다. 그중에서 한 가지 방법은 다음과 같다.
#include <stdio.h>
#include <stdlib.h> // rand()
#include <time.h> // time()
#include <stdint.h> // uint32_t
uint32_t rand_uint32(void) {
uint32_t r = 0;
for (int i = 0; i < 32; i++) {
r = r * 2 + rand() % 2;
}
return r;
}
// 4바이트를 읽어 버퍼의 특정 인덱스에 삽입
void val32_to_buf(char* buf, uint32_t val, uint32_t index) {
buf[index] = val;
buf[index + 1] = val >> 8;
buf[index + 2] = val >> 16;
buf[index + 3] = val >> 24;
}
int main(void) {
FILE* file;
char buf[8];
srand(time(NULL));
while (1) {
uint32_t r1 = rand_uint32();
uint32_t r2 = rand_uint32();
printf("%u\n", r1);
printf("%u\n", r2);
val32_to_buf(buf, r1, 0);
val32_to_buf(buf, r2, 4);
for (int i = 0; i < sizeof(buf); i++) {
printf("%X", buf[i]);
}
printf("\n");
}
}
컴파일 이후에 실행해 보면 다음과 같이 출력되는 것을 확인할 수 있다. 구체적으로 4 bytes 정수형 데이터 2개를 생성한 뒤에 이를 이용하여 64 bits 데이터를 생성해 낸다.
'기타' 카테고리의 다른 글
무료 파일 공유 서비스: 미디어파이어(MediaFire) 사용 방법 (0) | 2020.11.15 |
---|---|
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제 (0) | 2020.11.15 |
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법 (2) | 2020.11.09 |
카카오톡 오픈채팅 단체 카톡방 만드는 방법 (0) | 2020.11.09 |
Linux HID Gadget Driver를 이용해 나만의 커스텀 HID 장치 만들기 (0) | 2020.10.27 |
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법
[바꾸기] - [글꼴 바꾸기]를 클릭한다.
이후에 특정한 글꼴을 한꺼번에 다른 글꼴로 바꿀 수 있다.
'기타' 카테고리의 다른 글
Raspberry Pi Zero W를 이용한 USB Gadget Serial 데이터 전송 예제 (0) | 2020.11.15 |
---|---|
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기 (1) | 2020.11.15 |
카카오톡 오픈채팅 단체 카톡방 만드는 방법 (0) | 2020.11.09 |
Linux HID Gadget Driver를 이용해 나만의 커스텀 HID 장치 만들기 (0) | 2020.10.27 |
Raspberry Pi Zero W를 이용해 멀티 가젯(Multi-gadget) USB Device 만들기 (0) | 2020.10.23 |
카카오톡 오픈채팅 단체 카톡방 만드는 방법
※ 카카오톡 오픈채팅 단체 카톡방 만드는 방법 ※
카카오톡 모바일 애플리케이션을 연 뒤에 [채팅방 생성 버튼]을 누른다.
이후에 일반채팅, 비밀채팅, 오픈채팅 중에서 하나를 선택해 채팅방을 만들 수 있다. 오픈채팅의 경우 기본적으로 누구나 접속하여 참여할 수 있는 채팅방 유형을 의미한다. [오픈채팅]을 선택하자.
오픈채팅 채널로 들어오면 다른 사람이 만든 채팅방을 검색하여 들어갈 수도 있으며 혹은 내가 직접 오픈채팅방을 만들 수 있다. 여기에서는 [만들기] 버튼을 눌러 오픈채팅방을 만들어 보자.
이제 오픈채팅방을 만들 수 있다. 여기에서는 1:1 채팅방을 만들 수도 있고 그룹 채팅방을 만들 수도 있다. 일반적인 경우 [그룹채팅방]을 만들어 다수의 참여자가 하나의 주제로 대화를 나눌 수 있도록 한다.
이후에 오픈채팅방의 제목을 설정하면 된다. 필자의 경우 강의 수강생들에게 공지하거나 질문을 받기 위해 오픈채팅방을 만들었다.
이후에 다른 사람들이 검색을 통해 채팅방에 들어올 수 있도록 만들고 싶다면 해시태그를 붙여서 오픈채팅방의 주제에 대하여 넣어줄 수 있도록 한다.
이후에 방장으로서 자신의 프로필을 설정한다. 그리고 [톡 프로필로만 참여 허용] 버튼을 누르면 오픈채팅에 참가하는 사람들이 자신의 공식 카카오톡 프로필로만 채팅에 참여할 수 있게 된다. 또한 [검색 허용] 설정을 하면 오픈채팅방의 이름과 소개를 검색하여 방에 참여할 수 있게 된다.
이어서 채팅방의 커버 이미지를 설정할 수 있다. 이 커버 이미지는 새로운 오픈채팅방을 만들 때마다 자동으로 변경된다.
결과적으로 다음과 같이 오픈채팅방이 개설된다.
※ 참여 코드(비밀번호) 설정하는 방법 ※
오픈채팅방에 아무나 들어오면 안 되고, 내가 허락한 사람만 참여하도록 만들고 싶다면 참여 코드를 설정하면 된다. 참여 코드를 설정할 때는 가장 먼저 다음의 버튼을 누른다.
이후에 [오픈채팅방 설정]에 들어간다.
여기에서 [참여 코드] 탭을 눌러서 비밀번호를 직접 설정할 수 있다.
'기타' 카테고리의 다른 글
리눅스 C언어 랜덤(무작위) 64 bits 데이터 생성하여 출력하기 (1) | 2020.11.15 |
---|---|
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법 (2) | 2020.11.09 |
Linux HID Gadget Driver를 이용해 나만의 커스텀 HID 장치 만들기 (0) | 2020.10.27 |
Raspberry Pi Zero W를 이용해 멀티 가젯(Multi-gadget) USB Device 만들기 (0) | 2020.10.23 |
Raspberry Pi Zero W 소개, 구매 방법, 환경 설정 방법 총 정리 (+ Wi-Fi 연결까지) (1) | 2020.10.23 |
Linux HID Gadget Driver를 이용해 나만의 커스텀 HID 장치 만들기
라즈베리파이(Raspberry Pi) Zero W를 이용하면 나만의 커스텀 USB HID 기기를 만들 수 있다. 이는 엄밀히 말하면 USB OTG 포트를 이용하여 라즈베리파이를 일종의 USB 디바이스(device)로 사용하는 것으로, 대용량 저장소(mass storage)부터 시작하여 키보드(keyboard)나 마우스 등의 HID 기기까지 다양한 USB 기능을 제공할 수 있다. 하지만 나만의 커스텀 HID 기기를 만드는 방법에 관해서는 소개하고 있는 글이 적다. 본 포스팅에서는 나만의 커스텀 HID 장치를 만드는 방법을 소개한다.
1. 커스텀 HID 기기의 Descriptor 명시 및 구현
※ Libcomposite 라이브러리 소개 ※
최신 리눅스 커널(kernel)은 libcomposite 라이브러리를 제공한다. 이 라이브러리는 여러 개의 가젯(gadget)을 한꺼번에 활성화 할 수 있도록 해준다. 여기에서 가젯(gadget) 모드란, 리눅스 시스템을 호스트 컴퓨터의 입장에서 볼 때 USB 장치인 것처럼 보이게 하는 기능을 의미한다. 라즈베리파이 OS에서도 이를 지원하기 때문에 본 포스팅에서도 libcomposite을 이용하여 커스텀 HID 기능을 제공하도록 한다. 이는 현재 가장 정석적인 방법 중 하나이다.
※ USB 장치의 Descriptor 정의하기 ※
USB 장치는 다양한 descriptor에 자신의 정보를 담아 호스트(host) PC에게 전송한다. 호스트 PC 입장에서는 자신에게 연결된 USB 장치가 무엇인지 모르기 때문에, 이는 당연한 절차이다.
▶ Device Descriptor: USB 장치의 전반적인 정보를 표현한다. 대표적으로 Vendor ID, Product ID를 통해 이 제품이 어떤 제품인지 고유한 ID를 알려주도록 하고, 어떠한 USB 버전을 따르는지 작성한다.
▶ Configuration Descriptor: 각각의 configuration이 사용하는 소비 전력과 인터페이스(interface)의 개수를 명시한다. 일반적으로 USB 장치의 configuration은 1개이고, 인터페이스는 1개 혹은 여러 개를 사용한다.
▶ Interface Descriptor: USB 장치의 인터페이스(interface)에 대한 정보를 명시한다. 여기에서 하나의 인터페이스는 키보드와 같은 USB의 하나의 기능(function)을 담당한다. 여기에서 엔드포인트(endpoint)의 개수도 표시해야 한다. 예를 들어 HID가 USB 패킷을 받을 수도 있고, 내보낼 수도 있다면 엔드포인트는 IN, OUT 각각을 위해 2개를 사용하는 것이 일반적이다.
▶ String Descriptor: 문자열 형태의 추가적인 정보를 명시한다. 제조업체(manufacturer), 상품 이름(product), 시리얼 번호(serial number)가 여기에 포함된다.
필자의 경우 Vendor ID로 0x1D6B, Product ID로 0x0104를 사용하겠다. 이는 각 Linux Foundation, Multifunction Composite Gadget을 의미한다. 사실 엄밀히 말하면 자기만의 Vendor ID와 Product ID를 정의해서 사용해도 되지만, 리눅스 기본 예제를 따라 이처럼 작성하겠다. 또한 USB의 버전은 2.0을 사용하고, 제조업체명은 "Dongbin Na", 상품 이름은 "My USB"로 설정하겠다. 실질적으로 바로 다음과 같이 스크립트를 작성하면 된다.
필자의 경우 /usr/bin/my_usb라는 이름으로 스크립트를 작성했으며 chmod +7 /usr/bin/my_usb 명령어를 이용하여 실행 권한을 부여했다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# 실질적으로 USB 기능(function)들이 들어갈 공간
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
※ HID 환경 설정 ※
앞서 작성한 스크립트에서는 device descriptor와 configuration descriptor가 명시되었다. 이제 우리는 HID 기기를 만들 것이기 때문에 적절한 interface descriptor를 명시할 필요가 있다.
USB 프로토콜에 따르면, 각각의 기능(function)은 하나의 인터페이스(interface)를 이용하도록 구현된다. HID의 경우 bInterfaceClass로 0x03을 사용하기로 약속되어 있다. 또한 bInterfaceSubclass와 bInterfaceProtocol도 명시해야 하는데, 대부분의 커스텀 HID 디바이스는 bInterfaceSubclass와 bInterfaceProtocol를 모두 0x00으로 설정한다. 참고로 키보드는 bInterfaceProtocol를 1로, 마우스는 bInterfaceProtocol를 2로 사용한다.
다시 말해 커스텀 HID의 경우 다음과 같이 설정하면 된다.
▶ Interface Class: 0x03
▶ Interface Subclass: 0x00
▶ Interface Protocol: 0x00
※ HID Report Descriptor 정의하기 ※
또한 커스텀 HID 장치를 만들기 전에 HID report descriptor의 개념에 대하여 알아야 한다. 모든 HID 기기는 자신이 어떤 HID 기기인지에 대한 정보를 descriptor에 기록하여 호스트(host) PC로 보낸다. 호스트 PC 입장에서는 이 HID 기기가 어떠한 기기인지 알기 위해서 이러한 descriptor를 참고할 수 있다.
HID 기기는 usage를 이용해 그 기능을 명시한다. 이때 usage page와 usage ID가 사용된다. 예를 들어 usage page로 0x01을 사용하면 일반적인 데스크탑 목적이라는 의미이고, usage ID로 0x06을 사용하면 키보드 목적이라는 의미가 된다. 일반적으로 사용되는 usage 정보를 확인하고 싶다면 아래 링크를 참고하자.
▶ 참고: docs.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages
다시 말해 이미 존재하는 키보드(keyboard)나 마우스(mouse)로서 동작하기를 원한다면, 이미 정해진 설정에 따라서 개발을 진행하면 되는 것이다. 하지만 커스텀 HID 장치를 만들고자 한다면 이를 직접 정의할 필요가 있다.
필자는 usage page: 0xFF77, usage ID: 0x0009을 사용한다. 참고로 usage page나 usage ID를 범위 밖으로 마음대로 설정해 버리면 "보고서 ID 선언이 최상위 수준 컬렉션 외부에 있습니다."와 같은 메시지가 출력되며 정상적으로 HID 기기로 인식이 안 될 수도 있다. 따라서 usage page나 usage ID를 설정하는 것도 약속을 따라야 한다. HID descriptor를 만드는 것을 도와주는 USB 협회 공식 제공 툴도 있으니 참고하자.
/* Define: Custom HID Report Descriptor */
{
0x06, 0x77, 0xFF, // USAGE_PAGE (Vendor Defined)
0x09, 0x00, // USAGE (Undefined)
0xA1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Vendor Usage 1)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x85, 0x01, // REPORT_ID (1)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x40, // REPORT_COUNT (64)
0x81, 0x02, // INPUT (Data, Var, Abs): Device to Host
0x09, 0x02, // USAGE (Vendor Usage 2)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x85, 0x02, // REPORT_ID (2)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x40, // REPORT_COUNT (64)
0x91, 0x02, // OUTPUT (Data, Var, Abs): Host to Device
0xC0 // END_COLLECTION
};
따라서 최종적으로 다음과 같이 스크립트를 작성하면 된다. 현재 예시에서는 report size는 8비트, report count를 64를 사용한다. 결과적으로 패킷 크기(packet size)를 64 바이트로 설정하는 것이다.
mkdir -p functions/hid.usb0
echo 0 > functions/hid.usb0/protocol
echo 0 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x06\\x77\\xFF\\x09\\x00\\xA1\\x01\\x09\\x01\\x15\\x00\\x26\\xFF\\x00\\x85\\x01\\x75\\x08\\x95\\x40\\x81\\x02\\x09\\x02\\x15\\x00\\x26\\xFF\\x00\\x85\\x02\\x75\\x08\\x95\\x40\\x91\\x02\\xC0 > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/
실제로 HID를 이용하는 경우 HID report descriptor가 매우 중요한 역할을 수행한다. HID report descriptor가 잘못 작성되어 있으면 운영체제가 정상적으로 인식하지 못한다. 특히 윈도우(Windows)의 경우 USB 장치가 무엇인지 알 수 없을 때 기본 USB 드라이버를 적용한다. 따라서 위와 같이 작성한 report descriptor가 잘 동작하지 않을 가능성도 있다.
또한 라즈베리파이 측에서 스크립트에 실행 권한이 없어서 스크립트 자체가 실행되지 않는 경우에도 당연히 운영체제가 정상적으로 인식이 불가능 때문에, 이 부분에 대하여 인지할 필요가 있다. 추가로 윈도우(Windows)에서는 정상적으로 인식하지만 Virtual Machine에서는 인식을 못 하는 경우도 있다. 이럴 때는 VM을 재부팅하도록 하자.
※ 최종적인 환경 설정 스크립트 ※
최종적으로 다음과 같은 스크립트를 작성하여 라즈베리파이가 이를 실행하도록 만들면 된다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# 실질적으로 USB 기능(function)들이 들어갈 공간
mkdir -p functions/hid.usb0
echo 0 > functions/hid.usb0/protocol
echo 0 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x06\\x77\\xFF\\x09\\x00\\xA1\\x01\\x09\\x01\\x15\\x00\\x26\\xFF\\x00\\x85\\x01\\x75\\x08\\x95\\x40\\x81\\x02\\x09\\x02\\x15\\x00\\x26\\xFF\\x00\\x85\\x02\\x75\\x08\\x95\\x40\\x91\\x02\\xC0 > functions/hid.usb0/report_desc
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/hid.usb0 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
실제로 라즈베리파이가 위 스크립트를 실행하도록 만들고 라즈베리파이를 호스트 PC(리눅스 OS)에 연결해보자. 참고로 기본적인 OS가 윈도우(Windows)인 사람은 VM을 설치하여 사용할 수 있다. 기본적으로 윈도우(Windows) OS에 연결하면 장치 관리자에 다음과 같이 등장한다. 정상적으로 VID: 1D6B, PID: 0104로 기기가 인식되는 것을 알 수 있다.
앞서 말했듯이 Oracle VM VirtualBox 환경이라면 다음과 같이 USB 장치를 인식하도록 해주어야 한다.
이후에 호스트 PC에서 lsusb 명령어를 이용해 연결된 USB 정보를 확인하면 다음과 같다. 앞서 정의했던 내용 그대로 환경 설정이 되어 있는 것을 확인할 수 있다. 기본적인 Device Descriptor가 모두 정의했던 대로 인식된다.
참고로 USB 가젯의 설정 자체를 바꾼 경우라면 UDC를 새롭게 설정하더라도 재부팅을 하지 않으면 환경 설정 내용이 바뀌지 않는 경우가 있다. 따라서 동적으로 가젯 하나만 더 추가하는 정도가 아니라면 재부팅을 수행하자.
2. 커스텀 HID 기기 Host 사이드 구현
※ 호스트(Host) 프로그램 만들기 ※
리눅스 호스트(host) 상에서 커스텀 HID 기기를 처리하기 위한 프로그램을 간단히 만들어보자. 사실 Teensy 공식 사이트의 코드를 참고하여 미리 프로그램을 작성해 놓았다. 전체 코드는 필자의 깃허브 저장소에서 확인할 수 있다.
코드 중에서 hid_LINUX.c는 리눅스의 usb.h에 정의된 함수를 이용해 HID 관련 함수를 정의한 코드이다. HID 장치를 찾는 함수, HID 장치로 USB 패킷을 보내는 함수, HID 장치에서 USB 패킷을 받는 함수가 정의되어 있다. 흔히 HID 프로토콜을 따르되 단순히 interrupt transfer를 이용하여 패킷을 주고 받는 형식을 raw HID라고 부른다. 실제 코드를 확인해 보면 usb_interrupt_read() 함수와 usb_interrupt_write() 함수를 이용하는 것을 확인할 수 있다.
이후에 custom_hid.c 코드에서는 앞서 정의한 함수들을 이용하여 HID 기기와 통신을 진행한다. 앞서 설명했듯이 vendor ID: 0x1D6B, product ID: 0x0104, usage page: 0xFF77, usage ID: 0x0009를 가지는 HID 기기를 찾는 것을 알 수 있다. HID 기기를 찾아서 연 이후에는 매번 HID 기기로부터 64 바이트씩 데이터를 전달 받도록 한다. 데이터를 받은 이후에는 전달 받은 데이터를 출력하고, 다시 64바이트의 데이터를 채워서 전송한다.
단순히 라즈베리파이를 연결한 뒤에 custom_hid 프로그램을 실행하면 다음과 같이 HID 기기를 찾아내는 것을 알 수 있다. 참고로 알려지지 않은 HID 기기를 이용하기 위해서는 sudo 권한으로 프로그램을 실행할 필요가 있다.
3. 커스텀 HID 기기 Device 사이드 구현
※ 호스트(Host)로 데이터 전송해보기 ※
HID gadget을 사용할 때는 단순히 HID 장치에 파일 쓰기(file write)을 이용하여 문제를 해결할 수 있다. echo 명령어에 ne 옵션을 붙여서 (n 옵션: new line 제외, e 옵션: escape 문자 사용) 호스트로 데이터를 보낼 수 있다. 실제로 라즈베리파이에서 다음과 같은 명령어를 실행하면 된다.
sudo bash -c 'echo -ne "\x97\x77" > /dev/hidg0'
그러면 호스트(host) 쪽에서는 다음과 같이 메시지를 전송 받게 된다.
참고로 Raspberry Pi가 호스트(host) 쪽에서 온 데이터를 읽기 위한 목적으로 sudo cat /dev/hidg0을 입력한 이후에 위와 같은 echo 명령어를 다시 실행하면 write error: Resource temporarily unavailable 와 같은 오류가 발생할 수 있다. 필자의 경우 라즈베리파이와 Linux OS 재부팅 이후에 문제가 사라졌다.
※ 호스트(Host)로부터 받은 데이터 확인하기 ※
호스트로부터 받은 데이터를 확인하기 위해서는 간단히 /dev/hidg0에 전달된 데이터가 있는지 확인하면 된다.
※ Device 사이드 프로그램 작성하기 (기본형) ※
간단하게 무한정 호스트로 데이터를 보내고, 호스트에서 도착한 데이터를 받아 출력하도록 해보자.
#include <stdio.h>
#include <unistd.h> // sleep()
#include <string.h> // memset()
int main(void) {
FILE* file;
char buf[64];
while (1) {
// send to host
for (int i = 0; i < sizeof(buf); i++) {
buf[i] = 'A' + i;
}
printf("\nsend %d bytes:\n", sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%02X ", buf[i] & 255);
if (i % 16 == 15 && i < sizeof(buf)- 1) printf("\n");
}
printf("\n");
fflush(stdout);
file = fopen("/dev/hidg0", "w+");
fwrite(buf, 1, sizeof(buf), file);
fclose(file);
printf("send complete.");
fflush(stdout);
// receive from host
file = fopen("/dev/hidg0", "w+");
fread(buf, 1, sizeof(buf), file);
fclose(file);
printf("\nreceive %d bytes:\n", sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%02X ", buf[i] & 255);
if (i % 16 == 15 && i < sizeof(buf) - 1) printf("\n");
}
printf("\n");
fflush(stdout);
printf("receive complete.");
fflush(stdout);
sleep(1);
}
return 0;
}
실행하면 디바이스에서는 다음과 같이 동작한다. 매번 호스트로 64 바이트의 데이터를 보낸 뒤에 'F'(46)로 가득 채워진 64 바이트의 데이터를 받는 것을 알 수 있다.
반대로 호스트 쪽에서는 데이터를 전달 받은 뒤에 매번 'F'(46)로 가득 채운 64 바이트 크기의 문자열을 돌려준다.
이러한 과정이 무한정 반복된다. 참고로 파일 쓰기(file write)과 파일 읽기(file read) 사이에 sleep()을 주어 interrupt polling 시간을 컨트롤하려고 했는데, 중간에 sleep()을 주는 경우에는 호스트로부터 정상적으로 패킷을 받아올 수 없었다. 그 이유에 대해서는 추후에 시간이 날 때 다시 찾아 볼 예정이다. 현재 예상되는 바는, 지금 코드상에서는 호스트 쪽에서 데이터를 보내기까지 무한정 대기하는 것으로 보인다.
※ HID 패킷 속도(Speed) 측정하기 ※
이번에는 sleep() 호출 없이 무한정 패킷을 주고 받도록 만들어 보았다. 다음의 코드와 같이 차례대로 순번을 증가시키면서 데이터를 전송했는데, 데이터의 유실 없이 모든 패킷이 빠르게 주고 받아 지는 것을 확인할 수 있었다. 다만 패킷을 주고 받는 속도가 1초당 1,000번 정도는 나올 거라고 생각했는데, 실제로는 그보다 느리게 동작했다. 그래도 초당 30번 정도는 패킷을 주고 받는 것으로 보인다. 다시 말해 초당 2KB 정도의 속도는 나온다는 것인데, 대용량 데이터를 주고 받기에는 한계가 있는 것으로 보인다.
#include <stdio.h>
#include <unistd.h> // sleep()
#include <string.h> // memset()
int main(void) {
FILE* file;
char buf[64];
char sign = 0;
int cnt = 0;
while (1) {
// send to host
for (int i = 0; i < sizeof(buf); i++) {
buf[i] = 'A' + i;
}
buf[sizeof(buf) - 1] = sign++;
printf("\n[%d] send %d bytes:\n", cnt++, sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%02X ", buf[i] & 255);
if (i % 16 == 15 && i < sizeof(buf) - 1) printf("\n");
}
printf("\n");
fflush(stdout);
file = fopen("/dev/hidg0", "w");
fwrite(buf, 1, sizeof(buf), file);
fclose(file);
printf("send complete.");
fflush(stdout);
// receive from host
file = fopen("/dev/hidg0", "r");
fread(buf, 1, sizeof(buf), file);
fclose(file);
printf("\nreceive %d bytes:\n", sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%02X ", buf[i] & 255);
if (i % 16 == 15 && i < sizeof(buf) - 1) printf("\n");
}
printf("\n");
fflush(stdout);
printf("receive complete.");
fflush(stdout);
}
return 0;
}
디바이스(device) 쪽의 출력 형태는 다음과 같다.
※ Device 사이드 프로그램 작성하기: 호스트에서 데이터 받기만 하기 ※
무작정 호스트로부터 데이터를 받기만 할 때는 다음과 같이 작성한다.
#include <stdio.h>
#include <unistd.h> // sleep()
#include <string.h> // memset()
#define BUFFER_SIZE 64 // 패킷(버퍼) 사이즈
FILE* file;
char buf[BUFFER_SIZE];
int main(void) {
while (1) {
// receive from host
file = fopen("/dev/hidg0", "w+");
fread(buf, 1, sizeof(buf), file);
fclose(file);
printf("\nreceive %d bytes:\n", sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
printf("%02X ", buf[i] & 255);
if (i % 16 == 15 && i < sizeof(buf) - 1) printf("\n");
}
printf("\n");
fflush(stdout);
printf("receive complete.");
fflush(stdout);
}
return 0;
}
만약 호스트가 예시로 임의의 크기의 데이터를 보내는 코드를 실행하게 되면 다음과 같이 데이터를 받게 된다.
※ 라즈베리파이 부팅 이후에 프로그램 자동 실행 ※
부팅 이후에 HID로 데이터를 주고 받는 프로그램이 자동으로 실행되도록 만드려면 /etc/rc.local을 수정한다. 기본적으로 프로그램은 백그라운드(background)에서 실행되어야 하므로 다음과 같이 & 기호를 붙인다.
이어서 재부팅을 하면 다음과 같이 설정했던 프로그램이 자동으로 정상적으로 실행되는 것을 알 수 있다.
※ Mass Storage와 함께 쓰기 ※
다음과 같이 파일을 저장하기 위한 목적으로 블록 디바이스를 하나 만들 수 있다.
mkdir -p /home/pi/images
# 1Gb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk1.img bs=1048576 count=1024
mkdosfs /home/pi/images/usbdisk1.img
이제 환경 설정 파일을 다음과 같이 수정하여 HID와 Mass Storage Class를 같이 제공하도록 해보자. 호스트(host) PC 입장에서는 읽기만 가능하다고 가정한다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB 2.0
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# 실질적으로 USB 기능(function)들이 들어갈 공간
mkdir -p functions/hid.usb0
echo 0 > functions/hid.usb0/protocol
echo 0 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x06\\x77\\xFF\\x09\\x00\\xA1\\x01\\x09\\x01\\x15\\x00\\x26\\xFF\\x00\\x85\\x01\\x75\\x08\\x95\\x40\\x81\\x02\\x09\\x02\\x15\\x00\\x26\\xFF\\x00\\x85\\x02\\x75\\x08\\x95\\x40\\x91\\x02\\xC0 > functions/hid.usb0/report_desc
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/hid.usb0 configs/c.1/
################# Mass Storage 1 #################
# 첫 번째 이미지 지정하기
FILE1=/home/pi/images/usbdisk1.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE1/img/d}
mount -o loop, -t vfat $FILE1 ${FILE1/img/d} # 라즈베리 파이 입장에서 쓰기 가능
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
echo 1 > functions/mass_storage.usb0/lun.0/ro # 호스트 입장에서는 일기 전용(read-only)
echo 0 > functions/mass_storage.usb0/lun.0/nofua
echo $FILE1 > functions/mass_storage.usb0/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb0 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
이제 재부팅 이후에 성공적으로 HID 기능을 제공하면서, 그와 동시에 Mass Storage Class로의 기능도 제공하게 된다. 다만 한가지 유의할 점은 라즈베리파이가 호스트로부터 HID로 데이터를 주고받아 블록 장치에 쓰기 연산을 수행하는 상황에서, 호스트 입장에서는 Mass Storage 기능을 이미 제공 받고 있기 때문에 블록 장치에 새롭게 기록된 정보가 호스트 입장에서 즉시 보이지는 않는다. 이때 라즈베리파이의 데이터 케이블의 연결을 잠시 끊었다가 다시 연결하면, 비로소 호스트에 다시 Mass Storage 디바이스가 잡혔을 때 해당 파일들이 보이게 된다.
참고 자료
1) 리눅스 공식 HID Gadget 튜토리얼: 본 사이트에서는 기본적인 마우스, 키보드 기능을 구현하는 방법을 소개한다. 커스텀 HID를 만들기 전에 간단히 실습해 보기에 좋다.
2) HID Report Descriptor 생성 프로그램: usb.org 공식 웹 사이트에서 제공하는 HID descriptor 도구다.
'기타' 카테고리의 다른 글
파워포인트(PowerPoint)에서 글꼴(글씨체) 한꺼번에 변경하는 방법 (2) | 2020.11.09 |
---|---|
카카오톡 오픈채팅 단체 카톡방 만드는 방법 (0) | 2020.11.09 |
Raspberry Pi Zero W를 이용해 멀티 가젯(Multi-gadget) USB Device 만들기 (0) | 2020.10.23 |
Raspberry Pi Zero W 소개, 구매 방법, 환경 설정 방법 총 정리 (+ Wi-Fi 연결까지) (1) | 2020.10.23 |
USB 케이블 잘라서 데이터(Data-only) 케이블로 만들기 (2) | 2020.10.19 |
Raspberry Pi Zero W를 이용해 멀티 가젯(Multi-gadget) USB Device 만들기
USB 장치(device)는 태생적으로 하나의 장치가 여러 개의 기능(function)을 제공할 수 있도록 만들어졌다. 참고로 여기에서 기능(function)이란 USB 프로토콜 상에서 인터페이스(interface)를 의미한다. 예를 들어 마우스(mouse), 키보드(keyboard), HID 장치, 대용량 저장소(mass storage) 등은 각각 하나의 기능(function)이다. 이후에 각 기능은 한 개 이상의 엔드포인트(endpoint)를 가질 수 있으며, 이를 통해 호스트(host) PC와 USB 프로토콜에 맞게 데이터를 주고받는다.
이번 포스팅에서는 Raspberry Pi Zero W를 이용해 멀티 가젯(multi-gadget) USB 장치를 만드는 방법을 소개하고자 한다. 우리의 라즈베리 파이를 일종의 USB 장치로 쓰면서, 동시에 대용량 저장소나 마우스 등의 다양한 기능을 제공하는 기기가 될 수 있도록 하는 것이다.
※ 기초 환경 설정 ※
가장 먼저 USB OTG 기능을 제공하기 위해 라즈베리 파이의 /boot/config.txt 파일을 열어서 제일 마지막 줄에 다음의 코드를 추가한다. dwc2 USB 드라이버를 사용할 수 있도록 하는 것이다.
dtoverlay=dwc2
Raspberry Pi Zero W를 기준으로 다음과 같다.
이어서 /etc/modules의 마지막 줄에 다음 두 개의 커널 모듈을 넣어주자. 부팅 과정에서 아래의 두 모듈을 자동으로 로드하여 사용할 수 있도록 한다. 만약 부팅과 동시에 멀티 가젯 기능을 제공하지는 않고 수동적으로 필요할 때마다 기능을 제공하고 싶다면 libcomposite는 빼고 넣어주도록 하자.
dwc2
libcomposite
이때 libcomposite 커널 모듈이 핵심적인 역할을 수행한다. 이 모듈은 여러 개의 가젯(gadget)을 한꺼번에 활성화 할 수 있도록 해준다. 일반적으로 USB 가젯(gadget) 모드란, 리눅스 시스템을 호스트 컴퓨터의 입장에서 보았을 때 USB 장치(device)인 것처럼 보이게 하는 방법을 의미한다. 우리의 Raspberry Pi를 HID 장치나 Mass Storage Class (MSC)처럼 보이게 만들 수 있다.
이러한 배경에서 USB 가젯 기능을 제공하되, 한 번에 여러 개의 USB 디바이스 기능을 제공하려면 libcomposite 모듈을 사용할 수 있다. 이를 이용하면 나의 Raspberry Pi Zero W가 동시에 3개의 USB 대용량 저장소이면서, 그와 동시에 마우스와 키보드의 역할을 수행하도록 만들 수 있다. 이처럼 간단하게 여러 개의 USB 장치 역할을 수행하도록 만들 수 있다.
※ 멀티 가젯(Multi-gadget) 모드 확인 ※
이제 /sys/kernel/config/usb_gadget/ 폴더를 활성화해야 한다. 이 폴더 안에서 규칙에 맞게 생성된 파일들을 통해 실제로 multi-gadget 역할을 수행할 수 있다. 이 폴더는 libcomposite 모듈을 커널에 적재함으로써 활성화할 수 있다. 앞서 우리는 재부팅을 했을 때 리눅스 커널 모듈 중에서 libcomposite이 불러와지도록 만들었다. 자동으로 모듈을 불러오도록 설정하지 않았다면 modprobe 명령을 이용해 수동으로 적재하자.
modprobe 명령어는 커널 모듈(kernel module)을 리눅스 커널에 추가하거나 커널로부터 제거할 때 사용할 수 있다. 원래 모듈을 추가하거나 제거할 때는 insmod나 rmmod를 사용할 수 있는데, modprobe는 더욱 다양한 기능을 제공한다. modprobe는 모듈을 올릴 때 여러 개의 모듈을 한꺼번에 올릴 수 있으며, 의존성이 있는 모듈도 같이 적재할 수 있다.
재부팅하지 않고 바로 libcomposite을 적재하기 위해서는 다음과 같은 명령어를 입력하면 된다. (/etc/modules에 libcomposite을 추가했다면 sudo reboot 명령을 통해 재부팅 하자.)
sudo modprobe libcomposite
이후에 /sys/kernel/config/usb_gadget 경로가 생긴 것을 확인할 수 있다. 이제 이 경로에 각종 환경 설정 파일을 생성하여 우리의 라즈베리 파이가 USB 가젯으로 동작하게 만들 수 있다.
※ 환경 설정 스크립트 만들기 ※
Texas Instruments 교육 자료를 보면 USB 멀티 가젯(multi-gadget) 구조를 쉽게 확인할 수 있다. 바로 다음과 같은 형태로 파일을 배치하면 그에 맞게 다양한 기능(function)이 동작한다.
따라서 이러한 구조에 맞게 파일을 생성하는 하나의 환경 설정 스크립트를 작성하는 것이 좋다.
sudo touch /usr/bin/my_usb # 파일 생성
sudo chmod +x /usr/bin/my_usb # 실행할 수 있도록 만들기
이제 이러한 환경 설정 스크립트를 작성해보자.
sudo nano /usr/bin/my_usb # 스크립트 작성하기
가장 먼저 global configuration에 대하여 작성할 필요가 있다. 앞서 말했듯이 하나의 configuration 안에 여러 개의 기능(function)이 들어가 있는 형태로 동작한다. 따라서 일단 기능(function) 부분은 제외하고 configuration 관련 내용을 작성하여 전체적인 틀을 잡도록 하자.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# 실질적으로 USB 기능(function)들이 들어갈 공간
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
※ 대용량 저장소(Mass Storage) 기능 추가하기 ※
필자는 개인적으로 Mass Storage Class (MSC) 기능이 필요한 상황이다. 따라서 Mass Storage 기능을 추가하는 방법을 소개한다. 일단 가장 먼저 블록 장치(block device)를 만들 필요가 있다. 라즈베리파이를 USB 대용량 저장소로 보이도록 만들고 싶다면, 호스트(host) PC 입장에서는 블록 단위로 접근할 필요가 있기 때문이다.
필자는 다음과 같이 2개의 이미지를 만들었다. 이미지 내에서 하나의 블록 크기(block size)는 1,048,576 바이트(1Mb)로 설정했다. 이후에 mkdosfs 명령어를 이용해 파일 시스템 형태로 포맷(format)할 수 있도록 한다.
mkdir -p /home/pi/images
# 1Gb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk1.img bs=1048576 count=1024
mkdosfs /home/pi/images/usbdisk1.img
# 500Mb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk2.img bs=1048576 count=512
mkdosfs /home/pi/images/usbdisk2.img
이제 기능(function) 부분에 다음과 같은 코드를 추가하자. 필자는 다음과 같이 총 2개의 Mass Storage를 제공할 수 있도록 2개의 기능을 명시했다.
################# Mass Storage 1 #################
# 첫 번째 이미지 지정하기
FILE1=/home/pi/images/usbdisk1.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE1/img/d}
mount -o loop,ro, -t vfat $FILE1 ${FILE1/img/d} # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
echo 0 > functions/mass_storage.usb0/lun.0/ro # 호스트 입장에서는 쓰기 가능
echo 0 > functions/mass_storage.usb0/lun.0/nofua
echo $FILE1 > functions/mass_storage.usb0/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb0 configs/c.1/
################# Mass Storage 2 #################
# 두 번째 이미지 지정하기
FILE2=/home/pi/images/usbdisk2.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE2/img/d}
mount -o loop,ro, -t vfat $FILE2 ${FILE2/img/d} # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb1
echo 1 > functions/mass_storage.usb1/stall
echo 0 > functions/mass_storage.usb1/lun.0/cdrom
echo 0 > functions/mass_storage.usb1/lun.0/ro # 호스트 입장에서는 쓰기 가능
echo 0 > functions/mass_storage.usb1/lun.0/nofua
echo $FILE2 > functions/mass_storage.usb1/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb1 configs/c.1/
※ 두 개의 Mass Storage 장치 전체 스크립트 ※
두 개의 Mass Storage를 이용하는 예제의 전체 /usr/bin/my_usb 스크립트 최종본은 다음과 같다.
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p my_usb
cd my_usb
# 기본적인 USB 클래스 명시
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
# 내가 만들 USB 장치의 기본적인 이름
mkdir -p strings/0x409
echo "0123456789abcdef" > strings/0x409/serialnumber
echo "Dongbin Na" > strings/0x409/manufacturer
echo "My USB" > strings/0x409/product
# 하나의 Configuration 정보 작성
mkdir -p configs/c.1/strings/0x409
echo "My USB Config 1" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
################# Mass Storage 1 #################
# 첫 번째 이미지 지정하기
FILE1=/home/pi/images/usbdisk1.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE1/img/d}
mount -o loop,ro, -t vfat $FILE1 ${FILE1/img/d} # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
echo 0 > functions/mass_storage.usb0/lun.0/ro # 호스트 입장에서는 쓰기 가능
echo 0 > functions/mass_storage.usb0/lun.0/nofua
echo $FILE1 > functions/mass_storage.usb0/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb0 configs/c.1/
################# Mass Storage 2 #################
# 두 번째 이미지 지정하기
FILE2=/home/pi/images/usbdisk2.img
# 해당 이미지에 마운트(mount) 진행하기
mkdir -p ${FILE2/img/d}
mount -o loop,ro, -t vfat $FILE2 ${FILE2/img/d} # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
mkdir -p functions/mass_storage.usb1
echo 1 > functions/mass_storage.usb1/stall
echo 0 > functions/mass_storage.usb1/lun.0/cdrom
echo 0 > functions/mass_storage.usb1/lun.0/ro # 호스트 입장에서는 쓰기 가능
echo 0 > functions/mass_storage.usb1/lun.0/nofua
echo $FILE2 > functions/mass_storage.usb1/lun.0/file # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
ln -s functions/mass_storage.usb1 configs/c.1/
# UDC (Usb Device Controller)
ls /sys/class/udc > UDC
최종적으로 대략 다음과 같이 작성된다. (후반부 생략)
이제 이러한 스크립트를 sudo 권한으로 실행하면 곧바로 USB 멀티 가젯 기능이 제공된다.
이후에 호스트(host) PC에 연결하면 다음과 같이 정상적으로 대용량 저장소로 인식된다. 윈도우(Windows)와 우분투(Ubuntu) 모두에서 정상적으로 인식되는데, 윈도우 장치 관리자로 보았을 때는 다음과 같다.
앞서 만들었던 두 개의 이미지(image)가 정상적으로 인식된다.
※ 내 스크립트가 자동으로 실행될 수 있도록 하기 ※
또한 라즈베리파이가 재부팅되면 곧바로 스크립트가 실행되도록 설정할 수 있다. (단, 이게 정상적으로 동작하려면 앞서 /etc/modules에 libcomposite을 추가한 상태여야 한다.) /etc/rc.local을 수정하자. 리눅스는 부팅을 진행하는 과정에서 /etc/rc.local 스크립트를 자동으로 실행한다.
sudo nano /etc/rc.local
이후에 exit 0 위쪽 라인에 /usr/bin/my_usb를 추가하자. 라즈베리파이 구동 이후에 자동으로 우리가 작성했던 스크립트를 구동하도록 만드는 것이다.
이제 재부팅(reboot)을 진행한다.
그러면 재부팅 이후에는 라즈베리파이를 연결만 하면 자동으로 Mass Storage 형태로 인식이 된다.
다만 간혹 두 개의 Mass Storage를 이용하도록 했으나 하나만 인식되는 문제가 발생하거나, 연결이 되었다가 안 되었다가 하는 문제가 발생할 수 있다. 이럴 때는 데이터 공급선(USB OTG)이 정상적으로 연결되어 있는지를 다시 한 번 체크해보도록 하자. 혹은 호스트(host) PC의 다른 포트로 연결하면 잘 동작하는 경우가 있다.
※ 동적으로 Mass Storage 이미지 변경하기 ※
Mass Storage 목적으로 리눅스 가젯(gadget) 드라이버를 활용한다면, 다양한 기능에 대하여 추가적으로 알아 두면 좋다. 참고로 Mass Storage의 경우 동적으로 블록 장치(block device)를 변경할 수 있다. 동적으로 블록 장치를 변경해 보기 위하여 하나의 블록 장치를 만들어 보자.
# 500Mb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk3.img bs=1048576 count=512
mkdosfs /home/pi/images/usbdisk3.img
이후에 Mass Storage 기능이 제공되고 있는 상황에서 다음과 같은 명령어를 입력하여 동적으로 블록 장치 이미지를 변경할 수 있다.
sudo bash -c 'echo "/home/pi/usbdisk3.img" > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb0/lun.0/file'
※ 동적으로 멀티 가젯(Gadget) 기능 올리거나 내리기 ※
라즈베리파이를 호스트(host) PC에 연결한 상태에서 원할 때 멀티 가젯 기능을 열거나 닫는 작업도 가능하다. 다음과 같이 예시로 2개의 Mass Storage 기능을 제공하는 라즈베리파이가 있다고 해보자.
가젯(gadget) 기능을 enabling 하기 위해서는 UDC(USB Device Controller)에 바운딩을 시킬 필요가 있다. 같은 원리로 disabling을 할 때는 바운딩을 해제하면 되는 것이다. 따라서 다음과 같은 명령을 실행하면 전원은 공급된 상태에서 가젯 기능만 뺄 수 있다.
sudo bash -c 'echo "" > /sys/kernel/config/usb_gadget/my_usb/UDC'
다음의 명령어를 입력하면 다시 가젯 기능이 활성화된다.
sudo bash -c 'ls /sys/class/udc > /sys/kernel/config/usb_gadget/my_usb/UDC'
※ 동적으로 특정 가젯(Gadget) 올리거나 내리기 ※
1) 동적으로 특정한 가젯을 새롭게 올리기
그렇다면 라즈베리파이를 호스트(host) PC에 연결한 상태에서 원할 때 특정한 가젯 기능만을 열거나 닫고자 한다면 어떻게 할 수 있을까? 사실 우리가 Mass Storage USB를 이용할 때는, 그냥 물리적으로 USB를 PC에서 빼면 연결이 해제된다. 하지만 라즈베리파이에서 여러 개의 가젯(gadget) 기능을 제공하고 있을 때는 어떻게 하나의 기능만 제거할 수 있을지 알아보자. 현재의 라즈베리파이가 다음과 같이 2개의 MSC를 제공한다고 해보자.
이제 간단히 하나의 블록 장치를 만들어 보자.
# 500Mb 이미지 생성
dd if=/dev/zero of=/home/pi/images/usbdisk4.img bs=1048576 count=512
mkdosfs /home/pi/images/usbdisk4.img
이후에 라즈베리파이에서 다음의 코드를 실행해보자.
# 특정 이미지에 마운트(mount) 진행하기
sudo mkdir -p /home/pi/images/usbdisk4.d
sudo mount -o loop,ro, -t vfat /home/pi/images/usbdisk4.img /home/pi/images/usbdisk4.d # 라즈베리 파이 입장에서는 읽기 전용(read-only)
# USB 기능(function) 작성하기 (kernel.org reference 참고하기)
sudo mkdir -p /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2
sudo bash -c 'echo 1 > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2/stall'
sudo bash -c 'echo 0 > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2/lun.0/cdrom'
sudo bash -c 'echo 0 > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2/lun.0/ro' # 호스트 입장에서는 쓰기 가능
sudo bash -c 'echo 0 > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2/lun.0/nofua'
sudo bash -c 'echo /home/pi/images/usbdisk4.img > /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2/lun.0/file' # 이미지 파일 명시하기
# 만들어진 기능(function) 사용할 수 있도록 설정
sudo ln -s /sys/kernel/config/usb_gadget/my_usb/functions/mass_storage.usb2 /sys/kernel/config/usb_gadget/my_usb/configs/c.1/
단순히 위 코드를 실행하는 것으로는 동적으로 반영되지 않는 것을 알 수 있다. 다음의 두 코드를 추가적으로 실행하여 UDC를 재동작 시켰을 때 비로소 정상적으로 동작한다. 참고로 단순히 호스트(Host) PC와 라즈베리파이 사이의 데이터 케이블을 연결을 해제하고 다시 연결하는 작업만으로는 정상적으로 새 기능이 추가되지 않는다.
반면에 /sys/kernel/config/usb_gadget/my_usb/UDC의 값을 공백으로 바꾸었다가 다시 정상적인 UDC 값을 기입하면 새 기능이 추가되어 재연결 된 것을 확인할 수 있다.
sudo bash -c 'echo "" > /sys/kernel/config/usb_gadget/my_usb/UDC'
sudo bash -c 'ls /sys/class/udc > /sys/kernel/config/usb_gadget/my_usb/UDC'
아무튼 케이블을 뺐다가 다시 끼지 않더라도 기능을 변경할 수 있다는 점은 확실하다.
2) 동적으로 특정한 가젯을 내리기
특정한 가젯을 내릴 때에도 비슷하다. configuration에서 바운딩 된 mass_storage.usb2 기능(function)을 다음과 같은 명령어로 동적으로 제거할 수 있다.
sudo rm /sys/kernel/config/usb_gadget/my_usb/configs/c.1/mass_storage.usb2
물론 이 명령을 실행하면 configuration 정보가 바뀌기 때문에 UDC의 값이 자동으로 공백으로 바뀌어 호스트(Host) PC와의 연결이 해제될 수 있다. 따라서 가젯을 제거한 뒤에는 다음과 같이 UDC 값을 갱신할 필요가 있다.
sudo bash -c 'ls /sys/class/udc > /sys/kernel/config/usb_gadget/my_usb/UDC'
결과적으로 호스트 PC는 다시 USB를 인식하고, 하나의 Mass Storage 기능이 정상적으로 제거된 것을 알 수 있다.
※ (추가) Serial로 터미널 접속하기 ※
Serial을 열어서 터미널로 접속할 수도 있다. (사실 굳이 사용할 이유가 없는 경우가 많다.) 이를 위해 다음과 같은 내용을 환경 설정 파일에 추가한다. 그리고 재부팅을 하면 라즈베리파이에 /dev/ttyGS0 디바이스가 생성된다. 이 디바이스를 이용하여 통신이 가능하다.
mkdir -p functions/acm.usb0
ln -s functions/acm.usb0 configs/c.1/
이어서 라즈베리파이에서 USB 시리얼 콘솔 접속을 활성화 해주자.
sudo systemctl enable getty@ttyGS0.service
호스트(host) PC 입장에서는 다음과 같이 PuTTY를 이용하여 접속할 수 있다. 윈도우(Windows)의 경우 장치 관리자에서 COM 포트 번호를 확인한 뒤에 115200 레이트로 접속하면 된다.
그러면 다음과 같이 성공적으로 접속된다.
※ (추가) Serial로 통신하기 ※
Serial을 터미널 접속용이 아니라 데이터 주고 받기용으로 사용할 수도 있다. 설정 파일에 다음의 내용을 추가하고 재부팅 해보자.
mkdir -p functions/acm.usb1
ln -s functions/acm.usb1 configs/c.1/
그러면 라즈베리파이에서는 /dev/ttyGS1이 추가적으로 생성된다. 이제 이걸로 데이터를 주고 받아보도록 하자.
1) 호스트가 데이터를 보낼 때
호스트의 파이썬 3.7 프로그램은 다음과 같다.
import serial
values = [1, 2, 3, 4, 5, 6, 7, 8, 9]
s = serial.Serial("COM7", 115200)
s.write(b'Hello\n')
s.close()
라즈베리파이 쪽에서는 다음과 같이 /dev/ttyGS1에 적힌 내용을 확인하면 된다.
2) 호스트가 데이터를 받을 때
호스트의 파이썬 3.7 프로그램은 다음과 같다.
import serial
s = serial.Serial("COM7", 115200)
while True:
data = s.readline()
print(data)
s.close()
라즈베리파이 쪽에서는 다음과 같이 /dev/ttyGS1를 통해 데이터를 전송하면 된다.
그러면 다음과 같이 호스트 쪽에서는 데이터를 받을 수 있게 된다.
이어서 라즈베리파이가 보내는 데이터를 있는 대로 계속 받아서 파일에 바이트 스트림(byte stream)으로 저장하려면 다음과 같이 하면 된다.
import serial
s = serial.Serial("COM7", 115200)
total = 0
while True:
len = s.inWaiting()
if len == 0:
continue
f = open("log.dat", "ab")
data = s.read(len)
f.write(data)
f.close()
total += len
print(f"total length:", total)
s.close()
그러면 파이썬 프로그램이 계속 실행되고, 라즈베리파이로부터 받은 데이터를 그대로 log.dat에 계속 쓴다.
(참고) Serial을 데이터 송수신 목적으로 사용하는 경우에는 raw 모드로 데이터를 전송할 필요가 있다. 그렇지 않으면 \b10을 보냈을 때 \b13\b10의 형태로 보내지는 문제가 발생할 수 있다.
※ Mass Storage Class를 Boot Device로 사용할 때 ※
이 케이스는 흔한 케이스는 아니지만, 필자는 연구 목적으로 Raspberry Pi의 Mass Storage Class를 Boot Device로 사용해 본 경험이 있다. 만약 Raspberry Pi Zero W를 부팅 디스크로 사용한다고 하면, 호스트(host) PC의 UEFI 단계에서 Raspberry Pi를 부팅 디스크로 잡아 OS를 부팅하게 된다. 다만 이때 Multi Composite Gadget을 이용하면 OS 부팅 단계에서 오류가 발생할 수 있다.
구체적으로 필자는 g_mass_storage를 올릴 때만 동작하고, Multi Composite Gadget을 사용했을 때 Booting이 안 되는 문제가 발생했었다. g_mass_storage 모듈을 이용했을 때 USB 장치의 기본 Vendor ID는 0x0525, Product ID는 0xA4A5이다. 반면에 Composite Gadget은 일반적으로 Vendor ID 0x1D6B, Product ID 0x0104를 사용한다.
100% 확신하는 것은 아니지만, OS 부팅 과정에서 Well-known이 아닌 Identifier를 사용하면 OS를 올리는 과정에서 USB Reconnection이 이루어지며, 이 과정에서 Mass Storage와의 연결이 일시적으로 해제되어 부팅이 안 되었던 것으로 보인다. 그래서 기존의 Raspberry Pi의 config 파일에서 Vendor ID와 Product ID를 g_mass_storage와 동일하게 맞추어 실행했더니 문제가 해결된 적이 있었다. 이렇게 해도 Multi Composite Gadget의 기능을 가지며 정상 동작하는 것을 확인했다.
'기타' 카테고리의 다른 글
카카오톡 오픈채팅 단체 카톡방 만드는 방법 (0) | 2020.11.09 |
---|---|
Linux HID Gadget Driver를 이용해 나만의 커스텀 HID 장치 만들기 (0) | 2020.10.27 |
Raspberry Pi Zero W 소개, 구매 방법, 환경 설정 방법 총 정리 (+ Wi-Fi 연결까지) (1) | 2020.10.23 |
USB 케이블 잘라서 데이터(Data-only) 케이블로 만들기 (2) | 2020.10.19 |
Python에서 args 직접 기입하는 대신에 간단히 SimpleNamespace 라이브러리 사용하기 (0) | 2020.10.15 |