임베디드 소프트웨어/펌웨어 구현

DWIN DGUS HMI 개발 라이브러리

MachineJW 2025. 3. 2. 01:16

중국 DWIN 사의 HMI (Human Machine Interface)

1. Embedded System에서 HMI의 역할

HMI(Human-Machine Interface)는 사용자가 임베디드 시스템(Embedded System)과 상호작용할 수 있도록 하는 디스플레이, 터치스크린, 버튼, GUI 인터페이스 등을 의미한다.

임베디드 시스템은 주로 센서, 액추에이터, 마이크로컨트롤러(MCU) 등으로 구성되며, HMI를 통해 사용자에게 데이터를 표시하고, 제어 입력을 받을 수 있다. 즉, 여러 수동버튼을 하나의 화면으로 구성할 수 있다는 것이다.

비유하자면 개인용 컴퓨터에서 모니터를 통해 사용자와 시스템이 상호작용 하듯이 PLC에서도 HMI를 통해 데이터를 시각화하고 사용자 입력을 받을 수 있음.
개인용 컴퓨터에서 모니터 + 키보드 + 마우스가 사용자 인터페이스를 담당하듯, PLC의 HMI는 터치스크린, 버튼, 그래픽을 통해 사용자와 기계를 연결하는 역할을 한다.

 

2. HMI와 PLC 개발 방법의 차이

(1) GUI 처리를 HMI 에서 (전통적인 개발 방법)

HMI의 매커니즘에 따라 다를 수 있겠지만, 보통 아래와 같은 다이어그램 방식이 전통적으로 많이 사용되고 있다.

물론 매커니즘에 따라 다르지만. 보통은 위와 같은 방법을 많이 사용하여 HMI와 PLC간 상호작용을 구현한다.

 장점

 

  • 전용 하드웨어:
    HMI 전용 칩셋이나 프로세서가 그래픽 렌더링을 담당하여, GUI 성능이 우수함
  • 개발 용이성:
    GUI 디자이너 툴이나 전용 에디터를 통해 화면 디자인을 손쉽게 할 수 있음
  • PLC 부담 경감:
    PLC는 본연의 실시간 제어에 집중할 수 있어 시스템 안정성이 높아짐
  • 유연한 디자인:
    터치 인터페이스, 애니메이션, 고해상도 이미지를 쉽게 적용 가능

 

단점

  • 추가 비용:
    HMI 전용 장비가 필요하므로 시스템 비용이 추가됨
  • 통신 지연:
    PLC와 HMI 간의 데이터 교환은 별도의 통신 프로토콜(UART, Modbus 등)을 사용하여 약간의 지연이 발생할 수 있음
  • 시스템 복잡성:
    두 개의 독립된 시스템(PLC와 HMI)이 필요하므로 설계 및 통합 작업이 복잡할 수 있음

(2) GUI 처리를 PLC 프로세서에서

내장형 GUI 처리:
PLC에 탑재된 고성능 MCU나 SoC가 GUI 렌더링을 직접 수행
→ HMI 없이 디스플레이(터치스크린, TFT 등)를 직접 연결하여 사용자 인터페이스 구현

장점

  • 통합 시스템:
     한 개의 장치로 모든 기능(실시간 제어 + GUI)을 수행
  • 하드웨어 비용 절감:
    고성능 프로세서의 비용이 떨어지면서, 통합 솔루션 구현이 경제적일 수 있음
  • 빠른 응답 및 직접 제어:
    시스템 내부에서 GUI와 제어 로직이 통합되어 있어, 통신 지연 없이 빠른 반응이 가능
  • 유연한 커스터마이징:
    LVGL 같은 라이브러리를 통해 원하는 대로 UI를 커스터마이징할 수 있음

단점

  • 높은 연산 요구:
    GUI 렌더링을 직접 처리해야 하므로, CPU 및 메모리 자원 소모가 큼
    → 고성능 프로세서가 필요하며, 그렇지 않으면 실시간 제어에 영향을 줄 수 있음
  • 개발 시간 증가:
    GUI를 직접 코딩하고 디자인해야 하므로, 개발 시간이 오래 걸림
    → 개발자가 UI 디자인 감각을 갖추거나 별도의 디자이너와 협업 필요
  • 복잡한 유지보수:
    소프트웨어와 하드웨어가 통합되어 있으므로, 문제 발생 시 진단 및 수정이 어려울 수 있음

3. 중국 DWIN 회사의 HMI

https://www.dwin-global.com/ko/7inch-monitor/

 

China 7inch Monitor Supplier, Manufacturer - Dwin

7-Inch Monitor Supplier & Exporter in China - Quality Products Available Upgrade your viewing experience with the 7inch Monitor from Beijing Dwin Technology Co., Ltd. Designed for crisp and clear visuals, this monitor is perfect for a wide range of applica

www.dwin-global.com

보통 7인치 기준으로 HMI를 구매하려면 10만원 이상의 비용이 드는 것이 일반적인데, 중국 DWIN 사의 HMI는 3만 5천원 정도 비용으로 7인치 HMI를 구매할 수 있다. 성능도 직접 써보니 나쁘지 않은 것 같다. (터치감은 우리가 사용하는 스마트폰과 비교하면 안된다...ㅋㅋ)

HMI GUI 개발툴 "DGUS"와 개발 아키텍처 다이어그램

4. DWIN HMI 통신 개발 라이브러리

DWIN 사에서 친절하게도 전용 라이브러리를 개발하여 배포 중이다.

https://github.com/dwinhmi/DWIN_DGUS_HMI

 

GitHub - dwinhmi/DWIN_DGUS_HMI: Official Arduino Libabry for DWIN DGUS T5L HMI Displays

Official Arduino Libabry for DWIN DGUS T5L HMI Displays - dwinhmi/DWIN_DGUS_HMI

github.com

TTL 레벨과 RS232 레벨 둘 중에 개발 환경과 적합한 것을 납땜하여 선택할 수 있다.
TX2 , RX2 핀을 사용하여 통신 연결을 지원한다. (TX1과 RX1은 펌웨어 업로드시 UART 전용 채널)

(1) 클래스 생성자

#if defined(ESP32) // ESP32가 선언되었을때, 즉 ESP32 사용일때 생성자
    DWIN::DWIN(HardwareSerial& port, uint8_t receivePin, uint8_t transmitPin, long baud){
        port.begin(baud, SERIAL_8N1, receivePin, transmitPin);
        init((Stream *)&port, false);
    }
  • ESP32 환경 컴파일 시에는 HardwareSerial을 사용
  • RX, TX 핀을 설정하고, Serial.begin()으로 시리얼 통신을 초기화

(2) 통신 초기화 및 설정 (init ())

void DWIN::init(Stream* port, bool isSoft){
    this->_dwinSerial = port;
    this->_isSoft = isSoft;
}
  • DWIN HMI와의 시리얼 통신 객체를 Stream 타입으로 저장
  • _isSoft 변수로 SoftwareSerial을 사용하는지 여부를 저장

(3) DWIN HMI 펌웨어 버전 읽기 (getHWVersion())

double DWIN::getHWVersion(){  //  HEX(5A A5 04 83 00 0F 01)
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x04, CMD_READ, 0x00, 0x0F, 0x01};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer)); 
    delay(10);
    return readCMDLastByte();
}
  • DWIN HMI의 펌웨어(하드웨어) 버전 조회.
  • HMI의 펌웨어 버전을 읽기 위해 5A A5 04 83 00 0F 01 명령어를 전송
  • readCMDLastByte()를 사용하여 응답값을 읽음

(4) DWIN HMI 재시작 (restartHMI())

void DWIN::restartHMI(){  // HEX(5A A5 07 82 00 04 55 aa 5a a5 )
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x07, CMD_WRITE, 0x00, 0x04, 0x55, 0xaa, CMD_HEAD1, CMD_HEAD2};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer)); 
    delay(10);
    readDWIN();
}
  • DWIN HMI를 재시작하는 5A A5 07 82 00 04 55 aa 5a a5 명령어를 전송
  • 시리얼 통신을 통해 HMI가 재부팅됨

(5) DWIN HMI의 밝기 변경, 조회  ( setBrightness() , getBrightness() )

void DWIN::setBrightness(byte brightness){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x04, CMD_WRITE, 0x00, 0x82, brightness };
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    readDWIN();
}

byte DWIN::getBrightness(){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x04, CMD_READ, 0x00, 0x31, 0x01 };
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    return readCMDLastByte();
}
  • DWIN HMI의 밝기(0x00 0x82 주소)에 값을 쓰는 명령어를 전송하여 밝기를 조절함.
  • HMI의 현재 밝기를 getBrightness()로 읽을 수 있음.

(6) DWIN HMI에서 페이지 변경, 조회 ( setPage() , getPage() )

void DWIN::setPage(byte page){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x07, CMD_WRITE, 0x00, 0x84, 0x5A, 0x01, 0x00, page};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    readDWIN();
}

byte DWIN::getPage(){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x04, CMD_READ, 0x00 , 0x14, 0x01};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer)); 
    return readCMDLastByte();
}
  • HMI의 특정 페이지로 변경하는 명령어(5A A5 07 82 00 84 5A 01 00 XX)를 전송
  • setPage(1);을 호출하면 페이지가 1로 변경됨
  • HMI의 현재 페이지를 0x00 0x14 주소에서 읽어옴

(7) DWIN HMI  변수 주소 VP 에 값 쓰기 ( setVP() )

DWIN HMI 사의 메모리맵에는 VP(변수 주소)와 SP(상수 주소) 설정이 존재한다.

void DWIN::setVP(long address, byte data){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x05 , CMD_WRITE, (address >> 8) & 0xFF, (address) & 0xFF, 0x00, data};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    readDWIN();
}
  • 해당 클래스 함수는 HMI에서 설정한 VP 주소 값에 데이터를 전송한다. (PLC -> HMI )
  • 5A A5 05 82 XX XX 00 YY 명령어를 전송
  • XX XX → 메모리 주소, YY → 저장할 값
  • 예를 들면 HMI에서 데이터 표시부를 하나 생성하고 VP 3x10으로 지정했다고 가정했을때 그 해당 주소값으로 데이터를 전송하면 데이터 표시부의 데이터가 바뀌는 샘이다. (C/C++ 포인터의 개념과 비슷하다... 가르키는 것은 주소값이며 실제 데이터는 다르다.)
  • HMI의 특정 주소(VP)에 값을 설정하는 명령어를 전송
  • 예제: setVP(0x2000, 42); → 0x2000 주소에 42(0x2A) 저장

(8) DWIN HMI  텍스트 설정 ( setText() )

void DWIN::setText(long address, String textData){
    int dataLen = textData.length();
    byte startCMD[] = {CMD_HEAD1, CMD_HEAD2, dataLen+3 , CMD_WRITE, 
    (address >> 8) & 0xFF, (address) & 0xFF};
    byte dataCMD[dataLen];textData.getBytes(dataCMD, dataLen+1);
    byte sendBuffer[6+dataLen];

    memcpy(sendBuffer, startCMD, sizeof(startCMD));
    memcpy(sendBuffer+6, dataCMD, sizeof(dataCMD));

    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    readDWIN();
}
  • HMI의 특정 텍스트 필드에 문자열을 설정하는 함수
  • 5A A5 XX 82 XX XX 명령어를 전송하여 텍스트 저장

(9) DWIN HMI  이벤트 감지 (listenEvents())

void DWIN::listen(){
     handle();
}

String DWIN::handle(){

    int lastByte;
    String response;
    String address;
    String message;
    bool isSubstr = false;
    bool messageEnd = true;
    bool isFirstByte = false;
    unsigned long startTime = millis(); 
  
    while((millis() - startTime < READ_TIMEOUT)){
        while(_dwinSerial->available() > 0){
            delay(10);
            int inhex = _dwinSerial->read();  // 시리얼 데이터를 읽음
            if (inhex == 90 || inhex == 165){  // 패킷 헤더(5A A5) 감지
                isFirstByte = true;
                response.concat(checkHex(inhex)+" ");
                continue;
            }
            for(int i = 1 ; i <= inhex ;i++){
                int inByte = _dwinSerial->read();
                response.concat(checkHex(inByte)+" ");
                if (i <= 3){
                    if((i == 2) || (i == 3)){
                        address.concat(checkHex(inByte));  // 이벤트 발생 주소 저장
                    }
                    continue;
                }
                else{
                    if(messageEnd){
                        if (isSubstr && inByte != MAX_ASCII && inByte >= MIN_ASCII){
                            message += char(inByte);
                        }
                        else{
                            if(inByte == MAX_ASCII){
                                messageEnd = false;
                            }
                            isSubstr = true;
                        }
                    }
                }
                lastByte = inByte;  // 마지막 수신 데이터 저장
            }
        }
    }

    if (isFirstByte && _echo){
        Serial.println("Address : " + address + " | Data : " + String(lastByte, HEX) + 
                       " | Message : " + message + " | Response " + response );
    }
    if (isFirstByte){
        listenerCallback(address, lastByte, message, response); // 등록된 콜백 실행
    }
    return response;
}

// SET CallBack Event
void DWIN::hmiCallBack(hmiListener callBack){
    listenerCallback = callBack;
}

// 1. 사용자가 hmiCallBack(onDwinEvent)을 통해 이벤트 핸들러 등록
// 2. loop()에서 listen()이 실행되며, handle()을 통해 이벤트 감지
// 3. HMI에서 버튼이 눌리거나 슬라이더 값이 변경되면, UART 데이터가 MCU로 전송됨
// 4. handle()이 데이터를 분석하고 listenerCallback()을 실행
// 5. 사용자가 등록한 onDwinEvent()가 실행되며, 이벤트를 처리함
  • DWIN HMI에서 발생하는 이벤트(버튼 클릭, 슬라이더 변경, 데이터 입력 등)를 실시간으로 감지하고 처리하는 함수 (HMI -> PLC)
  • HMI의 특정 변수(VP 주소)에 변화가 있거나, 입력 이벤트가 발생하면 MCU가 이를 감지하고 처리함
  • 사용자가 등록한 이벤트 핸들러(콜백 함수)를 호출하여, 이벤트를 실행할 수 있음
/* 이벤트 처리 함수 코드 예시 */
// 1. HMI의 VP 0x3000에 슬라이더 값이 저장되면, MCU가 이를 감지하여 PWM 값을 조절
// 2. 슬라이더 값(0~100%)을 0~255로 변환하여 모터 속도를 조절

void onDwinEvent(String address, byte lastByte, String message, String response) {
    if (address == "3000") {  // 슬라이더 값이 저장된 VP 주소
        int speed = lastByte;  // 슬라이더 값 (0~100)
        Serial.println("모터 속도 변경: " + String(speed) + "%");
        analogWrite(5, map(speed, 0, 100, 0, 255)); // PWM 출력 조절
    }
}

void setup() {
    dwin.hmiCallBack(onDwinEvent); // 이벤트 핸들러 등록
    pinMode(5, OUTPUT); // 모터 제어 핀
}

void loop() {
    dwin.listen(); // HMI 이벤트 감지
}
  • 사용자가 listenerCallback에 이벤트 처리 함수를 등록할 수 있도록 설정
  • HMI에서 이벤트가 발생하면, handle()이 자동으로 listenerCallback()을 호출

(10) DWIN HMI 부저 울리기

DWIN HMI에는 부저가 내장되어 있다.

void DWIN::beepHMI(){
    byte sendBuffer[] = {CMD_HEAD1, CMD_HEAD2, 0x05 , CMD_WRITE, 0x00, 0xA0, 0x00, 0x7D};
    _dwinSerial->write(sendBuffer, sizeof(sendBuffer));
    readDWIN();
}
  • DWIN HMI에서 부저를 울리는 명령어를 전송 (0x00A0 주소에 0x7D 값 저장)