소신호용으로 최적, 납땜 필요. 500옴 코일로 아두이노 디지털 핀에 직접 연결 가능 (10mA)
커패시터 10µF
1
커패시터 10nF
1
폴리에스터 커패시터
AC-DC 100-220V to 5V 강압 전원 모듈
1
선택 사항, USB 미연결 시 회로 전원용
필요한 도구 및 장비
납땜 인두 (브레드보드에서 테스트만 할 경우 필수 아님)
앱 및 온라인 서비스
image2cpp – 커스텀 아이콘을 PROGMEM용 16진수 포맷으로 변환
프로젝트 소개
오랫동안 오실로스코프 구입을 미뤄왔습니다. 이제 오실로스코프를 장만했으니, 오디오 회로 진단 등 취미용으로 저렴한 신호 발생기도 필요해졌습니다. 전자공학과 컴퓨팅 두 가지 열정을 결합해 아두이노로 직접 제작하는 것이 최선입니다.
주요 기능
출력 주파수: 1Hz ~ 999,999Hz
주파수 변경 모드 2가지: 로그 방식 및 단일 자릿수 방식
파형 3종: 사인파, 삼각파, 구형파
AC 또는 DC 출력 커플링
사전 정의된 두 주파수 사이에서 연속 스윕 기능
단일 노브로 모든 제어 가능
OLED 수명 연장을 위한 화면 보호기 내장
참고 사항
빠른 참조 가이드
프로토타입
10A 접점의 기성 릴레이 모듈을 사용했습니다. 당시 마이크로 리드 릴레이가 없었기 때문인데, 전력 릴레이가 필요 없을 때는 리드 릴레이가 트랜지스터, 다이오드, 저항 없이 최적의 선택입니다. 아두이노 디지털 핀은 최대 40mA까지 공급 가능하므로, 120/150옴 코일의 전자기계식 릴레이는 직접 연결할 수 없습니다.
상용 전압(메인 전원) 사용 시에는 항상 매우 매우 주의하십시오!
감전 위험 및 피부/눈 손상 위험이 있습니다. 생명은 하나뿐입니다. 소중히 다루세요.
소스 코드
JX_Wave_Generator_8.7.7.ino
JXWG_Defs.h
JXWG_Graphics.h
JX_Wave_Generator_8.7.7.ino (C/C++)
버전 8.7.7. 2021년 1월 16일 마이너 리비전
/* Copyright (c) 2020 Janux
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
설정 저장/로드 코드는 Ragnar Ranyen Homb의 "How to Load and Save Configurations on an Arduino"
(Norwegian Creations 웹사이트)의 독창적인 아이디어를 발전시킨 것입니다. */
#include "JXWG_Defs.h"
#include "JXWG_Graphics.h"
void setup() {
// 단순 인코더와 3x10K 풀업 저항 사용 시 아래 설정 적용
pinMode(PinA, INPUT);
pinMode(PinB, INPUT);
pinMode(PinS, INPUT);
// 3x10K 저항 없는 단순 인코더 사용 시 다음 3줄 사용
// pinMode(PinA, INPUT_PULLUP);
// pinMode(PinB, INPUT_PULLUP);
// pinMode(PinS, INPUT_PULLUP);
// 대부분의 PCB 장착형 인코더는 핀 A, B에 풀업 저항이 있으나 스위치 핀에는 없음
// 아래 설정 사용
// pinMode(PinA, INPUT);
// pinMode(PinB, INPUT);
// pinMode(PinS, INPUT_PULLUP);
digitalWrite(PinA, HIGH);
digitalWrite(PinB, HIGH);
digitalWrite(PinS, HIGH);
pinMode(PinCoupling, OUTPUT); // 커플링 모드 핀
Encoder.setDebounceDelay(5);
display.begin(SH1106_SWITCHCAPVCC, 0x3C); // I2C 주소 0x3C로 초기화 (128x64용)
display.clearDisplay();
// display.setRotation(2); // 디스플레이를 거꾸로 장착 시 이 줄 주석 해제
Wire.begin(); // I2C 버스 마스터로 참여
TWBR = 5; // 주파수 615kHz, 주기 1.625µs
// 인코더 스위치 누름 이벤트를 인터럽트 1(아두이노 핀 3)에 할당
attachInterrupt(digitalPinToInterrupt(PinS), encoderSwitch, FALLING);
DDS_Init(); // DDS 모듈 초기화
setConfig(); // 설정 로드 및 시작값 설정
}
void loop() {
JX_WaveGenerator_MAIN();
}
//-----------------------------------------------------------------------------------------------------------------------------------------
// JX 웨이브 제너레이터 메인 함수
//-----------------------------------------------------------------------------------------------------------------------------------------
void JX_WaveGenerator_MAIN() {
byte encoderSpin = Encoder.rotate(); // 인코더 회전 방향: 1=CW, 2=CCW
byte encoderLongPush = Encoder.pushLong(1000); // 인코더 길게 누르기 이벤트
long lStep = 0; // 현재 주파수 스텝 값
long wTime = 600000; // 10분
if (encoderPush) delay(250);
if (encoderSpin) {
cTime = millis();
} else {
if (millis() - cTime > wTime) {
ScreenSaver();
}
}
switch (mode) {
case LOGARITHMIC: // 0
//-----------------------------------------------------------------------------------------------------------------------------------
// 로그 모드: 인코더 회전으로 주파수를 로그 스텝(1,10,100,1000,10000,100000Hz) 변경
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (lFreq >= 1) {
lStep = AutoStep(lFreq, encoderSpin); // 로그 스텝 계산
} else if (_CouplingMode == OFF) {
// 커플링 모드가 OFF이면
resetCouplingMode(); // 주파수가 0이 아닐 때 기본 커플링 모드로 설정
encoderSpin = 0; // 첫 스핀 건너뛰기
}
if (encoderSpin == CW && lFreq <= 999999 - lStep) {
// 시계 방향 회전 시 주파수 증가
lFreq += lStep;
}
if (encoderSpin == CCW && lFreq >= lStep + 1) {
// 반시계 방향 회전 시 주파수 감소
lFreq -= lStep;
}
DDS_FrequencySet(lFreq, Wave[_WaveType]); // DDS 모듈에 주파수 값 전송
displayFrequency(lFreq); // 포맷된 주파수 표시
lLastFreq = lFreq; // 현재 주파수 저장
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 로그 모드: 인코더 스위치 누름으로 옵션 모드 진입
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false; // 푸시 플래그 클리어
drawSymbol(1); // 화살표 심볼 그리기
selectIcon(0, WHITE); // 첫 번째 아이콘 테두리 그리기
idx = 0;
idy = 0; // 포인터 초기화
mode = OPTIONS; // 옵션 모드로 전환
}
break; // 로그 모드 끝
case SINGLEDIGIT: // 1
//-------------------------------------------------------------------------------------------------------------
// 단일 자릿수 서브모드: 인코더 회전으로 커서 좌우 이동
//-------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idx < MAXDIGIT + 2) idx++; // 시계 방향: 포인터 증가
if (encoderSpin == CCW && idx > 0) idx--; // 반시계 방향: 포인터 감소
//------------------------------------------------------------------------------------------------------------
// idx 0~5: 주파수 자릿수 선택
//------------------------------------------------------------------------------------------------------------
if (idx >= 0 && idx < MAXDIGIT) {
// 현재 위치가 자릿수 내에 있으면
drawSymbol(0); // 위쪽 화살표 그리기
selectDigit(idx); // 현재 위치에 커서 표시, 이전 커서 삭제
if (idx == 5) selectIcon(0, BLACK); // 첫 번째 아이콘 선택 숨김
}
//------------------------------------------------------------------------------------------------------------
// idx 6~8: 아이콘 선택
//------------------------------------------------------------------------------------------------------------
if (idx >= MAXDIGIT && idx <= MAXDIGIT + 2) {
// 현재 위치가 자릿수 범위를 벗어나면
hideCursor(MAXDIGIT - 1); // 마지막 자릿수 커서 숨김
drawSymbol(1); // 아래쪽 화살표 그리기
selectIcon(idx - MAXDIGIT, WHITE); // 아이콘 선택
}
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 단일 자릿수 서브모드: 인코더 스위치 누름 이벤트
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false;
//-------------------------------------------------------------------------------------------------------------
// 자릿수 0~5 선택 시 DIGITCHANGE 모드로 전환
//-------------------------------------------------------------------------------------------------------------
if (idx <= MAXDIGIT - 1) {
hideCursor(idx); // 커서 깜빡임
delay(250);
selectDigit(idx); // 시각적 확인
drawSymbol(2); // 회전 아이콘 그리기
mode = DIGITCHANGE; // 모드 변경
}
//-------------------------------------------------------------------------------------------------------------
// 그렇지 않으면 아이콘이 선택된 것이므로 OPTIONS 모드로
//-------------------------------------------------------------------------------------------------------------
else {
if (idx >= MAXDIGIT && idx <= MAXDIGIT + 2) {
idy = idx - MAXDIGIT;
selectOption(idy);
idy = options[idy];
}
}
}
break; // 단일 자릿수 모드 끝
case SWEEP: // 2
//----------------------------------------------------------------------------------------------------------------
// 스윕 모드: 인코더 회전으로 옵션 선택을 위한 커서 좌우 이동
//----------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++;
if (encoderSpin == CCW && idy > 0) idy--;
selectIcon(idy, WHITE);
}
if (encoderPush) {
//-----------------------------------------------------------------------------------------------------------------------------------
// 스윕 모드: 인코더 누름으로 OPTIONS, 스윕 옵션, 또는 스윕 시작/정지
//-----------------------------------------------------------------------------------------------------------------------------------
encoderPush = false;
switch (idy) {
case 0:
lFreq = atol(Freq);
selectOption(idy);
idy = options[idy];
delay(100);
if (_WorkMode != 2) displayFrequency(lLastFreq);
SweepReset();
break;
case 1:
lFreq = atol(Freq);
drawSymbol(9);
drawSymbol(1);
displaySweepIcons();
selectIcon(0, WHITE);
idy = 0;
displayFrequency(_Sweep(idy));
delay(100);
SweepReset();
mode = OPTSWEEP;
break;
case 2:
// sweepStatus: STILL 0(시작 안 함), 1 BREAK, 2 PAUSE
if (sweepStatus == STILL || sweepStatus == PAUSE) {
drawSymbol(3); // 일시정지 아이콘 그리기
selectIcon(2, WHITE); // 아이콘 선택
FrequencySweep(); // 스윕 실행
} else {
// 일시정지된 경우
drawSymbol(4); // 재생 아이콘 그리기
sweepStatus = PAUSE;
}
break;
}
}
if (sweepStatus == PAUSE) flashIcon(250); // 일시정지 텍스트 깜빡임
break; // 스윕 모드 끝
case OPTIONS: // 3
if (encoderLongPush) reset(); // 길게 누르면 리셋
//-----------------------------------------------------------------------------------------------------------------------------------
// 옵션 모드: 인코더 회전으로 변경할 옵션 선택 (작업 모드, 파형 타입, 커플링 모드)
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++;
if (encoderSpin == CCW && idy > 0) idy--;
selectIcon(idy, WHITE);
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 옵션 모드: 인코더 누름으로 해당 모드 진입
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false;
selectOption(idy);
hideCursor(0);
idy = options[idy];
}
break; // 옵션 모드 끝
case OPTMODE: // 4
//-----------------------------------------------------------------------------------------------------------------------------------
// 작업 모드 옵션: 인코더 회전으로 모드 아이콘 선택 (로그, 단일자릿수, 스윕)
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++;
if (encoderSpin == CCW && idy > 0) idy--;
selectIcon(idy, WHITE);
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 작업 모드 옵션: 인코더 누름으로 작업 모드 선택 (options[0]에 저장)
//-----------------------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false;
byte pMode = _WorkMode;
switch (idy) {
case 0:
hideCursor(0);
drawSymbol(0);
lFreq = lLastFreq;
displayFrequency(lFreq);
_setWorkMode(LOGARITHMIC);
break;
case 1:
drawSymbol(0);
selectDigit(0);
lFreq = lLastFreq;
displayFrequency(lFreq);
_setWorkMode(SINGLEDIGIT);
break;
case 2:
lLastFreq = lFreq;
displayFrequency(_SweepMin); // 시작 준비
_setWorkMode(SWEEP);
break;
}
mode = _WorkMode;
if (pMode != _WorkMode) saveConfig();
if (_CouplingMode == OFF) resetCouplingMode();
idx = 0;
drawAllIcons();
}
break; // OPTMODE 끝
case OPTWAVE: // 5
//----------------------------------------------------------------------------------------------------------------
// 파형 타입 옵션: 인코더 회전으로 파형 선택 (구형파, 사인파, 삼각파)
//----------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++;
if (encoderSpin == CCW && idy > 0) idy--;
selectIcon(idy, WHITE);
}
//--------------------------------------------------------------------------------------------------------------------
// 파형 타입 옵션: 인코더 누름으로 새 파형 타입 설정
//--------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false;
if (_WaveType != idy) {
_setWaveType(idy);
saveConfig();
}
UpdateFrequency(); // 파형 타입 업데이트
drawAllIcons();
idx = 0;
mode = _WorkMode;
}
break; // OPTWAVE 끝
case OPTCOUP: // 6
//----------------------------------------------------------------------------------------------------------------
// 커플링 모드 옵션: 인코더 회전으로 커플링 모드 선택
//----------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++;
if (encoderSpin == CCW && idy > 0) idy--;
selectIcon(idy, WHITE);
}
//--------------------------------------------------------------------------------------------------------------------
// 커플링 모드 옵션: 인코더 누름으로 현재 커플링 모드 선택
//--------------------------------------------------------------------------------------------------------------------
if (encoderPush) {
encoderPush = false;
setCouplingMode(idy);
drawAllIcons();
idx = 0;
mode = _WorkMode;
}
break; // OPTCOUP 끝
case OPTSWEEP: // 7
//--------------------------------------------------------------------------------------------------------------------
// 스윕 옵션: 인코더 회전으로 편집할 스윕 값 선택
//--------------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idy < 2) idy++; // 시계 방향: 포인터 증가
if (encoderSpin == CCW && idy > 0) idy--; // 반시계 방향: 포인터 감소
selectIcon(idy, WHITE); // 첫 번째 아이콘 선택
displayFrequency(_Sweep(idy)); // 현재 스윕 값 표시
}
if (encoderPush) {
encoderPush = false;
//--------------------------------------------------------------------------------------------------------------------
// 스윕 옵션: 인코더 누름으로 스윕 값 편집 확인
//--------------------------------------------------------------------------------------------------------------------
drawSymbol(0);
selectDigit(0);
selectIcon(idy, WHITE);
idx = 0;
displayFrequency(_Sweep(idy));
mode = SWEEPEDIT;
}
break; // OPTSWEEP 끝
case SWEEPEDIT: // 8
//--------------------------------------------------------------------------------------------------------------
// 스윕 편집 모드: 인코더 회전으로 변경할 자릿수 선택
//--------------------------------------------------------------------------------------------------------------
if (encoderSpin) {
if (encoderSpin == CW && idx < MAXDIGIT - 1) idx++; // 시계 방향: 포인터 증가
if (encoderSpin == CCW && idx > 0) idx--; // 반시계 방향: 포인터 감소
selectDigit(idx);
}
//--------------------------------------------------------------------------------------------------------------
// 스윕 편집 모드: 인코더 길게 누름으로 편집 종료 후 SWEEP 모드 복귀
//--------------------------------------------------------------------------------------------------------------
if (encoderLongPush == LONGPUSH) {
encoderPush = false;
drawAllIcons();
displayFrequency(_SweepMin);
hideCursor(idx);
_setWorkMode(SWEEP);
SweepReset();
mode = _WorkMode;
delay(250);
}
if (encoderPush) {
encoderPush = false;
//-------------------------------------------------------------------------------------------------------------
// 스윕 편집 모드: 인코더 누름으로 DIGITCHANGE 모드 진입
//-------------------------------------------------------------------------------------------------------------
hideCursor(idx); // 커서 깜빡임
delay(250);
selectDigit(idx); // 시각적 확인
drawSymbol(2); // 회전 아이콘 그리기
mode = DIGITCHANGE; // 모드 변경
}
break; // SWEEPEDIT 끝
case DIGITCHANGE: // 9
//---------------------------------------------------------------------------------------------------------
// 자릿수 변경 모드: 스윕 모드에서 인코더 길게 누름으로 DIGITCHANGE 종료
//---------------------------------------------------------------------------------------------------------
if (encoderLongPush == LONGPUSH && _WorkMode == SWEEP) {
encoderPush = false;
hideCursor(idx);
drawSymbol(1);
mode = OPTSWEEP;
delay(250);
}
//---------------------------------------------------------------------------------------------------------
// 자릿수 변경 모드: 인코더 회전으로 자릿수 값 변경 (0→9→0 순환)
//---------------------------------------------------------------------------------------------------------
if (encoderSpin) {
// 인코더 회전
if (encoderSpin == CW) {
// 시계 방향
Freq[idx]++;
if (Freq[idx] > '9') Freq[idx] = '0';
} else {
// 반시계 방향
Freq[idx]--;
if (Freq[idx] < '0') Freq[idx] = '9';
}
updateDigit(idx, Freq[idx]); // 디스플레이 자릿수 업데이트
}
//---------------------------------------------------------------------------------------------------------
// 자릿수 변경 모드: 인코더 누름으로 SINGLEDIGIT 또는 SWEEPEDIT 모드 복귀
//---------------------------------------------------------------------------------------------------------
if (encoderPush) {
// 인터럽트로 설정된 인코더 푸시 플래그
encoderPush = false; // 이벤트 플래그 리셋
hideCursor(idx); // 커서 깜빡임
delay(250);
selectDigit(idx); // 시각적 확인
drawSymbol(0);
if (_WorkMode == SWEEP) {
long ltemp = _Sweep(idy); // 값 저장
_setSweep(idy, atol(Freq)); // 배열에서 long으로 변환하여 새 값 설정
if (_SweepMax > 0 && _SweepMax > _SweepMin && _SweepStep > 0) {
// 새 스윕 값의 정합성 검사
if (_Sweep(idy) != ltemp) saveConfig(); // 값이 변경되면 EEPROM에 저장
displayFrequency(_Sweep(idy));
mode = SWEEPEDIT; // 모드 변경
} else {
_displayErrMsg; // 플래시 메모리에 저장된 에러 메시지 표시
delay(1000);
_setSweep(idy, ltemp); // 저장된 값 복원
displayFrequency(_Sweep(idy)); // 값 재표시
drawSymbol(2); // 회전 아이콘 다시 그리기
}
} else {
// 스윕 모드가 아닌 경우
if (_CouplingMode == OFF) {
// 커플링 모드가 OFF인 경우
lLastFreq = atol(Freq); // 현재 주파수 저장
resetCouplingMode(); // 기본 커플링 모드 설정
}
UpdateFrequency(); // DDS 모듈에 주파수 전송
mode = SINGLEDIGIT; // 모드 변경
}
}
break; // DIGITCHANGE 끝
default:
break;
}
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 인코더 푸시 이벤트 - 인터럽트로 호출됨
//-----------------------------------------------------------------------------------------------------------------------------------
void encoderSwitch(void) {
encoderPush = true;
}
//-----------------------------------------------------------------------------------------------------------------------------------
// 유틸리티 함수들
//-----------------------------------------------------------------------------------------------------------------------------------
// 그래픽 인터페이스 그리기
//----------------------------------------------------------------------------------------------------------------
void drawInterface() {
display.clearDisplay();
display.display();
delay(1000);
display.drawRoundRect(0, 0, 128, 64, 3, WHITE); // 외부 프레임 그리기
display.fillRect(1, 1, 126, 14, WHITE); // 캡션 프레임 그리기
displayText(12, 4, strFromFlash(0), BLACK, WHITE, SMALL); // 캡션 제목 출력
delay(1000);
if (cTime == 1) { // 최초 전원 켜질 때만
displayText(XPOS - 6, YPOS + 10, strFromFlash(1), WHITE, BLACK, BIG); // 환영 메시지 표시
delay(1000);
display.fillRect(2, 16, display.width() - 3, 35, BLACK); // 환영 메시지 지우기
cTime = 0;
}
display.display();
displayText(XPOS + 84, YPOS + 4, strFromFlash(2), WHITE, BLACK, SMALL); // "Hz" 출력
sprintf(Freq, "%06li", lFreq); // 주파수 값을 "000000" 템플릿으로 char 배열에 저장
for (int i = MAXDIGIT - 1; i >= 0; i--) {
display.drawChar(XPOS + 2 + i * DELTAX, YPOS, Freq[i], WHITE, BLACK, BIG); // 오른쪽에서 왼쪽으로 애니메이션 효과
display.display();
}
}
//----------------------------------------------------------------------------------------------------------------
// 지정된 위치, 색상, 크기로 문자열 출력
//----------------------------------------------------------------------------------------------------------------
void displayText(byte x, byte y, const char *str, byte foreColor, byte backColor, byte textSize) {
display.setTextSize(textSize); // 텍스트 크기: SMALL 또는 BIG 전역 상수
display.setTextColor(foreColor, backColor); // 색상: WHITE 또는 BLACK 전역 상수
display.setCursor(x, y); // 커서 위치 설정
display.print(str); // 문자열 출력
display.display(); // 디스플레이 업데이트
}
//----------------------------------------------------------------------------------------------------------------
// 플래시 메모리의 string_table 배열에서 요소 [i]를 RAM 버퍼로 복사하고 버퍼 포인터 반환
//----------------------------------------------------------------------------------------------------------------
char* strFromFlash(byte i) {
strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i])));
return (char*)buffer;
}
//----------------------------------------------------------------------------------------------------------------
// 선택된 아이콘 주위에 테두리 그리기 또는 지우기 (이전 아이콘 테두리 지운 후)
//----------------------------------------------------------------------------------------------------------------
void selectIcon(byte icon, byte color) {
static byte prevIcon;
display.drawRect(XPOS - 10 + prevIcon * 32, YPOS + 19, 29, 20, BLACK);
display.drawRect(XPOS - 10 + icon * 32, YPOS + 19, 29, 20, color);
display.display();
prevIcon = icon;
}
//----------------------------------------------------------------------------------------------------------------
// 모든 작업 모드 아이콘 표시
//----------------------------------------------------------------------------------------------------------------
void displayWorkModeIcons(void) {
byte const *bitmap[3] = {imgLog, imgDigit, imgSweep};
_clearIconsArea;
for (byte i = 0; i <= 2; i++) {
display.drawBitmap(XPOS - 8 + i * 32, YPOS + 21, bitmap[i], 25, 16, WHITE);
}
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 모든 파형 타입 아이콘 표시
//----------------------------------------------------------------------------------------------------------------
void displayWaveTypeIcons(void) {
byte const *bitmap[3] = {imgSqr, imgSin, imgTri};
_clearIconsArea;
for (byte i = 0; i <= 2; i++) {
display.drawBitmap(XPOS - 8 + i * 32, YPOS + 21, bitmap[i], 25, 16, WHITE);
}
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 모든 커플링 모드 아이콘 표시
//----------------------------------------------------------------------------------------------------------------
void displayCouplingModeIcons(void) {
byte const *bitmap[3] = {imgCoAc, imgCoDc, imgCoOff};
_clearIconsArea;
for (byte i = 0; i <= 2; i++) {
display.drawBitmap(XPOS - 8 + i * 32, YPOS + 21, bitmap[i], 25, 16, WHITE);
}
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 모든 스윕 아이콘 표시
//----------------------------------------------------------------------------------------------------------------
void displaySweepIcons(void) {
byte const *bitmap[3] = {imgSwMax, imgSwMin, imgSwStep};
_clearIconsArea;
for (byte i = 0; i <= 2; i++) {
display.drawBitmap(XPOS - 8 + i * 32, YPOS + 21, bitmap[i], 25, 16, WHITE);
}
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 모든 아이콘 그리기
//----------------------------------------------------------------------------------------------------------------
void drawAllIcons(void) {
_clearIconsArea;
drawModeIcon();
if (_WorkMode == SWEEP || _WorkMode == SWEEPEDIT) {
display.drawBitmap(XPOS + 24, YPOS + 21, imgSwOpt, 25, 16, WHITE);
display.drawBitmap(XPOS + 56, YPOS + 21, imgSwStart, 25, 16, WHITE);
drawSymbol(1);
idy = 2;
selectIcon(idy, WHITE); // 스윕 준비
drawSmallWaveIcon();
drawSmallCouplingIcon();
} else {
drawWaveIcon();
drawCouplingIcon();
drawSymbol(0);
if (_WorkMode == SINGLEDIGIT) selectDigit(0);
}
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 해당 옵션 값에 따른 아이콘 그리기
//----------------------------------------------------------------------------------------------------------------
void drawModeIcon(void) {
byte x = XPOS - 8, y = YPOS + 21;
byte const *bitmap[3] = {imgLog, imgDigit, imgSweep};
display.fillRect(x, y, 25, 16, BLACK);
display.drawBitmap(x, y, bitmap[_WorkMode], 25, 16, WHITE);
display.display();
}
void drawWaveIcon(void) {
byte x = XPOS + 24, y = YPOS + 21;
const byte *bitmap[3] = {imgSqr, imgSin, imgTri};
display.fillRect(x, y, 25, 16, BLACK);
display.drawBitmap(x, y, bitmap[_WaveType], 25, 16, WHITE);
display.display();
drawSmallWaveIcon();
}
void drawCouplingIcon(void) {
byte x = XPOS + 56, y = YPOS + 21;
const byte *bitmap[3] = {imgCoAc, imgCoDc, imgCoOff};
display.fillRect(x, y, 25, 16, BLACK);
display.drawBitmap(x, y, bitmap[_CouplingMode], 25, 16, WHITE);
display.display();
drawSmallCouplingIcon();
}
//----------------------------------------------------------------------------------------------------------------
// 해당 옵션 값에 따른 작은 파형 아이콘 그리기
//----------------------------------------------------------------------------------------------------------------
void drawSmallWaveIcon(void) {
byte x = 114, y = 41;
const byte *bitmap[3] = {imgSqrSmall, imgSinSmall, imgTriSmall};
display.fillRect(x, y, 9, 8, BLACK);
display.drawBitmap(x, y, bitmap[_WaveType], 9, 8, WHITE);
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 해당 옵션 값에 따른 작은 커플링 아이콘 그리기
//----------------------------------------------------------------------------------------------------------------
void drawSmallCouplingIcon(void) {
byte x = 114, y = 50;
const byte *bitmap[3] = {imgAcSmall, imgDcSmall, imgOffSmall};
display.fillRect(x, y, 9, 8, BLACK);
display.drawBitmap(x, y, bitmap[_CouplingMode], 9, 8, WHITE);
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// x 위치에 커서 표시
//----------------------------------------------------------------------------------------------------------------
void showCursor(byte x) {
display.drawChar(XPOS + 2 + x * DELTAX, YPOS + DELTAY, CURSOR, WHITE, WHITE, BIG);
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// x 위치 커서 숨기기
//----------------------------------------------------------------------------------------------------------------
void hideCursor(byte x) {
display.drawChar(XPOS + 2 + x * DELTAX, YPOS + DELTAY, CURSOR, BLACK, BLACK, BIG);
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 이전 커서 숨긴 후 x 위치에 커서 표시
//----------------------------------------------------------------------------------------------------------------
void selectDigit(byte x) {
static byte lastDigit;
hideCursor(lastDigit);
display.drawChar(XPOS + 2 + x * DELTAX, YPOS + DELTAY, CURSOR, WHITE, WHITE, BIG);
display.display();
lastDigit = x;
}
//----------------------------------------------------------------------------------------------------------------
// 단일 자릿수 주파수를 문자 값으로 업데이트
//----------------------------------------------------------------------------------------------------------------
void updateDigit(byte digit, char chr) {
display.drawChar(XPOS + 2 + digit * DELTAX, YPOS, chr, WHITE, BLACK, BIG);
display.display();
}
//----------------------------------------------------------------------------------------------------------------
// 일부 심볼/아이콘 그리기 또는 지우기
//----------------------------------------------------------------------------------------------------------------
void drawSymbol(byte symbol) {
switch (symbol) {
case 0: // 위쪽 화살표
display.fillRect(2, 20, 25, 16, BLACK);
display.fillRect(2, 43, 14, 16, BLACK);
display.drawChar(XPOS - 20, YPOS + 4, ARROW, WHITE, BLACK, SMALL);
break;
case 1: // 아래쪽 화살표
display.fillRect(2, 20, 25, 16, BLACK);
display.fillRect(2, 43, 14, 16, BLACK);
display.drawChar(XPOS - 20, YPOS + 25, ARROW, WHITE, BLACK, SMALL);
break;
case 2: // 회전 아이콘
display.fillRect(2, 20, 25, 16, BLACK);
display.drawBitmap(XPOS - 21, YPOS + 1, imgTurn, 13, 13, WHITE);
break;
case 3: // 재생 아이콘
display.fillRect(4, 23, 23, 11, BLACK); // 일시정지 아이콘 지우기
display.drawBitmap(4, 23, imgSwRun, 23, 11, WHITE); // 스윕 아이콘 그리기
display.fillRect(XPOS + 56, YPOS + 21, 25, 16, BLACK); // 아이콘 영역 지우기
display.drawBitmap(XPOS + 56, YPOS + 21, imgSwPause, 25, 16, WHITE); // 스윕 재생 심볼 그리기
break;
case 4: // 일시정지 아이콘
display.fillRect(4, 23, 23, 11, BLACK); // 스윕 아이콘 지우기
display.drawBitmap(4, 23, imgSwPsd, 23, 11, WHITE); // 일시정지 아이콘 그리기
display.fillRect(XPOS + 56, YPOS + 21, 25, 16, BLACK); // 아이콘 영역 지우기
display.drawBitmap(XPOS + 56, YPOS + 21, imgSwStart, 25, 16, WHITE); // 스윕 일시정지 심볼 그리기
break;
case 9: // 심볼 영역 단순히 지우기
display.fillRect(2, 20, 25, 16, BLACK); // 위쪽 심볼 영역 지우기
display.fillRect(2, 43, 14, 16, BLACK); // 아래쪽 심볼 영역 지우기
break;
default:
break;
}
display.display();
}
//---------------------------------------------------------------------------------------------------------------
// DDS 모듈에 현재 주파수 설정, 주파수가 0이면 1로 설정
//---------------------------------------------------------------------------------------------------------------
void UpdateFrequency(void) {
lFreq = atol(Freq); // char 배열을 long으로 변환
if (lFreq < 1) {
// 주파수 0은 의미 없음
++Freq[MAXDIGIT - 1]; // 가장 오른쪽 자릿수 증가
lFreq = 1; // 주파수 1로 설정
}
displayFrequency(lFreq); // 디스플레이 업데이트
DDS_FrequencySet(lFreq, Wave[_WaveType]); // DDS 모듈에 주파수 값 전송
lLastFreq = lFreq; // 현재 주파수 저장
}
//---------------------------------------------------------------------------------------------------------------
// 6자리 제로 템플릿으로 주파수 표시
//---------------------------------------------------------------------------------------------------------------
void displayFrequency(long f) {
sprintf(Freq, "%06li", f); // long을 '000000' 템플릿으로 char 변환
displayText(XPOS + 2, YPOS, Freq, WHITE, BLACK, BIG); // 주파수 디스플레이 출력
display.display(); // 디스플레이 새로고침
}
//---------------------------------------------------------------------------------------------------------------
// 커플링 모드 기본값으로 리셋
//---------------------------------------------------------------------------------------------------------------
void resetCouplingMode(void) {
if (lFreq == 0 && _CouplingMode == OFF) {
setCouplingMode(AC);
drawCouplingIcon();
}
}
//---------------------------------------------------------------------------------------------------------------
// 특정 커플링 모드 설정
//---------------------------------------------------------------------------------------------------------------
void setCouplingMode(byte cMode) {
byte pMode = _CouplingMode;
switch (cMode) {
case 0: // AC
if (lLastFreq > 0) lFreq = lLastFreq;
digitalWrite(PinCoupling, LOW);
_setCouplingMode(AC);
break;
case 1: // DC
if (lLastFreq > 0) lFreq = lLastFreq;
digitalWrite(PinCoupling, HIGH);
_setCouplingMode(DC);
break;
case 2: // OFF
lLastFreq = lFreq;
lFreq = 0;
digitalWrite(PinCoupling, LOW);
_setCouplingMode(OFF);
break;
}
DDS_FrequencySet(lFreq, Wave[_WaveType]);
displayFrequency(lFreq);
if (cMode != pMode) saveConfig();
}
//---------------------------------------------------------------------------------------------------------------
// 옵션 선택
//---------------------------------------------------------------------------------------------------------------
void selectOption(byte opt) {
selectIcon(opt, BLACK);
switch (opt) {
case 0: // 작업 모드
displayWorkModeIcons();
selectIcon(_WorkMode, WHITE);
mode = OPTMODE;
break;
case 1: // 파형 타입
displayWaveTypeIcons();
selectIcon(_WaveType, WHITE);
mode = OPTWAVE;
break;
case 2: // 커플링 모드
displayCouplingModeIcons();
selectIcon(_CouplingMode, WHITE);
mode = OPTCOUP;
break;
}
}
//----------------------------------------------------------------------------------------------------------------
// 주파수의 로그 스텝 계산
//----------------------------------------------------------------------------------------------------------------
long AutoStep(long value, byte spin) {
if (spin == CW) {
if (value >= 100000) return 100000;
if (value >= 10000) return 10000;
if (value >= 1000) return 1000;
if (value >= 100) return 100;
if (value >= 10) return 10;
if (value >= 1) return 1;
return 0; // 잘못된 값
} else {
if (value <= 10) return 1;
if (value <= 100) return 10;
if (value <= 1000) return 100;
if (value <= 10000) return 1000;
if (value <= 100000) return 10000;
if (value <= 1000000) return 100000;
return 0; // 잘못된 값
}
}
//-------------------------------------------------------------------------------------------
// 스윕 시작 또는 일시정지 전 지점에서 재시작
//-------------------------------------------------------------------------------------------
void FrequencySweep() {
do {
if (sweepDnPausedVal == 0) { // 스윕 다운이 정지되지 않은 경우
if (sweepUpPausedVal > 0) { // 스윕 업이 정지된 경우
sweepUpPausedVal = SweepUp(sweepUpPausedVal); // 현재 값부터 계속
} else {
sweepUpPausedVal = SweepUp(_SweepMin); // 아니면 최소값부터 시작
}
}
if (sweepStatus != BREAK) { // 스윕이 정지되지 않은 경우
if (sweepDnPausedVal > 0) { // 스윕 다운이 정지된 경우
sweepDnPausedVal = SweepDn(sweepDnPausedVal); // 현재 값부터 계속
} else {
sweepDnPausedVal = SweepDn(_SweepMax); // 아니면 최대값부터 시작
}
}
} while (sweepStatus != BREAK); // 정지될 때까지 스윕 계속
}
//-----------------------------------------------------------------------------------------
// 스윕 업: sweepmin부터 시작, 인코더 누름으로 일시정지
//-----------------------------------------------------------------------------------------
long SweepUp(long sweepmin) {
long f;
for (f = sweepmin; f < _SweepMax; f += _SweepStep) {
DDS_FrequencySet(f, Wave[_WaveType]);
displayFrequency(f);
if (encoderPush) {
sweepStatus = BREAK;
break;
}
}
if (sweepStatus == BREAK) return f;
return 0;
}
//-----------------------------------------------------------------------------------------
// 스윕 다운: sweepmax부터 시작, 인코더 누름으로 일시정지
//-----------------------------------------------------------------------------------------
long SweepDn(long sweepmax) {
long f;
for (f = sweepmax; f > _SweepMin; f -= _SweepStep) {
DDS_FrequencySet(f, Wave[_WaveType]);
displayFrequency(f);
if (encoderPush) {
sweepStatus = BREAK;
break;
}
}
if (sweepStatus == BREAK) return f;
return 0;
}
//-----------------------------------------------------------------------------------------
// 전역 스윕 변수 초기화 및 디스플레이 복원
//-----------------------------------------------------------------------------------------
void SweepReset(void) {
sweepStatus = STILL;
sweepUpPausedVal = 0;
sweepDnPausedVal = 0;
display.fillRect(4, 23, 23, 11, BLACK); // 스윕 텍스트 지우기
display.display();
}
//-----------------------------------------------------------------------------------------
// 스윕 일시정지 아이콘 깜빡임
//-----------------------------------------------------------------------------------------
void flashIcon(int interval) {
static long previousMillis;
static boolean picShow;
if (millis() - previousMillis >= interval) {
previousMillis = millis();
picShow = !picShow;
if (picShow) {
display.drawBitmap(4, 23, imgSwPsd, 23, 11, WHITE); // 스윕 일시정지 아이콘 그리기
display.display();
} else {
display.fillRect(4, 23, 23, 11, BLACK); // 스윕 일시정지 아이콘 지우기
display.display();
}
}
}
//-----------------------------------------------------------------------------------------
// 아두이노 소프트웨어 리셋
//-----------------------------------------------------------------------------------------
void reset(void) {
display.fillRect(1, 16, 125, 46, BLACK);
display.display();
displayText(30, 30, strFromFlash(4), WHITE, BLACK, BIG);
char str[2];
for (byte i = 3; i > 0; i--) {
sprintf(str, "%d", i);
displayText(95, 30, str, WHITE, BLACK, BIG);
// ... (이하 생략, 전체 코드는 다운로드 참조)
}
}
JXWG_Defs.h (C/C++)
선언부
// 이 파일은 JX_WaveGenerator.ino의 필수 부분이며 메인 파일과 함께 배포되어야 합니다.
// 메인 파일과 동일한 라이선스가 적용됩니다.
// Janux 2021/01/04 이탈리아 토리노.
#ifndef JXWG_Defs
#define JXWG_Defs
#include
#include
// 인코더 라이브러리: https://www.arduino.cc/reference/en/libraries/simplerotary/
#include
// SSD1306 라이브러리를 SH1106 디스플레이용으로 개조: https://github.com/wonho-maker/Adafruit_SH1106
#include
#define DEBUG 0
#define OLED_RESET -1
Adafruit_SH1106 display(OLED_RESET);
#define PinA 5 // 인코더 핀 A
#define PinB 4 // 인코더 핀 B
#define PinS 3 // 인코더 스위치 핀
SimpleRotary Encoder(PinA, PinB, PinS);
// 루프 모드 목록
#define LOGARITHMIC 0 // +작업모드
#define SINGLEDIGIT 1 // +작업모드
#define SWEEP 2 // +작업모드
#define OPTIONS 3 // -로그/단일자릿수의 서브모드
#define OPTMODE 4 // -옵션의 서브모드
#define OPTWAVE 5 // -옵션의 서브모드
#define OPTCOUP 6 // -옵션의 서브모드
#define OPTSWEEP 7 // -스윕의 서브모드
#define SWEEPEDIT 8 // -OPTSWEEP의 서브모드
#define DIGITCHANGE 9 // -단일자릿수/스윕편집의 서브모드
#define PinCoupling 7 // 커플링 모드 핀 (릴레이 핀)
// 상수
#define XPOS 28
#define YPOS 21
#define DELTAX 12
#define DELTAY 4
#define SMALL 1
#define BIG 2
#define CW 1
#define CCW 2
#define PUSH 1
#define LONGPUSH 1
// 주파수 자릿수
#define MAXDIGIT 6
// 파형 타입
#define SQUARE 0
#define SINE 1
#define TRIANGLE 2
// 커플링 모드
#define AC 0
#define DC 1
#define OFF 2
// 스윕 상태
#define STILL 0
#define BREAK 1
#define PAUSE 2
// 심볼 문자
#define CURSOR 0x5F
#define ARROW 0x10
// AD9833 모듈 핀 연결
#define DDS_FSY 9
#define DDS_CLK 10
#define DDS_DAT 11
// AD9833 파형 타입 상수
#define wSquare 0x28
#define wSine 0x00
#define wTriangle 0x02
//-----------------------------------------------------------------------------
// 변수 선언
//-----------------------------------------------------------------------------
// 플래시 메모리에 저장된 문자열 상수 (RAM 절약)
const char str1[] PROGMEM = "JX WAVE GENERATOR"; // 18바이트
const char str2[] PROGMEM = "WELCOME"; // 8바이트
const char str3[] PROGMEM = "Hz"; // 3바이트
const char str4[] PROGMEM = "ERROR!"; // 7바이트
const char str5[] PROGMEM = "RESET"; // 6바이트
// 총 42바이트
const char* const string_table[] PROGMEM = {str1, str2, str3, str4, str5};
char buffer[18]; // 문자열용 로컬 버퍼, 가장 큰 문자열을 담을 만큼 충분히 크게
long lFreq = 1000; // 메인 주파수 변수
long lLastFreq = 1000; // 특정 상황에서 현재 주파수 저장용
long sweepUpPausedVal = 0; // 스윕 일시정지 시 값
long sweepDnPausedVal = 0; // 스윕 일시정지 시 값
long lSweep[3] = {20000, 0, 100}; // 스윕 Hz 기본값: 최대, 최소, 스텝
byte sweepStatus = STILL; // 스윕 프로세스 현재 상태
const byte Wave[] = {wSquare, wSine, wTriangle}; // 파형 타입 배열
volatile boolean encoderPush = false; // 인터럽트 호출 루틴에서 사용
char Freq[MAXDIGIT + 1]; // 6자리 템플릿 "000000"으로 표시용 주파수 배열
byte mode = 0; // 현재 루프 모드
byte idx = 0; // 자릿수 인덱스 포인터 (0~5)
byte idy = 0; // 서브모드에서 idx와 동일
long cTime = 1; // 화면보호기 카운터
// 기본 시작 설정
byte options[3] = {LOGARITHMIC, SINE, DC}; // 모드, 파형타입, 커플링모드
// 기타 매크로 정의
#define _WorkMode options[0]
#define _setWorkMode(x) options[0]=x
#define _WaveType options[1]
#define _setWaveType(x) options[1]=x
#define _CouplingMode options[2]
#define _setCouplingMode(x) options[2]=x
#define _reservedbyte 0xFF
#define _SweepMax lSweep[0]
#define _SweepMin lSweep[1]
#define _SweepStep lSweep[2]
#define _Sweep(x) lSweep[x]
#define _setSweep(x,f) lSweep[x]=f
#define _setSweepMax(x) lSweep[0]=x
#define _setSweepMin(x) lSweep[1]=x
#define _setSweepStep(x) lSweep[2]=x
// 단축 함수 매크로
#define _clearIconsArea display.fillRect(XPOS - 11, YPOS + 18, 94, 24, BLACK)
#define _displayErrMsg displayText(XPOS + 2, YPOS, strFromFlash(3), WHITE, BLACK, BIG);
// CONFIG 상수 및 변수
#define CONFIG_START 32 // EEPROM 메모리 시작 위치
#define CONFIG_VERSION "JXWG1" // 설정 버전 ID
// 사용자 정의 구조체
typedef struct {
char version[6];
byte workmode;
byte wavetype;
byte couplingmode;
byte reservedbyte;
long sweepmax;
long sweepmin;
long sweepstep;
} config_type;
// 새 구조체 생성 및 기본값으로 로드
config_type CONFIG = {
CONFIG_VERSION, _WorkMode, _WaveType, _CouplingMode, _reservedbyte,
_SweepMax, _SweepMin, _SweepStep
};
// 프로세서 리셋 함수 정의
void(*ATmegaReset)(void) = 0;
#endif
JXWG_Graphics.h (C/C++)
아이콘 리소스 데이터 파일
// 이 파일은 JX_WaveGenerator.ino의 필수 부분이며 메인 파일과 함께 배포되어야 합니다.
// 메인 파일과 동일한 라이선스가 적용됩니다.
// Janux 2021/01/04 이탈리아 토리노.
#ifndef JXWG_Graphics
#define JXWG_Graphics
//----------------------------------------------------------------------------------------------
// 일반 흑백 비트맵 PROGMEM 아이콘 데이터
//----------------------------------------------------------------------------------------------
const byte imgLog[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgDigit[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSweep[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSqr[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSin[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgTri[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgCoAc[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgCoDc[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgCoOff[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwMax[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwMin[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwOpt[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwStep[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwStart[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwPause[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgTurn[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwRun[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSwPsd[] PROGMEM = { /* ... 데이터 생략 ... */ };
// 작은 아이콘
const byte imgSqrSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgSinSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgTriSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgAcSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgDcSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
const byte imgOffSmall[] PROGMEM = { /* ... 데이터 생략 ... */ };
// 아이콘 데이터가 사용하는 총 프로그램 메모리 공간: 1148바이트
#endif