x0FANSA-hk 님의 블로그
ESP32-C3-OLED 워밍업 본문
이번에 One-Plant-One-IOT 프로젝트에서 주력을 사용할 MCU는 ESP32 C3입니다. 이중에서도 SuperMini Size에 0.42인치 OLED가 내장되어있는 제품을 사용하게 되었습니다. ESP32의 경우 지능형센서응용에서 다룬 atmega128a보다 훨씬 무겁지만 다양한 기능이 있기에, 아주 기초적인 부분조차 따로 적어둘 필요가 있을거 같아서 이렇게 글을 쓰게 되었습니다.



이건 제가 가지고 있는 보드입니다. 사진으로 보면 커보이지만 의외로 패트병 뚜껑보다 작더라고요. 그리고 보통 개발보드를 구매하면 메인 SOC가 개발보드의 윗판에 있지만 공간상의 문제로 보드 아래에 배치되어 있습니다. 배선시 합선에 조심해야 합니다.

출처: https://www.espboards.dev/esp32/esp32-c3-oled-042/
ESP32-C3 OLED 0.42" Display Development Board Details
We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies. Learn more
www.espboards.dev
가장 중요한 핀맵입니다. atmega128a와는 다르게 대부분의 핀이 ADC를 지원하며 모든 핀이 인터럽트를 지원하는 등, 핀배치로부터 비교적 자유롭다(PLIC)는 장점이 있지만, 개발보드 자체가 작아서 직접적으로 사용가능한 gpio가 적다는 단점이 있습니다. 다행히 제 프로젝트도 딱 11개의 핀만을 필요로 하기에 적절한 선택이 되었습니다.
0. 기본적인 설정하기
일반적으로 저희가 하는 C언어가 대충 소스코드 짜고 대충 컴파일하면 대충 잘 실행됩니다. 그러나 이번에는 외부 장치인 MCU, 즉 ESP32가 동작하게 해야합니다. 따라서 저희가 소스를 짜고, MCU의 기계어와 대응되게 컴파일을 한 후에, 이걸 MCU와 연결해서 컴파일된 데이터 쓰기까지 해야합니다. 그렇기에 이전처럼 DevC나 메모장으로 코딩하는거보다는, MCU 코딩을 전문으로 하는 전용 편집기가 있는것이 편리합니다. 그 중에서도 지금부터 저는 Visual Studio Code의 확장 프로그램인 Platformio를 사용해서 코딩해보겠습니다.
https://code.visualstudio.com/
Visual Studio Code - The open source AI code editor | Your home for multi-agent development
Visual Studio Code is a free, open source AI code editor. Build with AI agents that plan, code, and debug for you. Manage multi-agent workflows across environments on Linux, macOS, and Windows.
code.visualstudio.com
PlatformIO: Your Gateway to Embedded Software Development Excellence
Unlock the true potential of embedded software development with PlatformIO's collaborative ecosystem, embracing declarative principles, test-driven methodologies, and modern toolchains for unrivaled success.
platformio.org



정상적으로 설치를 하면 VScode 가장 좌측 아이콘들 사이드바에서 벌 모양의 아이콘을 볼 수 있습니다. 이것이 platformio의 마크입니다. 클릭해서 실행해 줍니다. 그러면 첫번째 사진과 같이 프로젝트를 시작하는 창이 나옵니다.(안나온다면 좌측 두번째 사이드바에서 PIO HOME의 OPEN을 클릭해주면 됩니다.)
이후 우측의 "+ New Project"를 눌러서 프로젝트 제작을 시작해 줍니다. 이때 초기에 정할 항목이 몇가지 있습니다.
1. Name: 원하는 프로젝트 이름을 적습니다. 가급적 카멜, 스네이크 표기법을 권장합니다.(경험상 한글과 공백을 잘못쓰면 꼬였을 경우 골치아파져요.)
2. Board: 사용할 보드를 적습니다. 저희가 사용할 보드인 esp32-c3-oled도 어쨌든 esp32-c3기반이기에 "Espressif ESP32-C3-DevKitM-1"로 고릅니다.
3. Framework: "Arduino"를 고릅니다. 다른거는 문법이 아예 달라져요.
4.Location: 원하는 저장위치를 고릅니다.
여기까지가 일단 MCU에 코딩을 하기위한 준비과정입니다. 그 중에서도 저희는 특별히 ESP32-C3-OLED를 다루기 위해 추가적인 설정을 해야 합니다. 이때의 설정은 platformio.ini에서 합니다.(잘 안보인다면 3번 사진 왼쪽 사이드를 주목) 이거를 설정하면 해당 하드웨어가 기존의 저장된 데이터랑은 무슨 차이가 있는지, 혹은 직접 개조한 하드웨어인지, 어떤 외부 헤더를 가져왔는지, 저수준 영역은 어떻게 구현할건지를 설정할 수 있습니다. 즉 쉽게말해서 이전까지는 MCU들 공통사항이면 여기부터는 MCU중에서도 특정 MCU를 동작하기 위한 영역이라는 것이죠. 이번글에서는 저희가 워밍업수준만 다룰것이기에, 별거 쓰지는 않을거라서 아래 코드만 ini에 복사-붙여넣기 하셔도 문제 없을 것입니다.
[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
lib_deps =
olikraus/U8g2 @ ^2.35.11
SPI
저희가 주로 코딩하게 될 곳은 여기 main.cpp입니다.

기본적으로 프로그램을 처음 생성하면 다음과 같은 코드가 이미 작성되어있는데요, 이중에서 주로 사용하게 될 곳이 setup()함수와 loop()함수 입니다. 사실 이게 별거 아니에요. 걍 안에 무한루프 있는 main()함수와 똑같습니다. mcu가 처음 부팅되면 setup()함수에 있는 내용을 1회 실행하고 이후 loop()함수에 있는 내용을 무한 반복하는 것 입니다. 그러므로 "난 모르겠다." 하시는 분들은 사실 setup()함수를 main()함수로 보고 loop()함수안에 걍 아무것도 하지말라고 delay(1000); 한줄만 넣어도 문제없습니다.
C언어를 어느정도 다룬다는 가정하에 이제 실전으로 넘어가보겠습니다.
1. 내장 LED 깜빡이기
MCU의 "Hello, World!"와 같은 예제입니다. 핀맵을 보면 GPIO 8번, 즉 8번핀에 내장 LED도 포함되어 있다고 적혀있습니다. 무슨 뜻이냐면 8번핀이 1이면 LED는 OFF, 8번핀이 0이면 LED도 ON 하겠다는 것입니다. 코드를 바로 작성해보겠습니다.
#include <Arduino.h>
const int LED_PIN=8;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
저희가 뭔갈 출력하고 싶을때는 printf()함수, 동적메모리할당을 하고싶을때는 malloc을 쓰듯, GPIO관련 함수에서는 pinMode()와 digitalWrite(), digitalRead()를 주로 씁니다.(주로 그렇다는 거지 실제로는 훨씬 많아요.) 그중에서도 이번 예제에서 쓰인 두가지 함수를 소개하겠습니다.
pinMode(uint8_t pin, uint8_t mode);
특정 핀의 상태를 결정하는 함수 입니다. 같은 GPIO더라도 읽기, 쓰기, ADC, PWM 등을 동시에 할 수 없고 한번에 하나만 해야합니다. 이때 핀에게 "넌 앞으로 이런걸 해라"라고 하는 것이 이 pinMode()함수 입니다. 즉, '어떤 핀'에 '뭘 하게할지' 정하는 함수인 것이지요.
1번 매개변수: 이름그대로 핀 번호 입니다. 어떤 핀에 해당하는 매개변수입니다. 이때 선택된 번호의 핀에게 뭘 할지를 부여합니다.
2번 매개변수: 해당 핀의 동작모드입니다. 이걸로 뭘 시킬지를 지정합니다. 보통 상수로 작성됩니다. 여기 매개변수에 들어가는 값은 다음과 같은 것들이 있습니다.(실제로는 이거보다 더 많아요. 기초단계에서는 이것만 알아도 문제없습니다.)
| mode 매개변수 (상수) | 실제 값 | 용도 |
| INPUT | 0x01 | GPIO의 값을 디지털로 읽기 |
| OUTPUT | 0x02 | 디지털 출력 |
| INPUT_PULLUP | 0x05 | 입력 설정 + 내장 풀업저항 설정 |
| INPUT_PULLDOWN | 0x09 | 입력 설정 + 내장 풀다운저항 설정 |
| ANALOG | 0xC0 | ADC사용 활성화 |
| OUTPUT_OPEN_DRAIN | 0x06 | 오픈드레인구성으로 전환 |
(주의: 두번째 매개변수를 쓸경우 상수이기 때문에 대소문자를 구별해야함)
digitalWrite(uint8_t pin, uint8_t val);
이 함수는 매개변수로 받은 핀의 상태에 따라 달라집니다. 따라서 여기서는 예제에서 있는 것처럼 OUTPUT모드일 경우를 기준으로 설명하겠습니다.(사실 이 함수의 80%는 아래 용도로만 쓰입니다.)
OUTPUT모드에서 이 함수는 핀을 선택하고 그 핀이 3.3V의 전위를 가질지(1), 0V의 전위를 가질지(0)를 결정합니다. 이때 출력저항은 작기 때문에 전류가 온전히 흐릅니다. 이걸 만약 NMOS의 게이트에 연결한다면 해당 게이트를 GPIO핀으로 온오프 시킬수도 있는 것이지요. 즉 MCU가 직접 GPIO의 전압을 고를 수 있습니다.
1번 매개변수: 역시 GPIO핀번호를 선택합니다.
2번 매개변수: 출력전위를 선택합니다. 역시 보통은 상수를 사용합니다.
| mode 매개변수 (상수) | 실제 값 | 용도 |
| HIGH | 1 | 출력모드 GPIO의 전위를 3.3V로 |
| LOW | 0 | 출력모드 GPIO의 전위를 0V로 |
따라서 위의 코드를 해석해보면 일단 setup()함수에서 GPIO 8번핀을 MCU가 값을 넣을 수 있는 OUTPUT모드로 바꾸는 작업을 하고, 이후 0.5초마다 8번핀(LED)의 값을 1과 0을 변경합니다. 그렇다면 8번핀과 연동된 내장LED도 0.5초마다 깜빡이는 것을 볼 수 있습니다.
cf) 만약 HIGH상태를 3초, LOW상태를 1초유지하면 어떨까요? 3초동안 HIGH로 설정되면 LED가 3초동안 켜지겠죠? ... 아닙니다. 8번핀의 LED는 1초 켜지고 3초 꺼지고를 반복합니다. 즉 GPIO 8이 0일때 LED가 켜집니다. 헷갈림주의
2. UART로 시리얼 출력하기
#include <Arduino.h>
int i=2;
void setup() {
Serial.begin(115200);
printf("\n");
printf("Hello, World! - First Start\n");
}
void loop() {
printf("Hello, World! - %d\n",i);
i++;
delay(1000);
}
esp32 c3 oled모듈을 보면 C타입 포트가 있고, TX RX핀이 있을 거에요. 여기 2개 다 UART통신을 하는 단자입니다. UART를 간략히 소개드리면 비동기식 통신으로 개발보드에서는 컴퓨터의 시리얼모니터를 통해 MCU의 출력을 직접 볼 수 있게하는 통신규격 입니다. 따라서 저희가 문자열을 출력하라는 명령어를 넣는다면 컴퓨터와 연결된 C타입단자를 통해 문자열을 시리얼 모니터로 출력합니다. 마찬가지로 사용된 함수를 소개하겠습니다.
Serial.begin();
사실 begin()함수의 경우 매개변수가 많이 들어갑니다.
void begin(
unsigned long baud,
uint32_t config = SERIAL_8N1,
int8_t rxPin = -1,
int8_t txPin = -1,
bool invert = false,
unsigned long timeout_ms = 20000UL,
uint8_t rxfifo_full_thrhd = 120
);
실제 함수는 위처럼 이런식으로 생겼습니다. 그러나 나머지 인자는 기본값이 대입되어있고, 어지간해서 수정할 일이 없기 때문에 보통은 비트레이트만 매개변수로 넣고 사용합니다. 즉 저도 잘 사용하지 않기에 자세한 설명은 넘어가겠습니다.
Serial.begin()에서 가운데 점은 Serial이라는 객체의 참조입니다.
class HardwareSerial {
public:
int rx_pin;
int tx_pin;
void begin(unsigned long baud) {
/* 통신 시작 코드 */
}
void print(const char* str) {
/* 데이터 전송 코드 */
}
};
HardwareSerial Serial;
이상하지 않아요? 저흰 코드에서 Serial이라는 객체를 선언한적이 없는데... 사실 자동으로 객체선언이 되있다고 합니다. 즉, printf를 쓰기 위해서는 UART통신속도 중 115200 통신속도에 맞게 begin()함수에 값만 넣어주면 된다는 것이지요. (ini에 통신속도가 이미 설정되어 있습니다.)
printf();
C언어를 처음 배우면서 많이 사용했던 유명한 그 함수 입니다. printf가 출력되기 전에 begin()함수로 우선 UART통신을 하겠다고 설정했기에 자동으로 printf는 시리얼모니터 출력으로 배정됩니다. 저희가 흔히 쓰는 printf와 사용법이 같기 때문에 굳이 설명하지 않겠습니다.(아까전처럼 함수원형을 다 열어보기에는 함수가 너무 커서 무리가 있습니다.)
다만 정석적인 방법으로는 Serial.이라는 객체를 통해서 통신을 해야 하는 것이므로, Serial.print(), Serial.printf(), Serial.println()등과 같이 객체참조 함수를 사용합니다. 그러나 사실상 printf를 수행하는 장치가 MCU와는 UART통신만으로 이루어지기에 여기서는 printf를 해도 그렇게까지 큰 문제는 없습니다.
따라서 예제의 경우 Serial.begin(115200);으롤 115200 비트레이트로 UART통신을 시작하고, 이후 printf로 시리얼모니터에 출력하는 구조입니다.
3. 하강엣지 인터럽트 설정하기
#include <Arduino.h>
void IRAM_ATTR dkanrjsk() {
digitalWrite(8, !digitalRead(8));
}
int i=0;
void setup() {
Serial.begin(115200);
pinMode(6, INPUT_PULLUP);
pinMode(8, OUTPUT);
digitalWrite(8, HIGH);
attachInterrupt(digitalPinToInterrupt(6), dkanrjsk, FALLING);
}
void loop() {
printf("이 메세지는 인터럽트와는 관련없이 1초마다 출력됩니다. - %d\n",i);
i++;
delay(1000);
}
인터럽트에 대해서는 개념적인 면은 넘어가겠습니다. esp32의 경우 인터럽트가 좀 독특한데, 결론부터 말해서 일반적으로 사용가능한 모든 GPIO가 인터럽트핀으로 사용할 수 있습니다. 따라서 ISR()함수 매개변수에 특정핀의 인터럽트상황을 넣었던 atmega128과는 다르게 직접 엔트리포인트 내부에서 '이 핀은 인터럽트를 사용한다'라고 설정을 해주셔야 합니다. 역시 중요한 명령어를 다뤄보겠습니다.
IRAM_ATTR
이건 키워드 입니다. 함수, 변수를 선언할 때 앞에 const, static, inline, volatile 등을 붙이는 것처럼 IRAM_ATTR도 필요에 따라 int, void, char 등 자료형과 별개로 붙이는 것입니다. IRAM_ATTR키워드의 역할은 하드웨어랑 밀접하게 관련되어있습니다. 일반적으로 CPU의 접근속도는 플래시메모리보다 RAM이 훨씬 빠릅니다. 그렇기 때문에 빠르게 처리해야 하는 인터럽트 함수를 인터럽트가 발생할 때 플래시메모리에서 꺼네오는건 너무 느립니다. 따라서 인터럽트함수를 IRAM_ATTR로 선언한건데, 이 키워드가 붙으면 해당 함수는 RAM에 저장되기 때문에 빠르게 사용할 수 있습니다.
digitalPinToInterrupt()
이건 정확히는 매크로 입니다. 인터럽트를 매칭해주는 함수의 첫번째 매개변수의 경우 사실 GPIO와 인터럽트 핀이 하드웨어적으로 번호가 불일치합니다.(예를들어 0번핀 인터럽트는 내부적으로 2번을 사용합니다.) 이걸 다 기억하기 뭣하니, 간단하게 매크로를 사용해서 추출된 값을 받아서 사용합니다.
attachInterrupt();
인터럽트 핀과 인터럽트 함수, 인터럽트 조건을 연결합니다.
1번 매개변수: 인터럽트 핀을 설정합니다. digitalPinToInterrupt()매크로로 추출된 값을 넣었습니다.
2번 매개변수: 인터럽트 조건이 발생했을때 실행할 함수를 넣습니다. 예제에서는 코드 상단에 작성된 dkanrjsk()함수를 넣었습니다.
3번 매개변수: 인터럽트 조건을 설정합니다. 예제에서는 FALLING인데, 핀이 1에서 0으로 바뀌는 하강엣지시 발샹하는 인터럽트 입니다.
따라서 이번 코드의 경우 6번핀을 하강엣지 인터럽트에 할당(이를 위해 6번핀에 내장 풀업을 사용함), 8번핀(내장LED)을 MCU가 제어해서, 6번핀에 하강엣지가 발생 시 8번핀이 토글이 되는 코드 입니다.
아래는 실행영상입니다. 6번핀에 택트스위치를 구성하여 만들었습니다.
4. OLED에 글자와 그림 띄워보기
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#define SDA_PIN 5
#define SCL_PIN 6
U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, SCL_PIN, SDA_PIN);
inline void ClearOLEDnDelay(){
delay(1000);
u8g2.clearBuffer();
u8g2.sendBuffer();
delay(500);
}
static const unsigned char heart_bits[] = {
0x66, // 01100110
0xFF, // 11111111
0xFF, // 11111111
0xFF, // 11111111
0x7E, // 01111110
0x3C, // 00111100
0x18, // 00011000
0x00 // 00000000
};
char text1[8]="Hello,";
const char* text2="World!";
void setup() {
u8g2.begin();
u8g2.clearBuffer();
u8g2.sendBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
}
void loop() {
u8g2.drawStr(10, 10, text1);
u8g2.drawStr(20, 20, text2);
u8g2.sendBuffer();
ClearOLEDnDelay();
u8g2.drawFrame(0, 0, 72, 40);
u8g2.drawCircle(36, 20, 15);
u8g2.drawLine(0, 0, 72, 40);
u8g2.drawLine(72, 0, 0, 40);
u8g2.sendBuffer();
ClearOLEDnDelay();
u8g2.drawXBM(32, 5, 8, 8, heart_bits);
u8g2.drawStr(0, 28, "SYSTEM READY");
u8g2.sendBuffer();
ClearOLEDnDelay();
}
우선 U8G2가 OLED에 출력하는 방식을 알아야 합니다. 우선 엔트리포인트에 들어가기도 전에, 미리 OLED출력으로 쓸 버퍼를 정적할당 합니다.(즉, 그 공간은 프로그램 내에서 못바꿔요.) 이후 여러 함수들로 해당 버퍼에'만' 그림을 그립니다. 마지막으로 버퍼에서 OLED로 출력함수를 호출하여 버퍼의 내용을 OLED로 출력합니다. 여기서 통신방식은 SDA, SCL을 사용하는 I2C방식을 사용합니다. 그러나 OLED의 경우 개발보드에 납땜되어있기에, 내부적으로 SDA는 5, SCL은 6번핀으로 할당되어있습니다.(위의 핀맵이랑 달라요!)
이번에는 새로 사용된 명령어의 종류가 많기는 하지만 별 내용은 없기에 간략하게 소개하겠습니다.
U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, SCL_PIN, SDA_PIN);
U8G2를 사용하기 위한 객체를 선언합니다.
u8g2.begin();
I2C를 통한 OLED와의 통신을 시작합니다. 아무리 다른 함수로 출력을 해도, 결국 begin()함수를 사용하지 않으면 아무런 의미가 없습니다.
u8g2.clearBuffer();
출력버퍼를 전부 0으로 채움니다. 메모리 초기화같은 것입니다.
u8g2.sendBuffer();
출력버퍼에 있는 데이터를 I2C통신을 통해 OLED에 실제로 출력하는 함수입니다. 즉 아무리 다른 함수들로 버퍼에 데이터를 입력해도 결국 sendBuffer()함수로 출력하지 않으면 아무것도 볼 수 없습니다.
출력버퍼 그리기 함수
5개 다 출력버퍼에 내용을 넣는 함수들입니다. 이것 말고도 몇가지 더 있지만 전부 설명하기엔 내용이 너무 많아서 이정도만 기록하였습니다.
u8g2.drawStr(u8g2_uint_t x, u8g2_uint_t y, const char *s);
OLED버퍼의 (x,y)좌표에 문자열 s를 출력합니다. 예제에서는 (10,10)위치에 text1을 출력하는것으로 사용되었습니다.
u8g2.drawFrame(u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h);
OLED버퍼에 속이 빈 사각형을 출력합니다. (x,y)위치에서 오른쪽으로 가로 h, 아래로 세로 w만큼의 사각형 테두리만 출력합니다. (속이 색칠된 사각형을 출력하고 싶다면 매개변수를 유지하며 u8g2.drawBox()함수를 사용하면 됩니다.)
u8g2.drawCircle(u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rad, uint8_t opt = U8G2_DRAW_ALL);
OLED버퍼에 속이 빈 원을 출력합니다. (x0,y0)이 중심이고 rad만큼의 반지름을 가집니다. opt의 경우 그릴 원의 사분면을 선택합니다. 위의 예시의 경우 기본값인 모든 사분면 출력으로 설정하였습니다.
u8g2.drawLine(u8g2_uint_t x1, u8g2_uint_t y1, u8g2_uint_t x2, u8g2_uint_t y2);
OLED버퍼에 직선을 그립니다. 이때 직선은 (x1,y1)의 시점, (x2,y2)의 종점을 가집니다.
u8g2.drawXBM(u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, const uint8_t *bitmap);
OLED버퍼의 (x,y)좌표에 bitmap이라는 주소가 가르키는 데이터를 그립니다. bitmap은 정수데이터의 포인터입니다. 해당 라이브러리가 단색 OLED전용이기에 x,y좌표에서 시작하여 w만큼 배열을 데이터를 그리고, 이후 다음줄의 픽셀로 넘어가서 데이터를 그리는 것을 반복하여 총 w×h의 픽셀을 처리합니다. 이번 코드에서는 배열형태로 가독성을 높였지만 연속된메모리를 읽는 방식이기에 어느형태던 값만 있다면 상관없습니다.
번외 u8g2.clearDisplay();
버퍼의 내용을 유지한 채, '화면만 비우자'는 함수 입니다. 이번 코드에서는 사용되지 않았지만 버퍼를 유지하며 화면전환을 하고싶을때 쓰이기 때문에 알아둬서 좋을거 같아서 소개하였습니다.
따라서 이번 코드의 경우, 우선 setup()함수에서 U8G2를 사용하겠다는 의미로 begin()함수를 호출합니다. 이후 명시적으로 버퍼와 화면을 초기화합니다. 이후...
1. 문자열을 버퍼에 넣기 → OLED에 띄우기 → 기다리기 → 버퍼초기화하고 보내기(빈화면)
2. 사각형, 원, 직선2개를 버퍼에 넣기 → OLED에 띄우기 → 기다리기 → 버퍼초기화하고 보내기
3. 임베딩 된 데이터와 문자열을 버퍼에 넣기 → OLED에 띄우기 → 기다리기 → 버퍼초기화하고 보내기
를 반복하는 코드 입니다.
실행시 OLED가 이런식으로 동작할 것입니다.
5. STA모드로 현재시간 알아오기
#include <WiFi.h>
#include "time.h"
const char* ssid="와이파이 아이디";
const char* password="와이파이 비밀번호"; //단 개방형 WiFi는 NULL을 대입하세요.
const char* ntpServer="time.google.com";
struct tm NOW;
void GTI() {
if(!getLocalTime(&NOW)){
printf("Fail to get TIME\n");
return;
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status()!=WL_CONNECTED) delay(500);
//delay(2000); //와이파이 이름을 보기 위한 용으로 필수는 아닙니다.
printf("\nWi-Fi Connected: %s\n\n",ssid);
configTime(9*60*60, 0, ntpServer);
}
void loop() {
delay(1000);
GTI();
printf("NTP: %04d-%02d-%02d %02d:%02d:%02d\n",
NOW.tm_year+1900,
NOW.tm_mon+1,
NOW.tm_mday,
NOW.tm_hour,
NOW.tm_min,
NOW.tm_sec);
}
ESP32의 꽃인 Wi-Fi입니다. 이번에는 간략하게 구글 서버에서 실시간을 받아오는 코드입니다. 역시 코드해석을 해봅시다.
구조체 tm
우선 핵심적으로 사용되는 구조체를 볼게요. NOW라는 전역변수가 tm이라는 자료형으로 선언이 되어있습니다. 이건 time.h에 미리 만들어진 구조체를 가져온 것 입니다.
struct tm {
int tm_sec; // 초 (0 ~ 60, 윤초 감안)
int tm_min; // 분 (0 ~ 59)
int tm_hour; // 시 (0 ~ 23)
int tm_mday; // 일 (1 ~ 31)
int tm_mon; // 월 (0 ~ 11, 0이 1월을 의미)
int tm_year; // 년 (1900년 이후 지나온 연도, 즉 '현재 연도 - 1900')
int tm_wday; // 요일 (0 ~ 6, 0이 일요일을 의미)
int tm_yday; // 올해 1월 1일 이후 경과한 일수 (0 ~ 365, 0이 1월 1일)
int tm_isdst; // 일광 절약 시간(DST) 서머타임 플래그
// (양수: DST 적용 중, 0: 적용 안 됨, 음수: 정보 없음)
};
시간을 얻어오는 time.h계열 함수의 경우, 대부분이 매개변수로 tm구조체의 변수를 가지고 값이 자동으로 해당 변수의 원소로 반환됩니다. 따라서 함수로 알아낸 시간관련값으로 무언갈 하고싶다면, 이 구조체에서 직접 가져와야합니다. 이건 loop()함수에서 printf내 NOW. 이라는 구조체참조로 가져오고 있습니다.
참고로 위 코드에서 사용된 getLocalTime()함수와 configTime()함수도 겉보기에는 wifi관련 함수같지만 사실은 오프라인에서도 다룰 수 있는 time.h의 함수이기에 생략하겠습니다.
WiFi.begin(ssid, password);
사실 이 함수는 아래와 같이 더 많은 매개변수로 이루어져 있습니다.
wl_status_t begin(const char* ssid,
const char* passphrase = NULL,
int32_t channel = 0,
const uint8_t* bssid = NULL,
bool connect = true);
그러나 저희가 주로 사용하는건 앞의 2개이기 때문에 자세한 설명은 생략하겠습니다. 우선 WiFi.begin()함수가 하는 일은 받은 정보를 가지고 ESP32의 PCB안테나를 통하여 주변에 해당하는 2.4GHz대의 와이파이가 있는지 탐색합니다. 이때, ESP32의 와이파이 연결을 시도하기 위해서 안테나에 많은 전력이 소비되기에, 칩의 발열과 전압변동에 주의하셔야 합니다.
1번 매개변수: 말 그대로 SSID입니다. 저희가 스마트폰 등으로 와이파이를 잡을 때 그 화면에 뜨는 와이파이 이름을 그대로 입력해주시면 됩니다.
2번 매개변수: 말 그대로 PASSWORD입니다. 저희가 와이파이를 선택한 이후 연결을 위해 공유기의 비밀번호를 입력하듯, 똑같이 비밀번호를 입력해주시면 됩니다. 단 개방형 와이파이처럼 비밀번호가 없을 경우 NULL을 넣거나, 아예 매개변수를 입력하지 않아도 됩니다.
3번 매개변수: 와이파이의 채널을 선택합니다. 공유기의 경우 전세계에서 사용하는 2.4GHz ISM대역을 사용하는데 당연히 사용자가 너무 많다보니, 하나의 공유기도 조금씩 주파수대역을 다르게하여 기기 간 혼선을 피합니다. 이때의 다른 대역들을 채널이라고 하는데 그 채널을 선택합니다. 역시 보통은 알아서 혼잡하지 않은 채널을 선택하기에 크게 중요하진 않습니다.
4번 매개변수: 공유기의 MAC Address입니다. 우연찮게 같은 SSID의 공유기가 여러대 있을 때, 하나의 공유기를 콕짚어서 연결할 수 있습니다. MAC Address의 경우 겹칠 확률이 복권1보다 낮기에 거의 하나가 특정된다고 봐도 됩니다.
5번 매개변수: IC의 Reset핀과 비슷합니다. 여기 값이 true이면 정상적으로 연결을 시도합니다. false라면 다시 true가 될때까지 연결을 시도하지 않습니다.
이 함수로 특정 인터넷주소에 접속하면, 해당 페이지에서 제공하는 API또한 사용가능합니다.
WiFi.status();
현재 ESP32의 와이파이 상태에 대한 값을 반환합니다. 반환값은 아래와 같습니다.
WL_CONNECTED: 공유기와 연결이 된 상태입니다. 공유기를 통해 인터넷도 사용할 수 있습니다.
WL_NO_SSID_AVAIL: begin()함수에 넣은 SSID를 가진 와이파이를 찾을 수 없는 상태입니다.
WL_CONNECT_FAILED: 입력된 SSID를 가진 와이파이는 찾았지만, 모종의 이유로 연결이 실패한 상태입니다. 주로 통신감도가 너무 약하거나 비밀번호가 틀렸을 때 반환됩니다.
WL_DISCONNECTED: 와이파이가 끊어진 상태입니다.
WL_IDLE_STATUS: 연결을 시도하는 상태입니다. 스캔과 비슷한 상태이지요.
사실, 이것들은 전부 상수입니다. 원래는 일반적인 함수처럼 정수로 상태를 반환합니다. 위에 나열된 5가지의 경우 차례대로 반환값이 각각 3, 1, 4, 6, 0입니다.
이번 예제에서는 while문의 조건으로 쓰였는데 WL_CONNECTED가 될때까지, 즉 정상적으로 연결될때까지 대기를 하는 코드입니다.

이를 통해 와이파이가 적절히 연결되어 시간을 가져올 수 있음을 볼 수 있습니다. 아래의 컴퓨터시간과 ESP32의 시리얼 시계가 일치함을 확인할 수 있지요.
'Satellite' 카테고리의 다른 글
| 8비트 LED버스 제작하기 (0) | 2026.07.13 |
|---|---|
| ESP-NOW 간략하게 다뤄보기 (1) | 2026.07.09 |
| 10진수 16진수 자동변환 계산기 (0) | 2026.05.15 |
| n자리 난수생성기 (1) | 2026.04.30 |
