< 오늘의 과제 >

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 터미널 프로그램을 활용하여 열 수 있습니다.

오늘은 라이브러리를 활용하여 서보 모터를 제어하는 방법에 대해 다뤄볼 예정입니다. 기존에 C언어나 C++을 공부한 사람이라면 라이브러리에 대한 이해가 충분할 것이므로 라이브러리 추가 방법만 확인하고 서보 모터 제어로 넘어가도 좋습니다.

1. 라이브러리란?

소프트웨어 개발 시 사용되는 비휘발성 resource들을 하나의 파일로 저장한 것으로, 해당 resource들을 필요할 때마다 불러내어 사용할 수 있도록 해줍니다. 아두이노는 오픈소스 하드웨어 및 소프트웨어의 제공으로 아두이노 디바이스에 사용하고 싶은 센서 및 구동부에 대해서 표준 라이브러리를 제공합니다. 라이브러리 추가 방법은 IDE(아두이노 스케치)를 통해서 직접 라이브러리를 추가하는 방식과 작성된 라이브러리 파일을 직접 IDE상에 추가하는 두가지 방법이 존재합니다.

- IDE 상에서 온라인으로 추가하기

아두이노 IDE 의 모습

 

위의 그림과 같이 아두이노 스케치를 실행한 후, 스케치 > 라이브러리 포함하기 > 라이브러리 관리를 누르면 아래와 같은 화면이 나올 것 입니다.

라이브러리 관리를 눌렀을 때 나타나는 화면

위와 같은 화면이 나타났을때 검색에 필터하기 부분에 원하는 센서 혹은 구동부에 필요한 라이브러리를 입력하면 온라인 상에서 다운로드가 가능합니다.

- 직접 라이브러리 추가하기

위와 같은 방법이 아닌 다른 사용자 혹은 내가 별도로 작성해둔 라이브러리 파일을 직접 추가하는 방법에 대해 알아보겠습니다.

우선 아두이노가 설치된 경로로 들어갑니다. arduino.cc에서 압축파일 형태가 아닌 설치형 파일로 다운로드 했을 경우 기본 경로는 C:\Program Files(x86)\Arduino의 위치에 있을것입니다. 해당 경로에 들어가게 되면.

C:\Program Files(x86)\Arduino

 

위와 같이 나오게 되는데 libraries에 들어가 라이브러리 파일을 직접 붙여넣기 해주면 됩니다.

2. 서보모터

모터란? 전기 에너지를 공급받아 이를 움직임으로 전환하는 장치를 말합니다. 일반적인 DC모터의 경우 전원 공급이 이루 어 지면 모터가 지속적으로 회전하는 모습을 볼 수 있습니다. 하지만 오늘 다룰 서보 모터의 경우 회전각의 제한이 있습니다. 서보 모터마다 각자가 회전 할 수 있는 최대 각이 있기 때문에 모델명을 확인하고 datasheet를 참고하길 바랍니다. 다시말해 서보 모터는 제어 구동 보드를 포함해서 특정한 위치와 속도를 가지도록 맞출 수 있는 특수한 모터입니다. CCTV나 프린터 또는 로봇 관절 등에 주로 사용됩니다. 서보 모터를 제어하려면 PWM제어를 사용해야 하는데 PWM에 대한 이론은 다음에 자세하게 다루도록 하겠습니다. 오늘은 서보 모터 라이브러리를 활용할 것이기 때문에 PWM이론에 대한 것은 넘어가도록 하겠습니다.

우선 서보모터 예제를 설명하기에 앞서 회로도를 먼저 구성하겠습니다. 서보 모터의 경우 3개의 핀을 가지고 있는데, 대체로 빨간색은 Vin, 검정색은 GND, 노란색은 신호핀으로 연결합니다.

서보 모터 회로도

 

위와 같이 연결하게 되면 5번핀에서의 출력에 따라 서보 모터가 특정 각도로 움직이게 됩니다.

이제 아두이노 스케치에서 서보 모터 예제 코딩을 하게 될텐데, 앞에서 설명한 라이브러리를 활용할 것입니다. 아두이노 스케치에 보면 서보모터에 관련된 라이브러리가 존재합니다. 그것을 활용할 경우 클래스의 형태로 사용이 가능합니다. PWM에 대한 이해 없이 서보 모터를 쉽게 제어할 수 있습니다.

// 서보 모터 제어에 이용될 함수를 포함한 헤더 파일
#include <Servo.h>    

Servo servo;          // 서보 모터 클래스의 객체를 생성

int servoPin = 5;      // 서보 모터의 signal pin과 연결된 값을 입력
int degree = 0;        // 서보의 각도를 조절할 변수

void setup() {
  // put your setup code here, to run once:
  servo.attach(servoPin);     // 생성된 서보 객체를 연결된 핀에 초기화
}

void loop() {
  // put your main code here, to run repeatedly:
  for (degree = 0; degree < 180; degree += 45) {
    servo.write(degree);       // 서보 모터를 원하는 각도로 움직이게 하는 함수
    delay(1000);               // 움직이고 나서 1초의 delay를 가짐
  }   // 0부터 180도까지 45도씩 증가하면서 움직이도록 하는 for문

  for (degree = 180; degree > 0; degree -= 45) {
    servo.write(degree);       // 서보 모터를 원하는 각도로 움직이게 하는 함수
    delay(1000);               // 움직이고 나서 1초의 delay를 가짐
  }   // 180부터 0도까지 45도씩 감소하면서 움직이도록 하는 for문
}

모터에 따라 돌아갈 수 있는 최대 각도가 다르기때문에 모델명을 확인하고 데이터 시트를 확인해보길 바랍니다. 서보 모터가 돌아갈 수 있는 각도 이상으로 값을 줄 경우 모터의 고장 원인이 될 수도 있습니다. 이러한 모터 특성을 이용해서 로봇 관절과 같은 정확한 각도의 회전을 요구하는 부분에서 활용할 수 있습니다.

이렇게 서보 모터 헤더파일을 참조함으로써 간단하게 제어를 할 수 있지만 이 방법은 나중에 복잡한 코드가 되었을 때 좋은 효율을 내지는 않습니다. 이후에 다룰 PWM제어를 이용하여 제어를 할경우 보다 효율적으로 제어할 수 있습니다.

 

강의: youtu.be/D-AjGiJ0kcY

 

BOJ 8958

 

오늘 풀어볼 문제는 OX퀴즈의 채점표를 보고 최종 점수를 출력해내는 코드를 구현하면 되는 문제입니다. 여기서 특별한 점은 문제를 연속해서 맞출 경우 더해지는 점수가 높아진다는 점입니다. 처음 맞춘 경우 1점이 더해지고, 두번째 연속으로 맞출경우 2점, 세번 연속 맞출 경우에는 3점이 오르는 시스템입니다. 하지만 여기서 중간에 한번 틀릴 경우 초기화가 되어 다시 1점부터 시작하게 됩니다. 제가 처음에 접근했을때는 '이전의 값이 X인지 O인지를 비교해서 풀면 되나?' 생각을 했었지만 그렇게 접근하는 것보다 간단한 방법이 있었습니다. 우선 string type으로 채점표를 입력 받은 뒤에 첫번째 문자부터 탐색해 나갈 것입니다. 여기서 int형 score 변수 하나를 선언해주고 0으로 초기화 해줍시다. 이제 문자열을 하나씩 검사할텐데 여기서 만약에 'O'가 들어왔을때는 score값을 1 증가시켜줍니다. 만약 반대로 'X'가 들어왔을 경우에는 score를 다시 0으로 되돌려줍니다. 그리고 이렇게 판별이 끝난 후에는 score값만큼 정답에 더해주면 됩니다.

아주 간단하게 풀리는 것을 볼 수 있습니다. 이해를 돕기 위해서 하단에 표로 어떻게 알고리즘이 돌아가는지 그리고 플로우차트까지 함께 첨부해 드리겠습니다.

플로우 차트


Example) OOXXOXXOOO

현재 문자

score

answer

O

1

1 (= 0 + 1)

O

2

3 (= 1 + 2)

X

0

3 (= 3 + 0)

X

0

3 (= 3 + 0)

O

1

4 (= 3 + 1)

X

0

4 (= 4 + 0)

X

0

4 (= 4 + 0)

O

1

5 (= 4 + 1)

O

2

7 (= 5 + 2)

O

3

10 (= 7 + 3)


이러한 알고리즘 형태로 돌아가게 됩니다. 요번 문제는 아주 간단하게 풀렸는데, 마지막으로 소스코드와 실행 결과 첨부하고 마치도록 하겠습니다.

#include <iostream>
#include <string>

using namespace std;

int main(){
    int T;

    scanf("%d", &T);

    for(int i = 0;i < T;i++){
        string problem;
        int score = 0;      // 현재 누적된 점수를 저장하는 변수
        int answer = 0;

        cin >> problem;

        for(int j = 0;j < problem.size();j++){
            if(problem[j] == 'O')
                score++;       // 정답을 맞을때마다 누적 점수를 1점씩 증가시킨다
            else
                score = 0;      // 오답일 경우 누적 점수를 0점으로 초기화
            answer += score;    // 현재 누적 점수를 정답에 더해준다
        }

        printf("%d\n", answer);
    }

    return 0;
}

결과창
채점 결과

BOJ 2577

 

우선 이 문제 풀이를 시작하기에 앞서 사용한 함수 한개에 대해 설명해드리겠습니다. int type 변수를 string type으로 변환하는 함수입니다. string.h 헤더파일에 있으며, string.h를 참조를 해주어야 사용할 수 있습니다.

- to_string(int type) : 전달 매개 변수 안에 int type의 값을 넣게 되면 return값으로 string type이 나오게 됩니다. 이 방법 외에도 sprintf라는 함수를 사용하면 int형으로 변환할 수 있지만 sprintf의 경우 전달 매개 변수 값이 char type의 배열이어야 한다는 점입니다. string은 char 배열보다 사용이 간편한 장점이 있기 때문에 to_string함수를 사용했습니다. 이 방법 외에도 나눗셈과 나머지 연산을 활용하여 변환하는 알고리즘을 설계하는 방법도 있습니다.

이제 문제 풀이를 해봅시다. 가장 먼저 A와 B와 C라는 세 값을 입력받아 곱한 결과값을 받아옵니다. 이제 여기서부터 문제가 시작인데 물론 곱한 결과값인 int형을 가지고 풀이를 진행할 수 있지만, 문자열로 변환하여 풀어보는 것이 훨신 간단하여 그 방법으로 풀어보았습니다. 가장 먼저 곱한 결과값을 string type으로 변환해야하는데 위에서 설명한 함수를 활용하면 간단하게 한줄만에 변환을 할 수 있습니다. 그렇게 변환한 stirng 변수를 이용해 각 자리수의 숫자의 개수를 측정할 것입니다. 방법은 아주 간단합니다. 변환된 string 변수를 첫번째 자리부터 끝까지 탐색을 하면서 숫자를 측정하면 되는데 이 과정속에서 아스키 코드를 활용할 것입니다. 0~9까지의 숫자의 개수를 측정할 int type의 배열 num을 만든 뒤 for문을 이용해 곱한 결과값을 string으로 바꾼 문자열을 탐색할 것입니다.

첫번째 자리의 문자열을 탐색했을때 1~9사이의 문자가 나올 것입니다. 이것을 아스키코드 값만큼 빼주게 되면 int type으로 쉽게 변환할 수 있습니다. 그 값을 num의 index에 넣어주고 그 값에 1을 더해주면 됩니다.

가장 쉬운 예로 설명을 하자면, 만약 곱셈의 결과가 329라고 했을때 앞자리부터 탐색을 시작합니다. 가장 첫번째 오는 문자는 '3'일겁니다. 여기서 '0'의 아스키코드 값만큼 빼주게 되면, '3'의 아스키값은 51이고 '0'의 아스키값은 48입니다. 그렇기 때문에 51-48 = 3이 나오게 됩니다. 이렇게 int type의 값으로 바꾸어 num 배열의 index에 넣어주게 되면, num[3]++이 될 것입니다. 그럼 현재 num 배열은 생성 초기에 0으로 초기화를 해주었다는 가정하에 아래의 표와 같이 저장이 될 것입니다.

0

1

2

3

4

5

6

7

8

9

0

0

0

1

0

0

0

0

0

0

다음 과정을 진행하면 329에서 3까지 탐색했으니 2의 문자열을 처리할 것입니다. 위와 동일하게 '2'에서 '0'의 계산을 해주면, 아스키 값으로 봤을때, 50-48 = 2로 num[2]++의 과정을 거치게 될것입니다. 그럼 num에 저장된 값을 확인해보면 아래와 같을 것입니다.

0

1

2

3

4

5

6

7

8

9

0

0

1

1

0

0

0

0

0

0

이렇게 반복하다보면 최종적으로는 아래의 표와같이 나올것입니다.

0

1

2

3

4

5

6

7

8

9

0

0

0

1

0

0

0

0

0

1

그럼 이 결과값을 그대로 for문을 이용하여 한줄한줄 출력해주면 문제가 해결이 됩니다.


* Key point

1. 곱셈의 결과값을 string type으로 변환

2. 문자열을 탐색하면서 아스키 값만큼 빼주어 num 배열 index에 넣어줌

3. 해당 배열 값을 1만큼 증가시켜줌


#include <iostream>
#include <string>

using namespace std;

void solution(int sum) {
	string str;
	int num[10] = { 0, };       // 0 ~ 9까지 각 자리수의 개수를 기록하는 변수

	str = to_string(sum);       // int type의 변수를 string type으로 바꿔주는 변수

	for (int i = 0; i < str.length(); i++)
		num[str[i] - '0']++;    // 곱셈 결과 값을 맨 앞자리부터 탐색하면서 개수를 세는 과정
	
	for (int i = 0; i < 10; i++)
		printf("%d\n", num[i]);     // 결과 값 출력

}
int main() {
	int A, B, C;
	int sum;

	scanf("%d %d %d", &A, &B, &C);

	sum = A * B * C;

	solution(sum);
}

결과창
채점 결과

BOJ 문제번호 1152

이 문제를 풀때, 문자열을 다루는 것에 익숙하지 않아 코드 작성에 애를 먹었던 기억이 있습니다. 기본적으로 C언어나 C++을 공부하신 분들은 string클래스를 활용하시면 보다 편리하게 풀 수 있으실 겁니다. 일단 문장을 입력받아야 하기때문에 gets()함수를 이용해서 입력을 받았습니다. 그리고 문장에서 단어의 개수를 판단하는 기준은 보통 띄어쓰기를 기준으로 나누어져있기 때문에 그 기준을 이용하여 코드를 작성했습니다. 하지만 2번째 예제를 입력했을때 올바른 정답이 나오지 않았었는데, 2번째 예제를 자세히 보면 띄어쓰기로 시작을 하는 것을 볼 수 있습니다. 그 부분 때문에 단어 한개를 추가로 인식하는 오류가 발생해서 flag라는 변수를 추가했습니다.

flag함수를 활용해서 문장을 인식하는 방법은 제가 임베디드때 센서 활용을 할때 자주 쓰던 방법입니다. 운영체제 과목을 수강할 당시 Test And Set이라는 기법에서 응용한 것입니다. 즉 내가 원하는 상태일때를 알려주는 변수입니다. C++을 활용하기 때문에 bool type을 활용하여 내가 알고자 하는 상황을 감지할 수 있습니다. 쉽게말해서 이 문제의 경우 내가 지금 문장을 앞에서부터 한글자 한글자 읽어나가고 있을 때, 지금 읽고있는 글자가 알파벳인지 공백문자인지를 구별하기 위한 변수라고 생각하면 됩니다. 내가 지금 현재 읽고 있는 문자가 알파벳일 경우 flag는 true로 초기화 시켜주면, 코드 내에서 알파벳을 읽고 있을때 동작 특성을 정해줄 수 있습니다.

이렇게 flag함수를 넣어주게 되면 처음에 공백부터 시작하는 문자열이나 끝에 공백이 들어가는 문자열이 오더라도 정확하게 단어의 개수만 측정할 수 있도록 할 수 있습니다.


* Key Point

1. 문자열을 구분하려면 공백을 기준으로 구분한다.

2. 문자열 중 시작부분과 끝부분에 공백이 올때의 오류를 처리해야 한다.

3. 문자열에서 내가 읽고 있는 것이 알파벳인지 공백문자인지 구분해야 한다.


#include <iostream>
#include <stdio.h>

#define MAX 1000001     // 입력받을 수 있는 문자열의 최대 크기
using namespace std;

int main() {
	char str[MAX];      // 입력 받은 문자열을 저장하는 변수
	int index = 0;         // 배열을 탐색할 위치 변수
	int answer = 0;         // 단어의 개수
	bool flag = false;      // 띄어쓰기 구분을 위한 flag

	gets(str);      // 문자열을 입력

	while (str[index] != '\0') {
		if (str[index] != ' ')
			flag = true;        // 처음에 띄어쓰기로 시작한 부분때문에 문장이 시작한 것을 알기 위한 장치
		else if(str[index] == ' ' && flag ) {
			flag = false;
			answer++;
		}       // 띄어쓰기를 기준으로 단어의 개수를 세도록 설정
		index++;
	}
	if(flag)
		answer++;       // 띄어쓰기를 기준으로 했기 때문에 마지막 단어를 세기 위한 작업

	printf("%d\n", answer);

}

채점 결과

 

BOJ 문제번호 2448
예제 입,출력

문제를 읽어보면 알겠지만 3x2k의 수만큼 별을 찍어내려간다고 합니다. 3, 6, 12... 이런식으로 내려가게 됩니다. 처음에는 기본적인 별찍기 문제와 동일하게 생각해서 해당 좌측에 공백문자를 출력한 후 별을 찍는 방식의 for문을 생각했었는데 도저히 규칙을 생각해낼 수가 없었습니다. 그래서 생각해낸 방법이 바로 공백의 문자를 가지고 있는 문자열을 생성하고 그 위에 입력받은 N의 값만큼 별모양을 찍어내면 되는 것 입니다.

그러려면 일단 아래와 같은 별 한 세트를 찍는 함수를 만들었습니다. 좌측에 보이는 별을 한세트로 봤습니다. 왜냐하면 가장 작은 수가 3줄을 찍어내는 것이기 때문입니다.

이제 두번째로 직면한 문제는 정확한 위치에 별을 찍는것이 문제인데, 이것은 시작점의 좌표만 정해주면 규칙적으로 찍어내면 됩니다. 이를 구현하기 위해서는 재귀호출함수를 활용해야하는데, 재귀 함수를 모르시는 분이 있을 수도 있어서 간략하게 설명하고 넘어가겠습니다.

 

* 재귀 호출 함수(Recursive call)

함수 안에서 함수 자기 자신을 호출하는 방식을 재귀 호출(Recursive call)이라고 합니다. 재귀 호출은 일반적인 상황에서는 잘 사용하지 않지만 알고리즘을 구현할 때 매우 유용하게 쓰입니다. 보통 알고리즘에 따라서 반복문으로 구현한 코드보다 재귀호출로 구현한 코드가 좀 더 직관적이고 이해하기 쉬운 경우가 많습니다. 재귀 함수의 경우 하나의 함수가 끝나기 전에 본인 자신을 호출하기 때문에 스택(Stack)에 계속 함수를 호출하여 공간을 차지하게 됩니다. 그렇기 때문에 반드시 재귀 함수를 종료하는 시점을 만들어주는 구문이 필요합니다. 그렇지 않으면 제한된 메모리 용량을 초과해 스택 오버플로우(Stack overflow)현상이 발생하게 됩니다.

이 재귀 호출 함수를 활용하여 문제를 풀어보도록 하겠습니다. 가장 먼저 N이 늘어나는 규칙을 보면 2의 배수 씩 늘어나는 것을 볼 수 있습니다. 여기서 첫번째 3을 입력했을때 가장 위에 위치하는 별은 3번째에 찍히게 됩니다. 두번째 N인 6이 들어왔을 경우, 가장 최상단에 찍히는 별은 6번째 찍히게 됩니다. 그 다음줄에 찍히는 두개의 삼각형 별은 3번째와 8번째에 삼각형의 가장 위의 별이 찍히게 됩니다. 즉, 가장 위의 삼각형을 그리는 함수, 아래의 두개의 삼각형을 그리는 함수를 재귀 호출 함수를 활용하여 3개를 작성하면 됩니다. 여기서 종료 구문은 삼각형의 모양을 그리고 나서 함수를 종료하게 만들면 됩니다. 가장 작은 단위는 N이 3일때이므로 입력받은 N을 2씩 나눠가며 재귀 호출을 하면 됩니다. 삼각형이 추가되는 모형을 보면 N이 6일때는 3의 아래에 삼각형을 두개씩 추가하는 모습이고, 12일때는 6에 6을 아래에 두개 더 추가하는 모습입니다. 플로우 차트로 표현해보면 아래와 같은 모습이지 않을까 싶습니다. 재귀 호출 함수기때문에 직관적으로 표현하긴 어렵지만 대강 표현하게 된다면 저렇게 될 것입니다.

위의 플로우 차트만으로는 설명이 조금 부족하지만 이해를 돕기 위해서 첨부했습니다. 재귀 함수는 N의 값과 x좌표와 y좌표가 계속 업데이트 되면서 새로운 자기 자신 함수를 호출합니다.

재귀 호출 함수를 활용하여 풀이대로 진행을 하게 되면 문제가 원하는 결과 값을 얻을 수 있습니다. 마지막으로 코드와 실행 결과를 보고 포스팅을 마치도록 하겠습니다.

#include <iostream>
#include <memory.h>
char space[3072][6144];		// 별을 찍을 공간 생성 k값이 최대 10까지인 것을 고려하여 배열 크기 생성)

void star(int N, int x, int y) {
	if (N == 3) {
		space[y][x] = '*';
		space[y + 1][x - 1] = '*';
		space[y + 1][x + 1] = '*';
		space[y + 2][x - 2] = '*';
		space[y + 2][x - 1] = '*';
		space[y + 2][x] = '*';
		space[y + 2][x + 1] = '*';
		space[y + 2][x + 2] = '*';
		return;
	}		// 별 한세트를 찍는 구문
	
	star(N / 2, x, y);		// 벌의 윗부분을 찍는 재귀 호출
	star(N / 2,x - (N / 2), y + (N / 2));	// 별의 좌하단 삼각형을 찍는 재귀 호출
	star(N / 2, x + (N / 2), y + (N / 2));	// 별의 우하단 삼각형을 찍는 재귀 호출
}
int main() {
	int N;
	int i, j;

	scanf("%d", &N);

	memset(space, ' ', sizeof(space));		// for문보다 memset으로 배열을 초기화 해주면 시간을 단축 할 수 있다고 한다.

	star(N, N - 1, 0);		// 입력받은 N을 넣어 함수 실행

	for (i = 0; i < N; i++) {
		for (j = 0; j < N * 2; j++) {
			printf("%c", space[i][j]);
		}
		printf("\n");
	}			// 별이 그려진 배열 출력
}

채점 결과

BOJ 문제번호 1065

  우선 주어지는 자연수 N의 범위는 1보다 크거나 같고, 1000보다 작거나 같은 자연수 이다. 예를 들어 123이 주어졌을 때, 각 자리수가 1씩 증가하기 때문에 등차수열로 볼 수 있다. 이와 동일한 조건을 만족하는 숫자들을 카운팅하여 지정된 범위 내에 등차수열 조건을 만족하는 수가 몇개인지 알아보는 것이다.  

  첫번째로, 한자리 숫자와 두자리 숫자의 자연수의 경우 무조건 등차수열이다. 그 이유는 한자리 자연수의 경우 비교를 할 다음 자리 숫자가 없기 때문에, 무조건 등차수열로 본다. 즉, 기본적으로 1~9를 포함하는 범위의 경우 9개는 기본적으로 등차수열을 갖는다. 두자리 자연수의 경우도 마찬가지로 무조건 등차수열이다. 10의 자리 숫자와 1의 자리 숫자의 차이가 공차가 되는데, 그 다음 이어지는 숫자와의 차이를 비교해 볼 수 없기 때문에 등차수열로 본다. 즉 93의 경우 공차가 -6이게 되는데, 다음 비교할 숫자가 없기때문에 등차수열로 보는 것이다. 이렇게 되면 주어진 자연수가 100이상일 경우 1~99는 모두 등차 수열이다. 이제 세자리 수부터 등차수열인지 비교를 해보면 된다. 

  문제를 접근할때, 과거의 공차와 현재의 공차를 비교하기 위해서 2개의 변수를 사용하였다. 그리고 1의자리부터 하나씩 올라가며 각 자리수의 차이를 비교해보고, 각 자리수의 차이가 달라질 경우 비교를 종료하고 다음 숫자를 탐색한다. 여기서 나머지연산과 나눗셈 연산을 사용하였는데, 우선 나머지 연산을 활용하여 제일 끝자리 숫자를 저장하고, 10으로 나누게 되면 자릿수가 한자리씩 밀리게 된다. 이렇게 되면 1의 자리 숫자들을 빼서 인접한 다음 자리 숫자와 차이를 구하기 쉽기 때문이다. 이 과정이 반복될때 10으로 나누는 값이 10보다 작아질 경우 모든 자리수의 차이가 같기때문이므로 정답을 하나 카운트하고 다음 숫자를 또 반복한다. 



% 글을 작성하다 보니, for문을 탐색할때 불필요하게 1부터 99까지 검사를 하는것 같다. for문의 경우 100이 넘을때만 동작하게 하고 그 이전의 자연수는 입력받은 자연수 그대로 출력하면 더 효율적일 것 같다.

#include <iostream>

int main() {
	int N;		// 입력받을 자연수 N
	int answer = 0;							// 등차수열을 만족하는 자연수의 개수
	int temp = 0, n = 0;					// temp: 1의 자리 숫자를 제거하고 남은 수를 임시 저장하는 변수, n: 남아있는 수의 가장 끝자리 숫자
	int diff = 0, before_diff = 0;			// diff: 현재 끝자리 숫자와 인접한 숫자의 차이, before_diff: 이전의 끝자리 숫자와 인접한 숫자의 차이

	scanf_s("%d", &N);

	for (int i = 1; i <= N; i++) {
		if (i < 100) {
			answer++;
		}			// 1~99의 숫자는 모두 등차수열이다
		else {
			temp = i;		
			n = temp % 10;			// 1의 자리 숫자를 저장
			temp = temp / 10;		// 1의 자리를 빼내고 남은 숫자
			diff = (temp % 10) - n;	// 1의 자리 숫자와 그 다음 자리 숫자의 차를 저장
			before_diff = diff;		// 과거 공차로 추정되는 값으로 저장
			while (1) {
				n = temp % 10;
				temp = temp / 10;
				diff = (temp % 10) - n;
				if (before_diff != diff)		// 과거 공차로 추정되는 값과 현재 공차로 추정되는 값을 비교하여 다를 경우 반복문을 나감
					break;
				if (temp < 10) {				// 모든 자리 숫자의 차이가 같을 경우 남은 숫자가 1자리게 되는데 이 경우 해당 숫자는 등차수열이므로 answer을 1개 추가하고 반복문을 나감
					answer++;
					break;
				}
				before_diff = diff;				// 과거 공차로 추정되는 값을 현재 사용된 공차로 업데이트
			}
		}
	}

	printf("%d\n", answer);

}

 

+ Recent posts