대표적인 비동기식 시리얼 통신 방식이다. 이전에 배운 SPI, 는 동기식이라는 점에서 동작 방식이 다르니 살펴보도록 하자.

통신 구성이 1:1 방식으로 이루어져 있기에 만약 1:n 통신을 원할 경우 n개의 UART포트가 필요하다.
동기식과 다르게 비동기이므로 클럭이 존재하지 않으며 대신 시작과 끝을 나타내기 위해 비트를 추가한다.
클럭이 존재하지 않으면 발생하는 통신 범위의 문제(어디가 시작이고 어디서부터 어디까지 범위로 읽어야 하는지 등) 때문에 비트 전송 속도를 송/수신 간 동일하게 설정한다.
보율(Baud rate): 변조 속도, 단위는 bps


시작 비트: 통신 시작 전 High를 유지하다가 통신이 시작되는 순간 시작 비트를 0으로 전송
데이터 비트: 시작 비트 이후에 전송되는 5~8비트의 데이터 (일반적으로 1byte 사용)
패리티 비트: 패리티 검사를 위한 비트
(잘 사용하지 않으므로 참고만)
종료 비트: 종료 비트는 패킷의 마지막에 추가되며 1로 전송

라즈베리파이 4B의 경우 6개의 UART, 라즈베리파이 5의 경우 5개의 UART가 존재한다.

여기서 UART0은 이미 블루투스에 연결되어 있으므로 해당 포트를 사용하기 위해서는 블루투스 비활성화가 필요하지만, 그냥 다른 UART 포트를 사용하는게 낫다.
그래서 UART1부터 UART5(라즈베리파이 5는 UART4)까지 사용할 수 있는 셈인데, 4B에서 UART1은 리눅스 콘솔용으로 작업하는 별도의 포트가 존재하였으나, 5부터 해당 포트는 삭제하였으므로 참고하길 바란다.

라즈베리파이에서 UART 핀을 활성화 하기 위해 아래와 같이 설정창에서 시리얼 포트 활성화가 필요하다.
라즈베리 -> 기본 설정 -> Rasberry Pi Configuration -> Interfaces -> Serial Port Enable

근데 이렇게 하게 되면 UART0, UART1만 활성화가 되므로 다른 핀도 활성화를 진행하기 위해 vim이나 nano를 통해 아래 파일의 내용을 수정하자.
~$ sudo vim /boot/firmware/config.txt
아래 내용을 추가하고 재부팅을 진행하자.
(라즈베리파이5인 경우 #dtoverlay=uart1의 주석처리를 해제하고 모두 작성 필요)

아래 명령을 통해 현재 활성화된 UART 포트를 확인
~$ ls -al /dev/tty*
RPi 4는 ttyS0, ttyAMA2, ttyAMA3 활성화

그리고 아래 명령을 통해 Tx, Rx와 매칭되는 GPIO핀 번호를 확인하자.
~$ raspi-gpio funcs | grep TXD2
~$ raspi-gpio funcs | grep RXD2
RPi 5는 AMA0~3 활성화

RPi 5는 아래 명령을 통해 Tx, Rx와 매칭되는 GPIO핀 번호를 확인하자.
~$ pinctrl | grep TXD1
~$ pinctrl | grep RXD1
라즈베리파이 4B, 5에 대한 GPIO 별 추가 기능은 아래 링크를 참고하길 바란다.
RPi 4 https://www.tomshardware.com/reviews/raspberry-pi-gpio-pinout,6122.html
RPi 5
https://github.com/Felipegalind0/RPI5.pinout
#include <wiringSerial.h>
int serialOpen(char* device, int baud)
device: 오픈할 UART 포트와 디바이스 파일 경로
baud: 보율(bps)
반환값: 오픈한 디바이스 파일의 서술자
void serialClose(int fd)
fd: 종료할 UART 포트의 디바이스 파일 서술자
int serialDataAvail(int fd)
fd: 연결된 UART 포트의 디바이스 파일 서술자
반환값: 읽을 수 있는 데이터의 수(byte), 에러 발생 시 -1 반환
UART 포트를 오픈하게 되면 Linux 기본 파일 서술자를 반환하므로 Linux 기본 함수인 read(), write()를 통해 통신한다.
1994년 Ericsson이 최초로 개발한 디지털 통신 기기를 위한 개인 근거리 무선 통신 표준
2.4GHz ~ 2.485GHz 대역폭을 가지고 있으며, 덴마크와 노르웨이를 통일한 하랄드 블라톤 왕에서 유래한 이름이다.
(별칭이 푸른 이빨왕이거든)
우리가 사용할 블루투스 모듈인 HC-06은 아래와 같은 스펙을 가진다.
아래는 핀을 어떻게 연결해야할지 설명한다.


모드: Slave
보율: 9600
패리티 비트: N
데이터 비트 길이: 8
정지 비트 수: 1
Pin code: 1234
위의 설정을 바꾸기 위해서는 AT Command가 필요

AT+BAUD#는 보율을 변경하는 숫자인데, 해당 표는 아래와 같다.


#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#define BAUD_RATE 9600
//RPi 4용
static const char* UART2_DEV = " /dev/tty/AM2";
//RPi 5용
//static const char* UART1_DEV = " /dev/tty/AM1";
unsigned char serialRead(const int fd);
void serialWrite(const int fd, const unsigned char c);
void serialWriteBytes (const int fd, const char *s);
//여러 바이트의 데이터를 씀
void serialWriteBytes (const int fd, const char *s)
{
write (fd, s, strlen (s)) ;
}
//1바이트 데이터를 읽음
unsigned char serialRead(const int fd)
{
unsigned char x;
if(read (fd, &x, 1) != 1) //read 함수를 통해 1바이트 읽어옴
return -1;
return x; //읽어온 데이터 반환
}
//1바이트 데이터를 씀
void serialWrite(const int fd, const unsigned char c)
{
write (fd, &c, 1); //write 함수를 통해 1바이트 씀
}
int main(void)
{
int fd_serial;
unsigned char dat;
char buf[100];
if (wiringPiSetupGpio() < 0) return 1;
if ((fd_serial = serialOpen(UART2_DEV, BAUD_RATE)) < 0)
{
printf("Unable to open serial device.\n");
return 1;
}
while(1)
{
printf("Enter the AT Command: ");
scanf("%s", buf);
serialWriteBytes(fd_serial, buf);
printf("Response: ");
fflush(stdout);
delay(2000);
while (serialDataAvail(fd_serial))
{
dat = serialRead(fd_serial);
printf("%c", dat);
fflush(stdout);
}
printf("\n");
delay(10);
}
}
프로그램을 작성하기 전에 먼저 어떤 외부 장치를 선택할지 정해야 하는데, 보통 랩탑(노트북) 또는 스마트폰을 이용하여 통신하는 것이 일반적이고 편하다.
스마트폰의 경우 Android 운영체제를 탑재한 기기(갤럭시만) 가능하며 아이폰은 블루투스 3.0 이상부터 지원하여 연결이 어려우니 이 점을 유념하기 바란다.

스마트폰에서 스토어로 이동하여 SerialBluetooth Terminal App을 설치하도록 한다.



#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#define BAUD_RATE 115200
static const char* UART2_DEV = "dev/tty/AMA2" //RPi4: UART2 연결을 위한 장치 파일
//static const char* UART1_DEV = “/dev/ttyAMA1”; //RPi5: UART1 연결을 위한 장치 파일
unsigned char serialRead(const int fd); //1Byte 데이터를 수신하는 함수
void serialWrite(const int fd, const unsigned char c); //1Byte 데이터를 송신하는 함수
unsigned char serialRead(const int fd) //1Byte 데이터를 수신하는 함수
{
unsigned char x;
if (read(fd, &x, 1) != 1) //read 함수를 통해 1바이트 읽어옴
{
return -1;
}
return x;
}
void serialWrite(const int fd, const unsigned char c) //1Byte 데이터를 송신하는 함수
{
write(fd, &c, 1); //write 함수를 통해 1바이트 씀
}
int main()
{
int fd_serial;
unsigned char dat;
if (wiringPiSetupGpio() < 0) return 1;
if ((fd_serial = serialOpen (UART2_DEV, BAUD_RATE)) < 0)
{
printf("Unable to open serial device.\n");
return 1;
}
while(1)
{
if(serialDataAvail(fd_serial))
{
dat = serialRead(fd_serial);
printf("%c", dat);
fflush(stdout);
serialWrite(fd_serial, dat);
}
delay(10);
}
}

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#define BAUD_RATE 115200
#define GPIO 18
//static const char* UART2_DEV = "/dev/ttyAMA2"; //RPi4: UART2 연결을 위한 장치 파일
static const char* UART1_DEV = "/dev/ttyAMA1"; //RPi5: UART1 연결을 위한 장치 파일
unsigned char serialRead(const int fd); //1Byte 데이터를 수신하는 함수
void serialWrite(const int fd, const unsigned char c); //1Byte 데이터를 송신하는 함수
unsigned char serialRead(const int fd) //1Byte 데이터를 수신하는 함수
{
unsigned char x;
if(read (fd, &x, 1) != 1) //read 함수를 통해 1바이트 읽어옴
{
return -1;
}
return x; //읽어온 데이터 반환
}
void serialWrite(const int fd, const unsigned char c) //1Byte 데이터를 송신하는 함수
{
write (fd, &c, 1); //write 함수를 통해 1바이트 씀
}
void toggleLed(unsigned char msg)
{
if (msg == '1')
digitalWrite(GPIO, HIGH);
if (msg == '0')
digitalWrite(GPIO, LOW);
}
int main ()
{
int fd_serial;
unsigned char dat;
if (wiringPiSetupGpio () < 0) return 1;
pinMode(GPIO, OUTPUT);
if ((fd_serial = serialOpen(UART1_DEV, BAUD_RATE)) < 0)
{
printf ("Unable to open serial device.\n");
return 1;
}
while(1)
{
// 읽을 데이터가 존재한다면
if(serialDataAvail(fd_serial))
{
dat = serialRead (fd_serial); //버퍼에서 1바이트 값을 읽음
printf ("%c", dat);
fflush (stdout);
//입력 받은 데이터를 다시 보냄 (Echo)
serialWrite(fd_serial, dat);
//LED 작동 토글 함수
toggleLed(dat);
}
delay(10);
}
}
대략 10Hz의 주기로 각도 정보 (Pitch/Roll)를 스마트폰으로 전송한다.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#include <wiringPiSPI.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define BAUD_RATE 115200
#define CS_GPIO 8 //CS 핀 설정
#define SPI_CH 0
#define SPI_SPEED 1000000 // 1MHz
#define SPI_MODE 3
#define BW_RATE 0x2C //통신 속도 설정
#define POWER_CTL 0x2D //Power Control Register
#define DATA_FORMAT 0x31
#define DATAX0 0x32 //X-Axis Data 0
#define DATAX1 0x33 //X-Axis Data 1
#define DATAY0 0x34 //Y-Axis Data 0
#define DATAY1 0x35 //Y-Axis Data 1
#define DATAZ0 0x36 //Z-Axis Data 0
#define DATAZ1 0x37 //Z-Axis Data 1
// static const char* UART2_DEV = "/dev/ttyAMA2"; //RPi4: UART2 연결을 위한 장치 파일
static const char* UART1_DEV = "/dev/ttyAMA1"; //RPi5: UART1 연결을 위한 장치 파일
unsigned char serialRead(const int fd); //1Byte 데이터를 수신하는 함수
//ADXL345 에서 값을 읽는 함수
void readRegister_ADXL345(char registerAddress, int numBytes, char * values)
{
//read 기능을 시작하기 위해서는 주소의 최상위 비트가 1로 설정되어야 함
values[0] = 0x80 | registerAddress;
//멀티 바이트 읽기를 사용하는 경우, 6번 비트도 1로 설정
if(numBytes > 1) values[0] = values[0] | 0x40;
digitalWrite(CS_GPIO, LOW); // Low : CS Active
//데이터를 읽고 values에 저장
wiringPiSPIDataRW(SPI_CH, values, numBytes + 1);
digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}
//ADXL345에 값을 쓰는 함수
void writeRegister_ADXL345(char address, char value)
{
unsigned char buff[2];
buff[0] = address;
buff[1] = value;
digitalWrite(CS_GPIO, LOW); // Low : CS Active
wiringPiSPIDataRW(SPI_CH, buff, 2);
digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}
unsigned char serialRead(const int fd) //1Byte 데이터를 수신하는 함수
{
unsigned char x;
if(read (fd, &x, 1) != 1) //read 함수를 통해 1바이트 읽어옴
{
return -1;
}
return x; //읽어온 데이터 반환
}
void serialWriteBytes(const int fd, const char* c) //1Byte 데이터를 송신하는 함수
{
write (fd, c, strlen(c)); //write 함수를 통해 입력받은 길이의 Byte만큼 작성
}
int main (void)
{
unsigned char buffer[100];
short x, y= 0, z= 0;
float x_g, y_g, z_g;
float roll, pitch;
const float scaleFactor = 0.0038; // 1 LSB = 0.00098g for ±4g range with 13-bit resolution
if(wiringPiSetupGpio() == -1) return 1;
if(wiringPiSPISetupMode(SPI_CH, SPI_SPEED, SPI_MODE) == -1) return 1;
pinMode(CS_GPIO, OUTPUT); //Chip Select로 사용할 핀은 OUTPUT 으로 설정
digitalWrite(CS_GPIO, HIGH); //IDLE 상태로 유지
writeRegister_ADXL345(DATA_FORMAT, 0x09); //범위 설정 +- 4G, 풀 해상도 (13비트)
writeRegister_ADXL345(BW_RATE, 0x0C); //Output Data Rate 400 Hz
writeRegister_ADXL345(POWER_CTL, 0x08); //측정 모드
int fd_serial;
unsigned char dat;
if ((fd_serial = serialOpen(UART1_DEV, BAUD_RATE)) < 0)
{
printf("Unable to open serial device.\n");
return 1;
}
while(1)
{
readRegister_ADXL345(DATAX0,6,buffer); //데이터 수신
x = ((short)buffer[2]<<8)|(short)buffer[1]; //X축 값 반환
y = ((short)buffer[4]<<8)|(short)buffer[3]; //Y축 값 반환
z = ((short)buffer[6]<<8)|(short)buffer[5]; //Z축 값 반환
x_g = x * scaleFactor;
y_g = y * scaleFactor;
z_g = z * scaleFactor;
roll = atan2(y_g, sqrt(x_g * x_g + z_g * z_g)) * 180 / M_PI;
pitch = atan2(-x_g, sqrt(y_g * y_g + z_g * z_g)) * 180 / M_PI;
sprintf(buffer, "Roll: %7.2f°Pitch: %7.2f°\n", roll, pitch); //X, Y, Z 축 출력
serialWriteBytes(fd_serial, buffer);
delay (100);
}
}