Arduino

[아두이노] 시리얼 통신

기하 2021. 12. 4. 12:30

Serial 통신은 아두이노 보드와 컴퓨터가 서로 데이터를 주고받는 기본적인 통신에 해당한다.

쉽게 아두이노 스케치에서 코드를 넣을 때 사용되는 통신인 것이다.

 

Serial 통신에 대한 간단한 개념을 소개하자면 그 이름 그대로라고 말할 수 있다.

Serial 통신은 이름대로 직렬통신에 해당한다.

통신 방법에는 직렬 통신과 병렬(Parallel) 통신이 있는데 이 중 직렬 통신에 해당하는 것이다.

 

그럼 직렬 통신과 병렬 통신이 무엇인지 알아보자.

그림 9-1 직렬통신과 병렬통신

그림 9-1을 보면 직렬통신과 병렬통신을 간단히 이해할 수 있다.

 

모두 알다시피 디지털 신호는 0과 1로 구성되어 있다.

그리고 통신이라는 것은 이 0과 1로 이루어진 신호를 전달하는 것이다.

그럼 1101101101이라는 신호를 전달해보자.

어떤 방법이 있겠는가.

 

하나씩 천천히 상대에게 전달하는 것도 하나의 방법일 것이고

아니면 한꺼번에 전부 전달해 버리는 것도 하나의 방법이 될 것이다.

 

그림 9-1의 위쪽 그림과 같이 하나씩 연속으로 보내는 통신을 직렬통신이라고 하고

그림 9-1의 아래 그림처럼 한 번에 보내는 것을 병렬통신이라고 한다.

 

좀 더 아두이노에 맞게 설명하자면

한 번에 1비트씩 송수신하는 것을 직렬통신,

한 번에 8비트씩 송수신하는 것을 병렬 통신이라 말한다.

 

그러면 이런 생각을 가질 수 있다.

그럼 한 번에 하나씩 전달하는 것 보다 여러 개를 전달하는 것이 빠르고 좋지 않을까?

왜 굳이 한 번에 하나씩 보내야 하지? 라는 의문을 가질 수 있다.

앞의 설명과 같이 보면 분명 병렬통신이 직렬통신보다 효율이 좋아 보인다.

 

이 말은 어느 정도 맞는 말이긴 하지만 절대적인 것은 아니다.

둘을 비교해보면 병렬 통신은 속도가 빠르지만 통신거리가 비교적 짧으며 가격이 비싸다.

하지만 직렬 통신은 병렬 통신보다 느리지만 통신거리가 훨씬 길고 가격이 저렴하다.

그렇기 때문에 어느 것이 좋다 나쁘다를 말할 수 없는 것이다.

 

그럼 여기서 우리가 해야 할 직렬통신, Serial 통신에 대하여 좀 더 알아보자.

 

Serial 통신은 하드웨어적으로

USART(Universal Synchronous Asynchronous Receiver Transmitter) 또는 UART로 알려진 방식이며

표준 통신 방식인 RS-232C 프로토콜에 정의된 내용 중에서

데이터 송수신을 위한 세 개의 핀,

RXD(Receive data 또는 RX), TXD(Transmit data 또는 TX), GND(Ground) 핀만을

사용하는 통신을 말한다.

 

이 Serial 통신이 처음 말했듯이 아두이노에서는 코드를 주고받는 기본적인 통신에 해당한다.

그렇기 때문에 아두이노 보드에는 Serial이라는 이름의 시리얼 포트가 포함되어 있으며

아두이노 우노의 경우에는 하나의 포트가 아두이노 메가의 경우에는 네 개의 포트가 존재한다.

 

Serial 포트는 아두이노에는 공통적으로 디지털 0번(RX)고 디지털 1번(TX)으로 지정되어 있으며

그렇기 때문에 이 두 핀은 따로 디지털 입출력으로 사용하지 않는다.

 

시리얼 포트가 많은 아두이노 메가의 경우에는 ]

컴퓨터와 코드를 주고받는 메인 포트가 디지털 핀 0번, 1번 즉, 시리얼 포트 0번이고

나머지 3개의 시리얼 포트들은 코드 상에서 설정을 통해 사용할 수 있다.

(http://blog.naver.com/darknisia/220569815020)

 
좀 더 구체적으로 말하자면
아두이노 프로그램에서 코드를 컴파일하고 이를 USB를 통해 아두이노 보드로 업로드 하는데
컴퓨터의 가상 Serial 포트와 아두이노의 Serial 포트를 통해 시리얼 통신이 이루어진다.

또한 이때 이루어지는 Serial 통신인 USART 통신에서 중요한 것은 전송속도다.

컴퓨터와 아두이노의 통신 속도가 동일해야지 통신이 이루어지는 것이다.

통신의 전송속도는 일반적으로 보율(baud rate)을 사용하는데

이 보율은 변조속도를 나타내는 단위로 프랑스의 과학자 Baudot를 기념하여

데이터 전송속도를 표현하기 위해 사용되었다.

 

데이터 통신 초기에는 1초 동안 전달되는 데이터 비트의 개수를 나타내는 단위로 사용되었는데

이 경우에는 BPS(Bit Per Second)와 동일하다.

하지만 최근에는 통신 기술의 발달로 신호가 한 번 변할 때 한 비트 이상의 정보를 표현할 수 있게 되어

bps 단위는 보율보다 큰 값을 가지게 되었다.

 

다시 아두이노의 시리얼 통신으로 돌아오면

아두이노 프로그램 상에는 시리얼 통신으로 가장 좋은 기능이 있다.

그것이 시리얼 모니터다.

 

그림 9-2 시리얼 모니터

 

그림 9-2가 아두이노 프로그램의 시리얼 모니터다.

우측 상단에 있으며 그림 9-2에서 붉은 네모 상자가 있는 곳이다.

시리얼 모니터를 누르면 새로운 창이 나타나게 된다.

이 창에서 통신을 주고받는 것이다.

그림 9-2에서는 따로 코드를 넣은 것이 없기 때문에 아무것도 나타나 있지 않았다.

 

그럼 왜 이 시리얼 모니터 기능이 좋은 것일까.

그것은 다른 펌웨어 코딩 프로그램들에서는 지원하지 않는 기능이기 때문이다.

그렇기 때문에 다른 프로그램에서는

따로 무료나 유료로 통신 터미널 프로그램을 받아서 통신을 확인해야 했었다.

 

하지만 아두이노 프로그램은 프로그램 자체에 내장이 되어있고

또한 아두이노와 연결된 포트를 자동으로 잡아 동작하기 때문에 편리하면서 간편하다고 할 수 있다.

그럼 이번에는 Serial 통신을 사용하는 함수에 대하여 알아보자.

 

Serial은 시리얼 포트를 이용한 통신을 구현한 클래스로 실제 클래스 이름은 ‘Serial_’다.

Serial_ 클래스의 객체로 Serial이 정의되어 있으며 프로그램에서는 전역 객체인 Seiral을 이용한다.

 

만약 C/C++에 좀 더 관심이 있거나 어느 정도 아는 사람은

USBAPI.h나 Stream 클래스를 보는 것도 좋다. 그럼 Serial 통신의 함수를 살펴보자.

 

Serial. begin(baud, config)

시리얼 통신을 위한 전송 속도와 옵션을 설정한다.

baud가 전송속도에 해당하고

config은 데이터 비트 수, 패리티(parity), 정지 비트 설정에 해당한다.

config은 대개 따로 설정하지 않고 기본 지정되어 있는 디폴트 값을 사용한다.

디폴트 값은 SERIAL_8N1로 8N1에서

8이 데이터 비트를 나타내고 N은 패리티 비트 1은 정지 비트를 나타낸다.

데이터 비트는  5, 6, 7, 8의 4개의 값을 설정할 수 있으며

패리티 비트는 없을 때는 N, 짝수 패리티는 E, 홀수 패리티는 O로 설정한다.

마지막으로 정지 비트는 1과 2로 설정할 수 있다.

 

하지만 앞서 말했듯이 대부분의 통신에서 이 부분을 따로 설정하지 않고 디폴트 값을 사용한다.

그럼 다시 Serial.begin 함수로 돌아와서 baud를 보자.

baud는 앞서 말했듯이 통신 속도인 보율을 말한다.

아두이노에서 통신 보율은 시리얼 모니터를 통해 확인 할 수 있는데

그림 9-2에서 시리얼 모니터의 좌측 아래 보면 숫자와 함께 baud가 적혀있는 곳이 보일 것이다.

이곳이 통신을 할 때의 보율을 보는 것으로 저곳을 누르면

300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200의 보율이 나타난다.

그렇기 때문에 우리도 통신을 설정할 때 이 보율들로 설정을 해야 하는 것이다.

 

Serial.available()

시리얼 포트에 수신되어 저장되어 있는 데이터의 바이트 수를 반환하는 함수다.

 

시리얼 포트로 수신된 데이터는 64바이트까지 수신 버퍼에 저장되는데 여기에 저장되어 있는 바이트수를 반환하는 것이다.

 

 일반적으로 시리얼 통신이 작동하는지 하지 않는지 확인하는 경우로 사용된다.

 

Serial.print(value, format)

 

value를 시리얼 포트로 출력한다.

 

format은 정수의 경우 출력 형식인 진법을 선택할 수 있게 해주며 실수인 경우에는 소수점 이하 자릿수를 지정할 수 있게 해준다.

 

만약 format을 지정하지 않고 실수를 사용한다면 소수점 둘째자리까지 출력되게 설정되어 있다.

 

여기서 value는 여러 형식의 값을 출력할 수 있다.

 

char, char 배열, Stirng, 정수, 실수 등의 값이 대표적이다.

 

Serial.println(value, format)

 

Serial.pinrt 함수와 동일하며 다른 점은 문자열 출력 후 한 칸 밑으로 내려간다는 것이다.

 

쉽게 말하면 글자를 쓰고 엔터키를 누른 것과 같은 기능이고 좀 더 전문적으로 말하면 문자열 출력이후 개행 문자를 추가로 출력했다고 할 수 있다.

 

Serial.write(char), Serial.write(str), Serial.write(buffer, size)

 

이진 데이터를 시리얼 포트로 출력하고 출력된 바이트 수를 반환한다.

 

이진 형식으로 출력이 이루어지기 때문에 숫자의 경우는 Seiral.print나 Serial.println 함수를 사용하여야 한다.

 

간단히 설명하자면 65라는 숫자가 있으면 Serial.write 함수는 한 바이트만 전송하기 때문에 시리얼 모니터에서는 65를 아스키코드로 보고 ‘A’라는 문자를 출력한다.

 

하지만 Serial.print는 65라는 숫자를 전송할 때 6에 맞는 아스키코드를 보내고 이어서 5에 맞는 아스키코드를 보내 숫자 65를 표현한다.

 

요약하자면 Serial.write는 한 바이트만 전송하고 Seiral.print는 두 바이트를 전송하는 차이가 있다.

 

Serial.read()

 

시리얼 통신 수신 버퍼에서 첫 번째 문자를 읽어 반환한다.

 

수신 버퍼가 비어 있으면 –1을 반환한다.

 

또한 수신 버퍼에서 반환한 문자는 제거된다.

 

쉽게 사용되는 용도를 말하면 시리얼 통신을 할 때 시리얼 모니터에서 문자를 적어 보낼 수 있는데 이때 이 보낸 문자를 읽는 역할을 한다.

Serial.readbytes(buffer, length)

 

시리얼 통신 수신 버퍼의 문자를 읽어 버퍼에 저장한다.

 

length에 지정한 바이트 수의 문자를 읽었거나 시간 초과가 발생하면 종료하며 입력받은 문자의 수를 반환한다.

 

Serial.readBytesUntil(terminator, buffer, length)

 

시리얼 통신 수신 버퍼의 문자를 읽어 버퍼에 저장한다.

 

length에 지정한 바이트 수의 문자를 읽었거나 시간 초과가 발생한 경우 그리고 종료 문자가 발견된 경우 종료하며, 입력받은 문자의 수를 반환한다.

 

terminator가 종료 문자에 해당한다.

 

Serial.setTimeout(timeout)

 

Serial.readByte 함수나 Serial.readByteUntil 함수를 통해 데이터를 읽어 들일 때의 대기 시간을 밀리초 단위로 설정한다.

 

대기 시간을 초과하면 시간초과가 발생하여 입력함수는 종료된다.

 

이 함수를 사용하지 않았을 때 디폴트 값은 1초이다.

 

Serial.end()

 

시리얼 통신을 종료한다.

 

시리얼 통신이 종료되면 시리얼 통신을 위해 사용되었던 RX 핀과 TX 핀을 일반 입출력 핀으로 사용가능하다.

 

이후 다시 시리얼 통신을 사용하기 위해서는 Serial.begin 함수가 필요하다.

 

Serial.find(target)

 

시리얼 통신 수신 버퍼에서 주어진 문자열인 target이 발견될 때까지 데이터를 읽는다.

 

주어진 문자열이 발견되면 true를 반환하고 검색 시간이 초과되면 false를 반환한다.

 

검색시간은 1초로 설정되어 있다.

Serial.findUntil(target, terminator)

 

시리얼 통신 수신 버퍼에서 주어진 문자열 target이나 종료 문자열 terminator를 발견하면 true를 반환하고 검색시간이 초과되면 false를 반환한다.

 

검색시간은 마찬가지로 1초다.

 

Serial.flush()

 

시리얼 통신 송신 버퍼에 있는 데이터가 전송 완료할 때까지 대기한다.

 

Serial.parseFloat()

 

시리얼 통신 수신 버퍼에서 첫 번째 유효한 실수를 반환한다.

 

 현재 수신 버퍼가 비어있거나 실수가 발견되지 않으면 0을 반환한다.

 

Serial.peek()

시리얼 통신 수신 버퍼의 첫 번째 바이트 데이터를 반환한다.

단, 데이터를 수신 버퍼에서 제거하지 않으므로 여러 번 peek 함수를 호출하면 버퍼 내 첫 번째에 위치한 동일한 문자가 계속적으로 반환된다.

Serial.read가 반환된 문자를 제거하는 것과는 반대로 제거하지 않으므로 같은 문자가 계속 반환되는 것이다.

또한 수신 버퍼가 비어 있다면 –1을 반환한다.

 

여기까지가 Serial 통신을 이용할 때 사용되는 함수들이다.

모두 Serial 클래스의 맴버 함수에 속한다.

그럼 이제 이 함수들을 가지고 본격적으로 시리얼 통신을 사용해 보자.

void setup(){
    Serial.begin(9600);                          // 통신 속도 9600 baud 설정
    Serial.println("Hello World!");              // 문자열 출력
}  
void loop(){}

그림 9-3 코드 1 실행

코드 1을 업로드 시켜보면 그림 9-3과 같은 모습이 나타날 것이다.

간단하게 ‘Hello Wrold!’를 출력하는 코드였다.

간단하면서 Serial 통신을 잘 이해할 수 있는 코드라고 할 수 있다.

 

그럼 다시 그림 9-3을 보자.

그림 9-3을 보면 붉은 네모 상자가 두 개가 있을 것이다.

하나는 코드 1에 있는 Serial.begin(9600)이고 나머지 하나는 시리얼 모니터의 우측 하단의 탭 상자다.

여기서 특징은 두 개 모두 9600으로 설정되어 있다는 것이다.

이것은 앞에서도 잠깐 언급했지만 통신 속도를 말하고 두 가지 모두 일치되었을 때 통신이 되는 것이다.

 

Serial.begin으로 설정하는 통신 속도는 아두이노에 해당하는 것이고

시리얼 모니터에서 설정하는 통신 속도는 컴퓨터에 해당하는 통신 속도다.

 

만약 Serial.begin으로 9600을 설정하고 시리얼 모니터에서 14400과 같이 다른 보율을 설정한다면

시리얼 모니터에는 ‘Hello World!’가 아닌 깨진 글자가 출력되거나 아무것도 출력되지 않을 것이다.

 

그렇기 때문에 시리얼 모니터를 켤 때는 항상 이 점을 유의하면서 사용하자.

그리고 이것은 여기에만 국한되는 것이 아니다.

 

일반적으로 사용되는 센서 중에서도 이렇게 통신을 요구하는 센서가 있는데

이때는 센서들의 데이터 시트를 보고 그곳에 설정되어 있는 통신 속도를 보고

그에 맞춰 Serial.begin을 설정하여야 한다.

 

그럼 다음으로 코드 2를 업로드 시켜보자.

 

void setup()
{
    Serial.begin(9600);                           // 통신 속도 9600 baud 설정
}  

void loop()
{
    Serial.println("Original number : 77");        // 숫자 77의 2, 8, 10, 16진수 표현
    Serial.print("DEC: ");
    Serial.println(77, DEC);
    Serial.print("BIN: ");
    Serial.println(77, BIN);
    Serial.print("OCT: ");
    Serial.println(77, OCT);
    Serial.print("HEX: ");
    Serial.println(77, HEX);
    Serial.println("Original number : 1.23456");   // 실수 1.23456의 소수점 3자리 표현
    Serial.print("Changing number : ");
    Serial.print(1.23456, 3);
    while(1);

}

코드 2를 업로드하면 처음에 숫자 77이 나오고

그 밑으로 각 진법에 맞는 숫자들이 표현될 것이다.

 

이와 같이 Serial.print는 10진수(DEC)뿐만 아니라

2진수(BIN), 8진수(OCT), 16진수(HEX)의 표현까지 가능하다.

 

그리고 실수인 1.23456의 소수점 3자리까지 표현을 해보았다.

원래라면 1.234가 출력되어야 하지만 여기서는 4자리에서 반올림이 되어 출력은 1.235가 출력될 것이다.

 

char receive_char = 0; // 수신 데이터

void setup()
{
    Serial.begin(9600); // 통신 속도 9600 baud 설정
}

void loop()
{
    if(Serial.available() > 0)
    {                                // 수신 데이터 대기
       receive_char = Serial.read(); // 버퍼의 첫 번째 데이터 읽기
       Serial.print("I receive: ");
       Serial.println(receive_char); // 읽은 문자 출력
    }
}

코드 3을 업로드 하고 시리얼 모니터를 켜보면 아무것도 나오지 않을 것에 당황했을 것이다.

이 코드는 시리얼 모니터를 통해 아두이노로 문자를 보내고 아두이노에서 그것을 다시 보내주는 코드다.

그럼 시리얼 모니터 창에 abc라고 적어보자.

그러면 답이 올 것이다.

답은 abc 모두 오는 것이 아니라 우리가 보낸 abc에서 먼저 수신된 순서대로 a, b, c로 끊어서 오게 된다.

 

그럼 코드를 살펴보자.

코드를 보면 이전과 다른 새롭게 Serial.available 함수와 Serial.read 함수가 사용되었다.

먼저 available 함수는 간단히 시리얼 모니터에서 뭔가 입력이 있는지를 살펴보는 함수라고 생각하면 된다.

앞서 말했듯이 코드 3은 시리얼 모니터로 문자를 입력하면 그것을 다시 아두이노에서 출력해주는 코드이다.

 

그렇기 때문에 available 함수로 입력이 있는지를 살펴보는 것이다.

그리고 입력이 있다면 read 함수로 읽어 표현한다.

 

하지만 read 함수는 첫 번째 문자만을 읽어 반환하기 때문에

시리얼 모니터에서 ‘abc’를 연속으로 입력하여도 돌아올 때는 하나씩 돌아오는 것이다.

 

그림 9-4 코드 3 실행

코드 1에서 3까지를 해보며 이제 시리얼 통신에 어느 정도 감이 잡혔을 것이다.

 

그럼 이번엔 응용을 해보자.

코드 4는 시리얼 통신을 통해 ‘1’이 입력되면 LED가 하나 켜지고 ‘2’가 입력되면 LED가 두 개가 켜지게 하였다.

전체적인 코드는 코드 3과 LED가 켜지는 코드가 합쳐져 있는 난이도라

지금까지 해 온 독자라면 쉽게 이해 할 수 있을 것이다.

char receive_char = 0; // 수신 데이터
int LED1 = 6;          // 첫 번째 LED
int LED2 = 7;          // 두 번째 LED  

void setup()
{
    pinMode(LED1, OUTPUT);                  // LED 출력으로 설정
    pinMode(LED2, OUTPUT);
    Serial.begin(9600);                     // 통신 속도 9600 baud 설정
}  

void loop()
{
    if(Serial.available() > 0)
    {                      // 수신 데이터 대기
       receive_char = Serial.read();                // 버퍼의 첫 번째 데이터 읽기
    }

    if(receive_char == '1')
    {                                               // 읽은 데이터가 ‘1’일 때 
       digitalWrite(LED1, HIGH);                    // 1번 LED 출력
       receive_char = 0;                            // 수신 데이터 초기화
       delay(1000);
    }

    else if(receive_char == '2')
    {                                               // 읽은 데이터가 ‘2’일 때
       digitalWrite(LED2, HIGH);                    // 2번 LED 출력 
       receive_char = 0;
       delay(1000);
    } 
    else
    {                                              // 그 외의 경우 LED 모두 끄기
       digitalWrite(LED1, LOW);
       digitalWrite(LED2, LOW);
    }
}