[STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(라즈베리파이OS 환경)

2025. 12. 6. 18:50·프로젝트 작업기
반응형

* 이번 프로젝트 작업기는 포트폴리오용으로 쓰는 것이라 이전 프로젝트의 작업기보다 조금 더 자세히 쓴 글이라는 점을 참고 바랍니다.

 

 

[STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(라즈베리파이OS 환경)

* 이번 프로젝트 작업기는 포트폴리오용으로 쓰는 것이라 이전 프로젝트의 작업기보다 조금 더 자세히 쓴 글이라는 점을 참고 바랍니다. 1. 프로젝트 기술 스택언어: C, C++, QML플랫폼: STM32, ESP32, R

gun-ny.tistory.com

 

[Buildroot] 빌드루트로 임베디드 리눅스를 구축하여 라즈베리파이5에 올리기

[STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기* 이번 프로젝트 작업기는 포트폴리오용으로 쓰는 것이라 이전 프로젝트의 작업기보다 조금 더 자세히 쓴 글이라는 점

gun-ny.tistory.com

 

[Buildroot] 빌드루트를 올린 라즈베리파이5에서 Qt 애플리케이션 실행

[STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기* 이번 프로젝트 작업기는 포트폴리오용으로 쓰는 것이라 이전 프로젝트의 작업기보다 조금 더 자세히 쓴 글이라는 점

gun-ny.tistory.com

 

[Buildroot/Qt/라즈베리파이] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(빌드루트 임베디드

[STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(라즈베리파이OS 환경)* 이번 프로젝트 작업기는 포트폴리오용으로 쓰는 것이라 이전 프로젝트의 작업기보다 조금 더 자

gun-ny.tistory.com

 

해당 게시글은 위 게시글과 순서대로 연결되어 있으며 첫번째 글이다.

 

1. 프로젝트 기술 스택


  • 언어: C, C++, QML
  • 플랫폼: STM32, ESP32, Raspberry Pi
  • 프레임워크: STM32CubeIDE, VS Code, ESP-IDF, Qt(Qt Creator/Qt Design Studio)
  • 프로토콜: CAN, UART, GPIO
  • 환경: Linux(Raspberry Pi), Bare-metal(STM32)

 

2. 프로젝트 목표


  • 차량 아날로그 및 CAN 데이터를 STM32에서 수신 및 해석 → UART로 Raspberry Pi에 전달
  • Raspberry Pi에서 Qt GUI 애플리케이션(C++/QML)으로 데이터 시각화
  • 임베디드와 애플리케이션을 연동하여 실시간 그래픽 반응 구현

 

3. 프로젝트 서론


예전부터 어떤 대상을 제어하는 것에 대해 흥미를 느껴 오토모티브, 스마트홈, 스마트팜, 드론에 관심이 많았었다.

 

그 중 우리 생활에 가장 밀접해 있으면서 내가 가장 흥미를 느끼고 있는 오토모티브, 즉 자동차를 제어하는 것에 가장 흥미를 느껴 현재까지 주로 자동차에 대한 프로젝트만 진행하고 있다.

 

이번에는 자동차를 제어하면 그 결과나 상태를 눈으로 볼 수 있게 표출해주는 계기판으로 관련해서 프로젝트를 진행해 보려고 한다.

 

참고로 직접 해본다고 하면 우선 이 게시글을 전부 읽어본 뒤에 이 글을 참고하여 해보는걸 추천한다.

 

 

[QT] QQmlContext(setContextProperty 함수)를 이용하여 qml 객체의 속성 값을 C++ 클래스 멤버변수에 대입하

C++과 qml이 상호작용 하는 방법 중 하나인 setContextProperty 함수를 이용한 방법이다. Button을 클릭(onClicked)하면 main.cpp에서 setContextProperty 함수로 등록한 Indicator 클래스의 슬롯 sendSignal이 호출되고 Sen

gun-ny.tistory.com

 

[QT] qmlRegisterType을 이용하여 qml 객체의 속성 값을 C++ 클래스 멤버변수에 대입하는 방법

전의 QQmlContext를 이용한 방법과 코드만 조금 다를뿐이지 차이점을 잘..... 모르겠다.. 클래스를 라이브러리화 해도 cpp에서 include 해주느냐 qml에서 import 해주느냐 차이지 아직의 나로서는..ㅠ 아..

gun-ny.tistory.com

계기판 관련 프로젝트는 이번이 첫 시도가 아니다.

 

임베디드 개발을 막 시작하려는 때에 Qt 프레임워크로 안드로이드 애플리케이션을 만드려고 했었다.

애플리케이션을 아두이노와 바인딩하여 차량 계기판 인디게이터(좌우측 깜빡이)를 제어하고 상태를 표시하려고 했었는데 당시 내 경험으로는 한계가 있었고 위 2개의 게시물을 남기고 프로젝트를 무기한 연기하게 되었다.

 

그로부터 시간이 흘러 다시 프로젝트를 진행하려고 한다.

그런데 현재는 잘 사용하지 않는 아두이노를 이용하여 진행하는 것은 별도움이 될 거 같지 않아 당시 계획했던 그대로 진행하지 않고 계획을 살짝 바꿔 현재까지 내가 쌓아온 기술 스택으로만 진행을 해보려고 한다.

 

프로젝트의 목표를 요약하자면 아날로그 계기판을 디지털 계기판으로 만드는 것이다.

아날로그 계기판이 받는 차량의 데이터를 STM32로 받아 해석을 하여 라즈베리파이로 전송하고 해당 데이터를 기반으로 GUI 애플리케이션이 실시간으로 데이터를 시각화 시켜주는 것을 목표로 한다.

 

이쯤에서 드는 생각을 긍정적으로 보면 다양한 기술 스택으로 프로젝트를 진행한다는 것이고, 부정적으로 보면 "STM32에서 한번에 처리하면 되는거 아니야?"라고 생각할 수 있다.

 

맞는 말이다.

LVGL, TouchGFX, Qt for MCU 등의 프레임워크를 통해 개발하면 라즈베리파이 없이 Bare-metal단에서 처리가 가능하다.

결론은 경험 부족이지만 핑계를 조금 더하자면 라즈베리파이에 아직 익숙해지지 않아 익숙해지려고 조금 꼬아서(?) 진행하려는 것도 없지 않아 있다.

 

개발자는 죽을 때까지 공부하여 성장해야 하니 나중에 더 경험이 쌓여 더 많은 기술들을 다룰 수 있는 상태가 되면 Bare-metal단에서 재밌는 GUI 프로젝트를 만들어보면 좋을거 같다.

 

4-1. 프로젝트 과정(Raspberry Pi - Qt GUI Application 구현)


4-1. Qt Design Studio에서 UI QML 수정을 통해 UI 변경


4-1-1. ClusterTutorial 예제 프로젝트 불러오기


UI를 처음부터 만들기에는 시간이 오래 걸릴거 같아 Qt의 계기판 예제인 "ClusterTutorial"을 다듬어 진행하는 쪽으로 정했다.

우선 Qt Design Studio로 UI QML을 수정하여 테스트 차량 계기판에 맞게 UI 항목을 바꿔 CMake 프로젝트로 Export를 하고 Qt Creator에서 기존 자바스크립트와의 바인딩을 C++로 변경하여 백엔드를 구성하고 UI쪽을 마무리 하려고 한다.

 

확장자 참고

  • .ui.qml : QML의 프론트엔드(Qt Design Studio 생성)
  • .qml : QML의 백엔드

 

UI QML을 수정하여 UI를 변경해보도록 하자

테스트 차량 계기판의 역할을 하려면 기존 UI에서 위와 같은 내용이 수정되어야 한다.

 

참고로 Qt Quick에 대한 QML 레퍼런스 가이드는 아래 Qt 레퍼런스 페이지를 통해 참고하면 좋다.

 

 

Qt Quick QML Types | Qt Quick | Qt 6.10.1

 

doc.qt.io

 

4-1-2. 인디게이터 아이콘 제작


우선 좌우깜빡이 인디게이터로 심볼 변경부터 해보자

기존 예제를 시뮬레이션 해보면 엔진 경고등과 주유등은 깜빡이지만 나머지는 깜빡이지 않는다.

깜빡이지 않는 아이콘들은 단일 리소스 파일이라 따로 이미지를 만들어 연결해줘야 한다.

 

위와 같은 과정을 거쳐 인디게이터의 리소스 파일이 되어줄 이미지를 만들었다.

참고로 필자는 챗GPT에게 이미지 심볼을 요청하여 받고 추가로 가공하여 만들었다.

 

4-1-3. 인디게이터 심볼 변경 및 아이콘 연결


Iso_195_156.ui.qml


더보기
        IsoIcon {
            id: engineIcon
            x: 0
            y: 0
            active: Data.Values.engineTemp
            engineIconOffSource: "icons/engineIconOff.png"
            engineIconOnSource: "icons/engineIconOn.png"
        }

        IsoIcon {
            id: seatbeltIcon
            x: 0
            y: 0
            active: Data.Values.seatbelt
            engineIconOffSource: "icons/seatbeltIconOff.png"
            engineIconOnSource: "icons/seatbeltIconOn.png"
        }

        IsoIcon {
            id: parkingBrakeIcon
            x: 0
            y: 0
            active: Data.Values.parkingBrake
            engineIconOffSource: "icons/parkingBrakeIconOff.png"
            engineIconOnSource: "icons/parkingBrakeIconOn.png"
        }

        IsoIcon {
            id: leftIndicatorIcon
            x: 0
            y: 0
            // engineIconOffSource: "icons/parkingLightIcon.png"
            active: Data.Values.leftIndicator
            engineIconOffSource: "icons/leftIconOff.png"
            engineIconOnSource: "icons/leftIconOn.png"
        }

        IsoIcon {
            id: rightIndicatorIcon
            x: 0
            y: 0
            // engineIconOffSource: "icons/iceIcon.png"
            active: Data.Values.rightIndicator
            engineIconOffSource: "icons/rightIconOff.png"
            engineIconOnSource: "icons/rightIconOn.png"
        }

        IsoIcon {
            id: absIcon
            x: 0
            y: 0
            active: Data.Values.abs
            engineIconOffSource: "icons/absIconOff.png"
            engineIconOnSource: "icons/absIconOn.png"
        }

        IsoIcon {
            id: fuelIcon
            x: 0
            y: 0
            active: Data.Values.fuelLevel
            engineIconOnSource: "icons/fuelIconOn.png"
            engineIconOffSource: "icons/fuelIconOff.png"
        }

그리고 Iso_195_156.ui.qml 파일을 위와 같이 수정하여 각 인디게이터들을 깜빡일 수 있게 위에서 만든 이미지 파일을 연결해준다.

 

그럼 인디게이터 부분이 위와 같이 변경된 것을 볼 수 있다.

이후 C++ 바인딩을 하고 시뮬레이션을 돌려보면 모든 아이콘들이 깜빡이는 것을 볼 수 있을 것이다.

 

4-1-4. 5단계 기어 포지션(1, 2, 3, 4, 5) -> 7단계 기어 포지션(1, 2, 3, P, R, N, D) 변경


다음은 기존 1단에서 5단까지 5단계로 구성되어 있던 기어 포지션을 1단에서 3단, P R N D단, 총 7단계의 기어 포지션으로 구성할 것이다.

 

Gearbox_195_196.ui.qml


더보기
    Image {
        id: gear_7
        x: 0
        y: 0
        source: "assets/gearbox_visual.png"
    }

    Image {
        id: gear_6
        x: 0
        y: 0
        source: "assets/gearbox_visual.png"
    }
    // *
    // *
    Image {
        id: gear_5
        // x: -8
        x: 0
        y: 0
        // source: "assets/gearbox_visual_195_150.png"
        source: "assets/gearbox_visual.png"
    }

    Image {
        id: gear_4
        x: 0
        y: 0
        // source: "assets/gearbox_visual_4_195_179.png"
        source: "assets/gearbox_visual.png"
    }

    Image {
        id: gear_3
        x: 0
        y: 0
        // y: 9
        // source: "assets/gearbox_visual_3_195_181.png"
        source: "assets/gearbox_visual.png"
    }

    Image {
        id: gear_2
        x: 0
        y: 0
        // source: "assets/gearbox_visual_2_195_185.png"
        source: "assets/gearbox_visual.png"
    }

    Image {
        id: gear_1
        x: 0
        y: 0
        // y: 9
        // source: "assets/gearbox_visual_1_195_187.png"
        source: "assets/gearbox_visual.png"
    }

    states: [
        State {
            name: "gear1"
            when: gearbox.currentGear === 1

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }

            PropertyChanges {
                target: current_gear
                text: "1"
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 0
            }

            PropertyChanges {
                target: gear_7
                opacity: 0
            }
            // *
            // *
        },
        State {
            name: "gear2"
            when: gearbox.currentGear === 2

            PropertyChanges {
                target: current_gear
                text: "2"
            }

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_2
                opacity: 1
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 0
            }

            PropertyChanges {
                target: gear_7
                opacity: 0
            }
            // *
            // *
        },
        State {
            name: "gear3"
            when: gearbox.currentGear === 3

            PropertyChanges {
                target: current_gear
                text: "3"
            }

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }

            PropertyChanges {
                target: gear_3
                opacity: 1
            }
        },
        State {
            name: "gear4"
            when: gearbox.currentGear === 4

            PropertyChanges {
                target: current_gear
                // text: "4"
                text: "P"
            }

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 1
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 0
            }

            PropertyChanges {
                target: gear_7
                opacity: 0
            }
            // *
            // *
        },
        State {
            name: "gear5"
            when: gearbox.currentGear === 5

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }

            PropertyChanges {
                target: current_gear
                // text: "5"
                text: "R"
            }

            PropertyChanges {
                target: gear_5
                opacity: 1
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 0
            }

            PropertyChanges {
                target: gear_7
                opacity: 0
            }
            // *
            // *
        },
        // *
        // *
        State {
            name: "gear6"
            when: gearbox.currentGear === 6

            PropertyChanges {
                target: current_gear
                text: "N"
            }

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 1
            }

            PropertyChanges {
                target: gear_7
                opacity: 0
            }
            // *
            // *
        },
        State {
            name: "gear7"
            when: gearbox.currentGear === 7

            PropertyChanges {
                target: current_gear
                text: "D"
            }

            PropertyChanges {
                target: gear_2
                opacity: 0
            }

            PropertyChanges {
                target: gear_5
                opacity: 0
            }

            PropertyChanges {
                target: gear_1
                opacity: 0
            }

            PropertyChanges {
                target: gear_4
                opacity: 0
            }

            PropertyChanges {
                target: gear_3
                opacity: 0
            }
            // *
            // *
            PropertyChanges {
                target: gear_6
                opacity: 0
            }

            PropertyChanges {
                target: gear_7
                opacity: 1
            }
            // *
            // *
        }
        // *
        // *
    ]

Gearbox_195_196.ui.qml 파일을 위와 같이 수정하여 총 7단계의 기어 포지션으로 구성해준다.

 

그럼 위와 같이 기어 포지션 구성에 P R N D단이 추가된 것을 확인 할 수 있다.

이것도 마찬가지로 C++ 바인딩 후 시뮬레이션을 돌려보면 확인 할 수 있다.

 

4-1-5. KPL 단위 -> MPH 단위 변경


그리고 "KPL"이라고 적힌 부분을 "MPH"로 바꿔 시속을 마일 단위로도 디스플레이 해줄 것이다.

 

Speed_dial_195_151.ui.qml


더보기
    Text {
        id: kpl_readout_195_118
        x: 277
        y: 371
        color: "#FFFFFF"
        text: "MPH"
        font.weight: Font.ExtraLight
        font.pixelSize: 32
        font.family: "IBM Plex Mono"
    }

Speed_dial_195_151.ui.qml 파일을 위와 같이 수정한다.

15.5라고 적힌 부분과 더불어 140이라고 적힌 부분은 각각 C++ 바인딩 후 시속을 마일과 키로 단위로 환산하여 QML로 데이터를 넘겨줄 것이다.

 

4-1-6. 주유량 UI QML 속성값 확인


마지막으로 주유 관련 UI에서 따로 UI QML을 수정해줄 필요는 없다.

C++ 바인딩 이후 백엔드에서 해당 값들을 계산하여 QML로 데이터를 넘겨줄 것이기 때문이다.

"LITERS"에 디스플레이 되는 "20"은 그대로 주유량(최대 주유량 35L)을 디스플레이,

"KILOMETERS"에 디스플레이 되는 "200"은 현재 주유량에 평균 연비를 계산해서 남은 주행거리를 디스플레이,

게이지는 주유량(0~110L)이 아닌 주유량 비율(0~110%)를 디스플레이 할 것이다.

 

4-2. Qt Design Studio(.qmlproject) 프로젝트 CMake 프로젝트로 Export


이렇게 File → Export Project → Enable CMake Generator 하여 Qt Design Studio의 qml 프로젝트를 Qt Creator에서 C++로 작업할 수 있게 CMake 프로젝트로 만들어 준다.

그럼 Qt Design Studio에서 할 일은 끝이다.

 

다음은 C++ 바인딩을 위해 Qt Creator에서 작업을 해야한다.

나중에 STM32에서 UART를 수신하기 위해서는 라즈베리파이의 GPIO 드라이버를 사용해야 하니 이때부터는 라즈베리파이에서 Qt Creator 작업을 하는 것을 권장한다.

 

4-3. Qt Creator에서 QML과 C++(Backend) 바인딩


4-3-1. main.cpp에 "com.Backend" qml 컴포넌트 등록 후 backend 객체와 연결


라즈베리파이에서 Qt Design Studio에서 Export 했던 CMake 프로젝트를 Qt Creator로 불러온다.

라즈베리파이에서 Qt 개발환경 구성까지 다루기에는 내용이 너무 방대해질거 같아 따로 다루지 않겠다.

 

Terminal


sudo apt install qml6-module-qt5compat-graphicaleffects qt6-shadertools-dev qml6-module-qtquick-timeline qt6-quicktimeline-dev

  • qml6-module-qt5compat-graphicaleffects
  • qt6-shadertools-dev
  • qml6-module-qtquick-timeline
  • qt6-quicktimeline-dev

대신 Qt Design Studio에서 가져온 "ClusterTutorial" 프로젝트의 경우 위 컴포넌트들이 있어야 하니 터미널을 통해 패키지를 설치해야 한다.

 

main.cpp


..
#include "backend.h"
..
..
    Backend backend;
    qmlRegisterSingletonInstance("com.Backend", 1, 0, "Backend", &backend);
..

C++ 백엔드를 구성하기 위해 main.cpp에 위와 같이 소스코드를 추가 해준다.

qmlRegisterType으로 qml컴포넌트를 등록해줘도 되지만 백엔드 객체는 하나만 있는게 바람직하여 싱글톤 타입으로 등록 해주도록 한다.

 

4-3-2. backend 객체의 원형인 Backend 클래스 구현


그리고 qml컴포넌트에 등록할 백엔드를 구현해주기 위해 백엔드 클래스를 만들어 backend.cpp 파일과 backend.h 파일을 프로젝트에 추가 하기로 한다.

과정은 아래 설명 하도록 하겠다.

 

위 과정을 통해 main.cpp와 같은 App 폴더내에 backend.cpp와 backend.h를 생성한다.

 

여담으로 클래스를 생성하면서 체크했던 "Add QML_ELEMENT"를 체크하면 위에서 프로젝트를 만들고 main.cpp에 추가했던 qmlRegister 관련 구문을 추가하지 않아도 된다고 한다. 하지만 인텔리센스 버그인지 구문 오류라고 인식한다.

결과적으로는 체크하면 main.cpp에 qmlRegister를 해주지 않아도 기능이 동작하기는 한다.

 

backend.h


더보기
#ifndef BACKEND_H
#define BACKEND_H

#include <QObject>
#include <QQmlEngine>
#include <QDebug>
#include <QString>
#include <QStringList>

#include "uart.h"

class Backend : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
public:
    explicit Backend(QObject *parent = nullptr);

    enum valueType {
        Rpm,
        Kph,
        Liters,
        CurrentGear,
        Indicator
    };

    Q_INVOKABLE void print();

    Q_INVOKABLE void getValue(valueType type);

    Q_INVOKABLE void rpmSimulationTimer();
    Q_INVOKABLE void kphSimulationTimer();
    Q_INVOKABLE void litersSimulationTimer();
    Q_INVOKABLE void indicatorSimulationTimer();

    Q_PROPERTY(int rpm READ rpm WRITE setRpm NOTIFY rpmChanged FINAL);
    int rpm() const;
    void setRpm(int newRpm);

    Q_PROPERTY(QString displayRpm READ displayRpm WRITE setDisplayRpm NOTIFY displayRpmChanged FINAL);
    QString displayRpm() const;
    void setDisplayRpm(const QString &newDisplayRpm);

    Q_PROPERTY(double kph READ kph WRITE setKph NOTIFY kphChanged FINAL);
    double kph() const;
    void setKph(double newKph);

    Q_PROPERTY(QString displayKph READ displayKph WRITE setDisplayKph NOTIFY displayKphChanged FINAL);
    QString displayKph() const;
    void setDisplayKph(const QString &newDisplayKph);

    Q_PROPERTY(QString displayKpl READ displayKpl WRITE setDisplayKpl NOTIFY displayKplChanged FINAL);
    QString displayKpl() const;
    void setDisplayKpl(const QString &newDisplayKpl);

    Q_PROPERTY(int liters READ liters WRITE setLiters NOTIFY litersChanged FINAL);
    int liters() const;
    void setLiters(int newLiters);

    Q_PROPERTY(QString displayLiters READ displayLiters WRITE setDisplayLiters NOTIFY displayLitersChanged FINAL);
    QString displayLiters() const;
    void setDisplayLiters(const QString &newDisplayLiters);

    Q_PROPERTY(QString displayRange READ displayRange WRITE setDisplayRange NOTIFY displayRangeChanged FINAL);
    QString displayRange() const;
    void setDisplayRange(const QString &newDisplayRange);

    Q_PROPERTY(int currentGear READ currentGear WRITE setCurrentGear NOTIFY currentGearChanged FINAL);
    int currentGear() const;
    void setCurrentGear(int newCurrentGear);

    Q_PROPERTY(bool engineTemp READ engineTemp WRITE setEngineTemp NOTIFY engineTempChanged FINAL);
    bool engineTemp() const;
    void setEngineTemp(bool newEngineTemp);

    Q_PROPERTY(bool seatbelt READ seatbelt WRITE setSeatbelt NOTIFY seatbeltChanged FINAL);
    bool seatbelt() const;
    void setSeatbelt(bool newSeatbelt);

    Q_PROPERTY(bool parkingBrake READ parkingBrake WRITE setParkingBrake NOTIFY parkingBrakeChanged FINAL);
    bool parkingBrake() const;
    void setParkingBrake(bool newParkingBrake);

    Q_PROPERTY(bool leftIndicator READ leftIndicator WRITE setLeftIndicator NOTIFY leftIndicatorChanged FINAL);
    bool leftIndicator() const;
    void setLeftIndicator(bool newLeftIndicator);

    Q_PROPERTY(bool rightIndicator READ rightIndicator WRITE setRightIndicator NOTIFY rightIndicatorChanged FINAL);
    bool rightIndicator() const;
    void setRightIndicator(bool newRightIndicator);

    Q_PROPERTY(bool abs READ abs WRITE setAbs NOTIFY absChanged FINAL);
    bool abs() const;
    void setAbs(bool newAbs);

    Q_PROPERTY(bool fuelLevel READ fuelLevel WRITE setFuelLevel NOTIFY fuelLevelChanged FINAL);
    bool fuelLevel() const;
    void setFuelLevel(bool newFuelLevel);

signals:
    void rpmChanged();
    void displayRpmChanged();
    void kphChanged();
    void displayKphChanged();
    void displayKplChanged();
    void litersChanged();
    void displayLitersChanged();
    void displayRangeChanged();
    void currentGearChanged();
    void engineTempChanged();
    void seatbeltChanged();
    void parkingBrakeChanged();
    void leftIndicatorChanged();
    void rightIndicatorChanged();
    void absChanged();
    void fuelLevelChanged();

private:
    Uart* uart;

    int m_rpm;
    QString m_displayRpm;
    double m_kph;
    QString m_displayKph;
    QString m_displayKpl;
    int m_liters;
    QString m_displayLiters;
    QString m_displayRange;
    int m_currentGear;
    bool m_engineTemp;
    bool m_seatbelt;
    bool m_parkingBrake;
    bool m_leftIndicator;
    bool m_rightIndicator;
    bool m_abs;
    bool m_fuelLevel;
};

#endif // BACKEND_H

 

backend.cpp


더보기
#include "backend.h"

Backend::Backend(QObject *parent)
    : QObject{parent}
{
    uart = new Uart(this);
    uart->openPort("/dev/serial0");

    setRpm(0);
    setDisplayRpm("0");
    setKph(0);
    setDisplayKph("0");
    setDisplayKpl("0");
    setLiters(0);
    setDisplayLiters("0");
    setDisplayRange("0");
    setCurrentGear(4);

    setEngineTemp(false);
    setSeatbelt(false);
    setParkingBrake(false);
    setLeftIndicator(false);
    setRightIndicator(false);
    setAbs(false);
    setFuelLevel(false);
}

void Backend::print()
{
    qDebug() << "print Function Called";
}

void Backend::getValue(valueType type)
{
    // Example: ///500///58///15///1///0101010///qt_gunny
    QString message = uart->getMessage();

    if (!message.endsWith("qt_gunny")) {
        return;
    }

    QStringList tokens = message.split("///", Qt::SkipEmptyParts);

    if (type == Rpm) {
        setRpm(tokens.value(Rpm).toInt());
        setDisplayRpm(QString::number(tokens.value(Rpm).toInt() * 10));
    }

    if (type == Kph) {
        setKph(tokens.value(Kph).toInt());
        setDisplayKph(QString::number(tokens.value(Kph).toInt()));
        setDisplayKpl(QString::number(qRound(tokens.value(Kph).toInt() * 0.6214)));
        setCurrentGear(tokens.value(CurrentGear).toInt());
    }

    if (type == Liters) {
        setLiters((1100 / 33) * tokens.value(Liters).toInt());
        setDisplayLiters(QString::number(tokens.value(Liters).toInt()));
        setDisplayRange(QString::number(tokens.value(Liters).toInt() * 12));
    }

    if (type == Indicator) {
        QList<int> bits;
        for (QChar ch : tokens.value(Indicator)) {
            bits.append(QString(ch).toInt());
        }
        setEngineTemp(bits.value(0, 0));
        setSeatbelt(bits.value(1, 0));
        setParkingBrake(bits.value(2, 0));
        setLeftIndicator(bits.value(3, 0));
        setRightIndicator(bits.value(4, 0));
        setAbs(bits.value(5, 0));
        setFuelLevel(bits.value(6, 0));
    }
}

void Backend::rpmSimulationTimer()
{
    // 由ъ뀑
    if (rpm() > 1100) {
        setRpm(0);
        setDisplayRpm("0");
    }

    // 利앷?
    setRpm(rpm() + 1);
    setDisplayRpm(QString::number(rpm()));
}

void Backend::kphSimulationTimer()
{
    // 由ъ뀑
    if (kph() >= 200) {
        setKph(0);
        setDisplayKph("0");
    }

    // 利앷?
    setKph(kph() + 1);
    setDisplayKph(QString::number(kph()));
    setDisplayKpl(QString::number(qRound(kph() * 0.6214)));
}
void Backend::litersSimulationTimer()
{
    // 由ъ뀑
    if (displayLiters().toInt() > 33) {
        setDisplayLiters("0");
        setDisplayRange("0");
        setLiters(0);
    }

    // 利앷?
    setDisplayLiters(QString::number(displayLiters().toInt() + 1));
    setDisplayRange(QString::number(displayLiters().toInt() * 12));
    setLiters((1100 / 33) * displayLiters().toInt());
}

void Backend::indicatorSimulationTimer()
{
    // 由ъ뀑
    if (currentGear() == 7) {
        setCurrentGear(0);
    }

    // 利앷?
    setCurrentGear(currentGear() + 1);

    // ?좉?
    setEngineTemp(!engineTemp());
    setSeatbelt(!seatbelt());
    setParkingBrake(!parkingBrake());
    setLeftIndicator(!leftIndicator());
    setRightIndicator(!rightIndicator());
    setAbs(!abs());
    setFuelLevel(!fuelLevel());
}

int Backend::rpm() const
{
    return m_rpm;
}

void Backend::setRpm(int newRpm)
{
    if (m_rpm == newRpm)
        return;
    m_rpm = newRpm;
    emit rpmChanged();
}

QString Backend::displayRpm() const
{
    return m_displayRpm;
}

void Backend::setDisplayRpm(const QString &newDisplayRpm)
{
    if (m_displayRpm == newDisplayRpm)
        return;
    m_displayRpm = newDisplayRpm;
    emit displayRpmChanged();
}

double Backend::kph() const
{
    return m_kph;
}

void Backend::setKph(double newKph)
{
    if (m_kph == newKph)
        return;
    m_kph = newKph;
    emit kphChanged();
}

QString Backend::displayKph() const
{
    return m_displayKph;
}

void Backend::setDisplayKph(const QString &newDisplayKph)
{
    if (m_displayKph == newDisplayKph)
        return;
    m_displayKph = newDisplayKph;
    emit displayKphChanged();
}

QString Backend::displayKpl() const
{
    return m_displayKpl;
}

void Backend::setDisplayKpl(const QString &newDisplayKpl)
{
    if (m_displayKpl == newDisplayKpl)
        return;
    m_displayKpl = newDisplayKpl;
    emit displayKplChanged();
}

int Backend::liters() const
{
    return m_liters;
}

void Backend::setLiters(int newLiters)
{
    if (m_liters == newLiters)
        return;
    m_liters = newLiters;
    emit litersChanged();
}

QString Backend::displayLiters() const
{
    return m_displayLiters;
}

void Backend::setDisplayLiters(const QString &newDisplayLiters)
{
    if (m_displayLiters == newDisplayLiters)
        return;
    m_displayLiters = newDisplayLiters;
    emit displayLitersChanged();
}

QString Backend::displayRange() const
{
    return m_displayRange;
}

void Backend::setDisplayRange(const QString &newDisplayRange)
{
    if (m_displayRange == newDisplayRange)
        return;
    m_displayRange = newDisplayRange;
    emit displayRangeChanged();
}

int Backend::currentGear() const
{
    return m_currentGear;
}

void Backend::setCurrentGear(int newCurrentGear)
{
    if (m_currentGear == newCurrentGear)
        return;
    m_currentGear = newCurrentGear;
    emit currentGearChanged();
}

bool Backend::engineTemp() const
{
    return m_engineTemp;
}

void Backend::setEngineTemp(bool newEngineTemp)
{
    if (m_engineTemp == newEngineTemp)
        return;
    m_engineTemp = newEngineTemp;
    emit engineTempChanged();
}

bool Backend::seatbelt() const
{
    return m_seatbelt;
}

void Backend::setSeatbelt(bool newSeatbelt)
{
    if (m_seatbelt == newSeatbelt)
        return;
    m_seatbelt = newSeatbelt;
    emit seatbeltChanged();
}

bool Backend::parkingBrake() const
{
    return m_parkingBrake;
}

void Backend::setParkingBrake(bool newParkingBrake)
{
    if (m_parkingBrake == newParkingBrake)
        return;
    m_parkingBrake = newParkingBrake;
    emit parkingBrakeChanged();
}

bool Backend::leftIndicator() const
{
    return m_leftIndicator;
}

void Backend::setLeftIndicator(bool newLeftIndicator)
{
    if (m_leftIndicator == newLeftIndicator)
        return;
    m_leftIndicator = newLeftIndicator;
    emit leftIndicatorChanged();
}

bool Backend::rightIndicator() const
{
    return m_rightIndicator;
}

void Backend::setRightIndicator(bool newRightIndicator)
{
    if (m_rightIndicator == newRightIndicator)
        return;
    m_rightIndicator = newRightIndicator;
    emit rightIndicatorChanged();
}

bool Backend::abs() const
{
    return m_abs;
}

void Backend::setAbs(bool newAbs)
{
    if (m_abs == newAbs)
        return;
    m_abs = newAbs;
    emit absChanged();
}

bool Backend::fuelLevel() const
{
    return m_fuelLevel;
}

void Backend::setFuelLevel(bool newFuelLevel)
{
    if (m_fuelLevel == newFuelLevel)
        return;
    m_fuelLevel = newFuelLevel;
    emit fuelLevelChanged();
}

위와 같이 backend.h와 backend.cpp 파일을 작성해주면 Backend 클래스의 정의 및 구현을 마친 것이다.

 

현재까지 과정을 통해 main.cpp에서 선언한 "Backend" 클래스의 정의 및 구현을 마쳤고 "Backend" 클래스를 통해 생성된 "backend" 객체를 qmlRegisterSingletonInstance 함수로 이후에 qml에서 "com.Backend"라는 명칭으로 컴포넌트로 import하여 C++의 기능들을 사용할 수 있게 되었다.

 

4-3-3. Backend 클래스에서 사용할 UART 통신부 Uart 클래스 구현


이제 "Backend" 클래스 안에 선언되어 있는 UART 관련 클래스를 정의 및 구현을 해줄 차례이다.

위에서 잠깐 언급했듯이 라즈베리파이 GPIO 드라이버를 통해 UART를 수신할 것이다.

 

그런데 어떤 방법으로 수신을 하냐?

라즈베리파이에서 공식으로 지원하는 커널 API를 통해 UART를 수신할 수 있지만 Qt에서는 자체적으로 UART를 송수신 할 수 있는 컴포넌트를 제공한다.

 

Terminal


sudo apt install qt6-serialport-dev

위에서 Qt Design Studio에서 가져온 "ClusterTutorial" 프로젝트의 경우 4가지의 추가 컴포넌트 패키지를 설치해야 한다고 했다.

여기서 Qt에서 제공하는 UART 송수신 기능을 사용하기 위해서는 위 명령어를 통해 SerialPort 컴포넌트 패키지를 설치하면 된다.

 

CMakeLists.txt


find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Qml Quick QuickTimeline ShaderTools SerialPort)

target_link_libraries(${CMAKE_PROJECT_NAME}
  PRIVATE Qt6::Core
          Qt6::Gui
          Qt6::Widgets
          Qt6::Qml
          Qt6::Quick
          Qt6::QuickTimeline
          Qt6::ShaderTools
          Qt6::SerialPort
)

그리고 SerialPort 컴포넌트의 경우 따로 끌어서 쓰는거니 CMake에 SerialPort 컴포넌트를 사용한다고 위와 같이 따로 명시를 해줘야한다.

 

Core에서 ShaderTools 컴포넌트까지의 경우 Qt Design Studio에서 Export를 할 때 CMake Generator을 하면서 이미 명시가 되었기 때문에 위에 4가지 컴포넌트를 설치 후 따로 명시를 안해줘도 됐었던거다.

 

이로써 백엔드에서 UART 통신을 구현할 수 있게 되었다.

백엔드 클래스 생성때와 동일하게 UART 클래스도 만들어 본다.

 

백엔드 클래스 생성때와 같이 똑같이 생성하는데 한가지 차이점이라고 하면 "Add QML_ELEMENT"를 체크하지 않아도 된다.

UART 클래스는 qml에서 사용하지 않고 백엔드에서만 처리할 것이기 때문이다.

UART 클래스는 데이터를 수신받아 백엔드에 넘겨주기만 하고 백엔드에서 데이터를 처리하고 QML로 넘겨주는 절차이다.

 

Uart.h


더보기
#ifndef UART_H
#define UART_H

#include <QObject>
#include <QQmlEngine>
#include <QtSerialPort/QSerialPort>
#include <QDebug>
#include <QString>

class Uart : public QObject
{
    Q_OBJECT
    // QML_ELEMENT
    // QML_SINGLETON
public:
    explicit Uart(QObject *parent = nullptr);

    bool openPort(const QString &device);
    void closePort();
    void sendMessage(const QString &msg);
    void receiveMessage(const QString &newMsg);
    QString getMessage();

signals:
    void messageReceived(const QString &msg);

private slots:
    void onReadyRead();

private:
    QSerialPort m_serial;
    QByteArray m_buffer;
    QString m_msg;
};

#endif // UART_H

 

Uart.cpp


더보기
#include "uart.h"

Uart::Uart(QObject *parent)
    : QObject{parent}
{
    connect(&m_serial, &QSerialPort::readyRead, this, &Uart::onReadyRead);
}

bool Uart::openPort(const QString &device)
{
    if (m_serial.isOpen()) {
        m_serial.close();
    }

    m_serial.setPortName(device);
    m_serial.setBaudRate(QSerialPort::Baud115200);
    m_serial.setDataBits(QSerialPort::Data8);
    m_serial.setParity(QSerialPort::NoParity);
    m_serial.setStopBits(QSerialPort::OneStop);

    if (!m_serial.open(QIODevice::ReadWrite)) {
        qCritical() << "UART open failed: " << m_serial.errorString();
        return false;
    }

    qInfo() << "UART opened on " << device;
    return true;
}

void Uart::closePort()
{
    if (m_serial.isOpen()) {
        m_serial.close();
        qInfo() << "UART closed";
    }
}

void Uart::sendMessage(const QString &msg)
{
    if (m_serial.isOpen()) {
        m_serial.write(msg.toUtf8());
        qInfo() << "[Tx]: " << msg;
    } else {
        qWarning() << "UART send message failed";
    }
}

void Uart::receiveMessage(const QString &newMsg)
{
    if (m_msg == newMsg)
        return;
    m_msg = newMsg;
    emit messageReceived(newMsg);
}

QString Uart::getMessage()
{
    return m_msg;
}

void Uart::onReadyRead()
{
    QByteArray raw = m_serial.readAll();
    m_buffer.append(raw);

    int idx;
    while ((idx = m_buffer.indexOf('\n')) != -1) {
        QByteArray line = m_buffer.left(idx);
        m_buffer.remove(0, idx + 1);

        if (line.endsWith('\r'))
            line.chop(1);

        QString text = QString::fromUtf8(line);
        qInfo() << "[Rx]: " << text;

        receiveMessage(text);
    }
}

그리고 생성된 Uart.h 파일과 Uart.cpp 파일을 위와 같이 작성해주면 백엔드에서 UART 송수신까지 할 수 있게 된다.

 

4-3-4. 라즈베리파이 Serial Port 활성화 후 기존 디버그핀에서 GPIO핀으로 할당


그런데 나중에 UART 송수신이 되지않는 문제를 겪을 것이다.

필자의 경우 라즈베리파이5인데 이와 같이 GPIO핀을 통해 UART를 송수신 하려면 OS에서 따로 설정을 해줄것이 있다.

 

Terminal


ls -l /dev/serial*

위 명령어를 입력하면 "ttyAMA10"으로 표시될 것이다.

 

"ttyAMA10"은 아래 3핀 디버그핀으로 연결되어 있으며 위 GPIO핀(빨간색 박스)로 연결하기 위해 "ttyAMA0"으로 바꿔줄 것이다.

 

우선 설정을 통해 "Serial Port"를 활성화 시켜주고 온전한 하드웨어 Sirial 통신을 위해 "Serial Console"는 비활성화 시켜주도록 한다.

 

Terminal


sudo mousepad /boot/firmware/config.txt

그리고 위 명령어를 통해 펌웨어 설정 텍스트 파일을 열도록 한다.

참고로 sudo를 통해 관리자 권한으로 열어야 한다. 아니면 읽기 전용으로 열게되어 파일을 수정할 수 없다.

 

Config.txt


[all]
dtparam=uart0=on
dtoverlay=uart0
enable_uart=1

아마 맨 아래쪽 [all] 부분에 "dtparam=uart0=on" 설정만 표기되어 있을 것이다. 위와 같이 수정해주면 된다.

 

그리고 재부팅을 하게되면 "ttyAMA0"으로 변경된 것을 볼 수 있을 것이다.

 

여기까지 하여 백엔드에서 UART 통신을 할 수 있게 되었다.

 

4-3-5. Values.qml 수정으로 QML - C++(com.Backend) 바인딩


이제 라즈베리파이 Qt 애플리케이션 구성 마지막으로 qml에 백엔드 컴포넌트를 연결하여 qml에서 자바스크립트와 연결된 속성들을 백엔드와 연결해주면 된다.

 

Values.qml


더보기
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Design Studio.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

pragma Singleton
import QtQuick 2.10
// import "simulation.js" as JS

// import ClusterTutorial
import com.Backend

QtObject {
    id: values

    /* tachometer dial values */
    property int rpm: Backend.rpm // controls the needle / arc position
    property string displayRpm: Backend.displayRpm

    /* speedometer dial values */
    property real kph: Backend.kph /* controls the needle / arc position */
    property string displayKph: Backend.displayKph
    property string displayKpl: Backend.displayKpl

    /* fuel gauge dial values */
    property int liters: Backend.liters /* controls the needle / arc position */
    property string displayLiters: Backend.displayLiters
    property string displayRange: Backend.displayRange

    /* current gear */
    property int currentGear: Backend.currentGear

    /* Iso Icons Boolean Values */
    property bool engineTemp: Backend.engineTemp
    property bool seatbelt: Backend.seatbelt
    property bool parkingBrake: Backend.parkingBrake
    property bool leftIndicator: Backend.leftIndicator
    property bool rightIndicator: Backend.rightIndicator
    property bool abs: Backend.abs
    property bool fuelLevel: Backend.fuelLevel

    /* State change bool */
    property bool booting: true
    readonly property real bootDuration: 5000

    property Timer bootTimer: Timer{
        running: true
        repeat: false
        onTriggered: values.booting = false
        interval: bootDuration
    }

    property Timer rpmTimer: Timer{
        running: !values.booting
        repeat: true
        // onTriggered: Backend.rpmSimulationTimer()
        onTriggered: Backend.getValue(0)
        interval: 5
    }

    property Timer kphTimer: Timer{
        running: !values.booting
        repeat: true
        // onTriggered: Backend.kphSimulationTimer()
        onTriggered: Backend.getValue(1)
        interval: 5
    }

    property Timer litersTimer: Timer{
        running: !values.booting
        repeat: true
        // onTriggered: Backend.litersSimulationTimer()
        onTriggered: Backend.getValue(2)
        interval: 5
    }

    property Timer indicatorTimer: Timer{
        running: !values.booting
        repeat: true
        // onTriggered: Backend.indicatorSimulationTimer()
        onTriggered: Backend.getValue(4)
        interval: 200
    }

    // property Timer testTimer: Timer{
    //     running: !values.booting
    //     repeat: true
    //     onTriggered: Backend.print()
    //     interval: 1000
    // }
}

"import "simulation.js" as JS" -> "import com.Backend"

기존 자바스크립트와 바인딩 되어있던 것을 C++로 바인딩 하였다.

 

"property string displayRpm: JS.displayRpm" -> "property string displayRpm: Backend.displayRpm"

위와 같이 자바스크립트의 변수에서 받아왔던 속성 값을 C++의 백엔드 객체의 멤버 변수(*)에서 받도록 하였다.

(* 정확히는 멤버 함수이다. 객체지향의 캡슐화 특성상 변수 값은 함수로 받아오기 때문이다.)

 

    property Timer rpmTimer: Timer{
        running: !values.booting
        repeat: true
        // onTriggered: Backend.rpmSimulationTimer()
        onTriggered: Backend.getValue(0)
        interval: 5
    }

그리고 Timer 컴포넌트를 통해 5ms 마다 백엔드 객체의 멤버 함수를 지속적으로 호출한다.

이로써 UI는 실시간으로 백엔드로부터 상태 값을 받아올 수 있게 되었다.

 

빌드 후 실행을 해보면 위와 같은 상태를 볼 수 있다.

 

현재까지의 과정 요약 - 1 (UART 수신측 라즈베리파이 Qt 애플리케이션 데이터 시각화 구현)


  • Qt Design Studio에서 UI QML 수정을 통해 UI 변경
  •  - ClusterTutorial 예제 프로젝트 불러오기
  •  - 깜빡이 아이콘 제작
  •  - 주차등 -> 좌측 깜빡이 인디게이터 심볼 변경 및 아이콘 연결
  •  - 결빙등 -> 우측 깜빡이 인디게이터 심볼 변경 및 아이콘 연결
  •  - 벨트경고등, 주차브레이크등, ABS등 아이콘 연결
  •  - 5단계 기어 포지션(1, 2, 3, 4, 5) -> 7단계 기어 포지션(1, 2, 3, P, R, N, D) 변경
  •  - KPL 단위 -> MPH 단위 변경
  •  - 주유량 UI QML 속성값 확인
  • Qt Design Studio(.qmlproject) 프로젝트 CMake 프로젝트로 Export
  • Qt Creator에서 QML과 C++(Backend) 바인딩
  •  - main.cpp에 "com.Backend" qml 컴포넌트 등록 후 backend 객체와 연결
  •  - backend 객체의 원형인 Backend 클래스 구현
  •  - Backend 클래스에서 사용할 UART 통신부 Uart 클래스 구현
  •  - 라즈베리파이 Serial Port 활성화 후 기존 디버그핀에서 GPIO핀으로 할당
  •  - Values.qml 수정으로 QML - C++(com.Backend) 바인딩

이렇게 라즈베리파이가 STM32로부터 UART를 수신할 준비가 되었고 그 데이터를 토대로 Qt 애플리케이션에서 데이터를 계기판 UI로 시각화를 시켜줄 준비를 마쳤다.

 

이제 STM32에서 차량 계기판의 아날로그 전압값과 CAN 데이터를 해석하여 UART를 통해 라즈베리파이로 송신해주는 쪽을 구현해줄 차례이다.

 

4-4. 프로젝트 과정(STM32 - 계기판 데이터(아날로그 및 CAN) 수신 및 해석 구현)


4-4. GPIO, UART, CAN, DMA 하드웨어가 포함된 STM32F103RB Nucleo-64 개발보드 선택


STM32F103RB Nucleo-64 (출처 : STM32)

이번 프로젝트에 사용할 STM32 보드는 STM32F103 MCU를 사용하는 Nucleo-64 개발보드로 진행하게 된다.

 

STM32F103RB MCU 데이터시트 중 일부 (출처 : STM32)

사용할 하드웨어로는 GPIO, UART(USART), CAN 정도만 있어 무난한 칩을 탑재하고 있는 보드이다.

 

 

STM32F103RB | Product - STMicroelectronics

The STM32F103xx medium-density performance line family incorporates the high-performance Arm® Cortex®-M3 32-bit RISC core operating at a 72 MHz frequency,...

www.st.com

 

 

NUCLEO-F103RB | Product - STMicroelectronics

The STM32 Nucleo-64 board provides an affordable and flexible way for users to try out new concepts and build prototypes by choosing from the various...

www.st.com

자세한 내용은 위 ST사의 레퍼런스 페이지에서 확인 할 수 있다.

 

이제 계기판 데이터를 가지고 올 차량을 살펴보록 하자

 

4-5. 계기판 데이터 추출 핀 지정


TA모닝 계기판 (출처 : 기아)

이번에도 어김없이 등장하는 영원한(?) 테스트카 TA모닝이다.

 

RPM, 속도, 주유량, 주유등, ODO정보, ECO 인디게이터, 주차 브레이크등, 에어백 경고등, 벨트 경고등, 엔진 경고등, 오일등 등.. 각종 경고등을 표출한다.

 

위에 계기판 UI를 변경한것 처럼 해당 계기판에서 사용할 정보는 RPM, 속도, 주유량, 주차 브레이크등, 벨트 경고등, 깜빡이 인디게이터등 데이터를 이용하여 Qt UI로 표출할 것이다.

해당 프로젝트에서 오일등과 ABS등은 미구현으로 구현하지 않는다.

 

TA모닝 계기판 커넥터 핀맵 (출처 : 기아GSW)

우선 계기판에 어떤 핀으로 어떤 형태로 데이터들이 들어가는지 알아야 한다.

 

  • 1. W/B 파킹 브레이크 IND
  • 4. BR 운전석 시트 벨트 스위치
  • 10. B 접지
  • 16. L 우측 방향 지시등
  • 18. G 좌측 방향 지시등
  • 26. P 차속 신호
  • 34. G/O GSL 연료

접지 제외 총 7개의 신호선을 찾아야 하는데 RPM을 찾지 못해 6개 밖에 찾질 못했다.

 

위와 같이 다른 신호선들을 찍어보면서 "26번핀인 충전 IND 신호선이 알터네이터 회전수에 대한 PWM 신호가 나오는건가?" 생각하고 찍어봤으나 아니였다.

 

TA모닝 계기판 회로도 (출처 : 기아GSW)

회로도를 더 살펴보던 중 계기판 내부에 C-CAN 트랜시버가 있는것을 보았고 그 옆에 속도계, 타코미터, 연료게이지가 MICOM하고 직접 연결되어 있는 것을 보았다.

 

위 3가지의 정보를 CAN으로 주고 받는거 같아 C-CAN을 찍어보기로 하였다.

 

 

[Library] CAN통신 모니터링 라이브러리

예전에 차량의 CAN, KLINE, LIN 통신 값을 읽어들이기 위해 구성해봤던 프로토타입이다. 그 프로토타입을 이렇게 샘플로 만들었었다. 그런데 최근에 ESP32를 더 많이 활용하고 있고 기존에 만들었던

gun-ny.tistory.com

이전에 ESP32로 CAN 데이터를 모니터링 할 수 있게 만든 라이브러리가 있어 이번에 다시 활용해보기로 한다.

 

이전에는 기어 포지션까지만 찾아내지 못했는데 RPM 값도 찾은거 같다.

의미있는 결과이나 16진수로 표기되어 있어 특정 수식을 통해 표기값을 풀어줘야 한다.

 

 

GitHub - BogGyver/opendbc: democratize access to car decoder rings

democratize access to car decoder rings. Contribute to BogGyver/opendbc development by creating an account on GitHub.

github.com

 

https://canlogger.csselectronics.com/dbc-editor/v133/dbc-editor.html

Signal preview only supported for bit lengths < 54 bits Data = 0b = 0x = Physical value = * + =

canlogger.csselectronics.com

이걸 직접 풀어내기에는 시간적 낭비라 이미 공개되어 있는 opendbc에서 현대기아 공통 C-CAN dbc 파일을 받아 canlogger 사이트에 업로드하여 C-CAN 프로토콜이 어떻게 되어있는지 보기로 한다.

 

dbc RPM 영역
dbc 속도 영역
dbc 주유량 영역

RPM, 속도, 주유량 순서로

 

RPM 0x316(790) 인덱스 2, 3 (2바이트)

속도 0x316(790) 인덱스 6

주유량 0x329(809) 인덱스 7

위치에 있는 것을 알 수 있었고

 

RPM = data * 0.25

속도 = data

주유량 = data * 0.1

위와 같은 결과가 해석된 데이터 값인 것을 알 수 있다.

 

  • 1. W/B 파킹 브레이크 IND
  • 4. BR 운전석 시트 벨트 스위치
  • 10. B 접지
  • 16. L 우측 방향 지시등
  • 18. G 좌측 방향 지시등
  • 28. R/O C-CAN(High)
  • 29. L/O C-CAN(Low)

그럼 접지 제외 6개의 핀에서 유효한 값이 나오는지 확인하면 된다.

 

C-CAN에서 RPM, 속도, 주유량 유효한 데이터 값을 구할 수 있었고,

파킹 브레이크등, 벨트 경고등 (OFF상태 : VBAT, ON상태 : GND)

좌측 방향지시등, 우측 방향지시등 (OFF상태 : GND, ON상태 : VBAT) 상태를 관찰할 수 있었다.

 

그럼 STM32와 자동차를 공통 접지로 묶고 RPM, 속도, 주유량은 CAN으로 데이터를 받고 나머지 파킹 브레이크등, 벨트 경고등, 좌우측 방향지시등은 GPIO로 데이터를 받아 가공한 뒤 UART 송신을 통해 라즈베리파이에 데이터를 넘겨주는 절차로 진행하면 된다.

 

4-6. STM32 설정 및 코딩


4-6-1. 클럭 설정


우선 STM32F103RB Nucleo-64 보드 기준으로 프로젝트를 생성해준다.

STM32 프로젝트에서 코드 생성은 STM32CubeMX로 진행되며 직접 수정하는 파일은 main.cpp 파일 하나다.

 

우선 클럭 설정부터 확인해보자

필자는 내부클럭만 사용하여 위와 같이 설정을 해놓은 상태이다.

 

4-6-2. 디버깅용 printf 함수 재정의


main.cpp


더보기
/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart2, (uint8_t*) ptr, len, HAL_MAX_DELAY);
return len;
}

/* USER CODE END 0 */

그리고 나중에 디버깅 용도로 printf 함수를 호출하면 자동으로 시리얼 모니터에 문자열을 띄울 수 있도록 위와 같이 CubxMX로 ioc 파일을 설정해주고 main.cpp의 USER CODE BEGIN 0 부분에 위 소스코드를 추가해준다.

 

main.cpp


더보기
/* USER CODE BEGIN Header */
/**
 ******************************************************************************
 * @file           : main.c
 * @brief          : Main program body
 ******************************************************************************
 * @attention
 *
 * Copyright (c) 2025 STMicroelectronics.
 * All rights reserved.
 *
 * This software is licensed under terms that can be found in the LICENSE file
 * in the root directory of this software component.
 * If no LICENSE file comes with this software, it is provided AS-IS.
 *
 ******************************************************************************
 */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h> // strlen

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
CAN_HandleTypeDef hcan;

TIM_HandleTypeDef htim1;

UART_HandleTypeDef huart1;
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart1_tx;

/* USER CODE BEGIN PV */
// CAN 관련 변수
CAN_RxHeaderTypeDef CAN_RxHeaderStruct;
uint8_t can_rx_data[8];

// UART 관련 변수
char uart_tx_buf[128];

// 임계 구역 보호용 플래그 변수
volatile _Bool busy_flag;

// 계기판 관련 변수
typedef struct {
uint16_t rpm;
uint8_t speed;
uint8_t liters;
uint8_t curruntGear;

union {
struct {
_Bool engineTemp :1; // 미구현
_Bool seatbelt :1;
_Bool parkingBrake :1;
_Bool leftIndicator :1;
_Bool rightIndicator :1;
_Bool abs :1; // 미구현
_Bool fuel :1;
_Bool reserved :1;
};
uint8_t flags;
};
} CLUSTER_StatusTypeDef;
volatile CLUSTER_StatusTypeDef CLUSTER_StatusStruct;

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_CAN_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_TIM1_Init(void);
/* USER CODE BEGIN PFP */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan);
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart2, (uint8_t*) ptr, len, HAL_MAX_DELAY);
return len;
}

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_CAN_Init();
  MX_USART2_UART_Init();
  MX_USART1_UART_Init();
  MX_TIM1_Init();
  /* USER CODE BEGIN 2 */
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

HAL_TIM_Base_Start_IT(&htim1);

sprintf(uart_tx_buf, "///%d///%d///%d///%d///0%d%d%d%d00///qt_gunny\n",
CLUSTER_StatusStruct.rpm, CLUSTER_StatusStruct.speed,
CLUSTER_StatusStruct.liters, CLUSTER_StatusStruct.curruntGear,
CLUSTER_StatusStruct.seatbelt, CLUSTER_StatusStruct.parkingBrake,
CLUSTER_StatusStruct.leftIndicator,
CLUSTER_StatusStruct.rightIndicator);
HAL_UART_Transmit_DMA(&huart1, (uint8_t*) uart_tx_buf,
(uint16_t) strlen(uart_tx_buf));

printf("Initialization Succeeded...\r\n");

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
while (1) {

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
}
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief CAN Initialization Function
  * @param None
  * @retval None
  */
static void MX_CAN_Init(void)
{

  /* USER CODE BEGIN CAN_Init 0 */

  /* USER CODE END CAN_Init 0 */

  /* USER CODE BEGIN CAN_Init 1 */

  /* USER CODE END CAN_Init 1 */
  hcan.Instance = CAN1;
  hcan.Init.Prescaler = 4;
  hcan.Init.Mode = CAN_MODE_NORMAL;
  hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
  hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
  hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
  hcan.Init.TimeTriggeredMode = DISABLE;
  hcan.Init.AutoBusOff = ENABLE;
  hcan.Init.AutoWakeUp = ENABLE;
  hcan.Init.AutoRetransmission = ENABLE;
  hcan.Init.ReceiveFifoLocked = DISABLE;
  hcan.Init.TransmitFifoPriority = DISABLE;
  if (HAL_CAN_Init(&hcan) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN CAN_Init 2 */
/* CAN Filter */
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;

HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);

  /* USER CODE END CAN_Init 2 */

}

/**
  * @brief TIM1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM1_Init(void)
{

  /* USER CODE BEGIN TIM1_Init 0 */

  /* USER CODE END TIM1_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM1_Init 1 */

  /* USER CODE END TIM1_Init 1 */
  htim1.Instance = TIM1;
  htim1.Init.Prescaler = 640-1;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 1000-1;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 0;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM1_Init 2 */

  /* USER CODE END TIM1_Init 2 */

}

/**
  * @brief USART1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

/**
  * @brief USART2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART2_UART_Init(void)
{

  /* USER CODE BEGIN USART2_Init 0 */

  /* USER CODE END USART2_Init 0 */

  /* USER CODE BEGIN USART2_Init 1 */

  /* USER CODE END USART2_Init 1 */
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART2_Init 2 */

  /* USER CODE END USART2_Init 2 */

}

/**
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Channel4_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin : B1_Pin */
  GPIO_InitStruct.Pin = B1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : PARKING_GPIO_Pin SEATBELT_GPIO_Pin */
  GPIO_InitStruct.Pin = PARKING_GPIO_Pin|SEATBELT_GPIO_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pins : LEFTLD_GPIO_Pin RIGHTLD_GPIO_Pin */
  GPIO_InitStruct.Pin = LEFTLD_GPIO_Pin|RIGHTLD_GPIO_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pin : LD2_Pin */
  GPIO_InitStruct.Pin = LD2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);

  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == B1_Pin) {
printf("Interrupt Callback Test\r\n");
}
}

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
if (hcan->Instance == CAN1 && busy_flag == 0) {
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &CAN_RxHeaderStruct, can_rx_data);

if (CAN_RxHeaderStruct.StdId == 790) {
CLUSTER_StatusStruct.rpm = ((((uint16_t) can_rx_data[3] << 8) | can_rx_data[2]) * 0.25) / 10;
CLUSTER_StatusStruct.speed = can_rx_data[6];
}

if (CAN_RxHeaderStruct.StdId == 809) {
CLUSTER_StatusStruct.liters = (can_rx_data[7] * 0.1) + 10;
}

if (CAN_RxHeaderStruct.StdId == 1087) {
if (can_rx_data[1] == 64) {
CLUSTER_StatusStruct.curruntGear = 4; // P
} else if (can_rx_data[1] == 65) {
CLUSTER_StatusStruct.curruntGear = 1;
} else if (can_rx_data[1] == 66) {
CLUSTER_StatusStruct.curruntGear = 2;
} else if (can_rx_data[1] == 67) {
CLUSTER_StatusStruct.curruntGear = 3;
} else if (can_rx_data[1] == 69) {
CLUSTER_StatusStruct.curruntGear = 7; // D
} else if (can_rx_data[1] == 70) {
CLUSTER_StatusStruct.curruntGear = 6; // N
} else if (can_rx_data[1] == 71) {
CLUSTER_StatusStruct.curruntGear = 5; // R
} else {
CLUSTER_StatusStruct.curruntGear = 7; // D
}
}
}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1 && busy_flag == 0) {
if (HAL_GPIO_ReadPin(PARKING_GPIO_GPIO_Port, PARKING_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.parkingBrake = 0;
} else {
CLUSTER_StatusStruct.parkingBrake = 1;
}

if (HAL_GPIO_ReadPin(SEATBELT_GPIO_GPIO_Port, SEATBELT_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.seatbelt = 0;
} else {
CLUSTER_StatusStruct.seatbelt = 1;
}

if (HAL_GPIO_ReadPin(LEFTLD_GPIO_GPIO_Port, LEFTLD_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.leftIndicator = 1;
} else {
CLUSTER_StatusStruct.leftIndicator = 0;
}

if (HAL_GPIO_ReadPin(RIGHTLD_GPIO_GPIO_Port, RIGHTLD_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.rightIndicator = 1;
} else {
CLUSTER_StatusStruct.rightIndicator = 0;
}
}
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1 && busy_flag == 0) {
busy_flag = 1;

sprintf(uart_tx_buf, "///%d///%d///%d///%d///0%d%d%d%d00///qt_gunny\n",
CLUSTER_StatusStruct.rpm, CLUSTER_StatusStruct.speed,
CLUSTER_StatusStruct.liters, CLUSTER_StatusStruct.curruntGear,
CLUSTER_StatusStruct.seatbelt,
CLUSTER_StatusStruct.parkingBrake,
CLUSTER_StatusStruct.leftIndicator,
CLUSTER_StatusStruct.rightIndicator);
HAL_UART_Transmit_DMA(&huart1, (uint8_t*) uart_tx_buf,
(uint16_t) strlen(uart_tx_buf));

busy_flag = 0;
}
}

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1) {
}
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

참고로 필자는 main.cpp 파일을 위와 같이 작성하였으니 이후에 위 소스코드를 참고하면 좋을거 같다.

 

GPIO는 위와 같이 설정해준다. GPIO 인터럽트는 따로 사용하지 않을 것이다.

파킹 브레이크등과 벨트 경고등은 LOW 신호에서 점등되니 Pull-up 모드로,

좌우측 깜빡이등은 HIGH 신호에서 점등되니 Pull-down 모드로 설정한다.

 

STM32 GPIO 레퍼런스 문서는 아래 링크를 참고하면 도움이 된다.

 

Getting started with GPIO - stm32mcu

Main navigation contains tabs, main links and MediaWiki sidebar

wiki.st.com

 

main.cpp


더보기
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
더보기
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1 && busy_flag == 0) {
if (HAL_GPIO_ReadPin(PARKING_GPIO_GPIO_Port, PARKING_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.parkingBrake = 0;
} else {
CLUSTER_StatusStruct.parkingBrake = 1;
}

if (HAL_GPIO_ReadPin(SEATBELT_GPIO_GPIO_Port, SEATBELT_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.seatbelt = 0;
} else {
CLUSTER_StatusStruct.seatbelt = 1;
}

if (HAL_GPIO_ReadPin(LEFTLD_GPIO_GPIO_Port, LEFTLD_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.leftIndicator = 1;
} else {
CLUSTER_StatusStruct.leftIndicator = 0;
}

if (HAL_GPIO_ReadPin(RIGHTLD_GPIO_GPIO_Port, RIGHTLD_GPIO_Pin) == GPIO_PIN_SET) {
CLUSTER_StatusStruct.rightIndicator = 1;
} else {
CLUSTER_StatusStruct.rightIndicator = 0;
}
}
}
/* USER CODE END 4 */

타이머의 Prescaler와 Period를 위와 같이 설정하고 타이머의 업데이트 인터럽트를 켜준 다음, main.cpp에서 인터럽트 타이머를 시작해주는 함수인 HAL_TIM_Base_Start_IT와 weak 함수인 HAL_TIM_PeriodElapsedCallback 함수를 오버라이드하여 설정한 타이머에 도달했을 때 인터럽트가 발생하여 파킹 브레이크등, 시트 벨트등, 좌우측 깜빡이등의 상태를 변수로 가져올 수 있게 작성해준다.

 

T = 64,000,000(64MHz) / 640(Prescaler) / 1000(Period) = 100Hz(10ms)

타이머의 Prescaler는 640, Period는 100으로 설정하였는데 이것은 현재 타이머의 클럭인 64MHz 속도에서 640 만큼 나누면 100KHz(10us)의 속도가 나온다. 이 속도로 1000번 카운트를 하는것이니 10ms마다 타이머가 다 돌게되어 인터럽트가 발생한다.

설정에서 값마다 -1을 한 이유는 해당 값들은 0부터 시작하기 때문이다.

 

이로써 타이머를 통해 10ms마다 인터럽트가 발생하여 10ms마다 파킹 브레이크등, 시트 벨트등, 좌우측 깜빡이의 현재 상태를 가져올 수 있게 되었다.

 

그런데 글을 쓰면서 든 생각이 타이머를 쓰지 않고 GPIO의 인터럽트를 켜서 인터럽트 발생조건을 Rising / Falling에 놓고 각 핀들의 상태가 바뀔때마다 해당 값을 가져오면 비용을 더 아끼지 않았을까라는 생각이 든다.

 

뭐 TIM로 구성했으니 그대로 TIM 인터럽트를 통해 각 핀들의 상태를 받아오는걸로 진행하기로 한다.

 

STM32 TIM 레퍼런스 문서도 아래 링크를 통해 참고하면 도움이 될 것이다.

 

Getting started with TIM - stm32mcu

Main navigation contains tabs, main links and MediaWiki sidebar

wiki.st.com

 

main.cpp


더보기
/* USER CODE BEGIN PV */
CAN_RxHeaderTypeDef CAN_RxHeaderStruct;
uint8_t can_rx_data[8];
/* USER CODE END PV */
더보기
  /* USER CODE BEGIN 2 */
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
  /* USER CODE END 2 */
더보기
  /* USER CODE BEGIN CAN_Init 2 */
/* CAN Filter */
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
  /* USER CODE END CAN_Init 2 */
더보기
/* USER CODE BEGIN 4 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
if (hcan->Instance == CAN1 && busy_flag == 0) {
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &CAN_RxHeaderStruct, can_rx_data);

if (CAN_RxHeaderStruct.StdId == 790) {
CLUSTER_StatusStruct.rpm = ((((uint16_t) can_rx_data[3] << 8) | can_rx_data[2]) * 0.25) / 10;
CLUSTER_StatusStruct.speed = can_rx_data[6];
}

if (CAN_RxHeaderStruct.StdId == 809) {
CLUSTER_StatusStruct.liters = (can_rx_data[7] * 0.1) + 10;
}

if (CAN_RxHeaderStruct.StdId == 1087) {
if (can_rx_data[1] == 64) {
CLUSTER_StatusStruct.curruntGear = 4; // P
} else if (can_rx_data[1] == 65) {
CLUSTER_StatusStruct.curruntGear = 1;
} else if (can_rx_data[1] == 66) {
CLUSTER_StatusStruct.curruntGear = 2;
} else if (can_rx_data[1] == 67) {
CLUSTER_StatusStruct.curruntGear = 3;
} else if (can_rx_data[1] == 69) {
CLUSTER_StatusStruct.curruntGear = 7; // D
} else if (can_rx_data[1] == 70) {
CLUSTER_StatusStruct.curruntGear = 6; // N
} else if (can_rx_data[1] == 71) {
CLUSTER_StatusStruct.curruntGear = 5; // R
} else {
CLUSTER_StatusStruct.curruntGear = 7; // D
}
}
}
}
/* USER CODE END 4 */

이제 CAN을 통해 RPM, 속도, 주유량의 데이터를 받을 수 있도록 위와 같이 설정을 한다.

비트 타이밍 매개변수들을 똑같이 설정해야 C-CAN 속도인 500kBps로 Baud Rate를 맞출 수 있다.

CAN 데이터를 수신이 완료되었을 때 알림을 받기 위해 인터럽트도 켜주도록 한다.

 

그리고 main.cpp 수정은 CAN 수신시작부, CAN 초기화부와 USER CODE부로 나눠서 넣어줘야 한다.

CAN에게 시작을 알리지 않으면, CAN을 초기화하면서 필터를 설정하지 않으면 데이터를 수신하지 못한다.

그리고 인터럽트 콜백함수에서 CAN데이터가 수신되었으면 각각의 변수들에 맞는 데이터를 수식을 통해 계산해서 넣어주면 된다.

 

이로써 GPIO를 설정하고, 타이머를 이용하여 GPIO의 상태들을 읽어 변수에 저장하고, CAN을 이용하여 각 변수에 맞는 데이터들을 계산해서 넣는 구조가 되었다. 이제 완성된 데이터를 UART를 통해 라즈베리파이로 보내주면 된다.

 

*CAN의 경우 ST 레퍼런스 페이지에 공식 문서가 많지않아 STM32F1 펌웨어 레파지토리 내에 있는 EVAL 보드의 CAN 예제를 참고 하였다.

 

main.cpp


더보기
/* USER CODE BEGIN Includes */
#include <string.h>
/* USER CODE END Includes */
더보기
/* USER CODE BEGIN PV */
char uart_tx_buf[128];

volatile _Bool busy_flag;

typedef struct {
uint16_t rpm;
uint8_t speed;
uint8_t liters;
uint8_t curruntGear;

union {
struct {
_Bool engineTemp :1; // 미구현
_Bool seatbelt :1;
_Bool parkingBrake :1;
_Bool leftIndicator :1;
_Bool rightIndicator :1;
_Bool abs :1; // 미구현
_Bool fuel :1;
_Bool reserved :1;
};
uint8_t flags;
};
} CLUSTER_StatusTypeDef;

volatile CLUSTER_StatusTypeDef CLUSTER_StatusStruct;
/* USER CODE END PV */
더보기
  /* USER CODE BEGIN 2 */
sprintf(uart_tx_buf, "///%d///%d///%d///%d///0%d%d%d%d00///qt_gunny\n",
CLUSTER_StatusStruct.rpm, CLUSTER_StatusStruct.speed,
CLUSTER_StatusStruct.liters, CLUSTER_StatusStruct.curruntGear,
CLUSTER_StatusStruct.seatbelt, CLUSTER_StatusStruct.parkingBrake,
CLUSTER_StatusStruct.leftIndicator,
CLUSTER_StatusStruct.rightIndicator);
HAL_UART_Transmit_DMA(&huart1, (uint8_t*) uart_tx_buf,
(uint16_t) strlen(uart_tx_buf));
  /* USER CODE END 2 */
더보기
/* USER CODE BEGIN 4 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1 && busy_flag == 0) {
busy_flag = 1;

sprintf(uart_tx_buf, "///%d///%d///%d///%d///0%d%d%d%d00///qt_gunny\n",
CLUSTER_StatusStruct.rpm, CLUSTER_StatusStruct.speed,
CLUSTER_StatusStruct.liters, CLUSTER_StatusStruct.curruntGear,
CLUSTER_StatusStruct.seatbelt,
CLUSTER_StatusStruct.parkingBrake,
CLUSTER_StatusStruct.leftIndicator,
CLUSTER_StatusStruct.rightIndicator);
HAL_UART_Transmit_DMA(&huart1, (uint8_t*) uart_tx_buf,
(uint16_t) strlen(uart_tx_buf));

busy_flag = 0;
}
}
/* USER CODE END 4 */

데이터 전송용 UART의 경우 인터럽트와 더불어 DMA도 활성화 시켜주도록 한다.

그리고 관련 변수들을 추가적으로 넣어주기로 한다.

busy_flag 변수의 경우 UART가 송신하려고 각 변수의 데이터를 읽어 대입하는 타이밍에 다른 인터럽트들이 그 변수에 쓰는걸 방지하기 위해 지정한 플래그 변수이다.

 

이렇게 하여 TIM를 이용하여 GPIO를 모니터링을 하고 CAN으로도 모니터링을 하여 계기판의 각 상태를 계기판 데이터 전용 구조체에 저장한 뒤 DMA로 해당 데이터들을 라즈베리파이 Qt 애플리케이션이 읽을 수 있게 정형화 시켜 지속적으로 보내게 된다.

 

글을 쓰면서 코드를 다시 살펴봤는데 DMA 인터럽트 단계에서 전송 버퍼를 정형화 시키지 않고 미리 버퍼를 만들어두면 비용을 더 줄일 수 있지 않을까라는 생각이 든다.

 

현재까지의 과정 요약 - 2 (UART 송신측 STM32 아날로그 및 CAN 데이터 수신 및 해석 구현)


  • GPIO, UART, CAN, DMA 하드웨어가 포함된 STM32F103RB Nucleo-64 개발보드 선택
  • 자동차측에서 데이터를 뽑을 핀 지정
  • STM32 설정 및 코딩
  •  - 클럭 설정
  •  - 디버깅용 printf 함수 재정의
  •  - GPIO 설정(파킹 브레이크등, 벨트 경고등, 좌우측 깜빡이등 모니터링용)
  •  - TIM 설정(10ms마다 GPIO핀 상태를 읽어들이기)
  •  - CAN 설정(RPM, 속도, 주유량 모니터링용)
  •  - UART1 및 DMA 설정(라즈베리파이 UART 전송용)

이제 라즈베리파이 Qt 애플리케이션과 STM32 하드웨어 구현이 끝났으니 각 선들을 연결해보도록 하자

 

4-7. 프로젝트 과정(Raspberry Pi - STM32 바인딩)


 

4-7. 자동차 - 회로 - STM32 - Raspberry Pi 배선도 요약


위 모든 과정의 사진들을 요약하면 위 사진과 같다.

 

C-CAN에 묶여있는 RPM, 속도, 주유량 데이터는 CAN 트랜시버를 통해 STM32가 읽을 수 있도록 변환되어 STM32로 넘어간다.

아날로그 신호인 파킹 브레이크등, 시트 벨트등, 좌측 깜빡이등, 우측 깜빡이등은 최대 전압이 15볼트까지 오를 수 있어 전압 분배 회로를 거쳐 저전압으로 변환되어 STM32로 넘어간다.

 

아래 이어서 전압분배에 대해 조금 더 이야기를 해보도록 하겠다.

 

전압 분배 공식 (출처 : Digikey)
15V 전압 분배 (출처 : Digikey)
12볼트 전압 분배 (출처 : Digikey)

필자는 4.7K 저항과 1K 저항을 사용하여 전압분배 회로를 구성하였다.

최대 전압으로 예상되는 15볼트에서 전압분배를 하게되면 이론상 2.63볼트가 나온다.

STM32에서 안정적으로 받을 수 전압인 3.3V내 범위이다.

 

그런데 동작 중 최소 전압으로 예상되는 12볼트에서는 2.1볼트가 나온다.

이건 MCU마다 다르지만 2.1볼트를 HIGH 인식해야 현재까지 구성한 모든것들이 올바르게 동작한다.

일단 동작을 하여 그대로 진행하기로 한다.

 

자동차는 서지 전압으로 인해 순간적으로 높은 전압이 인가되어 MCU가 망가질 수 있으니 전압 분배 회로로 구성하지 않고 Vgs 전압을 확인하여 N채널 MOSFET등으로 구성하여 반전된 신호를 HIGH LOW 신호로 받아 진행하는게 안전한 방법일거 같다.

 

무튼 이렇게 자동차 계기판의 신호들을 STM32에서 CAN과 GPIO로 받았으면 이 신호들을 해석하여 정형화된 데이터를 UART 버퍼에 실어 라즈베리파이에 넘겨주면 된다.

 

UART 신호를 받은 라즈베리파이에서는 GPIO을 통해 데이터를 받지만 이를 Qt의 QSerialPort 컴포넌트가 데이터를 가져와 백엔드단 변수에 데이터를 대입한다.

그럼 QML에서 타이머 컴포넌트로 백엔드의 함수를 실시간으로 호출하여 백엔드 단에서 QML로 데이터를 지속적으로 넘겨주게 되고 QML은 이 데이터를 통해 실시간으로 UI에 계기판 데이터를 시각화 하게 된다.

 

참고로 필자는 차량에 안드로이드 올인원 네비를 탑재했으며 라즈베리파이의 화면은 안드로이드 VNC로 접속하여 띄운 것이다.

 

5. 프로젝트 완성


그렇게 최종적으로 완성된 모습이다.

 

이미 경험했던 기술을 조합하여 진행된 프로젝트라 며칠? 1주일? 내로 끝낸거 같다.

Qt를 거의 다시 배우는 수준으로 배워서 접목시키느라 조금 애 먹었지만 다 만들고나니 뭔가 해냈다는 느낌이 들어 뿌듯하다.

글 재주가 없어서 정신없이 말이 나오는대로 좔좔좔좔 썼지만 이 글을 보면서 나와 같은 프로젝트를 하는 분들이 참고 할만한 자료가 되었으면 하면서 이 글을 마치도록 하겠다.

 

(작성중) <TA모닝 아날로그 계기판 디지털 계기판 만들기(빌드루트 환경)>

(추가) 라즈베리파이OS 환경이 아닌 빌드루트 환경에서 Qt 애플리케이션을 실행하는 작업기도 올릴 예정

 

역대급으로 긴 글, 읽어주셔서 감사합니다.

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'프로젝트 작업기' 카테고리의 다른 글

[STM32] 커스텀 드론에 직접 만든 펌웨어를 올려 호버링 구현  (1) 2026.04.22
[Buildroot/Qt/라즈베리파이] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(빌드루트 임베디드 리눅스 환경)  (0) 2026.01.11
[ESP32] CAN 신호를 받아 해석하고 특정 아날로그 신호로 내보내기(CAN to Analog Converter - CAC)  (0) 2025.10.21
[ESP32] TA모닝 에어컨 자동 컨트롤러를 만들어 출력 및 연비 개선하기(일명 세상에서 가장 빠르게 에어컨을 켠 채 오르막길을 올라가는 순정 모닝 만들기) 下  (0) 2025.09.25
[Library] CAN통신 모니터링 라이브러리  (0) 2025.08.29
'프로젝트 작업기' 카테고리의 다른 글
  • [STM32] 커스텀 드론에 직접 만든 펌웨어를 올려 호버링 구현
  • [Buildroot/Qt/라즈베리파이] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(빌드루트 임베디드 리눅스 환경)
  • [ESP32] CAN 신호를 받아 해석하고 특정 아날로그 신호로 내보내기(CAN to Analog Converter - CAC)
  • [ESP32] TA모닝 에어컨 자동 컨트롤러를 만들어 출력 및 연비 개선하기(일명 세상에서 가장 빠르게 에어컨을 켠 채 오르막길을 올라가는 순정 모닝 만들기) 下
이니셜P
이니셜P
카카오톡 문의 : initial_p 유튜브 : https://www.youtube.com/@gun-ny
    반응형
  • 이니셜P
    #include <이니셜.P>
    이니셜P
  • 전체
    오늘
    어제
    • 분류 전체보기 (93)
      • 협력점 안내 (1)
      • 프로젝트 작업기 (11)
      • 프로젝트 포트폴리오 (3)
      • 끄적끄적 (2)
      • Arduino (11)
      • STM32 (0)
      • ESP32 (8)
      • EasyEDA (0)
      • QT (5)
      • LVGL (0)
      • Buildroot (14)
      • Yocto (2)
      • Git (2)
      • C언어, C++ (18)
      • 프로그래머스 (16)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      linux
      모닝
      리눅스
      계기판
      Buildroot
      SN65HVD230
      라즈베리파이
      RaspberryPi
      Embedded
      can
      SError
      임베디드
      esp32
      루트파일시스템
      아두이노
      rootfs
      Qt
      빌드루트
      Overlay
      0xbe000011
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.5
    이니셜P
    [STM32/라즈베리파이/Qt] TA모닝 아날로그 계기판 디지털 계기판으로 만들기(라즈베리파이OS 환경)
    상단으로

    티스토리툴바