ENC28J60 이더넷 모듈을 이용하여 아두이노를 웹 서버로 활용하기::인터넷 건반 만들어 소리 재생하기
이번 포스팅에서는 ENC28J60 를 이용하여 아두이노를 웹서버로 만들어 보겠다.
아두이노 모델중에 가장 많이 사용되는 UNO 에 탑재된 ATmega328의 2kbyte 라는 개미 코딱지 같은 메모리를 활용하여 그럴싸한 html 문서를 보여주는 웹 서버를 만드는 것은 쉽지 않은 일이다. 응용 소프트웨어 서버로 사용하기에는 조악한 성능을 갖고 있다. 그 때문에, 아두이노를 웹 서버로 사용하게 된다면 센서를 통하여 데이터를 수집하고 REST로 값을 전달해 주는 기능이나 물리적 장치 제어를 위한 컨트롤러 이상으로 확장하기는 어려울 것이다 . 하지만 이번 포스팅에서는 아두이노를 웹서버로 만들어 재미있는 것들을 만들어보고자 한다. (조만간 안 귀찮을 때 WIFI 사용도 같이 다뤄보고자 한다. )
1. 이더넷 모듈 구하기.
아두이노 UNO R3 기준으로 설명하겠다. 첫 번째로 점프 케이블을 구하고 두 번째로 가장 중요한 ENC28J60이 탑재된 이더넷 모듈을 구해야 한다. 사실 아두이노 공식 이더넷 쉴드는 W5100 라는 칩을 사용하지만 ENC28J60 에 비하여 가격이 비싸다. 당장 국내 온라인 쇼핑몰만 찾아봐도 ENC28J60 이더넷 모듈은 1만 3천원 안쪽으로 구입할 수 있지만 W5100 이더넷 모듈은 이 것보다 배 이상 비싸다. 성능 차이는 있을지 모르겠지만, 초 극 저사양 마이크로 컨트롤러 에서는 ENC28J60만 되어도 충분하지 않을까...;;;
알리익스프레스에서 enc28j60 으로 검색하면 정말로 저 가격에 살 수 있다. 참고로 본인은 할인 행사 덕분에 2달러에 구입했다. ㅡ , ㅡd 대신 도착하는데 보름 이상 걸린다. 국내 유명 전자 부품 쇼핑몰 엘xx츠 에서도 1만 3천원 안쪽으로 구입할 수 있으며 재고도 있는듯.
2. 연결하기
SPI 통신을 사용하기 때문에 점퍼 케이블을 이용하여 다음과 같이 연결해야 한다.
이더넷 모듈 핀 |
아두이노 핀 |
전원 |
5v or 3.3v (모듈에 따라서) |
GND |
GND |
SCK |
13 |
SO |
12 |
SI |
11 |
CS |
8 |
(CS 핀은 아무곳에나 연결해도 상관 없다. 코드상으로 이더넷을 초기화 하는 과정에서 CS 핀을 변경할 수 있기 때문이다.)
3. 라이브러리 구하기, 예제 돌려보기
이 곳 (https://github.com/jcw/ethercard) 에 접속하여 라이브러리를 다운로드 받고 압축을 풀어 아두이노 프로젝트 폴더 내의 라이브러리 폴더에 폴더채로 이동시킨다. 폴더명은 적절하게 수정하자. 또는 아두이노 IDE 의 스케치 -> 라이브러리 가져오기 메뉴를 활용하는 방법도 있다. (시간이 흘러 이 포스팅에서 언급하는 이더넷 라이브러리 버전과 최신 버전의 API 가 일치하지 않을 수 있으므로 포스팅 작성 시점에서 fork 하였다. fork된 git 프로젝트의 링크 : https://github.com/ice3x2/ethercard)
아두이노 IDE 를 실행하면 ethercard 라이브러리가 추가된 것을 확인할 수 있다. 이 라이브러리에 포함된 예제를 살펴보면 재미있고 유용한 예제들이 많다. 그 중에 rbbb_server 라는 예제를 살펴보겠다.
rbbb_server 를 선택하면 아주 짧고 간단한 코드가 등장하는데, 사용하기 편하도록 몇 가지만 수정해보겠다.
setup() 함수를 살펴보면 아래와 같이 구현이 되어있다.
void setup () {
if (ether.begin(sizeof Ethernet::buffer, mymac) == 0)
Serial.println(F("Failed to access Ethernet controller"));
ether.staticSetup(myip);
}
간단히 설명하자면, 이더넷을 버퍼와 mac 어드레스를 이용하여 초기화하고, 고정된 아이피 주소를 할당하도록 하는 코드이다. 하지만 이 코드는 불편하다. 이유는, 공유기의 설정에 들어가서 mac 주소 찾아서 할당해줘야 하기 때문이다. 요즘은 대부분 누구나 이더넷 케이블 연결하기만 하면 동적으로 주소 할당해주는 DHCP 기능이 지원되는 공유기를 갖고 있다. 그러므로 DHCP 기능을 사용하여 ip 주소를 자동으로 받아 오도록 하는 것이 편할 것이다.
DHCP를 이용하기 위하여 아래와 같이 수정해준다.
#define CS_PIN 8
#define HOST_NAME "arduino"
// ... 중략
void setup () {
Serial.begin(57600);
if (ether.begin(sizeof Ethernet::buffer, mymac,CS_PIN) == 0)
Serial.println(F("Failed to access Ethernet controller"));
while(!ether.dhcpSetup(HOST_NAME, true));
Serial.println("Connected");
ether.printIp("IP: ", ether.myip);
ether.printIp("Netmask: ", ether.netmask);
ether.printIp("Gateway IP: ", ether.gwip);
ether.printIp("DNS IP: ", ether.dnsip);
}
변경된 코드에서는 CS_PIN 번호를 추가하였다. ethercard 라이브러리 내에서 CS PIN 은 기본으로 8번으로 되어있지만, ether.begin 함수의 마지막 인자값으로 임의의 CS pin 번호를 넣어줄 수 있다.
두 번째로 정적으로 ip 를 할당하는 staticSetup 함수를 지우고 dhcp 를 통하여 ip 를 할당받을 수 있는 dhcpSetup 함수를 넣었는데, 제대로 설정될 때 까지 무한 반복되도록 하였다.
이제 코드를 아두이노에 업데이트 하고 인터넷 케이블을 연결한 다음에 도구->시리얼 모니터를 열어서 baud rate 를 57600 으로 설정하면 다음과 같은 메세지를 확인할 수 있다.
ip 주소가 192.168.0.49 로 자동 할당되었다.
공유기 설정 화면에서도 지정한 host name 으로 하여 ip 주소가 자동을 할당된 것을 확인할 수 있다.
이 주소로 접속하면 위 스샷 이미지와 같이 아두이노의 구동 시간을 보여주며 연속으로 refresh 하는 것을 볼 수 있다.
3. 인터넷 웹 브라우저로 연주하는 건반
이 예제를 이용하여 좀 더 재미있는 것을 만들어 보겠다.
원래는 브라우저로 LED 밝기를 조절하는 것을 만들어 보려고 했지만, 너무 식상해서간단한 건반으로 바꾸었다.
아래는 동작 영상이다. (동영상의 볼륨을 올려주세요.)
첫 번째로 준비물을 챙긴다.
우선 ENC28J60 모듈과 스피커를 준비한다.
스피커는 아무 것이나 상관 없는데, 극성만 잘 구분해서 + 단자를 아두이노의 핀 6번에 연결하면 된다.
두 번째로 아래와 같은 화면이 나오게끔 html 을 코딩한다.
위 스샷에 나오는 색이 들어있는 상자 하나는 각 음계를 재생하는 버튼이다. 예를들어 빨간색은 C4 주황색은 D4, 노란색은 E4 ... 보라색은 B4 를 나타낸다.
각각의 상자를 눌러서 각 음계로 해당하는 링크로 이동할 수 있도록 만들어야 한다.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<style type="text/css">
div { width: 14.28%;height: 100%; display: inline-block;}
body {width: 100%;height: 100%;margin: 0;position: absolute; overflow: hidden;font-size: 0px;
letter-spacing: 0px;
word-spacing: 0px;}
</style>
</head>
<body>
<a href="/0"><div style="background: red"></div></a>
<a href="/1"><div style="background: orange"></div></a>
<a href="/2"><div style="background: yellow"></div></a>
<a href="/3"><div style="background: green"></div></a>
<a href="/4"><div style="background: blue"></div></a>
<a href="/5"><div style="background: midnightblue"></div></a>
<a href="/6"><div style="background: darkviolet"></div></a>
</div>
</body>
</html>
각 음계에 해당하는 상자에는 /0 부터 /6 까지 링크가 걸려있으며 이 버튼을 누를 경우 해당 페이지로 이동하게 된다.
이제 이 html 코드를 아두이노로 옮겨야 한다. 그냥 옮기면 사이즈도 크고 복잡하므로 https://kangax.github.io/html-minifier/ 를 이용하여 코드를 한 줄로 만들어 버린다. 아마 다음과 같이 나올 것이다.
<!DOCTYPE html><html lang=en><head><meta charset=UTF-8><meta name=viewport content='width=device-width,user-scalable=no'><style type=text/css>div{width:14.28%;height:100%;display:inline-block}body{width:100%;height:100%;margin:0;position:absolute;overflow:hidden;font-size:0;letter-spacing:0;word-spacing:0}</style><body><a href=/0><div style=background:red></div></a> <a href=/1><div style=background:orange></div></a> <a href=/2><div style=background:#ff0></div></a> <a href=/3><div style=background:green></div></a> <a href=/4><div style=background:#00f></div></a> <a href=/5><div style=background:#191970></div></a> <a href=/6><div style=background:#9400d3></div></a>
주의해야 할 사항은 큰 따옴표를 작은 따음표로 바꿔줘야 한다.
이제 아두이노에 들어갈 코드를 살펴보자.
#include <EtherCard.h> #define CS_PIN 8 #define SPEAKER_PIN 6 #define HOST_NAME "keyboard" // 각 음계에 해당하는 값. CDEFGAB -> 도레미파솔라시 // 이 값은 아두이노 IDE의 파일->예제->Digital->toneKeyboard 에서 확인 가능. #define NOTE_C4 262 #define NOTE_D4 294 #define NOTE_E4 330 #define NOTE_F4 349 #define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_B4 494 unsigned int note[] = {NOTE_C4, NOTE_D4,NOTE_E4, NOTE_F4, NOTE_G4,NOTE_A4, NOTE_B4}; // MAC 어드레스 static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 }; byte Ethernet::buffer[860]; BufferFiller bfill; void setup () { Serial.begin(57600); if (ether.begin(sizeof Ethernet::buffer, mymac,CS_PIN) == 0) Serial.println(F("Failed to access Ethernet controller")); // HDCP 가 설정될 때 까지 대기한다. while(!ether.dhcpSetup(HOST_NAME, true)); Serial.println("Connected"); ether.printIp("IP: ", ether.myip); ether.printIp("Netmask: ", ether.netmask); ether.printIp("Gateway IP: ", ether.gwip); ether.printIp("DNS IP: ", ether.dnsip); } // HTML 을 버퍼에 기록한다. static word writeHtml() { bfill = ether.tcpOffset(); // https://kangax.github.io/html-minifier/ 를 통하여 압축한 html 코드 bfill.emit_p(PSTR("<!DOCTYPE html><html lang=en><head><meta charset=UTF-8><meta name=viewport content='width=device-width,user-scalable=no'><style type=text/css>div{width:14.28%;height:100%;display:inline-block}body{width:100%;height:100%;margin:0;position:absolute;overflow:hidden;font-size:0;letter-spacing:0;word-spacing:0}</style><body><a href=/0><div style=background:red></div></a> <a href=/1><div style=background:orange></div></a> <a href=/2><div style=background:#ff0></div></a> <a href=/3><div style=background:green></div></a> <a href=/4><div style=background:#00f></div></a> <a href=/5><div style=background:#191970></div></a> <a href=/6><div style=background:#9400d3></div></a>")); return bfill.position(); } word writeFail() { bfill = ether.tcpOffset(); bfill.emit_p(PSTR("HTTP/1.0 401 Unauthorized\r\nContent-Type: text/html\r\n\r\n<h1>401 Unauthorized</h1>")); return bfill.position(); } void loop () { // 패킷이 들어올 때 까지 루프. word pos = ether.packetLoop(ether.packetReceive()); if(pos) { ether.httpServerReplyAck(); // 데이터를 가져온다. char* data = (char *) Ethernet::buffer + pos; for(int i = 0; i < 7; ++i) { String str = "GET /" + String(i); str += ' '; Serial.println(str.c_str()); // 이 부분에 대하여 밑에서 설명하겠다. if(strncmp(str.c_str(), data, str.length()) == 0) { // 재생중인 소리 종료 noTone(SPEAKER_PIN); // 500ms 동안 소리를 재생한다. // 비동기식으로 동작한다. tone(SPEAKER_PIN, note[i],500); ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V); return; } } // 이 부분에 대하여 밑에서 설명하겠다. if(strncmp("GET / ", data, 6) == 0) { ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V); } // 정의된 path 값으로 접속하지 않았을 경우 에러 메세지 전송. else { ether.httpServerReply_with_flags(writeFail(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V); } } }
위 코드에서 중요한 부분이 있는데, 바로 path 값을 분류하는 것이다.
웹 브라우저를 통하여 http 프로토콜로 서버로 요청할 때 들어오는 데이터는 다음과 같다.
GET /0 HTTP/1.1 Host: 192.168.0.49 Connection: keep-alive Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Referer: http://192.168.0.49/0 Accept-Encoding: gzip, deflate, sdch Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
이 중에서 우리가 눈여겨 보아야 할 것은 첫 번째 줄이다.
첫 번째 줄의 첫 번째 블럭을 보면 GET 방식으로 접속 한다는 것을 알려주고 있고, 두 번째 블럭은 paht 값을 알려주고 있다.
이 path 값을 이용하여 통신할 수 있는 것이다.
HTTP 프로토콜 파싱 모듈을 만들거나 라이브러리를 이용한다면 완벽한 REST 서버도 구현할 수 있을 것이다.
어쨌든 이 번은 간단한 예제이므로, 이 것을 다음과 같이 처리하였다.
if(strncmp("GET / ", data, 6) == 0) { ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V); }
path 값을 검사하기 위해서 strncmp 를 이용하였는데, 첫 번째 인자는 비교할 문자열이고 두 번째 인자는 대상 문자열이다. 세 번째 인자는 비교할 길이다. (주로 첫 번째 인자로 들어간 문자열의 길이를 입력한다.) 즉, data 배열에 담긴 문자열의 시작 위치에서 6번째 까지 문자를 검사하고 이 것이 "GET / " 과 일치하는지 확인하는 과정이다. paht 값이 일치 한다면 ctrncmp 함수는 0을 반환한다.
ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
이 라인에서는 http 데이터를 반환하고 TCP ack 값을 날리고 연결을 닫아준다.(TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V)
확인결과 안타깝게도 소켓을 여려개 생성하고 커넥션을 관리하는 것이 힘들기 때문에 연결을 즉시 닫아주는 것이 좋을 것 같다.
만약 데이터를 여러번 나누어 보내고 싶을 때는 multipacket 예제를 살펴보자.
이제 주소값을 이용하여 소리를 재생하는 부분을 살펴보자.
for(int i = 0; i < 7; ++i) { String str = "GET /" + String(i); str += ' '; Serial.println(str.c_str()); // 이 부분에 대하여 밑에서 설명하겠다. if(strncmp(str.c_str(), data, str.length()) == 0) { // 재생중인 소리 종료 noTone(SPEAKER_PIN); // 500ms 동안 소리를 재생한다. // 비동기식으로 동작한다. tone(SPEAKER_PIN, note[i],500); ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V); return; } }
이 부분은 귀찮은 관계로 다소 비효율적으로 처리하였는데, 오히려 이해하기는 쉬울 것이다.
이 코드는 클라이언트(웹브라우저) 를 통하여 들어온 http 헤더 값의 첫 번째 줄을 총 7번 검사한다.
path 값들도 /0 /1 ... /5 /6 까지 총 7번 검사한다.
만약 path 값이 일치할 경우 현재 재생하고 있는 소리를 종료하고 해상 path 값에 해당하는 소리를 500ms 동안 재생한다.