Embedded System UART

Wonho Kim·2025년 1월 1일

Embedded System

목록 보기
7/7

UART

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

전이중 통신

  • 송/수신 동시에 가능
  • Rx 선과 Tx 선을 교차해서 연결하는 방식
    Tx: Transmitter
    Rx: Receiver

일대일(1:1) 통신 방식

통신 구성이 1:1 방식으로 이루어져 있기에 만약 1:n 통신을 원할 경우 n개의 UART포트가 필요하다.

비동기 방식

동기식과 다르게 비동기이므로 클럭이 존재하지 않으며 대신 시작과 끝을 나타내기 위해 비트를 추가한다.

클럭이 존재하지 않으면 발생하는 통신 범위의 문제(어디가 시작이고 어디서부터 어디까지 범위로 읽어야 하는지 등) 때문에 비트 전송 속도를 송/수신 간 동일하게 설정한다.

보율(Baud rate): 변조 속도, 단위는 bps

데이터 송/수신 형태

시작 비트: 통신 시작 전 High를 유지하다가 통신이 시작되는 순간 시작 비트를 0으로 전송

데이터 비트: 시작 비트 이후에 전송되는 5~8비트의 데이터 (일반적으로 1byte 사용)

패리티 비트: 패리티 검사를 위한 비트
(잘 사용하지 않으므로 참고만)

종료 비트: 종료 비트는 패킷의 마지막에 추가되며 1로 전송

라즈베리파이의 UART 통신

라즈베리파이 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

wiringPi와 UART 라이브러리

헤더파일

#include <wiringSerial.h>

UART 포트 오픈 함수

int serialOpen(char* device, int baud)

device: 오픈할 UART 포트와 디바이스 파일 경로
baud: 보율(bps)
반환값: 오픈한 디바이스 파일의 서술자

UART 포트 연결 종료 함수

void serialClose(int fd)

fd: 종료할 UART 포트의 디바이스 파일 서술자

읽을 데이터 존재여부 확인 함수

int serialDataAvail(int fd)

fd: 연결된 UART 포트의 디바이스 파일 서술자
반환값: 읽을 수 있는 데이터의 수(byte), 에러 발생 시 -1 반환

UART 포트를 오픈하게 되면 Linux 기본 파일 서술자를 반환하므로 Linux 기본 함수인 read(), write()를 통해 통신한다.

블루투스(Bluetooth)

1994년 Ericsson이 최초로 개발한 디지털 통신 기기를 위한 개인 근거리 무선 통신 표준

2.4GHz ~ 2.485GHz 대역폭을 가지고 있으며, 덴마크와 노르웨이를 통일한 하랄드 블라톤 왕에서 유래한 이름이다.
(별칭이 푸른 이빨왕이거든)

우리가 사용할 블루투스 모듈인 HC-06은 아래와 같은 스펙을 가진다.

  • 프로토콜: Bluetooth V2.0 표준 프로토콜
  • 밴드: 2.4GHz ~ 2.48GHz, ISM Band
  • 구동 전압: +3.6V ~ 6V
  • 구동 전류: 40 mA

아래는 핀을 어떻게 연결해야할지 설명한다.

  • RXD: 마이크로컨트롤러(마스터)의 TXD핀과 연결
  • TXD: 마이크로컨트롤러(마스터)의 RXD핀과 연결
  • VCC: 전원 공급핀 3.6V ~ 6V 사이에 연결
  • GND: Ground에 연결
  • State: LED와 연결되어 있음 (현재 보드 상태 표시)
  • Key: 상태를 결정하는 핀
    High: AT Command 모드
    Low: 일반 모드

HC-06의 기본 설정

모드: Slave
보율: 9600
패리티 비트: N
데이터 비트 길이: 8
정지 비트 수: 1
Pin code: 1234

위의 설정을 바꾸기 위해서는 AT Command가 필요

블루투스와 AT Command 표

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

AT 명령어 실행 프로그램

AT 명령어 실행 프로그램 개념도

AT 명령어 실행 프로그램 코드

#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);
    }
}

외부 장치에서 데이터를 받으면 다시 반환해주는 Echo 프로그램

프로그램을 작성하기 전에 먼저 어떤 외부 장치를 선택할지 정해야 하는데, 보통 랩탑(노트북) 또는 스마트폰을 이용하여 통신하는 것이 일반적이고 편하다.

스마트폰의 경우 Android 운영체제를 탑재한 기기(갤럭시만) 가능하며 아이폰은 블루투스 3.0 이상부터 지원하여 연결이 어려우니 이 점을 유념하기 바란다.

스마트폰의 연결과정


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

Echo 프로그램 개념도

Echo 프로그램 예제 코드

#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);
    }
}

실습 2: 스마트폰 -> RPi 데이터 전송 후 LED On/Off 제어 프로그램

  • LED는 GPIO 18번을 통해 제어하도록 한다.
  • 스마트폰에서는 0 or 1을 전송하면
    => LED Off: 0
    => LED On: 1
#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);
    }
}

실습 3: 가속도 센서(ADXL-345) -> 스마트폰으로 데이터 전송

대략 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);
    }
}
profile
새싹 백엔드 개발자

0개의 댓글