ppposclient, RS485 장치를 동시에 사용하는 경우 (예제 포함)
ESP32 IoT보드에서 ppposclient 라이브러리 ( https://github.com/codezoo-ltd/ppposclient )와 RS485 라이브러리 ( https://github.com/4-20ma/ModbusMaster )를 동시에 사용할 경우 제대로 동작이 되지 않는다는 사용자의 질문 글이 며칠 전 개발자 커뮤니티에 올라왔습니다.
각각의 라이브러리는 ESP32의 UART2, UART1에 각각 연결되어 동작되는데, 왜 동작이 되지 않는지 검토하고
코드를 개선해서 동작할 수 있도록 작업 했습니다.
먼저 아래와 같이 하드웨어를 구성 했습니다.
< 사용조건 >
- LTE CATM1 모뎀은 ppposClient 라이브러리를 통해 Serial2(UART2)에 연결해서 ppp모드로 동작되는 상태입니다.
- 여기에 Serial1(UART1)에 연결한 RS485 컨버터를 이용해서 온습도센서 데이터를 읽으려고 하면 값이 읽히지 않고 통신이 블락됩니다.
< 원인 >
- FreeRTOS 태스크 우선순위
일단 ppposClient 라이브러리는 PPPOS_init 함수를 호출할 때 내부에서 우선순위 5인 태스크를 호출합니다.
https://github.com/codezoo-ltd/ppposclient/blob/main/src/PPPOS.c : 170번 라인
void PPPOS_init(int txPin, int rxPin, int baudrate, int uart_number, char *user,
char *pass) {
PPPOS_uart_num = uart_number;
gpio_set_direction(txPin, GPIO_MODE_OUTPUT);
gpio_set_direction(rxPin, GPIO_MODE_INPUT);
gpio_set_pull_mode(rxPin, GPIO_PULLUP_ONLY);
uart_config_t uart_config = {.baud_rate = baudrate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE};
uart_param_config(PPPOS_uart_num, &uart_config);
uart_set_pin(PPPOS_uart_num, txPin, rxPin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
uart_driver_install(PPPOS_uart_num, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, 0);
tcpip_adapter_init();
PPP_User = user;
PPP_Pass = pass;
xTaskCreate(&pppos_client_task, "pppos_client_task", 10048, NULL, 5, NULL); //5 우선순위 (숫자가 클수록 우선순위 높음)
}
pppos_client_task가 내부에서 계속 동작되고 있는데, 이 태스크는 아두이노에서 호출되는 우선순위 0인 main 태스크 보다 우선순위가 높아서 자신이 가지고 있는 자원을 항상 먼저 독차지합니다.
- 붙잡고 있는 자원은 Serial 라이브러리에서 사용하는 UART API 함수들입니다.
- 자신이 붙잡고 있는 자원을 양보할 때는 아래 코드의 100ms 정도의 시간입니다.
(vTaskDelay(100/ portTICK_PERIOD_MS);
- Modbus RTU 온도센서 읽기가 완료되지 않았는데 계속 자원을 빼앗기기 때문에 온도센서의 값을 제대로 읽어올 수 없습니다.
static void pppos_client_task(void *pvParameters) {
char *data = (char *)malloc(BUF_SIZE);
while (1) {
while (PPPOS_started) {
memset(data, 0, BUF_SIZE);
int len = uart_read_bytes(PPPOS_uart_num, (uint8_t *)data, BUF_SIZE,
10 / portTICK_RATE_MS);
if (len > 0) {
pppos_input_tcpip(ppp, (u8_t *)data, len);
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
이제 ppposClient와 ModbusRTU가 동시에 동작할 수 있도록 코드를 작성해 보겠습니다.
#include <PPPOS.h>
#include <PPPOSClient.h>
#include "TYPE1SC.h"
#include <Arduino.h>
#include <U8x8lib.h>
#include <ModbusMaster.h>
#define SERIAL_BR 115200
#define GSM_SERIAL 2
#define GSM_RX 16
#define GSM_TX 17
#define GSM_BR 115200
#define PWR_PIN 5
#define RST_PIN 18
#define WAKEUP_PIN 19
#define EXT_ANT 4
#define DebugSerial Serial
#define M1Serial Serial2
char *ppp_user = "codezoo";
char *ppp_pass = "codezoo";
char *msg = "hello world ppp";
String APN = "simplio.apn";
#define TCP_SERVER "echo.mbedcloudtesting.com"
const uint16_t port = 7;
String buffer = "";
char *data = (char *)malloc(1024);
PPPOSClient ppposClient;
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/U8X8_PIN_NONE);
TYPE1SC TYPE1SC(M1Serial, DebugSerial, PWR_PIN, RST_PIN, WAKEUP_PIN);
#define U8LOG_WIDTH 16
#define U8LOG_HEIGHT 8
uint8_t u8log_buffer[U8LOG_WIDTH * U8LOG_HEIGHT];
U8X8LOG u8x8log;
/* EXT_ANT_ON 0 : Use an internal antenna.
* EXT_ANT_ON 1 : Use an external antenna.
*/
#define EXT_ANT_ON 0
float temp = 0.0F;
float humi = 0.0F;
uint8_t result;
void Modbus_task(void *pvParameters) {
HardwareSerial SerialPort(1); // use ESP32 UART1
ModbusMaster node;
result=node.ku8MBInvalidCRC;
/* Serial1 Initialization */
SerialPort.begin(9600, SERIAL_8N1, 33, 32); // RXD1 : 33, TXD1 : 32
// Modbus slave ID 1
node.begin(1, SerialPort);
// Callbacks allow us to configure the RS485 transceiver correctly
// Auto FlowControl - NULL
node.preTransmission(NULL);
node.postTransmission(NULL);
vTaskDelay(2000 / portTICK_PERIOD_MS);
while (1) {
// Request 2 registers starting at 0x0001
result = node.readInputRegisters(0x0001, 2);
if (result == node.ku8MBSuccess) {
temp = float(node.getResponseBuffer(0) / 10.00F);
humi = float(node.getResponseBuffer(1) / 10.00F);
// Get response data from sensor
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void extAntenna() {
if (EXT_ANT_ON) {
pinMode(EXT_ANT, OUTPUT);
digitalWrite(EXT_ANT, HIGH);
delay(100);
}
}
bool startPPPOS() {
PPPOS_start();
unsigned long _tg = millis();
while (!PPPOS_isConnected()) {
DebugSerial.println("ppp Ready...");
u8x8log.print("ppp Ready...\n");
if (millis() > (_tg + 30000)) {
PPPOS_stop();
return false;
}
delay(3000);
}
DebugSerial.println("PPPOS Started");
u8x8log.print("PPPOS Started\n");
return true;
}
void reconnect() {
// Loop until we're reconnected
while (!ppposClient.connected()) {
DebugSerial.print("Attempting TCP connection...");
u8x8log.print("\f"); // \f = form feed: clear the screen
u8x8log.print("Attempting TCP connection...");
// Attempt to connect
if (ppposClient.connect(TCP_SERVER, port)) {
DebugSerial.println("connected");
u8x8log.print("connected\n");
// Once connected, socket data send to server...
ppposClient.println("Device Ready.");
} else {
DebugSerial.print("socket failed");
DebugSerial.println(" try again in 5 seconds");
u8x8log.print("socket failed");
u8x8log.print(" try again in 5 seconds\n");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void setup() {
u8x8.begin();
u8x8.setFont(u8x8_font_chroma48medium8_r);
u8x8log.begin(u8x8, U8LOG_WIDTH, U8LOG_HEIGHT, u8log_buffer);
u8x8log.setRedrawMode(1); // 0: Update screen with newline, 1: Update screen for every char
// put your setup code here, to run once:
M1Serial.begin(SERIAL_BR);
DebugSerial.begin(SERIAL_BR);
DebugSerial.println("TYPE1SC Module Start!!!");
u8x8log.print("TYPE1SC Module Start!!!\n");
extAntenna();
/* TYPE1SC Module Initialization */
if (TYPE1SC.init()) {
DebugSerial.println("TYPE1SC Module Error!!!");
u8x8log.print("TYPE1SC Module Error!!!\n");
}
/* Network Regsistraiton Check */
while (TYPE1SC.canConnect() != 0) {
DebugSerial.println("Network not Ready !!!");
u8x8log.print("Network not Ready!!!\n");
delay(2000);
}
/* Get Time (GMT, (+36/4) ==> Korea +9hour) */
char szTime[32];
if (TYPE1SC.getCCLK(szTime, sizeof(szTime)) == 0) {
DebugSerial.print("Time : ");
DebugSerial.println(szTime);
u8x8log.print("Time : ");
u8x8log.print(szTime);
u8x8log.print("\n");
}
delay(1000);
int rssi, rsrp, rsrq, sinr;
for (int i = 0; i < 3; i++) {
/* Get RSSI */
if (TYPE1SC.getRSSI(&rssi) == 0) {
DebugSerial.println("Get RSSI Data");
u8x8log.print("Get RSSI Data\n");
}
/* Get RSRP */
if (TYPE1SC.getRSRP(&rsrp) == 0) {
DebugSerial.println("Get RSRP Data");
u8x8log.print("Get RSRP Data\n");
}
/* Get RSRQ */
if (TYPE1SC.getRSRQ(&rsrq) == 0) {
DebugSerial.println("Get RSRQ Data");
u8x8log.print("Get RSRQ Data\n");
}
/* Get SINR */
if (TYPE1SC.getSINR(&sinr) == 0) {
DebugSerial.println("Get SINR Data");
u8x8log.print("Get SINR Data\n");
}
delay(1000);
}
if (TYPE1SC.setPPP() == 0) {
DebugSerial.println("PPP mode change");
u8x8log.print("PPP mode change\n");
}
String RF_STATUS = "RSSI: " + String(rssi) + " RSRP:" + String(rsrp) + " RSRQ:" + String(rsrq) + " SINR:" + String(sinr);
DebugSerial.println("[RF_STATUS]");
DebugSerial.println(RF_STATUS);
u8x8log.print("[RF_STATUS]\n");
u8x8log.print(RF_STATUS);
u8x8log.print("\n");
DebugSerial.println("TYPE1SC Module Ready!!!");
u8x8log.print("TYPE1SC Module Ready!!!\n");
PPPOS_init(GSM_TX, GSM_RX, GSM_BR, GSM_SERIAL, ppp_user, ppp_pass);
DebugSerial.println("Starting PPPOS...");
u8x8log.print("Starting PPPOS...\n");
if (startPPPOS()) {
DebugSerial.println("Starting PPPOS... OK");
u8x8log.print("Starting PPPOS... OK\n");
u8x8log.print("\f"); // \f = form feed: clear the screen
} else {
DebugSerial.println("Starting PPPOS... Failed");
u8x8log.print("Starting PPPOS... Failed\n");
}
xTaskCreate(&Modbus_task, "Modbus_task", 2048, NULL, 6, NULL);
}
/* Test Property */
#define TEST_CNT 10
int cnt = 0;
void loop() {
if (PPPOS_isConnected()) {
if (!ppposClient.connected()) {
reconnect();
}
}
if (result == 0) {
DebugSerial.print("Temperature: ");
DebugSerial.println(String(temp, 2));
DebugSerial.print("Humidity: ");
DebugSerial.println(String(humi, 2));
}
// Data Receive with TCP Socket
// Read all the lines of the reply from Server and print them to Serial
while (ppposClient.available() > 0) {
String line = ppposClient.readStringUntil('\r');
DebugSerial.print(line);
u8x8log.print(line);
}
// Data Transfer with TCP Socket
if (cnt < TEST_CNT) {
ppposClient.println(msg);
delay(1000);
} else if (cnt == TEST_CNT) {
DebugSerial.println("socket Test complete.");
u8x8log.print("socket Test complete.\n");
}
cnt++;
}
빨간색 굵은 글자 내용만 주목해서 보시기 바랍니다.
1. Modbus로 동작시키는 부분만 Task로 묶습니다.
2. 태스크를 실행시킬 때 ppposClient 보다 우선순위를 하나 높여 둡니다. (읽는 동안 빼앗기지 않기 위해)
3. 읽기 성공한 값을 loop에서 읽거나 활용합니다.
태스크를 만들 때 주의하실 점은,
태스크 내부는 최대한 간결하게 만들고 print debug 구문은 모두 빼야 합니다. (자원 공유로 문제 일으킴)
동작하는 영상입니다.
ppposClient로 서버 접속 후 서버 데이터 전송, 수신, ModbusRTU 센서 데이터 획득이 동시에 동작됩니다.
10회 데이터 전송 후에는 ModbusRTU 센서 값만 표시합니다.
Murata Type1SC 모듈 구매, 자료 관련 문의
(주)아성코리아
전지만 이사 010-5418-8812 mlcc@asung.com
박상인 수석 010-6556-5405 sipark@asung.com
LTE-CATM1 내장형 모뎀 구매 상담, 외주 개발, 협업 문의
(주)코드주
장병남 대표 010-8965-1323 rooney.jang@codezoo.co.kr
https://www.devicemart.co.kr/goods/view?no=14077527
https://smartstore.naver.com/codezoo/products/8514695472
https://smartstore.naver.com/codezoo/products/7153689192