-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.json
1 lines (1 loc) · 279 KB
/
index.json
1
[{"content":"How to add Developer PowerShell for Visual Studio 2022 on Terminal Tab of VS Code. 윈도우 환경에서 Visual Studio(Code 아님) 또는 관련된 개발툴을 설치하면 Devloper Command Prompt나 Developer PowerShell을 별도로 제공을 하는데 이러한 환경에서 커맨드 작업이 필요한 경우가 있습니다.\n윈도우즈 환경 기준에서 VS Code를 사용하는 경우 터미널 탭에서 Command Prompt나 PowerShell를 선택 할 수 있고 만약 WSL를 설치하는 경우에는 Bash Shell 등이 자동으로 추가되어 여러 환경에서 커맨드를 실행할 수 있지만\nVS 에서 제공하는 Developer PowerShell은 VS Code 터미널 항목에는 자동으로 추가가 되지 않아서 보통은 윈도우 검색을 통해 단축 경로를 직접 실행 합니다.\n대신 VS Code에는 사용자 정의 파라미터(args)를 포함한 PowerShell 이나 Git Bash 환경을 추가 할 수 있는데, 이를 활용하여 Developer PowerShell을 VS Code내 터미널 탭에 추가할 수 있습니다.\n먼저 Developer PowerShell을 실행할 수 있는 스크립트가 있는 경로를 확인합니다. Visual Studio 2022 Community의 경우 C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/Launch-VsDevShell.ps1 에 스크립트가 위치하는 것을 확인 할 수 있습니다.\n경로를 확인 하였으면 VS Code의 setting.json에 terminal.integrated.profiles.{os} 항목을 아래와 같이 추가합니다.\n1 2 3 4 5 6 7 8 9 10 \u0026#34;terminal.integrated.profiles.windows\u0026#34;: { \u0026#34;Developer PowerShell for VS2022\u0026#34;: { \u0026#34;source\u0026#34;: \u0026#34;PowerShell\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-noexit\u0026#34;, \u0026#34;-File\u0026#34;, \u0026#34;C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/Launch-VsDevShell.ps1\u0026#34; ] } } 예제를 참고하면 Developer PowerShell for VS2022는 사용자 임의로 지정할 수 있으며, 터미널 선택 화면에서 해당 명칭으로 표시가 됩니다. source는 PowerShell을 지정하고 args에 -noexit와 -File 옵션과 함께 앞서 확인한 스크립트의 경로를 추가합니다.\n이렇게 설정하면 터미널 선택지에서 Developer PowerShell for VS2022가 추가 된 것을 확인 할 수 있습니다.\n그리고 실행을 하면 Could not start Developer PowerShell using the script path. 라는 경고성 메시지가 나오지만 다음과 같이 VS Code 내에서 Developer PowerShell을 직접 실행할 수 있게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. 새로운 기능 및 개선 사항에 대 한 최신 PowerShell을 설치 하세요! https://aka.ms/PSWindows Could not start Developer PowerShell using the script path. Attempting to launch from the latest Visual Studio installation. ********************************************************************** ** Visual Studio 2022 Developer PowerShell v17.2.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** PS C:\\Users\\username\\source\\repos\u0026gt; 하지만 경고 메시지가 나오므로 다른 방법으로 실행을 하도록 하겠습니다. C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Visual Studio 2022\\Visual Studio Tools의 경로를 확인하면 검색 시 결과로 나오는 Developer PowerShell for VS 2022 바로가기(shortcut) 파일이 해당 폴더에 존재를 합니다. 이 파일을 마우스 오른쪽 버튼을 눌러서 속성을 확인하면, Launch-VsDevShell.ps1를 이용하지 않고 Microsoft.VisualStudio.DevShell.dll 파일을 이용하여 실행을 하도록 되어 있습니다.\n해당 속성에 있는 방법과 동일하게 옵션을 활용하면 다음과 같이 설정을 할 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 \u0026#34;terminal.integrated.profiles.windows\u0026#34;: { \u0026#34;Developer PowerShell for VS2022\u0026#34;: { \u0026#34;source\u0026#34;: \u0026#34;PowerShell\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-noe\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;\u0026amp;{Import-Module \\\u0026#34;C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/Microsoft.VisualStudio.DevShell.dll\\\u0026#34;; Enter-VsDevShell 31daa83c}\u0026#34; ] } } 새로 변경된 방법으로 실행을 하면 아까와 달리 경고 없이 다음과 같이 Developer PowerShell을 실행 할 수 있습니다.\n1 2 3 4 5 6 7 ********************************************************************** ** Visual Studio 2022 Developer PowerShell v17.2.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** PS C:\\Users\\username\\source\\repos\u0026gt; ","description":"","id":0,"section":"posts","tags":["vscode","tips"],"title":"Visual Studio Code 터미널에 개발자 PowerShell 추가하기","uri":"/posts/dev/common/2022-07-03-how-to-add-developer-powershell/"},{"content":"테스트 롬파일 CPU 에뮬레이션이 어느정도 완료가 되었으면 실제 코드를 구동 시켜서 정상적으로 동작 하는지 검증이 필요합니다. 검증을 위해서 6502에 맞게 컴파일된 바이너리가 필요한데 NES ROM 파일에는 당연히 게임 실행을 위한 바이너리 코드가 포함되어 있으므로 이를 활용하면 됩니다.\n하지만 단순히 ROM 파일이 있어도 연산이 정상 동작을 하는지 여부를 알기는 어렵습니다. 연산을 수행한 결과와 레지스터 변화를 예측할 수가 없고, 구현된 명령어가 모두 활용되는 지 알 수 없기 때문입니다.\n다행히도 Emulator tests1나 Collection of test ROMs for testing a NES emulator2 페이지를 참고하면 에뮬레이션 테스트만을 위한 ROM 파일이 공유가 되고 있는 것을 알수 있습니다.\n여기서는 NES CPU 에뮬레이션 쪽에서는 골드 스탠다드?로 불리우는 The Ultimate NES CPU test ROM3을 이용하여 테스트를 진행하였습니다. 이 ROM 파일은 연산 과정에 따른 레지스터 변화에 대한 로그(log)가 함께 공개되어 있으므로 해당 로그와 유사한 포맷으로 로그를 기록하면 상호 비교하여 구현이 잘 되었는지 확인 할 수 있습니다.\n관련 파일이나 정보는 Emulator tests 페이지의 CPU Tests 테이블 밑에서 두번 째 항목의 링크를 참조하거나 아래 직접 링크를 통해 확인이 가능합니다.\n파일: http://nickmass.com/images/nestest.nes\n문서: https://www.qmtpro.com/~nes/misc/nestest.txt\n로그: https://www.qmtpro.com/~nes/misc/nestest.log\niNES 롬 구조(iNES ROM Structure) 롬 파일은 카트리지의 메모리를 덤프 한 것 입니다. Raw 데이터만을 그대로 옮겨 놓은 것들도 있지만 게임 정보나 에뮬레이션에 필요한 정보들이 함께 포함되어 있는 형태도 있습니다.\n그렇기 때문에 롬 파일도 하나의 포맷이 아니라 여러 포맷으로 공유가 되며 NES의 경우 보편적으로 초창기 NES 에뮬레이터를 개발한 Marat Fayzullin의 iNES 포맷이 많이 사용되고 있습니다.\n테스트 롬도 iNES 포맷으로 되어있으므로 짧막하게 관련 내용을 살펴보겠습니다.\n헤더 포맷 (Header Format)4 iNES는 버전에 따라 대략 3 ~ 4개의 섹션으로 이루어져 있습니다. 가장 첫 16바이트는 헤더 영역으로 PGR, CHR 영역 사이즈, Mapper 타입, RAM 크기 등 정보를 담고 있습니다.\noffset을 고려하여 아래와 같이 코드로 표현 할 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 internal unsafe struct INesHeader { public fixed byte magic[4]; public byte numPRG; public byte numCHR; public byte control1; public byte control2; public byte numRAM; public byte flags9; public byte flags10; public fixed byte reserved[5]; } magic은 \u0026lsquo;NES\u0026rsquo; 문자열을 담고 있는 매직 코드 입니다. NES 파일인지 여부를 확인하는 용도로 사용합니다.\nnumPGR은 16kB 단위 크기의 PGR 뱅크 갯수를 numCHR은 8kB 단위 크기의 CHR 뱅크 갯수를 의미합니다.\ncontrol1, control2는 데이터에 대한 부가정보를 담고 있습니다. 아래 NesDev 위키 사이트의 INES 페이지5 설명을 일부 첨부 합니다.\nControl 1\r76543210\r||||||||\r|||||||+- Mirroring: 0: horizontal (vertical arrangement) (CIRAM A10 = PPU A11)\r||||||| 1: vertical (horizontal arrangement) (CIRAM A10 = PPU A10)\r||||||+-- 1: Cartridge contains battery-backed PRG RAM ($6000-7FFF) or other persistent memory\r|||||+--- 1: 512-byte trainer at $7000-$71FF (stored before PRG data)\r||||+---- 1: Ignore mirroring control or above mirroring bit; instead provide four-screen VRAM\r++++----- Lower nybble of mapper number\rControl 2\r76543210\r||||||||\r|||||||+- VS Unisystem\r||||||+-- PlayChoice-10 (8KB of Hint Screen data stored after CHR data)\r||||++--- If equal to 2, flags 8-15 are in NES 2.0 format\r++++----- Upper nybble of mapper number\r이외에 나머지 바이트는 거의 사용되지 않는 항목이거나 예비 영역입니다.\n파일 읽기 CPU 테스트를 위해서는 PGR 영역만 가져오면 되므로 매직 코드로 iNES 포맷인지 확인 후 PGR 뱅크 개수 * 16kB 만큼 데이터를 읽어서 배열에 저장하여 이 정보를 가지고 테스트를 합니다. 참고로 NES의 경우 오래된 콘솔이고 게임 롬 사이즈가 수 백 킬로바이트 정도 수준이므로 모든 정보를 변수에 저장해서 사용해도 크게 무리가 없습니다.\n저장한 배열을 PC에 맞추어 순차대로 읽으면서 연산을 수행 할 것 입니다. 실제 게임 구동을 위해서라면 mirroring, mapper 정보에 대한 이해가 필요하지만 여기서는 CPU 테스트만 할 것이므로 신경쓰지 않아도 됩니다.\n아래는 iNES 파일 구조를 읽어서 카트리지 인스턴스를 생성하여 반환하는 예제 입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 class INes { public unsafe Cartridge LoadFile(string path) { FileStream file = null; try { file = File.OpenRead(path); } catch (Exception e) { if (file != null) { file.Close(); } throw; } BinaryReader binary = new BinaryReader(file); byte[] headerBytes = binary.ReadBytes(16); INesHeader header = Utils.BytesToStructure\u0026lt;INesHeader\u0026gt;(headerBytes); // check file format if (Convert.ToChar(header.magic[0]) != \u0026#39;N\u0026#39; || Convert.ToChar(header.magic[1]) != \u0026#39;E\u0026#39; || Convert.ToChar(header.magic[2]) != \u0026#39;S\u0026#39;) { throw new Exception(\u0026#34;Not INES Format\u0026#34;); } bool battery = ((header.control1 \u0026gt;\u0026gt; 1) \u0026amp; 0x01) != 0; // mapper byte mapper = (byte)((header.control2 \u0026amp; 0xF0) | ((header.control1 \u0026amp; 0xF0) \u0026gt;\u0026gt; 4)); Debug.WriteLine(\u0026#34;Control1: {0:X2}, Control2: {1:X2}\u0026#34;, header.control1, header.control2); // format version int version = ((header.control2 \u0026gt;\u0026gt; 2) \u0026amp; 0x03); if (version != 0) { throw new Exception(\u0026#34;Can not support INES 2.0 Format\u0026#34;); } // screen mirroring bool fourScreen = (header.control1 \u0026amp; 0x80) != 0; bool vertical = (header.control1 \u0026amp; 0x01) != 0; Debug.WriteLine(\u0026#34;four: {0}, vert: {1}\u0026#34;, fourScreen, vertical); byte mirroring = (byte)(((header.control1 \u0026gt;\u0026gt; 2) \u0026amp; 0x02) | (header.control1 \u0026amp; 0x01)); // has trainer? if (((header.control1 \u0026gt;\u0026gt; 2) \u0026amp; 0x1) != 0) { // unused binary.ReadBytes(512); } // read PRG-ROM byte[] prg = binary.ReadBytes(header.numPRG * 0x4000); // 16384 // read CHR-ROM byte[] chr; if (header.numCHR == 0) { chr = new byte[8192]; } else { chr = binary.ReadBytes(header.numCHR * 0x2000); // 8192 } Debug.WriteLine(\u0026#34;{0}{1}{2}, {3:X}, {4:X}, {5}, {6}, mirror: {7}\u0026#34;, Convert.ToChar(header.magic[0]), Convert.ToChar(header.magic[1]), Convert.ToChar(header.magic[2]), prg[0], prg[16383], mapper, header.numPRG, mirroring); binary.Close(); file.Close(); return new Cartridge(prg, chr, mapper, mirroring, battery); } } 테스트 CPU가 명령어 사이클(nstruction Cycles)을 수행 할 수 있고 롬파일에서 PGR 영역을 가져올 수 있으면 테스트 롬 파일을 읽어서 연산이 제대로 수행하는지 확인을 합니다.\n로그 출력 실제 연산이 잘 이루어졌는지는 확인을 위해서 로그를 출력해야 합니다. 위에 테스트 롬의 로그 파일을 다운 받아서 어떤 포맷으로 출력 하는지 확인 하도록 합니다.\nC000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 21 CYC:7\rC5F5 A2 00 LDX #$00 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 30 CYC:10\rC5F7 86 00 STX $00 = 00 A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 36 CYC:12\rC5F9 86 10 STX $10 = 00 A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 45 CYC:15\rC5FB 86 11 STX $11 = 00 A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 54 CYC:18\rC5FD 20 2D C7 JSR $C72D A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 63 CYC:21\rC72D EA NOP A:00 X:00 Y:00 P:26 SP:FB PPU: 0, 81 CYC:27\r...\r로그를 라인 단위로 확인하면 다음과 같은 순서대로 출력 된 것을 확인 할 수 있습니다.\n PC, Opcode, Operand, Instruction Addressing, A(ccumulator), X, Y, P(Status), SP(Stack Pointer), PPU cycles, CPU cycles(누적) 해당 정보들을 Opcode를 실행 하는 과정에서 문자열로 만들어 출력 할 수 있도록 합니다.\n1 2 3 4 5 6 7 8 9 private string WriteLog(byte opcode) { ... return String.Format(\u0026#34;{0:X4} {1:X2} {2} {3}\\t{4} {5,-26} A:{6:X2} X:{7:X2}\u0026#34; + \u0026#34;Y:{8:X2} P:{9:X2} SP:{10:X2} PPU:{11} CYC:{12}\u0026#34;, registers.PC, opcode, w1, w2, name, addrssing, registers.ACC, registers.X, registers.Y, registers.SR.GetByte(), registers.SP, ppuCycles, cpuCycles); } 실제 구현에서는 포맷이 완전하게 동일하지는 않고 어드레싱 포맷이 반영된 operand 주소는 단순화 시켜서 출력 하도록 했습니다. 대신 그 외 정보는 모두 동일하도록 하여 적어도 검증에 문제가 생기지 않도록 하였습니다.\n아래 그림과 같이 텍스트 박스에 로그를 출력하고 출력이 완료되면 원본 로그와 차이가 있는지 비교를 하였습니다.\n아래는 로그의 처음 3줄과 마지막 3줄 예시 입니다.\n// 원본 로그\rC000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 21 CYC:7\rC5F5 A2 00 LDX #$00 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 30 CYC:10\rC5F7 86 00 STX $00 = 00 A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 36 CYC:12\r...\rC69F 8D 07 40 STA $4007 = FF A:00 X:FF Y:15 P:27 SP:FB PPU:233,179 CYC:26544\rC6A2 60 RTS A:00 X:FF Y:15 P:27 SP:FB PPU:233,191 CYC:26548\rC66E 60 RTS A:00 X:FF Y:15 P:27 SP:FD PPU:233,209 CYC:26554\r// 출력 로그\rC000 4C F5 C5\tJMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 21 CYC:7\rC5F5 A2 00 LDX $C5F6 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 30 CYC:10\rC5F7 86 00 STX $00 A:00 X:00 Y:00 P:26 SP:FD PPU: 0, 36 CYC:12\r...\rC69F 8D 07 40\tSTA $4007 A:00 X:FF Y:15 P:27 SP:FB PPU:233,179 CYC:26544\rC6A2 60 RTS A:00 X:FF Y:15 P:27 SP:FB PPU:233,191 CYC:26548\rC66E 60 RTS A:00 X:FF Y:15 P:27 SP:FD PPU:233,209 CYC:26554\r지금은 검증이 완료된 상태이므로 출력이 동일하게 이루어진 것을 확인 할 수 있습니다.\n개발 중에는 당연히 실수나 또는 잘못된 구현으로 로그가 불일치가 발생하였고, 문제가 발생한 로그를 참조하여 해당 명령어나 어드레싱 모드의 문제를 수정하고 반복 테스트하는 과정을 거쳤습니다.\n유의 및 고려사항 테스트 로그와 동일한 PC, Opcode, 각종 레지스터와 사이클이 모두 일치하는 것을 확인으로 CPU 에뮬레이션을 1차료 마무리 하였습니다.\n로그 결과만 본다면 문제없이 구현된 것이 맞지만 확인이 필요한 사항이 있습니다.\n먼저 원본 로그의 CPU 사이클이 처음부터 7으로 되어 있습니다. 로그에서 사이클은 연산 완료 후 사이클이 아니라 명령 실행 전 사이클 입니다. (연산 후 사이클이라고 하기에는 JMP 에서 소요하는 사이클과 일치하지 않습니다.)\n6502에서는 인터럽트 발생 시 약 7사이클이 소요되는데, 초기 PC는 Reset Interrupt Vector($FFFC)에 있는 값으로 결정되기 때문에 Reset으로 인터럽트가 발생하여 PC를 새로 초기화 한 것을 기준으로한 것이 아닐까 하는 생각이 들었습니다.\n일단은 테스트 시 CPU 사이클 초기 값을 7이라고 지정하여 테스트를 진행 하였습니다.\n그리고 로그의 처음 PC는 C000으로 리셋 인터럽트 벡터의 값이 $C000 이어야 할 것 같은데, 테스트 롬파일을 확인해보면 실제로는 $C004가 기록되어 있었습니다. 해당 값의 불일치 원인이 명확하지 않아서 PC 값을 $C000로 기본 지정하여 테스트를 수행했습니다.\n현재는 CPU 기능상의 테스트만 고려하였기 떄문에 임의로 수정을 하여 테스트를 완료 하였지만 차후 완성된 에뮬레이션을 위해서는 해당 부분에 대한 명확한 원인 파악과 동작 정의가 필요할 것으로 생각됩니다.\n마무리 및 계획 어렵게나마 NES CPU 에뮬레이션이 부분적으로 완료되었습니다. 부분적이라고 한 것은 아직 다른 테스트 롬이나 실제 게임이 구동한 상태인지 추가 검증이 완료되지 않았기 때문입니다.\n그래도 어느정도 가능성을 확인 하였기 때문에 일단 구현 된 것을 완성도를 높이기 위해서 리팩토링과 추가 검증을 수행하고 PPU 개발을 진행해야 합니다. PPU에 대해서는 사실 일부 스터디와 구현이 진행중이었으나 개념이 복잡하고 어려운 부분이 있어서 이해를 하고 진행하려다보니 현재는 중단상태입니다. 짧은 시간내에 바짝 하고 싶었지만 결과보다는 과정이 중요하니 느긋하게 진행 될 것 같습니다.\n이 작업의 결과물도 공개를 하려고 하고 있는데 지금은 어렵고 PPU 완료한 이후 통합적으로 게임 구동이 가능한 상태 이후가 되지 않을까 합니다.\n그럼 CPU 에뮬레이션 포스팅은 여기서 마무리하고 나중에 PPU 구현이 완료될 때 PPU 관련 포스트를 남기겠습니다.\n https://wiki.nesdev.org/w/index.php/Emulator_tests\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://github.com/christopherpow/nes-test-roms\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.qmtpro.com/~nes/misc/nestest.txt\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://bugzmanov.github.io/nes_ebook/chapter_5.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://wiki.nesdev.org/w/index.php?title=INES\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":1,"section":"posts","tags":["프로젝트","NES","Emulator"],"title":"NES CPU(6502) 에뮬레이션 - 4 (마무리)","uri":"/posts/projects/nesdev/2022-03-07-nes-cpu-emulation-04/"},{"content":"테스트 환경에서 기본적인 기능을 구현하고 회로와 기구를 구성합니다.\n제작 회로 설계 회로 설계 자체는 모두 모듈을 사용하기 때문에 크게 어려운 점이 없습니다. NRF52와 배터리 충전 역시 모듈을 사용하기 때문에 엔코더와 스위치 연결만 잘 해주어도 됩니다. NRF52 모듈은 Holyiot사의 YJ-170951을 사용하였습니다. 크기가 9.4 x 9.25mm 밖에 안될 정도로 매우 작습니다.\n모듈 회로도나 핀 맵은 데이터시트에서 확인 가능합니다.\n배터리 충전은 SZH-EK026(TP4056) 라는 모듈을 사용했습니다. 간단히 vbat 부분에 배터리 연결하여 사용할 수 있습니다. 관련 정보는 데이터시트 페이지에서 확인 가능합니다.\n엔코더와 모듈 들을 다음과 같이 연결했습니다.\n참고로 공간이 협소하다 보니 원래 계획했던 회로 설계를 모두 반영하지 못했습니다. (LED 인디케이터, 배터리 관련) 그리고 BLE 모듈이 펌웨어 개발 테스트 후 패드 몇개가 날아가버려서 부득이하게 연결가능한 핀만 사용했습니다.\n아무래도 비용이나 기타 사유로 진행하지 않았던 PCB 설계/제작을 향후 버젼에는 고려 해야할 것 같습니다.\n기구 배치 고려 엔클로져는 50 x 50 x 25 mm 크기로 생각보다 많이 타이트 합니다. 회로를 실제로 제작하기에 앞서서 어떻게 엔클로져에 담을 것인지 고민이 필요했습니다.\n일단 하단에 충전모듈을 놓고 가배치 후 버튼이나 USB 포트가 연결될 부위에 구멍을 뚫었습니다.\n\r\r\r\r \r회로 제작 이전 포스트에서 언급하였 듯이 기성품으로는 서피스 다이얼과 동일한 형태로 구성이 어렵기 때문에 엔코더 노브 부분과 회로가 포함될 엔클로져 구분이 필요합니다.\n\r\r\r\r먼저 만능 기판(universal board)에 충전 모듈을 납땜하여 붙이고 사이드에 BLE 모듈에서 점퍼선을 날려서 보드에 연결했습니다. 안테나 방사 특성을 고려하여 가급적 BLE 모듈의 안테나는 보드사이에 위치 하지 않고 외곽으로 빠질 수 있도록 합니다.\n\r\r\r그 후 엔코더가 연결될 기판을 별도로 마련하여 상단 하단이 핀 헤더(pin header)로 연결 되도록 합니다. 두 보드가 최대한 가깝게 붙어야 하므로 핀 헤더 소켓은 사용하지 못했습니다.\n회로가 완료되었으면 펌웨어를 올리고 테스트를 해봅니다. 동작에 큰 이상이 없으면 기구와 조립을 합니다.\n기구 조립 조립은 무난하게 진행되는 듯 하였으나 역시 공차 문제로 딱 들어맞지 못했습니다. 뚜껑이 잘 안닫히는 문제가 발생했는데 추후 조치하기로 하고 일단 조립을 마무리 합니다.\n회전 시 디바이스가 움직이지 않도록 실리콘 패드를 하단에 붙여주었습니다.\n\r\r\r\r\r\r테스트 및 시연 간단한 테스트 동작 화면 입니다. 길게 버튼을 누르면 현재 활성화된 애플리케이션이나 환경에 맞게 원형 모양의 메뉴가 뜨고 메뉴를 선택하여 휠로 조절 할 수 있습니다.\n기본적인 동작은 큰 문제없이 가능하나 휠 동작 반응성이 좀 떨어지는 면이 있어서 개선이 필요합니다.\n동영상: https://youtu.be/cQLYvCz1rvM\n마무리 부족하게나마 서피스 다이얼을 흉내내는 디바이스를 제작 해보았습니다. 아직 리눅스에서 테스트 등도 이루어지지 않았고 회로상이나 펌웨어 상으로 보완하거나 개선해야 할 사항이 많이 남아 있습니다. \u0026lsquo;Wanna be\u0026rsquo;를 표방하고 있기 때문에 일회성으로 끝내지 않고 지속적으로 개선된 버전으로 탈바꿈 하여 비슷하게 따라가는 과정을 거칠까 합니다.\n소스코드는 아직 불안정한 부분도 있고 추가로 테스트가 많이 남아서 공개는 차후에 이루어질 것 같습니다. 아니면 차기 버젼에서 할 수 도 있구요 때가 되면 별도로 포스팅하고 Github에 공개 하겠습니다.\n그럼 이번 프로젝트는 ver 0.5 정도라고 하고 차기 버전을 기약하며 마무리 하겠습니다.\n http://www.holyiot.com/eacp_view.asp?id=298\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":2,"section":"posts","tags":["project","취미전자","Hobby Electronics","Surface Dial"],"title":"마이크로소프트 서피스 다이얼 클론 제작 - 3 (v0.5 마무리)","uri":"/posts/projects/suface-dial/2022-02-28-how-to-make-3/"},{"content":"Wanna be surface dial 펌웨어 개발 nrf52 SDK 폴더내에 샘플이 포함되어 있습니다. examples 폴더 안에 관련 샘플들을 찾아볼 수 있는데, 서피스 다이얼은 HID 디바이스 이므로 ble_app_hids_keyboard나 ble_app_hids_mouse를 베이스로 작성하면 어렵지 않게 시작할 수 있습니다.\n다만 BLE 스펙이나 nrf52 개발환경에 익숙하지 않은 분들에게는 좀 복잡하고 어려울 수 있습니다. 그래서 적어도 BLE 스펙상에서 통신 커넥션이나 시퀀스에 대해서는 어느정도 숙지를 해야 어려움이 없습니다.\nHID 관련 부분은 USB의 디스크립터와 유사하니 이전 포스트나 USB 스펙을 참고하시면 됩니다.\n여기서는 BLE 기초에 대하여 설명하지 않으므로 기본적인 정보가 필요하시면 The Basics of Bluetooth Low Energy1의 내용이 도움이 될 수 있습니다. 그리고 BLE 버젼별 차이가 궁금하신 경우 The differences between BLE 4.0, BLE 4.1, BLE 4.2, and BLE 52을 참고하세요.\n프로젝트 준비 및 BSP(Board Support Package) 선수 지식이 이미 준비가 되었다면 ble_app_hids_keyboard 예제를 기반으로 개발을 시작합니다.\n예제를 먼저 복사 한 후 프로젝트를 불러오면 main.c 파일에 peripheral 부터 BLE 스택 설정, HID 관련 코드가 전부 한 파일내에 들어있어 복잡한 상태입니다. 여기서 BLE 설정 및 동작과 연관된 코드를 분리하고 빌드하여 기존 예제 동작에 문제가 없는 지 확인을 합니다.\n별다른 오류가 없다면 구성한 하드웨어에 맞게 코드 수정을 합니다.\n예제 프로젝트들은 다양한 개발 보드 지원을 위해서 BSP 설정을 보드별로 선택하도록 되어 있습니다. 프로젝트 내의 bsp.c 와 board.c 파일을 참고하면 버튼과 LED 제어를 위한 인터페이스가 정의 되어 있습니다. 여기서는 특정 보드를 사용하지 않기 때문에 보드 관련 설정은 무시할 것 입니다.\n프로젝트 preprocess definitions에 BOARD_CUSTOM을 추가해 줍니다.\n그리고 custom_board.h를 추가하여 포트 핀 번호 정의를 해주도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define PRIMARY_BUTTON 15 #define LEDS_NUMBER 1 #define LED_START 28 #define LED_1 28 #define LED_STOP 28 #define LEDS_ACTIVE_STATE 0 #define LEDS_INV_MASK LEDS_MASK #define LEDS_LIST { LED_1 } #define BSP_LED_0 LED_1 #define BUTTONS_NUMBER 2 #define BUTTON_START 0 #define BUTTON_1 0 #define BUTTON_2 PRIMARY_BUTTON #define BUTTON_STOP PRIMARY_BUTTON #define BUTTON_PULL NRF_GPIO_PIN_PULLUP #define BUTTONS_ACTIVE_STATE 0 #define BUTTONS_LIST { BUTTON_1, BUTTON_2 } #define BSP_BUTTON_0 BUTTON_1 #define BSP_BUTTON_1 BUTTON_2 bsp 모듈 사용을 위해서는 PRIMARY_BUTTON 부분을 제외하고 LED, BUTTON에 대한 정의가 필요합니다.\n먼저 LED는 BLE 상태나 기타 상태 들을 표시(Indication) 목적으로 사용됩니다. 여기서는 Disconnected 및 Advertising 상태인지 Connected 상태인지 표시 정도만 있어도 되므로 1개의 LED만 고려하여 P0.28번핀을 해당 핀으로 설정 합니다.\n버튼은 adverting용 버튼과 primary (엔코더) 버튼 2개만 할당하였습니다. 버튼의 세부설정은 bsp_btn.c에서 확인 할 수 있습니다. 이 파일은 sdk에 포함된 파일로 이 파일을 프로젝트로 복사하여 필요에 맞게 수정을 했습니다.\n엔코더 디코더 엔코더에 대한 설명은 이미 유튜브 컨트롤러 제작3에서 포스팅 하였었습니다. 지난 번에는 2개의 채널을 polling 하는 방법으로 엔코더의 회전 방향과 증감량을 측정하였습니다. 그런데 이번에 사용한 NRF52832의 경우 Quadrature Encoder를 디코딩 할 수 있는 Quadrature Decoder(이하 QDEC)4를 포함하고 있습니다.\n드라이버 코드는 다음과 같이 작성하였습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 uint8_t accumulation_value = 0; static void qdec_event_handler(nrf_drv_qdec_event_t event) { dial_report_t dial_report = {0}; int16_t acc; uint16_t accdbl; uint8_t value; if (event.type == NRF_QDEC_EVENT_REPORTRDY) { accdbl = event.data.report.accdbl; acc = event.data.report.acc; value = event.data.sample.value; accumulation_value += event.data.report.acc; if (accumulation_value \u0026gt; 3) { dial_report.rotation = -100; ble_send_input_report((uint8_t *)(\u0026amp;dial_report)); accumulation_value = 0; } else if (accumulation_value \u0026lt; -3) { dial_report.rotation = 100; ble_send_input_report((uint8_t *)(\u0026amp;dial_report)); accumulation_value = 0; } } } void qdec_init(void) { ret_code_t err_code; nrf_drv_qdec_config_t config = { .reportper = (nrf_qdec_reportper_t)NRF_QDEC_REPORTPER_10, .sampleper = (nrf_qdec_sampleper_t)NRF_QDEC_SAMPLEPER_1024us, .psela = ENCODER_CH_A, .pselb = ENCODER_CH_B, //.pselled = NRFX_QDEC_CONFIG_PIO_LED, //.ledpre = NRFX_QDEC_CONFIG_LEDPRE, //.ledpol = (nrf_qdec_ledpol_t)NRFX_QDEC_CONFIG_LEDPOL, .dbfen = true, .sample_inten = true, .interrupt_priority = NRFX_QDEC_CONFIG_IRQ_PRIORITY, }; err_code = nrf_drv_qdec_init(\u0026amp;config, qdec_event_handler); APP_ERROR_CHECK(err_code); } 지난 프로젝트에서는 폴링(polling) 방식으로 엔코더 시그널 변화를 인식했다면 이번 프로젝트에서는 qdec 장치를 이용하여 엔코더의 회전이 발생 시 인터럽트와 콜백 이벤트로 엔코더의 회전 여부와 방향을 체크하도록 하였습니다.\n엔코더는 지난번과 동일한 벤더 제품으로 1 클릭 회전당 4회 채널 상태변화가 발생합니다. qdec은 리포트 타이밍에 따라 내부적으로 회전 변화량을 누적하여 반환을 하는데 1클릭 당 1 ~ 3 사이의 누적된 변화량이 연속적으로 리포팅 됩니다. 이 누적 변화량이 한쪽 방향으로 4회가 되는지 여부 확인을 위해서 임시 변수에 누적하여 저장 및 확인을 하고 재 설정하는 방식으로 1클릭을 인식하도록 하였습니다.\n참고로 dial_report는 BLE를 통해 전송할 데이터를 담는 구조체를 별도로 만들고 해당 구조체에 값을 저장한 것로 나중에 따로 설명하겠습니다.\nHID (Human Interface Device) Descriptor BLE HID 디스크립터는 USB와 동일한 디스크립터 형식으로 선언합니다. keyboard 샘플을 기반으로 하였으므로 기존 디스크립터는 키보드 장치를 위한 디스크립터 입니다. 이 부분을 Radial Controller 디스크립터로 변경해야합니다.\nWindows radial controller sample report descriptors5를 참고하면 샘플 디스크립터가 선언되어 있습니다. Haptick Feedback은 optional이고 현재 하드웨어에는 적용되어 있지 않으니 일단 제외하고 rotation과 primary button만 지정된 디스크립터를 적용합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 // Integrated Radial Controller TLC 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x0e, // USAGE (System Multi-Axis Controller) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x01, // REPORT_ID (Radial Controller) 0x05, 0x0d, // USAGE_PAGE (Digitizers) 0x09, 0x21, // USAGE (Puck) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Buttons) 0x09, 0x01, // USAGE (Button 1) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x01, // REPORT_SIZE (1) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x37, // USAGE (Dial) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x0f, // REPORT_SIZE (15) 0x55, 0x0f, // UNIT_EXPONENT (-1) 0x65, 0x14, // UNIT (Degrees, English Rotation) 0x36, 0xf0, 0xf1, // PHYSICAL_MINIMUM (-3600) 0x46, 0x10, 0x0e, // PHYSICAL_MAXIMUM (3600) 0x16, 0xf0, 0xf1, // LOGICAL_MINIMUM (-3600) 0x26, 0x10, 0x0e, // LOGICAL_MAXIMUM (3600) 0x81, 0x06, // INPUT (Data,Var,Rel) // not supported // 0x09, 0x30, // USAGE (X) // 0x75, 0x10, // REPORT_SIZE (16) // 0x55, 0x0d, // UNIT_EXPONENT (-3) // 0x65, 0x13, // UNIT (Inch,EngLinear) // 0x35, 0x00, // PHYSICAL_MINIMUM (0) // 0x46, 0xc0, 0x5d, // PHYSICAL_MAXIMUM (24000) // 0x15, 0x00, // LOGICAL_MINIMUM (0) // 0x26, 0xff, 0x7f, // LOGICAL_MAXIMUM (32767) // 0x81, 0x02, // INPUT (Data,Var,Abs) // 0x09, 0x31, // USAGE (Y) // 0x46, 0xb0, 0x36, // PHYSICAL_MAXIMUM (14000) // 0x81, 0x02, // INPUT (Data,Var,Abs) // 0x05, 0x0d, // USAGE_PAGE (Digitizers) // 0x09, 0x48, // USAGE (Width) // 0x36, 0xb8, 0x0b, // PHYSICAL_MINIMUM (3000) // 0x46, 0xb8, 0x0b, // PHYSICAL_MAXIMUM (3000) // 0x16, 0xb8, 0x0b, // LOGICAL_MINIMUM (3000) // 0x26, 0xb8, 0x0b, // LOGICAL_MAXIMUM (3000) // 0x81, 0x03 // INPUT (Cnst,Var,Abs) 0xc0, // END_COLLECTION 0xc0, // END_COLLECTION 배터리 잔량 체크 샘플에는 Battery Service에 대한 부분도 포함되어 있습니다. 다만, 실제 배터리 잔량이 아닌 가상의 값으로 전송하고 있으므로 실제 배터리 용량을 체크를 할 수 있도록 고려를 해보도록 합니다.\n배터리 잔량을 체크하기 위한 간단한 방법은 배터리 전압을 ADC로 읽는 것 입니다. 한가지 유의할 점은 MCU 구동 전압보다 배터리 최대 전압이 높을 경우 포트와 직접연결은 하지않고 저항으로 분압하여 MCU의 operation voltage maximum rating을 초과하지 않도록 합니다.\n외부에 10k 저항 2개를 직렬로 연결하여 1/2로 분압하도록 하였습니다. Li-Po 배터리는 만충 시 최대 4.2v이므로 ADC에서 측정이 되는 최대 전압은 2.1v가 될 것이며 Vdd를 3V로 사용할 예정이므로 특별한 문제는 없을 것 입니다.\n다만 배터리의 양극에 저항이 연결되어 있으므로 최대 210uA 정도 지속적인 전류 소모가 발생합니다. 280mAh 용량의 배터리이므로 대략 계산 시 빠른 시간내에 전부 소진되지는 않겠지만 불필요한 전류 소모이므로 이를 방지하기 위해서는 FET(Field Effect Transistor)를 이용하여 전압 측정 할 때에만 저항에 전압이 인가되도록 하는 방법이 있습니다.\n하지만 여기서는 실제로 고려되었다가 납땜 여유 공간 부족으로 적용하지 못했습니다. 다음 개선 버전 고려 시 적용 하도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void saadc_init(void) { ret_code_t err_code; nrfx_saadc_config_t saadc_config; nrf_saadc_channel_config_t batt_channel_config; saadc_config.low_power_mode = true; saadc_config.resolution = NRF_SAADC_RESOLUTION_12BIT; saadc_config.oversample = NRF_SAADC_OVERSAMPLE_DISABLED; saadc_config.interrupt_priority = APP_IRQ_PRIORITY_LOW; err_code = nrfx_saadc_init(\u0026amp;saadc_config, saadc_callback); APP_ERROR_CHECK(err_code); // batterty (0 ~ 2.4v) // Each two series resistors are 10k ohm. battery max voltage is 4.2v, so ADC input max voltage 2.1v. set_channel_config(\u0026amp;batt_channel_config, NRF_SAADC_INPUT_AIN4, NRF_SAADC_GAIN1_4); err_code = nrfx_saadc_channel_init(BATT_ADC_CHANNEL, \u0026amp;batt_channel_config); APP_ERROR_CHECK(err_code); } double saadc_read_battery_voltage(void) { nrf_saadc_value_t value; nrfx_saadc_sample_convert(BATT_ADC_CHANNEL, \u0026amp;value); return value * VALUE_PER_VOLTAGE * 2; } NRF는 ADC reference voltage를 0.6v 또는 Vdd 기준으로 설정 할 수 있습니다. 만약 0.6v에 gain을 1/4으로 설정하면 0.6/(1/4) = 2.4v로 측정이 가능합니다. resolution이 12bit이므로 2.4v / 4096 * adc value의 공식으로 측정한 ADC 값에 대한 전압을 계산할 수 있고 여기에 2를 곱하면 현재 배터리 전압에 대한 추정이 가능합니다.\n그럼 측정된 전압을 다시 퍼센트(%)로 환산이 필요합니다. 하지만 전압에 따른 배터리 잔량은 비선형입니다. 그래서 단순히 최대에서 최소 전압을 퍼센트로 나누어서 표시하는 것은 정확성이 많이 떨어집니다.\n정확도를 올리기 위해서는 제조사에서 제공하는 데이터시트 내 discharging 차트를 통해 확인 할 수 있습니다.\n차트에 제공된 전압와 전류량 그래프를 확인하여 구간별 근사값으로 룩업 테이블을 만들어 사용하거나 차트 그래프에 fit되는 다차원 방정식 정의하여 계산하는 방식도 가능합니다. 실제로는 제조사 데이터시트를 구할 수가 없어서 Lipo Voltage Chart6에 있는 테이블을 참조했습니다.\n정확도가 아주 높지는 않겠지만 배터리 잔량의 추세를 가늠할 수 있을것 입니다.\n이후엔 ADC로 측정한 값을 전압으로 계산하고 다시 이 값을 위 테이블의 값을 비교하면서 인덱스를 찾아 인덱스를 퍼센트로 바꾸어 호스트에 전송하면 다음과 같이 배터리 잔량 정보를 표시할 수 있습니다.\nInput Report 전송 Input report 전송은 디스크립터에서 선언한 버튼과 회전량(각도)를 전송하면 됩니다. 2바이크 크기로 최상위 1 비트가 버튼 눌림 여부이고 나머지 15비트가 signed 형으로 degree 값을 저장하여 전송을 해야합니다.\n값 지정 편하게 할 수 있도록 비트필드 구조체를 선언하고 사용자 입력이 발생할 때 해당 값을 저장하고 ble_send_input_report 함수를 호출하여 배열로 전송을 할 수 있도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct { unsigned button_pressed : 1; int16_t rotation : 15; } dial_report_t; static void bsp_event_handler(bsp_event_t event) { switch (event) { ... case BSP_EVENT_KEY_0: if (m_conn_handle != BLE_CONN_HANDLE_INVALID) { dial_report_t dial_report = {0}; dial_report.button_pressed = !nrf_gpio_pin_read(PRIMARY_BUTTON); ble_send_input_report((uint8_t *)(\u0026amp;dial_report)); } break; ... } } 그 외 알 수 없는 원인으로 하드웨어가 정지되어 panic 상태 시 자동으로 리셋하기 위한 WDT(Watch Dog Timer) 설정과 배터리 전압이 너무 낮거나(0% 수치) 또는 배터리 소모를 줄이기 위해서 일정 시간동안(약 5분간) 입력이 없으면 강제 슬립 상태로 진입하는 등의 실제 사용 시 필요한 기능들이 있습니다.\n이 기능들은 고려하지 않아도 구동에는 문제가 없지만 일단 문제가 발생하면 하드웨어 먹통이 되어 강제적인 리셋이 필요할 수 있습니다. 그리고 제때 충전하지 않아서 배터리가 완전 방전(3v 이하 전압)이 되면 배터리 수명이 급격히 나빠질 수 있으니 가급적 해당 기능을 고려하는 것이 좋습니다.\n실제 작업에서는 이 부분을 적용하였지만 일단 여기서는 생략하고 포스팅을 마무리 합니다.\n다음 포스팅에서는 하드웨어 제작과 실제 구동 관련하여 내용을 남기겠습니다.\n \u0026hellip; 다음 포스트에서 계속 \u0026hellip;\n https://www.novelbits.io/basics-bluetooth-low-energy/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.linkedin.com/pulse/differences-between-ble-40-41-42-5-michael-l/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://opensourcehardware.io/posts/projects/media-controller/2021-11-08-media-controller-dev-01/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fqdec.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-sample-report-descriptors\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://blog.ampow.com/lipo-voltage-chart/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":3,"section":"posts","tags":["project","취미전자","Hobby Electronics","Surface Dial"],"title":"마이크로소프트 서피스 다이얼 클론 제작 - 2","uri":"/posts/projects/suface-dial/2022-02-27-how-to-make-2/"},{"content":"이전 포스트에서 레지스터와 메모리 인터페이스를 정의 하였고 이번 포스트에서는 명령어 세트와 어드레싱 모드 구현에 대해 다루어 보고자 합니다.\n명령어 세트 (Instruction Sets) 공식 명령어 개수는 56개 정도이지만 어드레싱 모드가 다른 경우의 수를 고려하면 실제 실행 가능한 명령어 세트 수는 더 많습니다. 그리고 비공식 명령어와 더불어 중복 명령어까지 포함하면 1바이트 opcode 어느 값을 참조하더라도 특정 명령어 세트와 매핑이 가능합니다.\n 명령어 테이블1\n 명령어 사이클 (Instruction Cycles)2 명령어를 처리하는 단계는 설명마다 조금씩 차이가 있지만 보통 3 ~ 4단계로 구분할 수 있습니다. 여기서는 3단계로 명령어를 가져오는 단계(Fetch Stage), 그리고 명령어를 해석하는 디코딩 단계(Decode State), 마지막으로 실행 단계(Excute Stage)로 구분하여 진행합니다.\nFetch 단계에서는 프로그램 카운터(PC)를 참조하여 메모리 주소로 부터 1바이트 opcode를 가져온 후 다음 PC를 가리킵니다. Decode 단계에서는 해석에 따라 명령어나 어드레싱 모드에 의해 다른 크기의 operand 정보를 가져옵니다. 마지막으로 디코딩 된 정보를 바탕으로 실제 연산을 수행합니다.\n그리고 이 단계들은 PC가 중단될 때까지 계속 반복하며, 이 순차적인 과정을 만드는 것이 CPU 에뮬레이션의 기본적인 목표가 되겠습니다.\n명령어 사이클 중에서 Fetch 단계는 이미 메모리 인터페이스가 마련되어 있으므로 특별한 것이 없습니다. PC가 가리키는 주소를 참조하여 opcode만 읽어오면 됩니다.\n1 2 // fetch byte opcode = ReadByte(registers.PC) 명령어 세트 디코딩 (Decoding) PC를 통해 opcode를 읽어들였다면 디코딩 과정을 통해 실행할 명령어(instruction)와 어드레싱 모드를 통한 피연산자 위치와 크기를 결정합니다.\nThe 6502/65C02/65C816 Instruction Set Decoded3를 참고하면 명령어 세트는 일부 규칙이 있어서 1바이트 opcode를 aaabbbcc 포맷이라고 할 경우 aaa와 cc를 통해 명령어 종류, bbb를 통해 어드레싱 모드를 디코딩 할 수 있습니다. 하지만 아쉽게도 모든 명령어가 이 규칙을 따르지 않기 때문에 이 포맷의 규칙을 따르는 일부 명령어를 제외하면 나머지는 예외 처리가 요구됩니다.\nopcode의 크기가 1바이트이고 어떤 값을 참조하던지 특정 명령어 세트를 참조할 수 있기 때문에 포맷을 이용한 디코딩 방법 대신 좀 더 쉽고 단순한 방법을 사용할 수도 있습니다. 명령어 종류, 어드레싱 모드, 명령어 소요 사이클 등 명령어와 연관된 정보들을 256 크기를 가지는 룩업 테이블로 만들어 opcode를 인덱스로 사용하여 각 정보를 참조하는 방법입니다.\n이 방법은 명령어 세트 수가 많지 않기에 가능한 방법이고 특별한 규칙이나 연산 등이 요구되지 않으므로 포맷과 예외 처리를 동시에 하는 방법보다 직관적이고 단순한 방법으로 디코딩을 수행할 수 있습니다. 그리고 공개된 많은 에뮬레이터가 이와 같은 방법을 사용하고 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 enum Instruction { ADC, AND, ASL, BCC, BCS, BEQ, BIT, BMI, BNE, BPL, BRK, BVC, BVS, CLC, CLD, CLI, CLV, CMP, CPX, CPY, DEC, DEX, DEY, EOR, INC, INX, INY, JMP, JSR, LDA, LDX, LDY, LSR, NOP, ORA, PHA, PHP, PLA, PLP, ROL, ROR, RTI, RTS, SBC, SEC, SED, SEI, STA, STX, STY, TAX, TAY, TSX, TXA, TXS, TYA, ALR, ANC, ARR, AXS, LAX, SAX, DCP, ISC, RLA, RRA, SLO, SRE, SKB, IGN, // illegal AHX, KIL, LAS, SHX, SHY, TAS, XAA }; enum AddressingMode { ABSOLUTE, ABSOLUTE_X, ABSOLUTE_Y, ACCUMULATOR, IMMEDIATE, IMPLIED, INDEXED_INDIRECT, INDIRECT, INDIRECT_INDEXED, RELATIVE, ZERO_PAGE, ZERO_PAGE_X, ZERO_PAGE_Y } private readonly static AddressingMode[] addressModes = { AddressingMode.IMPLIED, AddressingMode.INDEXED_INDIRECT, AddressingMode.IMPLIED, AddressingMode.INDEXED_INDIRECT, AddressingMode.ZERO_PAGE, AddressingMode.ZERO_PAGE, AddressingMode.ZERO_PAGE, AddressingMode.ZERO_PAGE, AddressingMode.IMPLIED, ... }; private readonly static Instruction[] instructions = { Instruction.BRK, Instruction.ORA, Instruction.KIL, Instruction.SLO, Instruction.NOP, Instruction.ORA, Instruction.ASL, Instruction.SLO, Instruction.PHP, Instruction.ORA, Instruction.ASL, Instruction.ANC, Instruction.NOP, Instruction.ORA, Instruction.ASL, Instruction.SLO, Instruction.BPL, Instruction.ORA, ... }; 명령어 세트 실행 (Execution) 디코딩에 의해 명령어와 어드레싱 모드가 결정 되었으면 이제 어드레싱 모드에 의해 피연산자 위치를 찾아내서 명령을 수행하면 됩니다. 그리고 당연히 실행을 위해서는 그 전에 명령어 프로세스와 피연산자 참조를 위한 어드레싱 모드 정의가 필요한데요 6502 Instruction Set1나 Ultimate Commodore 64 Reference4 등을 참조하면 명령어와 어드레싱 모드에 대한 세부 설명과 동작 프로세스를 확인 할 수 있습니다.\n 명령어 세트 구현 먼저 명령어 정의를 위해 Ultimate Commodore 64 Reference를 참고 하겠습니다. 아래 카드는 ADC 명령어에 대한 설명을 담고 있습니다.\nNV-BDIZC✓✓----✓✓ADC - Add Memory to Accumulator with CarryOperation: A + M + C → A, C\nThis instruction adds the value of memory and carry from the previous operation to the value of the accumulator and stores the result in the accumulator.\nThis instruction affects the accumulator; sets the carry flag when the sum of a binary add exceeds 255 or when the sum of a decimal add exceeds 99, otherwise carry is reset. The overflow flag is set when the sign or bit 7 is changed due to the result exceeding +127 or -128, otherwise overflow is reset. The negative flag is set if the accumulator result contains bit 7 on, otherwise the negative flag is reset. The zero flag is set if the accumulator result is 0, otherwise the zero flag is reset.\n..어드레싱 모드 테이블 생략..\n\r먼저 Operation 부분을 보면 A + M + C \u0026ndash;\u0026gt; A, C 라고 되어 있는데요 A(ccumlator) 와 M(emory) 그리고 C(arry) 비트 값을 더한 값을 A, C에 반영 하라는 의미입니다. 굳이 이 부분을 참조하지 않아도 중간에 설명이 되어 있습니다.\n내용을 더 참조하면 sets the carry flag when the sum of a binary add exceeds 255, otherwise carry is reset라는 문구를 확인 할 수 있는데 A + M + C 연산 후 255를 초과하면 C를 set 하고 아니면 reset(clear) 하라는 의미입니다.\n그리고 카드 우측 상단 표를 보면 해당 연산 시 영향을 받는 상태 레지스터의 플래그들을 표시하고 있습니다. 내용을 더 참조하면 연산 결과의 부호 비트 즉, 7번째 비트에 변화에 따라 overflow flag와 negative flag를 설정하고, 결과가 0인지 여부에 따라 zero flag를 설정하라는 것을 알 수 있습니다.\n아래의 코드는 ADC 연산 과정을 보여줍니다.\n1 2 3 4 5 6 7 8 9 10 11 12 // Add Memory to Accumulator with Carry private void adc(ushort address) { byte value = ReadByte(address); ushort result = (ushort)(registers.ACC + value + registers.SR.CARRY); // A + M + C registers.SR.SetZeroNegativeFlags((byte)result); // zero and negative flags registers.SR.SetCarryFlag(result); // carry flag registers.SR.OVERFLOW = (byte)((((registers.ACC ^ value) \u0026amp; 0x80) == 0) // overflow flag \u0026amp;\u0026amp; (((registers.ACC ^ result) \u0026amp; 0x80) != 0) ? 1 : 0); registers.ACC = (byte)(result \u0026amp; 0xff); // store result to A } 코드를 참조하면 피연산자 위치에서 값을 읽어와서 A + M + C 연산을 수행합니다. 그리고 연산 결과에 따라 상태 레지스터 플래그를 설정하고, 결과를 A에 저장합니다.\n여기서 유의할 것은 캐리 비트 설정 즉, 255 초과 여부 체크를 쉽게 하기위해서 ushort 형으로 연산결과를 저장하였고, 나머지는 플래그나 A는 8비트 기준에서 연산을 해야하므로 byte로 타입 캐스팅하여 사용한 것 입니다. 그리고 SetCarryFlag나 SetZeroNegativeFlags 같은 메서드는 여러 명령어 수행에 있어서 흔하게 사용되는 상태 레지스터 반영을 위해 구조체 내에 미리 선언된 메서드 입니다.\n이와 같은 절차로 공식 명령어를 정의하면 됩니다. 그리고 비공식 명령어도 필요에 따라 정의를 하는데 사이트마다 약간씩 설명이 다른 경우가 있으므로 상호 참조를 통해 진행을 하는데 저의 경우 테스트 ROM으로 실행 시 로그가 이상하게 나오는 경우 다른 사이트를 참조하여 예상 값이 나올 때까지 수정하는 방식으로 오류를 수정하였습니다.\n 어드레싱 모드 구현 어드레싱 모드는 피연산자 주소나 값을 찾아가는 방법을 정의하는 것입니다. 어드레싱 모드는 룩업 테이블로 어떤 모드인지 가져올 수 있으므로 해당 어드레싱 모드의 주소 참조 방식을 구현 하면 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 private ushort GetOperandAddress(byte opcode) { ushort address = 0; switch (Opcode.GetAddresingMode(opcode)) { case AddressingMode.ABSOLUTE: address = ReadShort((ushort)(registers.PC + 1)); break; case AddressingMode.ABSOLUTE_X: address = (ushort)( ReadShort((ushort)(registers.PC + 1)) + registers.X ); cpuCycles += PageCrossed(address, (ushort)(address - registers.X)) ? Opcode.GetPageCycle(opcode) : 0; break; case AddressingMode.ABSOLUTE_Y: address = (ushort)( ReadShort((ushort)(registers.PC + 1)) + registers.Y ); cpuCycles += PageCrossed(address, (ushort)(address - registers.Y)) ? Opcode.GetPageCycle(opcode) : 0; break; case AddressingMode.IMMEDIATE: address = (ushort)(registers.PC + 1); break; case AddressingMode.INDEXED_INDIRECT: { ushort jmpAddress = (ushort)((ReadByte((ushort)(registers.PC + 1)) + registers.X) \u0026amp; 0xFF); address = ReadShortAbnormally(jmpAddress); } break; case AddressingMode.INDIRECT: address = ReadShortAbnormally( ReadShort((ushort)(registers.PC + 1)) ); break; case AddressingMode.INDIRECT_INDEXED: address = ReadShortAbnormally( ReadByte((ushort)(registers.PC + 1)) ); address += registers.Y; cpuCycles += PageCrossed(address, (ushort)(address - registers.Y)) ? Opcode.GetPageCycle(opcode) : 0; break; case AddressingMode.RELATIVE: { // The range of the offset is -128 to +127 bytes byte offset = ReadByte((ushort)(registers.PC + 1)); address = (ushort)((registers.PC + 2 + offset) - ((offset \u0026lt; 0x80) ? 0 : 0x100)); } break; case AddressingMode.ZERO_PAGE: address = ReadByte((ushort)(registers.PC + 1)); break; case AddressingMode.ZERO_PAGE_X: address = (ushort)((ReadByte((ushort)(registers.PC + 1)) + registers.X) \u0026amp; 0xFF); break; case AddressingMode.ZERO_PAGE_Y: address = (ushort)((ReadByte((ushort)(registers.PC + 1)) + registers.Y) \u0026amp; 0xFF); break; default: case AddressingMode.ACCUMULATOR: // operate directly on the contents of the accumulator case AddressingMode.IMPLIED: // do not require access to operands stored in memory case AddressingMode.INVALID: break; } return address; } 어드레싱 모드가 어떻게 피연산자 위치를 결정하는지는 이미 이전 포스트에서 설명을 하였으므로 이에 대한 설명은 하지 않겠습니다. 다만 코드 중간에 PageCrossed 라는 메서드를 호출 하는데 이는 주소 참조에 있어서 페이지 단위가 바뀌면 사이클이 더 소요되기 때문에 이러한 사유로 정확한 CPU 사이클 추정을 위해서 호출을 합니다.\n참고로 사이클을 정확하게 측정하는 이유는 PPU(Picture Process Unit)과 동기화를 하기 위함입니다. 이 작업이 왜 필요한지는 나중에 PPU를 구현하고 관련 내용을 포스팅 할 때 설명하겠습니다.\nINDIRECT, INDEXED_INDIRECT, INDIRECT_INDEXED의 경우 이전 포스트에서 한번 언급했던 ReadShortAbnormally라는 메서드를 호출 합니다. 6502 버그로 인해서 비정상적으로 16비트 값을 읽어오는 메서드 입니다. 관련 버그는 개발 포럼5에서도 논의가 되는 것을 확인 할 수 있는데요 이 버그는 다음과 같이 설명6하고 있습니다.\nThe indirect jump instruction does not increment the page address when the indirect pointer crosses a page boundary. JMP ($xxFF) will fetch the address from $xxFF and $xx00.\n말하자면 페이지 바운더리 즉, 페이지가 바뀌는 영역($xxFF)에서 16비트(8bit 두번 읽기) 값을 읽기 위해 하위 바이트을 읽고, 다음 상위 바이트를 읽기 위해서 주소를 1 증가 시 주소 상위 바이트가 증가하지 않고, 주소의 하위 바이트만 wrap-around 되어 페이지가 증가되지 못하고 다시 해당 페이지 0번 위치를 참조하는 버그입니다.\n예를 들어 $00FF 에서 16비트 값을 가져오러면 $00FF와 $0100을 연속으로 읽어야 하는데, 실제로는 $00FF와 $0000이 읽혀지게 되는 문제입니다. 만약 $00FE 였다면 $00FE, $00FF로 정상적으로 값을 읽을 수 있습니다.\n당연히 버그니깐 고쳐줘야(?) 싶겠지만 사실 이 버그 조차도 반영된 것이 실제 하드웨어이므로 오히려 정상적인 에뮬레이션을 위해서는 이 하드웨어 버그도 구현을 해줘야합니다. 만약 하지 않는 경우 나중에 NES CPU 전용 ROM 테스트 시 실패하게 됩니다.\n위 버그 외에도 몇 가지 버그7가 더 있지만 일부는 NES에서 사용하지 않는 decimal 모드에서만 유효한 것이고 그 외의 버그는 반영을 하지 않더라도 ROM 파일 테스트에서는 문제가 없어서 일단은 고려하지 않았습니다.\n자 그럼 실행할 준비도 다 된거 같으니 코드를 통해 명령어 사이클을 정리 해보겠습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // fetch byte opcode = ReadByte(registers.PC); // decode Instruction inst = Opcode.GetInstruction(opcode); ushort address = GetOperandAddress(opcode); // excute switch (inst) { case Instruction.ADC: adc(address); break; case Instruction.AND: and(address); break; ... } 코드를 보면 PC에서 opcode를 읽어오는 단계, opcode를 인덱스로 사용하여 명령어와 어드레싱 모드를 통한 피연산자 주소를 가져와서 명령어를 수행하는 단계를 확인 할 수 있습니다. 이 절차를 ROM 파일의 코드를 읽어 들여 계속 반복해서 수행하면 CPU의 에뮬레이션의 대부분은 완료되었다고 볼 수 있겠습니다.\n물론 여기에는 인터럽트나 CPU 사이클, PC를 증가시키는 부분도 고려가 되어야 합니다.\n그럼 이제 마지막으로 실제 롬파일을 CPU를 테스트 하는 과정을 설명하겠습니다.\n.. 다음 포스트에서 ..\n https://www.masswerk.at/6502/6502_instruction_set.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://en.wikipedia.org/wiki/Instruction_cycle\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://llx.com/Neil/a2/opcodes.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.pagetable.com/c64ref/6502/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://forums.nesdev.org/viewtopic.php?t=8353\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.pagetable.com/c64ref/6502/?tab=3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://atariwiki.org/wiki/Wiki.jsp?page=6502%20bugs\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":4,"section":"posts","tags":["프로젝트","NES","Emulator"],"title":"NES CPU(6502) 에뮬레이션 - 3","uri":"/posts/projects/nesdev/2022-02-19-nes-cpu-emulation-03/"},{"content":"들어가며 과거 기업 연구소에서 헬스와 메디컬 분야 선행기술 연구 및 개발을 주로 했었습니다. 당시 여러 분야에 걸쳐 다양한 업무와 경험을 했는데 그 중에서도 당뇨와 관련된 연구도 많이 진행했습니다.\n당시 인슐린 딜리버리 관련 신규 디바이스를 기획하면서 벤치마킹을 위해 검토를 해본 기기가 있었는데, 개인적으로는 인상적인 제품이었기 때문에 오랜시간이 지난 지금에도 기억에 남아있습니다. 오랜만에 다시금 찾아보고 당시를 복기 해보면서 어떠한 특징과 기술적인 요소를 지니고 있는지 소개를 해보려고 합니다.\n그 전에 이해를 돕기 위해서 당뇨나 인슐린에 관하여 전혀 모르는 분들 대상으로 알아야 할 몇 가지 개념을 설명하고자 합니다.\n다만, 의료나 질병 관련 설명은 레퍼런스를 달아두었음에도 후술할 디바이스 동작 메커니즘을 이해하기 위한 사전지식 용도로만 보시고 전문적인 레퍼런스로는 참고하지 않으시길 바라며, 의학적 사실과 확인이 필요하신 분들은 반드시 공인된 단체(국내 외 당뇨학회)에서 제공하는 공식 문서 참조나 의학 전문가와 상담을 하시길 바랍니다.\n당뇨 대부분 아시다시피 당뇨병(이하 당뇨)는 혈액중의 포도당(혈당)이 높아서 소변으로 포도당이 넘쳐 나오는데서 지어진 이름입니다.1 식사 시 섭취한 탄수화물이 포도당으로 분해되면서 신체에 흡수되는데, 이 때 췌장에서 분비되는 인슐린이라는 호르몬에 의해 조절이 이루어집니다.2\n만약 인슐린 분비에 문제가 생기는 경우 혈당(blood sugar) 즉, 혈액 속 당수치가 높아지게 되고, 당을 조절하는 호르몬은 인슐린 하나 밖에 없으므로 만약 인슐린 분비가 불가하거나 작용을 제대로 하지 못한다면 신체 외부에서 인위적으로 인슐린을 공급받아야 합니다.\n그렇지 않으면 혈액이 당으로 인해 물엿이나 꿀처럼 점도가 높아지게 되고 혈액순환이 원활하지 못해 모세혈관이 막히거나 산소공급이 제대로 이루어지지 않으면서 합병증이 발생하여 위험하기 때문입니다.\n성인병중에서도 고 위험군인 뇌혈관, 심혈관 질환이 당뇨에 의한 합병증 원인이기도 하므로 매우 조심해야하는 질병입니다.3\n1형, 2형 (Type 1, 2) 당뇨는 앞서 언급한대로 인슐린 분비 또는 작용에 문제가 있을 때 발생하는 병입니다. 당뇨는 크게 1형과 2형으로 구분 하는데 1형은 인슐린을 전혀 분비가 안되는 상태이고 2형은 인슐린 분비에 문제가 있거나 인슐린 작용이 제대로 되지 않는 상태입니다.4 1형은 우리나라에서는 2% 미만의 환자가 이에 해당하며, 주로 소아에 발생하나 성인에게도 발생할 수 있습니다. 2형은 칼로리의 과잉이나 상대적으로 운동량 감소로 인슐린의 성능이 떨어지거나 유전에 의해 발생하는 경우가 많습니다5.\n인슐린26 인슐린은 췌장에서 분비되는 호르몬으로서 신체 에너지원인 당을 분해해서 세포에 저장시키는 일을 합니다. 당뇨로 인해서 인슐린 분비나 작용에 문제가 있으면 외부에서 주입을 해야하는데 2형 당뇨의 경우는 약물, 식단 조절이나 운동으로 혈당을 높이지 않도록 관리하는 방법이 가능한 경우가 있어서 인슐린 주입이 제한적으로 사용되지만, 1형의 경우는 당을 통제할 수단이 전혀없기 때문에 외부에서 인슐린을 주입이 필수적으로 요구됩니다.\n이 경우 매우 불편한 점은 음식 섭취 후 혈당 체크를 위해서 손끝에 바늘로 채혈을 해야하는데 또 다른 부위에 바늘을 찔러서 인슐린을 맞아야 한다는 점입니다.\n그리고 인슐린을 맞을 때 조심해야할 부분이 있는데 인슐린을 적정양 이상으로 맞거나, 이미 주입한 것을 잠시 까먹어서 잛은 시간내에 연속으로 인슐린을 주입하는 경우 신체에서 사용할 에너지원이 갑자기 바닥나는 저혈당 쇼크가 발생할 수 있습니다.\n이 때 적절한 에너지원(당)이 공급되지 않으면 심한 경우 사망까지 이를 수 있기 때문에 인슐린 주입은 매우 조심스럽게 진행되어야 합니다.\n인슐린 주입(Insulin Delivery) - 인슐린 펜7 인슐린은 호르몬이기 때문에 섭취로는 불가하고 어쩔 수 없이 신체에 바늘을 꼽아서 주입을 해야합니다. 기본적으로 직접 주사기를 이용 하는 방법, 인슐린 펜을 사용하는 방법 그리고 인슐린 주입기(펌프)를 사용하는 방법이 있습니다.\n직접 주사를 하는 방법은 매우 열악한 방법이라 의료복지가 잘 되어있지 않은 곳이 아니면 잘쓰이지 않는 방법이고, 대부분은 인슐린 펜이라는 것을 사용합니다.\n(출처: https://www.drugs.com)\n인슐린 펜의 경우 다이얼을 돌려서 맞고자 하는 인슐린 양을 설정하고, 주입 버튼으로 쉽게 주입할 수 있도록 만들어져 있습니다. 보통 인슐린은 짧은 바늘로 뱃살이나 팔뚝 같은 부위의 피하지방층에 주입합니다.8\n펜에서 조절 가능한 인슐린 양을 units이라는 단어를 사용하는데 이 단위의 양이 1/100 ml 로서 매우 작은 용량입니다. 이처럼 작은 단위를 사용하는 것은 인슐린은 매우 적은 양으로도 혈당 조절이 가능하다는 점이고 앞서 언급했듯이 적은양이라도 정량을 초과할 경우 저혈당 쇼크가 발생할 정도로 매우 위험하기 때문입니다.\n인슐린 펜은 인슐린이 담긴 카트리지와 펜이 결합된 일체형과 카트리지만 교환해서 계속 사용하는 교체형(당연하지만 신체에 주입 시 사용한 바늘은 매번 교체해줘야 합니다)이 있고, 인슐린 약제가 신체에 작용하는 속도 또는 시간(지속성) 따라 종류가 세분화 됩니다.\n인슐린 Acting 타입에 따른 분류 인슐린 펜은 신체에 작용하는 속도에 따라 세부적인 분류가 가능하지만 기본적으로 크게 둘로 나누어 볼 수 있습니다. quick, fast, short과 같이 빠르고 짧게 작용하는 인슐린(bolus)과 intermediate, long 으로 느리고 길게 작용하는 인슐린 타입(basal)으로 구분될 수 있으며, basal은 직역하면 기저 혈당 조절, bolus는 덩어리, 즉 음식섭취 후 늘어나는 혈당 수치를 빠르게 조절하는 용도입니다.9\n실제 신체에서는 소량씩 인슐린을 분출하여 기저 혈당을 유지하면서, 탄수화물 섭취 시에는 인슐린 분비량을 늘려서 빠르게 혈당을 조절하기 때문에 Acting 속도가 다른 인슐린을 상호 보완해서 사용하면 신체의 메커니즘과 유사하게 혈당을 유지할 수가 있습니다.\n그렇기 떄문에 위와 같은 특성으로 basal은 이른 오전이나 취침전에 맞는 경우가 많고, bolus는 식사나 음식 섭취 후 맞게 됩니다. 최근에는 이러한 불편함을 개선하기 위해서 pre-mixed 타입으로 두가지 특성을 가진 형태도 나오고 있습니다.\n Quick Intermediate Long Pre-mixed (출처: https://www.healthxchange.sg)\n인슐린 주입 - 인슐린 펌프10 (출처: https://assets.aboutkidshealth.ca)\n인슐린 펌프는 인슐린이 담겨있는 카트리지 또는 시린지를 정밀 모터를 사용하여 주입하는 장치입니다. 보통 가늘고 긴 관이 연결된 바늘을 뱃살이나 엉덩이 피하지방쪽에 쪽에 꼽아두고 2 ~ 3일간 24시간 내내 인슐린 주입이나 조절이 가능합니다.\n펌프의 장점으로는 매우 소량의 인슐린을 연속적으로 주입이 가능하기 때문에 신체에서 췌장의 역할과 유사하게 기저 혈당 조절이 가능하다는 점이고, 식사 후 혈당 조절이 필요한 경우에는 혈당 수치에 따라 수동으로 추가 주입을 하면 되므로 인슐린 주입을 위해서 바늘을 자주 찌르지 않아도 되는 장점이 있습니다.\n최근에는 연속 혈당계라고 해서 혈당 센서를 꼽아두고 연속적으로 혈당 추적이 가능한 혈당계가 있으므로 두 기기의 조합하여 사용한다면 하루에도 몇번씩 신체에 바늘을 꼽는 횟수를 줄일 수 있습니다.\n하지만 가격이 비싸고, 항시 튜브(관)이 달린 바늘을 꼽고 생활을 해야하기 때문에, 해당 부위가 외부 충격이나 움직임으로 인해 통증을 유발할 가능성이 있거나 샤워나 씻을때 그리고 취침 시 불편한 점이 있을 수 있습니다.\n그리고 심리적인 부분으로 기기 착용 모습이 외부적으로 눈에 띄어 본인이 당뇨 질환을 앓고 있다는 사실이 의도치 않게 공개되는 점을 환자들이 싫어한다고 합니다.\n그리고 인슐린 펌프의 경우 한 바늘을 꼽아서 사용 시 장시간을 연속적으로 사용하지 않는데, 이러한 이유는 위생적인 문제를 회피하는 목적 외에 바늘을 꼽은 부위에 신체 면역 반응으로 인해 바늘 주위로 세포나 체액 등이 응고하여, 바늘이 막히는 현상(occlusion)을 최대한 피하기 위함입니다.\n만약 막힌 상태에서 펌프가 지속적으로 주입을 시도하게 되면, 바늘과 튜브에 순간적으로 인슐린이 압축된 상태가 되고 이후 일정하 압력에 의해 막힌 것이 갑자기 뚫리게 되면 짧은 시간내에 많은 인슐린이 신체에 주입되면서 저혈당 쇼크가 발생할 수 있습니다. 그렇기 때문에 인슐린 펌프에서 바늘이 막히는 현상은 매우 위험한 상황을 유발할 수 있으므로 인슐린 펌프는 기본적으로 막힘 감지(occlusion detection)가 가능해야하고 이는 매우 필수적이고 중요한 기능입니다.\n그럼 사전 설명은 여기까지 하고 관련 내용을 정리하면 다음과 같습니다.\n 인슐린은 외부에서 피하지방층으로 직접 주입해야함 인슐린은 정량 이상 또는 중복으로 맞으면 매우 위험 인슐린 펜 Acting 타입에 따라 크게 Basal, Bolus 그리고 두 가지 특성이 혼합된 Mixed 타입으로 나뉨 종류별로 맞아야하는 불편, 최근 맞은 시간을 기억(기록)하지 않으면 중복 주입 위험 있음 인슐린 펌프 2~3일 연속적으로 쓸 수 있음 수면, 씻기, 외부 활동 시 거추장스러움, 바늘이 간혹 통증 유발 바늘이 막히는 경우 위험 (교체 해야함) 정리한 내용을 보면 당뇨는 연관된 증상도 위험하지만 혈당 측정 방법이나 인슐린 투여 방법 그리고 overdose 발생 시 위험성으로 매우 불편하고 관리가 까다로운 병입니다.\n그래서 당뇨병에서 난제이자 신기술 이슈는 비침습(non-invasive) 혈당 체크, 먹어서 혈당을 조절하는 신약제에 관한 것들입니다.\n옴니팟(Omnipod)11 옴니팟12\nInsulet 사에서 개발한 인슐린 펌프입니다. 이름에서 알 수 있듯이 볼록한 Pod(팟) 형태를 가지고 있고 기본적으로 일회용 디바이스 입니다. 기존 인슐린 펌프가 인슐린 카트리지만 교환해서 사용하는 방식이라면, 옴니팟은 본체 자체를 사용하고 버리는 형식입니다.\n개인적인 의견이긴 하지만 과감히 \u0026lsquo;차세대\u0026rsquo; 인슐린 펌프라는 머릿말을 붙였습니다.\n제가 이렇게 붙인 이유는 인슐린 딜리버리 기술에 있어서 적어도 기존의 인슐린 펌프가 가지는 불편한 요소들을 대부분 개선했다고 보기 때문입니다.\n그럼 왜 차세대라고 부를 수 있는지 그리고 펌프라고 하면 모터와 같은 구동계나 배터리 등이 많은 부품 등이 들어갈 텐데 어떻게 일회용으로 만들 수 있는지 기술적 특징을 살펴보겠습니다.\n기기 구조와 특징 상단 이미지는 옴니팟을 분해했을 때의 이미지 입니다. 내부 구조를 살펴보면 우측 상단에 인슐린 저장소(resovior), 좌측 상단은 인슐린 펌핑을 위한 레버, 가운데 좌측은 파란색은 캐뉼라(cannula, 약물 주입을 위해 피부에 꽃는 관)와 중간에 붉은색은 SMA(Shape Memory Alloys) 와이어, 하단은 배터리로 구성되어 있습니다. 그 외 회로적으로 connectivity를 위한 무선 회로와 프로세서 등이 포함되어 있겠습니다.\n 바늘 삽입 (Neddle Insertion) 일반적인 인슐린 펌프는 가늘고 긴 관이 달린 바늘을 배에 꼽고 사용합니다. 당연하게도 바늘은 딱딱하고 날카롭기 때문에 긴 관을 늘어뜨려 달고 다니는 것이 매우 불편하고 움직임이나 충격에 따라 통증을 유발할 수도 있습니다. 옴니팟도 인슐린 펌프이기 때문에 인슐린 전달을 위해서는 피부에 바늘이 꼽아야하는 점은 동일하지만 조금 특별한 메커니즘을 가지고 있습니다.\n옴니팟의 바늘은 캐뉼라(삽입관)을 피부에 안착시키는 용도로만 사용하고 피부에는 캐뉼라만 남도록 설계가 되어있다는 점입니다.\n위 영상을 보시면 캐뉼라와 함께 바늘이 쏘아지고 바늘만 빠지면서 캐뉼라만 피부에 남게 되어있습니다. 설명글에 따르면 1/200초 안에 deploy(배치)와 retract(철회)가 이루어진다고 합니다.\n위와 같은 구조의 장점으로는 유연한 캐뉼라(또는 튜브)만 신체에 삽입되므로 움직임이나 일반적인 상황에서 통증이 매우 적거나 미비하다는 장점이 있습니다.\n그리고 본체를 신체에 직접 부착해서 사용하기 때문에 캐뉼라 관을 길게 늘어뜨려 달고 다니지 않아도 된다는 장점으로 운동, 샤워, 수면 등 활동의 제약에 크게 구애 받지 않게 됩니다.\n인슐린 유량 제어 (Drive Mechanism) 보통 인슐린 펌프는 모터와 기어를 이용한 액추에이터로 회전운동을 직선운동으로 변경시켜서 카트리지를 밀어내서 인슐린을 주입합니다. 옴니팟은 모터 대신 SMA 쉽게 이야기하면 형상 기억 합금 와이어를 사용해서 회전 기어에 연결된 레버를 왕복 운동시켜서 인슐린 주입을 합니다. 그리고 한번 움직일 때마다 .05 units 주입이 가능하다고 합니다.\n이 기술은 캐뉼라 슈터에 이어서 상당히 인상이 깊었는데 모터를 사용하는 것 대비 구조가 단순하면서도 부피도 비용도 적은 최적의 솔류션이라는 생각이 들었기 떄문입니다. 어떻게 1회용 디바이스로 제작이 가능했는지 이해가 되었습니다.\n막힘 감지 (Occulsion Detection) 인슐린 펌프의 특징을 소개할 때 펌프는 바늘이 막히는 부분 감지가 중요하다고 설명을 했습니다. 모터를 이용한 솔류션에서는 보통 모터 회전 시 걸리는 부하(전류) 등을 체크해서 간접적 확인이 가능합니다. 옴니팟은 모터를 사용하지 않기 때문에 막힘 감지를 위한 특별한 방법을 사용하고 있는데요 이 방법은 별도로 기술 소개가 되어 있지 않으니 특허를 살펴 보아야 합니다.\nFLOW CONDITION SENSOR ASSEMBLY FOR PATIENT INFUSION DEVICE13 특허를 살펴보면 막힘 감지를 검출하는 몇가지 방법을 제시하고 있습니다. 특허에 처음 등장하는 이미지를 살펴보겠습니다.\n위 이미지를 참고하면 인슐린이 이동하는 통로 중간에 연성 재질의 막(220, 특허에서 diaphragm로 표현)이 존재하는 것을 볼 수 있습니다.\n만약 캐뉼라에 막힘이 발생한 상태에서 지속적으로 주입을 시도하면, 인슐린이 이동하지 못하고 압축되어 통로 전체적으로 압력이 높아질 것 입니다. 이 경우 상대적으로 연성인 다이어프램은 부풀거나 구부러지는 변화를 발생시키게 되고 이 변화를 센서(232, 234)에서 감지합니다.\n만약 상단 센서를 전극이라고 가정 하고, 다이어프램의 상단에는 전도성 물질이 코팅이 되어있다고 한다면 일정 압력이상이 발생하는 경우 두 전극은 다이어프램으로인해 연결되어 통전이 가능해지고, 이를 확인하는 것으로 막힘 감지가 가능합니다.\n특허에서는 대표 이미지와 유사한 구조에서 막힘 감지하는 방법과 사례를 더 제시하고 있습니다. 관심이 있으신 분들은 참조해보시면 좋을 것 같습니다.\n일단 옴니팟에서 인상 깊게 여긴 특징은 위의 세가지이고 내용 외에 더 많은 내용을 설명드리면 좋겠지만 본래에는 기술적인 부분만 소개하려고 했기 때문에 사용성이나 기타 특징에 대한 설명은 생략 하겠습니다.\n마무리 옴니팟은 기술적으로 뛰어난 특징으로 사용 시 통증 저감과 활동성 제약이 적고 편의성 등 기능은 모두 갖춘 디바이스입니다.\n하지만 하나를 얻으면 하나를 잃는 법! 상대적으로 다른 인슐린 기기 대비 가격이 비쌉니다.\n조사 당시가 대략 8 ~ 9년 전이라 당시 유지비용 관련하여 크게 기억나는게 없어서, 현재 기준 대충 소매 가격을 찾아보니 인슐린 펌프 카트리지 대비 두 배 이상의 가격이었습니다.\n아마존에서 10개 팩이 275.99불 가격으로 판매를 하고 있었는데, 디바이스당 최대 3일을 사용을 가정하면 한달에 한화로 약 33-35만원 정도 비용이 들 것으로 예상됩니다.(미국의 경우 Medicare Plan 보험 적용으로 유지 비용을 낮출 수 있는 것으로 보입니다.)\n무선통신이 가능한 하드웨어가 포함된 카트리지라고 생각을 하면 가격이 비싼 것은 어느정도 이해가 됩니다.\n결국은 편의성과 경제성 중에서 무엇을 선택할 것인지에 따라서 옴니팟의 사용여부가 결정될 것입니다. 그런데 1형 당뇨는 소아 청소기 때부터 나타나는 경우가 많아서 부모 입장이라면 아무래도 일반적인 인슐린 펌프보다는 옴니팟을 선택할 확률이 더 높을 것으로 예상이 됩니다.\n아무래도 예상이 맞았는지? 귀여운 느낌의 옴니팟 전용 스티커나 악세서리가 따로 판매되고 있었습니다.\n(출처: Amazon14)\n1형 당뇨는 아시아인 대비 서양인의 발병율이 높습니다. 이러한 이유가 반영된 것인지 옴니팟 홈페이지에 소개된 리전을 보면 아시아 국가는 전혀 없고 북미와 유럽, 중동 일부 국가만 존재합니다.\n하지만 안타깝게도 최근 국내 당뇨 인구가 계속 증가15 추세이고 1형 소아당뇨도 증가16 추세이기 때문에 언젠가는 타 아시아 국가나 국내에도 수입이 되서 옴니팟을 보게될지도 모르겠습니다.\n포스팅을 마무리 하는 시점에서 국내에서도 옴니팟과 유사한 컨셉으로 이오패치17라는 패치 타입 인슐린 펌프가 개발되고 판매되는 것을 확인했습니다.\n포스팅을 마무리 하는 시점이라 자세히 들여다 보지는 않았는데요 기회가 되면 이오패치도 한번 살펴보기로 하고 포스트를 마치겠습니다.\n https://www.diabetes.or.kr/general/class/index.php?idx=1\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://medicine.yonsei.ac.kr/health/encyclopedia/treat_board.do?mode=view\u0026amp;articleNo=67023\u0026amp;article.offset=0\u0026amp;articleLimit=12\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://khmcmagazine.com/?p=4913\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.hihealth.co.kr/checkupinfo/?q=YToxOntzOjEyOiJrZXl3b3JkX3R5cGUiO3M6MzoiYWxsIjt9\u0026amp;bmode=view\u0026amp;idx=1473417\u0026amp;t=board\u0026amp;category=0d5ABsc13a\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://ko.wikipedia.org/wiki/%EC%A0%9C2%ED%98%95_%EB%8B%B9%EB%87%A8%EB%B3%91\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.diabetes.or.kr/general/class/medical.php?mode=view\u0026amp;number=324\u0026amp;idx=1\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.diabetes.or.kr/general/class/medical.php?mode=view\u0026amp;number=324\u0026amp;idx=1\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.medicalnewstoday.com/articles/316618\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.diabetes.co.uk/insulin/basal-bolus.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.diabetes.or.kr/general/class/medical.php?mode=view\u0026amp;number=323\u0026amp;idx=1\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.omnipod.com/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.diabeteseducatorscalgary.ca/devices/insulin-pumps/omnipod.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://patentimages.storage.googleapis.com/29/f3/b4/64f4ad7383201d/US6830558.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.amazon.com/Pack-Omnipod-Adhesive-Stickers-Accessory/dp/B06XS2V9VP\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.healthinnews.co.kr/news/articleView.html?idxno=26266\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://www.monews.co.kr/news/articleView.html?idxno=300599\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://www.eoflow.com/eopatch/eopatch_010100.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":5,"section":"posts","tags":["인슐린","약물","당뇨"],"title":"'차세대' 인슐린 펌프 옴니팟(Omnipod)","uri":"/posts/info/review/2022-02-17-omnipod/"},{"content":"Wanna be surface dial 마이크로소프트 서피스 다이얼(Microsoft Surface Dial)12 마이크로소프트 서피스 다이얼(이하 서피스다이얼 또는 다이얼)은 퍽(Puck) 형태로 회전과 클릭이 가능한 새로운 형태의 입력 디바이스 입니다.\n데모 영상을 보았을 때 꽤나 인상적이었습니다. 주력 입력장치로의 역할은 어렵겠지만 보조 입력수단으로서 마우스나 스타일러스 펜과 함께 사용하는 장면은 매우 매력적으로 보였습니다.\n\n(클릭 시 해당 유튜브 영상으로 이동)\n그리고 다이얼을 화면에 접촉했을 때 디바이스를 중심으로 둥글게 펼쳐지는 유저 인터페이스도 신선하고 유용해 보였습니다. 다만 해당 기능은 일부 서피스 디바이스에서만 가능한점이 아쉽습니다.\n새로운 형태의 입력장치3 서피스 다이얼을 출시하면서 마이크로소프트는 Radial Controller 라는 명칭의 새로운 입력장치 타입을 정의한 것으로 보입니다. 서피스 다이얼이 아니지만 유사한 형태의 USB 타입 디바이스가 존재하는 것을 최근에 알게 되었습니다.\n그래서 관련 정보를 찾아보니 Windows radial controller implementation guide4 페이지에서 정보를 찾아볼 수 있었고, 새롭게 정의된 래디얼 컨트롤러 디바이스는 윈도우즈에서 인식/사용이 가능하다는 것을 알게 되었습니다. (리눅스 커널에서도 관련 디바이스에 대한 HID를 추가한 것으로 보입니다)\n해당 문서내 하위 링크를 참조하면 래디얼 컨트롤러는 HID(Human Interface Device)로서 USB, BLE, I2C 통신 인터페이스 형태로 연결될 수 있다는 것과 디자인 가이드, 필수적인 기능 요구사항 등이 잘 정리되어 있었습니다.\n그리고 HID 디바이스로서 호스트와 주고받는 정보에 대해 기술된 샘플 디스크립터를 Windows radial controller sample report descriptors5에서 확인 가능했습니다.\n가능성 테스트 이전에 USB HID 장치로서 미디어 볼륨이나 재생 등을 제어하는 미디어 컨트롤러 제작에 관련된 포스팅을 작성했습니다6. Windows radial controller designs7를 참고하면 미디어 컨트롤러는 래디얼 컨트롤러에서 요구하는 하드웨어 사양과 비교했을 때 최소한의 요구사항을 만족하는 것으로 보였습니다.\n그래서 샘플 디스크립터를 이전에 제작한 컨트롤러에 적용하고, 래디얼 컨트롤러로서 동작 가능성을 확인했습니다.\n다행히도 큰 어려움없이 디스크립터를 정의하고 일부 조정만으로 서피스 다이얼이 제공하는 기능 동작 흉내가 가능했습니다.\n이로써 래디얼 컨트롤러 구현은 생각보다 쉽게 끝이 났습니다. 하지만 여기서 멈추지 않고 좀 더 그럴 듯한 디바이스 제작을 위한 프로젝트를 기획합니다.\n서피스 다이얼은 무선이잖아 비슷한 상용 제품을 보면 대부분 USB를 사용하는 유선 형태의 디바이스로 되어있습니다. 사실 일부 서피스 에서만 인식 가능한 스크린 컨택트 기능을 구현할 것이 아니라면 굳이 무선일 필요는 없을 것 같습니다.\n하지만 항상 개인 프로젝트를 진행함에 있어서 중요한 것은 결과물이 아닌 과정입니다. 유사한 디바이스를 만들겠다는 결과론적인 목표보다는 과정을 통해 관련 기술의 원리나 특징을 이해하고, 원 개발자가 치열하게 고민했던 흔적을 비슷하게 따라가 보는 지적유희에 대한 목표를 지향합니다.\n그래서 서피스 다이얼은 무선이니깐 무선으로 기획을 합니다?!\n래디얼 컨틀롤러에서 지원하는 통신 인터페이스에서 무선은 BLE(Bluetooth Low Energy) 뿐 입니다. BLE 통신 모듈과 프로세서를 구분사용하는 것은 일부 제약이 있을 수 있으므로, 통신과 프로세싱이 하나의 칩이나 모듈에서 이루어지도록 Nordic사의 nrf52 프로세서를 사용하기로 합니다. (나중에 알았지만 서피스 다이얼도 해당 디바이스로 제작되어있습니다)\n알리익스프레스에서 모듈이 대략 4~7불 사이에서 판매가되고 있는 것을 확인하고 일단 주문을 걸어두었습니다. 그리고 서피스 다이얼을 보유하고 있지 않기 때문에 서피스 다이얼이 어떤 구조로 구성 되어있는지 정보를 찾아보았습니다.\n서피스 다이얼 하드웨어 구조 서피스 다이얼의 하드웨어 구조는 IFixit의 Microsoft Surface Dial Teardown8에 분해 과정과 사진이 잘 나와 있었습니다.\n분해과정을 통해 알려진 하드웨어적인 특징을 나열하면 아래와 같습니다.\n Nordic NRF52832 프로세서 BLE 4.0 회전/클릭 인터페이스 Optical 센서 기반 회전량 측정 햅틱용 진동 모터 AAA 배터리 전원 프로세서는 이미 주문한 프로세서와 동일 프로세서가 사용되고 있었습니다. 회전량을 측정함에 있어서 엔코더가 아닌 옵티컬 센서(옵티컬 마우스)를 사용하는 것이 인상깊었습니다.\n일반적인 Quadrature 엔코더는 연결 축 지름이 6mm가 거의 표준이라 58mm의 지름을 가진 큰 다이얼을 체결하기에는 안정적이지 못할 수 있으니 상대적으로 더 안정적이고 흔들림 없게 하기 위해서 원통형 축에 옵티컬 센서 기반으로 회전량을 측정한 것이 아닌가하는 생각이 들었습니다.\n진동 모터는 사용자에게 피드백을 주는 용도로 회전 시 회전 클릭감을 주는 용도로 사용되는 것 같습니다.\n서피스 다이얼 클론 제작 기획 서피스 다이얼 하드웨어 구조를 보니 3D 프린터나 CNC 작업 없이는 유사한 하드웨어 구조를 재연하는 것은 어려울 것으로 판단이 들었습니다.\n사실상 서피스 다이얼 클론을 만드는 것은 어렵고 대신 서피스 다이얼이 되고 싶은 디바이스 그래서 Wanna be surface dial이라는 이름으로 디바이스를 기획하였습니다.\n이 디바이스는 이름 답게 언젠가는 서피스 다이얼이 되는 것을 목표로 단계적으로 개선된 버젼을 만들기로 합니다.\n 프로세서\n프로세서는 이미 서피스 다이얼과 동일한 NRF52832 프로세서 모듈로 선정했습니다. 포트가 많이 필요없으므로 사이즈를 최대한 줄인 소형 모듈로 선택했습니다.\n 다이얼(Dial) 또는 노브(Knob)\n기존의 상용 노브나 다이얼을 찾아보니 서피스 다이얼 지름을 가진 노브 찾기가 어려웠습니다. 지름이 AAA 배터리를 담을 수 있을만큼 크기여야 하는데 사실 대부분 노브는 조정용도로 사용되므로 지름이 클 이유가 별로 없습니다.\n 제가 현실적으로 구할 수 있는 노브중 가장 큰 40mm 지름 사이즈로 타협을 하고, 노브 내부에는 회로 구성이 어려우므로 회로와 배터리는 별도의 엔클로저(enclosure)에 구성을하도록 합니다.\n 엔코더(Encoder)\n서피스 다이얼과 같은 구조라면 옵티컬 센서가 필요하겠지만 클릭과 회전을 위한 기구를 만드는 것은 매우 어려우므로 기존 엔코더를 활용하기로 했습니다.\n 진동모터(Vibrator)\n기존 엔코더의 경우 회전 시 클릭감을 느낄 수 있기 때문에 햅틱 피드백이 크게 필요하지 않습니다. 물론 PC용 앱에서 피드백을 주는 경우가 있을 수는 있습니다 그래서 진동 모터 채용을 고민하였으나 회로나 배터리를 담을 물리적인 공간이 여유롭지 않고, 이번에도 별도 PCB 설계는 하지 않을 예정이므로 초기 버젼에서는 고려하지 않기로 했습니다.\n 배터리(Battery)\n큰 지름의 다이얼을 구할 수 있다면야 동일하게 AAA 배터리를 이용하려고 했으나 실질적으로는 그러지 못하였기 때문에 Li-Po 배터리를 이용하고 충전하는 방식을 고려했습니다. 충전 회로는 편의를 위해서 모듈을 사용하기로 합니다.\n 개발 환경 구축 IDE 및 SDK 설치 Nordic 개발환경은 기존에 잘 알려진 IAR이나 KEIL 같은 컴파일러와 IDE가 있지만 GCC 기반으로 개발환경 구성도 가능합니다. 그리고 ARM 디버거 제작사로 잘 알려진 Segger에서는 SES? 라는(1세대 걸그룹을 연상케 하는)이름으로 Segger Embedded Studio9를 비상업적 용도 사용 시 무료로 제공합니다.\nIAR이나 KEIL도 이와 비슷하게 32kB 제한된 버젼으로 평가판을 제공하기도 하는데 BLE 관련 스택 사이즈가 크다 보니 코드 사이즈 제한에 걸리기 쉽습니다. 그래서 개발환경을 SES로 구성하도록 합니다.\nIDE는 https://www.segger.com/downloads/embedded-studio/ 에 방문하여 다운로드 및 설치 가능합니다.\n설치 후 실행하면 다음과 같은 IDE 화면을 확인 할 수 있습니다.\nSES를 설치하였으면 NRF52 개발을 위한 SDK를 다운받아 개발 할 수 있는 준비를 해야합니다. 여기서는 개발환경 구축에 대한 디테일한 설명을 하지 않습니다. 정보가 필요하신 분들은 아래의 페이지를 참고하시기 바랍니다.\nhttps://infocenter.nordicsemi.com/index.jsp?topic=%2Fug_gsg_ses%2FUG%2Fgsg%2Fintro.html\n개발용 하드웨어 구성 프로세서 모듈을 배송받고 개발을 위해 하드웨어 개발 환경을 구성했습니다. 소형 모듈에 점퍼선과 핀 헤더를 연결하여 브레드 보드에 연결하고 엔코더와 버튼 그리고 디버거을 연결할 수 있도록 하였습니다.\n테스트용 펌웨어를 라이팅하여 하드웨어 구성이나 동작에 이상이 없음을 확인하고 이제 본격적인 펌웨어 개발을 시작하도록 합니다.\n \u0026hellip; 다음 포스트에서 계속 \u0026hellip;\n https://www.microsoft.com/en-us/d/surface-dial/925r551sktgn\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://support.microsoft.com/ko-kr/surface/surface-dial-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-1e58a0e6-4d4a-6303-afcd-ef0234047628\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://blogs.windows.com/windowsdeveloper/2016/12/01/new-input-paradigm-windows-surface-dial/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-implementation-guide\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-sample-report-descriptors\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://opensourcehardware.io/posts/projects/media-controller/2021-11-08-media-controller-dev-01/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-designs\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://ifixit.com/Teardown/Microsoft+Surface+Dial+Teardown/74808\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.segger.com/products/development-tools/embedded-studio/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":6,"section":"posts","tags":["project","취미전자","Hobby Electronics","Surface Dial"],"title":"마이크로소프트 서피스 다이얼 클론 제작 - 1 (Wanna be Surface Dial)","uri":"/posts/projects/suface-dial/2022-02-15-how-to-make-1/"},{"content":"이전 포스트에서 6502 프로세서의 간략한 특징과 명령어, 어드레싱 모드에 대해서 정리를 했습니다. 에뮬레이션을 위해서는 더 많은 내용의 정리가 필요하지만 이미 레퍼런스 자료가 많이 있으므로 상세한 정보는 생략하고 일부 코드 예제를 들어 어떻게 구현할 것인지에 대한 고찰과 짤막한 코드와 함께 구현하는 과정을 남겨 보겠습니다.\n프로그래밍 언어의 선택 오픈소스로 공유된 NES 에뮬레이터들을 보면 알려진 거의 모든 언어로 포팅이 되어있었습니다. C/C++은 물론이고 Javascript 나 Go, Rust 등 언어로도 구현이 되어 공유되고 있었습니다. 처음 에뮬레이션 작업을 시작할 때 메모리나 하드웨어 제어라는 측면에서 볼 때 C언어가 적합하다고 판단하여 해당 언어로 opcode를 작성을 시작하였습니다.\n하지만 롬 파일 제어나 간단하게나마 그래픽 표시나 사운드 출력이라도 하려면 적어도 GUI 플랫폼 기반 환경에서 작업하는 것이 나을 것이라는 판단이 들었습니다. 그래서 처음에는 Visual Studio의 C++ 이나 Qt1를 고려하였었습니다. 그런데 어차피 원리만 이해하면 언어는 크게 상관이 없을거란 생각이 들어 결론적으로는 윈폼(Win Forms) 환경에서 C#으로 개발을 시작 하게 되었습니다.\nNES CPU 에뮬레이션 개발 절차 말이 좋아 에뮬레이션이지 평소 개발과는 전혀 생소한 것을 만들기 위해서 첫 코드를 작성하려니 어디서 부터 어떻게 시작을 해야할지 생각보다 막막했습니다. 목표와 해야할 일은 있었으나 어느 절차대로 해야할지 우선순위가 쉽게 파악되지 않았습니다. 그래서 일단은 공개된 코드 참조를 통해서 디테일 보다는 동작 흐름을 파악하여 에뮬레이션을 위한 지식의 마중물을 마련했습니다.\n그렇게 머릿속으로 정리를 한번 하고 그 후에는 가장 선행되어야 할 항목부터 우선 순위를 두어 다음과 같은 순서로 개발을 진행 하였습니다.\n 레지스터 정의 및 제어 메모리 정의 및 제어 명령어 세트 어드레싱 모드 명령어 사이클(Instruction Cycles) 인터럽트 및 기타 레지스터 정의 및 제어 레지스터는 총 6개이고 8비트 또는 16비트(PC) 크기를 가지고 있습니다. 레지스터 대부분이 특별한 조건이나 동작이 필요없이 일반 메모리처럼 데이터를 읽고 저장할 수 있기만 하면 되므로 크게 고려 할 사항은 없었습니다.\n다만, 상태 레지스터는 비트 단위 플래그 제어가 필요하므로 이에 따른 고려가 필요했습니다. 이때 하드웨어와 가장 유사하게 구현하는 방법은 byte 단위의 변수를 선언하고 비트 마스킹을 통해서 플래그 비트(bit)를 세트(set) 또는 클리어(clear) 하는 것입니다. 하지만 우리는 상태 레지스터 동작의 구현에 있어 반드시 자료형과 하드웨어 구성을 완전 동일해야 유지해야하는 것은 아니므로 각각의 플래그를 일반적인 자료형으로 선언하여 마스킹 대신 1과 0만 사용하는 방법을 고려했습니다.\n그래서 플래그를 byte로 단위로 정의하고 직접 1과 0만 사용하는 구조체를 작성하였습니다. C#에서는 구조체에 메서드 선언이 가능하므로 일부 조건에 의한 동작 기능 들을 메서드로 정의 하였습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 ////////////////////////////////////////////////////////// // 바이트로 선언, 비트 단위 제어 (미채용) [Flags] enum StateFlags { Carry = 0b_0000_0001, Zero = 0b_0000_0010, InterruptDisable = 0b_0000_0100, DecimalMode = 0b_0000_1000, BreakCommand = 0b_0001_0000, Unused = 0b_0010_0000, Overflow = 0b_0100_0000, Negative = 0b_1000_0000, } byte statusRegister = 0; void SetStatusFlag(StateFlags flag) {} void ClearStatusFlag(StateFlags flag) {} ////////////////////////////////////////////////////////// // 구조체로 선언, 직접 플래그 제어 또는 메서드 이용 (실질적으로 채용한 방법) struct StatusRegister { public byte CARRY; public byte ZERO; public byte INTERRUPT_DISABLE; public byte DECIMAL_MODE; public byte BREAK; public byte UNUSED; public byte OVERFLOW; public byte NEGATIVE; public void Reset() { SetByte(0x24); } public void SetByte(byte value) { this.CARRY = (byte)(value \u0026amp; 0x01); this.ZERO = (byte)((value \u0026gt;\u0026gt; 1) \u0026amp; 0x01); this.INTERRUPT_DISABLE = (byte)((value \u0026gt;\u0026gt; 2) \u0026amp; 0x01); this.DECIMAL_MODE = (byte)((value \u0026gt;\u0026gt; 3) \u0026amp; 0x01); this.BREAK = (byte)((value \u0026gt;\u0026gt; 4) \u0026amp; 0x01); this.UNUSED = (byte)((value \u0026gt;\u0026gt; 5) \u0026amp; 0x01); this.OVERFLOW = (byte)((value \u0026gt;\u0026gt; 6) \u0026amp; 0x01); this.NEGATIVE = (byte)((value \u0026gt;\u0026gt; 7) \u0026amp; 0x01); } public byte GetByte() { return (byte)((this.CARRY \u0026amp; 0x01) | ((this.ZERO \u0026lt;\u0026lt; 1) \u0026amp; 0x02) | ((this.INTERRUPT_DISABLE \u0026lt;\u0026lt; 2) \u0026amp; 0x04) | ((this.DECIMAL_MODE \u0026lt;\u0026lt; 3) \u0026amp; 0x08) | ((this.BREAK \u0026lt;\u0026lt; 4) \u0026amp; 0x10) | ((this.UNUSED \u0026lt;\u0026lt; 5) \u0026amp; 0x20) | ((this.OVERFLOW \u0026lt;\u0026lt; 6) \u0026amp; 0x40) | ((this.NEGATIVE \u0026lt;\u0026lt; 7) \u0026amp; 0x80)); } public void SetCarryFlag(ushort value) { this.CARRY = (byte)(value \u0026gt; 0xFF ? 1 : 0); } public void SetZeroFlag(byte value) { this.ZERO = (byte)(value == 0 ? 1 : 0); } public void SetNegativeFlag(byte value) { this.NEGATIVE = (byte)((value \u0026amp; 0x80) \u0026gt; 0 ? 1 : 0); } public void SetZeroNegativeFlags(byte value) { SetZeroFlag(value); SetNegativeFlag(value); } } //////////////////////////////////////////////////////// // CPU 레지스터 struct CpuRegisters { public byte X; // index X public byte Y; // index y public byte A; // accumulator public byte SP; // stack pointer public ushort PC; // program counter //public byte SR; // status register public StatusRegister SR; } CPU 메모리 인터페이스 레지스터가 마련되었으니 이제는 게임 카트리지(여기서는 롬파일)로부터 Opcode를 읽거나 램이나 레지스터에 접근하여 읽기/쓰기 작업을 수행할 수 있도록 메모리 인터페이스를 정의합니다.\nCPU는 아래에 표시된 메모리 맵2에 접근 할 수 있습니다. 하지만 CPU의 메모리 맵이라고 해서 CPU 내에 메모리만 접근 할 수 있는 것은 아니고 롬 카트리지와 PPU 레지스터의 하드웨어 메모리가 버스(Bus) 형태로 공유되어 접근할 수 있도록 되었습니다.\n일단 Mirrors 영역을 제외하면 Zero Page, Stack, RAM(CPU 내부 램)은 CPU 내에 존재하는 메모리 영역입니다. 그리고 그 외의 영역은 PPU(Picture Processing Unit)나 카트리지, 기타 하드웨어(APU, 조이스틱, 인터럽트 벡터)의 메모리나 레지스터 영역에 해당하고 버스 인터페이스를 통해 상호 접근이 가능합니다.\n// CPU 메모리 맵\r+--------------------+ $10000 +--------------------+ $10000\r| | | |\r| | | PPG-ROM |\r| | | Upper Bank |\r| | | |\r| PPG-ROM | +--------------------+ $C000\r| | | |\r| | | PPG-ROM |\r| | | Lower Bank |\r| | | |\r+--------------------+ $8000 +--------------------+ $8000\r| SRAM | | SRAM |\r+--------------------+ $6000 +--------------------+ $6000\r| Expansion ROM | | Expansion ROM |\r+--------------------+ $4020 +--------------------+ $4020\r| | | I/O Registers |\r| | +--------------------+ $4000\r| | | |\r| I/O Registers | | Mirrors |\r| | | $2000-$2007 |\r| | | |\r| | +--------------------+ $2008 | | | I/O Registers |\r+--------------------+ $2000 +--------------------+ $2000 | | | |\r| | | Mirrors |\r| | | $0000-$07FF |\r| | | |\r| RAM | +--------------------+ $0800\r| | | RAM |\r| | +--------------------+ $0200\r| | | Stack |\r| | +--------------------+ $0100\r| | | Zero Page |\r+--------------------+ $0000 +--------------------+ $0000\r하드웨어 컴포넌트를 CPU, PPU, APU, Cartridge와 같이 구분하여 구성한다고 가정한다면, 각 생성된 인스턴스를 Bus 인스턴스를 생성하고 연결하는 과정을 통해 컴포넌트간 메모리 영역을 상호 공유할 수 있도록 합니다.\n참고로 지금은 CPU만 고려하고 있고 PPU와 APU를 비롯한 기타 하드웨어 컴포넌트 들은 미구현 상태이므로 이들 접근을 위한 인터페이스나 버스 연결은 생략해도 됩니다. 다만 카트리지 영역은 접근이 가능해야, 롬 데이터를 읽어들여 실제 명령어 수행 테스트를 할 수 있으므로 나중에라도 구현에 대한 고려를 할 수 있도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 // memory read/write in CPU instance // +--------------------+ $0800 // | RAM | // +--------------------+ $0200 // | Stack | // +--------------------+ $0100 // | Zero Page | // +--------------------+ $0000 private byte[] RAM = new byte[0x800]; private byte ReadByte(ushort address) { if (address \u0026lt; 0x2000) { return RAM[address \u0026amp; 0x07FF]; } ... else if (address \u0026gt;= 0x6000) { return bus.ReadMapperByte(address); } Debug.WriteLine(String.Format(\u0026#34;Invalid Memory Address : {0:X}\u0026#34;, address)); } private void WriteByte(ushort address, byte value) { if (address \u0026lt; 0x2000) { RAM[address \u0026amp; 0x07FF] = value; } ... else if (address \u0026gt;= 0x6000) { bus.WriteMapperByte(address, value); } else { Debug.WriteLine(String.Format(\u0026#34;Invalid Memory Address : {0:X}\u0026#34;, address)); } } private ushort ReadShort(ushort address) { ... } private void WriteShort(ushort address, ushort value) { ... } public ushort ReadShortAbnormally(ushort address) { byte high; byte low = ReadByte(address); if ((address \u0026amp; 0xFF) == 0xFF) { high = ReadByte((ushort)(address \u0026amp; 0xFF00)); } else { high = ReadByte((ushort)(address + 1)); } return (ushort)((high \u0026lt;\u0026lt; 8) | low); } Zero Page, Stack, RAM을 통합하여 0x800 크기의 배열 형태로 정의하였습니다. 해당 영역은 CPU 메모리 맵 기준에서 0x2000 이하의 주소에서 접근할 수 있습니다. 하지만 실질적 메모리 크기는 0x800 이므로 접근 가능한 실제 주소와 인덱스 크기가 일치하지 않습니다. 실제 하드웨어 동작에서는 $800 이상 주소부터는 Mirrors 되어 참조 메모리 위치가 반복되므로($800을 참조하면 $00을 참조하는 것과 동일), 0x7FF로 AND 마스킹을 하면 인덱스를 벗어나지 않고 미러링 되는 효과를 나타낼 수 있습니다.\nReadMapperByte의 경우는 Cartridge(여기서는 ROM 파일)의 메모리에 접근하기 위한 메서드 입니다. 코드에는 생략되어 있지만 cartridge와 mapper 인스턴스를 생성하고 bus에 연결하여 CPU에서 참조 할 수 있도록 하였습니다. 이와 관련된 부분은 차후 테스트 관련 포스트에서 다룰 예정입니다.\nReadShort, WriteShort는 16비트 크기의 데이터를 읽고/쓰기는 메서드를 별도로 정의한 것 입니다. value를 바이트 단위로 쪼개거나 합치는 과정과 ReadByte, WriteByte를 두번 씩 호출하는 방식으로 되어 있습니다. 그리고 ReadShortAbnormally 라는 이름의 말 그대로 비정상적인 16비트 읽기를 수행하는 메서드를 정의가 되어있는데요 이는 6502의 하드웨어 버그로 인해서 일부 어드레싱 모드에서 비정상적인 주소를 참조하는 상황을 에뮬레이션 하기 위해서 별도로 선언한 것 입니다.\n이 부분은 나중에 어드레싱 모드에서 설명하도록 하겠습니다.\n스택 포인터 Zero Page 페이지와 Stack, CPU RAM 영역중에서 Zero Page와 RAM 부분은 위에 작성된 메모리 인터페이스를 통하여 직접 엑세스하여 읽고 쓰기를 수행하면 됩니다. 하지만 Stack은 별도의 동작 방식이 있으므로 SP 레지스터를 활용하여 Push, Pull(or Pop) 동작 정의가 필요합니다.\n6502는 스택 포인터가 오버플로우나 언더플로우가 발생하면, 다른 메모리 영역을 가리키지 않고 스택 메모리 영역안에서 순환하여 가리켜야 하므로 이 부분을 유의하여 정의를 하도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private readonly ushort STACK_OFFSET = 0x100; private void PushStackByte(byte value) { WriteByte((ushort)(STACK_OFFSET | registers.SP), value); registers.SP -= 1; } private byte PullStackByte() { registers.SP += 1; return ReadByte((ushort)(STACK_OFFSET | registers.SP)); } private void PushStackShort(ushort value) { byte high = (byte)((value \u0026gt;\u0026gt; 8) \u0026amp; 0xFF); byte low = (byte)(value \u0026amp; 0xFF); PushStackByte(high); PushStackByte(low); } private ushort PullStackShort() { byte low = PullStackByte(); byte high = PullStackByte(); return (ushort)((high \u0026lt;\u0026lt; 8) | low); } 스택 포인더는 CPU가 리셋이 되면 기본적으로 0xFD(253)의 값으로 초기화 됩니다. 스택 메모리 시작 오프셋이 0x100이므로 리셋이 되면 실제 스택 포인터는 $01FD 가리키게 됩니다. 스택이 하단으로 쌓이는 구조로 되어 있으므로 Push를 하면 현재 포인터에 값을 저장하고 하단으로 포인터를 변경하고, Pull을 하면 포인터를 증가시킨 후 값을 가져옵니다. SP 값이 8비트로서 0xFF(255)에서 오버플로우 되거나 0x00에서 언더플로우 되더라도 0 ~ 255를 벗어나지 않고 순환하므로 스택 메모리 영역 벗어나서 가리키지 않게 됩니다.\n이렇게 스택까지 완료가 되었으니 CPU내 메모리나 레지스터의 기본적인 제어는 완료가 되었습니다. 명령어를 가져오거나 실행할 수 있는 기반이 마련되었으므로 명령어와 명령어 사이클, 어드레싱 모드 동작을 정의하고 구현하도록 하겠습니다.\n.. 다음 포스트에서 ..\n https://www.qt.io/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://nesdev.com/NESDoc.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":7,"section":"posts","tags":["프로젝트","NES","Emulator"],"title":"NES CPU(6502) 에뮬레이션 - 2","uri":"/posts/projects/nesdev/2022-02-10-nes-cpu-emulation-02/"},{"content":"NES 에뮬레이션을 위해서 C# 환경에서 6502 CPU를 구현 하면서 참조한 지식과 경험을 정리합니다. 이전의 NESDOC1 포스트 내용이 상당수 그대로 반영이 되어있습니다.\n들어가며 NES (Nintendo Entertainment System, 아시아에서는 Famicom, 국내 정발 현대 컴보이) 에뮬레이터를 구현해보려고 마음을 먹고 관련 문서를 찾아보면서 가장 먼저 해야하는 했던 것은 NES의 전체적인 시스템 구조에 대한 이해와 더불어 NES의 CPU로 사용된 6502 프로세서 코어에 대한 이해였습니다.\n참고로 6502는 모스 테크놀로지에서 1975년에 출시한 프로세서로서2 애플 1~2, 아타리, NES 등 유명한 제품에서 사용되었기 때문에 다행히도 6502에 관련된 문서나 코드가 많이 공개되어 있d어서 참고할 수 있는 자료가 많이 있었습니다.\n하지만 코어도 제조사별 또는 개선 여부에 따라 버젼이 나뉘면서 지원하는 명령어 세트(instruction set)가 다르기고 하고, 공개된 여러 문서를 참조해보면 같은 명령어에 대한 설명이라도 명령어 사이클(instruction cycle) 단계에서 참조하는 레지스터나 변경되는 레지스터 설명 차이가 있어서 NES에 사용된 6502 코어를 유사하게 에뮬레이션을 할 수 있는지에 대한 보증을 하기가 어려웠습니다.\n대신 검증되고 공개된 에뮬레이터의 소스 코드를 참조하면 되지만 에뮬레이터 간에도 조금씩 다른 차이점을 발견하거나 코드 만으로는 이해할 수 없는 부분이 존재했기 때문에 코어에 대한 정확한 이해없이 코드 만 참조하는 것은 그냥 컨버젼 수준 밖에 되질 않기에 적극적 참조는 피하려고 했습니다.\n에뮬레이션을 구현의 주 목표는 단순히 NES 롬파일을 구동시키는 여러 에뮬레이터 중 하나(결과 지향적)를 만드는 것이 아니라 NES 시스템을 가급적 정확히 이해하는 것(절차 치향적, 지적 탐구)이 최우선이었기 때문입니다.\n그리고 이러한 목표에는 C#으로 개발중인 에뮬레이터를 차후에는 Python이나 C로 컨버젼하는 작업도 고려하고 있기 때문에 무엇보다 하드웨어 레벨에서의 이해가 많이 필요했습니다.\n하지만 맞닥뜨린 사정이 이렇다 보니 다양한 문서과 코드를 참조가 필요했고, 뼈대를 잡고 명령어 세트를 구현하면서 문서 간이나 실제 테스트 코드를 돌리면서 충돌이 나는 부분은 검증된 에뮬레이터 코드와 상호 참조를 하면서 작업을 시작했습니다.\n간략한 특징 및 레지스터 이미 Nintendo Entertainment System - CPU를 정리하면서 CPU에 대한 특징을 기술했었습니다. (정확히는 6502 코어가 사용된 Ricoh 2A03/2A07 프로세서)\n중복되는 내용이지만 간략한 특징과 레지스터 관련 설명을 다시 기술합니다.\n 항목 특징 처리능력 8bit 어드레싱 16bit 레지스터 총 6개(A, X, Y, P, SP, PC) 6502는 기본적으로 8비트 프로세서입니다. 16비트 어드레스 버스를 가지므로 64kB 메모리에 직접적인 접근이 가능합니다. 메모리는 리틀엔디언(Little Endian) 체계를 가집니다. 즉, 자료형 크기에 따라 메모리 주소가 낮을수록 LSB(Least Significant Byte)에 가까워지고, 높을수록 MSB(Most Significant Byte)에 가까워 집니다.\n기본적으로 A(Accumulator), X(X Index Register), Y(Y Index Register), P(Processor Status), SP(Stack Pointer), PC(Program Counter)와 같이 6개의 레지스터를 포함합니다.\n프로그램 카운터(PC) 프로그램 카운터는 다음에 실행할 명령어의 주소를 저장하는 16비트 레지스터입니다. 명령어가 실행되면 프로그램 카운터의 값이 업데이트되며 일반적으로 시퀀스의 다음 명령어로 이동합니다. 값은 분기 및 점프 명령, 프로시저 호출 및 인터럽트의 영향을 받을 수 있습니다.\n스택 포인터(SP) 스택은 $0100-$01FF 메모리 위치에 있습니다. 스택 포인터는 $0100에서 오프셋 역할을 하는 8비트 레지스터입니다. 스택은 하향식으로 작동하므로 바이트가 스택에 푸시되면 스택 포인터가 감소하고 스택에서 바이트를 가져오면 스택 포인터가 증가합니다. 스택 영역을 초과하는 경우 별도로 오버플로우가 감지되지 않고 스택 포인터가 $00에서 $FF로 순환 됩니다.\n누산기(A) 누산기는 산술 및 논리 연산의 결과를 저장하는 8비트 레지스터입니다. 누산기는 메모리에서 조회된 값으로 설정할 수도 있습니다.\n인덱스 레지스터 X(X) X 레지스터는 일반적으로 특정 주소 지정 모드에 대한 카운터 또는 오프셋으로 사용되는 8비트 레지스터입니다. X 레지스터는 메모리에서 조회된 값으로 설정할 수 있으며 스택 포인터의 값을 가져오거나 설정하는 데 사용할 수 있습니다.\n인덱스 레지스터 Y(Y) Y 레지스터는 X 레지스터와 같은 방식으로 카운터로 사용되거나 오프셋을 저장하는 데 사용되는 8비트 레지스터입니다. X 레지스터와 달리 Y 레지스터는 스택 포인터에 영향을 줄 수 없습니다.\n상태 레지스터(P) 상태 레지스터는 연산이 실행 될 때마다 세트 또는 클리어 되는 비트 플래그들의 집합으로 구성되는 레지스터 입니다.\n Carry Flag (C) - 마지막 명령어가 비트 7에서 오버플로우(overflow) 또는 비트 0에서 언더플로우(underflow)가 발생한 경우 캐리 플래그가 세트됩니다. 예를 들어 255 + 1을 수행하면 결과는 0이 되고 캐리 비트는 세트가 됩니다. 이를 통해 시스템은 첫 번째 바이트에서 계산을 수행하고 캐리를 저장한 다음 두 번째 바이트에서 계산을 수행할 때 해당 캐리를 사용하여 8비트보다 긴 숫자에 대한 계산을 수행할 수 있습니다. 캐리 플래그는 SEC(Set Carry Flag) 명령으로 설정하고 CLC(Clear Carry) 명령으로 Clear할 수 있습니다.\n Zero Flag(Z) - 마지막 명령어의 결과가 0인 경우 제로 플래그가 세트됩니다. 예를 들어 128 - 127은 0 플래그를 세트 되지 않는 반면 128 - 128은 세트합니다.\n Interrup Disable (I) - 인터럽트 비활성화 플래그는 시스템이 IRQ에 응답하는 것을 방지하는 데 사용할 수 있습니다. 이것은 SEI(Set Interrupt Disable) 명령에 의해 설정되고 IRQ는 CLI(Clear Interrupt Disable) 명령이 실행될 때까지 무시됩니다.\n Decimal Mode (D) - 10진수 모드 플래그는 6502를 BCD 모드로 전환하는 데 사용됩니다. 이 플래그는 SED(Set Decimal Flag) 명령으로 설정하고 CLD(Clear Decimal Flag)에 의해 클리어 할 수 있습니다. (NES용 6502에서는 사용되지 않습니다)\n Break Command (B) - BRK(Break) 명령이 실행되어 IRQ가 발생했음을 나타내는 데 사용됩니다.\n Overflow Flag (V) - 이전 명령어에서 잘못된 2의 보수 결과를 얻은 경우 오버플로우 플래그가 세트 됩니다. 이것은 양수가 예상되었을 때 음수를 얻었거나 그 반대의 경우를 의미합니다. 예를 들어, 두 개의 양수를 더하면 결과 또한 양수가 되어야 합니다. 그러나 64 + 64는 sign bit로 인해 -128 결과 제공합니다. 따라서 이 경우 오버플로우 플래그가 세트됩니다. 오버플로우 플래그는 비트 6과 7 사이와 비트 7과 캐리 플래그 사이에서 캐리의 배타적 논리합을 취하여 결정됩니다. 자세한 사항은 문서의 Appedix A를 참고하시기 바랍니다.\n Negative Flag (N) - 바이트의 비트 7은 해당 바이트의 부호를 나타내며 0은 양수이고 1은 음수입니다. 이 부호 비트가 1이면 음수 플래그(부호 플래그라고도 함)가 세트됩니다.\n // 상태 레지스터\r7 6 5 4 3 2 1 0\r+-------------------------------+\r| N | V | | B | D | I | Z | C |\r+-------------------------------+\r// 5비트는 unused\r어드레싱 모드(Addressing Mode) 6502는 복잡하고(?) 다양한 어드레싱 방식을 지원합니다. 직접적인 어드레싱 외의 다양한 방법을 사용하는 것은 어드레싱 버스 크기(여기서는 16bit)로 제한 된 영역을 벗어난 확장된 주소를 지정하거나 또는 어드레싱 버스 크기보다 상대적으로 적은 메모리를 사용하는 등의 장점을 가질 수 있습니다. 에뮬레이션을 위해서는 명령어 세트 만큼 어드레싱 모드에 대한 이해도 중요하므로 관련 내용을 정리 해보겠습니다. 여기서 메모리 주소는 HEX(16진수) 방식으로 $1F24 와 같이 표현 됩니다.\n참조 문서 및 자세한 사항은 Nesdoc의 Appendix E, Easy 65023 또는 Ultimate Commodore 64 Reference4에서 찾아볼 수 있습니다.\nImplied(Implicit) INX(Increment X Register)처럼 피연산자(operand) 위치가 결정되어 있는 명령어들이 있습니다. 이러한 명령어들은 암시적 어드레싱(Implicit Addressing)이라고 합니다.\nAccumulator 누산기(Accumlator)를 직접 조작하는 명령어들이 사용하는 어드레싱 모드 입니다.\nImmediate 특정 메모리 주소나 위치를 참조하는 것이 아니라 값(value)으로서 사용 합니다!\n1 2 3 ; 명령어 #$값 LDX #$01 ; X 레지스터에 #$01을 저장 (Immediate) LDX $01 ; X 레지스터에 $01 번지에 있는 값을 저장 (Zero Page) Zero Page Zero Page는 첫 페이지(256 byte) 메모리 $0000 ~ $00FF 위치를 뜻하기도 하며, 해당 어드레싱 모드는 바로 첫 페이지만을 참조하기 위해서 8bit 크기 만큼만 주소영역으로 사용합니다. 상대적으로 짧은 주소 및 적은 operand 사용으로 빠른 접근과 연산을 목적으로 하고 있습니다.\n1 2 ; 명령어 $주소(8bit) AND $12 ; $12 번지의 데이터와 AND 연산 Indexed Zero Page (X, Y-Indexed Zero Page) Zero Page 주소에서 X, Y 레지스터에 저장된 값 만큼 증가한 주소를 참조합니다. Zero Page Y의 경우는 오직 LDX (Load X Regisger)와 STX (Store X Register) 명령어에서만 사용합니다.\n그리고 참조할 기본 주소와 X, Y 레지스터에 저장 된 오프셋(offset)을 더한 주소가 Zero Page를 넘어가는 경우 참조할 주소는 페이지 영역을 벗어나지 않고 순환됩니다.\n예를 들어 X 레지스터 값이 $01이고 $FF가 참조할 기본 주소인 경우 최종 참조 주소는 $0100이 아닌 $00이 됩니다.\n1 2 3 LDX #$01 ; X 레지스터에 #$01을 저장 STA $FF, X ; $FF 번지에서 X 레지스터 오프셋 만큼 증가한 위치에 A 값을 저장 ; $FF + $01 is $00, not $0100 Absolute 어드레싱 버스 크기(16bit)의 메모리 주소를 참조합니다.\n1 2 ; 명령어 $주소(16bit) STA $2000 # $2000 번지에 A(accumlator) 값을 저장 Indexed Absolute (Absolute X, Y) Absolute 메모리 주소에서 X, Y 레지스터에 저장된 값 만큼 증가한 주소를 참조합니다.\n1 2 3 ; 명령어 $주소(16bit), X(or Y) LDX #$01 ; X 레지스터에 #$01을 저장 STA $2000, X ; $2000 번지에X 레지스터 값 만큼 증가한 위치($2001)에 A 값을 저장 Absolute Indirect Indirect는 어드레싱 버스 크기(16bit) 주소 위치에 저장된 값을 참조할 주소로서 해당 위치를 기준으로 16비트 크기 만큼 저장된 값의 메모리 주소를 참조합니다.\n예를 들어 $F0 주소 위치에 $01이 저장되어 있고, $F1 주소 위치에 $CC 값이 저장되어 있는 경우, Indirect 어드레싱 모드로 $00F0 위치를 참조하면 실질적으로는 $CC01을 참조하게 됩니다.\n다시 말하자면 직접 참조할 메모리 주소에 최종적으로 참조할 메모리 주소가 있다는 뜻입니다.\n1 2 3 4 5 6 ; 명령어 $주소(16bit) LDA #$01 STA $F0 LDA #$CC STA $F1 JMP ($00f0) ; 실제 참조할 주소 $CC01 Relative Branch 명령어에서 사용되는 어드레싱 입니다. 조건에 따라 프로그램 카운터 증가 값이 달라집니다. 조건에 관계없이 PC는 2만큼 증가하지만, 명령어 조건을 만족하면 추가적으로 PC가 증가합니다.\nIndexed Indirect (X-Indexed Zero Page Indirect) 여기서 부터는 두 가지 어드레싱 모드가 합쳐진 형태라 조금 복잡합니다. Nesdoc의 Appendix E에 이미지로 표현이 잘 되어 있으니 그 부분을 참고하시면 좋습니다.\n기본적으로 X-Indexed Zero Page 처럼 Zero Page 를 가리키는 주소와 X 레지스터의 오프셋만큼 더한 후 해당 위치에서 16비트 크기 만큼 참조한 값을 주소로 한 위치를 참조합니다.\n예를 들면 X 레지스터에 #$01 값이 저장되어 있고, 제로 페이지 $11 위치에는 $23가 $12에 $12가 저장되어 있다고 가정한다면 Indexed Indirect 어드레싱 모드의 명령어가 피연산자 위치를 $10로 지정할 경우 $10 + $01 = $11 (X-Indexed Zero Page) 가 되어 제로 페이지 $11을 기준으로 $11, $12 (Absolute Indirect)로 재 참조하여 $1234 위치를 참조하게 됩니다.\nIndirect Indexted (Zero Page Indirect Y-Indexed) 여기서는 위의 어드레싱 절차와 반대로 동작을 수행합니다.\nZero Page를 가리키는 주소에서 16비트 크기 만큼 참조한 값에 Y 레지스터 오프셋 만큼 더한 값을 주소로 한 메모리 영역을 참조합니다.\n예를 들어 위 어드레싱과 유사하게 Y 레지스터에 $01이 저장되어 있고, $11에는 $34, $12에는 $12가 저장되어 있다고 가정하고 명령어 피연산자 위치가 $11을 가리키는 경우 $11, $12 즉, $1234 + $01 = $1235 위치를 참조하게 됩니다.\n명령어 세트 (Instruction Set) 6502는 56개의 명령어가 있지만 일부 명령어는 여러 어드레싱 모드를 사용하기 때문에 1바이트로 구별 가능한 256개 중 총 151개의 유효한 opcode를 가집니다.\n명령어는 어드레싱 모드에 따라 1바이트 ~ 3바이트 길이를 가집니다. 첫 번째 바이트는 opcode이고 나머지 바이트는 피연산자(operand)입니다.\n아래는 Illega Opcode를 제외한 명령어 세트 테이블 입니다. 6502 instruction set5를 참조하였습니다.\n\r\rInstruction Table\r\r $x0 $x1 $x2 $x3 $x4 $x5 $x6 $x7 $x8 $x9 $xA $xB $xC $xD $xE $xF $0x BRK impl ORA X,ind ORA zpg ASL zpg PHP impl ORA # ASL A ORA abs ASL abs $1x BPL rel ORA ind,Y ORA zpg,X ASL zpg,X CLC impl ORA abs,Y ORA abs,X ASL abs,X $2x JSR abs AND X,ind BIT zpg AND zpg ROL zpg PLP impl AND # ROL A BIT abs AND abs ROL abs $3x BMI rel AND ind,Y AND zpg,X ROL zpg,X SEC impl AND abs,Y AND abs,X ROL abs,X $4x RTI impl EOR X,ind EOR zpg LSR zpg PHA impl EOR # LSR A JMP abs EOR abs LSR abs $5x BVC rel EOR ind,Y EOR zpg,X LSR zpg,X CLI impl EOR abs,Y EOR abs,X LSR abs,X $6x RTS impl ADC X,ind ADC zpg ROR zpg PLA impl ADC # ROR A JMP ind ADC abs ROR abs $7x BVS rel ADC ind,Y ADC zpg,X ROR zpg,X SEI impl ADC abs,Y ADC abs,X ROR abs,X $8x STA X,ind STY zpg STA zpg STX zpg DEY impl TXA impl STY abs STA abs STX abs $9x BCC rel STA ind,Y STY zpg,X STA zpg,X STX zpg,Y TYA impl STA abs,Y TXS impl STA abs,X $Ax LDY # LDA X,ind LDX # LDY zpg LDA zpg LDX zpg TAY impl LDA # TAX impl LDY abs LDA abs LDX abs $Bx BCS rel LDA ind,Y LDY zpg,X LDA zpg,X LDX zpg,Y CLV impl LDA abs,Y TSX impl LDY abs,X LDA abs,X LDX abs,Y $Cx CPY # CMP X,ind CPY zpg CMP zpg DEC zpg INY impl CMP # DEX impl CPY abs CMP abs DEC abs $Dx BNE rel CMP ind,Y CMP zpg,X DEC zpg,X CLD impl CMP abs,Y CMP abs,X DEC abs,X $Ex CPX # SBC X,ind CPX zpg SBC zpg INC zpg INX impl SBC # NOP impl CPX abs SBC abs INC abs $Fx BEQ rel SBC ind,Y SBC zpg,X INC zpg,X SED impl SBC abs,Y SBC abs,X INC abs,X \r\r\r\r어드레싱 모드 설명\r\r 약어 이름 포맷 A Accumulator OPC A abs absolute OPC $LLHH abs,X absolute, X-indexed OPC $LLHH,X abs,Y absolute, Y-indexed OPC $LLHH,Y # immediate OPC #$BB impl implied OPC ind indirect OPC ($LLHH) X,ind X-indexed, indirect OPC ($LL,X) ind,Y indirect, Y-indexed OPC ($LL),Y rel relative OPC $BB zpg zeropage OPC $LL zpg,X zeropage, X-indexed OPC $LL,X zpg,Y zeropage, Y-indexed OPC $LL,Y \r\r\r\r명령어 리스트(이름)\r\r Opcode Name ADC add with carry AND and (with accumulator) ASL arithmetic shift left BCC branch on carry clear BCS branch on carry set BEQ branch on equal (zero set) BIT bit test BMI branch on minus (negative set) BNE branch on not equal (zero clear) BPL branch on plus (negative clear) BRK break / interrupt BVC branch on overflow clear BVS branch on overflow set CLC clear carry CLD clear decimal CLI clear interrupt disable CLV clear overflow CMP compare (with accumulator) CPX compare with X CPY compare with Y DEC decrement DEX decrement X DEY decrement Y EOR exclusive or (with accumulator) INC increment INX increment X INY increment Y JMP jump JSR jump subroutine LDA load accumulator LDX load X LDY load Y LSR logical shift right NOP no operation ORA or with accumulator PHA push accumulator PHP push processor status (SR) PLA pull accumulator PLP pull processor status (SR) ROL rotate left ROR rotate right RTI return from interrupt RTS return from subroutine SBC subtract with carry SEC set carry SED set decimal SEI set interrupt disable STA store accumulator STX store X STY store Y TAX transfer accumulator to X TAY transfer accumulator to Y TSX transfer stack pointer to X TXA transfer X to accumulator TXS transfer X to stack pointer TYA transfer Y to accumulator \r\r비공식 명령어 (Unofficial/Illegal/Undocumented Opcodes)6 명령어 세트 중에서는 일종의 비공식 명령어 세트가 있습니다. 원래 디자인에서는 공식적으로 사용되지도 않고 문서화되어 있지 않지만 실제로는 다양한 작업을 수행할 수 있습니다. 이러한 명령어는 유용하기도 하지만 예측이 불가능하고 불안정하기도 합니다.\n일반적인 상황에서는 이러한 명령어를 무시해도 되겠지만 문제는 일부 게임들이 이 코드를 사용하여 제작되었다는 점입니다. 그래서 모든 illegal opcode를 고려할 필요는 없지만 몇가지 명령어 들은 구현을 해놔야 구동에 문제가 없을 수 있습니다.\n여기서는 일단 공식 명령어만 기술하였습니다. 자세한 내용은 참조 페이지를 확인하시길 바랍니다.\n명령어 분류 명령어는 동작 방식 또는 피연산자에 따라 다음과 같이 분류 할 수 있습니다. 각 명령어에 대한 상세 설명은 내용 분량 및 참조 페이지 기술이 잘 되어있으므로 링크로 대체합니다. 그리고 링크는 위 테이블의 참조 페이지가 아닌 Ultimate Commodore 64 Reference7 페이지를 참조했습니다.\n가져오기/저장(Load/Store Operations)\n 항목 설명 설명 메모리에서 레지스터를 로드하거나 레지스터의 내용을 메모리에 저장합니다 명령어 LDA, LDX, LDY, STX, STX, STY 레지스터 전송(Register Transfer Operations)\n 항목 설명 Register Transfer 설명 명령어 TAX, TAY, TSX, TXA, TXS, TYA Stack\n 항목 설명 설명 스택을 푸시 또는 풀하거나 X 레지스터를 사용하여 스택 포인터를 조작합니다 명령어 PHA, PHP, PLA, PLP Logical\n 항목 설명 설명 누산기 및 메모리에 저장된 값에 대한 논리 연산을 수행합니다 명령어 AND, BIT, EOR, ORA Arithmetic\n 항목 설명 설명 레지스터와 메모리에 대한 산술 연산을 수행합니다 명령어 ADC, CMP, CPX, CPY, SBC Increase/Decrease\n 항목 설명 설명 X 또는 Y 레지스터 또는 메모리에 저장된 값을 증가 또는 감소시킵니다 명령어 DEC, DEX, DEY, INC, INX, INY Shifts\n 항목 설명 설명 누산기 또는 메모리 위치의 비트를 왼쪽이나 오른쪽으로 1비트 이동합니다 명령어 ASL, LSR, ROL, ROR Jumps/Calls\n 항목 설명 설명 순차적인 실행 시퀀스를 중단하고, 지정된 주소에서 재게 합니다 명령어 BRK, JMP, JSR, RTI, RTS Branches\n 항목 설명 설명 특정한 조건을 만족하면 분기하여 Jumps/Calls 처럼 동작합니다. 조건은 상태 레지스터의 특정 비트와 관련이 있습니다 명령어 BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS Status Register\n 항목 설명 설명 상태 레지스터에서 플래그를 세트하거나 클리어 합니다 명령어 CLC, CLD, CLI, CLV, SEC, SED, SEI System Functions\n 항목 설명 설명 거의 사용하지 않는 기능을 수행합니다 명령어 NOP 참조 페이지 명령어 설명 Ultimate Commodore 64 Reference 페이지에서 명령어 설명을 참조할 시 이해를 돕기 위한 내용을 기술 합니다.\nNV-BDIZC✓✓----✓✓ADC - Add Memory to Accumulator with CarryOperation: A + M + C → A, C\nThis instruction adds the value of memory and carry from the previous operation to the value of the accumulator and stores the result in the accumulator.\nThis instruction affects the accumulator; sets the carry flag when the sum of a binary add exceeds 255 or when the sum of a decimal add exceeds 99, otherwise carry is reset. The overflow flag is set when the sign or bit 7 is changed due to the result exceeding +127 or -128, otherwise overflow is reset. The negative flag is set if the accumulator result contains bit 7 on, otherwise the negative flag is reset. The zero flag is set if the accumulator result is 0, otherwise the zero flag is reset.\nNote on the MOS 6502:\nIn decimal mode, the N, V and Z flags are not consistent with the decimal result.\n\rAddressing ModeAssembly Language FormOpcodeNo. BytesNo. CyclesImmediateADC #$nn$6922AbsoluteADC $nnnn$6D34X-Indexed AbsoluteADC $nnnn,X$7D34+pY-Indexed AbsoluteADC $nnnn,Y$7934+pZero PageADC $nn$6523X-Indexed Zero PageADC $nn,X$7524X-Indexed Zero Page IndirectADC ($nn,X)$6126Zero Page Indirect Y-IndexedADC ($nn),Y$7125+pp: =1 if page is crossed.\n\rNV-BDIZC✓-----✓-LDA - Load Accumulator with MemoryOperation: M → A\nWhen instruction LDA is executed by the microprocessor, data is transferred from memory to the accumulator and stored in the accumulator.\nLDA affects the contents of the accumulator, does not affect the carry or overflow flags; sets the zero flag if the accumulator is zero as a result of the LDA, otherwise resets the zero flag; sets the negative flag if bit 7 of the accumulator is a 1, other wise resets the negative flag.\nAddressing ModeAssembly Language FormOpcodeNo. BytesNo. CyclesImmediateLDA #$nn$A922AbsoluteLDA $nnnn$AD34X-Indexed AbsoluteLDA $nnnn,X$BD34+pY-Indexed AbsoluteLDA $nnnn,Y$B934+pZero PageLDA $nn$A523X-Indexed Zero PageLDA $nn,X$B524X-Indexed Zero Page IndirectLDA ($nn,X)$A126Zero Page Indirect Y-IndexedLDA ($nn),Y$B125+pp: =1 if page is crossed.\n --\r특정 명령어를 참조하시면 위의 카드형태로 설명이 기술 되어 있는 것을 보실 수 있습니다. 기본적으로 최상단과 중간에 이름과 설명, 우측 상단에는 명령어 실행 시 영향을 받는 상태 레지스터의 플래그를 표시해주고 있습니다. 그리고 지원하는 어드레싱 모드와 연산 수행 시 소요되는 사이클 수를 표시하고 있습니다.\n차후 설명하겠지만 소요되는 사이클은 매우 중요한데 CPU 연산 시 소모되는 시간을 정확하게 계산해야 하기 때문입니다. 물론 단순 명령 연산 결과만을 얻기 위해서는 사이클을 무시해도 됩니다. 하지만 에뮬레이터에서는 원래 하드웨어 동작과 똑같거나 거의 유사하게 할 필요가 있으므로 이 사이클을 유지할 수 있도록 별도로 관리를 합니다. 그리고 차후에 설명할 PPU와 타이밍을 맞추기 위해서도 엄격한 사이클 유지가 필요합니다.\n그리고 명령이 수핼 될 때마다 소요되는 사이클이 항상 일정하지 않고 if page(or page boundary) is crossed 라는 조건에 따라 추가적으로 사이클이 소요되는 것을 알 수 있습니다. 여기서 page라는 것은 256 바이트 단위의 바운더리를 가지는 메모리 영역을 뜻하고 page가 crossed가 되었다는 것은 16비트 기준 메모리에서 상위 바이트가 변경되었다는 의미를 나타내기도 합니다.\n마지막으로 빨간색 박스에 Note가 별도로 기술되어 있는데 재미있는 점은 일종의 하드웨어 버그가 존재하고, 이러한 내용을 알려주는 내용을 담고 있습니다. NES에 사용된 6502는 Decimal 모드를 사용하지 않으므로 무시할 수도 있는 내용이지만 몇몇의 하드웨어 버그 중 하나는 꼭 고려해야하므로 이 부분은 차후에 기술하겠습니다.\n.. 다음 포스트에서 ..\n http://nesdev.com/NESDoc.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://en.wikipedia.org/wiki/MOS_Technology_6502\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://skilldrick.github.io/easy6502\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.pagetable.com/c64ref/6502/?tab=3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.masswerk.at/6502/6502_instruction_set.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://wiki.nesdev.org/w/index.php/CPU_unofficial_opcodes\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.pagetable.com/c64ref/6502/?tab=2\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":8,"section":"posts","tags":["프로젝트","NES","Emulator"],"title":"NES CPU(6502) 에뮬레이션 - 1","uri":"/posts/projects/nesdev/2022-02-07-nes-cpu-emulation-01/"},{"content":"지난 포스트에서 미디어 컨트롤러를 제작하고 동작하는 것을 확인하였습니다.\n다만, 포스트 마지막에 남겼듯이 유튜브 다양한 기능 제어를 위해서는 미디어 컨트롤러만으로는 한계점이 있습니다. 당연히도 그럴 것이 기본적으로 표준 규격 컨트롤로서의 역할만 가능 하기 때문입니다.\n그럼 유튜브 전용 컨트롤을 위한 요구사항 부터 정리해보겠습니다.\n요구사항 범용 미디어 제어 외에 브라우저내에 유튜브 플레이어 직접 제어 윈도우 볼륨이 아닌 유튜브 플레이어 자체 볼륨만 제어 플레이 리스트가 아니더라도 다음 영상으로 이동 유튜브 챕터 이동 제어 유튜브 탐색 (앞으로 감기/뒤로 감기) 유튜브 미니/극장모드/최대화 토글 브라우저가 백그라운드에 있어도 제어가 가능 할 것 위 기능이 모두 가능하고도 미디어 컨트롤러 기능이 유지가 되어야 할 것 신규 요구사항은 모두 유튜브 커스텀 제어를 위한 것들입니다. 범용 기능으로는 한계가 있으니 커스터마이징을 통하여 위의 기능을 수행가능하도록 하는 것이 목표입니다.\n유튜브 플레이어 제어 유튜브는 페이지 내에서 다음과 같은 단축키1를 지원합니다.\n 단축키 기능 스페이스바 탐색 막대가 선택된 경우 재생/일시중지, 버튼에 포커스가 있는 경우 버튼 활성화 키보드의 미디어 재생/일시중지 키 재생/일시중지 k 플레이어에서 일시중지/재생 m 동영상 음소거/음소거 해제 키보드의 미디어 중지 키 중지 키보드의 다음 트랙 미디어 키 재생목록의 다음 트랙으로 이동 탐색바에서 왼쪽/오른쪽 화살표 5초 뒤로/앞으로 탐색 j 플레이어에서 10초 뒤로 탐색 l 플레이어에서 10초 앞으로 탐색 . 동영상이 일시중지된 경우 다음 프레임으로 건너뛰기 , 동영상이 일시중지된 경우 이전 프레임으로 돌아가기 \u0026gt; 동영상 재생 속도 높이기 \u0026lt; 동영상 재생 속도 줄이기 탐색바에서 Home/End 키 동영상의 시작/끝부분으로 탐색 탐색바에서 위쪽/아래쪽 화살표 볼륨 5% 높임/낮춤 탐색바에서 숫자 1~9(숫자 패드 아님) 동영상의 10~90% 부분 탐색 탐색바에서 숫자 0(숫자 패드 아님) 동영상의 시작 부분 탐색 / 검색창으로 이동 f 전체화면 활성화. 이미 전체화면 모드인 경우 F를 다시 누르거나 Esc 키를 누르면 전체화면 모드가 종료됩니다. c 자막이 있는 경우 자막 표시. 자막을 숨기려면 C를 다시 누릅니다. Shift+N 다음 동영상으로 이동. 재생목록을 사용하는 경우에는 재생목록의 다음 동영상으로 이동합니다. 재생목록을 사용하지 않는 경우 다음 YouTube 추천 동영상으로 이동합니다. Shift+P 이전 동영상으로 이동. 이 단축키는 재생목록을 사용하는 경우에만 작동합니다. i 미니 플레이어 열기 단축키 만으로 유튜브 플레이어 대부분 기능을 직접적으로 제어가 가능한 것을 볼 수 있습니다.\n위 정보로 판단해 볼 때 유튜브 제어를 위한 가장 쉬운 방법으로는 브라우저에 키 입력 값을 직간접적으로 전달 하는 것 입니다.\nUSB 키보드로서 키 입력 전달하기 가장 단순하고 직접적인 방법은 현재 미디어 컨트롤러 역할만 가능한 디바이스를 키보드 역할을 추가적으로 부여해서 키보드 입력 값을 직접 전송하도록 하는 것 입니다.\n참고로 USB 디바이스는 하나의 디바이스에서 다 기능 또는 역할을 부여할 수 있습니다. 우리가 사용하는 키보드 중에서 미디어 컨트롤러 기능이 포함된 키보드는 엄밀하게 따지자면 키보드 기능 뿐만 아니라 컨슈머 컨트롤러 기능이 포함된 복합 디바이스이고 이와 유사하게 정의한다면 키보드 입력을 전달 할 수 있습니다.\n그런데 이 방식의 경우 해결해야 할 문제가 하나 있습니다. 키보드 입력은 원칙적으로는 활성화 된 창에서만 그 입력이 유효합니다. 예를 들어 브라우저가 백그라운드에 있거나 아니면 유튜브가 띄워진 탭이 선택 되어 있지 않으면 아무리 키 입력을 한들 이 방법은 유효하지 않게 됩니다.\n결국 이 방법은 좋은 해결 방법이 될 수 없고, 브라우저에 좀 더 직접적으로 키 입력 정보를 전달할 다른 수단이 필요합니다.\n별도의 애플리케이션을 통한 간접 전달 직접적인 키 입력으로는 한계가 있기 때문에 간접적인 방식으로 전용 애플리케이션을 개발하는 것을 고려하였습니다. 키 입력을 전달하는데 있어서 난이도의 문제는 어떨지 모르더라도 브라우저 프로세스에 키 입력을 전달하는 것이 충분히 가능할 것이라고 예상을 하고 검증전에 우선은 USB 디바이스와 전용 애플리케이션간 데이터 공유 방법을 우선 검토하였습니다.\n보통은 USB 드라이버와 인터페이스를 통하여 기기에 직접 접근하게 되는데, 과거 LibUSB2 기반으로 만든 디바이스와 데이터 통신을 했던 기억이 남아 있어서 관련 정보를 찾아보았습니다. 결론만 남기자면 HID 디바이스의 경우 별도의 드라이버가 필요없므으로 OS 레벨에서 직접적으로 데이터를 주고 받을 수 있기 때문에 LibUSB를 사용에 대한 고려는 사실 할 필요가 없었습니다. 대신 .NET용 공식 HID API는 확인하지 못하여서 랩퍼(native) API를 찾아서 간단하게 디바이스를 검색하고 연결상태를 확인하는 테스트를 진행하였습니다.\n다음으로는 브라우져 프로세스에 키 입력 정보를 보내는 방법을 찾고 검증하는 과정이 남아있었습니다.\n이 부분도 결론만 말하자면 키 입력 정보를 보낼 수 있으나 강제로 활성화 시켜서 넘겨야 하는 방법 밖에는 찾지 못하였고 이 또한 좋은 방법은 아니기 때문에 조금 더 복잡하더라도 좀 더 나은 방법이 있는지 확인이 필요했습니다.\n그러던 중 새로운 접근 방법을 찾게 됩니다.\nWebHID345 최근 웹에서 HID 디바이스와 웹 페이지간 직접 정보를 주고받을 수 있는 규격의 API가 새로 생긴 것을 확인했습니다.\n근래에 웹 페이지에서 로컬 파일 접근을 위한 API가 정식이 되면서 직접적으로 접근이 허용되었는데, 웹에서도 USB HID 디바이스와 직접 통신할 수 있는 방법이 마련된 것 입니다. 아직은 Draft 단계이지만 Chrome 기반 브라우저에서는 이미 지원을 하고 있어서 구현이 가능한 것을 확인했습니다.\n하지만 WebHID를 사용하려면 고려해야할 점이 있습니다. WebHID를 사용하려면 스크립트로 코드를 생성해서 동작을 시켜야하는데, 유튜브는 별도의 서비스이므로 사용자 임의의 스크립트를 실행하여 직접적인 제어를 직접 할 수 없기 때문입니다.\n개인적으로는 사이트나 페이지를 만들어 유튜브 데이터 API6를 이용한 별도의 플레이어를 구성하는 방법도 사용할 수 는 있겠지만, 이는 범용적이지도 못하고 실험적인 수준에서 그치게 됩니다.\n하지만 방법이 아예 없는 것은 아닙니다. 마지막 방법은 \u0026lsquo;크롬 확장프로그램\u0026rsquo;을 만드는 것 입니다.\n확장프로그램의 경우 기존 웹 사이트의 스타일을 재 정의한다거나 아니면 사용자 임의의 스크립트를 삽입하고 실행할 수 있습니다.\n결론은 WebHID를 이용하여 미디어 컨트롤러 디바이스와 통신하고, 컨트롤러 입력 값을 유튜브의 단축키 이벤트로 변환해주는 웹앱을 만들면 됩니다. (말은 참 쉽습니다)\n개발 과정 현재 만들어진 미디어 컨트롤러는 Consumer Control 표준 규격의 정보를 전송합니다. WebHID에서는 호스트와 HID 기기간 주고 받는 정보를 읽을 수는 있지만, 이벤트나 데이터를 독점적으로 점유 할 수 없기 때문에 이를 그대로 사용할 수는 없습니다.\n예를 들어 엔코더 회전 시 유튜브 빨리 감기 기능이 가능하도록 만든다고 한다면, 컨트롤러로 엔코더를 돌리면 유튜브 영상이 앞으로 이동하면서 볼륨도 조절이 되는 기능이 겹치는 일이 발생합니다.\n그러므로 유튜브 전용 제어는 표준이 아닌 독자 규격의 인터페이스가 필요합니다.\n커스텀 디스크립터 이전 포스트에서 USB 레포트 디스크립터에 대해서 짧게 설명을 했습니다. USB 기기는 인터페이스를 추가하거나 또는 엔드포인트(데이터를 주고 받을 통로라고 생각하시면 됩니다)를 늘림으로써 논리적 기능이나 통로를 확장하는 방법을 사용할 수 있는데 이 방법 외에도 레포트 규격을 추가하여 다른 성질의 데이터 형식을 주고 받을 수 있습니다.\n쉽게 비유하자면 문서를 발송하는데 절차, 방법은 모두 동일한하고 다만 보내는 문서 포맷만 하나 더 늘리는 것 입니다.\n간단히 기술적인 설명을 하자면 Vender Defined Page라고 규격화되지 않은 커스텀 레포트 지정이 가능합니다. 주고 받을 데이터는 보내는 데이터는 1바이트만 할당하고, 받는(호스트로부터 전송된) 데이터는 64바이트로 지정하였습니다. (이 크기는 원하는대로 조절하면 됩니다)\n그리고 기존 Consumer Control과 보내는 통로가 겹치게 되므로 두 레포트를 구분할 무언가가 필요합니다. 여기에서는 Report ID를 각각 부여하여 레포트 타입이 어떤 형식인지 구분이 될 수 있도록 합니다.\n커스텀 데이터는 0x01, 미디어 컨트롤은 0x02로 부여하였고 WebHID에서 데이터를 수신 받을 때 0x01 아이디가 부여된 데이터를 인식하도록 합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const uint8_t hid_report[] = { 0x06, 0x00, 0xFF, // Usage Page = 0xFF00 (Vendor Defined Page 1) 0x09, 0x01, // Usage (Vendor Usage 1) 0xA1, 0x01, // Colsslection (Application) 0x85, 0x01, // Report ID = 1 0x19, 0x01, // Usage Minimum 0x29, 0x01, // Usage Maximum 0x75, 0x08, // Report Size: 8-bit field size 0x95, 0x01, // 8bit (Report Size) * 1 0x81, 0x00, // Input (Data, Array, Abs) 0x19, 0x01, // Usage Minimum 0x29, 0x40, // Usage Maximum 0x91, 0x00, // Output (Data, Array, Abs): Instantiates output packet fields. 0xC0, 0x05, 0x0C, // Usage Page (Consumer Device) 0x09, 0x01, // Usage (Consumer Control)\t 0xA1, 0x01, // Collection (Application)\t 0x85, 0x02,\t// Report ID = 2 0x05, 0x0C, // Usage Page (Consumer Devices)\t 0x15, 0x00, // Logical Minimum (0), bit value 0x25, 0x01, // Logical Maximum (1), bit value 0x75, 0x01, // Report Size (1) 0x95, 0x06, // Report Count (6) 0x09, USB_HID_CONSUMER_SCAN_NEXT_TRACK, // Usage 0x09, USB_HID_CONSUMER_SCAN_PREVIOUS_TRACK, // Usage 0x09, USB_HID_CONSUMER_PLAY_PAUSE, // Usage (Play / Pause) 0x09, USB_HID_CONSUMER_MUTE, // Usage 0x09, USB_HID_CONSUMER_VOLUME_INCREMENT, // Usage 0x09, USB_HID_CONSUMER_VOLUME_DECREMENT, // Usage 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x02, // Report Count (2) 0x81, 0x01, // Input (Constant) 0xC0 }; 물리적인 키의 한계와 기능확장 지난 포스트에서 미디어 컨트롤에 필요한 키는 총 6개이나, 미디어 컨트롤러에서 제어가능한 방법은 키 액션은 8개라고 했었습니다. 유튜브 제어를 위해서 추가적으로 고려한 기능은 이 숫자보다도 많으므로 입력 방법을 늘릴 방법이 필요합니다.\n그리고 기존 미디어 컨트롤러 기능도 유지가 되어야 함을 전제로 합니다.\n여기서는 팡션 Fn(Function) 키 조합 방식을 사용했습니다.\n(여기서는 기능 확장을 위해서 꼭 필요합니다.)\n4개의 키가 있는데 왼쪽부터 순서대로 네 번째 키(노란불)가 바로 Fn 키 입니다. 미디어 컨트롤러 모드에서는 일단 아무 기능도 할당되어 있지 않는데데 유튜브 조작 시에는 이 키가 사용됩니다. 다만, 미디어 컨트롤러 기능과 키가 겹치므로 컨트롤러 내부적으로는 모드가 구분되어야 합니다. 가장 오른쪽 사이드에 작은 키가 붙어 있는데 이 키가 모드를 강제로 변경하는 키 입니다.\nWebHID에서 연결이 되면 자동으로 모드를 변경할 수 있도록 고려하였습니다.\n기본적은 미디어 컨트롤러로서 동작을 하지만 모드 변경키를 누르거나 WebHID에 연결이 되면 불이 들어오는데 이 상태가 유튜브 제어가 가능한 가칭 유튜브 모드 입니다.\n불이 꺼지면 노멀 모드, 켜지면 유튜브 모드가 되겠습니다. 유튜브 모드에 대한 키입력 및 조합은 다음과 같이 정하였습니다.\n 모드 키 입력 동작 노멀 엔코더 시계 시스템 볼륨 업 엔코더 반시계 시스템 볼륨 다운 엔코더 누름 시스템 뮤트 버튼 1 시스템 이전 트랙 버튼 2 시스템 재생/정지 버튼 3 시스템 다음 트랙 버튼 4 Fn (기능 미 할당) 유튜브모드 엔코더 시계 유튜브 볼륨 업 엔코더 반시계 유튜브 볼륨 다운 엔코더 누름 유튜브 뮤트 버튼 1 유튜브 이전 트랙/챕터 영상일 경우 이전챕터 버튼 2 유튜브 재생/정지 버튼 3 유튜브 다음 트랙/단일 영상일 경우 다음 추천영상/챕터 영상일 경우 다음챕터 버튼 4 Fn (단독 사용 불가) 유튜브모드(펑션) Fn + 엔코더 시계 5초 이후 이동 Fn + 엔코더 반시계 5초 이전 이동 Fn + 엔코더 누름 유튜브 뮤트(동일) Fn + 버튼 1 미니플레이어 Fn + 버튼 2 극장모드 Fn + 버튼 3 전체화면 크롬 확장 프로그램 확장 프로그램 설명부터 만들기에 대한 내용은 별도의 포스트로 작성해야 하기 때문에 여기서는 제작 방법자체 보다는 진행 과정을 남기겠습니다.\n몇달 전에 개인 프로젝트로 커뮤니티 메모 관련 기능 확장을 위한 고민을 했고 실제 프로토타입을 구현하고 현재는 작업 보류중인 것이 있습니다.(이것도 언젠가는 한번 공개하지 않을까 싶습니다)\n당시 확장 프로그램을 구현하기 위한 플랫폼으로 VueJS, Webpack 기반으로 된 템플릿을 사용하였습니다. 첫 개발 경험을 이 템플릿을 통하여 진행하였기 때문에 처음에는 이 템플릿을 이용하려고 했습니다. 하지만 이 템플릿이 오래전의 것이라 빌드가 원활하지 않아서(npm 모듈을 수정해야함) 사용의 불편함이 있었고, 굳이 별도의 플랫폼을 사용해야 할 정도의 복잡도나 규모가 있는 앱이 아니기 때문에 바닐라JS(순수 자바스크립트)와 JQuery를 이용하여 개발을 시작했습니다.\nExtensionizr7라는 곳을 방문하면 템플릿을 쉽게 만들수 있기 때문에 여기서 최소한으로 필요한 코드를 생성하고 작업을 진행했습니다. 그러나 막상 호기롭게 시작하였으나 작업을 진행할 수록 자잘하게 신경써야할 부분이 많아지면서 그래도 기왕이면 좀 더 개발이 수월한 다른 방법을 찾아보게 됩니다.\n그리고 \u0026lsquo;기왕이면 새로운 것을 배우면서 해볼까?\u0026lsquo;하는 의식흐름으로 처음 접해보는 스벨트(Svelte)8라는 웹프레임워크 기반의 템플릿을 찾아서 사용하기로 했습니다. 다른 웹 프레임워크 대비 가볍고 심플하기 때문에 적정한 플랫폼이라고 생각했기 때문입니다. 그래서 누군가 만들어놓은 템플릿을 이용하려고 했는데 이 또한 빌드 환경이 구형 버젼이고 크롬 확장 프로그램을 명세하는 Manifest도 v3가 아닌 v2로 되어 있었기 때문에\n\u0026lsquo;직접 구성해보자\u0026rsquo; 라는 의식흐름으로 넘어와서 목이 마른자 직접 우물을 팠습니다.\n그리고 새로 만든 템플릿을 기반으로 웹앱을 만들었습니다.\n아래 링크는 템플릿만 추려서 공개해놓은 것 입니다.\nhttps://github.com/micro-artwork/chrome-extension-svelte-boilerplate\n동작 절차 유튜브 접속 시 플레이어 하단에 아래와 같은 버튼을 만들어 유튜브 플레이어가 있는 페이지에 삽입이 되도록 했습니다.\n해당 버튼을 누르면 WebHID를 통해서 디바이스의 Vendor ID와 Product ID를 기반으로 필터링하여 크롬에서 연결을 시도합니다.\n연결 버튼을 누르면 페어링이 되고 통신이 가능한 상태가 되면 탭 상단에는 조이패드 아이콘이 표시 됩니다.(크롬 자체제공)\n그리고 앱에서는 버튼의 아이콘 색을 녹색으로 표시하여 연결이 된 것을 확인할 수 있도록 하였습니다.\n여기서 다시 연결 버튼을 누르면 연결이 해제 됩니다. 물론 USB 장치를 물리적으로 분리해도 해제 됩니다.\n이 경우 조이패드 아이콘도 녹색 아이콘도 원래대로 돌아옵니다.\n여기까지 기본적인 동작 절차 입니다.\n참고로 편의성을 위해서 USB 장치가 연결되거나 유튜브 화면에 진입하면 자동으로 연결하는 것을 고려하였으나 보안상의 이유로 위와 같이 연결 확인을 거쳐야해서 자동 연결 기능은 현재로서는 불가한 것으로 확인 됩니다.\n동작 영상 동작은 다음 화면처럼 가능합니다. GIF 영상은 챕터를 이동 하는 부분입니다.\n챕터 이동은 유튜브 사이트에 단축키 공개가 되어 있지 않은데 \u0026lsquo;컨트롤 + 좌/우 화살표\u0026rsquo;를 눌러서 이동합니다.\n다행히도 이 모든 기능은 브라우져가 백그라운드 상태에 있어도 동작을 합니다. 다만 유튜브 영상을 멀티로 동작 시켰을 때에 예외처리 등은 고려하지 않았습니다.\nhttps://youtu.be/KZX7UcYx0UY\n마무리 볼륨 조절만 편하게 해볼생각으로 가벼운 마음으로 시작한 프로젝트가 다양한 경험을 해보는 프로젝트가 되었습니다.\n소스코드는 아래의 링크에서 확인 하실 수 있습니다.\n 하드웨어\nhttps://github.com/micro-artwork/youtube-player-controller\n 확장 프로그램\nhttps://github.com/micro-artwork/youtube-player-controller-chrome-extension\n 이 다음 프로젝트는 좀 더 심화된 장치를 목표로 스트림덱과 유사항 장치를 구상하였는데 구현을 위해서 검토한 MCU가 반도체 수급 문제로 현재 구할 수가 없는 상태라서 진행여부가 불투명하게 되었습니다. 그래서 디바이스 개발이 필요한 프로젝트는 장기적 유예가 될 확률이 높고 상대적으로 단순하고 재밌는 것들을 찾아서 오도록 하겠습니다.\n https://support.google.com/youtube/answer/7631406?hl=ko\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://libusb.info/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://wicg.github.io/webhid/index.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://web.dev/hid/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://developers.google.cn/youtube/v3/getting-started?hl=ko\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://extensionizr.com/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://svelte.dev/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":9,"section":"posts","tags":["project","취미전자","Hobby Electronics"],"title":"유튜브 플레이어 컨트롤러 제작기 - 02","uri":"/posts/projects/media-controller/2021-11-12-media-controller-dev-02/"},{"content":"PC에서 게임을 하거나 음악을 들을 때 간혹 소리가 너무 작거나 또는 큰 이유로 볼륨을 조절해야할 때가 종종 있습니다. 외장 DAC이나 사운드 카드 또는 스피커를 사용 시 물리적인 볼륨조절 노브(knob)가 있으면 그 것들을 이용하면 되지만 헤드폰이나 이어폰 또는 물리적인 볼륨 조절이 어려운 경우는 작업표시줄 하단의 스피커 볼륨 조절을 이용해야합니다.\n일반적인 상황에서는 큰 불편함은 없지만 전체화면으로 게임을 하는 경우 조절을 위해서는 Alt + Tab으로 빠져나오거나 또는 윈도우 키를 눌러서 작업표시줄이 보이도록 한 후에 조절을 해야하는 경우가 있어서 약간의 불편한 점이 있습니다. 물론 인 게임 내에서 조절도 가능하지만 볼륨을 쉽게 조절하는 방법이 있으면 편리하겠지요?\n이러한 요구사항을 반영하여 일부 게이밍 또는 멀티미디어 키보드는 별도의 미디어 제어 버튼을 가지고 있고, 이 버튼을 통해 쉽게 조절이 가능합니다.\n하지만 전용 키보드가 아닌 일반 기계식 키보드나 아님 폼팩터가 작고 슬림한 키보드는 이러한 키를 가지고 있지 않은 것들이 대부분이라서, 이럴때는 별도의 장치를 사용할 수도 있습니다.\n이 프로젝트는 위에 나열한 기기들처럼 윈도우 볼륨과 미디어 재생/트랙 제어가 가능한 미디어 조절기를 만드는 것입니다.\n초기 목표 및 요구사항 멋진 노브와 엔코더(encoder)를 이용한 볼륨 조절 물리적인 키를 이용하여 재생/정지, 트랙 이동 등 미디어 제어 초기 목표는 윈도우에서 미디어를 제어하는 것뿐입니다. 대신 기왕이면 고급(알루미늄 노브) 부품을 사용해서 그럴 듯 하게 만들어보려고 합니다.\n사전 지식 미디어 컨트롤러 설계와 제작에 앞서 전공 관련 내용이 어려우신 분들을 위해 이해를 돕는 사전 설명을 하고자 합니다.\nQuadrature Encoder (이하 쿼드 엔코더) Quadrature 라는 용어는 공학별로 약간씩 의미는 다르지만 전체적으로 직각 또는 직사각형과 관련된 의미를 지닌 것으로 보입니다. 수학에서는 구적법, 천문학에서는 지구 시점에서 외행성이 태양과 직각(90, 270도) 방향인 상태, 전자 공학에서는 직각 위상(位相)이라는 뜻으로 쓰입니다.1\n보통 위상이 다르다고 할 때에는 반복되는 신호가 시간 축을 기준으로 앞뒤로 이동한 상태인데, 실제로 쿼드 엔코더는 2개의 채널이 있고, 회전 시 두 개의 채널의 논리 값이 다음 그림 처럼 90도 위상차가 발생합니다.\n이미지 출처2\n이해를 쉽게 돕기 위해서 만약 180도 위상차가 난다고 가정한다면 A 채널이 하이 상태(high state, 이하 1)되면 B 채널은 로우 상태(low state, 이하 0)가 되고 반대로 A가 0이면 B가 1이 되므로 A와 B는 서로 상반된 신호 출력을 하게 될 것 입니다.\n(하이, 로우 상태는 뭘까 싶으신 분들은 쉽게 스위치가 붙었다 떼였다하는 상태라고 생각하시면 됩니다. 여기서는 디지털적인 표현으로 1과 0으로 표현합니다)\n쿼드 엔코더는 90도 차이가 나므로 두 채널의 신호는 시간(t)/2 만큼 위상 차이가 나고 각 채널의 상태가 변경되는(trasition) 시점을 기준으로 두 채널의 상태를 보면 총 4가지의 경우의 수([0, 0], [0, 1], [1, 0], [1, 1])를 가질 수 있습니다. 쉽게 말하면 두개의 스위치를 모두 뗀 것, 하나만 누른것, 둘다 누른 경우를 따진 것 입니다.\n그리고 회전 방향에 따라 4가지 상태가 일정한 패턴으로 반복되는데 이를 통해서 회전 방향과 회전량을 추측할 수 있습니다.\n오디오나 기타 기기에서 볼륨 조절기에 사용되는 엔코더의 경우 물리적 회전 시 걸림없이 부드럽게 돌아가거나 또는 특정 반경 회전 시 걸리는(pulse) 느낌의 클릭감이 있는데, 후자의 경우 하나의 펄스(이하 클릭)당 4가지 패턴이 1 사이클로 나타납니다.\n엔코더 신호 읽기 쿼드 엔코더는 모터 회전량 제어를 위해서 많이 쓰이기도 하므로 모터 제어(motor controll) 전용 MCU 경우 엔코더를 읽을 수 있는 장치(peripheral)를 내장하기도 합니다. 일반 용도(general purpose) MCU의 경우에는 이러한 장치가 없는 경우가 많으므로, 이 때에는 두 채널을 GPIO(General Purpose Input/Output) 포트에 연결한 후 변화상태를 읽으면 되는데, 예를 들어 엔코더의 A, B채널을 PORTA의 0번, 1번 PIN에 각각 할당한 뒤 PORTA의 레지스터 값을 0x03으로 마스킹하면, 엔코더의 신호 패턴을 2bit 크기의 숫자처럼 읽을 수 있습니다.\n1 uint8_t encoder_value = PORTA \u0026amp; 0x03; 그럼 심화과정으로 위의 방법을 사용하여 클릭감이 있는 쿼드 엔코더를 기준으로 1사이클 단위로 신호의 변화 값을 읽어서 회전 방향과 회전량(사이클 증가량)을 측정해보겠습니다.\n쿼드 엔코더를 위한 별도의 장치가 없는 MCU를 가정하여, 엔코더 신호 변화에 따른 각각 다른 패턴을 보기 위해서는 GPIO 핀(PIN) 인터럽트에 의한 이벤트(event-driven) 방식, 주기적으로 GPIO 포트를 읽는 폴링(polling) 방식 사용이 가능합니다. 참고로 전자의 경우에는 상승, 하강(rising/falling) 인터럽트가 모두 가능해야 디코딩이 수월합니다.\n여기서는 폴링 방식을 사용할 예정인데 주기를 USB의 SOF(Start of Frame) 이벤트에 맞추었습니다.\n폴링 방식을 사용할 경우 조건이 있는데, 엔코더 신호가 변하는 비율(rate)보다 읽는 비율이 더 높아야 합니다(가급적 2배수 이상), USB의 SOF 이벤트는 호스트에 의해 1ms(1kHz)마다 발생하므로 비교적 높은 수치이기 때문에 모터 회전이 아닌 사람이 손으로 돌리는 엔코더를 읽어오는데는 큰 문제가 없을 것으로 예상 됩니다.\n그리고 폴링 방식은 인터럽트 방식과 달리 패턴신호가 바뀌지 않음에도 정해진 시간에 지속적으로 값을 읽어서 중복 패턴값을 읽을 수 있으므로 이전 값과 현재 값이 다름을 확인하여 패턴의 변화를 감지합니다.\n프로젝트에는 PEC12R-4225F-S00243라는 엔코더를 사용하였습니다. 1회전(360° Rotation) 시 24의 펄스(클릭) 발생이 가능합니다. 이 엔코더는 비 회전 상태인 경우(펄스와 펄스사이에 위치) 두 채널 상태는 모두 [0, 0]이고 2bit 크기의 숫자값으로 패턴 변화를 본다면, 시계 방향(CW)으로 1펄스(클릭) 회전 시 1 \u0026ndash;\u0026gt; 3 \u0026ndash;\u0026gt; 2 \u0026ndash;\u0026gt; 0 순으로, 역시계 방향(CCW) 회전 시 2 \u0026ndash;\u0026gt; 3 \u0026ndash;\u0026gt; 1 \u0026ndash;\u0026gt; 0 순으로 패턴 변화를 보입니다.\n결론적으로 4번의 패턴을 누적하여 체크하면 회전을 어느 방향으로 했는지, 1 클릭 증가를 했는지 확인이 가능하므로 4개의 버퍼크기를 가진 큐(queue)를 만들어 패턴 변화를 확인하면 될 것 같습니다.\n엔코더 채널을 읽는데에는 2bit 크기로 패턴 4개만 필요하므로 byte(8bit) 단위의 변수에 좌측으로 2비트씩 쉬프팅 하면서 누적하면 적은 자원과 쉬운 방법으로 패턴 인식이 가능합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define ENCODER_CW ((0x01 \u0026lt;\u0026lt; 6) | (0x03 \u0026lt;\u0026lt; 4) | (0x02 \u0026lt;\u0026lt; 2) | 0x00) #define ENCODER_CCW ((0x02 \u0026lt;\u0026lt; 6) | (0x03 \u0026lt;\u0026lt; 4) | (0x01 \u0026lt;\u0026lt; 2) | 0x00) uint8_t encoder_value = 0; uint8_t previous_channel_value = 0; void read_encoder() { uint8_t channel_value = PORTA \u0026amp; 0x03; if (previous_value != encoder_value) { encoder_value = (encoder_value \u0026lt;\u0026lt; 2) | channel_value; switch (encoder_value) { case ENCODER_CW: // increment break; case ENCODER_CCW: // decrement break; default: break; } } previous_channel_value = channel_value; } USB Report Descriptor HID 키보드에서 전송가능한 키코드를 확인하였을 시 Volume up, down에 대한 별도 키 코드가 존재하는 것을 확인하였습니다. 지난 프로젝트인 모스 키보드처럼 USB 키보드로서 동작을 하도록 하고 이 값을 전송할 시 Windows 10에서 볼륨 조절이 되지 않습니다.\n볼륨이나 미디어 재생 등을 제어하기 위해서는 HID 키보드의 역할로는 제어를 할 수 없고 미디어 컨트롤 역할을 수행 할 수 있도록 별도의 명세가 필요합니다.\nUSB는 디바이스, 컨피그레이션, 인터페이스, 엔드포인드 디스크립터 등 많은 디스크립터를 정의하여 호스트에 연결 시 해당 정보를 교환합니다. 키보드 역할이나 미디어 컨트롤 역할이나 HID 클래스 내의 장치인 것은 동일하므로 대부분 디스크립터에 대한 변경은 필요없고, 실제 엔드포인트에서 주고 받을 정보를 명세한 HID 리포트 디스크립터(report descriptor)를 미디어 컨트롤이 가능하도록 정의가 필요합니다.\nCONSUMER CONTROL HID 장치는 Usage Page라는 항목을 지정함으로써 Report 성격을 결정할 수 있습니다. 키보드 입력 정보를 주고 받기 위해서는 Generic Desktop Page (0x01)라고 정의해야하고, 미디어 제어를 위해서는 Consumer Page (0x0C)4로 정의합니다. 그리고 하위영역에 Consumer Control을 정의합니다.\n우리가 보통 미디어 컨트롤을 할 때에는 보통 버튼 입력을 사용하기 때문에 on/off 정보만 필요하므로 굳이 큰 자료형이 필요가 없습니다. 0, 1 비트 단위의 정보 교환이 가능하므로 여러 버튼(컨트롤)을 플래그(flag)로 정의하여 1바이트에 실어 보낼 수 있도록 합니다.\n제어는 볼륨 조절(up, down), 뮤트, 트랙 제어(이전/이후), 재생만 할 것이므로 총 6비트만 필요합니다. 나머지 2비트는 사용하지 않을 키 값을 채워도 되겠지만 여기서는 padding 값으로 2비트를 지정합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const uint8_t hid_report[] = { 0x05, 0x0C, // Usage Page (Consumer Device) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x05, 0x0C, // Usage Page (Consumer Devices) 0x15, 0x00, // Logical Minimum (0), bit value 0x25, 0x01, // Logical Maximum (1), bit value 0x75, 0x01, // Report Size (1) 0x95, 0x06, // Report Count (6) 0x09, USB_HID_CONSUMER_SCAN_NEXT_TRACK, // Usage 0x09, USB_HID_CONSUMER_SCAN_PREVIOUS_TRACK, // Usage 0x09, USB_HID_CONSUMER_PLAY_PAUSE, // Usage (Play / Pause) 0x09, USB_HID_CONSUMER_MUTE, // Usage 0x09, USB_HID_CONSUMER_VOLUME_INCREMENT, // Usage 0x09, USB_HID_CONSUMER_VOLUME_DECREMENT, // Usage 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x02, // Report Count (2) 0x81, 0x01, // Input (Constant) 0xC0 }; 회로 설계 엔코더나 버튼 등을 입력포트에 잘 매칭만 하면되므로 회로 설계 시 크게 고려할 사항은 없습니다. 참고로 Pull-up, down 처리는 MCU 내부에서 처리가 되도록 되어 있습니다. 키는 기계식 키 4개와 일반 택트 스위치 1개 엔코더에 달린 키까지 총 6개의 입력이 가능하고 엔코더 디코딩 시 시계/반시계 방향 인식으로 총 8가지의 액션이 가능합니다. 앞서 디스크립터에 정의한 액션은 총 6가지 이므로 부가적으로 2가지 액션이 가능합니다.\n회로도는 이글 캐드로 작성하였습니다.\n제작 지난번과 동일하게 테스트 보드를 대상으로 펌웨어를 작성하여 예상한 대로 동작하는지 선행 작업을 진행합니다. 기초 회로와 펌웨어 검증이 어느정도 완료되면 예상 레이아웃을 배치해보고 제작을 시작합니다.\n만능기판에 기계식 키를 바로 붙이는 것이 어려워서 hot swap socket을 기판에 고정하고 그 위에 꼽는식으로 하여 고정을 하도록 하였습니다. 체결이 단단하지는 않지만 일단 제작에 고생을 덜하는 것에만 만족하기로 했습니다.\n부품을 늘여놓고 땜질을 시작합니다.\n완료된 작품 입니다!\n회로 검증과 디버깅, 입력 테스트를 거친 후 펌웨어 세부 작업을 진행합니다.\n테스트 및 동작 https://youtu.be/gxZb6ow81-E\n포커싱이 잘 안맞아 흐릿하게 나오긴 했지만 엔코더를 돌리면 볼륨 조절이 되는 것을 확인 하실 수 있습니다.\n윈도우 10에서는 브라우저에 유튜브가 재생중이면 제목, 스크린샷 정보 표시와 함께 볼륨 조절이 됩니다.\n미디어 컨트롤러도 무난하게 완성했으니 프로젝트 이것으로 끝!?\n낼 수도 있겠지만 이것만 만들려고 시작한 프로젝트는 아니오니 추가 기능을 고려해봅니다. 그럼 어떤 기능을 추가로 구현할 것인지 고민하기전에 현재 구현된 미디어 조절기의 한계점을 짚어보겠습니다.\n유튜브 재생 시 한계점과 추가 요구사항 유튜브에서는 볼륨과 재생 외에 이전/다음 트랙 이동 기능은 오직 플레이 리스트에서만 동작합니다.\n유튜브에서는 단일 영상을 묶어서 플레이 리스트로 만들 수 있는데(믹스), 이렇게 영상이 연속적으로 있는 경우에만 트랙 이동이 가능한 상태가 됩니다.\n일반 단일 유튜브 영상은 컨트롤 패널을 보면 이전 버튼이 없고 다음 이동 버튼만 있는데, 다음 영상은 엄밀하게는 추천 영상이므로 이전/다음 트랙 이동 버튼으로 이동 할 수 있는 대상이 아닙니다.\n아래 이미지를 참고하면 단일 영상과 플레이 리스트 영상일 때 패널 모양이 약간 다른 것을 확인 할 수 있습니다.\n상단 이미지는 단일 영상만 재생했을때 패널 모습이고, 하단 이미지는 믹스(플레이리스트)의 영상을 재생했을 때 패널 모습입니다.\n여튼 단일 영상에서도 다음 추천 영상으로 이동할 수 있으면 합니다.\n그리고 최근에는 타임라인을 보면 챕터(chapter)로 분리된 영상들이 있습니다. 이 영상은 단일 영상에서 점프가 가능한 구간으로 영상 내에서 특정 구간으로 이동을 쉽게 해줍니다. 이 구간을 미디어 컨트롤러로 이동할 수 있으면 좋을 것 같습니다.\n이 외에도 미디어 볼륨 조절은 윈도우 전체 볼륨을 조절하는 기능인데 유튜브만 조절하거나 음소거를 할 수 있으면 유용할 것 같습니다.\n그리고 음악이나 영상을 빠르게 앞으로 돌리거나 되돌리는 조그셔틀 같은 기능도 있으면 좋겠지요?\n생각하다보니 유튜브 전용 제어 기능만 추가하더라도 미디어 컨트롤러의 활용성이 많이 늘어 날 것 같습니다.\n그럼 다음 목표는 유튜브 전용 컨트롤 기능 구현 입니다.\n다음 포스트에서 계속\u0026hellip;!\n https://en.wikipedia.org/wiki/Quadrature\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.dynapar.com/technology/encoder_basics/quadrature_encoder/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.bourns.com/docs/product-datasheets/pec12r.pdf?sfvrsn=58877ff9_11\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.usb.org/sites/default/files/hut1_22.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":10,"section":"posts","tags":["project","취미전자","Hobby Electronics"],"title":"유튜브 플레이어 컨트롤러 제작기 - 01","uri":"/posts/projects/media-controller/2021-11-08-media-controller-dev-01/"},{"content":"잠이오지 않아 뒤척이던 어느 늦은밤 커뮤니티의 게시물을 훑어보던 도중 재미있는 짤을 하나 발견했습니다.\n아앗!\n작고 아름다운 그것은 야심한 새벽에 그렇게 제 마음에 들어왔습니다.\n마침 잠도 오질 않았기에 모스 키보드를 만들기 위해 필요한 정보와 부품들을 탐색하게 되었습니다.\n모스 부호의 기본 모스 코드에 대해서 대충 짧고 긴 신호로 메시지를 전한다는 것만 알고 있었지 자세히 아는 것은 없었습니다. 그래서 간단하게 체계를 우선 파악하기로 했습니다.\n모스 코드는 크게 dot(또는 dit), dash(또는 dah)로 신호 접점을 누르는 시간에 따라 신호 구분을 하고 이 신호의 조합에 따라 정해진 글자(character)를 전달하도록 되어있습니다. 참고로 신호 길이의 비율은 dot이 1이라고 하면 dash는 3이고, 신호 간격 1, 글자(word) 간격은 3이라고 합니다.\n만약 ㆍㅡ 와 같이 두 신호를 입력하게 되면 알파벳으로는 \u0026lsquo;A\u0026rsquo;자가 되며, 이러한 부호 조합은 아래의 표와 같습니다.12\n 한글 알파벳 모스부호 숫자,특수문자 모스부호 ㄱ L ㆍㅡㆍㆍ 1 ㆍㅡㅡㅡㅡ ㄴ F ㆍㆍㅡㆍ 2 ㆍㆍㅡㅡㅡ ㄷ B ㅡㆍㆍㆍ 3 ㆍㆍㆍㅡㅡ ㄹ V ㆍㆍㆍㅡ 4 ㆍㆍㆍㆍㅡ ㅁ M ㅡㅡ 5 ㆍㆍㆍㆍㆍ ㅂ W ㆍㅡㅡ 6 ㅡㆍㆍㆍㆍ ㅅ G ㅡㅡㆍ 7 ㅡㅡㆍㆍㆍ ㅇ K ㅡㆍㅡ 8 ㅡㅡㅡㆍㆍ ㅈ P ㆍㅡㅡㆍ 9 ㅡㅡㅡㅡㆍ ㅊ C ㅡㆍㅡㆍ 0 ㅡㅡㅡㅡㅡ ㅋ X ㅡㆍㆍㅡ . ㆍㅡㆍㅡㆍㅡ ㅌ Z ㅡㅡㆍㆍ , ㅡㅡㆍㆍㅡㅡ ㅍ O ㅡㅡㅡ ? ㆍㆍㅡㅡㆍㆍ ㅎ J ㆍㅡㅡㅡ / ㆍㅡㅡㆍㅡ ㅏ E ㆍ : ㅡㅡㅡㆍㆍㆍ ㅑ I ㆍㆍ ; ㅡㆍㅡㆍㅡㆍ ㅓ T ㅡ ' ㆍㅡㅡㅡㅡㅡㆍ ㅕ S ㆍㆍㆍ \u0026quot; ㆍㅡㆍㆍㅡㆍ ㅗ A ㆍㅡ ( ㅡㆍㅡㅡㆍ ㅛ N ㅡㆍ ) ㅡㆍㅡㅡㆍㅡ ㅜ H ㆍㆍㆍㆍㆍ - ㅡㆍㆍㆍㅡ ㅠ R ㆍㅡㆍ $ ㆍㆍㆍㅡㆍㆍㅡ ㅡ D ㅡㆍㆍ _ ㆍㆍㅡㅡㆍㅡ ㅣ U ㆍㆍㅡ 정정부호 ㅔ Y ㅡㆍㅡㅡ 한글 ㆍㆍㆍㅡㆍ ㅐ Q ㅡㅡㆍㅡ 영문 ㆍㆍㆍㆍㆍㆍㆍㆍ 기존 제품 분석 및 설계 기존 제품 분석 짧은 영상이라 분석이라고 할 것 까지는 없지만 영상을 보면 몇가지 특징을 확인 할 수 있었습니다.\n 모스 신호 입력을 위해서 단 하나의 키 입력만 사용 신호 입력 과정을 눈으로 확인하기 위해서 Dot과 Dash 정보도 전송(타이핑) 변환된 문자가 입력되기 전에는 입력된 Dot과 Dash는 지움 모스 코드로는 입력 불가한 코드 입력을 위해 별도의 커스텀 코드를 사용하는 것으로 추정 대문자 입력을 위해서 Shift가 눌린 것처럼 하기 위해서 커스텀 코드를 사용하는 것으로 추정 H 입력 후에 소문자 e 입력 하기전에 신호 입력 과정이 \u0026lsquo;.\u0026lsquo;이 \u0026lsquo;\u0026gt;\u0026lsquo;로 \u0026lsquo;-\u0026rsquo;, \u0026lsquo;_\u0026lsquo;로 변한 것을 볼때 유추 위에 나열된 특징을 요약하자면 모스코드 신호 입력 과정이 표시되며, 커스텀 코드를 사용하여 기존 모스코드에 없는 문자 입력이나 대/소문자 입력을 하는 것으로 보였습니다.\n관련 정보를 찾아보던 도중 영상의 키보드는 모스 키 입력을 위해서 만들어진게 아니라 단일 키 입력이 가능한 키보드일지도 모른다는 생각이 들었습니다. 유사한 제품이 판매하는 것을 보았기 떄문입니다.\n그래서 모스 신호 입력 및 변환은 아마도 입력된 키 값 또는 별도의 프로그래밍으로 변환하는 과정을 거친게 아닐까 예상이 되기도 합니다.\n기능 설계 위에 간단하게 분석된 내용을 바탕으로 단순히 그대로 구현하는 것보다는 독자적인 기능이나 편의사항 등을 고려하여 몇가지 개선점과 추가 기능을 고려해보았습니다. 그리고 별로의 프로그램을 사용하지 않는 단독으로 입력과 변환이 가능하도록 설계를 하기로 했습니다.\n 한글 입력을 고려 자/모 분리가 되는 현상을 회피하기 위해서 신호 입력과정 표시 유무 선택 가능 신호 입력 과정(dot, dash)을 디바이스 자체에서 LED 또는 LCD로 시각적 표시 고려 대소문자 입력은 Shift 키가 아닌 Caps Lock 키 입력을 통한 대소문자 변환 고려 신호 입력 과정이 점과 대시가 아닌 괄호와 언더바로 표시되는 현상을 개선 띄어쓰기나 들여쓰기 등 지원하지 않는 입력이 가능하도록 부가적인 커스텀 코드 적용 키 입력 시 부저(buzzer)를 이용하여 모스 신호 소리와 유사한 효과 선택 가능 Dot, Dash 입력 타이밍을 사용자가 임의로 설정 또는 입력 타미잉을 보정 하는 기능 고려 모스 코드 신호를 분석해서 키보드 입력으로 전송하는 기본적인 기능 이외에 시각적, 편의적 기능을 추가적으로 고려하였습니다.\n기능이 추가로 인해 하드웨어/소프트웨어 복잡성 증가로 회로도 커지고 고려사항이 많아지겠지만, 보통 개인 프로젝트를 할 때에는 어떤 것을 만들었다라는 결과적 성취감 보다는 만들면서 새로운 것을 배우고 고민하고 해결하는 과정을 좋아하기 때문에 가급적 신규 기능을 최대한 수용하기로 했습니다.\n하드웨어(Hardware) 설계 및 작성 MCU 선정 영상처럼 컴팩트한 느낌으로 만들려면 PCB를 설계부터 적정한 케이스를 찾거나 또는 만들어야겠지만 시간, 비용을 고려할 때 일회성 프로젝트데 많은 것을 투자할 수는 없어서 작업 용이성과 완성도 사이이의 적정한 타협이 필요했습니다.\n그래서 다음과 같은 몇가지 조건을 정했습니다.\n 완성된 디바이스 사이즈는 50 x 50mm 이하를 목표 20 ~ 30핀 이하의 소형 MCU(Micro Controller Unit) 디바이스 사용 편의성을 위하여 미리 만들어진 하드웨어 플랫폼(아두이노, 라즈베리파이 제로 등) 사용 배제 만능기판 작업을 고려하여 DIP 타입 패키지가 제공되어야 할 것 미디어 제어용 디바이스 구상을 하면서 필요한 MCU 디바이스를 찾아보고 있었기 때문에, 이후 프로젝트도 고려하여 개발이 용이한 MCU를 선정하기로 했습니다. 우선 대상은 STMicroelectronics(이하 STM)사의 STM32 시리즈 또는 Microchip사의 PIC8, 16, 32 시리즈를 우선하기로 했습니다.\n처음에는 단순한 기능 요구사항만 소형 사이즈의 8bit 프로세서를 적극 고려하였으나 처리 성능이나 peripheral 세부 기능 차이에서 부터 개발 플랫폼까지 32bit 프로세서 대비 상대적으로 부족하거나 펌웨어 개발 시 고려사항이 늘어날 것으로 판단이 되었고, 잠깐 블루투스를 고민하여 nordic사의 nRF 시리즈도 찾아보았지만 이 경우는 하드웨어 고려사항이 너무 많아지므로 나중을 기약하고 USB peripheral을 제공하는 32bit 프로세서를 사용하는 것으로 결정했습니다.\n결국 STM32와 PIC32 중에서 선택을 해야 했는데 마이크로칩 같은 경우에는 다른 제조사와 달리 거의 유일하게 DIP 타입 패키지를 제공하기 때문에 결론은 요구사항에 가장 부합하는 마이크로칩 사의 PIC32MX230F256B 28핀 디바이스로 결정하였습니다.\n기타 하드웨어 프로세서 외의 부품 중에서는 키보드 부품이 중요했는데 요즘은 기계식 키보드 축을 따로 팔고 있으므로 게이트론 저소음 갈축이 8개 세트로 판매가 되고 있길래 해당 축을 선택하였고, 그 외 회로 구성을 위한 기판, 스위치, RLC, LED 와 같은 부품을 선정하여 리스트 업 했습니다.\n회로 구성 및 작성 회로도는 현재 Autodesk에 인수된 Eagle CAD를 사용하였습니다. 인수되어 구독형 제품으로 제공되고 있는데 무료버젼도 제공하고 있고, 해당 버젼만으로도 충분하기 때문에 수월하게 회로도 작성이 가능했습니다.\n펌웨어(Firmware) 설계 및 작성 대학생때 USB 2.0 공부해보겠다고 스펙시트 500페이지 넘는걸 전체 인쇄해서 보고, 구하기도 힘든 칩은 해외 제조사에 샘플 신청해서 받아본 기억이 납니다. 다시 보려니 엄두는 안나지만 요즘은 제조사에서 친절하게 프로토콜 스택과 간단한 예제를 제공해주니 그걸 바탕으로 우선 키보드로서 동작이 되도록 하였습니다.\n개발환경 마이크로칩의 경우에는 MPLAB X라는 NetBeans IDE 기반(참고로 STM사 IDE는 Eclipse 기반)를 개발환경을 제공합니다. 32bit 프로세서가 출시된 이후부터는 이전에 Pheriperal, 통신 프로토콜(USB, Ethernet) 또는 특정 애플리케이션(LCD, Codec 등) 개별로 전용 라이브러리만 제공하던 것 대신 Harmony라는 이름으로 통합 소프트웨어 스택을 제공합니다. 그리고 별도의 내장 유틸리티로 기존 데이터 시트를 참고하여 일일히 작성하던 코드 대부분을 GUI로 쉽게 지정 및 생성할 수 있도록 변경되었습니다.(STM사 CubeMX와 유사)\n그럼 디테일한 설명은 지면상 생략하고 디버깅을 위한 UART와 USB 스택, 기타 필요한 것들을 배치하여 베이스 코드가 생성되도록 합니다.\n별도로 ThreadX나 FreeRTOS 등을 추가로 선택할 수도 있지만 굳이 RTOS까지 사용할 필요는 없으니 RTOS는 제외합니다.\n핀설정와 클럭 설정까지 완료하여 Harmony기반 베이스 코드를 생성하는 경우 Non-OS 환경이라도 명목상 Task라는 이름과 State 및 Callback 기반 펌웨어를 작성할 수 있는 기반을 마련해줍니다.\n그리고 장치나 소스코드에 따라 App_Task 와 같이 [코드 또는 장치 이름]_[함수명] 같은 구조로 생성이 됩니다. 개인적으로 C 개발 한정 snake case (소문자 및 언더스코어) 방식으로 프로그래밍을 하는데 일관성 유지를 위해서 최대한 자동 생성된 코드와 코드 컨벤션을 일치 하도록 합니다.\n모스 부호 인식 모스 부호는 짧은 신호와 긴 신호 두가지로 구분이 됩니다. 스위치를 누르는 시간을 측정하면 되므로 인풋캡쳐(Input Capture) 장치(peripheral)를 사용하여 신호 변화에 따른 시간 길이를 측정하거나 포트 인터럽트(Port Interrupt)와 타이머(Timer)를 사용해서 시간을 측정하는 방법이 있습니다. 여기서는 후자의 방법을 사용하였고 간단하게 1ms로 타이머 인터럽트 시 시간 카운트를 하도록하고 포트 신호가 변경 될 때 카운터를 리셋하거나 카운트를 세서 시간을 측정하는 단순한 방식을 사용했습니다.\n이렇게 측정된 시간에 따라서 dot, dash를 구분하고 신호를 누적하여 임시 저장합니다. 대략 이 시간은 dot은 150 ms, dash는 dot의 3배수를 두고 약간의 tolerance 시간을 두고 기준 시간 입력을 초과하였는지 여부로 간단하게 dot과 dash를 구분하게 하였습니다. 그리고 일정 시간이상 입력이 없거나(timeout)가 지정한 시간에 미치지 못하게 되면 입력이 완료된 것으로 간주하고 누적된 신호를 문자로 변환하는 과정으로 넘어가게 됩니다.\n모스 부호 변환 모스 신호는 두가지 상태만 존재하며, 모스 코드 테이블을 보면 신호의 길이는 최대 8을 넘지 않는 것을 확인 할 수 있습니다. 이러한 특징으로 신호는 두가지 밖에 없으므로 이진수(binary)로 표현 할 수 있고(dot: 0, dash: 1), 길이는 최대 8을 넘지 않으니 char 타입(1바이트)이면 입력된 신호를 임시로 저장하고 변환하는데 충분할 것으로 예상 할 수 있습니다.\n그래서 신호를 이진화하여 좌측으로 쉬프트 하면서 채워 나가면 입력된 신호를 1바이트 값에 저장할 수 있습니다. 다만 이경우 ㆍ 이나 ㆍㆍ 처럼 char 기준 모두 0으로 동일한 값이 되는 문제가 있습니다. 하지만 신호 길이가 다르므로 신호를 누적하여 저장함과 동시에 입력된 신호 길이를 재서 변환 시 참고를 하면 각자 다른 부호로 인식할 수 있습니다.\n1 2 3 4 5 6 7 unsigned char buffer = 0; unsigned char length = 0; void input_signal (char signal) { buffer = (buffer \u0026lt;\u0026lt; 1) | signal; // signal is 0 or 1 length += 1; }; 그럼 입력 가능한 모든 모스 신호를 문자로 변환하기 위해서 미리 변환하여 룩업(Lookup) 테이블을 작성합니다.\n1 2 3 4 5 6 7 unsigned char code_table[][2] = { ... { 0x01, 1 }, // A { 0x08, 4 }, // B ... }; 이렇게 작성된 테이블은 신호 입력이 완료되면 신호 길이와 코드 값으로 테이블 인덱스(index) 찾아서 해당 값을 바탕으로 HID(Human Interface Device) 키보드 코드로 변환을 해서 호스트 기기(PC)에 전송을 하면됩니다.\n저의 경우는 HID 키보드 코드 변환전에 개발의 용이와 유지보수를 위해서 제가 읽기 쉽도록 ASCII 코드로 변환 과정을 한번 더 거치도록 하였습니다.\n그래서 모스코드 테이블의 항목들은 아스키코드 순서대로 배치를 하도록 하였고, 모스부호에서 지원하지 않는 코드들은 일부 커스텀 코드로 대체하거나 또는 오프셋(offset) 유지를 위해서 빈 값(코드, 길이 모두 0)을 채우도록 하였습니다.\n그래서 전체적인 모스 신호 변환 시퀀스를 나열하면 아래와 같은 순서로 진행됩니다.\n키 입력(짧음, 김) -\u0026gt; 2진화 -\u0026gt; 바이트 변환 및 길이 저장 -\u0026gt; ASCII 코드 변환 -\u0026gt; HID 키보드 값 변환\n참고로 $와 숫자 4는 같은 HID 키보드 코드를 사용합니다. 그래서 $나 괄호같은 값은 쉬프트가 필요하므로 HID 키보드 코드와 함께 modifier(컨트롤, 쉬프트, 알트 등) 정보를 같이 전송하게 합니다.\n기타 기능 키입력을 이진화 하는 과정을 타이핑으로 보여주기 위해서서는 .(dot)과 -(dash) HID 키보드 코드를 전송합니다. 이 기능은 DIP 스위치로 끄고 켤 수 있도록 합니다. 대신 이 기능이 활성화 되어 있을 경우에는 입력된 dot과 dash를 모두 지워준 뒤에 입력될 키를 전송해야합니다. 그리고 dot과 dash가 입력되는 상황을 보여주기 위해서 키보드에 red, green 두가지 색 표시가 가능한 LED를 달아서 dot이 입력되면 빨간색, dash가 입력되면 green 표시를 해줍니다. 이 또한 DIP 스위치로 끄고 켤 수 있도록 합니다. 마지막으로 키가 눌릴때마다 MCU의 OC(Output Capture)를 이용해서 대략 800hz의 PWM(Pulse Width Modulation)로 제어하여 부저에서 신호 소리가 나올 수 있도록 하였습니다. 그리고 이 기능또한 DIP 스위치로 끄고 켤 수 있습니다.\n미완 기능 dot, dash 인식을 위한 시간 값을 사용자 임의로 지정하기 위해서 각각 신호의 길이를 5개씩 측정해서 평균값을 사용하는 기능을 기획했으나 일단 초기 릴리즈에서 제외하였습니다.\n테스트 및 제작 실제 회로를 제작하기에 앞서 기능 테스트를 위해서 오래전에 구입하고 방치해두었던 디바이스를 브레드보드에 장착시켜서 펌웨어 테스트만 진행하였습니다.\n펌웨어 기능을 확인 후 부품을 준비해서 땜질을 해봅니다.\n완성 및 시연 간만에 뜨개질 좀 했습니다.\n\r\r\r\r유튜브: https://youtu.be/risKpr9GJgU\n모스부호에 익숙치 않아서 천천히 눌러서 촬영을 하였는데, 공유된 영상은 실제보다 2배 빠른 속도입니다.\n완성된 디바이스의 회로도와 펌웨어는 Github에 공개 하였습니다.\nhttps://github.com/micro-artwork/simple_morse_keyboard\n https://morsecode.world/international/morse2.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://sbtech.kr/bbs/board.php?bo_table=sbtech_techdata\u0026amp;wr_id=32\u0026amp;page=14\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":11,"section":"posts","tags":["project","취미전자","Hobby Electronics"],"title":"모스(Morse) 부호 키보드 만들기","uri":"/posts/projects/morse-keyboard/2021-10-28-morse-keyboard-dev/"},{"content":"개발동기 18년도 초 몬스터헌터 월드가 플스로 발매될 때 워낙 유명한 게임이기도 하고 추천을 받아서 한번은 플레이를 해보고자 매제로부터 PS4를 빌린 후 게임을 예약 구입해서 첫 PS4 게임 플레이를 해보았습니다.\n당시 게이밍 환경은 PS4를 PC용 모니터에 연결하여 사용을 하였고, 지인들과 협동 플레이를 위해서 디스코드를 사용했었는데 몇가지 개인적으로 아쉬운 점이나 불편한점이 존재했습니다.\n패드 그립감 게임은 주로 PC와 XBOX 컨트롤러를 사용했기 때문에 여기에 너무 익숙해져버린 탓에 듀얼쇼크는 그립감이나 트리거 부분에 대한 아쉬움이 많이 느껴졌습니다. 별도의 컨버터를 통하여 연결이 가능했지만 빌린 콘솔을 위해서 컨버터를 구입할 수는 없었습니다.\n사운드 출력 스위칭 모니터를 디스플레이 장치로 사용하다보니 모니터 스피커나 AUX 아웃을 통하여 사운드 출력이 가능했습니다. PC에는 DAC 내장 인티앰프와 패시브 스피커를 사용하고 있었기 때문에 가능하면 DAC과 스피커를 활용하고 싶었습니다. 하지만 이럴 경우에는 게임 시 마다 광케이블을 PC에서 분리해서 PS4에 꼽아야 했는데, 물론 광 출력 분배기를 사용하는 방법도 있겠으나 이 또한 잠깐을 위해서 비용 지출을 해야하니 선뜻 할 수는 없었습니다. 그리고 스위칭을 하게 되면 PC에서 나오는 소리는 출력을 할 수 없기 때문에 게임을 하면서 동시에 유튜브 공략 영상을 본다거나 PC 기반 음성 채팅용 애플리케이션을 이용할 수 없었습니다.\n음성대화 불편 당시 PS4에서는 친구끼리 음성대화 채널 기능을 제공하지 않아서 인 게임(In-Game) 채널과 친구 음성 채널이 구분되지 못해서, 게임 내 채널이 나뉘거나 일시적으로 게임을 벗어나는 경우(홈 화면으로 진입 시) 연속적인 대화가 불가했기 때문에, PC 또는 휴대폰 디스코드를 사용하여 게임 사운드와 음성을 별도의 기기로 출력하고 이를 동시에 듣기 위해서는 이어폰을 별도로 사용해야했습니다.\n그래서 이를 일부 해결하기 위해서 변칙적인 게이밍 환경을 구성했습니다.\n 일단 PS4 게임 화면은 PC에 있는 2개의 모니터중 하나에 직접 출력 나머지 하나의 모니터에는 PC로 리모트 플레이를 켜서 사운드만 PC에 연결된 DAC과 스피커를 통해 출력 위와 같은 환경으로 구성했을 경우 2, 3번에 대한 불편함을 일부 해결 할 수 있었습니다. PS4 이더넷을 유선으로 연결하였고, 집 내 네트워크 환경이라서 그런지 다행히도 사운드가 끊기거나 체감될 정도로 딜레이 되는 것은 느끼지 못했습니다.\n그럼 남은 문제는 1번인데 ViGEmBus1라는 것을 이용하면 PC상에서 XBOX 컨트롤러를 DualShock 4로 인식시킬 수 있다는 것을 알게 되었습니다. 그래서 해당 드라이버를 설치하고 해당 드라이버와 같이 공개된 매핑을 도와주는 애플리케이션을 사용해서 XBOX 컨트롤러를 DS4로 에뮬레이션하여 리모트 플레이를 실행하고 사용 할 수 있게 되었습니다.\n그래서 결론적으로 화면 출력은 네이티브(Native) 출력으로 컨트롤러 입력과 사운드 출력은 리모트 플레이를 이용한 변칙적인 게이밍 환경을 구축하게 되었고, 다행히도 체감될만한 네트워크 지연 이슈는 거의 겪지 않았기 때문에 몬스터 헌터 월드를 즐기는데는 큰 문제를 겪지 않았습니다.\n다만 사소한 불편한 점이 하나 남아있었는데, 기존에 공개된 앱에서는 DS4 터치패드의 클릭을 매핑을 지원하지 않았습니다. 일부 게임의 경우 맵을 열거나 또는 필수적으로 해당 버튼이 필요한 경우가 있었기 때문에 결국 별도의 매핑 애플리케이션을 실행해서 해결해야 했습니다. 그런데 맵을 열때마다 컨트롤러를 놓고 컴퓨터 마우스나 키보드에 손을 대야하다보니 불편함이 점점 크게 느껴졌습니다.\n그 와중에 이를 해결할 방법을 찾던 중 ViGEmBus 드라이버에 접근할 수 있는 Client SDK2가 별도로 있는 것을 발견하게 되었고, 기왕 이렇게된거 마지막 남은 불편한 점을 개선하기 위해서 직접 개발을 하기로 결심하게 됩니다. (목마른 사람이 우물을 팝니다)\n프로토타입 개발 SharpDX3라는 DirectX API wrapper를 이용하여 XBOX 컨트롤러 상태와 입력값을 받아서 ViGEmBus를 통해 DS4 입력 값으로 컨버팅하는 프로그램 프로토타입을 만들었습니다. 어차피 혼자(?)만 사용할거라 단순하게 만들어졌습니다.\n(혼자 사용할거라 공개 예정도 없었는데 메일주소는 왜 넣었는지..)\n기존 앱이 처리하지 못했던 터치패드 클릭이나 PS Home 버튼 기능은 게임내에서 전혀 같이 눌리지 않을 것 같은 범퍼와 아날로그 스틱 클릭의 조합으로 구현되도록 했고, SharpDX에서 XBOX 컨트롤러 배터리 레벨 상태 정보도 가져올 수 있었으므로 progress 컴포넌트로 간단하게 배터리 잔량 표시와 기존 앱이 지원하지 않던 포스 피드백 진동도 지원되도록 했습니다.\n결론적으로는 처음 불편했던 점을 모두 개선하게 되었고, 갓 오브 워나 라스트 오브 어스, 레드 데드 리뎀션을 플레이하는데 전혀 문제가 없었습니다.\n불편함을 개선하겠다고 먼길을 돌아왔지만 PS4 빌려쓰는 기간동안 유용하게 잘 사용했습니다.\n다만 혼자 쓰기 아까워서 공개를 하려고 했으나 빌린 플스는 돌려준 상태였고 나중으로 미루다보니 2년을 방치했습니다.\n그러다가 만든게 아깝기도 하고 개인 프로젝트로 정리해서 남겨놓으면 좋을 것 같아 최근 개선작업을 시작했습니다.\nXI2DS 공개를 목적으로 시작하였기 때문에 정식 프로젝트화를 위해서 XInput to DualShock 4의 약어로써 XI2DS라는 이름으로 브랜딩 하고 아이콘을 그려서 적용하였습니다.\n\r\r\r기능 개선 기존 앱이 이미 충분한 기능을 제공하기 때문에 마이너한 개선만 진행 했습니다. 우선 하나만 인식가능했던 컨트롤러를 최대 4개가 인식되도록 확장했습니다.\n하지만 현재 PC용 리모트 플레이에는 단 하나의 패드만 인식 가능합니다??\n(그런데 플스가 없어서 이걸 다 만들고놓고 나서야 알았습니다)\n결론적으로 현재 상황에서는 크게 개선된 기능으로 보긴 어렵고, 다중으로 컨트롤러가 연결되어 있으면 그 중에서 골라서 쓸 수 있는 정도의 기능으로만 의미가 있을 것 같습니다.\n배터리 상태 표시(오직 무선만)를 그래픽으로 표시하도록 했고, 일반 USB 연결의 경우 유선여부도 구분될 수 있도록 하였습니다.\n기존에 포스 피드백 기능은 당연히 그대로 유지가 되었고, 특수 버튼 누르는 조합을 좀 더 단순화 시켜서 Select 버튼을 기본으로 하는 조합으로 변경되었습니다. (프로토타입은 아날로그 스틱 클릭에 범퍼, 트리거까지 동시에 눌러야하는\u0026hellip; 만든 사람도 며칠지나면 까먹는 버튼 조합이었습니다)\n특수 키 조합은 다음과 같습니다.\n 조합 매핑 Select + Left Bumper Share Select + Right Bumper Touchpad Press Select + Start PS Home 그리고 기존에는 프로그램을 닫는 경우 곧바로 종료가 되었으나, 현재는 실수로 종료 되는 것을 방지하기 위해 종료대신 트레이에 유지되도록 했습니다. 완전 종료는 트레이 또는 앱 메뉴에서 Exit를 선택하면 됩니다.\n프로그램이 중복 실행되는 것도 방지하도록 했습니다.\nGithub 공개 및 다운로드 이렇게 만들어진 프로그램은 빌드된 실행파일만 배포하는 대신 Github에 public repository로 공개 배포하였습니다. 가급적 많은 사람들에게 공개가 되길 바라면서 부족한 영작과 번역기의 도움으로 영문 README를 작성하여 공개하였습니다.\nhttps://github.com/micro-artwork/xi2ds\n실행파일 릴리즈는 불완전성을 감안하여 v0.9 버젼으로 지정하고 릴리즈 하였습니다. 그러나 릴리즈 후 자체 테스트 하자마자 트리거 버튼이 인식이 안되는 버그를 발견해서, (에일로이 왜 활을 쏘지 못하는거야) 버그 수정과 마이너한 업데이트를 거쳐서 v0.9.2로 판올림하여 일단 첫 릴리즈를 마무리 하였습니다.\n다운로드는 아래의 경로에서 가능하고 초기 실행 시 Microsoft Defender SmartScreen에 의해 \u0026lsquo;알 수 없는 게시자\u0026rsquo; 경고 화면이 나올 수 있습니다. 참고로 이 앱은 여러분 PC를 해치지 않으니 믿고(?) 실행하시면 됩니다.\n[직접 다운로드]\nhttps://github.com/micro-artwork/xi2ds/releases/download/v0.9.2/XI2DS_0.9.2.exe\n테스트 및 데모 현재 윈도우 10의 PS4 리모트 플레이 환경에서만 테스트 되었습니다. PS5 리모트 플레이나 윈도우 11 등 환경에서는 동작을 보증하지 않습니다.\n아래는 간단한 데모 영상입니다. https://gamepad-tester.com/에 방문하시면 XBOX 컨트롤러 입력이 DS4로 변환되어 입력되는 상태를 확인 하실 수 있습니다.\nhttps://youtu.be/bRIGUEyhO0w\n ViGEmBus https://github.com/ViGEm/ViGEmBus\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ViGEm.NET https://github.com/ViGEm/ViGEm.NET\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n SharpDX http://sharpdx.org/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":12,"section":"posts","tags":["project","XBOX","DualShock4"],"title":"XInput to DualShock 4 (XI2DS) 개발","uri":"/posts/projects/xi2ds/2021-10-27-xi2ds-dev/"},{"content":"XInput to Dualshock 4 (XI2DS) https://github.com/micro-artwork/xi2ds\nAbout The XI2DS is designed to run and control Play Station Remote Play(PSRP) using Xbox(or XInput) controllers(XC) instead of DualShock4(DS4) controllers on Windows. Although it is considered to be able to recognize and map up to 4 controllers, PSRP only accepts one controller input, so only the first connected controller can be recognized.\nUnfortunately, for some unknown reason, the touchpad press may not work in some games.\nIt was developed for personal use, so I haven\u0026rsquo;t been able to test many cases. After hundreds of hours of play, no major problems were found, but if find problems, I will try to fix bugs.\nFeatures Map XInput to Dualshock 4 XC and DS4 have near similar function buttons (e.g. D-Pads, start(option), analog sticks, bumpers, triggers and face buttons) and their buttons can be mapped directly. However SHARE, PS Home and Touchpad(Press) buttons of DS4 are mapped by XC\u0026rsquo;s button combinations.\nXC\u0026rsquo;s select(or View) button is not mapped and used only for combinations.\n Combinations Select + Letf Bumper Select + Right Bumper Select + Start DS4 Mapping Share Touchpad Press PS Home Support Force Feedback If play a game that supports force feedback on PSRP, XC can be vibrated.\nIndicate XC Battery(or Connection) Status Display XC\u0026rsquo;s battery level(on wireless) or wired status. Additionally, The battery level is not detailed, it will be displayed as mid for most of time. If indicate low, need to charging or replacing. And if connect a controller, will not diaplay status immediately. After any buttons input, it will be displayed.\nHow to use It is very simple to use!\n Download and install ViGEm bus driver\nhttps://github.com/ViGEm/ViGEmBus/releases\n Download and excute XI2DS\n Connect a controller\n Click \u0026lsquo;DS4 Connect\u0026rsquo; Button\n Play games on PSRP\n When click close the application, go to tray instead of termination. If want termination compeletly, use exit on menu.\nDemo https://youtu.be/bRIGUEyhO0w\n","description":"","id":13,"section":"posts","tags":["project","XBOX","DualShock4"],"title":"XInput to Dualshock 4 (XI2DS)","uri":"/posts/projects/xi2ds/2021-10-26-xi2ds/"},{"content":"요즘은 사회 전반적으로 특정 이벤트나 이슈가 생길 때마다 사람들이 재치있는 아이디어로 국내에선 짤, 해외에선 밈(meme)이라고 불리우는 패러디 이미지가 많이 퍼지고 있습니다.\n보통은 이런 이미지를 그림판이나 포토샵 등을 이용해서 만드는데 인기있는 짤의 경우 보통 대사 위치가 정해져있는 경우가 많기 때문에 별도의 이미지 도구 없이 html과 javascript를 이용하여 간단하게 짤 이미지를 생성할 수 있는 웹페이지를 한번 만들어 보겠습니다.\n가급적 관련 기술에 대해서 모르시는 분들도 어느정도 이해하고 읽을 수 있도록 내용을 풀어서 작성하였습니다. 오히려 복잡할 수도 있는데요 만약 이해하기 어려우시면 html이나 css, javascript에 대한 일부 기초 지식을 참고하시길 바랍니다.\n이미지 선정 이 이미지는 for the better, right? 라는 짤(밈)으로 커뮤니티에서 자주 볼 수 있는 스타워즈의 한장면 입니다.\n파드메의 해맑은 표정, 당황하는 표정, 아나킨의 무표정 장면을 연결해서 만들어진 이미지로 각종 패러디 이미지로 만들어지고 있습니다.\n기본 레이아웃 설계 패러디되는 짤을 보면 보통 파드메 영역에만 대사를 넣거나 또는 상단 부분에는 역할 내지 이름에 해당하는 내용을 넣기도 하기 때문에 이미지를 각 장면별로 구분하여 네등분 한다고 할 때 각 장면마다 상단, 하단에 각각 텍스트를 입력 받는 형태를 가정했습니다.\nHTML 문서 작성 HTML 포맷은 웹페이지에서 사용되는 문서 포맷중 하나 입니다. 크게 head와 body 영역으로 구분되어있고 head는 보통 문서의 정보 및 외부 참조 스크립트, 스타일 등이 포함되고 body는 실제 표시할 컨텐츠나 사용자 스크립트 등이 포함됩니다. 여기서는 자세한 내용은 다루지 않을 예정이니 기초 정보는 https://www.w3schools.com/html/ 같은 사이트를 참조하시길 바랍니다.\n레이아웃 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;image-container\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;./for-the-better-right.png\u0026#34; alt=\u0026#34;starwars\u0026#34; /\u0026gt; \u0026lt;div class=\u0026#34;textbox-container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;textbox\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;textbox\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;textbox\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;textbox\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;text-input\u0026#34; contenteditable=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 위 코드를 보면 body 영역에 div(ision) 라는 태그 이름을 가진 컨테이너(이하 영역)가 다수의 쌍으로 구성되어 있는 것을 볼 수 있습니다. 해당 영역들은 실제 컨텐츠가 저장되는 구역을 지정해놓은 것이고, 쌍으로 이루어진 영역내에 들여쓰기된 영역은 하위 영역으로 위치한 것으로 볼 수 있습니다.\n그리고 각 영역은 id 또는 class 속성(attribute) 부분에 사용자 임의의 문구로 지정이 되어있는데, 이는 영역을 구분하기 위한 것이고 일반적으로 id 속성 값은 단일 속성 값으로 단독 구분/선택이 필요할 때 사용되고, class 속성 값은 복수의 값을 가질 수 있으며 복수 구분/선택이 필요할 때 사용됩니다.\n같은 관계 설정을 하는 것 입니다. 이런 특성이 있다고 해서 꼭 역할을 엄격히 구분하지 않아도 됩니다. 이해'서울특별시' 처럼 class에서 단 하나의 영역에만 id 대신 고유 값을 지정하는 것도 가능합니다. --\r위 코드에서 계층 관계를 그리면 다음 그림과 같은 형태가 되는데 포함하는 영역과 포함된 영역은 각각 상위/하위 또는 부보/자식 관계에 해당합니다. 이후부터는 해당 명칭으로 계층 구분을 하도록 하겠습니다.\n마지막으로 text-input 영역의 경우 contenteditable라는 속성이 true로 지정되어 있는데, 이렇게 지정하는 경우에는 input 태그처럼 일반 div를 사용자 입력 이 가능한 상태로 만들어 줍니다.\n이렇게 작성된 html 코드를 문서로 저장하고 브라우져에서 읽어오면 다음과 같은 화면으로 표시가 됩니다.\n이미지 밑에 검은 박스는 contenteditable 영역을 마우스로 클릭하여 텍스트 입력 상태로 전환된 상태이고 현재 상태에서는 이미지의 각 장면에서 텍스트 입력이나 표시가 불가한 상태입니다. 이미지 하단에서만 텍스트 입력이 되는 상태이므로 원래 계획했던 레이아웃을 만들기 위해서는 div 영역에 대한 스타일을 지정해주는 작업이 필요합니다.\n스타일은 CSS(Cascading Style Sheets)1라는 스타일 언어로 각 영역의 디자인 요소를 지정 할 수 있는데 해당 언어는 영역 태그 내 style 속성이나 또는 head 내에 style 태그를 이용하거나 css 확장자를 가진 별도의 파일을 생성하고 연결하여 정의할 수 있습니다.\n여기서는 head 내의 style 영역에서 스타일을 지정하겠습니다.\n스타일 지정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;head\u0026gt; \u0026lt;style\u0026gt; #image-container { position: relative; width: 768px; height: 768px; } .textbox-container { display: flex; position: absolute; flex-wrap: wrap; width: 100%; height: 100%; top: 0; left: 0; } .textbox { display: flex; flex-direction: column; justify-content: space-between; width: 50%; height: 50%; } .text-input { padding: 16px; text-align: center; font-weight: bold; font-size: 24px; min-height: 28px; color: white; } .text-input:empty:after { content: \u0026#39;텍스트 입력\u0026#39;; color: #aaa; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; CSS에서는 태그 이름(img, input, div 등등)이나 id, class 같은 속성 값 또는 tag의 상태에 따라 영역을 선택하고 원하는 디자인 요소를 적용할 수 있습니다. 참고로 몇몇 스타일 속성들은 영역의 계층간(부모/자식) 상호 영향을 받습니다.\n그리고 id 속성 앞에 #(sharp)을, class 속성 앞에 .(dot)을 붙임으로써 해당 속성을 가진 영역 선택이 가능하므로 #과 .을 이용하여 영역별로 디자인 속성을 각각 적용 할 수 있습니다.\n먼저 이미지 컨테이너(#image-cotainer)는 밈 이미지 크기(768 x 768)에 맞게 가로세로를 지정하였습니다. 사이즈를 별도로 지정하지 않아도 내부의 컨텐츠에 맞추어 자동 크기가 지정되기도 하지만 자식 영역의 스타일 속성에 따라 달라질 수도 있기 때문에 이미지 사이즈에 맞추었습니다.\n다음은 이미지 컨테이너 하위의 텍스트 박스 컨테이너(.textbox-container)의 position 속성을 absolute로 지정 하였습니다, 이렇게 정의하면 부모 또는 문서의 최좌측 상단의 좌표를 기준으로, 독립적으로 절대적인 위치(좌표)에 영역을 표시할 수 있습니다. 그러나 이미지 컨테이너 position 속성이 relative 이기 때문에 텍스트 박스 컨테이너는 자신의 좌표 기준점을 이미지 컨테이너를 기준으로 삼게 됩니다. 그리고 top, left 절대 좌표 속성 값이 모두 0이기 때문에 텍스트 박스 컨테이너는 이미지 컨테이너 내부를 기준으로 좌상단 좌표(0, 0)에 위치하게 될 것 입니다.\n여기서 이미지 컨테이너와 img 태그(이미지)는 모두 동일한 사이즈에 img는 별도의 포지션 지정이 없으므로 결론적으로는 이미지 컨테이너, img, 텍스트 박스 컨테이너는 모두 동일한 좌표에 놓여있게 됩니다.\n그리고 텍스트 박스 컨테이너의 width, height 속성 값이 100% 로, 가로 세로 크기 속성의 퍼센트 값은 부모 영역에 영향을 받으므로 하므로 최종적으로 세 영역은 동일한 크기 동일한 좌표에 표시가 될 것 입니다.\n텍스트 박스 컨테이너 속에 텍스트 박스(.textbox)는 가로 세로 50% 로서 앞서 언급한 특성으로 인해 텍스트 컨테이너(곧 이미지 컨테이너)의 절반 사이즈(384 x 384)로 계산되어 적용되며, 텍스트 컨테이너의 display 속성 값이 flex이고 flex-wrap 속성 값이 wrap임 따라 내부에 있는 자식 영역(텍스트박스)들은 세로가 아닌 가로로 늘어진 형태로 놓일 수 있게 됩니다.\n다만 텍스트 박스 영역은 4개이고 3개 이상부터는 부모 영역의 가로 크기 넘어서는데, 정의된 스타일 속성상 가로 크기를 넘어서 표시가 될 수 없으므로 세번째, 네번째 텍스트 박스는 가로로 표시되지 못하고 하단으로 밀려 내려오게 되서 4등분 된 것처럼 보이게 됩니다.\n텍스트 박스 또한 dispaly 속성 값이 flex 이지만 flex-direction 속성 값이 column 이므로 자식 영역인 텍스트 인풋(.text-input)은 가로가 아닌 세로로 상단에서부터 순차적으로 나열됩니다. 그러나 justify-content 속성 값이 space-between 이므로 자식 영역들은 인접하지 못하고 분리가 되는데, 이 속성으로 인하여 두 개의 텍스트 인풋은 텍스트 박스 내부에서 상단과 하단 양 끝에 위치하게 됩니다.\n(flex 속성은 https://developer.mozilla.org/ko/docs/Web/CSS/flex 링크에서 더 자세한 내용을 확인할 수 있습니다.)\n마지막으로 .text-input:empty:after 선택자에 content 속성이 지정되어 있습니다. 이 선택자는 텍스트 인풋 영역 내부가 비어있는(:empty) 조건에만 발생하는 스타일 입니다. :after2와 같은 선택자는 일명 의사(pseudo, 가짜 또는 가상의) 요소(element)로서 직접 html 태그를 작성하지 않고도 컨텐츠를 생성하여 추가해주는 역할을 합니다.\n그래서 .text-input:empty:after 선택자를 풀어서 쓰면 다음과 같습니다.\n class에 text-input 값을 가진 빈 영역 내 \u0026lsquo;텍스트 입력\u0026rsquo;이라는 content를 가진 임의의 영역을 표시 이렇게 스타일을 적용한 뒤 저장하고 다시 불러오면 다음 화면과 같이 원래 의도했던 디자인 레이아웃이 적용된 것을 확인 할 수 있고 해당 위치에 텍스트 입력이 가능하게 됩니다.\n스크립트 작성 사전 준비 기본적인 레이아웃이 완성되었지만 현재 상태에서는 이미지와 텍스트를 각각 표시만 가능한 상태이므로 이미지와 텍스트를 합쳐서 하나의 이미지로 만드는 작업이 필요합니다. 이러한 작업은 html과 css만으로는 어렵기 때문에 자바스크립트를 작성해서 기능을 구현할 것 입니다.\n마침 이러한 동작을 쉽게 구현할 수 있게 해주는 html2canvas3라는 라이브러리가 있습니다. html2canvas는 html내 특정 영역을 선택하여 화면에 표시된 모양 그대로 이미지화(캡쳐) 시킬 수 있도록 도와줍니다.\n이 라이브러리를 사용하기 위해서 head 영역에 script 태그를 이용하여 라이브러리를 등록합니다.\n1 2 3 \u0026lt;head\u0026gt; \u0026lt;script src=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.2/html2canvas.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; 이처럼 등록하면 문서를 읽어올 때 해당 라이브러리를 불러올 것 입니다. 하지만 라이브러리를 등록했다고 해서 이미지 변환이 자동으로 되는 것은 아니므로 제어를 위한 이미지 캡쳐 버튼, 작성된 텍스트를 쉽게 초기화 하기 위한 텍스트 지우기 버튼 마지막으로 캡쳐된 결과를 표시할 영역을 추가합니다.\n그리고 해당 영역들에 대한 스타일도 지정을 해야 하는데 결과 이미지 영역은 평소엔 보이지 않다가 캡쳐 동작 이후에만 화면 정중앙에 팝업 형태로 표시할 예정이므로 display 속성을 none으로 하여 기본적으로 보이지 않도록 합니다.\n앞서 정의했던 .text-input:empty:after 선택자를 #image-container:not(.placeholder\u0026ndash;hidden) .text-input:empty:after 로 변경해줍니다. 이에 대한 설명은 하단에 설명하겠습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 \u0026lt;head\u0026gt; \u0026lt;style\u0026gt; ... #image-container:not(.placeholder--hidden) .text-input:empty:after { content: \u0026#39;텍스트 입력\u0026#39;; color: #aaa; } #dialog-dimming { display: none; position: absolute; width: 100%; height: 100%; top: 0; left: 0; align-items: center; justify-content: center; background-color: #0000007f; } #dialog { display: inline-block; background-color: #fff; border-radius: 4px; padding: 16px; } button { margin: 8px 8px 0 0; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... \u0026lt;div\u0026gt; \u0026lt;button id=\u0026#34;btn-capture\u0026#34;\u0026gt;이미지 캡쳐\u0026lt;/button\u0026gt; \u0026lt;button id=\u0026#34;btn-clear\u0026#34;\u0026gt;텍스트 지우기\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div id=\u0026#34;dialog-dimming\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;dialog\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;dialog-content\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;button id=\u0026#34;btn-close\u0026#34;\u0026gt;닫기\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; 스크립트 UI 요소가 마련되었으므로 실제로 동작 수행을 위해서 스크립트를 작성합니다. body 하단 영역에 script를 태그를 추가해줍니다. 해당 스크립트는 문서가 모두 로드 되면 호출이 될 것입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 \u0026lt;body\u0026gt; ... \u0026lt;scrpit\u0026gt; // elements const imgContainerEl = document.getElementById(\u0026#39;image-container\u0026#39;); const dimmingEl = document.getElementById(\u0026#39;dialog-dimming\u0026#39;); const dialogContentEl = document.getElementById(\u0026#39;dialog-content\u0026#39;); // events document.getElementById(\u0026#39;btn-capture\u0026#39;).onclick = () =\u0026gt; { dimmingEl.style.display = \u0026#39;flex\u0026#39;; imgContainerEl.classList.add(\u0026#39;placeholder--hidden\u0026#39;); html2canvas(imgContainerEl) .then((canvasEl) =\u0026gt; { dialogContentEl.appendChild(canvasEl); }) .catch((error) =\u0026gt; { console.log(error); }) .then(() =\u0026gt; { imgContainerEl.classList.remove(\u0026#39;placeholder--hidden\u0026#39;); }); }; document.getElementById(\u0026#39;btn-clear\u0026#39;).onclick = () =\u0026gt; { [...document.getElementsByClassName(\u0026#39;text-input\u0026#39;)].forEach((boxEl) =\u0026gt; (boxEl.innerHTML = \u0026#39;\u0026#39;)); }; document.getElementById(\u0026#39;btn-close\u0026#39;).onclick = () =\u0026gt; { ...dialogContentEl.children].forEach((childEl) =\u0026gt; childEl.remove()); dimmingEl.style.display = \u0026#39;none\u0026#39;; }; \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; 자바스크립트를 전혀 모르시는 분들이 있을 수 있으므로 코드 보다는 동작 위주로 설명을 하겠습니다. 먼저 doument.getElementById 를 이용해서 스크립트에서 직접 속성 조작이 필요한 영역(element)을 선택해줍니다.\n그리고 각 버튼은 사용자가 클릭 시 동작해야 하므로 onlick에 이벤트를 설정합니다.\n먼저 캡쳐 버튼(btn-capture)의 이벤트 동작은 결과 이미지를 표시할 영역(팝업 다이얼로그) display 속성을 none 에서 flex로 변경해줍니다. html에서는 css 조작만으로도 새로고침이나 특별한 작업 없이도 새 스타일 적용이 가능하기 때문에 이전에 표시가 되지 않던 팝업 화면이 보이게 됩니다.\n그리고 이미지 컨테이너 class에 placeholder\u0026ndash;hidden 값을 추가 해주는데 이 것을 추가하는 이유는 앞서 텍스트 인풋에 아무런 내용도 없을 때 표시해주던 메시지를 숨김처리 하기 위함 입니다. 이 동작을 수행하지 않으면 텍스트를 입력하지 않은 영역에서는 텍스트 입력이라는 글자가 같이 캡쳐가 됩니다.\n그래서 캡쳐 직전에 일시적으로 숨김처리하고 캡쳐 이후에 이미지 컨테이너 class에서 placeholder\u0026ndash;hidden 값을 제거해서 다시 원래 상태로 되돌려 놓습니다.\n그 다음엔 html2canvas 라이브러리로 이미지 컨테이너 영역을 캡쳐합니다. 이때 반환되는 canvas element를 결과 이미지를 표시할 다이얼로그 컨텐트 영역에 추가하면 캡쳐된 이미지를 확인 할 수 있습니다.\n닫기 버튼은 아까 생성하여 추가한 canvas를 지우고 다이얼로그 영역을 숨김처리 합니다. 텍스트 지우기 버튼은 getElementsByClassName를 이용하여 모든 텍스트 인풋 영역들을 선택해서 내부 컨텐츠를 공백 문자로 변경하여 초기화 시킵니다.\n결과 자 그럼 이렇게 작성된 코드를 저장해서 다시 불러오면 다음과 같이 쉽게 짤을 생성할 수 있는 페이지가 완성되었습니다. 캡쳐된 이미지는 마우스 오른 클릭 메뉴를 통해 저장 또는 복사가 가능합니다.\n만들어진 페이지는 다음 주소에서 사용해보실 수 있습니다.\n스타워즈 밈 생성기\n그리고 최근 인기가 있는 김연경 선수 밈이나 집이 무너졌어요 슬펐어요 (그것이 알고싶다 싱크홀 편) 밈도 추가된 페이지 링크도 공유드립니다.\n밈 생성기\n추가 정보 아까 CSS 파일이 별도로 작성하여 연결이 가능하지만 head 내에 직접 삽입도 가능했습니다. 이미지 파일도 Base64라는 문자열 데이터로 변환하면 마찬가지로 html에 삽입할 수 있습니다.\n물론 이렇게 하는 경우 매우 긴 문자로 문서개 가득채워지기 때문에 복잡하지만 html 파일 하나로만 작성이 가능하기 때문에 변환 및 적용 방법을 알려드리겠습니다.\n먼저 이미지 변환을 위해 https://elmah.io/tools/base64-image-encoder/ 같은 사이트에 이미지를 업로드 하여 변환을 합니다.\n잠시 기다리면 결과 값이 나오는데 중간에 HTML usage 를 보시면\nimg src=\u0026ldquo;data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4\u0026hellip;.\n와 같이 매우 긴 문자열로 변환된 값을 보실 수 있습니다. 이 태그를 img 태그 대신 사용하시면 별도로 이미지 파일없이 html 파일 하나만으로 결과물을 만들 수 있습니다.\n https://developer.mozilla.org/ko/docs/Web/CSS\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://developer.mozilla.org/ko/docs/Web/CSS/::before\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://html2canvas.hertzen.com/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":14,"section":"posts","tags":["web","javascript","meme"],"title":"짤(밈) 생성 페이지 제작","uri":"/posts/dev/web/2021-10-07-simple-meme-generator/"},{"content":"Nintendo Entertainment System\nNesDev Wiki1와 Nintendo Entertainment System Documentation2 문서를 기반으로 NES 시스템을 정리 하고자 합니다. 해당 문서에서는 하드웨어 및 프로세서에 대한 내용을 많이 담고 있어서 이해를 위해서는 기본적인 마이크로 프로세서에 대한 지식이 요구됩니다.\n문제가 발생할 경우 해당 포스트는 삭제 또는 비공개 처리 됩니다.\nCPU 오버뷰 메인 CPU는 Richo 2A033 8비트 프로세서로 BCD 모드를 제거한 6502 코어에 22개의 메모리 맵 I/O 레지스터를 가지고 있으며 PSG(Programmable Sound Generator) 사운드, DMA, 게임 컨트롤러 폴링 등의 기능을 추가 한 CPU 입니다.\n동작은 NTSC 모드에서 1.79MHz, PAL 모드에서는 1.66 MHz 속도로 동작하며, 리틀엔디안을 사용합니다.\n2kB의 온보드 램을 포함하고 있으며, 카트리지를 통해서 최대 8k ~ 1MB 까지 확장된 램을 사용할 수 있으며 대부분 128 ~ 384kB를 사용합니다.\n그래픽 처리를 위해서 별도의 PPU(Picture Processing Unit)을 사용하며 2kB의 비디오램, 256B의 OAM(Object Attribute Memory)로 최대 64개 스프라이트 위치, 색상 및 타일 인덱스를 저장하고, 28 바이트의 온-다이 팔레트 램은 배경과 스프라이트 색을 선택할 수 있습니다.\n콘솔의 2kB 온보드 램은 타일 맵 및 속성에 사용될 수 있으며, 8kB의 타일 패턴 롬 또는 램이 카트리지에 포함 될 수 있습니다.\n시스템은 48개 색상과 6개의 그레이 팔레트를 사용할 수 있으며, 중간 프레임에 새 값을 쓰지 않고 최대 25개 색을 사용할 수 있습니다.\n(배경색, 3가지 타일 색상 4세트, 3가지 스프라이트 색상 4세트)\nNES의 팔레트는 RGB가아닌 NTSC를 기반으로 하며, 화면 중간에 스프라이트를 다시 로드하지 않고 총 64 개의 스프라이트를 화면에 표시 할 수 있다. NES의 표준 디스플레이 해상도는 256 x 240 픽셀로 구성됩니다.\nCPU 메모리 맵 2A03은 16비트 어드레스 버스를 가지고 있고, $0000-$ffff의 주소로 64kB 메모리를 지원할 수 있습니다.\nZero Page는 $0000-$00FF를 참조 합니다. 이 영역은 메모리의 첫 페이지이자 빠른 실행을 위한 명확한 어드레싱 모드에 의해 사용됩니다. 메모리 영역 중 $0000-$07FF는 $0800-$1FFF에서 세번 미러링 됩니다. 이것은 $0000 에 어떤 데이터를 기록하면 $0800, $1000, $1800 에도 기록이 된다는 의미이며 $2000-$401F에 위치한 I/O 레지스터도 8k 마다 $2008-$3FFF이나 남은 레지스터 영역에 미러링 됩니다. SRAM(WRAM)은 Save RAM이고, 카트리지 내 게임 저장을 위한 RAM에 엑세스 하기 위해 사용 됩니다.\n$8000 부터는 카트리지 PRG-ROM에 위치한 어드레스 입니다. PPG-ROM 16KB 뱅크가 오직 하나인 게임은 $8000과 $C000에 모두 로드 되고, 2개의 PPG-ROM 16KB 뱅크를 가진 게임은 하나는 $8000, 나머지는 $C000에 로드 됩니다. 2개 이상의 뱅크를 가진 게임은 어느 뱅크에 로드 할 것인 결정하기 위해서 메모리 매퍼를 이용합니다. 메모리 매퍼는 특정 주소(또는 범위)에 메모리가 기록되는 것을 감시하고 메모리가 기록 될 때 뱅크를 스위칭 합니다.\n// CPU 메모리 맵\r+--------------------+ $10000 +--------------------+ $10000\r| | | |\r| | | PPG-ROM |\r| | | Upper Bank |\r| | | |\r| PPG-ROM | +--------------------+ $C000\r| | | |\r| | | PPG-ROM |\r| | | Lower Bank |\r| | | |\r+--------------------+ $8000 +--------------------+ $8000\r| SRAM | | SRAM |\r+--------------------+ $6000 +--------------------+ $6000\r| Expansion ROM | | Expansion ROM |\r+--------------------+ $4020 +--------------------+ $4020\r| | | I/O Registers |\r| | +--------------------+ $4000\r| | | |\r| I/O Registers | | Mirrors |\r| | | $2000-$2007 |\r| | | |\r| | +--------------------+ $2008 | | | I/O Registers |\r+--------------------+ $2000 +--------------------+ $2000 | | | |\r| | | Mirrors |\r| | | $0000-$07FF |\r| | | |\r| RAM | +--------------------+ $0800\r| | | RAM |\r| | +--------------------+ $0200\r| | | Stack |\r| | +--------------------+ $0100\r| | | Zero Page |\r+--------------------+ $0000 +--------------------+ $0000\r 메모리 맵 테이블4 Address range Size Device $0000-$07FF $0800 2KB internal RAM $0800-$0FFF $0800 Mirrors of $0000-$07FF $1000-$17FF $0800 Mirrors of $0000-$07FF $1800-$1FFF $0800 Mirrors of $0000-$07FF $2000-$2007 $0008 NES PPU registers $2008-$3FFF $1FF8 Mirrors of $2000-2007 (repeats every 8 bytes) $4000-$4017 $0018 NES APU and I/O registers $4018-$401F $0008 APU and I/O functionality that is normally disabled. See CPU Test Mode. $4020-$FFFF $BFE0 Cartridge space: PRG ROM, PRG RAM, and mapper registers (See Note) 카트리지 공간 마지막 부분에 인터럽트 벡터 위치\n$FFFA-$FFFB = NMI vector\n$FFFC-$FFFD = Reset vector\n$FFFE-$FFFF = IRQ/BRK vector 레지스터 6502는 유사한 프로세서보다 적은 수의 레지스터를 가지고 있습니다. 세 가지 특수 목적 레지스터, 즉 프로그램 카운터, 스택 포인터 및 상태 레지스터가 있으며 각각 특정 용도를 가지고 있습니다. 또한 3개의 범용 레지스터, 누산기 및 인덱스 레지스터 X, Y가 있습니다. 데이터를 임시로 저장하거나 정보를 제어하는 데 사용할 수 있습니다.\n프로그램 카운터(PC) 프로그램 카운터는 다음에 실행할 명령어의 주소를 저장하는 16비트 레지스터입니다. 명령어가 실행되면 프로그램 카운터의 값이 업데이트되며 일반적으로 시퀀스의 다음 명령어로 이동합니다. 값은 분기 및 점프 명령, 프로시저 호출 및 인터럽트의 영향을 받을 수 있습니다.\n스택 포인터(SP) 스택은 $0100-$01FF 메모리 위치에 있습니다. 스택 포인터는 $0100에서 오프셋 역할을 하는 8비트 레지스터입니다. 스택은 하향식으로 작동하므로 바이트가 스택에 푸시되면 스택 포인터가 감소하고 스택에서 바이트를 가져오면 스택 포인터가 증가합니다. 스택 영역을 초과하는 경우 별도로 오버플로우가 감지되지 않고 스택 포인터가 $00에서 $FF로 순환 됩니다.\n누산기(A) 누산기는 산술 및 논리 연산의 결과를 저장하는 8비트 레지스터입니다. 누산기는 메모리에서 조회된 값으로 설정할 수도 있습니다.\n인덱스 레지스터 X(X) X 레지스터는 일반적으로 특정 주소 지정 모드에 대한 카운터 또는 오프셋으로 사용되는 8비트 레지스터입니다. X 레지스터는 메모리에서 조회된 값으로 설정할 수 있으며 스택 포인터의 값을 가져오거나 설정하는 데 사용할 수 있습니다.\n인덱스 레지스터 Y(Y) Y 레지스터는 X 레지스터와 같은 방식으로 카운터로 사용되거나 오프셋을 저장하는 데 사용되는 8비트 레지스터입니다. X 레지스터와 달리 Y 레지스터는 스택 포인터에 영향을 줄 수 없습니다.\n상태 레지스터 (P) 상태 레지스터는 연산이 실행 될 때마다 세트 또는 클리어 되는 비트 플래그들의 집합으로 구성되는 레지스터 입니다.\n Carry Flag (C) - 마지막 명령어가 비트 7에서 오버플로우(overflow) 또는 비트 0에서 언더플로우(underflow)가 발생한 경우 캐리 플래그가 세트됩니다. 예를 들어 255 + 1을 수행하면 결과는 0이 되고 캐리 비트는 세트가 됩니다. 이를 통해 시스템은 첫 번째 바이트에서 계산을 수행하고 캐리를 저장한 다음 두 번째 바이트에서 계산을 수행할 때 해당 캐리를 사용하여 8비트보다 긴 숫자에 대한 계산을 수행할 수 있습니다. 캐리 플래그는 SEC(Set Carry Flag) 명령으로 설정하고 CLC(Clear Carry) 명령으로 Clear할 수 있습니다.\n Zero Flag(Z) - 마지막 명령어의 결과가 0인 경우 제로 플래그가 세트됩니다. 예를 들어 128 - 127은 0 플래그를 세트 되지 않는 반면 128 - 128은 세트합니다.\n Interrup Disable (I) - 인터럽트 비활성화 플래그는 시스템이 IRQ에 응답하는 것을 방지하는 데 사용할 수 있습니다. 이것은 SEI(Set Interrupt Disable) 명령에 의해 설정되고 IRQ는 CLI(Clear Interrupt Disable) 명령이 실행될 때까지 무시됩니다.\n Decimal Mode (D) - 10진수 모드 플래그는 6502를 BCD 모드로 전환하는 데 사용됩니다. 이 플래그는 SED(Set Decimal Flag) 명령으로 설정하고 CLD(Clear Decimal Flag)에 의해 클리어 할 수 있습니다. (NES용 6502에서는 사용되지 않습니다)\n Break Command (B) - BRK(Break) 명령이 실행되어 IRQ가 발생했음을 나타내는 데 사용됩니다.\n Overflow Flag (V) - 이전 명령어에서 잘못된 2의 보수 결과를 얻은 경우 오버플로우 플래그가 세트 됩니다. 이것은 양수가 예상되었을 때 음수를 얻었거나 그 반대의 경우를 의미합니다. 예를 들어, 두 개의 양수를 더하면 결과 또한 양수가 되어야 합니다. 그러나 64 + 64는 sign bit로 인해 -128 결과 제공합니다. 따라서 이 경우 오버플로우 플래그가 세트됩니다. 오버플로우 플래그는 비트 6과 7 사이와 비트 7과 캐리 플래그 사이에서 캐리의 배타적 논리합을 취하여 결정됩니다. 자세한 사항은 문서의 Appedix A를 참고하시기 바랍니다.\n Negative Flag (N) - 바이트의 비트 7은 해당 바이트의 부호를 나타내며 0은 양수이고 1은 음수입니다. 이 부호 비트가 1이면 음수 플래그(부호 플래그라고도 함)가 세트됩니다.\n // 상태 레지스터\r7 6 5 4 3 2 1 0\r+-------------------------------+\r| N | V | | B | D | I | Z | C |\r+-------------------------------+\r// 5비트는 unused\r인터럽트 인터럽트는 코드의 순차 실행을 중지하고 인터럽트에 집중하도록 합니다. 인터럽트는 일반적으로 특정 상황에 의해 하드웨어에 의해 발생하지만, 소프트웨어에 의해 트리거될 수 있습니다. NES에는 세 가지 유형의 인터럽트 NMI, IRQ, reset이 있습니다. 인터럽트가 발생할 때 점프할 주소는 $FFFA-$FFFF의 벡터 테이블에 저장됩니다. 인터럽트가 발생하면 시스템은 다음 작업을 수행합니다.\n 인터럽트 요청이 발생 인식 현재 명령의 실행을 완료 프로그램 카운터와 상태 레지스터를 스택에 푸시합니다. 더 이상의 인터럽트를 방지하기 위해 인터럽트 비활성화 플래그 설정 벡터 테이블에서 프로그램 카운터로 인터럽트 처리 루틴의 주소를 로드 인터럽트 처리 루틴을 실행 RTI(Return From Interrupt) 명령을 실행한 후 스택에서 프로그램 카운터와 상태 레지스터 값을 가져옴 프로그램 실행 재개 IRQ 또는 maskable 인터럽트는 특정 메모리 매퍼에 의해 생성됩니다. 인터럽트 비활성화 플래그가 설정된 경우 프로세서에서 무시됩니다. IRQ는 BRK(중단) 명령을 사용하여 소프트웨어에서 트리거할 수 있습니다. IRQ가 발생하면 시스템은 $FFFE 및 $FFFF에 있는 주소로 점프합니다.\nNMI(Non-Maskable Interrupt)는 각 프레임의 끝에서 V-Blank가 발생할 때 PPU에서 생성하는 인터럽트 유형입니다. NMI는 상태 레지스터의 인터럽트 비활성화 비트의 영향을 받지 않으므로 발생 시 항상 실행이 중단됩니다]. 그러나 PPU 제어 레지스터 1($2000)의 비트 7이 지워지면 NMI 트리거를 방지할 수 있습니다. NMI가 발생하면 시스템은 $FFFA 및 $FFFB에 있는 주소로 점프합니다.\nReset 인터럽트는 시스템이 처음 시작될 때와 사용자가 리셋 버튼을 누를 때 트리거됩니다. 재설정이 발생하면 시스템은 $FFFC 및 $FFFD에 있는 주소로 점프합니다.\n시스템은 재설정 요청에 가장 높은 우선 순위를 부여하고 NMI와 마지막으로 IRQ가 뒤따릅니다.\nNES는 7 사이클의 입터럽트 레이턴시를 가지는데, 이것은 인터럽트 처리를 실행하기 위해서는 7 CPU 사이클이 필요하다는 것을 의미합니다.\n// NMI(Non-Maskable Interrupt) 처리\rMemory\r+---------+\r+---------+ -----\u0026gt; | P | $0100 + SP - 2\r| P | +---------+\r+---------+ | PC | $0100 + SP - 1\r| A | +---------+\r+---------+ /| PC | $0100 + SP\r| X | / +---------+\r+---------+ / | |\r| Y | / +---------+\r+---------+ / | |\r| SP | / +---------+\r+---------+ / | |\r| PC |/ +---------+\r+---------+ | |\r+---------+ --+\r+--\u0026gt; | | bbaa |\r| +---------+ |\r| | | |--- Interrupt Handler\r| +---------+ |\rInterrupt | | | |\rHandler | +---------+ --+\rAddress | | |\r| +---------+\r| | |\r| +---------+\r| +-| aa | $FFFA\r+--| +---------+\r+-| bb | $FFFB\r+---------+\r| |\r+---------+\r어드레싱 모드 6502에는 메모리에 액세스하기 위한 몇 가지 다른 주소 지정 모드가 있습니다. 또한 메모리가 아닌 레지스터 컨텐츠 상에서 작동하는 에드레싱 모드도 있습니다. 6502에는 총 13개의 다른 주소 지정 모드가 있으며, 일부 명령어는 둘 이상의 다른 주소 지정 모드를 사용할 수 있습니다. 주소 지정 모드에 대한 자세한 내용은 문서의 Appendix E에서 확인 가능합니다.\n명령어 집합 6502는 56개의 명령어가 있지만 일부는 다른 주소 지정 모드를 사용하여 여러 변형이 있어 가능한 256개 중 총 151개의 유효한 opcode를 생성합니다. 명령은 주소 지정 모드에 따라 1바이트, 2바이트 또는 3바이트입니다. 첫 번째 바이트는 opcode이고 나머지 바이트는 피연산자입니다.\n 로드/저장 작업 - 메모리에서 레지스터를 로드하거나 레지스터의 내용을 메모리에 저장합니다.\n 레지스터 전송 작업 - X 또는 Y 레지스터의 내용을 누산기에 복사하거나 누산기의 내용을 X 또는 Y 레지스터로 복사합니다.\n 스택 작업 - 스택을 푸시 또는 풀하거나 X 레지스터를 사용하여 스택 포인터를 조작합니다.\n 논리 연산 - 누산기 및 메모리에 저장된 값에 대한 논리 연산을 수행합니다.\n 산술 연산 - 레지스터와 메모리에 대한 산술 연산을 수행합니다.\n 증가/감소 - X 또는 Y 레지스터 또는 메모리에 저장된 값을 증가 또는 감소시킵니다.\n Shifts - 누산기 또는 메모리 위치의 비트를 왼쪽이나 오른쪽으로 1비트 이동합니다.\n 점프/호출 - 지정된 주소에서 다시 시작하여 순차적 실행 시퀀스를 중단합니다.\n 분기 - 조건이 충족되면 지정된 주소에서 다시 시작하여 순차적 실행 시퀀스를 중단합니다. 조건에는 상태 레지스터의 특정 비트 검사가 포함됩니다.\n 상태 레지스터 작업 - 상태 레지스터에서 플래그를 설정하거나 지웁니다.\n 시스템 기능 - 거의 사용하지 않는 기능을 수행합니다.\n http://wiki.nesdev.com/w/index.php/Nesdev_Wiki\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://nesdev.com/NESDoc.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://www.nesdev.com/2A03%20technical%20reference.txt\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://wiki.nesdev.com/w/index.php/CPU_memory_map\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":15,"section":"posts","tags":["프로젝트","NES","Emulator"],"title":"Nintendo Entertainment System - CPU","uri":"/posts/projects/nesdev/2021-07-06-nesdoc-kr-01/"},{"content":"2007년 초쯤 마이크로프로세서(이하 MCU) 스터디를 하면서 주로 해외에서 하드웨어 관련 정보를 찾아보던 도중 우연히 어느 일본인이 개인 홈페이지에 플레이스테인션2(이하 PS2) 듀얼쇼크2 컨트롤러(이하 듀속 또는 DS2) 시그널을 분석해서 공유한 페이지를 발견하게 됩니다.\n관련 정보를 확인 한 이후 부터는 딱히 쓸데는 없었지만 DS2를 어떻게든 MCU에 연결해서 써먹어 보고 싶은 생각이 간절했습니다. 마침 사촌동생에게 PS2를 빌려서 사용하고 있었기 때문에 컨트롤러도 있겠다 연장선도 따로 구매한 것이 있어서 연장선을 중간에 단선시켜서 별도로 핀을 뽑아낸다면 MCU에 DS2를 연결이 수월할 수 있을거라고 판단이 되었습니다.\n그래서 MCU에서 DS2 컨트롤러의 값을 읽을 수 있도록 호스트 에뮬레이션을 하기로 결정하고 실행에 나섰습니다.\n그리고 결국 구현에 성공을 했는데요.\n벌써 14년전으로 오래된 일이라 작업했던 상황이 모두 기억이 나지 않지만 예전 기억과 추억을 정리하는 측면에서 분석 위주로 글을 남겨볼까 합니다.\n당시엔 관련 정보를 찾아보기 힘들었었는데, 현재에는 PS2, DS2 관련 내용이 많아서 몇가지 페이지나 사이트를 참고하여 수월하게 정리가 가능했습니다. 가급적 비 전공자분들도 보는데 불편함이 없도록 내용을 작성해보겠습니다.\nDS2 컨트롤러 특징 DS2 컨트롤러는 디지털 키와 아날로그 조이스틱의 구분, 그리고 범퍼와 트리거로 구성된 구조 등현재 유행하는 컨트롤러들의 가장 기본 틀이 되는 구조를 가지고 있습니다.\n전체의 키 맵은 디지털 상, 하, 좌, 우, select, start, 세모, 네모, 동그라미, 엑스, 2개의 아날로그 스틱(LX, LY, RX, RY)과 각 스틱 누름(L3, R3), 각각 2개의 범퍼(L1, R1)와 트리거(L2, R2)로 구성된 컨트롤러이고 피드백 진동 모터를 2개 탑재하고 있습니다.\nDS2는 별도로 하나의 특수 버튼을 포함하는데요 중간에 디지털/아날로그 모드 선택이 가능한 버튼이 포함되어있습니다. 디지털은 말그대로 디지털 입력만 전용으로 받는 모드이고, 아날로그 모드는 좌우 아날로그 조이스틱 값을 추가로 사용하는 모드입니다. 지금이야 아날로그 버튼이 일반적이지만 당시에는 아날로그 버튼을 지원하지 않는 게임도 많았기 때문에 구분을 한 것이 아닐까 예상이 됩니다. 그리고 컨트롤러에 명시적으로 표시는 안되어있지만 컨트롤러 내부적으로 아날로그 모드에서 추가적으로 듀얼쇼크2 네이티브 모드(Dualshock 2 Native Mode)라는 것이 있습니다.\n네이티브 모드의 경우 특수하게 디지털 버튼의 누르는 압력 감지가 가능한 모드인데요. 현재 일반적인 게임 컨트롤러의 경우 트리거 버튼이나 좌우 아날로그 조시스틱만 아날로그 값으로써 입력이 가능한데, 네이티브 모드의 경우 일반 버튼도 누르는 세기에 따라 압력 감도 구분이 가능합니다. 특정 PS2 게임에서 \u0026lsquo;버튼을 세게 누르세요\u0026rsquo;라는 인 게임 메시지를 표시하는 게임들이 있는데, 일반적인 컨트롤러 구조만 생각했을 때에는 선뜻 이해가 되지 않았으나 실제로 세게 누름이 인식된다는 것이 신기했었습니다.\n이러한 특징이 가능한 것은 대부분 일반적인 컨트롤러에서 디지털 버튼의 눌림은 두 전극이 통전될 시 전원 논리 값 변화(on, off)만 감지하는데 반해, DS2의 경우에는 누르는 압력에 의한 전도성의 차이1 감지가 가능하기 때문입니다.\nDualshock2 Port 구조 아래 그림?은 DS2 커넥터를 바라본 시점에서 포트 이미지를 텍스트로 구성2한 것 입니다.\n 1 2 3 4 5 6 7 8 9\r-------------------------------\r| o o o | o o o | o o o | (at the Controller)\r\\_____________________________/\r Pin Name Direction Description 1 DATA IN Data 2 CMD OUT Command 3 +7V OUT 7.6VDC 4 GND Ground 5 VCC OUT Vcc (3-5 VDC) 6 /ATT OUT ATT select 7 CLK OUT Clock 8 N/C Not connected\\ 9 /ACK IN Acknowledge DS2은 총 9개의 핀으로 구성된 포트를 가지고 있습니다. 표에서 Direction 기준은 PS2 본체 기준이며, 전압 또는 데이터 흐름(논리 변화 값) 방향이라고 보시면 됩니다.\n9핀중 3, 4, 5번은 전원과 관련된 핀이고 1, 2, 6, 7, 9번 핀은 통신과 관련된 핀입니다.\n먼저 통신 핀 구성을 살펴보면 전자제품 개발쪽 일을 하시거나 회로쪽 관심 있으신 분들은 알겠지만 임베디드 환경에서 호스트 프로세서와 장치(Peripheral)간 통신을 할 때 사용되는 방식인 SPI(Serial Peripheral Interconnect)3와 유사하다는 것을 확인하실 수 있습니다. 다만 예외적으로 9번 ACK핀이 존재하기 때문에 일반적인 SPI와도 다르다는 것도 확인할 수 있습니다. ACK는 데이터 전송(또는 교환)이 정상적으로 되었음을 알리거나 다음 커맨드 또는 데이터를 받을 준비가 되었음을 알리기 위해서 보내는 신호입니다. 실제로 호스트 에뮬레이션 구현 시 정해진 시퀀스와 타이밍대로 통신을 하면 ACK 라인이 없어도 컨트롤러와 데이터를 주고 받는데는 문제가 없습니다. ACK를 N/C 처리하면 사실상 SPI 동일합니다.\n다음으로 전원 관련 핀을 살펴보면 4, 5번 핀으로 컨트롤러에 메인 전원을 공급합니다. 3번 핀을 보면 예외적으로 높은 전압을 추가로 인가 하도록 되어있습니다. 3번 핀은 DS2 내부의 모터 구동을 위한 추가 전원입니다. 권장 전압은 7 ~ 9V 이지만 실제로는 5v나 더 낮은 전압으로도 동작은 가능했습니다. 대신 진동 모터 구동을 인한 전원이므로 메인 전원과 분리가 안되어있거나 또는 호스트측에서 전류를 충분히 제공하지 못하면 전압 강하가 발생할 수 있고, 이는 오동작 내지 디바이스가 리셋 될 수 있기 때문에 진동 기능을 사용하고자 전원을 제공할 때에는 주의해야합니다. 진동 기능을 사용하지 않는다면 불필요한 라인이 되므로 과감히 N/C 처리를 해도 컨트롤러의 입력값을 읽어오는 동작에는 문제가 없습니다.\n참고로 6, 9번 핀을 보면 핀 이름에 /(슬래시)가 붙은 것을 볼 수 있습니다. 비 전공자들을 위해서 간략하게 설명드리자면 Active Low라는 의미를 나타내기 위한 표시입니다. 전자 회로에서 통신의 신호 논리 값을 high, low 또는 1, 0 등으로 표시를 많이 하는데, 논리 값이 low 상태가 될 때 특별한 동작이나 이벤트 발생한다라는 것을 명시적으로 나타내기 위해 사용되는 표시입니다.\n이 경우에는 6번 핀의 논리 신호가 low가 되면 PS2가 \u0026lsquo;너랑 통신을 할거야\u0026rsquo; 라고 DS2에 알려주는 용도이고, 9번핀은 논리 신호가 low가 되면 DS2가 PS2에게 \u0026lsquo;데이터 잘 받았어, 다음 데이터를 받을 수 있어\u0026rsquo;라고 알려주는 용도로 사용됩니다.\n조금 더 자세한 특성 정보는 Interfacing a PS2 (PlayStation 2) Controller4 페이지에서 확인 하실 수 있습니다.\n통신 라인 및 시그널 분석 Overview\r____ _____\rSEL- |____________________________________________________________| ______ ____ ____ ____ ____ _________\rCLK |||||||| |||||||| |||||||| |||||||| |||||||| _______________________________________________________________________\rCMD X 01h XXXX 42h XXXX 00h XXXX 00h XXXX 00h XXXX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ _____________________________________________________________ DAT -----XXXXXXXXXXXXX ID XXXX 5Ah XXXX key1 XXXX key2 XXXX-----\r~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ACK- ---------------|_|---------|_|---------|_|---------|_|-----------------\rTop command. First comminucation(device check)\r____ SEL- |__________________________________________________________________\r______ _ _ _ _ _ _ _ __________________ _ _ _ _\rCLK |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| __________ ___ CMD |________________________________________________| |_______\r____ DAT -----XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX |___________\rACK- ----------------------------------------------|___|--------------------\rX = none, - = Hi-Z\r통신 시퀀스 다이어그램도 PS2 관련 텍스트5를 참고하여 두루넷, 메가패스 시절 감성으로 구성된 다이어 그램입니다. 참고로 해당 다이어그램에 표시된 것은 디지털 모드 기준이며 분석도 디지털 기준으로 설명합니다.\n라인 구성 먼저 시그널 라인 구성을 보면 앞서 언급했듯이 ACK만 제외하면 SPI와 동일합니다. 레퍼런스 페이지마다 포트 구성에 적인 핀 이름과 다이어그램에 사용된 라인 이름의 차이는 있지만 SEL(Select)은 용어 그대로 특정 디바이스(여기서는 DS2)를 선택하기 위한 라인입니다. (임베디드 환경에서는 CS: Chip Select 라고도 자주 사용함) CLK(Clock)는 통신을 하기 위해 제공되는 라인으로써 데이터 라인들은 클럭과 동기화 되어 논리 신호값을 변경하고 읽게 됩니다. 당연한 이야기지만 클럭의 Frequecy가 높을 수록 논리 값을 빠르게 변경 할 수 있고 이는 곧 통신 속도도 빨라짐을 의미합니다. 통상 250kHz ~ 500kHz의 클럭 속도를 가진다고 합니다. CMD, DAT는 실질적인 데이터를 주고 받는 라인으로서 데이터 전송방향에 따라 CMD는 formal한 표현으로는 MOSI(Master Out/Slave In) 라인에 해당하고, DAT는 MISO (Master In/Slave Out) 라인에 해당합니다.\n시그널 분석 상단 다이어그램을 보면 SEL 라인이 low로 떨어질 때 통신이 시작되고, high가 되면 통신 사이클이 종료되는 것을 볼 수 있습니다. 일반적으로 Master/Slave 관계의 통신 방식에서는 통신을 하고자 하는 디바이스 SEL 신호를 low로 낮추는 것을 시작으로 통신을 시작하게 됩니다. 다이어그램을 보면 SEL 신호가 high가 되기 전까지 PS2는 총 5바이트 CMD를 전송하고, 동시에 4바이트 DAT를 수신하는 것을 볼 수 있습니다. 그리고 그 사이사이 1바이트 통신이 완료될 때 마다 DS2는 ACK라인을 low ~ high로 일정시간 변화를 주면서 1바이트 송/수신이 완료되었음을 PS2에게 전달하고 있고, 마지막 ACK는 보내지 않아서 총 4번의 ACK만 보내는 것을 확인할 수 있습니다.\n일반적으로 디바이스나 내부 Chip 간 통신에서는 커멘드 전송 - 데이터 수신 - 커맨드 전송 - 데이터 수신(CMD - DAT - CMD - DAT \u0026hellip;)과 같이 핑퐁(Ping Pong) 스타일로 주고받는 반이중 통신(half duplex)을 하는 경우가 많은 편입니다. PS2와 DS2는 일반적인 핑퐁 스타일과 달리 시작 커멘드 전송 - 다음 커멘드 전송/데이터 수신 - 다음 커멘드 전송/데이터 수신(CMD - CMD/DAT - CMD/DAT \u0026hellip;)과 같이 커맨 전송과 데이터 수신을 동시에 하는 전이중 통신(full duplex) 방식을 취하고 있습니다.\n버튼 입력 정보를 얻기 위해서는 컨트롤러 부터 적어도 4바이트 데이터를 수신하고 있으므로, 만약 반이중 통신 방식이었다면 커맨드 포함 전체 통신 길이가 8 bytes 정도 되겠지만, 전이중 방식을 채용하여 처음과 마지막을 제외하고는 커맨드와 데이터를 동시 주고 받고 있기 때문에 시간 기준으로 5 bytes 정보만 교환하고 있으므로 반이중 방식 대비 상대적으로 효율적인 통신을 하고 있는 것을 볼 수 있습니다. 물론 중간에 ACK 신호를 별도로 보내고 확인하는 시간이 있기 때문에 효율이 월등히 좋다고는 할 수 없지만 적어도 반이중 통신보다는 적은시간에 1 사이클을 완료가 가능한 것을 볼 수 있습니다.\n추가로 ACK 관련하여 만약 반이중 통신 방식이었을 경우에는 핑퐁 스타일로 데이터를 주고 받기 때문에 DAT 데이터를 수신받는 것 자체가 ACK 역할을 대신할 수 있어서 별도의 ACK 라인데 대한 필요성이 상대적으로 낮은데요, 전이중 통신 방식에서는 커맨드 전송과 데이터 수신을 동시에 하기 때문에 Slave(DS2) 측에서 커맨드를 잘 받았는지 또는 다음 커맨드를 보내도 괜찮은지에 대한 타이밍 체크를 좀 더 명확하기 위해서 별도 ACK 핀을 추가한게 아닐까 하는 예상을 해봅니다.\n그리고 다이어그램 상으로는 확인할 수는 없는 내용이지만 PS2와 DS2 통신 시 비트 전송 순서는 일반적으로 많이 사용되는 MSB(Most Significant Bit)와 달리 LSB(Least Significant Bit) 방식으로 통신을 합니다. 독자규격을 사랑하는 소니 아니랄까봐 SPI에 ACK 라인을 넣은 것도 모자라 LSB를 채용하는 행보를 보여줍니다. 참고로 비전공자분들을 위한 부연설명을 드리자며 Byte 기준 첫번째 비트 부터 전송하면 LSB, 마지막 비트부터 낮은 비트 순서대로 전송하면 MSB 입니다. 예를 들어 16진수 0x7F는 2진수로 01111111 로 표시할 수 있는데요, 여기서 젤 낮은 비트는 가장 오른쪽에 있는 값 입니다. LSB 방식으로 전송하면 젤 오른쪽부터 1-1-1-1-1-1-1-0 순서대로 전송을 하고, MSB라면 젤 왼쪽 비트부터 0-1-1-1-1-1-1-1-1 순서대로 전송을 하게 됩니다.\n통신 속도 앞 부분에서도 언급했지만 클럭은 250 ~ 500kHz로 PS1은 250kHz, PS2 500kHz 속도의 클럭으로 동작한다고 합니다. 1 클럭당 1bit 정보 교환(논리값 변화)이 가능하므로 500KHz는 500Kbps(Bit Per Second)와 동일하며 Byte 기준으로 하면 전송속도(baud rate)는 500k / 8 = 62.5k Bps(Byte Per Second)가 되겠습니다. 디지털 모드의 경우 송/수신 합쳐서 5bytes 마다 1 cycle이 완료되므로 62.5k / 5 = 12.5k Cycle Per Second가 되고 1개의 DS2 컨트롤러 기준 초당 12,500 번 컨트롤러 입력 값을 읽어 올 수 있습니다. 아날로그 모드는 62.5 / 9 = 6.94k, 네이티브 모드는 62.5k / 21 = 2.98k 정도로 컨트롤러 입력값을 읽을 수 있게됩니다. 이는 그냥 단순 계산에 의한 예측치이므로 참고 정도만 하시길 바랍니다.\n커맨드 및 데이터 시퀀스4 이번에는 PS2와 DS2 간 통신 시 주고 받는 커맨드와 데이터를 한번 살펴보겠습니다. 내용상 DS2의 버튼 입력값을 읽어오는 부분만 설명하고, 별도로 컨트롤러의 설정이나 상태를 확인하는 커맨드는 생략하고자 합니다. 자세한 내용은 참고 페이지를 확인하시기 바랍니다.\n디지털 모드 #byte 1 2 3 4 5 Command 0x01 0x42 0x00 0x00 0x00 Data 0xFF 0x41 0x5A 0xFF 0xFF Section Header Digital 디지털 모드에서 커맨드/데이터 교환 시퀀스 입니다. 순서상 1 ~ 3 바이트까지를 일종의 통신 헤더로 볼 수 있고, 4번째부터는 컨트롤러의 입력 데이터가 반환되는 것을 볼 수 있는습니다.\n먼저 PS2에서 보내는 Command 기준에서 첫 번째 바이트 값인 0x01은 통신을 시작할 때 보내는 데이터로 항상 일정합니다. 두 번째 바이트는 설정에 따라 달라질 수 있지만, 컨트롤러를 입력 값을 읽어올 때에는 0x42를 사용합니다. 3번째는 의미없이 항상 0x00 값으로 유지하고, 4, 5번째에 값은 설정에 따라 달라질 수 있지만 컨트롤러의 피드백 모터 구동 여부와 세기를 결정 할 수 있습니다.\nDS2에서 전송하는 Data 기준에서 두 번째 바이트 0x41 값은 컨트롤러의 mode를 나타내는 값입니다. 모드에 따라 상위 니블(바이트 기준으로 상위 4바이트, 예를 들어 0x7F의 상위니블은 0x7) 값이 달라지는데요. 디지털 모드는 (0x4), 아날로그는 (0x7), 설정은 (0xF)로 구분이 됩니다. 하위 니블은 헤더 이후 전송될 (DS2 기준) 바이트 수를 나타내는데, 단위가 16bit(즉 2바이트) 입니다. 그러므로 위의 0x41 값을 해석해보면 현재 컨트롤러는 디지털 모드에 2바이트 버튼의 입력 데이터를 전송할 것을 의미합니다. 만약 아날로그 모드라면 디지털 입력 값 외에 좌우 아날로그 조이스틱 축 변화량 4바이트(Left X-Axis, Left Y-Axis, Right X-Axis, Right Y-Axis)가 추가로 포함되서 0x73이 될 것이고, 네이티브 모드라면 디지털 입력, 아날로그 축 데이터 외에 부가적으로 디지털 버튼의 압력 값 12바이트를 포함하여 0x79 값이 됩니다.\n디지털 모드에 4, 5번 바이트는 실제 컨트롤러의 버튼 눌림 여부를 나타내는 값 입니다. 각각 비트 별로 다음과 같이 버튼이 매핑되어 있습니다.\n #bit of 4th byte 8 7 6 5 4 3 2 1 Button Map Left Down Right Up Start R3 L3 Select #bit of 5th byte 8 7 6 5 4 3 2 1 Button Map □ X ○ △ R1 L1 R2 L2 아날로그 모드 아날로그 모드는 기본적으로 5번째까지는 디지털 모드와 동일하고 2개의 아날로그 조이스틱 X, Y축 데이터를 추가로 반환합니다.\n #byte 6 7 8 9 Analog Map RX RY LX LY 아날로그 스틱 Axis별 1바이트 값을 사용하고 있으며 중립 값은 0x80 입니다. 그러므로 좌, 우, 상, 하 각각의 약 127단계로 구분이 됩니다. 예를 들어 RX 기준 값이 0이면 오른쪽 아날로그 스틱 X 축의 가장 작은 값이므로 오른쪽 아날로그 스틱을 가장 왼쪽 기울였을 때의 값이고, 255면 오른쪽 아날로그 스틱를 가장 오른쪽으로 기울였을 때의 값이 됩니다.\n네이티브 모드 네이티브 모드는 아날로그 모드에서 추가로 12바이트의 디지털 버튼 압력값을 추가로 반환합니다.\n #byte 10 11 12 13 14 15 16 17 18 19 20 21 Analog Map Right Left Up Down △ ○ X □ L1 R1 L2 R2 각 버튼당 1 바이트이므로 255단계의 압력 인식이 가능합니다.\n호스트 구현 실제 구현에 대한 상세 설명이나 코드를 첨부하는 것은 내용자체가 너무 길어질 것 같아(사실 너무 오래전 일이라 작업기 자체가 가물가물 합니다.) 어떻게 작업을 했는지에 대해 간략히 남기는 것으로 대신하겠습니다.\n당시 처음으로 호스트 에뮬레이션을 위해서 사용한 마이크로프로세서는 Microchip의 PIC16F877A(이하 877A)6 라는 8bit 프로세서 입니다. 간단한 스펙을 나열하면 다음과 같습니다.\n ROM: 14KB RAM: 368B SPEED: 5 MIPS/DMIPS DATA EEPROM: 256B Digital Communication Peripherals: 1-UART, 1-SPI, 1-I2C1-MSSP(SPI/I2C) Capture/Compare/PWM Peripherals: 2 Input Capture, 2 CCP Timers: 2 x 8-bit, 1 x 16-bit ADC Input: 8 ch, 10-bit Number of Comparators: 2 877A는 Microchip 사의 8bit 계열 MCU에서는 Mid range 급에 해당하는 프로세서입니다. 현재 인기있는 ARM Cortex-M (32bit) 시리즈에 비하면 매우 평범한 스펙이지만, 당시에는 나름 좋은 스펙으로 출시된 미드레인지 제품이었습니다. 실제 많은 상용 전자제품 또는 산업용 컨트롤러 등이 단가 등 기타 이유로 매우 성능이 제한적이고 낮은 성능의 8비트 프로세서를 많이 씁니다.(심지어 4bit 쓰는 곳도 있다고.. 877A는 그에 비하면 선녀급) 당시에는 하드웨어 스터디용으로 구입해서 사용하고 있었기 때문에 구태여 다른 프로세서를 사용하지 않고 가지고 있는 것을 활용하기로 했습니다.\n다만 877A를 사용하면서 몇가지 문제점이 있었는데요. 877A의 경우 SPI 관련 레지스터 설정이 하이엔드 급 MCU 보다는 제한적이어서 MSB, LSB 선택이 불가하고 오로지 MSB 방식 통신만 가능했습니다. 그리고 통신 클럭 속도 제어도 분주방식으로 MCU 메인 클럭에서 4, 8, 16, 32 등으로 나눈 속도로 통신이 가능했기 때문에 원하는 속도를 정확하게 설정하기 힘든 구조였습니다.(클럭의 경우 최대 속도만 초과하지만 않아도 됩니다만 초반에는 가급적 스펙과 유사하게 구현하여 혹시 모를 실패에 대한 가능성을 줄이고자 했습니다) 결국 877A 내부에 있는 SPI peripheral 자체를 활용하기에는 어려웠습니다.\n그래서 일반 입출력 포트를 SPI 통신처럼 동작하도록 Software Driven 방식으로 같이 에뮬레이션 하여 호스트 구현을 진행했습니다.\n사실 오래전 일이라 어떻게 작업했는지 디테일하게 생각은 나지 않지만, 며칠을 고생했던 것으로 기억이 납니다. 구현자체의 난이도 보다는 정확한 스펙 시트가 없는 상태에서 진행하는 작업이다보니 원하는대로 동작이 안될 때 어디서 문제가 있는 것인지 검증하는 것이 어려웠기 때문입니다.\n예를 들면, 직접 구현한 SPI 에뮬레이션 자체가 문제가 있는 것인지? 호스트 구현에서 내가 빠뜨린 것은 없는지?\n컨트롤러와 프로세서간 하드웨어 연결에는 문제가 없는지? 타이밍에 문제가 있는 것인지?\n의심할만한 구간은 많고 실제 검증은 어렵다보니 어디서부터 어떻게 확인을 하고 무엇을 고쳐봐야하는지 감을 잡기가 매우 어려웠습니다.\n결국 일일히 펙터 등을 수정 등을 반복하며, 다행히 삽질이 지쳐갈 때쯤 가까스로 구현에 성공하였는데요 신호를 read/write 하는 타이밍쪽 문제였던 것으로 기억합니다. 어쨌든 방학기간에 홀로 동아리방에 처박혀서 며칠을 고생하여 컨트롤러 입력값을 읽어오는 데 성공했을 때의 쾌감이란 말로 할 수 없을 정도로 기뻤었습니다.\n막상 구현해놓으니 당장 어디 써먹을 데는 없었지만 성취감만으로 매우 만족할 수 있었습니다.\n동작 화면 하도 오래전 일이라 동작화면을 남겨둔게 많지 않은데, 피쳐폰으로 찍어놓은 동작 영상이 남아있습니다.\n졸업작품으로 막 출시된 Microhip 사의 16비트 프로세서를 활용해서 MP3, BMP 이미지, TXT 리딩이 가능한 멀티 플레이어를 만든적이 있는데, 거기에 부가적으로 테트리스 게임을 구현해서 넣었었습니다. LCD 터치로도 동작이 되지만 PS2 패드로도 조작이 가능하도록 했습니다.\n\n나중에는 기회가 되면 TETRIS나 멀티 플레이어 구현 및 작업기도 남겨볼까 싶은데요. 확실하지 않은데 메모리 아끼겠다고 맵을 bit 단위로 만들어서 적은 코드로 동작하게 만들고자 했던 기억만 조금 남아있습니다.\n마치면서 이전에 어떻게 작업했었는지 정리차원에서 남긴 글인데 다시 보니 즐거운 추억입니다. 한편으론 학생때는 시간도 많고 열정도 많아서 이런저런 개인 스터디와 프로젝트 진행을 과감히 할 수 있었는데, 지금은 삶이 바쁘다는 이유로 지난 추억으로만 회상하고 돌아봐야하는 현실이 조금 아쉽습니다. 기회가 되면 시간을 내서라도 개인 스터디와 프로젝트를 진행하고 싶네요. 읽으시는 분들께도 잠시 즐거운 시간이 되었길 바라면서 마칩니다.\n https://electronics.howstuffworks.com/ps23.htm\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n http://www.hardwarebook.info/Sony_Playstation_Controller_Port\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.ti.com/lit/ug/sprugp2a/sprugp2a.pdf?ts=1611639876953\u0026amp;ref_url=https%253A%252F%252Fwww.google.com%252F\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://store.curiousinventor.com/guides/PS2\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.raphnet.net/electronique/psx_adaptor/Playstation.txt\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://www.microchip.com/wwwproducts/en/PIC16F877A\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":16,"section":"posts","tags":["마이크로컨트롤러"],"title":"PS2 듀얼쇼크 컨트롤러 호스트(리더) 구현 및 분석","uri":"/posts/dev/micro/2021-01-27-dualshock2-reader-implementation/"},{"content":"Vue 3 - 간단한 툴팁 기능 구현\nVuetify를 사용하다 보면 복잡한 레이아웃으로 구성된 화면이나 content가 많은 즉, 스크롤해야 하는 화면에서 v-menu, v-tooltip 같이 별도 hidden content가 이벤트(hover)에 따라 show/hide 되는 컴포넌트의 경우 제대로 표시되지 않는 문제가 있습니다.\n이럴때 \u0026lsquo;attach\u0026rsquo; property로 일부 해결이 되는 경우도 있지만 그렇지 않은 경우도 있어서 대안으로 다른 라이브러리를 쓰거나 필요 시 직접 구현을 하기도 하는데 심플하게 동작하는 tooltip을 한번 만들어 보겠습니다.\n먼저 vue component에서 template 영역입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;tooltip-container\u0026#34;\u0026gt; \u0026lt;div ref=\u0026#34;targetEl\u0026#34; @mouseover=\u0026#34;showContent(true)\u0026#34; @mouseout=\u0026#34;showContent(false)\u0026#34;\u0026gt; \u0026lt;slot\u0026gt;\u0026lt;/slot\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div ref=\u0026#34;contentEl\u0026#34; class=\u0026#34;tooltip-content\u0026#34; :style=\u0026#34;{ opacity: opacity, top: `${contentTop}px`, left: `${contentLeft}px`, \u0026#39;font-size\u0026#39;: `${fontSize}px`, }\u0026#34; \u0026gt; {{ tooltip }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \n마우스 이벤트를 발생시킬 타겟이 되는 slot영역을 감싸는 target element와 tooltip content가 표시되는 content element를 구분하고 이를 감싸는 container로 구성되어 있습니다. 여기서 target element 마우스 이벤트에 따라 tooltip의 visibility를 조절하는데, 이때 v-show (display is none), v-if (not mounted)로 제어하는 방법, 다른 방법으로는 style의 display나 opacity 값으로 제어를 할 수 있습니다. 여기서는 opacity를 사용하여 투명도를 조절해서 보이고 사라지는 방식을 택하겠습니다.\n이유는 tooltip 위치를 target 위치를 기준으로 결정해야 하는데 특히, top이나 left 위치에 표시하는 경우 traget element와 content element가 겹치지 않도록 하기 위해서 content element의 width나 height 만큼 offset 참조가 필요합니다. 만약 content element가 rendered 상태가 아닐때 해당 값을 읽을 경우에는 위치나 크기 값이 제대로 반환이 되지 않기 때문에, vue component가 mounted 된 시점에 content element도 rendered 된 상태가 될 수 있도록 display 상태가 변경되지 않도록 합니다.\n만약 opacity 대신 다른 방식을 사용하고자 할 때에는 vue component에서 updated 이벤트 시 content element의 rendered 상태를 확인 후 위치 값을 계산하는 방법을 쓸수도 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 // Vue 3, Composition API, Typescript \u0026lt;script lang=\u0026#34;ts\u0026#34;\u0026gt; import { defineComponent, onMounted, ref, watch } from \u0026#39;vue\u0026#39;; export default defineComponent({ name: \u0026#39;Tooltip\u0026#39;, props: { top: Boolean, left: Boolean, right: Boolean, bottom: { type: Boolean, default: true, }, tooltip: String, fontSize: { type: String, default: \u0026#39;12px\u0026#39;, }, margin: { type: Number, default: 8, }, }, setup(props) { const contentTop = ref(0); const contentLeft = ref(0); const opacity = ref(0); const contentisible = ref(false); const targetEl = ref(null); const contentEl = ref(null); const showContent = (visible: boolean) =\u0026gt; { opacity.value = visible ? 1 : 0; if (contentEl.value \u0026amp;\u0026amp; targetEl.value) { const { top, left, right, bottom } = (targetEl.value! as HTMLElement).getBoundingClientRect(); const { width, height } = (targetEl.value! as HTMLElement).getBoundingClientRect(); const contentRect = (contentEl.value! as HTMLElement).getBoundingClientRect(); if (props.top) { contentTop.value = top - (contentRect.height + props.margin); contentLeft.value = left; } else if (props.left) { contentTop.value = top; contentLeft.value = left - (contentRect.width + props.margin); } else if (props.right) { contentTop.value = top; contentLeft.value = right + props.margin; } else { contentTop.value = bottom + props.margin; contentLeft.value = left; } } }; return { contentTop, contentLeft, opacity, targetEl, contentEl, showContent, }; }, }); \u0026lt;/script\u0026gt; \nprops에 툴팁이 위치할 곳을 결정하는 top, left, right, bottom를 가집니다. 그 외 content의 font-size나 margin을 별도로 조절할 수 있도록 했습니다.\n기본적인 동작인 target에 mouseover 이벤트가 발생 시 target element의 위치와 content element의 크기 값을 가져와서 위치를 계산하고 지정합니다.\n위치 계산의 경우 rendered 된 시점에서 한번만 해도 됩니다. 다만 브라우져 내부 window 크기가 변경되는 경우 target의 위치나 content element 크기도 변경이 될 수 있으므로 갱신이 필요합니다. 이를 위해서 window resize 이벤트 시 갱신을 수행하면 되는데 window 크기가 조금만 변경되더라도 resize 이벤트가 수십 ~ 수백번 이벤트가 발생하기 때문에, 이벤트를 debouncing이나 throttling 되도록 하여 중복을 최대한 줄여주는 것이 좋습니다. 하지만 실제로 tootip을 적용한다고 할 시 tooltip를 표시하는 이벤트는 상대적으로 많지 않을 것이므로 중복여부 상관없이 매번 계산하는 것으로 하겠습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;style scoped\u0026gt; .tooltip-container { display: inline-block; } .tooltip-content { position: absolute; font-size: 12px; background: #fefefe; color: #555; box-shadow: 0 0 6px 1px rgba(0, 0, 0, 0.15); padding: 4px 8px; border-radius: 4px; pointer-events: none; transition: opacity 0.4s; } \u0026lt;/style\u0026gt; 스타일 지정은 특별한 것은 없고 slot에 맞추기 위해서 container의 display를 inline-block으로 지정했습니다. 다른 유형으로 선택하는 경우 표시되는 위치가 달라질 수 있기 때문에 이 경우 tooltip-content의 position을 fixed로 선택해야 할 수 있습니다. transition에 opacity를 지정하였기 때문에 fade in/out 효과가 적용되어 표시되게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;tooltip\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Tooltip\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt; \u0026lt;vTooltip tooltip=\u0026#34;left message\u0026#34; left\u0026gt; Left \u0026lt;/vTooltip\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;div\u0026gt; \u0026lt;vTooltip tooltip=\u0026#34;top message\u0026#34; top\u0026gt; \u0026lt;p\u0026gt;Top\u0026lt;/p\u0026gt; \u0026lt;/vTooltip\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;vTooltip tooltip=\u0026#34;right message\u0026#34; right\u0026gt; \u0026lt;p\u0026gt;Right\u0026lt;/p\u0026gt; \u0026lt;/vTooltip\u0026gt; \u0026lt;br /\u0026gt; \u0026lt;vTooltip tooltip=\u0026#34;bottom message\u0026#34; bottom\u0026gt; \u0026lt;p\u0026gt;Bottom\u0026lt;/p\u0026gt; \u0026lt;/vTooltip\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script lang=\u0026#34;ts\u0026#34;\u0026gt; import { defineComponent } from \u0026#39;vue\u0026#39;; import vTooltip from \u0026#39;@/components/Tooltip.vue\u0026#39;; // @ is an alias to /src export default defineComponent({ components: { vTooltip, }, }); \u0026lt;/script\u0026gt; 그럼 간단한 테스트 페이지를 작성해보겠습니다.\n타겟이 되는 태그 자체를 감싸거나 태그 내의 content만 타겟으로 하여 간단하게 tooltip 메시지를 표시할 수 있습니다.\n이름의 경우 vTooltip으로 지정했는데 최근 vue 3 업데이트 변경점이 있었는지 tooltip(대소문자 구분없이)의 이름을 그대로 쓰는 경우 알 수 없는 이유로 에러가 발생하기 때문에 (이전에 발생하지 않던 에러라 추가 확인이 필요) 부득이하게 접두어를 붙였습니다.\n아래 gif 이미지를 보면 동작하는 툴팁 예제를 확인 할 수 있습니다.\n","description":"","id":17,"section":"posts","tags":["vue","vue-next","vue3","typescript"],"title":"Vue 3 - Implement simple tooltip","uri":"/posts/dev/vue/2020-12-22-vue-3-simple-tool-tip/"},{"content":"기존 v2에서는 prototype에 변수나 모듈을 정의하면 this 키워드를 이용하여 접근할 수 있었는데 v3 부터는 config의 globalProperties에 정의를 하도록 변경되었습니다.\n1 2 3 4 5 6 // Before Vue.prototype.$http = () =\u0026gt; {} // After const app = Vue.createApp({}) app.config.globalProperties.$http = () =\u0026gt; {} Typescript를 쓰는 경우 ComponentCustomProperties 인터페이스에 정의하는 변수나 모듈에 대한 Type 선언이 필요합니다.\n단, 기본으로 선언되는 일부 모듈(e.g. $router)에 대한 타입도 함께 선언해야 하는 것으로 보입니다.\n참조 링크12\n1 2 3 4 5 6 declare module \u0026#39;@vue/runtime-core\u0026#39; { interface ComponentCustomProperties { $http: Function; $router: Router; } } https://stackoverflow.com/questions/64175742/using-globalproperties-in-vue-3-and-typescript\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n https://github.com/vuejs/vue-next/blob/1abcb2cf61ec16807cae11cfe56acefab19487a1/packages/runtime-core/src/componentPublicInstance.ts#L41-L66\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n ","description":"","id":18,"section":"posts","tags":["vue","vue-next","vue3","typescript"],"title":"Vue 3 Global Properties and Typescript","uri":"/posts/dev/vue/2020-10-06-vue-3-global-properties-and-typescript/"},{"content":"최근 결혼을 하고 미루고 미루었던 차(車)를 구입 했다. 구입은 오래전부터 생각은 하고 있었지만 대중교통이 잘 마련되어 있는 수도권에서만 생활을 하다보니 내차에 대한 바람과 불편은 전혀 없었다. 하지만 결혼 이후 내 바람만으로는 살 수가 없고 양가의 어르신들을 모셔야하는 일도 생기다보니(처가에서 장인어른 차를 얻어타고 가는 기분이란) 늘 대중 교통이나 타인의 차량으로 해결 할수는 없게 되었다. 그래서 긴 장롱면허를 깨고 불혹을 바라보기 직전 첫차 구매를 결심하고 진행하게 되었다.\n나 같은 장롱면허 소지자에게 운전이란 너무나도 걱정되고 어려운 일이다. 장롱이 얼마나 되짚어보니 면허를 취득한게 2001년이 막 지난 겨울이었고 차를 인수받은건 불과 2주전이니 대략 18년이 넘는 기간을 운전과는 담을 쌓아두고 지냈다. 그렇다고 면허취득 후 운전을 한번도 안해본 것은 아니지만 그렇다고 했다고 하기에도 애매한데 그 간 운전 경험을 되돌아보면 2005년즈음 포터 더블캡 수동을 30분 몰아 본 일, 2008년즈음 야간에 수동 마티즈를 끌고 1시간 정도 운전한 경험, 2017년 겨울에 도로 연수 10시간을 받았고, 마지막으로 차 인수전에 본가의 싼타페를 연습삼아 1 ~ 2시간 정도 운전해본 것이 전부이다.지난 18년간 운전대를 잡아본 시간은 15시간을 넘지 않은 셈이다.\n이런 상태에서 운전 자체에 대한 막연한 걱정, 게다가 차량을 구매하기 위한 지식의 부재가 스트레스처럼 다가왔는데 지금은 어쨌든 차를 구매했고, 짧게나마 홀로 운전을 해보면서 앞서 고민했던 부분들을 어느정도 털어내었다.\n요즘같은 시대에는 나와 비슷한 처지의 사람이 많지 않을까? 하는 생각에서 비슷한 고민을 하는 사람들에게 참고가 되길 바라며 짧은 소회를 가상의 QnA로 남겨본다.\nQ. 차량 구매에 대한 결심은 언제부터 했는지? A. 생각은 오래전 부터 있었던 것 같다. 30대 초중반을 지나갈 때 쯤엔 슬슬 운전을 해봐야하지 않을까? 하는 막연한 생각과 자가 차량이 주는 장점을 누려보고 싶었기 때문이다. 하지만 차량이라는게 종류도 많고 가격도 가격이다보니 선뜻 구매하기가 너무 어려는데 브랜드, 차량 등급, 옵션에 따라 촘촘하게 나열된 수백가지의 선택 중에 하나를 고르기란 여간 어려운게 아니었기 때문이다. 내 기준에서는 필요성에 대한 간절함이 부족했는데 결혼을 하고나니 내 기준으로만 살아갈 수 없기 때문에 편의성을 위해서 결국은 식 후 결심을 확고히 하게 되었다.\nQ. 차량 선택 기준은 어떻게 정했는지? 일단 예산은 3 ~ 4천만원 사이의 준준형이나 중형 차량을 고민했다. 조금만 비용을 더 쓰면 중대형이나 외제차도 눈에 들어오긴 했지만 그런식이면 예산 제어가 힘들어져서 마지노선을 끊었다. 그리고 더 적은 비용을 쓰면 좋겠지만 안전사양과 편의사항을 갖춘 차량을 우선 선택하려고 하다보니 적은 예산에서는 상대적으로 옵션 선택이 많지 않았다.\n특히 안전사양과 편의사양의 경우 중요한 선택 기준이 되었는데, 초보 운전임을 감안했을 때 안전사양으로 최대한 위험한 상황을 피하고 실수할 수 있는 부분을 보완 받을 수 있기를 바랬고, 주차나 주행 중에 편의사양의 도움을 받기 원했기 때문이다.\nQ. 중고차에 대한 고려? A. 오래전 부터 차량 구입시 중고차에 대한 고민도 많이 했다. 중고차 매물을 가끔씩 확인하곤 했는데 차에 대한 경험이 많지 않다보니, 일단 어떤 차를 고를지 자신이 없었고 중고차의 경우 좋은 매물이 나오면 바로 결정해야 했는데, 아무리 중고라해도 큰 비용을 쓰는 문제인데 빠른 선택을 내리는 것은 쉽지 않았다. 그래서 비용적인 장점을 포기하는 대신 잘못된 차량을 선택할지 모른다는 불안감을 피하고, 조금이라도 신뢰하고 만족 할 수 있는 선택을 할 수 있는 방향으로 선택했다. 그리고 운전미숙으로 보통 실수로 긁는다거나 어디 부딛힐 걱정을 많이하는데 길게 탈생각으로 상처가 나면 나는대로 운전하겠다는 마음가짐이었기에 신차 구매에 부담이 많지 않았다.\nQ. 어떤 차량을 검토했는가? A. 초반에는 쉐보레의 트레일블레이져가 출시되면서 가장 먼저 눈에 들어왔었다. 개인적으로 SUV를 구매하고 싶기도 했지만 디자인도 내 기준에서는 맘에 들었기 때문이다. 하지만 내부 사양을 확인 했을때엔 뭔가 아쉬운 부분이 있었기 때문에 일단 보류를 하고 그 후로 셀토스, XM3, K5, 쏘렌토와 같은 차량을 추가로 검토했는데 최종적으로는 기아 K5 DL3 1.6 터보를 선택했다.\nQ. 왜 K5 1.6 터보인가? A. 마침 K5가 페이스리프트 되면서 디자인이 생각보다 너무 마음에 들게 나왔다. 그리고 기본적인 안전사양이 다 갖춰져 있었고, 상위 트림에서 어라운드뷰를 선택 할 수 있어서 좋았다. 아무래도 긴 장롱면허 초보 운전을 가정할 때 안전, 편의사양의 유무는 큰 선택요건으로 작용했고, 아직까지 운전을 많이 한 것은 아니지만 현재까지 기준에서는 매우 잘한 선택이 되었다. 물론 셀토스나 XM3, 트레일블레이저 같은 소형차량도 안전사양이 대부분 기본적으로 채용되서 차이가 크게 나지 않았지만 기본사양 외에 부가적인 사양은 아무래도 등급이 올라야 부가적으로 적용되는 경우가 많아서 결론적으로는 K5를 선택하게 되었다. 참고로 K5 3가지(1.6T, 2.0, Hybrid)에서 1.6 터보를 선택한 이유는 같은 차량임에도 변속기, 핸들, 휠 등에서 차이가 나는 부분이 있어서 결론적으로 전반적으로 평이 좋은 1.6 터보를 선택했다.\nQ. 견적과 구매 진행은 어디서 했는가? A. 아는 지인이라도 있으면 추천을 받고 싶었지만, 그러질 못했고 인근 대리점 방문할 수도 있었지만 차알못이라서 대리점 방문자체가 부담이 되서, 앱을 이용할까 다른 방법을 알아볼까 하다가 고민끝에 다나와 차 게시판에서 견적을 요청했다. 다나와 견적 게시판에 차 종류와 옵션을 적어두면 대략 덧글에 소위 영맨이라고 불리우는 카마스터들이 본인의 장점을 어필하고 연락처를 남긴다. 그럼 그 중에서 마음에 드는 카마스터와 연락해서 진행하면 된다.\nQ. 온라인에서 견적을 받고 구매하는 것이 걱정되지 않았는지? A. 첫 차량 구매를 진행하게 되면 고가의 물품이라 걱정이 되긴하는데, 보통 입금처가 해당 차 브랜드 계좌이므로 카마스터에게 직접 송금을 하는게 아니라면 크게 걱정할 것은 없는 것 같다. 나의 경우 카드로 선수금을 일시불 결제를 하였고, 잔금의 경우도 혹시 몰라서 오토카드로 할부를 했는데 이 경우 임시카드 번호가 발급되어 진행하다 보니 개인정보를 공유할 일도 없고, 실수로 돈을 잘 못 입금할 문제도 없어서 크게 걱정없이 진행했다.\nQ. 차량 선택에 있어 추가 의견은? A. 참고로 첫 차 구입이기도 하고 운전경험이 적다보니 차량간 승차감이나 운전 편의성, 반응성 같은 부분은 직접 운전해봐야지만 알 수 있는 부분인지라 스스로 비교하기는 어려워서 참고만 했다. 그리고 인터넷에서 차량 관련 정보를 얻으려고 했는데 지나치게 주관적인 내용들이나 결함이나 불량 후기 같은 부정적인 내용이 눈에 많이 들어와서 선택을 방해하는 경우가 많았다. 그래서 연비, 안전, 편의사양 같은 객관적인 항목에 대한 기준을 최우선 하고, 어느 차량이든 결함이 있을 수 있고, 사람이 조립하다보니 불량도 있고 사고나면 비용 차이야 나겠지만 결국 돈 드는건 매한가지라고 생각하면 조금 더 차량선택에 유연하게 대처할 수 있다. 기본적으로 차량 구매는 본인이 직접 지불하는 고가의 물건이고 본인이 직접 탈 물건이기 때문에 타인의 의견은 경청하고 참고하되 선택은 본인 기준에 부합하는 것으로 해야 후회가 없을 것 같다.\nQ. 자동차 보험은 어떻게 선택했는지? A. 초보에게는 자동차 보험사 선택하는 것도 항목을 비교해서 보는 것도 상당히 어려웠다. 먼저 보험료 비교를 위해서 인터넷 각 보험사의 다이렉트(보험 설계사를 거치지 않고 계약하는 것) 페이지에서 예상비용을 산출 할 수 있으니 몇개의 보험사를 선택하고 알아봤을 때에는 비용차이는 3~4만원 내외로 비슷했다. 그래서 보배드림이나 기타 자동차 커뮤니티에서 각 보험사 후기 등을 찾아보고 하나를 선택해서 계약을 진행했다.\nQ. 보험 보장내역 선택은 어떻게? A. 보험 보장내역 선택도 초보에게는 많이 어려운데, 보통 추천을 많이 하는게 기본 보험보장내역에서 대물배상, 자차손해 보장액을 확대하고, 무보험차상해 보장을 추가하는 것이다. 그외 특약으로는 견인거리 확대 등을 받으면 좋다는 평을 보고 기본 선택을 했다.\nQ. 운전자 보험은 고려했는지? A. 최근 운전자 보험 가입이 급증했다고 한다. 아무래도 본인이 조심한다고 해도 발생할 수 있는게 사고라서 법률이나 법적 비용이 발행하는 사유가 발생하는 경우가 생기는 것을 걱정해서 가입하는 운전자가 많아지는 것 같다. 참고로 자동차 보험에도 특약으로 일부 보장이 가능한데 나의 경우 이미 실비에 비슷한 내용이 보장되어 있어서 별도로 보장내역을 선택하지 않았다. 참고로 초보들은 자동차 보험은 알겠는데 운전자 보험은 뭘까? 하는 의문을 갖게 하는데 자동차 보험은 말 그대로 자동차에 드는 보험이고 운전자 보험은 운전자에게 드는 보험이라서 보장범위가 다르다. 자동차 보험은 보험을 든 차량 관련된 문제가 생겼을때 보장 받을 수 있고, 운전자 보험은 운전자가 운전을 할때 발생한 문제에 대하여 보장 받을 수 있으므로 운전자 보험은 한 사람이 여러 차량을 운행할때 필요한 보험이다.\nQ. 차량 인수전 별도로 연수를 받지 않았는지? A. 학원이나 전문 강사에 의한 도로 연수 필요성에 대한 고민이 많았다. 2017년 연수를 받을 당시에도 도로주행 자체에 대한 어려움은 느끼지 못했기 때문에 다시 연수를 받는다고 크게 나아질건 없을 것 같기 때문이었다. 하지만 당시 차량 탁송을 틴팅 업체에 보내놨었는데, 업체 위치가 집 기준으로 시외로 꽤나 떨어져 있었기 때문에 차량을 받자마자 장거리로 운행을 할 수 있을지에 대한 자신감이 확실하지 않았다. 그래서 쉽게 도로 연수를 하지 않겠다고 마음먹지 못했는데, 결론적으로는 아버지를 동승하여 집안 차량(보험에 내가 포함)으로 1시간 정도 짧게 연수를 받아서 운전에 대한 감각만 살려놓고, 차량을 받아올 때에는 운전 경험이 있는 매제를 동승하여 직접 운전해서 받아왔다. 아무래도 운전에 능통한 동승자의 유무에 따라 심리적인 안정감 차이가 커서 짧게나마 연수를 받는게 낫지 않나 싶다.\nQ. 틴팅과 블랙박스 선택은? A. 틴팅은 솔라가드 챠콜(비금속식)으로 선택했고 농도는 전체 50%(선택사항중 가장 밝은 농도)로 선택했다. 샵에서 너무 밝은거 선택한거 아니냐고 거꾸로 물어봤지만 운전이 미숙하기도 하고, 밤운전이 걱정이 되어 시야를 최대한 확보하려고 밝은 색으로 했다. 여름은 지난 상태라 뜨거운 것까진 확인하지 못했지만 전반적으로 만족한다. 블박은 아이나비 QXD5000로 달았는데, 블박은 사고 날 시 부가적인 대응책으로만 생각하고 접근하였기 때문에 크게 비교나 고민하지 않고 선택했다.\nQ. 장롱이 길었는데 운전이 무섭거나 두렵지는 않았는지? A. 실제로 차를 받아서 가져올 때는 동승자 덕분인지 무섭거나 두려움없이 편하게 오긴 했지만, 받아오기 전까지는 아무래도 걱정이 많이 들긴했다. 운전 자체가 무섭다기 보다는 직접 운전해봐야만 겪을 수 있는 상황들이 익숙하지 않았기에 분명 돌발상황이 발생을 할 것이고 이에 대한 대처를 잘 할 수 있을지에 대한 걱정이 들었다. 가능하다면 연수라도 많이 받으면 좋긴 하겠지만 짧은 연수로는 그러한 걱정을 모두 해소가 되지 않기 때문에 유투브 운전 강좌 영상을 많이 시청했다. 요즘은 운전자 시선 기준으로 영상을 찍어서 공유해주는 분들이 많기 때문에, 직접적으로 경험하지 못하는 상황을 간접으로 나마 체험하고 이미지 트레이닝하는 과정을 거쳤다. 실제 운전했을 때에는 초보운전 스티커를 실착하고 달리면 대체로 주변 운전자들이 감안하고 대처를 하니 초보라고 부끄러워하지 말고 초보임을 확실하게 알리는 것이 좋겠다.\nQ. 주차는 어렵지 않았는지? A. 인수전 주차에 걱정이 많아서 본가의 차량(싼타페)으로 공터에 박스 놓고 사이드 미러만 보고 1 ~ 2시간 주차 연습을 했다. 결론적으로는 애초에 주차나 좁은길 주행을 생각해서 어라운드 뷰 옵션이 가능한 있는 차량을 선택했다. 직접 사용해보니 사이드미러나 후방카메라 의존도가 많이 줄어서 주차가 한결 편하다. 나중에 없는 차량을 타면 문제 아니냐고 할지 모르겠지만, 없다고 아예 주차를 못하는건 아니니 그건 그 때가서 고민해도 될 일이다.\nQ. 돌발상황에 대한 걱정은 들지 않았는지? A. 운전이 막연하다보니 돌발상황에 대처에 대한 걱정도 당연히 따라온다. 운전 시간이 짧기 때문에 많은 다양한 상황을 많이 겪은건 아니지만 의도치 않은 상황이 주행때마다 조금씩은 발생하는 것 같다. 사실 이 부분 고민이 가장 컸기 때문에 최대한 안전사양을 많이 갖춘 차량을 우선했다. 당장 지난 주말 실제 도움을 받은 사례를 적어보자면 한번은 신호가 노란불이 되면서 속도를 줄이고 있었는데 정지선과 나 사이에는 차량이 없었기 때문에 천천히 속도를 줄이고 있었다. 근데 정지선에 거의 다 왔을 때쯤 그 앞으로 옆차량이 급격히 끼어들면서 급하게 브레이크를 밟아야하는 상황이 발생했는데 살짝 당황스러웠지만 브레이킹 중에 추가로 긴급 제동이 발동하면서 브레이킹이 부족하거나 늦어져서 가벼운 추돌이 발생할 수 있는 상황을 피할 수 있었다.\nQ. 추가로 도움 받은 편의 기능들은? A. 차에 대한 경험이 전무하다시피 하지만 짦막하게 경험해본 과거의 차량(거의 10년전)과 비교를 해보자면 현재의 안전, 편의사양은 너무나 편리했다. 한 두가지 꼽아보자면 먼저 차선이탈 경고 및 유지 기능이다. 도로 주행중 중간 유지하는게 익숙하지 않아 한쪽 라인으로 치우져 달리기 쉬운데 이 때 경고를 해주기도 하고, 적극적으로 핸들을 돌리지 않는 경우 알아서 조향을 조정해주는 경우가 있다. 이 기능을 100% 맹신할 수는 없지만 그래도 실수로 차선을 넘어가는 상황을 조금이라도 더 줄여줄거란 측면에서 실수에 대한 심리적 부담을 많이 줄어들었다. 그 다음엔 HUD 인데 보통 초보가 어려운게 전방 주시를 하면서 계기판, 좌우 사이드 미러를 수시로 확인하면서 주행하는 것이다. 부가적으로 네비게이션 화면까지 확인 해야하는데 운전 습관이 들기전까지는 한쪽에 집중하면 나머지를 못하는 경우가 생긴다. 하지만 HUD를 이용하면 전방 주시를 하면서 계기판 속도, 순정 네비 방향 확인을 따로 하지 않아도 한번에 가능하다 보니 상대적으로 여유가 있었다. 전방 주시에 너무 신경을 쓴 나머지 엑셀을 의도된 속도보다 더 밟는다던가 시선을 번갈아 옮기느라 운전 집중을 못한다라던가 하는 상황을 최대한 줄일 수 있는 것 같다. K5의 경우 차선 변경을 위해 깜빡이를 넣으면 사이드미러에는 사각지대 알림, 계기판에도 좌측 카메라로 실 차량 존재 여부를 확인 할 수 있는데 HUD에서 사이드미러와 동일한 경고를 확인 할 수 있기 때문에 차선 변경 시 전방 주시를 유지하면서 1차로 확인 할 수 있어서 편리했다.\n","description":"","id":19,"section":"posts","tags":["일상"],"title":"18년 장롱면허의 첫차 구입과 운전","uri":"/posts/life/daily/2020-09-21/"},{"content":"새로이 블로그를 개설! :)\n그간 많은 플랫폼을 오가면서 블로깅을 찔끔찔끔 했었는데\n자유롭고 재미있는 환경을 찾아보다가 Github으로 정착을 하려고 한다.\n나 또는 디지털 공간을 유랑하는 그 누군가에게 잠시 쉬는 공간이 될 수 있기를..\n","description":"","id":20,"section":"posts","tags":["일상"],"title":"블로그 재시작","uri":"/posts/life/daily/2020-05-31/"},{"content":"Yogo\[email protected]\n","description":"","id":21,"section":"","tags":null,"title":"","uri":"/about/"}]