-
[ESP32] Modbus 를 활용한 센서 데이터를 가져오기Programming/C 2023. 3. 16. 14:17
일단 이 글을 읽기에 앞서 제 먼저 소개를 하자면
저는 백엔드 개발자로 펌웨어 개발은 이번에 처음으로 하는 것이기에 틀린 부분이 있을 수 있습니다.
주어진 상황
목표
RS485를 통해서 병렬 연결되어 있는 센서에서 측정 데이터를 가져오는 것
조건
- ESP32 SOLO 1을 사용.
- 센서는 총 3개가 병렬로 연결.
- 외주 개발업체에서 STM32 기준으로 제작했던 펌웨어가 존재.
- 센서 업체에서 제공했던 데이터 시트가 존재.
해결 방법
선행 조건
- 센서는 RS485를 지원했고 보드가 그것을 이용하기 위해 RS485 to TTL 자동흐름제어 컨버터를 이용했다.
- 센서의 경우 12V를 지원했기에 컨버터를 이용해서 따로 전원을 제공해 주었다.
코드
ESP32에서 지원하는 Modbus 그중에서도 mbc_master_send_request를 이용해서 해결을 했다.
#include "string.h" #include "esp_log.h" #include "modbus_params.h" #include "mbcontroller.h" #include "sdkconfig.h" #define MB_PORT_NUM (CONFIG_MB_UART_PORT_NUM) UART 포트 넘버 #define MB_DEV_SPEED (CONFIG_MB_UART_BAUD_RATE) BAUD RATE 설정 // 입력과 출력 사이의 딜레이 #define POLL_TIMEOUT_MS (300) #define POLL_TIMEOUT_TICS (POLL_TIMEOUT_MS / portTICK_PERIOD_MS) static const char *TAG = "MASTER_TEST"; union converter { unsigned short hex[2]; float val; }; static void getSensorData() { // 센서 주소 총 3개 int slave_addrs[3] = {10, 20, 30}; // 센서주소 배열의 길이만큼 반복문 for (int idx = 0; idx < sizeof(slave_addrs) / sizeof(slave_addrs[0]); idx++) { esp_err_t err = ESP_OK; uint8_t type = 0; // 쓰기 진행(센서한테 측정하라고 명령을 보냄 op code 가 0x10 일 경우 쓰기 작업) // 차례대로 센서 주소, 명령어, 레지스터 시작점, 레지스터 사이즈 mb_param_request_t request = {slave_addrs[idx], 16, 1, 1}; uint16_t val = 0; val = 0x00ff; // 값 err = mbc_master_send_request(&request, &val); vTaskDelay(POLL_TIMEOUT_TICS); // 읽기 진행(센서한테 측정하라고 명령을 보냄 op code 가 0x03 일 경우 읽기 작업) mb_param_request_t rec_request = {slave_addrs[idx], 3, 83, 10}; uint16_t rec_val[50]; // 값을 담을 공간 배열 사이즈는 50 err = mbc_master_send_request(&rec_request, &rec_val); ESP_LOGI("START", "--------------------------"); // rec_val 에 담긴 데이터 읽고 파싱 for (int i = 0; i < 4; i++) { union converter con; con.hex[0] = rec_val[(i * 2) + 1]; con.hex[1] = rec_val[i * 2]; ESP_LOGI("PARAMATER DATA", "SLAVE_ADDR(%d), PARAMATER(%d) = %f", slave_addrs[idx], i, con.val); } ESP_LOGI("END", "--------------------------"); } } // Modbus master initialization static esp_err_t master_init(void) { // Initialize and start Modbus controller mb_communication_info_t comm = { .port = MB_PORT_NUM, #if CONFIG_MB_COMM_MODE_ASCII .mode = MB_MODE_ASCII, #elif CONFIG_MB_COMM_MODE_RTU .mode = MB_MODE_RTU, #endif .baudrate = MB_DEV_SPEED, .parity = MB_PARITY_NONE }; void *master_handler = NULL; esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &master_handler); MB_RETURN_ON_FALSE((master_handler != NULL), ESP_ERR_INVALID_STATE, TAG, "mb controller initialization fail."); MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb controller initialization fail, returns(0x%x).", (uint32_t)err); err = mbc_master_setup((void *)&comm); MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb controller setup fail, returns(0x%x).", (uint32_t)err); // Set UART pin numbers err = uart_set_pin(MB_PORT_NUM, CONFIG_MB_UART_TXD, CONFIG_MB_UART_RXD, CONFIG_MB_UART_RTS, UART_PIN_NO_CHANGE); MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb serial set pin failure, uart_set_pin() returned (0x%x).", (uint32_t)err); err = mbc_master_start(); MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb controller start fail, returns(0x%x).", (uint32_t)err); // Set driver mode to Half Duplex err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb serial set mode failure, uart_set_mode() returned (0x%x).", (uint32_t)err); vTaskDelay(5); ESP_LOGI(TAG, "Modbus master stack initialized..."); return err; } void app_main(void) { // Initialization of device peripheral and objects ESP_ERROR_CHECK(master_init()); vTaskDelay(10); getSensorData(); }
해당하는 코드의 초기화(master_init)는 ESP32 가 기본으로 제공하는 예제에서 몇 가지를 제거하기는 했지만 추가하는 경우는 없었다.
그래서 작성코드는 getSensorData 함수에만 있다고 봐도 된다.
코드는 짧고 간단하다 그렇기에 따로 코드에 대해서 설명을 하지는 않고
왜 mbc_master_send_request를 사용했는지, 또 주의해야 할 점은 무엇인지 작성하겠다.mbc_master_send_request 왜 사용했을까?
ESP32 기준으로 제공되는 예제에서는 mbc_master_send_request를 사용하는 경우를 찾지 못했다.
반대로 mbc_master_get_parameter와 mbc_master_set_parameter를 사용한다.
위 두 개의 함수는 단점이 명확하게 존재했다.
- 설정이 어렵다(value 넘기기가 어렵다.)
- 파생되는 코드가 지저분해진다.
그리고 장점은 발견을 못했다.
이유는 mbc_master_send_request 기준으로 비교하다 보니 더 나은 점을 발견을 못하고 단점만 찾게 되었다.그에 반해 mbc_master_send_request는 설정과 짜놓은 코드를 봤을 때 한눈에 들어온다.
나는 코드를 봤을 때 그래도 한 번에 읽히는 코드를 짜는 것을 선호하기에 위의 이점이 크게 다가왔다.
그리고 가장 큰 장점은 value에 대해 넘기는 것이 편했다.
사실 mbc_master_get_parameter와 mbc_master_set_parameter에서
그만큼 위의 2개의 함수는 사용자 측면에서는 어려웠다.
그래서 만약에 처음으로 modbus를 한다면 mbc_master_send_request를 쓰는 것을 추천한다.
알아두면 좋은 점
- mbc_master_send_request의 경우 value의 주소를 넘긴다는 점을 인지하자.
쓰기 작업을 할 때에는 해당하는 주소를 넘기고 거기에 있는 주소에 있는 데이터를 함수 내부에서 읽은 후 센서에 전송한다.
읽기 작업을 할 때에는 해당하는 주소를 넘기고 데이터를 거기에 받아온다.
만약에 데이터가 많다면 처음에 넘긴 주소를 기준으로 다음 주소 넘어가서 데이터를 바인딩하는 작업을 할 것이다.
그래서 코드를 보면 rec_val [50]으로 설정한 이유가 주소를 index를 통해서 데이터를 편하게 확인하기 위해서다. - 개발을 하면서 log output level을 debug로 해놓는 것을 추천한다.
이유는 센서에 데이터를 보낼 때 어떻게 데이터를 보냈는지 debug 레벨에서 확인이 가능한데 기본은 info 레벨로 설정되어 있어
확인이 어렵다. 그래서 debug 레벨로 변경하면 더욱더 수월한 작업이 될 것이다. - 저장 방식 순서가 리틀 에디안 방식이다.
이것을 말하는 이유는 우리가 센서 값을 읽어서 바인딩이 되어있는데 읽을 때 순서를 다르게 읽으면 완전 다른 값이 나오게 되기 때문이다.
현재 16비트로 되어있고 값이 0x1234인 경우 빅 에디안은 0x12, 0x34 순서이다.
하지만 리틀 에디안은 0x34, 0x12 순으로 저장이 되기 때문에 나중에 파싱 할 때 잘 못 읽어서 값이 이상하게 되는 경우가 있기 때문에 이점을 알아두자.
여담
일단 앞서 말했지만 웹에서 백엔드 개발을 하다 보니 펌웨어 개발은 사실 너무 생소했다.
이것을 주력으로 하는 것은 아니지만 일단은 이전에 맡긴 외주가 별로였기 때문에 이번에는 자체적으로 만들어보고
가능하면 제품으로 만들려고 하다보니 남는 시간에 작업을 하게 되었다.
그렇게 작업을 하면서 마주하는 문제가 아두이노 기준으로는 레퍼런스가 많지만 ESP32 기준으로 레퍼런스가 많지는 않았다.
물론 ESP32 기준으로 문서가 너무 잘 되어있어서 그럴 수도 있지만 그래도 레퍼런스가 많으면 참고하려고 했는데 별로 없었다.
그래서 이 글을 쓰게 된 계기이다.
누군가가 보고 조금이라도 도움이 되었으면 해서 말이다.
참고로 AWS IOT를 완벽하게 사용하는 것을 목표로 하고 있기에 현재 진행 상태는 AWS IOT를 통해서 보드->AWS IOT 까지는 진행된 상태이다.
하지만 이 부분도 힘들었던 것이 사전에 esp-idf를 설치하는 과정에서 AWS 문서를 정독하면서 하다가 실패하고
esp32 문서를 보고 설치했다.맥에서 m1 혹은 m2 칩을 사용할 경우 build 가 안 되는 문제였고 aws의 경우 3.x 버전 esp의 경우 5.x 버전이다.
만약에 본인이 m1 혹은 m2 칩을 사용하는 맥을 사용한다면 esp-idf를 5.x 버전으로 사용하시기를 권장한다.