오늘은 아두이노 출력 함수 중 마지막인 analogWrite 함수에 대해서 알아보도록 하겠습니다. analogWrite 함수는 이름 그대로 Analog 값을 출력할 수 있게 해 줍니다. 아두이노는 Digital 시스템인데 어떻게 Analog 출력을 낼 수 있는지 궁금하실 겁니다. 사실상 정확하게 말해서는 Analog 값을 선형적으로 매끄럽게 출력하는 것이 아닌 PWM 제어를 통해서 Analog 출력을 모사한다고 볼 수 있습니다. 그럼 본론으로 들어가기 전에 PWM 제어란 무엇인지 알아보도록 하겠습니다. 

 

1. PWM 제어

  PWM이란 Pulse-width modulation의 약자로 파형(Pulse)의 폭(Width)을 변조 혹은 조정(Modulation)을 한다는 의미입니다. 말 그대로 Digital로 출력되는 파형의 폭을 변조하면 0(GND)과 1(5V)이 아닌 Analog 값(0 ~ 5V)을 출력할 수 있습니다. 이렇게 말씀드리면 이해가 어려울 거 같으니 그림 보면서 더 자세하게 설명해드리도록 하겠습니다. 

  위 그림을 보시면 duty에 따라서 출력이 달라지는 것을 볼 수 있습니다. 여기서 duty란 일정 주기(Period) 내에서 HIGH로 올라와있는 비율, 혹은 시간을 의미합니다. 첫 번째 예시로 duty가 5%인 경우에는 1주기 내에서 5퍼센트의 시간만큼만 HIGH로 올라와 있다는 뜻입니다. 좀 더 디테일하게 보면, 1주기가 100 msec라고 가정했을 때, HIGH값을 유지하는 시간은 100 * 0.05로 5 msec만큼만 HIGH로 올라오겠죠? 이렇게 주기적으로 일정한 duty비만큼 HIGH로 인가하게 되었을 때, 5V(HIGH)의 값에 duty비만큼 곱해준 것과 동일한 출력을 볼 수 있습니다. 각 duty비 별로 설명드리면, 

Case 1. 5V 시스템 / Duty 5% / Period 100msec

  5V 시스템의 경우 HIGH의 값은 5V로 출력됩니다. 여기서 한 주기 100 msec동안 Duty비인 5%만큼만 HIGH로 출력을 하면, 이전에 계산했던 것처럼 5 msec동안만 HIGH를 출력하게 됩니다. 그러면 100 msec동안 평균 출력 전압을 계산해보면 (5V*5msec)/100msec = 0.25 V입니다. 즉, 이 동작이 계속 주기적으로 반복하게 되면 0.25V를 출력하는것과 동일하게 보인다는 이야기입니다. 

Case 2. 5V 시스템 / Duty 30% / Period 100msec

  위와 조건은 동일하고 Duty비가 30% 일 경우에 대해 생각해봅시다. 30 msec만큼 5V의 전압이 인가되었으니, 위와 동일한 방식으로 평균 출력 전압을 계산해보면 (5V*30 msec)/100 msec = 1.5V입니다. 그렇단 이야기는 Duty비를 30%로 두고 PWM을 출력하게 되면 1.5V를 모사할 수 있단 이야기겠네요.

위 두 Case를 통해 식 하나를 도출해낼 수 있습니다.

[ Microcontroller 출력 전압값 ] X [ Duty 비 ] = [ PWM 출력 전압 ]

  이를 통해서 우리는 PWM의 Duty비만 조정을 한다면, Microcontroller의

  PWM을 구현하기 위해서는 Timer/Counter라는 모듈을 이용해야 하는데, 우리는 아두이노 내장 함수를 활용할 것이기 때문에 이에 대한 설명은 다음으로 미루도록 하겠습니다.

다시 본론으로 돌아와서 위에서 설명한 PWM 제어를 아두이노에서 어떻게 사용하고 활용할 수 있을지 다뤄봅시다.

 

2. analogWrite()

  우리는 아두이노 스케치를 이용하면 복잡하게 구현해야 했던 PWM을 손쉽게 사용할 수 있습니다. 먼저 analogWrite로 출력을 내보낼 수 있는 Pin이 무엇인지 알아보아야 하는데, 그 답은 보드에 적혀있습니다. 

  위의 아두이노 우노 사진을 보면 Digital Pin 숫자 옆에 물결 표시가 있는 것을 볼 수 있습니다. 빨간색 동그라미로 표시되어 있는 것처럼 PWM으로 사용 가능한 Pin은 ~표시를 해두었다입니다. '~'표시가 되어있는 핀들 아무거나 골라서 이용하시면 됩니다. 

  - analogWrite(Pin number, Value): PWM 출력을 내보내고자 하는 Pin number를 입력 (PWM 출력 가능 여부를 확인) / Value는 0 ~ 255의 값이 들어가는데, 만약 50% Duty비를 가진 PWM을 출력하고자 할 때, 255 * 0.5 = 127.5 (127 or 128)을 입력

analogWrite 함수에 대해 좀 더 설명을 추가하면, value값에는 0 ~ 255 사이의 값을 넣을 수 있는데 그 이유는 해당 PWM을 만들어내는 Timer/Counter가 8-bit기 때문이다. 물론 아두이노 메가에는 16-bit Timer/Counter도 존재하지만, 해당 Timer/Counter도 8-bit에 맞춰서 함수를 사용하도록 설계된 것 같다. 해당 내용에 대해 자세히 알고 싶을 경우 "~\Arduino\hardware\arduino\avr\cores\arduino\wiring_analog.c"를 확인하시면 됩니다. 

void analogWrite(uint8_t pin, int val)
{
	// We need to make sure the PWM output is enabled for those pins
	// that support it, as we turn it off when digitally reading or
	// writing with them.  Also, make sure the pin is in output mode
	// for consistenty with Wiring, which doesn't require a pinMode
	// call for the analog output pins.
	pinMode(pin, OUTPUT);
	if (val == 0)
	{
		digitalWrite(pin, LOW);
	}
	else if (val == 255)
	{
		digitalWrite(pin, HIGH);
	}
	else
	{
		switch(digitalPinToTimer(pin))
		{
			// XXX fix needed for atmega8
			#if defined(TCCR0) && defined(COM00) && !defined(__AVR_ATmega8__)
			case TIMER0A:
				// connect pwm to pin on timer 0
				sbi(TCCR0, COM00);
				OCR0 = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR0A) && defined(COM0A1)
			case TIMER0A:
				// connect pwm to pin on timer 0, channel A
				sbi(TCCR0A, COM0A1);
				OCR0A = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR0A) && defined(COM0B1)
			case TIMER0B:
				// connect pwm to pin on timer 0, channel B
				sbi(TCCR0A, COM0B1);
				OCR0B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR1A) && defined(COM1A1)
			case TIMER1A:
				// connect pwm to pin on timer 1, channel A
				sbi(TCCR1A, COM1A1);
				OCR1A = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR1A) && defined(COM1B1)
			case TIMER1B:
				// connect pwm to pin on timer 1, channel B
				sbi(TCCR1A, COM1B1);
				OCR1B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR1A) && defined(COM1C1)
			case TIMER1C:
				// connect pwm to pin on timer 1, channel B
				sbi(TCCR1A, COM1C1);
				OCR1C = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR2) && defined(COM21)
			case TIMER2:
				// connect pwm to pin on timer 2
				sbi(TCCR2, COM21);
				OCR2 = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR2A) && defined(COM2A1)
			case TIMER2A:
				// connect pwm to pin on timer 2, channel A
				sbi(TCCR2A, COM2A1);
				OCR2A = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR2A) && defined(COM2B1)
			case TIMER2B:
				// connect pwm to pin on timer 2, channel B
				sbi(TCCR2A, COM2B1);
				OCR2B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR3A) && defined(COM3A1)
			case TIMER3A:
				// connect pwm to pin on timer 3, channel A
				sbi(TCCR3A, COM3A1);
				OCR3A = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR3A) && defined(COM3B1)
			case TIMER3B:
				// connect pwm to pin on timer 3, channel B
				sbi(TCCR3A, COM3B1);
				OCR3B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR3A) && defined(COM3C1)
			case TIMER3C:
				// connect pwm to pin on timer 3, channel C
				sbi(TCCR3A, COM3C1);
				OCR3C = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR4A)
			case TIMER4A:
				//connect pwm to pin on timer 4, channel A
				sbi(TCCR4A, COM4A1);
				#if defined(COM4A0)		// only used on 32U4
				cbi(TCCR4A, COM4A0);
				#endif
				OCR4A = val;	// set pwm duty
				break;
			#endif
			
			#if defined(TCCR4A) && defined(COM4B1)
			case TIMER4B:
				// connect pwm to pin on timer 4, channel B
				sbi(TCCR4A, COM4B1);
				OCR4B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR4A) && defined(COM4C1)
			case TIMER4C:
				// connect pwm to pin on timer 4, channel C
				sbi(TCCR4A, COM4C1);
				OCR4C = val; // set pwm duty
				break;
			#endif
				
			#if defined(TCCR4C) && defined(COM4D1)
			case TIMER4D:				
				// connect pwm to pin on timer 4, channel D
				sbi(TCCR4C, COM4D1);
				#if defined(COM4D0)		// only used on 32U4
				cbi(TCCR4C, COM4D0);
				#endif
				OCR4D = val;	// set pwm duty
				break;
			#endif

							
			#if defined(TCCR5A) && defined(COM5A1)
			case TIMER5A:
				// connect pwm to pin on timer 5, channel A
				sbi(TCCR5A, COM5A1);
				OCR5A = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR5A) && defined(COM5B1)
			case TIMER5B:
				// connect pwm to pin on timer 5, channel B
				sbi(TCCR5A, COM5B1);
				OCR5B = val; // set pwm duty
				break;
			#endif

			#if defined(TCCR5A) && defined(COM5C1)
			case TIMER5C:
				// connect pwm to pin on timer 5, channel C
				sbi(TCCR5A, COM5C1);
				OCR5C = val; // set pwm duty
				break;
			#endif

			case NOT_ON_TIMER:
			default:
				if (val < 128) {
					digitalWrite(pin, LOW);
				} else {
					digitalWrite(pin, HIGH);
				}
		}
	}
}

위의 함수에서 보이듯 입력받는 value는 정수형으로 전달이 되어야 하기 때문에 정확한 50% Duty비를 구현하지 못합니다. 그에 근사한 127이나 128을 입력하여 출력할 수 있습니다. 

 

3. Example

  PWM 제어는 대부분 모터를 제어하는데 많이 쓰입니다. 하지만 모터 제어에 대해서는 뒤에서 좀 더 자세하게 다루고 오늘은 LED를 이용해서 PWM 제어 실습을 해보도록 하겠습니다. LED에 PWM을 이용하여 Duty비를 조정했을 때, 출력 전압이 달라지기 때문에 밝기가 변하는 것을 볼 수 있습니다. 우선 코드 먼저 확인하시죠.

#define LEDPin 10

int value = 0;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:
  for (value = 0; value < 255; value += 25) {
    analogWrite(LEDPin, value);					    // value 값 만큼 PWM 출력
    delay(1000);									// 1초 지연
  }
  for (value = 255; value > 0; value -= 25) {
    analogWrite(LEDPin, value);						// value 값 만큼 PWM 출력
    delay(1000);									// 1초 지연
  }
}

  위의 코드의 구조는 익숙한 구조처럼 보일 것입니다. 그 이유는 이전 실습 때 Servo Motor를 제어할 때 비슷한 방식으로 구현을 했죠. 방식은 동일합니다. 모터 출력을 25 값 씩 증가시켜나갔다가 25씩 감소시키는 코드입니다. 여기서 25의 값이란 Timer/Counter에서의 25 값으로 duty비를 의미하는 것이 아닙니다. 여기서 각 값이 의미하는 duty비를 구하려면, Timer/Counter의 Max값인 255만큼 나누고 100을 곱해주면 어렵지 않게 구할 수 있습니다. 우리가 증가시켜주는 25의 값은 duty비로 환산했을 때, (25 / 255) * 100 = 9.8039 % 입니다. 즉, 25씩 증가시킨다는 것은 duty비를 약 9.8%씩 증가시키는 것으로 5V 시스템이라 가정하였을 때, 5 * 0.098039 = 0.490195 V 씩 증가합니다. 각 반복문이 돌면서 LED에 PWM으로 인가되는 전압이 달라지므로 밝기가 밝아졌다 어두워졌다 변화를 볼 수 있습니다. 

  회로는 위와 같이 구성해주시면 됩니다. 

 

다음 시간에는 모터의 구동 원리를 알아보고 PWM을 이용하여 모터를 구동해보는 시간을 갖도록 하겠습니다. 

 

강의: youtu.be/tVER6nWyjXM

 

< 오늘의 과제 >

1. 위의 소스코드 중 available()함수를 활용하여 내가 보낸 문자를 다시 출력해주는 코드에서 Serial.write()함수 대신에 print함수를 쓰면 어떤 문제가 발생하는지 확인하고, 이를 해결하기 위해서는 어떻게 수정해주어야 하는지 생각해보세요.

  우선 Serial.read()함수의 경우 이전 글에서 설명했듯이, return type이 int형입니다. 즉 입력받은 문자의 ASCII 값을 숫자로 반환이 된다고 이해를 하시면 쉽습니다. 그러한 이유로 읽어온 데이터를 Serial.write()함수를 이용해 출력한 것 입니다. Serial.write()는 괄호 안의 값을 byte 단위로 전송하여 괄호 안의 숫자 값을 ASCII코드에서 일치하는 값의 문자를 찾아 문자로 전송하게 됩니다.

  그런데 여기서 Serial.print()함수로 바꿔주게 된다면, Serial.print()함수는 String type으로 전송을 해버리게 됩니다. 다시말해서, Serial.read()에서 문자를 ASCII코드에서 해당하는 숫자로 바꿔서 return을 했는데 그 숫자를 문자 그대로 전송을 해버리는 것입니다.

예시)

- A라는 문자를 전송했을때, Serial.write(Serial.read())의 경우

- A라는 문자를 전송했을때, Serial.print(Serial.read())의 경우

  이러한 이유 때문에 이 오류를 해결하려면 Serial.read()에서 return한 값을 char변수로 강제 형 변환을 해주면 문자로 바뀌게 됩니다. 즉 , (char)를 추가해주는 것으로 해결할 수 있습니다.

void setup() {
  Serial.begin(9600); 
  // 컴퓨터와 통신을 위해 초당 비트 전송률을 9600bps로 설정
}

void loop() {
  if (Serial.available()) // 현재 수신된 Data가 있는지 확인( 전송중이면 false, 아니라면 true )
  {
    Serial.print((char)Serial.read());
    // print함수는 Serial.read에서 받은 int타입의 값을 스트링 자체로 받아주기 때문에 char로 강제
    // 형 변환을 해주어서 char형태로 출력한다.
  }

 

2. SoftwareSerial를 활용하여 두 대의 아두이노를 연결하고 아래의 조건에 맞는 환경을 만들어 보세요.

  - 2명의 사용자가 사용하는 USART 채팅 프로그램

  - Client1이 컴퓨터를 통해 문자열을 입력하면, Arduino가 수신하여 client2와 연결된 Arduino로 전송하고, Client2의 Arduino가 Client2의 컴퓨터에 수신된 문자열을 출력

  - Client2의 입력 역시 동일하게 Client1의 컴퓨터에 출력되어야 합니다.

// 이 예제의 경우 아두이노 스케치는 1개의 아두이노의 시리얼 모니터를 지원합니다 그렇기 때문에, 다른 한개의 아두이노는 putty 와 같은 Serial 터미널 프로그램을 활용하여 열 수 있습니다.

  이 문제를 풀려면 두가지 개념을 숙지하고 있어야 합니다. 우선 String클래스를 활용할 수 있는 점과, 문자열이 전송되었을때, 그것을 문자 하나하나가 아닌 문자열로 통으로 저장하게 하는 방법이 필요합니다.

  첫번째로 String클래스를 이용하는 것입니다. 기존에 C언어만 공부하신 분들은 문자열을 활용하셔도 문제 없습니다. String클래스를 두개를 선언하여, 한개의 아두이노의 데이터를 저장하는 문자열을 저장하는 변수와 또 다른 하나의 아두이노의 데이터를 저장하는 문자열 변수를 선언해야합니다. 그리고 초기에 ""로 아무런 데이터가 들어있지 않은 상태로 초기화 해 줍니다. 그래야 전송받은 문자를 이어 붙일 수 있기 때문입니다. 전송받은 문자열을 출력한 이후에는 필수적으로 다시 변수를 ""로 초기화 해주어야 정상적인 작동을 하는 것을 볼 수 있습니다.

  두번째로 전송된 값을 문자열로 한번에 저장하는 방법에 대해 알려드리겠습니다. 우선 이전 글을 보면 if문을 활용하여 전송된 데이터를 바로바로 출력해본 예제가 있습니다. 이 코드에서 if문을 while문으로 바꿔주게 되면, 현재 전송된 데이터가 다 처리가 될때까지만 반복하는 반복문을 만들 수 있습니다. 이 방법을 활용하여 while문 내에서 전송된 문자를 읽어온 후 전역변수로 선언한 string type의 문자열 변수에 이어붙여주면 됩니다. 여기서 이어붙여준다는 말은, string함수 특성을 활용하여 +=연산자를 활용한다는 말입니다. 이 두 줄을 while문 안에 넣어주게 되면 아두이노가 문자열을 받았을때, 전송받은 값을 string type으로 저장하게 할 수 있습니다.

  이 두가지를 활용하게 되면, while문과 if문으로 유선 챗봇을 만들 수 있습니다. 윗 문단에서 설명한 while문을 활용한 후 if문에서 문자열이 ""이 아닌 경우 입력받은 데이터가 있다는 것으로 판단하여 그것을 출력하고 ""로 다시 초기화만 해준다면 큰 오류 없이 설계하실 수 있으실 겁니다.

  추가적으로 두개의 Serial을 써야하기 때문에 SoftwareSerial.h를 활용하해야 합니다. 그리고 두개의 아두이노에 같은 코드를 업로딩 하시면 됩니다.

#include <SoftwareSerial.h> // 이 헤더파일을 추가하면서 GPIO를 USART를 추가로 사용가능

String myString = ""; // 컴퓨터와 통신하여 받은 Data값을 String을 사용하여 문자열로 저장하는 공간
String chatString = ""; // 외부 아두이노와의 통신으로 받은 Data값을 String을 사용하여 문자열로 저장하는 공간

SoftwareSerial chat(10, 11); // 외부 아두이노와 USART를 클래스로 선언 Rx : pin 10, Tx : pin 11

void setup() {
  Serial.begin(9600); // 컴퓨터와의 통신 초당 비트 전송률을 9600bps로 설정
  chat.begin(9600); // 외부 아두이노와 통신 초당 비트 전송률을 9600bps로 설정
}

void loop() {
  // 외부 아두이노에서 들어온 Data가 전송중인지 아닌지 판별 (전송중 : False, 전송중이 아닐 때 : True)
  while (chat.available()) {
    char receive = (char)chat.read(); // 외부 아두이노에서 보내온 Data를 int형으로 읽어 char로 강제 형 변환 하여 receive변수에 저장
    chatString += receive; // receive에 저장된 문자를 chatString 문자열에 추가
    delay(10); // 전송되는 Data가 잘리거나 분리되어 저장되는 것을 막기위해 delay를 추가함
  }

  // 위의 while문에서 Data전송이 끝나 chatString에 문자열이 저장되어 Data값을 가지고 있음을 확인하고 출력하는 조건문 ( 있으면 : True , 비어있으면 : False )
  if (chatString != "") {
    Serial.println(chatString); // 외부에서 온 Data를 내 Serial모니터에 출력하고 줄바꿈
    chatString = ""; // 출력 후 다시 chatString을 비워 다음 Data를 저장할 수 있도록 초기화함 
  }

  // 내 컴퓨터와 Serial통신으로 들어온 Data가 전송중인지 아닌지 판별 (전송중 일 때 : False. 전송중이 아닐 때 : Tre)
  while (Serial.available()) {
    char transmit = (char)Serial.read(); // 내 컴퓨터에서 전송한 Data를 int형으로 읽어들이고 char로 강제 형 변환 하여 transmit변수에 저장
    myString += transmit; // transmit에 저장된 문자를 myString 문자열에 추가
    delay(10); // 전송되는 Data가 잘리거나 분리되어 저장되는 것을 막기위해 delay를 추가함
  }

  // 위의 두번째 while문에서 Data전송이 끝나 myString에 문자열이 저장되어 Data값을 가지고 있는지 확인하고 출력하는 조건문 ( 있으면 : True , 비어있으면 : False )
  if (myString != "") {
    chat.println(myString); // 컴퓨터에서 읽어들인 문자열 myString을 외부로 전송하여 출력 후 줄바꿈
    Serial.println(myString); // 컴퓨터에서 전송하려 하는 문자열을 내 Serial모니터에 출력 후 줄바꿈
    myString = ""; // 전송이 끝난 후 다시 myString을 비워 다음 Data를 저장할 수 있도록 초기화
  }
}

 

  앞으로 간단한 것부터 복잡한 임베디드 설계를 할 때, 센서를 활용할 일이 많을 것입니다. 설계를 하다보면 현재 시스템을 모니터링 해야할 일이 많습니다. 지금 환경을 센서가 어떻게 받아들이는지, 내가 원하는 동작이 구현되지 않았을 때, 어느 부분이 잘못됐는지 확인할 방법이 없습니다. 컴퓨터 내에서 C++과 같은 언어로 프로그램을 작성할 경우 한줄한줄 디버깅해서 오류를 찾아내면 되지만 임베디드는 그렇게 하기가 쉽지 않습니다. 이러한 다양한 문제점들을 해결해 줄 것이 바로 시리얼(Serial) 통신입니다.

우선 시리얼(Serial) 통신에 대해 알아보면, 시리얼 통신이란 말 그대로 직렬 통신을 의미합니다. 직렬 통신은 하나 또는 두 개의 전송 선을 사용하여 데이터를 송수신하는 통신 방법을 의미합니다. 하나 또는 두개의 송수신선을 활용하기 때문에 한 번에 한 비트씩 데이터를 지속적으로 주고받습니다. 적은 송수신 선으로 연결이 가능하기 때문에 설계하는데 있어서 비용이 감소되는 장점과 속도가 느린 단점을 가지고 있습니다. 그리고 직렬통신의 경우 동기식(USART) 비동기식(UART) 통신이 있습니다.

 

​* 동기/비동기 통신 (USART, Universal Synchronous Asynchronous Receiver/Transmitter)

  - 동기식 통신

    이 방법은 다른 장비에서 생성된 클럭 또는 자체 생성된 클럭에 동기된 데이터를 송수신 합니다. 송신은 송신하는 쪽에서 각 비트에 추가된 동기 신호(전송 시작을 알리는 신호 또는 끝을 알리는 신호)를 기반으로 수행됩니다. 이 방법은 데이터 전송을 하는데 있어서 시작과 끝을 알리는 추가 신호가 있어서 효율은 좋지만 전송 절차가 복잡해진다는 단점이 존재합니다.

  - 비동기식 통신

    이 방법은 위와는 다르게 각 측의 자체 생성 클럭에 동기화된 데이터를 송수신하게됩니다. 그렇기 때문에 두 통신 개체가 전송 속도 설정이 일치하지 않을 경우 정상적인 통신이 불가능합니다. 즉, 송신 측과 수신 측 모두 초기에 전송할 비트 수에 대해 몇 비트 단위로 전송할 것인지, 초당 몇비트 전송 속도를 이용할 것인지 설정해 주어야 합니다. 그리고 나서 각각은 그 전송 속도와 일치하는 주파수의 동기화 신호를 생성하여 전송을 하게 됩니다. 비동기 통신의 경우 한 번에 한 비트씩 데이터를 송수신하므로, 각 측의 통신 조건이 초기에 일치하지 않으면 정상적인 통신이 불가능 합니다.

 

아두이노 우노의 경우 ATmega328제품으로서 비동기식 통신을 지원한다고 합니다. 보드에 내장된 Microcontroller에 따라 사양이 다를 수 있으니 데이터 시트를 참고하시길 바랍니다.

 

이제 아두이노 우노를 활용하여 배운 내용을 확인해보겠습니다. 아두이노 우노의 경우 0번과 1번핀에 보면 옆에 RX와 TX가 써있는 것을 볼 수 있습니다. 이 핀을 통해서 다른 기기와 통신을 할 수도 있고, USB포트를 활용해 컴퓨터와 통신을 할 수도 있습니다. 우리는 USB포트를 활용하여 통신을 해보도록 하겠습니다. 우선 아두이노에서 시리얼 통신을 하기 위해서 필요한 함수들에 대해 알아보도록 하겠습니다. 시리얼 통신의 경우 별도의 헤더파일을 추가해주지 않아도 사용이 가능합니다.

- Serial.begin(speed): 위에서 설명한 것 처럼 기기와 통신 속도를 맞추기 위한 설정입니다. 괄호 안에 들어가는 변수는 통신 속도를 의미하는데 300 ~ 115200 까지 입력이 가능합니다. 통상적으로 9600의 속도를 많이 쓰고, 저 같은 경우 115200을 자주 씁니다.

- Serial.print(): 괄호 안의 값을 ASCII 문자열로 출력을 해주는 함수입니다. 우리가 원하는 숫자 값을 넣어주게 되면 해당 숫자를 화면에 출력해고, 문자나 문자열을 넣을 경우에 그대로 모니터에 출력을 해줍니다.

- Serial.prinln(): 위와 같은 역할을 하는 함수입니다. 하지만 '\n'을 써주지 않아도 괄호 안에 있는 문자를 출력해준 후 줄바꿈을 실행해 줍니다.

- Serial.write(): Serial.print()함수와 비슷한 역할을 하는데 print()함수의 경우 String Type의 전송을 실행하지만 Serial.write()함수의 경우 Byte type의 전송을 실행하게 됩니다.

// String Type은 Arduino가 지원하는 문자열 형태의 자료구조입니다. 기존의 C++을 하신 분은 아마 익숙하신 클래스 형태의 자료형입니다. C언어만 공부하신 분들은 char형의 문자열이라고 보시면 이해가 쉬우실 겁니다. 하지만 차이점은 String은 문자열간의 +와 같은 연산이 가능하고 추가적인 다양한 함수를 사용할 수 있습니다. Byte type의 경우 1Byte의 크기를 갖는 변수인데, char와 동일한 크기를 갖습니다. 하지만 char type과는 출력 결과가 명확히 다르게 나타납니다. char형의 경우 ASCII 코드에 따른 문자 변환을 진행하지만 byte type의 경우 그대로 출력하게 됩니다.

- Serial.available(): Serial.available()은 뜻 그대로 이용가능한 것이 있는지 묻는 역할을 합니다. 그래서 이 함수를 사용할 경우 송신 기기에서 송신한 문자열이 있을 경우 읽어올 데이터의 byte수를 리턴하게 됩니다. 그렇기 때문에 이러한 특성을 활용하여 보통 if문에 넣어 수신된 문자가 있는지 확인한 후 있을 경우에만 읽어오도록 설정할 수 있습니다.

- Serial.read(): USART을 통해서 수신된 데이터를 리턴해주게 됩니다. 한번에 한 byte단위로 반환해주게 됩니다. 여기서 읽어온 데이터 값은 int type으로 변형되어 리턴됩니다.

 

이제 위 함수들을 활용하여 예제를 진행해 봅시다.

  예제를 진행하기 앞서서 아두이노 스케치에서 시리얼 데이터를 확인할 수 있는 터미널을 여는 방법을 알아보도록 하겠습니다. 아래의 사진과 같이 시리얼 모니터 아이콘을 클릭하거나 Ctrl + Shift + M을 누르면 열립니다. 그리고 아두이노 스케치는 초기 설정이 9600으로 설정이 되어있기때문에 시리얼 모니터를 열고 나서 우측 하단의 전송 속도를 115200으로 맞춰주어야지 예제 코드가 정상적으로 실행됩니다.

  가장 먼저 아두이노에서 컴퓨터로 문자열을 출력하는 예제를 작성해 봅시다. 이번 예제에서는 별다른 회로 구성 없이 컴퓨터랑 아두이노랑 USB연결만 하시면 됩니다. 우선 우리가 원하는 것은 아두이노가 통신을 하기 위해 아두이노를 준비시키는 것이 첫번째입니다. 그 다음에는 위의 함수들을 활용하여 원하는 문자열을 출력하면 됩니다.

void setup(){
  Serial.begin(115200);  // 통신 속도를 115200으로 설정해줍니다.
}

void loop(){
  Serial.print("print: Hello world!\n");  // String type으로 전송
  Serial.println("println: Hello world!");  // String type으로 전송 후 줄바꿈
  Serial.write("write: Hello world!\n");  // Byte type으로 전송
}

  위의 결과를 보고 차이점이 무엇인지 확인해 봅시다. 그리고 저 문자열이 아니더라도 아래와 같이 다른 것들을 입력해보고 각자 함수별로 차이점을 확인해 보세요.

int ascii = 65;

void setup(){
  Serial.begin(115200);
}

void loop(){
  Serial.print(ascii);  // String type으로 전송했을때의 결과
  Serial.write(ascii);  // Byte type으로 전송했을때의 결과
}

  두번째로 String클래스를 활용해서 문자열 덧셈 등을 확인해 봅시다. 개인적으로 C언어 공부 후에 C++를 공부하면서 가장 반가웠던 클래스입니다. 문자열을 다루기도 훨씬 편한데다가 편리한 함수들이 많았기 때문입니다. 두개의 String type의 문자열을 덧셈 기호를 통해 간단하게 붙일 수 있습니다.

String str1 = "Hello";
String str2 = "world";

void setup(){
  Serial.begin(115200);
}

void loop(){
  Serial.print(str1 + str2 + "\n");  // 문자열끼리의 합치는 과정이기 때문에 '\n'이 아닌 "\n"으로 써줍니다. 
}

 

  마지막으로 아두이노와 컴퓨터가 양방향으로 통신하는 것을 확인해보고 포스팅을 마치도록 하겠습니다. 위에서 설명한 available()함수를 활용하면 됩니다. 컴퓨터에서 문자를 입력하여 아두이노에 전송했을때 그 문자를 아두이노가 다시 컴퓨터로 되돌려 전송하는 코드를 작성해 보겠습니다.

void setup(){
  Serial.begin(115200);
}

void loop(){
  if(Serial.available()){  // 아두이노로 전송된 데이터가 있는지 확인(있을 경우 전송된 byte수를 return)
    Serial.write(Serial.read());  // 1 byte를 읽어들인 후 int type으로 반환
  }
}

  위의 코드를 실행할 경우에 내가 입력한 문자 한글자 한글자가 그대로 돌아와 출력이 되는 것을 볼 수 있습니다. 하지만 여기서 우리가 문자열을 받았을때 엔터키가 눌린 문자열이 한번에 모아져서 출력이 되도록 하려면 어떻게 짜야하는지 각자 한번 생각해보시고 코드를 작성해보시길 바랍니다.

  참고로 아두이노 우노의 경우 위에서 설명한 것 처럼 0번핀과 1번핀을 활용해 타 기기와 통신을 할 수 있다고 말씀드렸는데, 2개 이상의 기기와 통신을 원할 경우 SoftwareSerial.h헤더 파일을 추가해주면 일반적인 GPIO핀을 통신 핀으로 활용할 수 있습니다. 사용 방법은 아래의 예시와 같습니다. 참고로 다른 기기와 통신을 할경우 RX와 TX를 교차시켜서 연결해주어야 합니다. 그 이유는 내 기기에서 송신한 데이터는 타 기기의 수신부에 들어가야 하기 때문에 교차시켜서 연결해 주시면 됩니다.

#include <SoftwareSerial.h>

SoftwareSerial newSerial(10,11);  // RX: pin 10, Tx: pin 11로 사용

void setup(){
  newSerial.begin(115200);
}

void loop(){
  newSerial.println("Hello new Serial");
}

 

 

< 오늘의 과제 >

1. 위의 소스코드 중 available()함수를 활용하여 내가 보낸 문자를 다시 출력해주는 코드에서 Serial.write()함수 대신에 print함수를 쓰면 어떤 문제가 발생하는지 확인하고, 이를 해결하기 위해서는 어떻게 수정해주어야 하는지 생각해보세요.

2. SoftwareSerial를 활용하여 두 대의 아두이노를 연결하고 아래의 조건에 맞는 환경을 만들어 보세요.

- 2명의 사용자가 사용하는 USART 채팅 프로그램

- Client1이 컴퓨터를 통해 문자열을 입력하면, Arduino가 수신하여 client2와 연결된 Arduino로 전송하고, Client2의 Arduino가 Client2의 컴퓨터에 수신된 문자열을 출력

- Client2의 입력 역시 동일하게 Client1의 컴퓨터에 출력되어야 합니다.

 

// 이 예제의 경우 아두이노 스케치는 1개의 아두이노의 시리얼 모니터를 지원합니다 그렇기 때문에, 다른 한개의 아두이노는 putty 와 같은 Serial 터미널 프로그램을 활용하여 열 수 있습니다.

+ Recent posts