ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 윈도우 콘솔 시리얼 통신
    개발환경 구축 2023. 2. 20. 00:51

    VScode로 AVR 개발환경을 만들고 보니, 시리얼 모니터가 아쉽다. 시리얼 모니터를 사용하자고 아두이노 IDE을 열자니, 2.0으로 올라오면서 은근 여는데 시간이 오래 걸린다. 윈도우에서도 리눅스처럼 콘솔로 간단하게 시리얼 출력을 볼 수 있으면 좋겠다고 생각을 해서 처음으로 window 프로그램을 코딩해보았다.

     

    https://playground.arduino.cc/Interfacing/CPPWindows/

     

    Arduino Playground - CPPWindows

    Interfacing... Arduino and C++ (for Windows) As I found it pretty hard finding the good information, or an already working code to handle Serial communication on windows based system, I finally made a class that do what is needed for basic Serial Communica

    playground.arduino.cc

    https://blog.naver.com/PostView.naver?blogId=kimmch696&logNo=130172345862&redirect=Dlog&widgetTypeCall=true&directAccess=false 

     

    [MFC] Serial 통신에 필요한 DCB 구조체

    다음은 winbase.h 에 내장된 DCB 구조체로써 시리얼 통신 환경을 설정하는데 필요한 구조체이다...

    blog.naver.com

    여기를 많이 참고했다.

     

    실행시 전달 인자 설명
    -p COMn COM 포트 지정
    -b (baud rate) 보율 설정(기본값 9600)
    -B (byte size) 한번에 주고 받을 1바이트의 크기(기본값 8)*
    -Sn 0: 1 stop bit(기본값)
    1: 1.5 stop bit
    2: 2 stop bit
    -Pn 0: 패리티 없음** (기본값)
    1: 홀수 패리티
    2: 짝수 패리티
    3: 마크 패리티
    4: 스페이스 패리티
    -Dn 0:장치를 열거나 DTR이 on 되려할 때 DTR을 off 한다.
    1:장치를 열거나 DTR이 off 되려할 때 DTR을 on 한다.(기본값)
    2:DTR handshaking을 사용한다.

     *아두이노는 8비트 마이크로컨트롤러이니 한 번에 8비트를 처리할 수 있음, 만약 16비트 프로세서면, 한번에 2바이트 즉 16비트를 전송하는 것이 가능함

     

    **패리티 비트: 데이터가 제대로 전송되었는지 확인을 위해 추가로 보내는 비트, 홀수 패리티일 경우 1의 갯수가 홀수가 되도록, 짝수면 짝수가 되도록 8비트를 보내고 추가로 1비트를 더 전송한다. 예를 들어 0b10101110을 전송한다면 1의 갯수가 5개 이니 홀수 패리티 규칙인 경우 0을, 짝수 패리티 규칙인 경우 1을 추가로 전송한다. 마크 패리티는 패리티 비트가 항상 1, 스페이스는 항상 0이다.

     

    ***Data Terminal Ready: 통신버퍼가 오버플로우 되지 않도록 전송속도를 조절하는 흐름제어 방식이다.

     

    https://github.com/sidreco214/winserial

     

    GitHub - sidreco214/winserial: Simple Serial Terminal for Windiw Console

    Simple Serial Terminal for Windiw Console. Contribute to sidreco214/winserial development by creating an account on GitHub.

    github.com

     

    WinSerial.h

     

    /*
    참고 https://playground.arduino.cc/Interfacing/CPPWindows/
    */
    #ifndef WINSERIAL
    #define WINSERIAL
    
    #define ARDUINO_WAIT_TIME 2000 //아두이노 보드 리셋되는 시간
    
    #include <windows.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    class WinSerial {
        private:
        bool connection; //COMPORT 연결 상태
        HANDLE hWinSerial; //WinSerial Handler
        COMSTAT status; //COMPORT에 대한 여러가지 상태 출력
        DWORD errors; //최근 에러를 저장
    
        public:
        WinSerial(const char* COMPort, const unsigned int& baud, const int& ByteSize, const int& StopBit, const int& Parity, const int& DTR);
        ~WinSerial();
    
        bool connected();
        int read(char* buffer, unsigned int buf_size); //시리얼 통신으로 읽어온 값을 버퍼에 저장후 읽은 바이트 수를 리턴, 읽을게 없으면 0리턴
        bool send(const char* buffer, unsigned int buf_size); //입력된 값을 시리얼 통신으로 출력, 성공하면 1출력
    };
    
    #endif

    WinSerial.cpp

    #include "WinSerial.h"
    
    WinSerial::WinSerial(const char* COMPort, const unsigned int& baud, const int& ByteSize, const int& StopBit, const int& Parity, const int& DTR)
    : connection(false)
    {
        hWinSerial = CreateFile(COMPort,
                        GENERIC_READ | GENERIC_WRITE,
                        0,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL
                    );
        //시리얼 연결 확인
        if(hWinSerial == INVALID_HANDLE_VALUE) {
            if(GetLastError() == ERROR_FILE_NOT_FOUND)  {
                printf("ERROR: Handle was not attached. Reason: %s is not available.\n", COMPort);
            }
            else printf("ERROR!!");
        }
        else {
            //DCB 구조체 설정, 기본적으로 0이어야하는 게 있기 때문에 구조체 초기화
            DCB dcbSerialParams = {0}; 
    
            //현 상태 확인
            if(!GetCommState(hWinSerial, &dcbSerialParams)) {
                printf("failed to get current serial parameters!");
            }
            else {
                dcbSerialParams.BaudRate = baud;
                dcbSerialParams.ByteSize = ByteSize;
                dcbSerialParams.StopBits = StopBit;
                dcbSerialParams.Parity   = Parity;
                dcbSerialParams.fDtrControl = DTR;
    
                if(SetCommState(hWinSerial, &dcbSerialParams)) {
                    //설정 저장한 구조체가 제대로 전달됨
                    connection = true;
                    PurgeComm(hWinSerial, PURGE_RXCLEAR | PURGE_TXCLEAR); //버퍼 비우기
                    Sleep(ARDUINO_WAIT_TIME); //아두이노 보드가 리셋될때 까지 기다리기
                    
                }
                else {
                    //구조체 전달 실패
                    printf("ALERT: Could not set Serial Port parameters");
                }
            }
        }
    }
    
    WinSerial::~WinSerial() {
        if(connection) {
            connection = false;
            CloseHandle(hWinSerial);
        }
    }
    
    bool WinSerial::connected() {
        return connection;
    }
    
    int WinSerial::read(char* buffer, unsigned int buf_size) {
        DWORD bytesRead;
        unsigned int num; //읽을 데이터 수, 오버플로우 방지
    
        ClearCommError(hWinSerial, &(errors), &(status));  //COM포트 상태 읽기
        //읽을 게 있으면
        if(status.cbInQue > 0) {
            if(status.cbInQue > buf_size-1) num = buf_size-1; //배열 인덱스는 배열크기-1 까지
            else                            num = status.cbInQue;
    
            if(ReadFile(hWinSerial, buffer, num, &bytesRead, NULL)) return bytesRead;
        }
        //읽어올게 없는 경우, '0'을 int로 바꾸면 0이 아니니 이래도 괜찮음
        return 0;
    }
    
    bool WinSerial::send(const char* buffer, unsigned int buf_size) { 
        DWORD bytesSend;
    
        if(WriteFile(hWinSerial, (void*)buffer, buf_size-1, &bytesSend, 0)) return true;
        else {
            ClearCommError(hWinSerial, &(errors), &(status));
            return false;
        }
    }

    main.cpp

    /*
    참고 https://playground.arduino.cc/Interfacing/CPPWindows/
    */
    
    #include <stdio.h>
    #include <string>
    using std::string;
    #include "src/WinSerial.h"
    
    #define BUF_LENGTH 256
    
    const string Arg[] = {"-p",                              //COMPORT
                          "-b",                              //Baud Rate
                          "-B",                              //Byte Size
                          "-S0", "-S1", "-S2",               //Stop Bit
                          "-P0", "-P1", "-P2", "-P3", "-P4", //Parity
                          "-D0", "-D1", "-D2",               //DTR
                          };
    
    enum _arg {
        p = 0,
        b,
        B,
        S0, S1, S2,
        P0, P1, P2, P3, P4,
        D0, D1, D2
    };
    
    int main(int arc, char* argv[]) {
        char* comport = argv[0];
        //printf("arc: %d\n",arc);
        //printf("%s\n",comport);
        unsigned int baud = 9600, ByteSize = 8, StopBit = ONESTOPBIT,
                     Parity = NOPARITY, DTR = DTR_CONTROL_ENABLE;
    
        //argv 의 첫 문자열은 winserial.exe, arc는 배열 원소 갯수이니
        //그래서 1번 인덱스 부터 arc-1 인덱스까지 검색
        for(int i=1; i<arc; i++) {
            int arg = -1;
            for(int j=0; j<sizeof(Arg)/sizeof(Arg[0]); j++) {
                if(Arg[j] == argv[i]) {arg = j; break;}
            }
            
            switch(arg) {
                default:
                break;
    
                case p:
                comport = argv[++i];
                break;
    
                case b:
                baud = atoi(argv[++i]);
                break;
    
                case B:
                ByteSize = atoi(argv[++i]);
                break;
    
    
                case S0:
                StopBit = ONESTOPBIT;
                break;
    
                case S1:
                StopBit = ONE5STOPBITS;
                break;
    
                case S2:
                StopBit = TWOSTOPBITS;
                break;
    
    
                case P0:
                Parity = NOPARITY;
                break;
    
                case P1:
                Parity = ODDPARITY;
                break;
    
                case P2:
                Parity = EVENPARITY;
                break;
    
                case P3:
                Parity = MARKPARITY;
                break;
    
                case P4:
                Parity = SPACEPARITY;
                break;
    
    
                case D0:
                DTR = DTR_CONTROL_DISABLE;
                break;
    
                case D1:
                DTR = DTR_CONTROL_ENABLE;
                break;
    
                case D2:
                DTR = DTR_CONTROL_HANDSHAKE;
                break;
            }
        }
    
        //comport가 지정되지 않은 경우
        if(string(comport) == argv[0]) {printf("ERROR: COMPORT must be assigned!"); return 0;}
        //printf("TEST Baud Rate: %d, Byte Size: %d\n%s is connected\n", baud, ByteSize, comport);
        
        WinSerial Serial(comport, baud, ByteSize, StopBit, Parity, DTR);
    
        char buffer[BUF_LENGTH] = "";
        int readResult = 0;
    
        if(Serial.connected()) {
            printf("Baud Rate: %d, Byte Size: %d\n%s is connected\n", baud, ByteSize, comport);
        
            while(1) {
                readResult = Serial.read(buffer, BUF_LENGTH);
                buffer[readResult] = 0; //저장한 데이터 바로 뒤의 바이트를 초기화
                printf("%s", buffer);
            }
        }
        return 0;
    }

    2023.02.24 argument bug fix

    Makefile

    #사용할 컴파일러
    CC = g++
    #컴파일러에 전달할 argument
    CFLAGS = -O2 -Wall -std=c++17
    
    #소스+헤더파일 경로
    SOURCEDIR = src
    #빌드시 나올 오브젝트 파일과 실행 파일 저장위치
    BUILDDIR = build
    
    #실행파일 이름
    EXECUTABLE = serial.exe
    #실행파일 실행시 전달할 argument, 아무것도 없는 경우 RUNFLAG =
    RUNFLAG = -p COM3
    
    #main 함수가 정의된 파일 이름
    MAINSRC = main.cpp
    MAINFOBJ = $(BUILDDIR)/$(patsubst %.cpp,%.o,$(MAINSRC))
    
    #patsubst = pattern substitude
    SOURCES = $(wildcard $(SOURCEDIR)/*.cpp)
    OBJECTS = $(patsubst $(SOURCEDIR)/%.cpp,$(BUILDDIR)/%.o,$(SOURCES))
    
    #그냥 make 하면 make all로 인식됨
    all: dir $(BUILDDIR)/$(EXECUTABLE)
    
    #build 폴더가 없을 때만 폴더를 생성하도록 함
    dir: $(BUILDDIR)
    $(BUILDDIR):
    	mkdir -p $(BUILDDIR)
    
    #링크 과정
    $(BUILDDIR)/$(EXECUTABLE): $(OBJECTS) $(MAINFOBJ)
    	$(CC) $^ -o $@
    
    #소스파일 컴파일, 소스파일 안에 include로 헤더를 포함하기 때문에
    #소스파일만 컴파일 하면 됨, 다만 해더파일이 변경되었을 때도 새로 컴파일 하도록 종속성에 추가함
    $(OBJECTS): $(SOURCES) $(patsubst %.cpp,%.h,$(SOURCES))
    	$(CC) $< -c -o $@ $(CFLAGS)
    
    #메인 함수있는 파일 컴파일
    $(MAINFOBJ): $(MAINSRC)
    	$(CC) $(MAINSRC) -c -o $@ $(CFLAGS)
    
    #build 폴더내 오브젝트 파일과 실행파일 비우기
    clean:
    	rm -f $(BUILDDIR)/*o $(BUILDDIR)/$(EXECUTABLE)
    
    #프로그램 실행
    run:
    	$(BUILDDIR)/$(EXECUTABLE) $(RUNFLAG)

    make 파일은 자동으로 종속성이 추가되도록 만들어 놔서 다른 프로젝트에서도 그대로 복붙해서 쓸 수 있다.

     

    사용은 실행파일 위치를 윈도우 환경변수에 추가하거나, avr-gcc 컴파일러 있는 위치에 복붙한 뒤 아래 명령어를 콘솔창에 입력하면 된다.(VScode인 경우 ctrl + `(탭 위에 있음)으로 쉽게 콘솔창을 열 수 있다.)

    serial -p COM3 -b 9600

    serial.exe
    0.10MB

    시리얼 연결이 끊기면 자동으로 프로그램이 종료되도록 할 수 있을 텐데, connected() 매서드가 리턴전에 현재 포트 상태를 확인하도록 할 방법을 모르겠다. ClearCommError 함수로 상태 핸들에 저장시키는건 알겠는데, 구조체에 어떤 변수가 무슨 값이어야 연결이 끊긴건지를 모른다. 

    참고로 터미널에서 프로세스가 실행 중일때 ctrl+c를 입력하면 화면상에서는 ^C가 입력되면서 종료된다.(git 콘솔에서는 안보인다.)

    그리고 VScode git 콘솔에서는 제대로 출력되는데, 그냥 git Bash에서 사용해서는 출력이 안보이다가, ctrl+c를 눌러서 종료하면 한번에 출력을 보여준다. (git 콘솔에서는 hello 예제를 실행할 때 endl대신 \n을 사용하면 제대로 안보이는 것으로 보아 버퍼를 비워줘야 출력이 되는 것 같다.)

    댓글

Designed by Tistory.