Contents

 

[개요]

일반적으로 시제품에 Firmware를 업데이트하고 시험하기 전 Simulation Level에서 실제 부하를 모델링하고 변경한 제어 알고리즘이 안정적으로 동작되는지 사전 검증을 하거나 또는 실제 환경에서는 DAC 채널 수 부족이나 애초에 DAC를 지원하지 않는 등의 제약 조건으로 경향성을 보기 어려운 내부 Parameter들에 대해 어떤 양상이 있는지 등을 확인할 때 Simulation을 많이 접하게 됩니다.

 

특히나 연구, 개발하시는 Target Microcontroller(MCU)와 같이 이산 시간(Discrete-Time) 환경에서 알고리즘을 검증하기 위해서는 C언어로 개발한 함수를 그대로 적용하여 검증하기 용이한 C Block, DLL Block이라는 Model을 PSIM에서 지원하고 있습니다.

 

기존에 Dev-C++[1], Visual Studio Community[2](구 Express) 환경에서도 DLL을 생성해 사용했었지만 Dev-C++은 복수 개의 프로그램 실행이 불가하다는 단점, Visual Studio Community는 기업에서 사용하려면 License가 필요하다는 단점이 있기도 하고 요즘은 취미로 Python이나 ESP32 Board도 사용하다 보니 Visual Studio Code 환경에 익숙해져서 DLL 생성도 같은 환경에서 사용하려고 시도해보았고 그 과정을 정리해보고자 합니다.

 

[본문]

1. Compiler 환경 Setup

1.1 MSYS2 설치

우선 MSYS2[3]를 설치합니다. Windows 환경에서 Unix 계열 개발 도구와 라이브러리를 사용하고 Software를 Build할 수 있게 해주는 Software platform이라고 합니다.

Fig1. MSYS2 Download page

 

설치를 완료한 후 MSYS2 UCRT64를 실행시킵니다.

Fig2. MSYS2 UCRT64 icon

 

Terminal 창이 나오고 아래와 같은 명령어를 입력하여 Update를 진행합니다.

pacman -Syu

Fig3. MSYS2 UCRT64 Terminal 창

 

중간에 Y를 눌러 설치를 진행시키고 Y를 눌러 닫고 다시 UCRT64를 실행시킵니다.

Fig4. Update 완료

 

다시 실행된 Terminal에 아래 명령어를 입력하여 설치된 패키지에 대한 최신 업데이트를 한번 더 Check 및 진행하고 끝나길 기다립니다.

pacman -Su

Fig5. Package Update 명령어

 

다음은 GCC 및 Build 도구를 설치합니다. 명령어는 아래와 같습니다.

pacman -S --needed mingw-w64-ucrt-x86_64-toolchain

Fig6. gcc & build 도구 설치 명령어

 

중간에 Package 선택이 나오면 default로 전체 설치를 합니다. (그냥 Enter 누르면 됨)

이후 gcc가 설치되었는지 확인합니다.

gcc --version

Fig7. gcc 설치 완료

 

위와 같이 gcc 16.2.0 version이 확인되었으니 이제 설치 경로를 확인합니다.

which gcc

Fig8. gcc 설치 경로

 

설치 경로가 "/ucrt64/bin/gcc"로 나오는지 확인합니다.

 

1.2 Windows 기반 환경 변수 등록

VS Code의 PowerShell Terminal에서 gcc를 사용하려면 Windows 환경 변수 등록이 필요합니다.

기본 MSYS2 설치 위치가 C:\msys64\ucrt64\bin 라면

"Windows 시작 버튼 - 설정 - 시스템 - 정보 - 고급 시스템 설정 - 환경 변수" 로 들어가서 시스템 변수의 path를 찾고 아래와 같이 설치 경로를 추가해줍니다.

Fig9. 환경 변수, 시스템 변수 편집창

 

Fig10. 환경 변수 편집 창

 

여기서 저의 경우 Compile시에 LaTeX로 문서 작성을 위해 설치했던 Strawberry안에도 gcc가 있어서 환경변수 우선순위를 UCRT64를 더 높게 설정해주었습니다.

 

이후 VS Code를 실행하고 Terminal을 엽니다.

Fig11. VS Code Terminal 위치

 

앞서 UCRT64에서 확인했던 gcc version Check 명령어를 적용해서 아래 사진과 같이 출력되면 VS Code와 gcc가 연동됐다고 판단할 수 있습니다.

Fig12. VS Code gcc 연동

 

1.3 VS Code Extension 설치

다음은 VS Code의 Extension 설치를 진행합니다. 좌측 패널에서 Extension Marketplace를 열고 "ms-vscode.cpptools" 를 검색하고 설치합니다. 

Fig13. VS Code C/C++ Extension 설치

 

2. PSIM DLL 파일 생성

2.1 Workspace 구조 설정

우선 폴더 트리 예시를 잡고 가겠습니다. 저는 하나의 Workspace안에 2개의 Project를 생성하고 각각의 DLL 파일을 PSIM Schematic에서 2개의 DLL을 연동해 사용하고자 합니다.

 

기본적인 Workspace Folder Tree는 아래와 같습니다.

TEST_DLL(Workspace)
│
├─ CNV(Project 1)
│ ├─ Source Folder
│ │─ main.c
│ ├─ Header Folder
│ └─ main.h
│
├─ INV(Project 2)
│ ├─ Source Folder
│ │─ main.c
│ ├─ Header Folder
│ └─ main.h
│
├─ Converter.dll
├─ Inverter.dll
└─ PSIM.psimsch

 

이와 같이 Workspace 하위에 2개의 Project Folder로 각각 관리를 하고 Build 시에는 선택한 Project 만 별도 Build 진행 그리고 생성되는 DLL 파일은 상위 폴더인 Workspace Folder 안에 생성되어 관리하고자 합니다. 

Fig14. Folder 구조
Fig15. Workspace와 Project1, 2 구성

 

우선 DLL 파일을 제외하고 원하는 구조로 임시 구성해둔 후 나중에도 본 구조의 Workspace를 VS Code에서 불러올 수 있도록 저장합니다.

File - Save Workspace As... 를 누르고 Workspace 안으로 경로를 지정해서 저장해줍니다.

Fig16. VS Code 현재 Workspace 저장

 

저장을 하게되면 아래 사진과 같이 저장당시 지정한 명칭으로 Workspace 이름이 바뀌게되며 ".code-workspace" 파일이 생성된 것을 확인할 수 있습니다.

제가 만든 Workspace 폴더는 실제 이름은 TEST_DLL이었지만 Workspace를 저장하면서 정의한 명칭은 "PCS_Si"로 저장했습니다.

Fig17. VS Code Workspace 명칭 적용 및 파일 생성 확인

2.2 Workspace & Project & Makefile Setup

앞서 생성된 ".code-workspace" 파일을 더블클릭으로 열어보면 아래와 같이 나오게 됩니다.

만약 제가 작업중인 것 처럼 2개 이상의 Project 폴더를 이용하자고 한다면 여기서 본 Workspace에 프로젝트명과 실제 폴더명을 Matching 시켜줄 수 있습니다.

Fig18. 변경 전 .code-workspace 파일 초기 값

 

아래 사진에서 "path", "name"의 정의는 아래와 같습니다. 

"path" : Workspace안에 실제 Project Folder 명칭

"name" : VS Code에서 인식하는 대체 명칭

 

저는 아래와 같이 Workspace 안의 폴더 CNV, INV를 각각 Converter, Inverter로 인식되도록 변경 적용 하였습니다.

{
    "folders": [
        {
            "name": "Converter",
            "path": "CNV"
        },
        {
            "name": "Inverter",
            "path": "INV"
        },
    ],
    "settings": {
        "makefile.configureOnOpen": false
    }
}

Fig19. 변경 후 .code-workspace 파일

 

다음은 2개의 Project 폴더 안에 각각 ".vscode" 라는 이름의 폴더를 생성하고 그 안에 "c_cpp_properties.json", "tasks.json" 파일을 생성합니다.

 

우선 "c_cpp_properties.json" 파일은  build 시 2개의 Project 각각 build를 선택할 수 있도록 명칭을 정의합니다.

 

"name" : build 시 복수 개의 프로젝트 중 선택하기 용이한 명칭으로 정의

"includePath" : 프로젝트 파일 내부 Source & Header File의 검색 시 정확한 경로를 Searching 하도록 하는 VS Code IntelliSense 용 경로 Matching

{
    "configurations": [
        {
            "name": "Converter GCC",
            "compilerPath": "C:/msys64/ucrt64/bin/gcc.exe",
            "includePath": [
                "${workspaceFolder}/Header/**",
                "${workspaceFolder}/Source/**"
            ],
            "defines": [
                "_WIN64"
            ],
            "cStandard": "c11",
            "intelliSenseMode": "windows-gcc-x64"
        }
    ],
    "version": 4
}

 

Fig20. c_cpp_properties.json

 

다음은 "tasks.json" 파일로 단축키(Ctrl + Shift + B)로 build Task를 실행하고 결과 값에 build가 성공적인지 실패했는지 시간은 얼마나 걸렸는지를 Terminal 상에 현시 해주는 내용을 담고 있습니다.

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Build Converter DLL",
            "type": "shell",
            "command": "$sw = [System.Diagnostics.Stopwatch]::StartNew(); mingw32-make; $code = $LASTEXITCODE; $sw.Stop(); Write-Host ''; if ($code -eq 0) { Write-Host ('========== BUILD SUCCESS : {0:N3} sec ==========' -f $sw.Elapsed.TotalSeconds) } else { Write-Host ('========== BUILD FAILED : {0:N3} sec ==========' -f $sw.Elapsed.TotalSeconds) }; exit $code",
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": [
                "$gcc"
            ]
        }
    ]
}

 

Fig21. tasks.json

 

마지막으로 가장 중요한 "Makefile" 입니다.

Makefile은 Source/ 아래에 모든 .c 파일을 재귀적으로 찾아 gcc로 컴파일하고, Header/ 아래의 모든 .h 파일을 자동으로 Include 한 뒤 최종적으로 .dll을 생성하는 compile을 담당합니다.

 

우선 2개의 Project 폴더 안에 각각 New File로 확장자 없이 "Makefile"이라는 것을 생성해줍니다. (내용은 아래 내용을 붙여 넣습니다.)

SHELL := cmd.exe
 
CC = gcc

TARGET = ../CNV.dll

OBJ_DIR = build/obj


# ============================================================
# Recursive Wildcard
# ============================================================

rwildcard = $(foreach d,$(wildcard $1*),\
             $(call rwildcard,$d/,$2) \
             $(filter $(subst *,%,$2),$d))
  

# ============================================================
# Source Files
# ============================================================

SRC = $(call rwildcard,Source/,*.c)
  

# ============================================================
# Header Include Directories
# ============================================================

HEADER_FILES = $(call rwildcard,Header/,*.h)

HEADER_DIRS = $(sort $(dir $(HEADER_FILES)))

INCLUDES = $(addprefix -I,$(HEADER_DIRS))
  

# ============================================================
# Object / Dependency Files
# ============================================================

OBJ = $(patsubst Source/%.c,$(OBJ_DIR)/%.o,$(SRC))

DEP = $(OBJ:.o=.d)
 

# ============================================================
# Compiler / Linker Options
# ============================================================

CFLAGS = -O2 -Wall -std=c11 $(INCLUDES) -MMD -MP

LDFLAGS = -shared


# ============================================================
# Build DLL
# ============================================================

all: $(TARGET)

$(TARGET): $(OBJ)
    $(CC) $(LDFLAGS) $(OBJ) -o $(TARGET)

# ============================================================
# Compile C -> Object
# ============================================================

$(OBJ_DIR)/%.o: Source/%.c
    @if not exist "$(subst /,\,$(dir $@))" mkdir "$(subst /,\,$(dir $@))"
    $(CC) $(CFLAGS) -c $< -o $@


# ============================================================
# Dependency
# ============================================================

-include $(DEP)


# ============================================================
# Clean
# ============================================================

clean:

    @if exist build rmdir /S /Q build
    @if exist "$(subst /,\,$(TARGET))" del /Q "$(subst /,\,$(TARGET))"

 

지금까지 상기 내용과 같이 복수개의 프로젝트들 내부에 Source Folder, Header Folder 안에 .c, .h 파일을 나눠서 넣어줬다면 dll이 생성되는 경로인 "TARGET = ../CNV.dll" 이 부분만 원하시는 이름으로 수정하셔서 사용할 수 있습니다.

 

Fig22. Makefile

 

2.3 build 동작 확인

마지막으로 Terminal 창에서 단축키(Ctrl + Shift + B)를 Command 한다면 아래 사진과 같이 상단에 2개의 프로젝트가 현시되면서 선택이 가능하게되며 수정작업이 완료된 프로젝트를 클릭하면 build를 진행합니다.

Fig.23 Project 1 Build

 

build가 진행중일때 build 폴더가 생성되면서 폴더 내부에 object 파일이 생성됩니다.

초기 처음 build를 진행시에는 시간이 다소 소요되지만 이후부터는 수정사항이 연관된 부분만 build를 수행하고 나머지는 기존 build시 생성된 파일들을 참조하여 시간을 단축시키게 됩니다.

 

build가 완료되면 아래 사진과 같이 "BUILD SUCCESS" 라는 문구가 Terminal에 표기됩니다.

Fig24. Build Success

 

Fig25. Project 2 Build

 

저는 Project 1 (Converter)에서 입력된 값을 2배로 출력, Project2 (Inverter)에서 입력된 값을 3배로 출력되도록 Test Code를 적용해보았습니다.

Fig26. Build 후 생성된 dll 파일

 

결과적으로 2개의 dll 파일이 제가 원했던 Workspace (Project1, Project2의 상위 경로) 각각 생성되었음을 확인할 수 있습니다.

이렇게 구성하면 Project1, Project2가 서로 영향을 주지 않고 필요한 Project를 추가하거나 빼기 용이하며 PSIM Schematic과 동일 위치에 dll 파일이 있어서 경로로 인한 인식 불가 현상도 막을 수 있습니다.

 

Fig27. PSIM Schematic

2개의 General DLL Block에 생성한 CNV.dll, INV.dll을 매칭시켜두었고 100[V]의 Sine wavefrom을 입력받아 각각 dll을 거쳐 출력하는 간단한 구성을 그려보았습니다.

 

Fig28. Simview 결과

 

dll 연동 및 동작이 정상적으로 수행됨을 알 수 있습니다.

3. 결론

지금까지 VS Code 기반에서 PSIM Schematic에 연동하기위한 DLL Block용 dll 파일 생성 방법에 대해 정리해 보았습니다. 생성된 dll 파일은 x64 기반으로 동작하게 됩니다. (x86 기반으로 변경시에는 본 내용을 따라 1차적으로 만들고 AI의 도움을 받아보면 쉽게 해결될 것으로 사료됩니다. ㅎ)

 

[참고자료]

 

[1] : https://www.dev-cpp.com/

 

[2] : https://visualstudio.microsoft.com/ko/vs/community/

 

Visual Studio Community | 최신 무료 버전 다운로드

Windows, Android 및 iOS용 최신 개발자 앱을 만들기 위한 모든 기능을 갖춘 확장 가능한 무료 IDE를 사용해 보세요. 지금 무료로 커뮤니티를 다운로드하세요.

visualstudio.microsoft.com

[3] : https://www.msys2.org/

 

MSYS2

Software Distribution and Building Platform for Windows

www.msys2.org

 

 

 

[개요]

Thinkpad 10 Tablet의 Docking 단자의 고장으로 Ultrabook Keyboard의 Pogo Pin에 USB Cable을 연장하는 과정을 지난 19년도에 포스팅[1]으로 다뤄보았습니다.

Fig. Thinkpad 10 Ultrabook Keyboard USB 유선으로 DIY 후 결과 사진

 

Thinkpad 10 Ultrabook Docking Keyboard는 작은 사이즈와 만족스러운 타건감 그리고 트랙패드의 존재로 굉장히 매력적인 제품인 데다가 정상 동작하는데 폐기하기에는 아까웠고 다만 지난번에 Tuning을 통해 유선으로 만들어서 사용하였지만 노트북에 USB Port를 하나 점유하는 부분이 아쉬움이 남아 무선 Bluetooth Keyboard로 개조 작업을 진행하게 되었습니다.

 

이번 DIY는 USB to Bluetooth로 변환하는 과정이 핵심이며 AI가 없던 시절에는 엄청나게 많은 시간과 노력을 들여 가능했던 DIY가 현재는 Vibe Coding 방식으로 완성도는 다소 떨어지더라도 원했던 동작이 가능한 수준까지 빠른 시일내에 만들어 볼 수 있었고 이제 그 과정을 남겨보고자 합니다.

 

- 완성된 Bluetooth Keyboard

지난번에 Function Generator를 만들었을 때처럼 우선 결론부터 이야기하면 Keyboard랑 Trackpad 모두 정상적으로 동작합니다.

Fig. Thinkpad 10 Bluetooth Keyboard DIY 후 결과 사진

 

[본문]

1. Hardware Design

1.1 Hardware 구성 설명

Fig. 구성 설명 블록도

 

상기 그림과 같이 Thinkpad Keyboard와 PC(Host)로 직접 연결되는 방식에서 중간에 별도의 Host가 Keyboad의 정보를 가져오고 이를 Bluetooth HID 로 변환하여 PC로 전송하는 기능이 추가되어야 함을 알 수 있습니다.

지금까지 사용해봤던 MCU들은 USB Device는 가능하나 Host 기능은 지원하지 않는데 이번 DIY는 USB Host 기능과 Bluetooth 기능이 요구되었고 이 기능을 모두 가지고 있는 Espressif 社의 IoT용 Microcontroller인 ESP32 Board를 알게되어 사용하게 되었습니다.

 

1.2 ESP32-S3-DevKitC-1 (1st Prototype)

앞서 설명한대로 현재 USB Host 기능과 BLE(Bluetooth) 기능을 모두 지원하는 Espressif Systems 社의 ESP32 Series를 사용하고자 합니다.

구매 진행한 개발보드는 ESP32-S3-DevKitC-1[2]이며 산업용 IoT에 특화된 제품으로 Wi-Fi, Bluetooth 5(LE) 무선 통신 기능이 내장 240Mhz로 동작하는 듀얼 코어 MCU가 적용되었다고 합니다.

Fig. ESP32-S3-DevKitC-1 구성 및 사용 Port 표기

 

기능 구현 단계에서는 동작 확인이 주목적이므로 간단하게 5V를 Keyboard와 ESP32-S3-DevKitC-1 Board에 병렬로 전원을 공급하였습니다.

1.3 Battery 용량 검토

또한 무선으로 사용해야 하므로 Battery 및 Battery 충전 Module 또한 필요하므로 1차적으로 Prototype을 만들어서 대략적인 소비전력과 Keyboard의 외관을 최대한 유지할 수 있도록 배터리의 형태 및 용량을 선정하였습니다.

번외로 nRF52840 + TinyUSB + ZMK로 구성하면 저전력으로 구성할 수 있다고 하나 최대한 적은 시간으로 동작이 가능한 DIY를 하고 싶은 상황이어서 쉬운 길을 택하게 되었습니다.

 상기 영상은 ESP32-S3 DevKitC-1과 Thinkpad Keyboard에 병렬로 연결한 후 Power Supply로 5V를 인가한 후 동작을 확인한 영상[3]이며 Power Supply에 현시된 전류를 기준으로 아래와 같은 결과가 나왔습니다.

Thinkpad 10 Keyboard
단독 동작
ESP32-S3 DevKitC-1 BLE,
Typing 동작
ESP32-S3 DevKitC-1 BLE,
Trackpad 동작
Unit
20 100 110 mA

Thinkpad Keyboard에서 기본적으로 소모하는 전류가 20mA

Bluetooth 기능을 활성화하고 실제 Keyboard 인식 및 Typing 동작시 전류는 약 100mA

Trackpad로 Cursor를 움직이고 Click하고 Two point Scroll 동작시에 최대 전류는 약 110mA

따라서 결론적으로는 Keyboard Typing 동작에는 최대 0.5Wh 가 요구되는 것을 확인했습니다. (5V 기준 약 100mA)

소비 전력이 예상했던 것보다 다소 높다고 판단되어 낮출 수 있는 방안을 생각해보았고 Default로 적용된 160Mhz Clock을 80Mhz로 낮춰보았으나 소비 전류가 줄어들진 않았습니다.

충전 Module은 TP4056 리튬폴리머 배터리 충전 모듀 3.7V 1A 1S을 사용했습니다.

Fig. 최종적용 Battery 정보(DTP951765, 1200mAh)

 

위와 같이 최종적으로 적용된 Battery는 1200mAh 입니다. (상기 Battery 사진 정보 및 구매처 : [4])

DTP951765 (Height : 9.5, Horizontal : 65, Vertical : 17[mm]) 사이즈로 Keyboard의 외관을 손상시키지 않고 적용가능한 기성품중에서 가장 큰 용량이었습니다.

따라서 최대 0.5Wh 소비 전력 기준으로 3.7V를 적용하면 135mA정도가 예상되는데 1200mAh 기준으로 8~9시간 정도 사용이 가능하다고 판단되며 실제 Battery 전압은 초기 완충시 약 4.2V에서 점점 낮아지는 비선형 특성을 가지고 있으므로 상기에 단순 계산으로 도출된 8~9시간 보다는 더 사용이 가능할 것으로 판단됩니다.

 

추가적으로 이후에 소개할 내용의 ESP32-S3 Super Mini로 변경 및 불필요한 Code 최적화 (Debugging을 위해 Serial 통신으로 전송하는 Log Data 부분 삭제) 한 후에 약 20mA 정도 감소되어 최종적으로는 기본동작이 5V / 80mA 정도로 확인되었습니다.

 

1.4 Thinkpad 10 Keyboard 내부 공간 확보(기구 측면)

Fig. Keyboard 내부 공간 확보 관련 사진

이제 동작 확인은 완료된 상태이므로 기존 Thinkpad Keyboard 안에 넣는 작업이 남았습니다.

 

우선 Thinkpad keyboard 에 적용되어있던 Wacom pen holder를 전체적으로 탈거하고 지지대 같은 부분을 전부 잘라내고 거친 부분은 줄로 갈아내서 마감했습니다.

대략적으로 가 조립을 해보았는데 문제는 ESP32-S3-DevKitC-1 Board 사이즈가 너무 커서 공간이 부족해 Housing을 뚫고 확장 작업 및 외부 돌출 형태가 될 수 밖에 없는 상황이었습니다.

 

1.5 ESP32-S3 Super Mini (2nd)

사이즈로 인해 기구부를 어떻게 해야 외관상 최소한의 수정으로 이쁘게 만들수 있을까 고민하던 찰나에 동일한 ESP32-S3 Series 중에 Super Mini 라는 Board가 있는 걸 알게되었습니다.

마침 이번 프로젝트에 필요한 Bluetooth HID와 USB Host 기능은 그대로 적용이 가능했습니다.

다만 CP2102 Driver가 없는 모델이고 Program download와 serial monitor 기능을 이용한 debugger mode로 사용하는 Port가 서로 다릅니다.

따라서 Program download 및 최종적으로 Keyboard로 연결되는 Port는 USB-C Port이고 Serial Monitor로 Debugging을 하기위해서는 UART0 Port (TX, RX) 위치에 FT232 USB to UART Converter를 이용해서 Debugging을 해야 합니다.

Fig. ESP32-S3 Super Mini 구성 및 사용 Port 설명

 

만약 Download Mode 상태에서 Debugger Mode(Serial Monitor)로 연결할 때 장치 관리자의 COM Port가 제대로 인식되지 않고 계속 Reboot이 되는거 같은 느낌이 들때는 아래와 같이 조치합니다.

 

 1. BOOT Switch 누른 상태 유지

 2. RST Switch 한 번 눌렀다 떼기

 3. BOOT Switch 떼기

 4. 위와 같이 조치한 후 Port Number 확인하는 명령어 : python -m serial.tools.list_ports

 

1.6 전체 완성 구성

Fig. 최종 Bluetooth Keyboard 완성 사진

최종 구성은 위 사진과 같으며 집에 보유중인 자재들을 최대한 활용하여 비용을 아껴보고자 DC-Jack, USB OTG까지도 사용하였습니다. ㅎ

Fig. 배터리 충전에 따른 LED 색상

2. Firmware 개발 환경 구성

- IDE : VS Code (Visual Studio Code)

- ESK : ESP-IDF

- Programming Language : Python

 

2.1 USB to UART driver install

ESP32-S3-DevKitC-1 Board를 연결하기 위해서는 CP2102 USB to UART Bridge Driver [5]설치가 필요합니다.

Fig. 장치 관리자 사진

Driver 설치가 완료되면 COM Port가 인식됩니다.

 

2.2 ESP_IDF Install Guide

다음은 VS Code에서 ESP-IDF 를 설치해야 합니다. Extensions Market에서 ESP-IDF를 검색하고 설치 진행하는데 문제는 우측 하단에 뭔가 Pop-up이 여러개 떳었는데 잘 읽어보고 승인해야 할 것이 있었습니다...

중간 과정은 생략되었으나 처음에는 ESP-IDF v6.0.1을 설치했었고 컴파일 시도 과정에서 우여곡절이 있었고 컴파일러 버전문제인지 경로 문제인지 설정 문제(json) 무엇이 문제인지를 확인하기 위해서 다양한 시도 끝에 결론적으로는 v6.0.1은 삭제했고 v5.5.4로 정착했음

  • ESP-IDF의 다른 Version을 설치하는 방법은 다음과 같음
  • VS Code에서 Ctrl + Shift + P 단축키로 명령어 팔레트 창을 띄우고 ESP-IDF : Open ESP-IDF Installation Manager를 누르면 아래와 같은 창이 Pop-Up 됨

 

여기서 New Installation을 누르면 원하는 Version의 ESP-IDF를 선택할 수 있습니다.

ESP-IDF를 설치하면 바탕화면에 아래와 같은 Icon이 생성됨

2.3 Python 기본 명령어

이번에 ESP32를 사용하면서 Python Compile 명령어를 알게된 부분이 있어 핵심만 정리하면 아래와 같습니다.

설명 명령어
(venv)를 탈출  deactivate
Python의 경로를 확인 where.exe python
재부팅 후 ESP-IDF Path 설정 C:\esp\v5.5.4\esp-idf\export.ps1
Build Target 설정 idf.py set-target esp32s3
Build idf.py build
Program Flash Download idf.py -p COM3 flash
COM Port Search idf.py monitor
Program Monitoring idf.py -p COM3 monitor
Monitoring을 멈추는 방법 Ctrl + ]
Project Folder를 Clean 하는 방법 (Build clean) idf.py fullclean

- Program Flash Download 시에는 장치 관리자에서 찾은 Device COM Port Number를 적용해야 합니다.

 

2.4 Example Code Merge로 만든 최종 Test Firmware

결과적으로는 ESP-IDF를 설치하면 Example Code를 제공하며 예를 들면 하기 경로에

HID Host 관련 예제 : C:\esp\v5.5.4\esp-idf\examples\peripherals\usb\host\hid\main

Bluetooth HID 관련 예제 : C:\esp\v5.5.4\esp-idf\examples\bluetooth\bluedroid\ble\ble_hid_device_demo\main

상기 예제들을 조합하고 Code 분석 및 방향성은 GPT를 이용해 Vibe Coding 방식으로 완성하였습니다.

ESP32_R2.6.zip
0.15MB

[결론]

현재 1주일간 노트북에 연동해서 사용해본 결과 처음에 Power Switch를 On 하고 약 2~3초 후부터 정상적으로 동작되며 Keyboard 전원은 켜둔 후 노트북만 다른 장소로 이동한 후 돌아왔을때는 Keyboard가 바로 Bluetooth 연결이 잡히지 않는 문제가 있었습니다. (개선 필요) 다시 Power Switch를 Off, On 해야 정상적으로 동작함을 확인했습니다.

 

하루 평균 12 ~ 12.5시간 정도 사용했었고 중간에 배터리 부족으로 꺼지는 현상은 없었습니다.

 

소비전력이 너무 높아 장시간 사용이 불가한 가장 큰 단점이 존재하지만 Vibe Coding을 통해 짧은 시간을 투자해서 동작 가능한 Bluetooth Keyboard를 얻어 매우 재밌는 diy 였습니다. ㅎ

 

[참고자료]

[1] : https://marasca.tistory.com/8

 

[Repair] Thinkpad 10 Ultrabook keyboard 고장 진단

1. Thinkpad 10 Ultrabook Keyboard 고장 및 A/S 판단 불가2015년 6월 28일에 Lenovo 공식 홈페이지에서 주문했었던 Thinkpad10 과 docking Keyboard가 도착하여 잘 사용하였었다. 그러다 작년 봄쯤 어느날 갑자기 keyboar

marasca.tistory.com

 

[2] :

https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp32-s3-devkitc-1/index.html

 

ESP32-S3-DevKitC-1 - ESP32-S3 - — esp-dev-kits latest documentation

ESP32-S3-DevKitC-1 [中文] The ESP32-S3-DevKitC-1 is an entry-level development board equipped with ESP32-S3-WROOM-1, ESP32-S3-WROOM-1U, or ESP32-S3-WROOM-2, a general-purpose Wi-Fi + Bluetooth® Low Energy MCU module that integrates complete Wi-Fi and Bl

docs.espressif.com

[3] : https://youtube.com/shorts/13XD8b6mJaU

 

[4] :

https://www.eleparts.co.kr/goods/view?no=11235136

 

리튬폴리머 배터리 DTP951765 3.7V 1200mAh

충전지 3.7V 1200mAh 사이즈 17x65x0.95mm(T)

www.eleparts.co.kr

 

[5] :

https://www.silabs.com/software-and-tools/usb-to-uart-bridge-vcp-drivers?tab=downloads

 

CP210x USB to UART Bridge VCP Drivers - Silicon Labs

The CP210x USB to UART Bridge Virtual COM Port (VCP) drivers are required for device operation as a Virtual COM Port to facilitate host communication with CP210x products. These devices can also interface to a host using the direct access driver.

www.silabs.com

 

[개요]

MCU의 Firmware Design을 진행함에 있어 ADC 기능 구현은 필수적으로 요구되며 이를 테스트하기 위해서 Function Generator를 활용하면 테스트가 매우 용이해집니다.
하지만 취미 활동을 위해 제품으로 구매하기에는 비용이 부담스러워 저렴하게 하나 만들어봤으면 좋겠다고 생각했던 차에 Great Scott!라는 Youtube 채널에서 회로도, BOM, 제작 과정 영상까지 자료를 얻을 수 있었고 저는 그 자료를 기반으로 따라 만들어보고자 합니다. (날먹을 해보고자 합니다. ㅎ)  :D

 

- Great Soctt!이 Design한 Function Generator 관련 자료

Great Scott! Youtube Channel과 [1], Cezar Chirila 가 작성한 글 [2]에서 얻은 회로도와 BOM(Bill of Materials) 정보를 기반으로 부품 구매를 진행했었고 이후 회사 일하기 바쁘다는 핑계로 긴 시간을 손 놓고 있다가 최근 다시 의지가 생겨 제작을 시작하였습니다.

Fig. Great Scott가 배포한 회로도
Fig. Great Scott!가 제작한 Function Generator 전체 구성 사진

위 사진은 Housing 작업 전에 기능 테스트를 진행하는 단계에서 전체적인 구성을 사진으로 설명하는 부분을 발췌해왔습니다.

- 완성된 Function Generator

우선 제가 제작한 Function Generator의 형상부터 공개합니다. 부품의 배치 및 일부 부품의 Maker가 상이할 뿐 거의 동일한 형태입니다.

Fig. 제작된 Funciton Generator 상단 View
Fig. 제작된 Function Generator 하단 View
Fig. Housing을 포함한 전체 완성 사진

 

[본문]

1. Hardware Design

  1.1 Housing 작업

외함의 경우 처음부터 PCB Design 및 부품배치 많은 걸 고려해서 예쁘게 만들면 좋지만... 3D 프린터도 없고... 쉽게 가자는 생각에  튼튼하고 저렴하게 심지어 사이즈도 다양해서 선택의 폭이 넓은 Enclosure를 사용하였습니다.

Fig. 외함으로 사용 할 Enclosure
Fig. Enclosure 사이즈에 맞춘 만능 기판 타공 작업

일반적으로 Enclosure는 내부에 볼트를 체결할 수 있는 홈들이 있는데 심지어 도면도 보통 제조사에서 제공해 줍니다. 그 위치에 맞도록 만능기판을 하나 컷팅해서 수작업으로 홀 가공을 진행하였습니다.

P.S 나중에 다 만들고 나서야 타공을 확인했는데 8개 홀 중에 3개만 맞더군요. ㅋㅋ OMG

  1.2 몇년 전 진행했었던 전원부 부품 배치 작업

Fig. 전원부 부품 배치 작업 후 사진

위 사진은 처음에 Function Generator Youtube 영상을 보고 부품을 구매 후 당시에 전원부까지만 부품을 배치했었던 시점의 사진입니다. 여기까지 작업했을 때가 사진 날짜를 확인해보니 23년이더군요. 당시에 나름 열심히 머리를 굴려 전원 흐름을 고려해 부품을 배치했다고 생각했는데 음... 잘 만든건지... 우선 이어서 다시 작업을 이제야 시작하게 되었습니다. :o

  1.3 입력 전원부 연결 작업 및 전원 파형

여기서 입력 전원을 Connector로 적용해서 탈 부착이 가능하도록 만들기 위해 Enclosure에 구멍을 뚫어야 했는데 이거 이쁘게 뚫기가 좀 많이 힘들더군요..  

Fig. 입력 전원부 변압기 1, 2차측 Connector 단말 처리

입력 전원은 한국 가정용 220[Vrms], 60[Hz] 전원을 사용하였고, Fuse는 Connector안에 삽입이 가능한 구조로 2[A] Fuse로 적용했습니다. 변압기를 통해 2차 측으로 24[Vrms]가 출력됩니다. 

Fig. 전원부 구성도
Fig. 변압기 2차측 전원 파형

무부하 상태에서 변압기 2차측 24[Vrms]를 Oscilloscope로 계측해본 결과 약 26[Vrms]가 계측되었습니다.

음 Spec에는 ±12V (24[Vrms])라고 명시되어 있는데 실제로는 26[Vrms]로 2[Vrms] 정도 크게 계측 되었습니다.

Fig. 정류기 출력단 전압 파형

따라서 정류기 출력 및 평활 Capacitor 측 전압도 위와 같이 ±18[Vdc]로 높게 출력 되었습니다. 그리고 12V Regulator와 5V Regulator 출력 전압 사진은 생략했지만 이 전압들을 통해서 전체 저전압 회로에 전원이 공급됩니다.

  1.4 부품 배치 완성 및 구성 설명

Fig. 전체 부품 배치 및 각 부 구성 설명

최종적으로는 위와 같이 부품 배치가 되었습니다.

  1.5 사용한 부품 BOM

 

(BOM) Function Generator R260207.xlsx
0.02MB

 

음... BOM은 블로그에 표로 넣기가 쉽지 않은 상황이어서 파일로 대체 합니다. :o

단가, 부품명, 구매링크 등에 대한 정보가 기재되어있습니다.

  1.6 회로도

Fig. 전체 구성 회로도

실제로는 2.4 구성과 같이 Connector를 적용한 부분이 다소 있지만 위 회로도에는 Connector가 없이 직결되었다는 가정으로 그렸습니다. Kicad 연습을 해보고 싶어서 한번 시도해본 결과입니다.  

2. Software Design

아두이노를 마지막으로 사용 해본지가 벌써 15년이나 되었네요 :O 
아두이노가 무엇인지는 본 글을 읽어보시는 분이라면 아실 것이라 생각합니다. 제가 현재 참고하고있는 본 프로젝트는 아두이노를 기반으로 설계되어있고 Source Code까지 Open되어있어서 그대로 활용하여 빠른 제작 및 결과를 보고자 시작했던 것이므로 기억이 가물가물한 Arduino 사용을 위해 IDE[3]를 다운로드 및 설치 해주었습니다. :D

Arduino Source Code는 [4]에서 다운로드가 가능합니다.

  2.1 Arduino Debugging 방법

처음에는 모든 부품을 연결하고 전원 투입을 하였는데 LCD도 동작이 안되고 어디서 동작이 막힌건지 알 수가 없어서 난감한 상황이었는데 Device mart에서 제공하는 [5] 영상과 같이 Serial Monitor 기능을 이용하면 Debugging이 가능함을 알게 되었습니다. 
이 기능을 사용해서 아래와 같이 각부의 기능별로 하나하나 확인하기 위해 Digital Input 관련 코드만 추출해서 테스트한 후 I2C LCD 기능만 추출해서 테스트하면서 Step by Step으로 진행해보았습니다.

 

  2.2 Pin D4 Digital Input Test - Source Code

아래와 같이 설정하면 D4 Pin으로 입력된 신호에 따라 Serial Monitor에서 결과를 확인할 수 있습니다.
Button을 누르면 : Low (0), 때면(Ideal) : High (1) 출력

#define DEBUG
//Digital Input Pin Number : 4 Test Setting
unsigned long lastButtonPress; // Value used by button debounce
unsigned char lastButtonState;
unsigned char button;
const int buttonPin = 4;
void setup() 
{
  // put your setup code here, to run once:
  
  // Initialise pin as input with pull-up enabled and debounce variable for
  // encoder button

  pinMode(4, INPUT_PULLUP);
  lastButtonPress = millis();

#ifdef DEBUG
  Serial.begin(9600);
  while(!Serial) {}
  Serial.println("Initialize Complete");  
#endif
}

void loop()
{
  // put your main code here, to run repeatedly:
  // Check to see if the button has been pressed
  checkButton();

  #ifdef DEBUG

  #endif
}

  

// Function to debounce the button

// 0 = pressed, 1 = depressed, 2 = long press
void checkButton() 
{
  if ((millis() - lastButtonPress) > 100) 
  {
    if (digitalRead(buttonPin) != lastButtonState) 
    {
      button = digitalRead(buttonPin);
      lastButtonState = button;
      lastButtonPress = millis();
      
      Serial.print("Button Value : ");
      Serial.println(button);
    }
  }
}

 

Fig. D4 Push Button Switch Test

위와 같이 D4 Pin에 연결된 Rotary Encoder의 Button을 누르고 때면 Serial Monitor 창에서 값이 출력되어 동작 확인 유무를 직관적으로 판단이 가능합니다.

  2.3 Pin D2 & D3 Digital Input Test - Source Code

아래와 같이 설정하면 Rotary Encoder를 시계 방향, 반시계 방향으로 회전시켰을 때 Serial Monitor에서 결과를 확인할 수 있습니다. 시계 방향 회전시 : CW, 반시계 방향 회전시 : CCW

#include "Rotary.h"

#define DEBUG

//Digital Input Pin Number : 2, 3 (A, B Pulse Setting)

unsigned char menuState = 0;
Rotary encoder(2, 3);// Initialise the encoder on pins 2 and 3 (interrupt pins)
volatile unsigned char cursorPos = 0;
unsigned long frequency0 = 1000;
unsigned long frequency1 = 1000;
unsigned long frequency = frequency0;
unsigned long newFrequency = 1000;
int digitPos = 0;
volatile bool updateDisplay = false;
int freqRegister = 0; // Default FREQ register is 0
const unsigned long maxFrequency = 14000000;
unsigned char currentMode = 0;

void setup()
{
  //put your setup code here, to run once:

  // Set pins A and B from encoder as interrupts
  attachInterrupt(digitalPinToInterrupt(2), encChange, CHANGE);
  attachInterrupt(digitalPinToInterrupt(3), encChange, CHANGE);

#ifdef DEBUG
  Serial.begin(9600);
  while(!Serial) {}
  Serial.println("Initialize Complete");
#endif
}

void loop()
{
  //put your main code here, to run repeatedly:
  //Check to see if the button has been pressed

  #ifdef DEBUG

  #endif
}

unsigned char state = 0;

void encChange()
{
  // Depending in which menu state you are
  // the encoder will either change the value of a setting:
  //-+ frequency, change between FREQ0 and FREQ1 register (or -+ phase), On/Off, mode
  // or it will change the cursor position
  // unsigned char state = encoder.process();
  state = encoder.process();
  // Serial.print("State Value : ");
  // Serial.println(state);
  // Direction clockwise
  if (state == DIR_CW) 
  {
    Serial.println("State Value : CW");
    switch (menuState) 
    {
      case 1: 
      {
          if (cursorPos == 3)
            cursorPos = 0;
          else
            cursorPos++;
        } break;
      case 2: 
      {
          // Here we initialise two variables.
          // newFrequency is the value of the frequency after we increment it
          // dispDigit is the digit that we are currently modifing, and we obtain it
          // by a neat little trick, using operator % in conjunction with division
          // We then compare these variables with the maximum value for our
          // frequency, if all is good, make the change
          unsigned long newFrequency = frequency + power(10, digitPos);
          unsigned char dispDigit =
            frequency % power(10, digitPos + 1) / power(10, digitPos);
          if (newFrequency <= maxFrequency && dispDigit < 9) 
          {
            frequency += power(10, digitPos);
            updateDisplay = true;
          }
          if (freqRegister == 0) 
          {
            frequency0 = frequency;
          } else if (freqRegister == 1) 
          {
            frequency1 = frequency;
          }
        } break;
      case 4: {

          // if usePhase has been defined, changes in the encoder will vary the phase
          // value (upto 4096)
          // A better implementation would be to use increment of pi/4 or submultiples of
          // pi where 2pi = 4096
#ifdef usePhase
          unsigned long newPhase = phase + power(10, digitPos);
          unsigned char dispDigit =
            phase % power(10, digitPos + 1) / power(10, digitPos);
          if (newPhase < maxPhase && dispDigit < 9) 
          {
            phase += power(10, digitPos);
            updateDisplay = true;
          }
#endif
        } break;
      case 5: 
      {
          if (currentMode == 2)
            currentMode = 0;
          else
            currentMode++;
          updateDisplay = true;
        } break;
    }
  }
  // Direction counter clockwise
  else if (state == DIR_CCW) {
   Serial.println("State Value : CCW");
    switch (menuState) 
    {
      case 1: 
      {
          if (cursorPos == 0)
            cursorPos = 3;
          else
            cursorPos--;
        } break;
      case 2: 
      {
          unsigned long newFrequency = frequency + power(10, digitPos);
          unsigned char dispDigit =
            frequency % power(10, digitPos + 1) / power(10, digitPos);
          if (newFrequency > 0 && dispDigit > 0) 
          {
            frequency -= power(10, digitPos);
            updateDisplay = true;
          }
          if (freqRegister == 0) 
          {
            frequency0 = frequency;
          } else if (freqRegister == 1) 
          {
            frequency1 = frequency;
          }
        } break;
      case 4: 
      {
          // if usePhase has been defined, changes in the encoder will vary the phase
          // value (upto 4096)
          // A better implementation would be to use increment of pi/4 or submultiples of
          // pi where 2pi = 4096
#ifdef usePhase
          unsigned long newPhase = phase + power(10, digitPos);
          unsigned char dispDigit\ =
            phase % power(10, digitPos + 1) / power(10, digitPos);
          if (newPhase > 0 && dispDigit > 0) 
          {
            phase -= power(10, digitPos);
            updateDisplay = true;
          }
#endif
        } break;
      case 5: 
      {
          if (currentMode == 0)
            currentMode = 2;
          else
            currentMode--;
          updateDisplay = true;
        } break;
    }
  }
}
unsigned long power(int a, int b)
{
  unsigned long res;
  if (b == 0) 
  {
    res = 1;
  } else 
  {
    res = a;
    for (int i = 0; i < (b - 1); i++) 
    {
      res *= a;
    }
  }
  return res;
}

 

 

Fig. Rotary Encoder 회전방향 확인 Test

Rotary Encoder는 A, B Signal Line으로 Pulse 형태의 전압이 D2, D3 Pin으로 발생하는데 이를 이용해서 Rotary Encoder의 회전 방향을 알 수 있습니다.

정방향 회전시 Serial Monitor에서 CW 출력, 역방향 회전시 CCW가 출력됩니다.

  2.4 I2C LCD 기능 확인

다음은 LCD 동작을 확인하는 단계인데 이건 너무 간단히 해결되어 버려 당황스럽군요. :o

우선 LCD Board에 Connector로 체결하기위해 작업한 Crimp가 잘못 작업되어 전원 라인이 단선되는 문제가있어 수정했고 두번째 문제는 LCD Board 뒷편에 Backlight 밝기를 조절하는 가변 저항이 있었는데 처음에는 최대 밝기로 되어있었고 그걸로 밝기를 낮추니까 글씨가 보이더군요. -_-;;

3. AD9833 출력 파형

AD9833은 Sine Wave, Triangle Wave, Square Wave 출력이 가능합니다.

  3.1 Sine Waveform

아래는 1[kHz], 100[kHz] 출력 지령시 결과 파형

Fig. Sine Waveform

 

Sine Waveform은 위 파형과 같이 Vpp 11[V] = 7.9[Vrms] 까지 출력이 가능함을 확인했고 10[kHz] 이상부터는 Amplitude가 감쇠되는 것을 확인하였습니다. 고주파수로 갈 수록 Impedance가 커져 그런것으로 추측됩니다.

  3.2 Triangle Waveform

아래는 10[Hz], 10[kHz] 출력 지령시 결과 파형

Fig. Triangle Waveform

Triangle Waveform은 약 1[kHz] 이하에서는 완전한 삼각파였으나 1[kHz] 초과부터 삼각형 꼭짓점이 뭉그러지는 것을 확인할 수 있었습니다.

  3.3 Square Waveform

아래는 1[kHz] 출력 지령시 결과 파형

Fig. Square Waveform

Square Waveform은 1[kHz] 출력으로 설정했는데 실제로는 절반인 약 500[Hz]로 출력이 되더군요... 흠..

4. 결론

현재 제작 진행에 복붙하여 사용한 Source Code에는 약간의 Bug가 존재하더군요. 예를 들면 너무 빠르게 Rotary Encoder를 CCW로 돌렸을때 Counter가 씹히는 등의 오동작이 발생하는 경우가 있습니다.

그 외에 Menu tree가 직관적이고 쉽게 되어있기는 하지만 출력 주파수 설정시 1의 자리부터  1,000,000의 자리까지 숫자를 하나씩 설정 해줘야 하고 중간에 취소나 뒤로가기 같은 기능이 없어 편의성 측면에서는 개선 할만한 부분이 있더군요.  :D 

그리고 이건 Scope의 문제인지 아니면 Clock이 문제인지는 모르겠지만 출력 주파수의 오차 성분 위 파형과 같이 존재 했습니다.  

우선 오래전에 Function Generator를 하나 갖고 싶어서 오랫만에 납땜하면서 재밌게 제작을 완료 해보았습니다. 원래 Enclosure에 Hole을 더 가공해서 Rotary Encoder나 Variable Resistor, LCD 등을 이쁘게 배치해야 하지만 우선 동작을 한번봐서 그런지...  다른 것을 또 만들어보고 싶어 일단 함수 발생기 제작기는 여기서 일단락하고 향후 여유가 생기면 Arduino가 아닌 다른 MCU로 변경 및 LCD Menutree 및 Bug 개선 등을 해보고자 합니다. :)

 

[참고자료]

[1] : https://www.youtube.com/watch?v=Y1KE8eAC9Bk

 

[2] : https://www.allaboutcircuits.com/projects/how-to-DIY-waveform-generator-analog-devices-ad9833-ATmega328p/

 

How to Build Your Own Function Generator Using Analog Devices’ AD9833 - Projects

Tired of using your PC or mobile phone as a signal generator? It’s time to upgrade to a standalone one by making it yourself.

www.allaboutcircuits.com

 

[3] : https://www.arduino.cc/en/software/

 

https://www.arduino.cc/en/software/

 

www.arduino.cc

 

[4] : https://www.instructables.com/DIY-FunctionWaveform-Generator/

 

DIY Function/Waveform Generator

DIY Function/Waveform Generator: In this project we will have a short look at commercial function/waveform generators in order to determine what features are important for a DIY version. Afterwards I will then show you how to create a simple function gener

www.instructables.com

 

[5] : https://www.youtube.com/watch?v=gBOk1fSZUIs

 

+ Recent posts