การอ่านค่าจากเพาเวอร์มิเตอร์ไฟฟ้าสามเฟส: CJ-3D3YS (ZGCJ)#

บทความนี้กล่าวถึง ตัวอย่างการเขียนโปรแกรม Arduino Sketch และใช้งานบอร์ดไมโครคอนโทรลเลอร์ Arduino ESP32 เพื่ออ่านค่าจากมิเตอร์วัดกำลังไฟฟ้าแบบ 3 เฟส รุ่น CJ-3D3YS (Brand: ZGCJ) โดยใช้โพรโทคอล RS485 / Modbus RTU

Keywords: 3-Phase Power Meter, Arduino Sketch, ESP32-C3, RS485, Modbus RTU


เพาเวอร์มิเตอร์แบบดิจิทัล#

มิเตอร์วัดกำลังไฟฟ้า หรือ "เพาเวอร์มิเตอร์" แบบดิจิทัล (Digital Power Meter) เป็นอุปกรณ์ที่ใช้สำหรับการวัดค่าหรือพารามิเตอร์ต่าง ๆ สำหรับไฟฟ้ากระแสสลับ เช่น

  • ความถี่ (Hz)
  • แรงดันไฟฟ้า (V)
  • กระแสไฟฟ้า (I)
  • ค่าเพาเวอร์แฟคเตอร์ หรือ ค่าตัวประกอบกำลังไฟฟ้า (Power Factor)
  • กำลังไฟฟ้าที่ใช้งานจริง (Real Power: kW)
  • กำลังไฟฟ้าที่ปรากฏ (Apparent Power: kVA)
  • กำลังไฟฟ้ารีแอคทีฟ (Reactive Power: kVAR)
  • พลังงานไฟฟ้าที่ใช้ (Energy: kWh)

มิเตอร์ไฟฟ้าประเภทนี้ จำแนกได้ตามจำนวนเฟสไฟฟ้าที่ต้องการวัดทางไฟฟ้า ได้แก่ มิเตอร์แบบเฟสเดียว (Single-Phase Power Meter) และมิเตอร์แบบสามเฟส (3-Phase Power Meter) นอกจากมีหน้าจอแสดงผลแบบ LCD มิเตอร์ไฟฟ้าแบบดิจิทัลในยุคปัจจุบัน ยังรองรับการเชื่อมต่อด้วยโพรโทคอล Modbus RTU และเชื่อมต่อกับระบบบัส RS485 ได้ด้วย

การต่อวงจรไฟฟ้าเพื่อใช้งานมิเตอร์ มีหลายรูปแบบ โดยดูจากความจำเป็นที่ต้องใช้อุปกรณ์อื่นร่วมด้วยหรือไม่ เช่น การใช้หม้อแปลงแรงดันไฟฟ้า (Potential Transformer: PT) และหม้อแปลงกระแสไฟฟ้า (Current Transformer: CT) เพื่อการลดทอนแรงดันไฟฟ้าด้วย PT หรือลดกระแสไฟฟ้าด้วย CT ให้มีค่าต่ำลงและอยู่ในช่วงเหมาะสมกับมิเตอร์ไฟฟ้า

 


มิเตอร์สามเฟส: CJ-3D3YS#

เพาเวอร์มิเตอร์ที่ได้นำมาทดลองใช้งานเพื่อการสาธิตการเขียนโปรแกรมและการเชื่อมต่อสื่อสารข้อมูล คือ โมเดล CJ-3D3YS (แบรนด์สินค้า ZGCJ)

รูป: ZGCJ CJ-3D3YS

  • เป็นเพาเวอร์มิเตอร์แบบสามเฟส ผลิตในประเทศจีน และจำหน่ายโดยบริษัท Shanghai Chujing Electric Co., Ltd.
  • สามารถเชื่อมต่อกับ RS485 และสื่อสารข้อมูลด้วยโพรโทคอล Modbus RTU
  • รองรับการตั้งค่า Baudrate ได้ในช่วง 1200 ถึง 9600 (สูงสุด)
  • รองรับการวัดแรงดันไฟฟ้าในช่วง 450V (AC) ต่อเฟส และกระแสไฟฟ้าไม่เกิน 5A ต่อเฟส
  • คลาสของความแม่นยำ (Accuracy Class): 0.5
  • ใช้แรงดันไฟเลี้ยง (Power Supply) ได้ในช่วง AC/DC 85V ~ 265V โดยต่อเข้าที่ช่อง (หมายเลข) ต่อไปนี้
    • L (1)
    • N (2)
  • ช่องสำหรับวัดกระแสไฟฟ้าแต่ละเฟส (Current Signal Inputs: Iin)
    • Ia⭑ (4) และ Ia (5) — กระแสไหลเข้าและออกสำหรับเฟส a
    • Ib⭑ (6) และ Ib (7) — กระแสไหลเข้าและออกสำหรับเฟส b
    • Ic⭑ (8) และ Ia (9) — กระแสไหลเข้าและออกสำหรับเฟส c
  • ช่องสำหรับวัดแรงดันไฟฟ้าแต่ละเฟส (Voltage Signal Inputs: Vin)
    • L1 (11) และ N (14) — แรงดันไฟฟ้าเฟส a
    • L2 (12) และ N (14) — แรงดันไฟฟ้าเฟส b
    • L3 (13) และ N (14) — แรงดันไฟฟ้าเฟส c
  • ช่องสัญญาณสำหรับ RS485
    • A (58)
    • B (59)

การต่อวงจรเพื่อใช้งานมิเตอร์ ได้เลือกรูปแบบ 3-phase, 4-wire (ไม่มีการต่อหม้อแปลงภายนอก PT และ CT)

รูป: ช่องเสียบสายไฟที่อยู่ด้านหลังของมิเตอร์ไฟฟ้า

รูป: การเชื่อมต่อสายไฟสำหรับมิเตอร์ไฟฟ้า

 


โค้ดตัวอย่าง#

โค้ด Arduino Sketch ต่อไปนี้ สาธิตการอ่านค่าจากรีจิสเตอร์บางตัวภายในมิเตอร์ไฟฟ้า และใช้สำหรับบอร์ดไมโครคอนโทรลเลอร์ ESP32-C3

ในตัวอย่างนี้ ได้เลือกใช้ขา Tx=GPIO21 และ Rx=GPIO20 สำหรับ วงจร Hardware Serial หมายเลข 0 ของ ESP32-C3 เพื่อนำไปต่อกับโมดูล RS485 Transceiver (ตั้งค่า Baudrate 9600)

ขา A และ B ของโมดูล RS485 Transceiver เชื่อมต่อด้วยสายไฟหนึ่งคู่ไปยังช่องสัญญาณ A และ B ของมิเตอร์ไฟฟ้า

การเชื่อมต่อกับคอมพิวเตอร์ผู้ใช้ จะใช้วิธี USB-CDC เพื่อการอัปโหลด Arduino Sketch และการรับข้อความจากบอร์ดไมโครคอนโทรลเลอร์

รีจิสเตอร์ที่เก็บค่าต่าง ๆ ของมิเตอร์และได้จากการวัดปริมาณทางไฟฟ้ามีขนาด 16 บิต การอ่านค่าจากรีจิสเตอร์ทำได้โดยใช้คำสั่ง Function Code 0x03 ตามโพรโทคอล Modbus RTU

เมื่ออ่านค่าด้วยฟังก์ชัน read_short(...) จะได้ค่าเป็นเลขจำนวนเต็ม 16-bit integer (short) และจะต้องมีการหารด้วย 10.0, 100.0 หรือ 1000.0 แล้วแต่กรณี เพื่อให้ได้ค่าเป็นเลขทศนิยม

ฟังก์ชัน scan_regs(...) สามารถใช้สำหรับการอ่านค่าจากรีจิสเตอร์ในช่วงแอดเดรสที่กำหนด เช่น ในช่วง 0x0010 ถึง 0x0040 และแสดงค่าตัวเลขจำนวนเต็มของแต่ละรีจิสเตอร์

//////////////////////////////////////////////////////////////
// Date: 2024-01-25
// Board: Super-Mini ESP32-C3
// Arduino IDE: v2.2.1
// Arduino ESP32 Core: v3.0.0alpha3
//////////////////////////////////////////////////////////////
#include <HardwareSerial.h>

// Use Hardware Serial 0 or 1
#define HW_SERIAL (0)
HardwareSerial RS485( HW_SERIAL ); 
//#define RS485 Serial0

void setup() {
  Serial.begin(115200); // USB-CDC
  while(!Serial);
  RS485.begin(9600);
  // Set Tx/Rx pins for RS485-serial
  if (HW_SERIAL==0) {
    // Default pins for Serial0: RX=GPIO20, TX=GPIO21
    RS485.setPins( 20 /*RX*/, 21 /*TX*/ );
  } else {
    // Default pins for Serial1: RX=GPIO18, TX=GPIO19
    // Use RX=GPIO10 and TX=GPIO9 for SuperMini ESP32C3 Board
    RS485.setPins( 10 /*RX*/, 9 /*TX*/ );
  }
  RS485.setRxTimeout(1);
  RS485.flush();
  delay(1000);
  Serial.println("\n\n\n\n\n");
  Serial.println("Arduino-ESP32C3 Demo...");
  Serial.println("Power Meter Reading: ZGCJ CJ-3D3YS\n");
  Serial.flush();
}

// Calculate the CRC of a Modbus RTU response.
uint16_t calc_modbus_crc(const byte* data, size_t len) {
  uint16_t crc = 0xFFFF;
  for (size_t i=0; i < len; i++) {
    crc ^= data[i];
    for (int j=0; j < 8; j++) {
      if (crc & 1) {
        crc >>= 1;
        crc ^= 0xA001;
      } else {
        crc >>= 1;
      }
    }
  }
  return crc;
}

// Function to read modbus registers
bool read_regs( byte dev_addr, byte func_code, 
    uint16_t start_reg_addr, size_t num_regs, 
    byte *result, uint32_t delay_ms=1 ) 
{
    byte reg_addr_hi = (start_reg_addr >> 8) & 0xFF;
    byte reg_addr_lo = start_reg_addr & 0xFF;
    byte num_regs_hi = (num_regs >> 8) & 0xFF;
    byte num_regs_lo = num_regs & 0xFF;
    byte req_frame[8] = { dev_addr, func_code,
                          reg_addr_hi, reg_addr_lo,
                          num_regs_hi, num_regs_lo };
    size_t req_frame_len = sizeof(req_frame);
    uint16_t crc = calc_modbus_crc(req_frame, req_frame_len-2 );
    req_frame[req_frame_len-2] = crc & 0xff;
    req_frame[req_frame_len-1] = (crc >> 8) & 0xff;
    RS485.write( req_frame, req_frame_len );
    delay( delay_ms );

    size_t buf_len = 5 +(2*num_regs); 
    byte resp_frame[ buf_len+1 ] = {0};
    size_t resp_frame_len = 0;
#if 1
    resp_frame_len = RS485.readBytes( resp_frame, buf_len );
#else
    uint32_t retries = 100;
    while ( resp_frame_len < buf_len && --retries > 0 ) {
      if ( RS485.available() ) {
        resp_frame[resp_frame_len++] = RS485.read();
      }
      delay(1);
    }
#endif
    if ( resp_frame_len < buf_len ) {
       return false; // error
    }
    uint16_t crc16, expected_crc16;
    byte *crc_bytes = &resp_frame[resp_frame_len-2];
    expected_crc16 = (crc_bytes[1] << 8) | crc_bytes[0];
    crc16 = calc_modbus_crc(resp_frame, resp_frame_len-2);
    if (crc16 == expected_crc16) {
      memcpy( result, resp_frame, resp_frame_len );
      return true; // ok
    } else {
      Serial.print("CRC error: ");
      Serial.print(crc16, HEX);
      Serial.print(" : ");
      Serial.println(expected_crc16, HEX);
      return false; // error
    }
}

bool read_short( 
  uint8_t dev_addr, uint16_t reg_start_addr, uint16_t *value ) 
{
  uint8_t func_code = 0x03;
  byte result[16] = {0}; // frame buffer for response
  if ( read_regs(dev_addr, func_code, reg_start_addr, 1, result) ) {
    uint32_t _value  = (result[3] << 8) | result[4];
    *value = _value;
    return true; // success
  }
  *value = 0;
  return false; // error
}

void scan_regs( uint8_t dev_addr, uint16_t start_reg, uint16_t end_reg ) {
  uint16_t value;
  RS485.setRxTimeout(100);
  for ( uint16_t addr=start_reg; addr <= end_reg; addr += 1 ) {
     if ( read_short(dev_addr, addr, &value) ) {      
       Serial.printf( "RegAddr=0x%04X: %lu\n", addr, value);
     } else {
       Serial.printf( "RegAddr=0x%04X: ------\n", addr );
     }
     delay(100);
  }
}

// The register addresses were found by an experimental method
// (no manual available).
#define UA_REG_ADDR      (0x0025)
#define UB_REG_ADDR      (0x0026)
#define UC_REG_ADDR      (0x0027)
#define UAB_REG_ADDR     (0x0028)
#define UBC_REG_ADDR     (0x0029)
#define UCA_REG_ADDR     (0x002A)
#define IA_REG_ADDR      (0x002B)
#define IB_REG_ADDR      (0x002C)
#define IC_REG_ADDR      (0x002D)
#define TOTAL_P_REG_ADDR (0x0031)
#define TOTAL_Q_REG_ADDR (0x0035)
#define TOTAL_S_REG_ADDR (0x003D)
#define PFA_REG_ADDR     (0x0036)
#define PFB_REG_ADDR     (0x0037)
#define PFC_REG_ADDR     (0x0038)
#define FREQ_REG_ADDR    (0x003E)

typedef struct {
  uint16_t reg_addr;
  uint16_t decimals;
  char name[8];
  char unit[4];
} param_t;

param_t params[]= {
  { UA_REG_ADDR,       10, "Ua",  "V"   }, 
  { UB_REG_ADDR,       10, "Ub",  "V"   },
  { UC_REG_ADDR,       10, "Uc",  "V"   },
  { UAB_REG_ADDR,      10, "Uab", "V"   },
  { UBC_REG_ADDR,      10, "Ubc", "V"   },
  { UCA_REG_ADDR,      10, "Uca", "V"   },
  { IA_REG_ADDR,     1000, "Ia",  "A"   },
  { IB_REG_ADDR,     1000, "Ib",  "B"   },
  { IC_REG_ADDR,     1000, "Ic",  "C"   },
  { PFA_REG_ADDR,    1000, "PFa", "-"   },
  { PFB_REG_ADDR,    1000, "PFb", "-"   },
  { PFC_REG_ADDR,    1000, "PFc", "-"   },
  { TOTAL_P_REG_ADDR, 100, "P",   "W"   },
  { TOTAL_Q_REG_ADDR, 100, "Q",   "VA"  },
  { TOTAL_S_REG_ADDR, 100, "S",   "VAR" },
  { FREQ_REG_ADDR,    100, "Freq", "Hz" },
};

void loop() {  
  uint8_t dev_addr = 1;  
  uint16_t value;

  //scan_regs( dev_addr, 0x0010, 0x0040 );

  int N = sizeof(params)/sizeof(param_t);
  int index = 0;
  int attempts = 1;
  while ( index < N ) {
    param_t *p = &params[index];
    if ( read_short(dev_addr, p->reg_addr, &value) ) {   
      String fmt_str = "RegAddr=0x%04X: %4s, ";
      if (p->decimals==10) {
        fmt_str += "%6.1f";
      } else if (p->decimals==100) {
        fmt_str += "%6.2f";
      } else if (p->decimals==1000) {
        fmt_str += "%6.3f";
      } else {
        fmt_str += "%f";
      }
      fmt_str += " [%s] (#%d)\n";
      Serial.printf( fmt_str.c_str(), 
                     p->reg_addr, p->name,
                     value/((float)p->decimals),
                     p->unit, attempts );
      index++;
      attempts = 1;
    } else {
      attempts++;
    }
    delay(80);
  }
  Serial.println("====================================");
  Serial.flush();
  delay(2000);
}
//////////////////////////////////////////////////////////////

เนื่องจากไม่มีระบบไฟฟ้าสามเฟสให้ทดลอง จึงใช้เพียงหนึ่งเฟสเท่านั้น โดยต่อเข้าที่ L1 และ N และนำไปต่อกับโหลดไฟฟ้า เช่น หลอดไฟ

รูป: ตัวอย่างข้อความเอาต์พุตที่ได้รับจากบอร์ด ESP32-C3 แสดงค่าจากการวัดปริมาณทางไฟฟ้าด้วยเพาเวอร์มิเตอร์

รูป: ตัวอย่างการแสดงค่าที่วัดได้บนจอ LCD (แรงดันไฟฟ้าของแต่ละเฟส)

รูป: บอร์ด ESP32-C3 Super-Mini และโมดูล Serial-to-RS485

รูป: อุปกรณ์ที่ได้นำมาทดลอง

 


กล่าวสรุป#

บทความนี้ได้นำเสนอการทดลองใช้งานเพาเวอร์มิเตอร์สามเฟสแบบดิจิทัล รุ่น ZGCJ J-3D3YS และนำเสนอโค้ดตัวอย่างเพื่อใช้งานบอร์ดไมโครคอนโทรลเลอร์ Arduino ESP32 ให้สามารถอ่านค่าจากมิเตอร์ได้ โดยเชื่อมต่อผ่านบัส RS485 และใช้โพรโทคอล Modbus RTU เพื่อส่งเฟรมข้อมูลสำหรับคำสั่งและข้อมูลตอบกลับ

บทความที่เกี่ยวข้อง

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2024-01-25 | Last Updated: 2024-01-27