이전에 아두이노에서 설정한 문자열을 라즈베리파이를 통해 Serial로 연결하여 수신받는 예제를 확인해 보았다. 이번에는 반대로 라즈베리파이에서 텍스트파일을 읽어와 아두이노에 뿌려줄 수 있는 통신을 구축해보도록 하자. 필자의 경우에는 라즈베리파이에 있는 파일을 아두이노 SD카드에 전송하고자하는 특수한 목적 때문에 본 예제를 활용했을뿐 실상 잘 쓰이는 방법이 아니기 때문에 그냥 이렇게도 가능하구나 정도만 이해하고 넘어가도 될 듯 하다.


예제의 경우 이전과 같은 예제를 활용하였으며 파일을 읽어 아두이노에 전송하기 위해 약간의 수정을 가했다. 따라서 수정된 부분에 대한 설명을 위해 C언어 파일 입출력에 대한 간단한 개념을 기술하였고 이를 통해 아두이노와 직접 통신하는 부분까지 진행해보자.



1. 파일 문자열 송신 예제


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#ifdef RaspberryPi 
#include <stdio.h> //for printf
#include <stdint.h> //uint8_t definitions
#include <stdlib.h> //for exit(int);
#include <string.h> //for errno
#include <errno.h> //error output
#include <wiringPi.h>
#include <wiringSerial.h>
 
char device[]= "/dev/ttyACM0";
int fd;
unsigned long baud = 9600;
unsigned long time=0;
 
char strHello[] = "HELLO";
char cTemp[512= {0};
int ch;
char Endc;
 
//prototypes
int main(void);
void loop(void);
void setup(void);
 
void setup(){
 
  printf("%s \n""Raspberry Startup!");
  fflush(stdout);
 
  //get filedescriptor
  if ((fd = serialOpen (device, baud)) < 0){
    fprintf (stderr, "Unable to open serial device: %s\n", strerror (errno)) ;
    exit(1); //error
  }
 
  //setup GPIO in wiringPi mode
  if (wiringPiSetup () == -1){
    fprintf (stdout, "Unable to start wiringPi: %s\n", strerror (errno)) ;
    exit(1); //error
  }
}
 
// main function for normal c++ programs on Raspberry
int main(){
  setup();
  FILE * fp=fopen("/home/pi/Desktop/Work/transfer.txt","rt");
  if(fp==NULL){
    printf("file open fail\n");
    return -1;
}else{  
   printf("file open success\n"); 
   while(fgets(cTemp, sizeof(cTemp),fp) != NULL){
      serialPuts (fd, cTemp);
      printf(cTemp);
}
  
if(feof(fp) != 0){
    printf("file upload success\n");
}
 else
    printf("file upload fail\n");
 fclose(fp);
}
 
  return 0;
}
 
#endif //#ifdef RaspberryPi
 
 
cs

예제를 보면 아래 int main() 부분만 다르다는 것을 알 수 있다. 동작 순서를 간단히 언급하자면 설정된 경로의 텍스트 파일을 읽어와 각 줄단위로 읽고 이를 아두이노로 전송하며 만약 파일의 끝부분에 도달했을 경우 통신을 마치게 되는 구조이다. 그럼 각 함수에 대해 간단하게 설명하도록 하겠다.


2. 코드 설명


  FILE * fp=fopen("transfer.txt","rt")


C언어 프로그램상에서 파일에 저장되어 있는 데이터를 읽기 위해서는 데이터가 이동할 수 있는 다리 역할을 할 수 있는 스트림을 형성해야 한다. 쉽게 말해 파일로부터 데이터를 읽을 수 있는 최소한의 준비를 해주는 과정이라고 생각하면 된다.  따라서 위 fopen 함수는 스트림 형성을 요청하는 호출문 역할을 하며 괄호 안에 첫번째 인자의 경우 읽어들일 파일의 경로를 뜻하고 두번째 인자는 형성할 스트림의 종류를 뜻하며 입력스트림과 출력스트림 이렇게 2가지 형태가 존재한다. 위 코드는 "rt" 로 설정하였으므로 입력스트림을 뜻하며 파일의 데이터를 읽을 수 있는 반면에 쓰지는 못하기 때문에 만약 데이터를 쓰기 원한다면 별도로 출력스트림을 다시 형성해야 한다. C언어에서는 이러한 개념을 바탕으로 총 6가지 스트림으로 세분화 할 수 있다.


 

내용

   오직 읽기만 가능

w

   오직 쓰기만 가능

   만약 파일이 존재하지 않으면 새로운 파일을 생성해서 데이터 쓰기

   만약 파일이 존재하면 기존의 데이터를 지우고 데이터 쓰기 

a

   w 모드와 달리 파일이 존재하면 파일 끝에 덧붙여 데이터 추가

r+ 

   읽기와 쓰기 가능

   만약 파일이 존재하면 기존의 데이터를 지우지 않고 데이터 덮어 쓰기

w+

   읽기와 쓰기 가능

   만약 파일이 존재하면 기존의 데이터를 지우고 데이터 쓰기

a+

   읽기와 쓰기 가능 (쓰기의 경우 a 모드와 특징이 같다.)


 "rt" 에서 뒤의 t는 텍스트모드를 뜻하며 t의 텍스트모드와 b의 바이너리모드가 있다고만 이해하고 넘어가자. 


  char *fgets(char *string, sizeof(*string), File *stream)


위 함수는 텍스트 파일에 저장된 문자를 줄 단위로 읽어들어와 반환하는 함수로서 첫번째 인자인 *string은 입력 받은 문자열을 저장할 포인터(읽은 데이터를 잠시 저장하는 공간이라고만 생각하자)를 뜻하고 두번째 인자인 sizeof()는 입력 받을 문자의 수를 설정하는 것이며 세번째 인자인 파일 포인터는 형성한 스트림의 이름을 뜻한다.  이 함수는 읽어드린 문자열에 대한 포인터를 반환하며 파일의 끝에 도달하거나 오류가 발생할 경우 NULL을 반환한다.


  void serialPuts (int fd, char *s) 


위 함수는 wiringPi의 입출력 함수중 하나로 쉽게 설명하자면 fgets로 저장한 문자열을 fd(여기서 fd는 시리얼 통신할 디바이스라고 생각해두자)로 전송하게 된다. 사실 위 함수만 제대로 활용해도 아두이노로 문자열을 송신할 수 있지만 여기서는 파일입출력까지 같이 응용하여 예제를 만들어 보았다. wiringPi가 지원하는 함수에 대해 보다 자세히 알고싶다면 공식 홈페이지를 참고하도록 하자.


http://wiringpi.com/reference/serial-library 


마지막으로 fopen을 통해 파일을 개방하였다면 fclose 함수를 통해 파일을 꼭 닫아주어야 한다. 그 이유는 스트림을 형성하기 위해서는 시스템에서 메모리를 할당해야 하는데 파일을 닫아주지 않으면 메모리가 할당된 채로 유지되면서 손실이 일어나기 때문이다. 쉽게 말해 메모리가 유출되는 것을 막기 위한 것으로 이해하면 된다.



3. 아두이노 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char cTemp;
String sReal = "";
 
void setup()
{
  Serial.begin(9600);
}
void loop()
{
    while(Serial.available()){
      cTemp = Serial.read();
      sReal.concat(cTemp);
      if(cTemp=='\n')
      {
        Serial.print(sReal);
        sReal="";
      }
    }
}
 
cs


아두이노 소스코드는 간단하게 구현해보았는데 여기서 유의해야할 것이 Serial.read() 함수이다. 라즈베리파이의 경우 문자를 한 줄로 읽거나 쓰는 것이 가능한데 아두이노는 한 글자씩 밖에 읽기가 불가능하다. 때문에 이러한 문제점을 해결하기위해 필자의 경우에는 while문을 활용하여 라즈베리파이를 통해 송신된 문자열을 각 문자 단위로 읽고 이를 concat 함수 (문자와 문자를 이어주는 역할을 한다.)를 사용하여 한 문장으로 만든 뒤에 해당 문자열의 끝을 나타내는 개행문자가 입력으로 들어올 시 문자열을 출력하고 문자열 변수를 초기화 하는 형태로 코드를 구현해보았다. 나름대로 구현이 잘 되긴 했지만 완성도면에서 미흡하다고 생각하기 때문에 후에 더 나은 방향으로 수정이 필요해 보인다.



4. 결과 확인


컴파일 방법은 기존과 동일하며 읽어올 파일을 형성하고 그 경로를 지정하여 예제 코드에 반영해주자.



라즈베리파이와 아두이노 사이에는 SPI, I2C, UART 등 다양한 통신 방법들을 적용할 수 있다. 하지만 아두이노의 입출력 핀 전압이 5V인 반면에 라즈베리파이의 GPIO 입출력 핀 전압은 3.3V이기 때문에 직접적으로 결선할 경우 문제가 발생하게 된다. 따라서 여기서는 가장 간편하게 사용할 수 있는 Serial(UART) 통신을 활용해보자. 


시리얼통신은 라즈베리파이와 아두이노를 USB케이블로 연결하는 것으로 간단하게 구현할 수 있으며 전압에 따른 문제 또한 발생하지 않는다.



1. 아두이노 Idle 설치하기


  $ sudo apt-get install arduino


패키지 설치를 위해 라즈베리파이에 update 명령어를 통해 업데이트 시켜주고 위 명령어를 입력하여 아두이노 패키지를 설치한다. 이때 설치된 패키지는 권한을 아직 가지고 있지 않은 상태이므로 바로 실행시키지 말고 아래 명령어를 입력하여 아두이노 패키지가 시리얼 포트에 엑세스 할 수 있는 권한을 얻도록 한다.


  $ sudo usermod -a -G tty pi

  $ sudo usermod -a -G dialout pi


위 과정을 완료하였다면 아두이노 패키지가 성공적으로 라즈베리파이에 설치되었다. 이제 아두이노를 실행시키고 라즈베리파이에 아두이노를 연결한 후에 포트를 설정하고 ("/dev/ttyACM0" 일 것이다.) blink 예제를 업로드하여 제대로 동작하는지 확인해본다.




2. 시리얼 통신하기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
 Pi_Serial_test.cpp - SerialProtocol library - demo
 Copyright (c) 2014 NicoHood.  All right reserved.
 Program to test serial communication
 
 Compile with:
 sudo gcc -o Pi_Serial_Test.o Pi_Serial_Test.cpp -lwiringPi -DRaspberryPi -pedantic -Wall
 sudo ./Pi_Serial_Test.o
 */
 
// just that the Arduino IDE doesnt compile these files.
#ifdef RaspberryPi 
 
//include system librarys
#include <stdio.h> //for printf
#include <stdint.h> //uint8_t definitions
#include <stdlib.h> //for exit(int);
#include <string.h> //for errno
#include <errno.h> //error output
 
//wiring Pi
#include <wiringPi.h>
#include <wiringSerial.h>
 
// Find Serial device on Raspberry with ~ls /dev/tty*
// ARDUINO_UNO "/dev/ttyACM0"
// FTDI_PROGRAMMER "/dev/ttyUSB0"
// HARDWARE_UART "/dev/ttyAMA0"
char device[]= "/dev/ttyACM0";
// filedescriptor
int fd;
unsigned long baud = 9600;
unsigned long time=0;
 
//prototypes
int main(void);
void loop(void);
void setup(void);
 
void setup(){
 
  printf("%s \n""Raspberry Startup!");
  fflush(stdout);
 
  //get filedescriptor
  if ((fd = serialOpen (device, baud)) < 0){
    fprintf (stderr, "Unable to open serial device: %s\n", strerror (errno)) ;
    exit(1); //error
  }
 
  //setup GPIO in wiringPi mode
  if (wiringPiSetup () == -1){
    fprintf (stdout, "Unable to start wiringPi: %s\n", strerror (errno)) ;
    exit(1); //error
  }
 
}
 
void loop(){
  // Pong every 3 seconds
  if(millis()-time>=3000){
    serialPuts (fd, "Pong!\n");
    // you can also write data from 0-255
    // 65 is in ASCII 'A'
    serialPutchar (fd, 65);
    time=millis();
  }
 
  // read signal
  if(serialDataAvail (fd)){
    char newChar = serialGetchar (fd);
    printf("%c", newChar);
    fflush(stdout);
  }
 
}
 
// main function for normal c++ programs on Raspberry
int main(){
  setup();
  while(1) loop();
  return 0;
}
 
#endif //#ifdef RaspberryPi
cs

위 코드는 wiringPi 라이브러리를 활용하여 아두이노와 통신할 수 있는 예제 코드이다. 여기서 "/dev/ttyACM0" 부분은 아두이노가 연결된 장치의 포트명을 말하는 것으로 가끔씩 에러가 발생할 경우 ACM1, ACM2 이렇게 순차적으로 증가할 수 있으니 유의하도록 한다. 라즈베리파이의 USB포트 구성 목록을 확인하려면 다음 과 같은 명령어를 입력하면 된다.

  $ dmesg|tail



그럼 이제 위 소스코드를 컴파일한다. 필자는 소스코드의 파일명을 test.c로 하였으며 hello라는 이름으로 컴파일했다. 컴파일할 때는 파일이 저장된 디렉토리로 들어간 후에 하도록 한다.


  $ sudo gcc test.c -o hello -l wiringPi -DRaspberryPi

  $ sudo ./hello


아래는 간단하게 구현한 아두이노 소스코드이며 1초마다 라즈베리파이로 Hello World 문자를 송신할 것이다.


1
2
3
4
5
6
7
8
void setup(){  
  Serial.begin(9600);
 
void loop(){
  Serial.println("Hello World");
  delay(1000);
}
cs


3. 결과 확인


위와 같은 결과가 나오면 성공이다. 여기서 실행된 프로그램을 종료하려면 Ctrl + C를 눌러주면 된다. 만약 종료를 하지 않을경우 터미널을 종료해도 계속해서 프로그램이 동작하며 아두이노를 다시 연결시 ttyACM0 포트는 사용하지 못하기 때문에 테스트 종료시에 유의하도록 한다.