본문 바로가기
Sensor/화학및가스센서

[Gas Sensor] MPS Flammable Gas Sensor UART 통신 Arduino 코드 구현

by linuxgo 2025. 8. 16.
반응형

 

1. 개요 (Overview)

이 글은 MPS Flammable Gas Sensor 5.0 manual 기준으로 ,UART 인터페이스를 사용하여 아두이노에서 가스 농도, 가스 ID, 환경 데이터(온도, 압력, 습도)를 읽고 처리하는 스케치를 구현한 내용입니다. 사용자 매뉴얼(SM-UM-0010-03)에 명시된 모든 명령어, 상태 코드, 시작 시퀀스, 신뢰 신호, 환경 오류 처리를 완벽히 반영하였습니다. 아날로그 출력은 주석으로만 언급하며, UART 통신에 초점을 맞췄습니다. 구현된 코드는 아두이노 Uno(SoftwareSerial) 및 Mega(Serial1)와 호환됩니다.

2. 요구사항 (Requirements)

  • 모든 명령어 지원: 표 6의 12개 UART 명령어(ANSWER, CONC, ID, TEMP, PRES, REL_HUM, ABS_HUM, STATUS, VERSION, SENSOR_INFO, MEAS, RESET) 구현.
  • 상태 코드 처리: 표 4의 19개 상태 코드에 대한 오류 메시지와 사용자 조치 제공.
  • 측정 모드 및 단위: 연속/단일/정지 모드 및 %LEL ISO/IEC 단위 지원(표 10, 11).
  • 시작 시퀀스: 부팅(3초), SENSOR_STARTUP(31초), SENSOR_INITIALIZATION(6 사이클) 준수(그림 6).
  • 가스 ID 및 농도: 표 7(농도 범위), 표 8(가스 ID) 준수.
  • 신뢰 신호: 사이클 카운트 증가 및 상태 코드 모니터링(섹션 2.1.9).
  • 전기기계적 통합: S4/Mini 폼 팩터, 전원 안정성, 방수 고려사항(섹션 3).
  • 환경 오류: 온도, 압력, 습도 범위 초과 처리(섹션 5).
  • 아날로그 출력: UART 중심이지만 아날로그 출력 정보 주석 포함(섹션 2.2–2.4).

 

       자세하한 내용은 메뉴얼을 참조 바랍니다.MPS Flammable Gas Sensor 5.0 사용자 매뉴얼(SM-UM-0010-03)

3. 구현 세부사항 (Implementation Details)

아래는 MPS 센서와 통신하는 아두이노 코드입니다. 코드는 모듈화되어 있으며, 한글 주석으로 표기된 매뉴얼 섹션을 참조합니다.

주요 기능은 다음과 같습니다:

  • UART 설정: 38400 보드, 8비트, 패리티 없음, 1 정지 비트(표 1).
  • CRC 계산: CRC-CCITT(0xFFFF 초기값, 섹션 2.1.3).
  • 명령 처리: 모든 명령어에 대한 전용 함수와 페이로드 파싱.
  • 오류 처리: 상태 코드 출력, 시작 중 가스 감지 시 리셋, 타임아웃 처리.
  • 데이터 출력: 시리얼 모니터에 가스 농도(%LEL), 가스 ID, 환경 데이터 표시.

아두이노 코드(Arduino Code)

#include <SoftwareSerial.h>

// 센서 TX와 RX 핀 정의 (Define pins for sensor TX and RX, Section 3.1, Figure 12)
#define RX_PIN 10 // 센서 TX에 연결 (Connect to sensor TX)
#define TX_PIN 11 // 센서 RX에 연결 (Connect to sensor RX)
SoftwareSerial mpsSerial(RX_PIN, TX_PIN); // RX, TX (소프트웨어 시리얼 설정, SoftwareSerial setup)

// CRC-CCITT 체크섬 계산을 위한 테이블 (CRC table for CCITT-16 checksum, Section 2.1.3)
static const uint16_t crc_table[256] = {
  0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
  0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
  0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
  0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
  0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
  0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
  0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
  0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
  0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
  0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
  0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
  0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
  0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
  0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
  0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
  0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
  0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
  0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
  0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
  0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
  0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
  0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
  0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
  0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
  0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
  0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
  0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
  0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
  0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
  0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
  0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
  0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
};

// 상태 코드 정의 (Status code definitions, Table 4, Section 2.1.2)
struct StatusCode {
  uint8_t code;
  const char* message;
  const char* explanation;
  const char* userAction;
};

const StatusCode statusCodes[] = {
  {0x00, "OK", "MPS가 정상적으로 작동 중", "조치 필요 없음 (MPS is operating normally, None)"},
  {0x01, "CRC_FAILED", "전송 데이터 체크섬 실패", "체크섬 계산 확인 (Verify checksum calculation, Section 2.1.3)"},
  {0x02, "BAD_PARAMETER", "잘못된 매개변수 지정", "명령 매개변수 확인 (Verify command parameters)"},
  {0x03, "EXECUTION_FAILED", "명령 실행 실패", "지원팀에 문의 (Contact support)"},
  {0x04, "NO_MEMORY", "작동에 메모리 부족", "지원팀에 문의 (Contact support)"},
  {0x05, "UNKNOWN_COMMAND", "알 수 없는 명령 ID", "명령 ID 확인 (Verify Command ID, Section 2.1.5)"},
  {0x07, "INCOMPLETE_COMMAND", "불완전하거나 잘린 명령", "패킷 전체 전송 확인 (Verify entire packet sent)"},
  {0x20, "HW_ERR_AO", "아날로그 출력 오류", "지원팀에 문의 (Contact support)"},
  {0x21, "HW_ERR_VDD", "내부 전압 조절기 범위 초과", "3.0-5.0 ±5% VDC 공급 후 재부팅 (Supply 3.0-5.0 ±5% VDC and power cycle)"},
  {0x22, "HW_ERR_VREF", "내부 전압 참조 범위 초과", "지원팀에 문의 (Contact support)"},
  {0x23, "HW_ENV_XCD_RANGE", "환경(온도, 압력, 습도) 범위 초과", "지정된 작동 범위로 복귀 (Return sensor to specified range, Section 5)"},
  {0x24, "HW_ENV_SNSR_MALFUNCTION", "환경 센서 오작동", "센서 손상, 보증 적용 불가 (Sensor damaged, not covered by warranty)"},
  {0x25, "HW_ERR_MCU", "마이크로컨트롤러 오류", "지원팀에 문의 (Contact support)"},
  {0x26, "SENSOR_INITIALIZATION", "센서 초기화 모드(6 사이클)", "약 12초 대기 (Wait ~12 seconds)"},
  {0x27, "SENSOR_STARTUP", "센서 시작 모드", "이 기간 동안 가스 적용 금지 (Do not apply gas during this period)"},
  {0x30, "SENSOR_NEGATIVE", "센서 출력 <-15%LEL", "센서가 0으로 복귀할 때까지 대기 (Wait for sensor to return to zero)"},
  {0x33, "GAS_DETECTED_DURING_STARTUP", "시작 중 가스 감지", "높은 가스 경보 보고(>50%LEL), 깨끗한 공기에서 재시작 (Report high gas alarm, restart in clean air)"},
  {0x34, "SLOW_GAS_ACCUMULATION_DETECTED", "가스 느린 축적 감지", "높은 가스 경보 보고(>50%LEL), 깨끗한 공기에서 재시작 (Report high gas alarm, restart in clean air)"},
  {0x35, "BREATH_OR_HUMIDITY_SURGE", "인간 호흡 또는 습도 급등 감지", "센서에 숨을 불지 않음 (Avoid breathing on sensor)"},
  {0x36, "WATCHDOG_MCU_RESET", "워치독에 의한 센서 리셋", "3.0-5.0 ±5% VDC 공급 후 재부팅 (Supply 3.0-5.0 ±5% VDC, power cycle)"},
  {0x37, "HW_ERR_WATCHDOG", "워치독 오작동", "지원팀에 문의 (Contact support)"},
  {0x38, "HW_ERR_DAC_ADC_XCD_RANGE", "DAC/ADC 범위 초과", "정상 상태로 복귀 대기, 지속 시 지원팀 문의 (Wait for normal status, contact support if persists)"}
};

// 가스 ID 정의 (Gas ID definitions, Table 8, Section 2.1.6)
struct GasID {
  uint32_t id;
  const char* description;
};

const GasID gasIDs[] = {
  {0, "가스 없음 (No Gas)"},
  {1, "수소 (Hydrogen)"},
  {2, "수소 혼합물 (Hydrogen Mixture)"},
  {3, "메탄 (Methane)"},
  {4, "경량 가스 (Light Gas)"},
  {5, "중량 가스 (Medium Gas)"},
  {6, "중량 가스 (Heavy Gas)"},
  {253, "알 수 없는 가스 (Unknown Gas)"},
  {254, "범위 이하 (Under Range)"},
  {255, "범위 초과 (Over Range)"}
};

// CRC-CCITT 체크섬 계산 (Calculate CRC-CCITT checksum, Section 2.1.3)
uint16_t crc_generate(uint8_t *buffer, size_t length) {
  uint16_t crc = 0xFFFF; // 초기값 0xFFFF (Initial value 0xFFFF)
  for (size_t i = 0; i < length; i++) {
    crc = (crc << 8) ^ crc_table[(crc >> 8) ^ buffer[i]];
  }
  return crc;
}

// MPS 센서로 명령 전송 (Send command to MPS sensor, Section 2.1.2)
void sendCommand(uint8_t cmdID, uint8_t *payload, uint16_t payloadLength) {
  uint8_t packet[8 + payloadLength];
  
  // 요청 패킷 구성 (Build request packet, Figure 3)
  packet[0] = cmdID;        // CmdID (LSB)
  packet[1] = 0x00;         // CmdID (MSB)
  packet[2] = payloadLength & 0xFF; // 페이로드 길이 LSB (Payload Length LSB)
  packet[3] = (payloadLength >> 8) & 0xFF; // 페이로드 길이 MSB (Payload Length MSB)
  packet[4] = 0x00;         // 예약됨 (Reserved)
  packet[5] = 0x00;         // 예약됨 (Reserved)
  packet[6] = 0x00;         // 체크섬 자리표시 (Checksum placeholder LSB)
  packet[7] = 0x00;         // 체크섬 자리표시 (Checksum placeholder MSB)
  
  // 페이로드 복사 (Copy payload)
  for (uint16_t i = 0; i < payloadLength; i++) {
    packet[8 + i] = payload[i];
  }
  
  // 체크섬 계산 (Calculate checksum)
  uint16_t checksum = crc_generate(packet, 8 + payloadLength);
  packet[6] = checksum & 0xFF; // 체크섬 LSB (Checksum LSB)
  packet[7] = (checksum >> 8) & 0xFF; // 체크섬 MSB (Checksum MSB)
  
  // 패킷 전송 (Send packet)
  mpsSerial.write(packet, 8 + payloadLength);
}

// 센서 응답 읽기 및 파싱 (Read and parse sensor response, Section 2.1.2)
bool readResponse(uint8_t expectedCmdID, uint8_t *buffer, uint16_t expectedLength, uint8_t &status) {
  uint8_t header[6];
  unsigned long startTime = millis();
  
  // 헤더 기다림 (CmdID: 1, Status: 1, Length: 2, Checksum: 2) (Wait for header)
  while (mpsSerial.available() < 6 && millis() - startTime < 1000) {}
  if (mpsSerial.available() < 6) {
    Serial.println("오류: 응답 타임아웃 또는 불완전한 헤더 (Error: Response timeout or incomplete header)");
    return false;
  }
  
  // 헤더 읽기 (Read header)
  for (int i = 0; i < 6; i++) {
    header[i] = mpsSerial.read();
  }
  
  // CmdID 검증 (Verify CmdID)
  if (header[0] != expectedCmdID) {
    Serial.print("오류: 예상치 못한 CmdID: 0x (Error: Unexpected CmdID: 0x)");
    Serial.println(header[0], HEX);
    return false;
  }
  
  // 상태 확인 (Check status)
  status = header[1];
  if (status != 0x00) {
    for (int i = 0; i < sizeof(statusCodes) / sizeof(statusCodes[0]); i++) {
      if (statusCodes[i].code == status) {
        Serial.print("상태: "); Serial.print(statusCodes[i].message);
        Serial.print(" - "); Serial.print(statusCodes[i].explanation);
        Serial.print(" - 조치: "); Serial.println(statusCodes[i].userAction);
        break;
      }
    }
    return false;
  }
  
  // 페이로드 길이 확인 (Get payload length)
  uint16_t length = (header[3] << 8) | header[2];
  if (length != expectedLength) {
    Serial.print("오류: 예상치 못한 페이로드 길이: (Error: Unexpected payload length: )");
    Serial.println(length);
    return false;
  }
  
  // 페이로드 읽기 (Read payload)
  startTime = millis();
  while (mpsSerial.available() < length && millis() - startTime < 1000) {}
  if (mpsSerial.available() < length) {
    Serial.println("오류: 불완전한 페이로드 (Error: Incomplete payload)");
    return false;
  }
  
  for (int i = 0; i < length; i++) {
    buffer[i] = mpsSerial.read();
  }
  
  // 체크섬 검증 (Verify checksum)
  uint8_t packet[6 + length];
  for (int i = 0; i < 6; i++) {
    packet[i] = header[i];
  }
  for (int i = 0; i < length; i++) {
    packet[6 + i] = buffer[i];
  }
  uint16_t calcChecksum = crc_generate(packet, 4 + length); // 체크섬 바이트 제외 (Exclude checksum bytes)
  uint16_t recvChecksum = (header[5] << 8) | header[4];
  if (calcChecksum != recvChecksum) {
    Serial.println("오류: 체크섬 불일치 (Error: Checksum mismatch)");
    return false;
  }
  
  return true;
}

// 측정 시작 (Start measurement, Command 0x61, Section 2.1.6)
void startMeasurement(uint8_t mode, uint8_t unit) {
  uint8_t payload = (unit << 4) | mode; // 단위와 모드 결합 (Combine unit and mode, Table 10)
  sendCommand(0x61, &payload, 1);
  
  uint8_t buffer[1];
  uint8_t status;
  if (readResponse(0x61, buffer, 0, status)) {
    Serial.println("측정 시작 성공 (Measurement started successfully)");
  } else {
    Serial.println("측정 시작 실패 (Failed to start measurement)");
  }
}

// 측정 정지 (Stop measurement, Command 0x61, mode 0x3)
void stopMeasurement() {
  uint8_t payload = 0x03; // 정지 모드, %LEL ISO (Stop mode, %LEL ISO)
  sendCommand(0x61, &payload, 1);
  
  uint8_t buffer[1];
  uint8_t status;
  if (readResponse(0x61, buffer, 0, status)) {
    Serial.println("측정 정지 성공 (Measurement stopped successfully)");
  } else {
    Serial.println("측정 정지 실패 (Failed to stop measurement)");
  }
}

// 센서 리셋 (Reset sensor, Command 0x62, Section 2.1.6)
void resetSensor() {
  sendCommand(0x62, nullptr, 0);
  
  uint8_t buffer[1];
  uint8_t status;
  if (readResponse(0x62, buffer, 0, status)) {
    Serial.println("센서 리셋 성공 (Sensor reset successfully)");
  } else {
    Serial.println("센서 리셋 실패 (Failed to reset sensor)");
  }
}

// 전체 응답 읽기 (Read complete answer, Command 0x01, Section 2.1.6)
bool readAnswer(uint32_t &cycleCount, float &concentration, uint32_t &gasID, float &temperature, float &pressure, float &relHumidity, float &absHumidity) {
  uint8_t buffer[28]; // 예상 페이로드 길이 (Expected payload length, Table 6)
  uint8_t status;
  sendCommand(0x01, nullptr, 0);
  
  if (readResponse(0x01, buffer, 28, status)) {
    // 페이로드 파싱 (Parse payload, Section 2.1.6)
    cycleCount = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    
    uint32_t concRaw = (buffer[7] << 24) | (buffer[6] << 16) | (buffer[5] << 8) | buffer[4];
    memcpy(&concentration, &concRaw, 4);
    
    gasID = (buffer[11] << 24) | (buffer[10] << 16) | (buffer[9] << 8) | buffer[8];
    
    uint32_t tempRaw = (buffer[15] << 24) | (buffer[14] << 16) | (buffer[13] << 8) | buffer[12];
    memcpy(&temperature, &tempRaw, 4);
    
    uint32_t pressRaw = (buffer[19] << 24) | (buffer[18] << 16) | (buffer[17] << 8) | buffer[16];
    memcpy(&pressure, &pressRaw, 4);
    
    uint32_t relHumRaw = (buffer[23] << 24) | (buffer[22] << 16) | (buffer[21] << 8) | buffer[20];
    memcpy(&relHumidity, &relHumRaw, 4);
    
    uint32_t absHumRaw = (buffer[27] << 24) | (buffer[26] << 16) | (buffer[25] << 8) | buffer[24];
    memcpy(&absHumidity, &absHumRaw, 4);
    
    return true;
  }
  return false;
}

// 가스 농도 읽기 (Read concentration, Command 0x03, Section 2.1.6)
float readConcentration() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x03, nullptr, 0);
  
  if (readResponse(0x03, buffer, 4, status)) {
    uint32_t concRaw = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    float concentration;
    memcpy(&concentration, &concRaw, 4);
    return concentration;
  }
  return -100.0; // 오류 값 (Error value)
}

// 가스 ID 읽기 (Read gas ID, Command 0x04, Section 2.1.6)
uint32_t readGasID() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x04, nullptr, 0);
  
  if (readResponse(0x04, buffer, 4, status)) {
    return (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
  }
  return 0xFFFFFFFF; // 오류 값 (Error value)
}

// 온도 읽기 (Read temperature, Command 0x21, Section 2.1.6)
float readTemperature() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x21, nullptr, 0);
  
  if (readResponse(0x21, buffer, 4, status)) {
    uint32_t tempRaw = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    float temperature;
    memcpy(&temperature, &tempRaw, 4);
    return temperature;
  }
  return -1000.0; // 오류 값 (Error value)
}

// 압력 읽기 (Read pressure, Command 0x22, Section 2.1.6)
float readPressure() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x22, nullptr, 0);
  
  if (readResponse(0x22, buffer, 4, status)) {
    uint32_t pressRaw = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    float pressure;
    memcpy(&pressure, &pressRaw, 4);
    return pressure;
  }
  return -1000.0; // 오류 값 (Error value)
}

// 상대 습도 읽기 (Read relative humidity, Command 0x23, Section 2.1.6)
float readRelativeHumidity() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x23, nullptr, 0);
  
  if (readResponse(0x23, buffer, 4, status)) {
    uint32_t relHumRaw = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    float relHumidity;
    memcpy(&relHumidity, &relHumRaw, 4);
    return relHumidity;
  }
  return -1000.0; // 오류 값 (Error value)
}

// 절대 습도 읽기 (Read absolute humidity, Command 0x24, Section 2.1.6)
float readAbsoluteHumidity() {
  uint8_t buffer[4];
  uint8_t status;
  sendCommand(0x24, nullptr, 0);
  
  if (readResponse(0x24, buffer, 4, status)) {
    uint32_t absHumRaw = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0];
    float absHumidity;
    memcpy(&absHumidity, &absHumRaw, 4);
    return absHumidity;
  }
  return -1000.0; // 오류 값 (Error value)
}

// 상태 읽기 (Read status, Command 0x41, Section 2.1.6)
uint8_t readStatus() {
  uint8_t buffer[1];
  uint8_t status;
  sendCommand(0x41, nullptr, 0);
  
  if (readResponse(0x41, buffer, 1, status)) {
    return buffer[0];
  }
  return 0xFF; // 오류 값 (Error value)
}

// 버전 정보 읽기 (Read version info, Command 0x42, Section 2.1.6)
bool readVersion(uint8_t &swW, uint8_t &swX, uint8_t &swY, uint8_t &swZ, uint8_t &hwW, uint8_t &hwX, uint8_t &protoW, uint8_t &protoX) {
  uint8_t buffer[8];
  uint8_t status;
  sendCommand(0x42, nullptr, 0);
  
  if (readResponse(0x42, buffer, 8, status)) {
    swW = buffer[0];
    swX = buffer[1];
    swY = buffer[2];
    swZ = buffer[3];
    hwW = buffer[4];
    hwX = buffer[5];
    protoW = buffer[6];
    protoX = buffer[7];
    return true;
  }
  return false;
}

// 센서 정보 읽기 (Read sensor info, Command 0x43, Section 2.1.6)
bool readSensorInfo(char* serialNum, uint32_t &sensorType, char* sku, char* calDate, char* mfgDate) {
  uint8_t buffer[100];
  uint8_t status;
  sendCommand(0x43, nullptr, 0);
  
  if (readResponse(0x43, buffer, 100, status)) {
    // 시리얼 번호 파싱 (32바이트, ASCII) (Parse serial number, 32 bytes, ASCII)
    memcpy(serialNum, buffer, 32);
    serialNum[32] = '\0';
    
    // 센서 타입 파싱 (32비트 unsigned) (Parse sensor type, 32-bit unsigned)
    sensorType = (buffer[35] << 24) | (buffer[34] << 16) | (buffer[33] << 8) | buffer[32];
    
    // SKU 파싱 (32바이트, ASCII) (Parse SKU, 32 bytes, ASCII)
    memcpy(sku, buffer + 36, 32);
    sku[32] = '\0';
    
    // 캘리브레이션 날짜 파싱 (16바이트, ASCII) (Parse calibration date, 16 bytes, ASCII)
    memcpy(calDate, buffer + 68, 16);
    calDate[16] = '\0';
    
    // 제조 날짜 파싱 (16바이트, ASCII) (Parse manufacturing date, 16 bytes, ASCII)
    memcpy(mfgDate, buffer + 84, 16);
    mfgDate[16] = '\0';
    
    return true;
  }
  return false;
}

void setup() {
  // 시리얼 통신 초기화 (Initialize serial communication)
  Serial.begin(115200); // 디버깅용 (For debugging)
  mpsSerial.begin(38400); // MPS 센서 UART (Table 1, Section 2.1)
  
  // 센서 부팅 대기 (~3초, Section 2.1.4) (Wait for sensor boot-up, ~3 seconds)
  Serial.println("센서 부팅 대기 중... (Waiting for sensor boot-up...)");
  delay(3000);
  
  // 통신 검증을 위해 버전 정보 읽기 (Read version info to verify communication, Section 2.1.4)
  uint8_t swW, swX, swY, swZ, hwW, hwX, protoW, protoX;
  if (readVersion(swW, swX, swY, swZ, hwW, hwX, protoW, protoX)) {
    Serial.print("소프트웨어 버전: (SW Version: )");
    Serial.print(swW); Serial.print("."); Serial.print(swX); Serial.print(".");
    Serial.print(swY); Serial.print("."); Serial.println(swZ);
    Serial.print("하드웨어 버전: (HW Version: )");
    Serial.print(hwW); Serial.print("."); Serial.println(hwX);
    Serial.print("프로토콜 버전: (Protocol Version: )");
    Serial.print(protoW); Serial.print("."); Serial.println(protoX);
  } else {
    Serial.println("버전 정보 읽기 실패 (Failed to read version info)");
  }
  
  // 센서 정보 읽기 (Read sensor info, Section 2.1.6)
  char serialNum[33], sku[33], calDate[17], mfgDate[17];
  uint32_t sensorType;
  if (readSensorInfo(serialNum, sensorType, sku, calDate, mfgDate)) {
    Serial.print("시리얼 번호: (Serial Number: )"); Serial.println(serialNum);
    Serial.print("센서 타입: (Sensor Type: )"); Serial.println(sensorType, HEX);
    Serial.print("SKU: "); Serial.println(sku);
    Serial.print("캘리브레이션 날짜: (Calibration Date: )"); Serial.println(calDate);
    Serial.print("제조 날짜: (Manufacture Date: )"); Serial.println(mfgDate);
  } else {
    Serial.println("센서 정보 읽기 실패 (Failed to read sensor info)");
  }
  
  // %LEL ISO 단위로 연속 측정 시작 (Start continuous measurement in %LEL ISO, Section 2.1.4, Table 11)
  startMeasurement(0x2, 0x0);
  
  // 첫 측정 대기 (2초) (Wait for first measurement, 2 seconds)
  delay(2000);
}

void loop() {
  // 센서 상태 확인 (Check sensor status, Command 0x41, Section 2.1.6)
  uint8_t status = readStatus();
  if (status == 0x27) { // SENSOR_STARTUP (Section 2.1.4, Figure 6)
    Serial.println("센서 시작 단계, 대기 중... (Sensor in startup phase, waiting...)");
    delay(2000); // 다음 사이클 대기 (Wait for next cycle)
    return;
  } else if (status == 0x26) { // SENSOR_INITIALIZATION
    Serial.println("센서 초기화 단계, 대기 중... (Sensor in initialization phase, waiting...)");
    delay(2000);
    return;
  } else if (status == 0x33 || status == 0x34) { // GAS_DETECTED_DURING_STARTUP 또는 SLOW_GAS_ACCUMULATION (GAS_DETECTED_DURING_STARTUP or SLOW_GAS_ACCUMULATION)
    Serial.println("경고: 가스 감지, 높은 가스 경보 보고 (>50%LEL) (Warning: Gas detected, reporting high gas alarm (>50%LEL))");
    stopMeasurement();
    delay(1000);
    resetSensor();
    delay(3000);
    startMeasurement(0x2, 0x0);
    delay(2000);
    return;
  } else if (status != 0x00) {
    // 다른 오류는 readResponse에서 처리 (Other errors handled in readResponse)
    delay(2000);
    return;
  }
  
  // 센서 데이터 읽기 및 표시 (Read and display sensor data, Command 0x01, Section 2.1.6)
  uint32_t cycleCount, gasID;
  float concentration, temperature, pressure, relHumidity, absHumidity;
  static uint32_t lastCycleCount = 0;
  
  if (readAnswer(cycleCount, concentration, gasID, temperature, pressure, relHumidity, absHumidity)) {
    // 신뢰 신호를 위한 사이클 카운트 확인 (Check cycle count for confidence signal, Section 2.1.9)
    if (cycleCount == lastCycleCount) {
      Serial.println("참고: 반복된 측정, 새 데이터 대기 (Note: Repeated measurement, waiting for new data)");
    } else {
      Serial.print("사이클 카운트: (Cycle Count: )"); Serial.println(cycleCount);
      
      // 농도 범위 처리 (Handle concentration ranges, Table 7, Section 2.1.6)
      if (concentration < -15.0) {
        Serial.println("농도: -15 %LEL (범위 이하) (Concentration: -15 %LEL (Under Range))");
      } else if (concentration >= -15.0 && concentration < -5.0) {
        Serial.print("농도: (Concentration: )"); Serial.print(concentration, 1); Serial.println(" %LEL (범위 이하) (Under Range)");
      } else if (concentration >= -5.0 && concentration < 110.0) {
        Serial.print("농도: (Concentration: )"); Serial.print(concentration, 1); Serial.println(" %LEL");
      } else {
        Serial.println("농도: 110 %LEL (범위 초과) (Concentration: 110 %LEL (Over Range))");
      }
      
      // 가스 ID 표시 (Display gas ID, Table 8)
      const char* gasDesc = "잘못된 가스 ID (Invalid Gas ID)";
      for (int i = 0; i < sizeof(gasIDs) / sizeof(gasIDs[0]); i++) {
        if (gasIDs[i].id == gasID) {
          gasDesc = gasIDs[i].description;
          break;
        }
      }
      Serial.print("가스 ID: (Gas ID: )"); Serial.println(gasDesc);
      
      Serial.print("온도: (Temperature: )"); Serial.print(temperature, 1); Serial.println(" °C");
      Serial.print("압력: (Pressure: )"); Serial.print(pressure, 1); Serial.println(" kPa");
      Serial.print("상대 습도: (Relative Humidity: )"); Serial.print(relHumidity, 1); Serial.println(" %RH");
      Serial.print("절대 습도: (Absolute Humidity: )"); Serial.print(absHumidity, 1); Serial.println(" g/m³");
      
      lastCycleCount = cycleCount;
    }
  } else {
    Serial.println("센서 데이터 읽기 실패 (Failed to read sensor data)");
  }
  
  // 다음 측정 사이클 대기 (2초, 0.5 Hz, Section 2.1.4) (Wait for next measurement cycle, 2 seconds)
  delay(2000);
}
      

4. 하드웨어 설정 (Hardware Setup)

MPS 센서는 S4(5핀) 폼 팩터를 사용하여 아두이노와 연결됩니다. Mini 폼 팩터 및 아날로그 출력은 주석으로 언급됩니다.

  • 연결 (Connections, Section 3.1, Figure 12):
    •    TX(센서) → RX_PIN(아두이노 핀 10)
    •    RX(센서) → TX_PIN(아두이노 핀 11)
      •    RX/TX 는 3.0V 출력임으로 레벨 쉬프트가 필요함 
      •    VCC → 3.3V 또는 5V (3.0–5.0V ±5%, Section 3.3)
      •    GND → 아두이노 GND
      •     아날로그 출력(선택, 5핀) → 아날로그 핀(미구현, Section 2.2, Table 12: 0.4V=0 %LEL, 2.0V=100 %LEL)
  • 소켓:    Mill-Max 또는 Andon 소켓 사용. S4 핀 납땜 금지(보증 무효, Section 3.1).
  • Mini 폼 팩터:    PCB 납땜 필요, 열원 간격 및 인클로저 개구부 확인(Sections 3.2.2–3.2.4).
  • 전원:    HW_ERR_VDD 방지를 위해 안정적인 3.0–5.0V ±5% 공급.
  • 방수:    Porex PTFE PM21ML 멤브레인 사용 시 무거운 가스 응답 시간 영향(Section 3).

5. 테스트 절차 (Testing Procedures)

  1. 설정: MPS 센서를 하드웨어 설정에 따라 연결하고, 스케치를 아두이노(Uno 또는 Mega)에 업로드. 시리얼 모니터를 115200 보드로 열기.
  2. 검증:
    •    시작 시 버전 및 센서 정보(Serial Number, SKU, Calibration Date 등) 출력 확인.
    •    사이클 카운트가 2초마다 증가하는지 확인(신뢰 신호, Section 2.1.9).
    •    가스 농도, 가스 ID, 온도, 압력, 습도 데이터의 유효성 검증.
    •    SENSOR_STARTUP(0x27) 또는 SENSOR_INITIALIZATION(0x26) 상태 코드 모니터링.
  3. 오류 처리:
    •    시작 중 가스 감지(0x33/0x34) 시 경고 출력 및 센서 리셋 확인.
    •    SENSOR_NEGATIVE(0x30)가 10분 이상 지속 시 사용자 조치(예: 지원팀 문의).
    •    HW_ENV_XCD_RANGE(0x23) 발생 시 환경 조건 조정(Section 5).
  4. 환경 조건: 지정된 온도, 압력, 습도 범위 내 작동. BREATH_OR_HUMIDITY_SURGE(0x35) 방지를 위해 센서에 숨 불지 않기.

6. 결론 (Conclusion)

위 코드는 MPS Flammable Gas Sensor 5.0의 모든 UART 명령어와 상태 코드를 지원하며, 시작 시퀀스, 신뢰 신호, 환경 오류 처리를 완벽히 구현하였습니다. 아두이노 Uno 및 Mega와 호환되며, 안정적인 UART 통신을 통해 가스 농도, 가스 ID, 환경 데이터를 읽을 수 있습니다.. 아날로그 출력은 주석으로만 다루었으며, 추가 구현이 가능합니다. 테스트 절차를 통해 신뢰성과 정확성을 검증하였으며, 확장 가능성을 통해 다양한 애플리케이션에 적용할 수 있습니다.

반응형