-
[Part2] Arduino FreeRTOS로 CZ-ME310G1모뎀 MQTT Example 실시간 처리하기MQTT 2026. 6. 20. 21:31
시작하기 전에,
UNO R4 보드(R4 Minima, R4 WiFi, NANO R4)로 Arduino FreeRTOS로 코드를 만들 때 태스크 스택 사이즈를 잘못 지정하거나 FreeRTOS에서 사용하는 메모리사이즈가 지정된 HeapSize를 넘어가서 Stack과 충돌하는 경우가 발생할 때 아두이노가 정상 부팅되지 않습니다. 정상 부팅되지 않으면 펌웨어를 올리는 COM Port가 비활성화되기 때문에 손을 쓸 수 있는 방법이 없습니다. 이 상황을 복구하는 방법은 UNO R4 부트로더를 다시 올리는 방법 외에는 없습니다.
이번에 다룰 MQTT_dht11_freertos.ino를 만들면서 태스크 스택 사이즈 최적화를 위해 부트로더를 여러 번 다시 올렸습니다.
다음 순서에 맞춰서 하나하나 따라 하시면 손쉽게 부트로더를 다시 올리실 수 있습니다.
먼저, Renesas Flash Programmer를 다운로드합니다.
https://www.renesas.com/en/software-tool/renesas-flash-programmer-programming-gui?srsltid=AfmBOorSMrQqn4AqvNGBYHejeOsGycxLAFL_wNXUIeQnlcN1--fbWat1#downloadsRenesas Flash Programmer (Programming GUI)
Renesas Software and Tools. Flash memory programming software for RL78 Family, RX Family, RH850 Family, Renesas Synergy, Renesas USB Power Delivery Family, Power Management, V850 Family, 78K0R, and 78K0 embedded systems.
www.renesas.com
사용하시는 운영체제에 맞는 Flash Programmer를 다운로드합니다.
Confirm을 누릅니다.
프로그램을 다운로드하기 위해서는 로그인이 필요합니다. 계정이 없으신 경우 오른쪽의 Register Now 누르고 계정을 만듭니다. 계정이 있으신 경우 Email address, Password를 넣고 Log In을 누릅니다.

Accept and download를 누릅니다.
윈도 11 기준으로 다운로드 폴더에 Flash Programmer 프로그램이 다운로드됩니다. 압축을 해제하고 설치합니다.
Next를 누릅니다.
Agree를 선택하고 Next를 누릅니다.
설치 중 장치 소프트웨어를 설치하겠습니까?라고 물어보면 설치를 누릅니다.
Finish를 누릅니다.
윈도 시작버튼을 누르면 맞춤 아래 Renesas Flash Programmer V3.23 프로그램 아이콘을 확인할 수 있습니다.
실행합니다.
앞으로 자주 실행해야 할 것 같아 작업표시줄에 고정했습니다.
프로그램 실행 전 UNO R4 보드를 부트로더를 업로드할 수 있게 준비합니다.
점퍼선 1개로 아래와 같은 방법으로 연결합니다. 연결을 마치면 USB케이블을 꼽고 PC에 연결합니다.
[ NANO R4 ]
B1과 GND핀을 연결합니다.
[ R4 Minima, R4 WiFi ]
BOOT핀과 GND핀을 연결합니다.
Flash Programmer로 부트로더를 업로드하기 위해서는 먼저 프로젝트를 만들어야 합니다.
File --> New Project를 실행합니다.
Microcontroller는 RA를 지정하고 Project Name은 구분할 수 있도록 부트로더 종류를 적어줍니다.
Communication에서 Tool을 COM port로 선택하면 Num에 Port번호가 출력됩니다.
이 상태에서 Browse를 눌러서 프로젝트가 저장된 위치를 확인합니다. 이후 Open Project에서 불러올 때 확인한 위치를 선택합니다.
모든 작업이 완료되면 보드의 Reset 버튼을 누르고 Connect를 누릅니다.
아래 로그창에 Operation completed 메시지가 출력되면 정상적으로 연결된 상태입니다.
이제 Add/Remove Files를 눌러서 부트로더를 선택합니다.
Add File(s)를 누릅니다.
윈도 11 기준, 부트로더 위치는 각각 아래와 같습니다.
[ NANO R4 ]
C:\Users\사용자계정\AppData\Local\Arduino15\packages\arduino\hardware\renesas_uno\1.6.0\bootloaders\NANOR4\
dfu_nano.hex - NANO R4 bootloader
[ R4 Minima, R4 WiFi ]
C:\Users\ 사용자계정 \AppData\Local\Arduino15\packages\arduino\hardware\renesas_uno\1.6.0\bootloaders\UNO_R4\
dfu_minima.hex - UNO R4 Miniama bootloader
dfu_wifi.hex - UNO R4 WiFi bootloader
보드에 맞는 부트로더 hex파일에 열기를 누르고 File Details에서 OK를 누릅니다.
이제 Start를 누릅니다.
혹시 NG가 나오면 당황하지 말고 보드 리셋버튼을 누른 뒤 다시 Start를 누릅니다.
아래 로그와 같이 Operation completed가 나오면 부트로더 올리기가 성공한 것입니다.
언제든지 부트로더를 올리기 위해 현재 프로젝트를 저장합니다. File --> Save Project를 누릅니다.
이제 보드와 PC 간 USB연결을 해제하고 부트로더 업로드를 위해 연결했던 점퍼선 제거 후
USB를 연결하고 Arduino IDE를 실행합니다.
Examples --> 01.Basics --> Blink를 선택합니다.
Tools --> Port를 누르면 Arduino R4 dfu ports가 표시된다. 포트를 선택합니다.
Arduino IDE 오른쪽 아래 선택한 포트가 표시됩니다.
이제 펌웨어를 업로드합니다.
펌웨어 업로드가 마무리되면 Arduino IDE 오른쪽 아래 포트번호가 잡히는 것을 확인할 수 있습니다.
Tools --> Board의 내용을 확인해 보면 COM포트가 살아난 것을 확인할 수 있습니다. 이제 다시 FreeRTOS 코드를 만들 수 있는 환경이 준비되었습니다.
보드가 부팅되지 않을 때 살리는 방법을 먼저 배웠습니다.
J-Link를 사용하실 수 없는 경우도 있기 때문에 Boot 모드를 설정해서 부트로더를 다시 올리는 방법으로 진행했습니다. 이번 코드를 만들면서 여러 번 부트로더를 다시 올려야 했습니다. 앞으로 작업을 위해 꼭 익숙해 지시길 바랍니다.
이제 본격적으로 시작해 봅시다.
우리가 사용할 MQTT_dht11_freertos는 코드주에서 배포하는 ME310G1 라이브러리 0.1.11 버전 이상에서 사용하실 수 있습니다.
LIBRARY MANAGER를 실행하고 ME310G1을 검색해서 버전 확인 후 이전 버전이면 UPDATE 합니다.
라이브러리가 준비되면 Examples --> ME310G1 --> UNO_R4 --> MQTT_dht11_freertos를 실행합니다.
이전 블로그에서 다뤘던 Arduino FreeRTOS의 태스크 구성에서 하나의 태스크가 추가되었습니다.
dht11 온습도 센서를 추가했는데, 해당 장치는 dht11 센서로부터 데이터를 수집하고 있다가 MQTT 브로커를 통해서 요청이 오면 온도와 습도값을 json타입으로 반환합니다.
요청이 올 때 읽은 센서값을 바로 보내는 대신, 1분 기준 이동평균값을 가지고 있다가 요청이 오면 해당값을 보내도록 코드를 구현했습니다. 이동평균값을 사용하는 이유는 지금 측정한 센서값 보다는 기준시간을 두고 변하는 변화를 모니터링하는 경우 적합합니다. dht11의 경우 2초 간격으로 새로운 온습도 센서값을 가져올 수 있는데, 일반적으로는 1분 기준이면 30회 가져올 수 있으므로 float타입 배열 30개씩 각각 온습도에 배치해서 값을 채우다가 마지막 배열이 다 차면 첫 번째 배열에 새로운 값을 넣는 원형큐 방식으로 유지하고 있다가 사용자가 요청하면 현재 슬롯의 30개 데이터를 합산 후 30으로 나눠서 온도와 습도 값을 각각 구해서 반환합니다. 센서값이 튀는 경우가 발생되면 내부 배열값을 정렬알고리즘으로 다시 한번 순서대로 배치하고 제일 작은 값과 제일 큰 값을 빼고 나머지 값을 합산하고 나머지 개수만큼 나눠서 평균을 구하는 필터를 적용하기도 합니다.
정리하면
1. 센서 데이터에 이동평균을 적용하는 3대 실무적 이유
① 순간적인 노이즈(Spike) 차단
하드웨어 센서는 완벽하지 않습니다. 주변의 미세한 전자기적 간섭(EMI), 전원 전압의 미세한 출렁임, 혹은 센서 표면을 스치는 미세한 바람 때문에 순간적으로 말도 안 되는 튀는 값(예: 갑자기 23도에서 45도로 튀었다가 다음 초에 다시 23도가 되는 현상)을 뱉을 수 있습니다.
이동평균을 적용하면 이런 뾰족한 송곳 같은 노이즈를 뭉툭하게 깎아내어 전체 데이터의 신뢰도를 지켜줍니다.
② 거친 계단 현상(Resolution Step)의 마법 같은 완화
우리가 사용하는 DHT11 센서는 온도를 1°C 단위(정수)로만 출력하는 저해상도 센서입니다. 필터 없이 그래프를 그리면 23도에 한참 머물다가 갑자기 24도로 툭 점프하는 거친 계단 모양의 그래프가 됩니다.
여기에 이동평균을 적용하면 23.0 ➡️ 23.1 ➡️ 23.3 ➡️ 23.6... 처럼 값이 부드러운 곡선을 그리며 소수점 단위로 변하는 가상의 고해상도 데이터로 재탄생합니다. 시각화(대시보드)를 했을 때 퀄리티 차이가 극명합니다.
③ 제어 시스템의 '발작(채터링, Chattering)' 방지
이 부분이 실무에서 가장 치명적이고 중요한 이유입니다. 예를 들어 온도가 25°C 이상이 되면 에어컨(릴레이)을 켜는 시스템을 만들었다고 가정해 봅시다.
현재 온도가 딱 25°C 부근일 때, 생 센서값을 쓰면 노이즈 때문에 1초 사이에도 24.9°C ➡️ 25.1°C ➡️ 24.9°C ➡️ 25.1°C로 요동칠 수 있습니다. 필터가 없다면 에어컨 스위치가 1초에 몇 번씩 켜지고 꺼지기를 반복하며 부품이 타버리거나 고장 나는 대참사가 발생합니다. 평균값을 쓰면 데이터가 묵직하고 젠틀하게 움직이므로 시스템이 안정됩니다.
💡 요약하자면
이동평균을 사용하는 이유는 센서의 **'불안정한 떨림'**과 **'거친 해상도'**를 가볍고 똑똑한 수학 공식 하나로 극복하여, 상용 장비로서 부끄럽지 않은 신뢰할 수 있는 데이터 트렌드를 확보하기 위함입니다. 편하게 날것의 데이터를 쓰다가 현장에서 겪는 수많은 예외 상황을 예방하는 훌륭한 방어벽입니다.
32비트 프로세서 기준 float 변수 1개는 4바이트 이므로 4*30 = 120바이트를 차지합니다.
온도 + 습도 = 240바이트를 사용하게 됩니다.
이번에는 위 방식에서 메모리 다이어트를 하면서 비슷한 효과를 얻을 수 있는 지수이동평균을 사용했습니다.
지수이동평균 수식은 다음과 같습니다.

가중치( α ) 구하기 (황금비율)
1분 동안 30개의 데이터를 가져오고 이 값을 new + old 두 가지 케이스로 나눠야 하기 때문에
수식을 구하면 아래와 같습니다.
즉, 새로 들어온 센서 값에 6.4%만 반영하고, 기존의 부드러운 평균을 93.6% 유지하는 것이 가장 이상적입니다.
(* 수식으로 그렇다는 이야기고 실제로 출력된 내용을 그래프로 그려서 원하는 형태의 변이량이 표현될 수 있게 튜닝이 필요합니다.)
Arduino FreeRTOS로 구현한 코드는 아래와 같습니다. 100%의 6.4% 비중이므로, 코드에서는 새로 들어오는 값에 0.064를 곱하고 기존 누적값에는 (1-0.064)을 곱해 줍니다. 곱해 준 두 값을 더한 후 다시 누적값에 넣어 줍니다.
센서값을 처음 얻을 때는 누적값이 없기 때문에 온도, 습도 누적값에 처음 구해진 센서값을 넣어줍니다. 이 루틴은 한 번만 실행됩니다.
vSensorTask는 SerialPrint를 사용하지 않았습니다. 센서태스크는 장비센서값 수집에만 온전히 집중합니다.
혹시 궁금하시면 수식 구현부만 예제로 만들어서 SerialPrint를 넣어서 확인해 보시기 바랍니다.
마지막으로 구해진 지수이동평균값을 FreeRTOS xQueueOverwrite 커널함수를 사용해서 준비한 xSensorQueue에 OverWrite 합니다. xSensorQueue는 두 개의 flaot변수를 가지고 있어서 각각 온도 지수이동평균값, 습도 지수이동평균값의 새로운 값을 계속 갱신하도록 했습니다. 전역변수 대신 Queue를 사용한 이유는 서로 다른 태스크(센서태스크, 통신태스크)에서 동시접근해서 데이터를 오염시키거나 파괴할 수 있기 때문에 커널 데이터 공유를 위한 방법 중 하나인 Queue를 사용했습니다./* [TASK 1] Sensor Acquisition: EMA Filtering & Queue Update */void vSensorTask(void *pvParameters) {float emaT = 0.0, emaH = 0.0;bool first = true;float alpha = 0.064;float buffer[2];
for (;;) {float rawT = dht.readTemperature();float rawH = dht.readHumidity();
if (!isnan(rawT) && !isnan(rawH)) {if (first) {emaT = rawT;emaH = rawH;first = false;} else {emaT = (alpha * rawT) + ((1.0 - alpha) * emaT);emaH = (alpha * rawH) + ((1.0 - alpha) * emaH);}buffer[0] = emaT;buffer[1] = emaH;xQueueOverwrite(xSensorQueue, buffer);}vTaskDelay(pdMS_TO_TICKS(2000));}}
vMQTTTask에서는 구독한 토픽으로 메시지가 들어오면 xQueuePeek 커널함수를 사용해서 현재의 큐값을 Peek 합니다. Peek은 데이터를 가져가는 것이 아닌 들어있는 데이터를 본다, 복사한다 의미로 해석하시면 될 것 같습니다.
여기서 중요한 내용이 하나 있습니다.
Telit ME310G1-W3에 제공하는 MQTT AT커맨드로 동작시키는데 해당 명령어를 사용해서 전송하는 데이터에 따옴표가 있을 경우 전송이 되지 않습니다. 그래서 예제에서 더블쿼테이션(따옴표)을 싱글쿼테이션으로 변경해서 보냈습니다.
서버에서 데이터를 받을 때 이 부분은 JSON포맷에 맞춰서 다시 한번 변환이 필요합니다.
MQTT 개발 시 꼭 기억해 주셨으면 합니다.
// Since MQTT AT commands do not support double quotes,// single quotes are used as a fallback for JSON structure.sprintf(jsonBuf, "{'temp':%.1f,'hum':%.1f}", data[0], data[1]);int msgId = 1;// Parse integer data safely using sscanf (Completely replaces String functions)sscanf(ringPtr, "#MQRING: %d,%d", &instanceNum, &msgId);
DEBUG_SERIAL.print("[INFO] MQTT Instance : ");DEBUG_SERIAL.println(instanceNum);DEBUG_SERIAL.print("[INFO] Message ID : ");DEBUG_SERIAL.println(msgId);DEBUG_SERIAL.println("-------------------------------------");
myME310.debugMode(false);myME310.mqtt_read(instanceNum, msgId, ME310::TOUT_5SEC);DEBUG_SERIAL.println(myME310.buffer_cstr_raw());
float data[2];if (xQueuePeek(xSensorQueue, data, 0) == pdTRUE) {char jsonBuf[64];// Since MQTT AT commands do not support double quotes,// single quotes are used as a fallback for JSON structure.sprintf(jsonBuf, "{'temp':%.1f,'hum':%.1f}", data[0], data[1]);myME310.mqtt_publish(1, PUB_TOPIC, 1, 0, jsonBuf);DEBUG_SERIAL.println("[MQTT] Status Report Sent.");}
이제부터 FreeRTOS Heap Memory를 검사하며 개발하고 있습니다.
메모리 누수가 있는지 남은 Heap Memory는 얼마나 되는지 태스크 중간중간에 넣어서 모니터링하고 있습니다.
코드는 아래와 같습니다.size_t freeHeap = xPortGetFreeHeapSize();Serial.print("Current Free Heap: ");Serial.println(freeHeap);
마지막으로 센서데이터를 담을 큐생성과 태스크의 우선순위와 커널 힙메모리 할당량입니다.
xSensorQueue = xQueueCreate(1, sizeof(float[2])); // [0]:Temp, [1]:Hum
float데이터를 2개 저장할 수 있고 각각 데이터를 한 개씩만 넣을 수 있게 큐를 만들었습니다.xTaskCreate(vMQTTTask, "MQTT_Task", 1000, NULL, 2, &xMQTTTaskHandle);vMQTTTask는 세 개 태스크 중 가장 높은 우선순위인 2를 갖습니다. 커널힙메모리 사이즈도 최대한 줄여서 4000바이트(1000x4)로 모든 동작이 이상 없이 동작되는 것을 확인했습니다. (기존 예제에서는 5KB를 사용했습니다.)
xTaskCreate(vSensorTask, "Sensor_Task", 200, NULL, 1, NULL);vSensorTask는 세 개 태스크 중 우선순위 1을 갖습니다. MQTT통신 중 중간에 들어가지 않게 우선순위를 낮게 했고 커널힙메모리 800바이트(200x4) 사용합니다. 더 줄여 봤는데, 부팅이 안 되는 문제로 이 정도까지 조정했습니다.
xTaskCreate(vCommandTask, "Cmd_Task", 150, NULL, 1, &xCommandTaskHandle);
vCommandTask는 구현 내용은 동일하지만 커널힙메모리를 다이어트해서 기존 192에서 150으로 줄여서 168바이트(42*4)를 줄였습니다. q 또는 Q를 입력하고 엔터키를 누르면 모뎀을 안전하게 종료할 수 있습니다.
xMQTTConnectSemaphore = xSemaphoreCreateBinary();xSensorQueue = xQueueCreate(1, sizeof(float[2])); // [0]:Temp, [1]:Hum
xTaskCreate(vMQTTTask, "MQTT_Task", 1000, NULL, 2, &xMQTTTaskHandle);xTaskCreate(vSensorTask, "Sensor_Task", 200, NULL, 1, NULL);xTaskCreate(vCommandTask, "Cmd_Task", 150, NULL, 1, &xCommandTaskHandle);
vTaskStartScheduler();
DHT11 센서의 신호선은 8번 핀에 연결하고 VCC는 5V핀, GND는 GND핀에 각각 연결합니다.

장비에서 출력한 동작로그 및 MQTT Exploer에서 수신받은 내용입니다.
부팅 후 제어태스크까지 동작하게 된 시점에서 Free Heap 사이즈는 832바이트입니다.
MQTT태스크가 토픽을 수신받고 지수이동평균 온습도 데이터를 전송 후 측정한 Free Heap 사이즈도 832바이트입니다.
장비에서 출력한 전체 로그입니다.
이후 몇 번의 토픽메시지를 장비로 전달하고 지수이동평균 온도, 습도값을 받았습니다.
Kernel Free Heap 사이즈는 832바이트를 유지하는 것을 확인했습니다.AT
AT
AT
AT$GPSP=1
AT$GPSP=0
[INIT] Disconnecting Leftover MQTT Sessions...
AT#MQDISC=1
AT#MQEN=1,0
AT#SGACT=1,0,"",""
AT+CMEE=2
AT+CPIN?
AT+CGDCONT=1,"IP","simplio.apn"
AT+CGDCONT?
AT+CGREG?
AT#SGACT=1,1
AT#MQEN?
AT#MQEN=1,1
AT#MQEN?
AT#MQCFG=1,"broker.emqx.io",1883,1,0
AT#MQCONN=1,"czme310g1001","",""
AT#MQSUB=1,"czme310/topic/get"
AT#MQPUBS=1,"czme310/topic/put",1,0,"Connected smoothly"
>>> MQTT Setup Complete. Streaming Listening Loop Running...
[SYSTEM] Command Task Unlocked via Semaphore.
Current Free Heap: 832
-------------------------------------
[URC DETECTED] #MQRING: 1,1,czme310/topic/get,10
[INFO] MQTT Instance : 1
[INFO] Message ID : 1
-------------------------------------
1234567890
[MQTT] Status Report Sent.
Current Free Heap: 832
-------------------------------------
[URC DETECTED] #MQRING: 1,1,czme310/topic/get,5
[INFO] MQTT Instance : 1
[INFO] Message ID : 1
-------------------------------------
12345
[MQTT] Status Report Sent.
Current Free Heap: 832
-------------------------------------
[URC DETECTED] #MQRING: 1,1,czme310/topic/get,5
[INFO] MQTT Instance : 1
[INFO] Message ID : 1
-------------------------------------
00000
[MQTT] Status Report Sent.
Current Free Heap: 832
-------------------------------------
[URC DETECTED] #MQRING: 1,1,czme310/topic/get,14
[INFO] MQTT Instance : 1
[INFO] Message ID : 1
-------------------------------------
Hello World!!!
[MQTT] Status Report Sent.
Current Free Heap: 832
==============================================
[USER CMD] 'q' detected! Signalling Graceful Shutdown...
==============================================
[TASK_INTERNAL] Executing Safe Shutdown Sequence...
AT#MQDISC=1
AT#MQEN=1,0
AT#SGACT=1,0,"",""
ME310G1 Modem Power Off
[SYSTEM] MQTT Task Terminated Itself Safely.
=== All MQTT Public Procedures Cleaned up ===
[SYSTEM] Shutdown sequence completed successfully.
마지막으로 동작영상입니다.
LTE-CATM1 내장형 모뎀 대량 구매 상담, 외주 개발, 협업 문의, vodafone IoT유심 문의
(주)코드주
장병남 대표 010-8965-1323 rooney.jang@codezoo.co.kr
코드주 LTE-CatM1 내장형 모뎀 CZ-ME310G1 / GNSS(GPS) 지원 / 외장 LTE 안테나 포함 / Zephyr 및 Arduino 지원
메카솔루션 공식 쇼핑몰
www.mechasolution.com
iot유심 : codezoo
[codezoo] IoT Global SIM, IoT Connectivity, IoT Device
smartstore.naver.com
CodeZoo LTE-CatM1 전용 아두이노 브레이크아웃 쉴드
메카솔루션 공식 쇼핑몰
mechasolution.com
'MQTT' 카테고리의 다른 글
Arduino FreeRTOS로 CZ-ME310G1모뎀 MQTT Example 실시간 처리하기 (0) 2026.06.15 HiveMQ TLS MQTT를 Type1SC AT커맨드로 연결하기 (2) 2025.06.10 hivemq.com broker에 접속이 안되요 (1) 2025.04.29 아두이노에서 실시간으로 MQTT 메시지를 받을수 있을까요? (0) 2025.02.02 MQTT 이제 모바일로 테스트 하세요 (0) 2023.10.12