Tüm eğitimler
Rehber Gömülü Linux · Endüstriyel libmodbus · pymodbus

Modbus —
endüstriyel haberleşme protokolü.

PLC ve sensörlerle konuş — RTU/TCP farkı, register haritası, libmodbus ve pymodbus.

00 Modbus nedir

Modbus, 1979'dan bu yana endüstriyel otomasyon dünyasının ortak dili; PLC, sensör, VFD ve enerji sayaçlarını birbirine bağlar.

1979 yılında Modicon (bugünkü Schneider Electric) tarafından kendi PLC ürün ailesi için yayımlanan Modbus, kısa sürede endüstrinin fiili standardı haline geldi. Protokol tasarımı bilinçli olarak basit tutulmuştu: master bir cihaz soru sorar, slave yanıt verir. Başka bir şey yok. Bu sadelik, protokolün royalty-free lisansıyla birleşince donanım üreticilerinin geniş çaplı benimsemesini sağladı. Bugün piyasada on binlerce Modbus destekli cihaz bulunur.

Gömülü Linux mühendisleri açısından Modbus şu anlama gelir: bir Raspberry Pi veya i.MX üzerinde çalışan Linux süreci, RS-485 hattına bağlı bir PLC'den üretim verilerini okuyabilir, bir VFD'nin (Variable Frequency Drive) hız referansını yazabilir ya da bir enerji sayacının akım ölçümlerini SCADA sistemine iletebilir.

Modbus RTU, ASCII ve TCP karşılaştırması

ÖzellikModbus RTUModbus ASCIIModbus TCP
Fiziksel katmanRS-232 / RS-485RS-232 / RS-485Ethernet (TCP/IP)
KodlamaBinary (ham byte)ASCII hex karakterleriBinary (MBAP + PDU)
Hata denetimiCRC-16LRC (Longitudinal RC)TCP checksum (CRC yok)
Frame sınırı3.5 karakter boşluk: ile başlar, CR+LF ile biterLength alanı
HızYüksekDüşük (ASCII 2x byte)En yüksek (Ethernet)
Mesafe1200 m (RS-485)1200 m (RS-485)Ağ topolojisine bağlı
Cihaz sayısı247 (RS-485)247 (RS-485)Sınırsız (IP ağı)
YaygınlıkEn yaygınEski sistemlerModern endüstriyel

Kullanım alanları

PLCProgramlanabilir Lojik Kontrolörler. Üretim hattı I/O verisi okuma/yazma, proses kontrolü.
VFD / SürücüVariable Frequency Drive. Motor hız referansı yazma, akım/frekans okuma.
SensörlerSıcaklık, basınç, nem, akış, titreşim transmitterleri. Ölçüm değerleri input register'dan okunur.
Enerji sayaçlarıVoltaj, akım, güç, enerji (kWh) ölçümleri. SDM630, DTSD1352 gibi modeller yaygındır.
SCADA / HMISupervisory Control. Merkezi izleme yazılımları, endüstriyel dokunmatik paneller.

Neden hâlâ popüler

Modbus'un 40 yılı aşkın süredir hayatta kalmasının üç temel nedeni vardır. Birincisi basitlik: protokolü sıfırdan implemente etmek birkaç saatlik iş; hata ayıklamak için sadece bir seri port monitörü yeterli. İkincisi donanım desteği: piyasadaki neredeyse her endüstriyel cihaz Modbus konuşur; tedarikçi seçiminde protokol kısıtı yoktur. Üçüncüsü royalty-free: lisans ücreti ödenmez, standart serbestçe kullanılır.

NOT

Modbus, güvenlik mekanizması içermez. Kimlik doğrulama, şifreleme veya erişim kontrolü yoktur. Modbus ağlarını güvenlik duvarı arkasında veya izole VLAN'larda tutun. Modbus TCP'yi doğrudan internete açmayın.

Bu bölümde

  • Modbus'un 1979'daki kökeni ve endüstride benimseniş nedenleri
  • RTU, ASCII ve TCP varyantlarının farkları
  • PLC, VFD, sensör ve enerji sayacı kullanım alanları
  • Protokolün güvenlik sınırlamaları

01 Modbus veri modeli

Modbus, cihaz verilerini dört ayrı adres uzayına böler; hangi veriyi okuyup yazabileceğiniz bu uzayın tipine göre belirlenir.

Dört veri türü

TürKısaltmaBoyutErişimAçıklama
Coil0x (00001–09999)1 bitOkuma + YazmaDijital çıkış. Röle, solenoid, LED.
Discrete Input1x (10001–19999)1 bitSadece okumaDijital giriş. Buton, limit switch, sensör.
Input Register3x (30001–39999)16-bit wordSadece okumaAnalog giriş. Sıcaklık, basınç ölçümü.
Holding Register4x (40001–49999)16-bit wordOkuma + YazmaKonfigürasyon, setpoint, ayar değerleri.

Register adresleme: 0-indexed vs 1-indexed

Modbus protokolünün PDU (Protocol Data Unit) katmanında adresler 0-indexedtir: ilk holding register, protokol frame'inde 0x0000 olarak gönderilir. Ancak Modicon'un orijinal dokümantasyonu ve birçok cihaz datasheet'i 1-indexed Modicon notation kullanır: aynı register "40001" olarak gösterilir.

DİKKAT

Datasheet'te "40001" yazıyorsa libmodbus'ta modbus_read_registers(ctx, 0, ...) kullanın. "40001" rakamının "4" öneki veri tipini (holding register), "0001" ise 1-indexed adresi gösterir. libmodbus, pymodbus ve çoğu kütüphane 0-indexed adres bekler.

Örnek register haritası: endüstriyel akış sensörü

Modicon AdresiProtokol AdresiAçıklamaBirim / Ölçek
400010x0000Anlık akış hızıL/min × 10
400020x0001Toplam akış (high word)L
400030x0002Toplam akış (low word)L
400040x0003Sıcaklık°C × 10
400050x0004Alarm durumu bit fieldbitmask
400100x0009Ölçüm birimi seçimi0=L/min, 1=m³/h
400110x000AAlarm üst sınırıL/min × 10
401000x0063Slave ID1–247
401010x0064Baud rate kodu3=9600, 4=19200, 6=115200

32-bit değerler: iki register birleştirme

Modbus register'ları 16-bit genişliğindedir. 32-bit bir değer (örneğin toplam sayaç veya float) iki ardışık register'a bölünür. high word ve low word sırası cihaza göre değişir; datasheet'te "word order" veya "byte order" olarak belirtilir.

combine_registers.c
/* 40002 ve 40003 → 32-bit unsigned integer */
uint16_t regs[2];
modbus_read_registers(ctx, 1, 2, regs); /* addr=1 (0-indexed) */

/* Big-endian word order (high word first) */
uint32_t total_flow = ((uint32_t)regs[0] << 16) | regs[1];

/* Little-endian word order (low word first) */
uint32_t total_flow_le = ((uint32_t)regs[1] << 16) | regs[0];

/* IEEE 754 float — libmodbus yardımcı makrosu */
float value = MODBUS_GET_FLOAT_ABCD(regs); /* big-endian */
float value_le = MODBUS_GET_FLOAT_DCBA(regs); /* little-endian */

Bu bölümde

  • Coil, Discrete Input, Input Register ve Holding Register farkları
  • 0-indexed protokol adresi ile 1-indexed Modicon notation arasındaki fark
  • Gerçek bir cihazın register haritasını okuma
  • İki 16-bit register'dan 32-bit değer oluşturma

02 Function code'lar

Her Modbus isteği bir function code ile başlar; bu kod, master'ın ne yapmak istediğini slave'e bildirir.

Standart function code'lar

FC (hex)İsimVeri Türüİşlem
FC01 (0x01)Read CoilsCoil1–2000 coil oku
FC02 (0x02)Read Discrete InputsDiscrete Input1–2000 discrete input oku
FC03 (0x03)Read Holding RegistersHolding Register1–125 register oku
FC04 (0x04)Read Input RegistersInput Register1–125 register oku
FC05 (0x05)Write Single CoilCoilTek coil yaz (0xFF00=ON, 0x0000=OFF)
FC06 (0x06)Write Single RegisterHolding RegisterTek register yaz
FC15 (0x0F)Write Multiple CoilsCoil1–1968 coil yaz
FC16 (0x10)Write Multiple RegistersHolding Register1–123 register yaz

RTU request/response frame formatı

Modbus RTU frame'i şu alanlardan oluşur:

  ┌──────────┬────────────────┬──────────────────────────────────┬─────────┐
  │ Slave ID │ Function Code  │ Data                             │ CRC-16  │
  │  1 byte  │    1 byte      │  n byte                          │ 2 byte  │
  └──────────┴────────────────┴──────────────────────────────────┴─────────┘

  FC03 Request (slave=1, addr=0, count=3):
  01  03  00 00  00 03  05 CB

  FC03 Response (3 register: 0x1234, 0x0064, 0x0000):
  01  03  06  12 34  00 64  00 00  XX XX
fc03_request.txt — byte analizi
01       → Slave ID (cihaz adresi 1)
03       → Function Code 3: Read Holding Registers
00 00    → Başlangıç adresi: 0x0000 (register 40001)
00 03    → Okunacak register sayısı: 3
05 CB    → CRC-16 (little-endian)

Yanıt:
01       → Slave ID
03       → Function Code (aynı)
06       → Byte count: 3 register × 2 byte = 6
12 34    → Register 0 değeri: 0x1234 = 4660
00 64    → Register 1 değeri: 0x0064 = 100
00 00    → Register 2 değeri: 0x0000 = 0
XX XX    → CRC-16

Hata yanıtı

Slave, isteği işleyemezse function code'un en yüksek bitini set eder (FC | 0x80) ve hata nedenini exception code olarak ekler:

exception_response.txt
01       → Slave ID
83       → FC03 | 0x80 = 0x83 → hata yanıtı
02       → Exception Code: Illegal Data Address
XX XX    → CRC-16

Exception code tablosu

KodİsimAçıklama
0x01Illegal FunctionSlave bu function code'u desteklemiyor.
0x02Illegal Data Addressİstenen adres aralığı slave'de mevcut değil.
0x03Illegal Data ValueData alanındaki değer geçersiz (örn. count=0).
0x04Server Device FailureSlave işlemi gerçekleştirirken iç hata oluştu.
0x05AcknowledgeUzun işlem kabul edildi; sonucu için tekrar sor.
0x06Server Device BusySlave meşgul; isteği tekrar gönder.
NOT

libmodbus, exception code'ları errno aracılığıyla raporlar. EMBXILFUN (0x01), EMBXILADD (0x02), EMBXILVAL (0x03) gibi sabitler modbus.h içinde tanımlıdır. modbus_strerror(errno) insan okunabilir metin döndürür.

Bu bölümde

  • FC01–FC16 arası sekiz standart function code
  • RTU frame yapısı: slave ID, FC, data, CRC-16 byte düzeni
  • Hata yanıtı: FC | 0x80 ve exception code'lar

03 Modbus RTU ve RS-485

RS-485 diferansiyel sinyalleme, Modbus RTU için omurga görevi görür; gürültüye dayanıklı yapısıyla fabrika ortamında 1200 metreye kadar ulaşır.

RS-485 temel özellikleri

Diferansiyel sinyalA ve B hatları arasındaki gerilim farkı veriyi taşır. Ortak mod gürültüsü her iki hatta eşit etki eder, fark sinyalini bozmaz.
Multi-dropAynı çift bükümlü kablo üzerinde 32 standart yük birimi (cihaz) bağlanabilir. 1/8 yük cihazlarla bu sayı 256'ya kadar çıkar.
Maksimum mesafe115200 baud'da 300 m, 9600 baud'da 1200 m. Hat kapasitansı ve kablo kalitesi belirleyicidir.
Half-duplexAynı anda yalnızca bir cihaz veri gönderebilir. Modbus master-slave modeli buna doğal olarak uyar.

Karakter timing ve inter-frame gap

Modbus RTU, frame sınırlarını marker byte yerine zaman boşluklarıyla belirler. İki ardışık karakter arasında 1.5 karakterlik sessizlik inter-character gap'tir ve frame içinde geçersizdir. İki frame arasında en az 3.5 karakterlik sessizlik gerekir.

timing_calc.c
/* 1 karakter = 1 start + 8 data + 1 parity + 1 stop = 11 bit */
/* 3.5 karakter gap hesabı */
int baud = 9600;
double char_time_us = 11.0 * 1e6 / baud;   /* ~1145 µs @ 9600 */
double gap_35_us    = 3.5 * char_time_us;   /* ~4010 µs @ 9600 */

/* Baud rate > 19200 için Modbus spec sabit değer önerir */
/* inter-frame: 1750 µs, inter-character: 750 µs */
double gap_fixed_us = (baud > 19200) ? 1750.0 : gap_35_us;

Linux RS-485 sürücüsü

Modern Linux çekirdeği RS-485 half-duplex yönetimini çekirdek düzeyinde destekler. ioctl() çağrısıyla RS-485 modunu etkinleştirirsiniz; alıcı/verici geçişini sürücü otomatik yönetir.

rs485_setup.c
#include <linux/serial.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <termios.h>

int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);

/* RS-485 modunu yapılandır */
struct serial_rs485 rs485conf = {0};
rs485conf.flags |= SER_RS485_ENABLED;          /* RS-485 etkinleştir */
rs485conf.flags |= SER_RS485_RTS_ON_SEND;      /* TX sırasında RTS yüksek */
rs485conf.flags &= ~SER_RS485_RTS_AFTER_SEND;  /* TX sonrası RTS düşük */
rs485conf.delay_rts_before_send = 0;           /* µs gecikme */
rs485conf.delay_rts_after_send  = 0;

if (ioctl(fd, TIOCSRS485, &rs485conf) < 0) {
    perror("ioctl TIOCSRS485");
    return -1;
}

/* Standart termios ayarları */
struct termios tty = {0};
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
tty.c_cflag = CS8 | CLOCAL | CREAD;  /* 8N1 */
tcsetattr(fd, TCSANOW, &tty);

USB-RS485 adaptör

Geliştirme ortamında FTDI veya CH340 tabanlı USB-RS485 adaptörler kullanılır. Kernel modülü otomatik yüklenir, cihaz /dev/ttyUSB0 veya /dev/ttyUSB1 olarak görünür.

bash
# Cihazı tespit et
dmesg | grep ttyUSB
# usb 1-1.2: ch341-uart converter now attached to ttyUSB0

# İzin ver (kalıcı: kullanıcıyı dialout grubuna ekle)
sudo usermod -aG dialout $USER
ls -la /dev/ttyUSB0

# Manuel test: 9600 baud, raw mod
stty -F /dev/ttyUSB0 9600 raw cs8 -cstopb -parenb

# İlk bağlantı testi: Wireshark veya socat ile yakalama
socat /dev/ttyUSB0,b115200,raw,echo=0 STDOUT | xxd
DİKKAT

RS-485 hattında bitiş dirençleri (termination resistor) kritiktir. Hat her iki ucunda 120 Ω direnç bağlanmalıdır. Eksik terminasyon yansımalara ve veri bozulmalarına yol açar. Kısa mesafelerde (<10 m, düşük baud) terminasyon olmadan çalışabilir ancak uzun hatta mutlaka gereklidir.

Bu bölümde

  • RS-485 diferansiyel sinyal, multi-drop ve half-duplex çalışma prensibi
  • 3.5 ve 1.5 karakter boşluk hesabı, baud rate'e göre değişimi
  • ioctl(TIOCSRS485) ile Linux RS-485 sürücü yapılandırması
  • USB-RS485 adaptör kullanımı ve terminasyon direncinin önemi

04 libmodbus ile C client

libmodbus, C dilinde Modbus RTU ve TCP istemci/sunucu uygulamaları yazmak için olgunlaşmış, Linux'ta yaygın kullanılan açık kaynaklı bir kütüphanedir.

bash
# Debian/Ubuntu
sudo apt install libmodbus-dev

# pkg-config ile derleme
gcc modbus_client.c -o modbus_client $(pkg-config --cflags --libs libmodbus)

Context oluşturma

modbus_new_rtuRTU bağlantısı için context. Parametre sırası: device, baud, parity ('N'/'E'/'O'), data_bit, stop_bit.
modbus_new_tcpTCP bağlantısı için context. IP adresi ve port (standart 502).
modbus_set_slaveSlave ID ayarla. RTU'da 1–247, TCP'de unit ID (genellikle 1 veya 255).
modbus_connectSeri port aç veya TCP bağlantısı kur. Başarıda 0, hata durumunda -1.

Okuma fonksiyonları

modbus_read_registersFC03 — Holding register okuma. (ctx, addr, nb, dest) → okunan register sayısı veya -1.
modbus_read_input_registersFC04 — Input register okuma. Aynı imza, sadece FC04 kullanır.
modbus_read_bitsFC01 — Coil okuma. dest: uint8_t dizi; her eleman 0 veya 1.
modbus_read_input_bitsFC02 — Discrete input okuma.

Yazma fonksiyonları

modbus_write_registerFC06 — Tek holding register yaz. (ctx, addr, value).
modbus_write_registersFC16 — Birden fazla holding register yaz. (ctx, addr, nb, src).
modbus_write_bitFC05 — Tek coil yaz. value: 0 veya 1 (TRUE/FALSE).
modbus_write_bitsFC15 — Birden fazla coil yaz.

Tam çalışan C client örneği

modbus_client.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>

#define SLAVE_ID    1
#define REG_START   0      /* 40001 → 0-indexed */
#define REG_COUNT   10

int main(void)
{
    modbus_t *ctx;
    uint16_t  regs[REG_COUNT];
    int       rc;

    /* RTU context — /dev/ttyUSB0, 115200 baud, No parity, 8 bit, 1 stop */
    ctx = modbus_new_rtu("/dev/ttyUSB0", 115200, 'N', 8, 1);
    if (!ctx) {
        fprintf(stderr, "modbus_new_rtu: %s\n", modbus_strerror(errno));
        return EXIT_FAILURE;
    }

    /* TCP için alternatif:
       ctx = modbus_new_tcp("192.168.1.100", 502); */

    if (modbus_set_slave(ctx, SLAVE_ID) == -1) {
        fprintf(stderr, "set_slave: %s\n", modbus_strerror(errno));
        goto cleanup;
    }

    /* Debug modunu etkinleştir (ham frame'leri basar) */
    modbus_set_debug(ctx, TRUE);

    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "connect: %s\n", modbus_strerror(errno));
        goto cleanup;
    }

    /* FC03: 10 holding register oku */
    rc = modbus_read_registers(ctx, REG_START, REG_COUNT, regs);
    if (rc == -1) {
        fprintf(stderr, "read_registers: %s\n", modbus_strerror(errno));
        goto disconnect;
    }

    printf("Okunan %d register:\n", rc);
    for (int i = 0; i < rc; i++) {
        printf("  [%d] (addr %d) = %d (0x%04X)\n",
               i, REG_START + i, regs[i], regs[i]);
    }

    /* FC06: tek register yaz — örn. setpoint güncelle */
    rc = modbus_write_register(ctx, 9, 850); /* addr=9, value=850 */
    if (rc == -1) {
        fprintf(stderr, "write_register: %s\n", modbus_strerror(errno));
    } else {
        printf("Register 9 → 850 yazıldı\n");
    }

    /* FC01: 8 coil oku */
    uint8_t coils[8];
    rc = modbus_read_bits(ctx, 0, 8, coils);
    if (rc != -1) {
        printf("Coil durumları:");
        for (int i = 0; i < 8; i++) printf(" %d", coils[i]);
        printf("\n");
    }

disconnect:
    modbus_close(ctx);
cleanup:
    modbus_free(ctx);
    return EXIT_SUCCESS;
}

Timeout ayarı

timeout_setup.c
/* Response timeout: 500 ms */
modbus_set_response_timeout(ctx, 0, 500000); /* sec, usec */

/* Byte timeout: birbirini izleyen byte'lar arası max süre */
modbus_set_byte_timeout(ctx, 0, 100000); /* 100 ms */

Bu bölümde

  • libmodbus kurulumu ve pkg-config ile derleme
  • RTU ve TCP context oluşturma farkları
  • FC01, FC03, FC04, FC06 için API fonksiyonları
  • Hata yönetimi: errno ve modbus_strerror()
  • Timeout yapılandırması

05 libmodbus ile C server

libmodbus ile Modbus TCP sunucusu yazmak, bir PLC veya test cihazı simüle etmek ya da gerçek donanım verilerini Modbus üzerinden sunmak için kullanılır.

Modbus mapping — veri deposu

modbus_mapping_new() çağrısı dört veri dizisini tek yapıda tahsis eder. Sunucu bu yapıyı bellekte tutar; client okuma/yazma talepleri bu dizileri günceller.

mapping_alloc.c
/* modbus_mapping_new(nb_bits, nb_input_bits, nb_registers, nb_input_registers) */
modbus_mapping_t *mb_mapping = modbus_mapping_new(
    8,    /* coil sayısı */
    8,    /* discrete input sayısı */
    100,  /* holding register sayısı */
    50    /* input register sayısı */
);

if (!mb_mapping) {
    fprintf(stderr, "mapping_new: %s\n", modbus_strerror(errno));
    exit(EXIT_FAILURE);
}

/* Dizi erişimi */
mb_mapping->tab_bits[0]           = 1;       /* coil 0 = ON */
mb_mapping->tab_input_bits[0]     = 0;       /* discrete input 0 = OFF */
mb_mapping->tab_registers[0]      = 1234;    /* holding register 0 */
mb_mapping->tab_input_registers[0] = 2345;    /* input register 0 */

Tam TCP sunucu örneği — select() ile çoklu client

modbus_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/select.h>
#include <modbus.h>

#define NB_CONNECTIONS  5
#define PORT            502

int main(void)
{
    modbus_t         *ctx;
    modbus_mapping_t *mb_mapping;
    int               server_socket;
    fd_set            refset, rdset;
    int               fdmax;
    uint8_t           query[MODBUS_TCP_MAX_ADU_LENGTH];

    ctx = modbus_new_tcp("0.0.0.0", PORT);
    modbus_set_debug(ctx, FALSE);

    mb_mapping = modbus_mapping_new(8, 8, 100, 50);
    if (!mb_mapping) { perror("mapping"); return 1; }

    /* Başlangıç değerleri */
    mb_mapping->tab_registers[0] = 2350; /* sıcaklık × 10 */
    mb_mapping->tab_registers[1] = 600;  /* nem × 10 */
    mb_mapping->tab_bits[0]       = 1;    /* çalışma durumu */

    server_socket = modbus_tcp_listen(ctx, NB_CONNECTIONS);
    if (server_socket == -1) {
        fprintf(stderr, "listen: %s\n", modbus_strerror(errno));
        return 1;
    }

    printf("Modbus TCP sunucusu port %d'de dinliyor...\n", PORT);

    FD_ZERO(&refset);
    FD_SET(server_socket, &refset);
    fdmax = server_socket;

    int client_sockets[NB_CONNECTIONS];
    for (int i = 0; i < NB_CONNECTIONS; i++) client_sockets[i] = -1;

    for (;;) {
        rdset = refset;
        if (select(fdmax + 1, &rdset, NULL, NULL, NULL) == -1) break;

        for (int fd = 0; fd <= fdmax; fd++) {
            if (!FD_ISSET(fd, &rdset)) continue;

            if (fd == server_socket) {
                /* Yeni client bağlantısı */
                int new_fd = modbus_tcp_accept(ctx, &server_socket);
                if (new_fd != -1) {
                    FD_SET(new_fd, &refset);
                    if (new_fd > fdmax) fdmax = new_fd;
                    printf("Yeni bağlantı: fd=%d\n", new_fd);
                }
            } else {
                /* Mevcut client'tan istek */
                modbus_set_socket(ctx, fd);
                int rc = modbus_receive(ctx, query);
                if (rc > 0) {
                    modbus_reply(ctx, query, rc, mb_mapping);
                } else {
                    printf("Client fd=%d bağlantıyı kapattı\n", fd);
                    close(fd);
                    FD_CLR(fd, &refset);
                    if (fd == fdmax) fdmax--;
                }
            }
        }
    }

    modbus_mapping_free(mb_mapping);
    close(server_socket);
    modbus_free(ctx);
    return 0;
}
NOT

Sunucu mapping'ini başka bir thread'den güncellemek istiyorsanız mutex kullanın. tab_registers dizisi basit bir C dizisi olduğundan thread-safe değildir. Gerçek zamanlı sensör verilerini sunmak için ayrı bir okuma thread'i + pthread_mutex_t kombinasyonu gerekir.

Bu bölümde

  • modbus_mapping_new() ile dört veri dizisi tahsisi
  • modbus_mapping_t struct alanlarına doğrudan erişim
  • select() ile çoklu TCP client yönetimi
  • modbus_receive() + modbus_reply() döngüsü

06 Modbus TCP

Modbus TCP, klasik Modbus PDU'sunu TCP/IP üzerine taşır; CRC'nin yerini MBAP header alır, port 502 IANA tarafından ayrılmıştır.

MBAP Header yapısı

  ┌─────────────────┬─────────────┬──────────┬───────────┬────────────────┐
  │ Transaction ID  │ Protocol ID │  Length  │  Unit ID  │  Modbus PDU    │
  │    2 byte       │   2 byte    │  2 byte  │  1 byte   │  n byte        │
  └─────────────────┴─────────────┴──────────┴───────────┴────────────────┘
  Transaction ID: istek-yanıt eşleştirme (client artar, server aynı değeri döner)
  Protocol ID:    her zaman 0x0000 (Modbus için)
  Length:         Unit ID + PDU byte sayısı
  Unit ID:        RTU'daki slave ID'ye karşılık gelir (genellikle 1 veya 255)

TCP vs RTU farkları

ÖzellikModbus RTUModbus TCP
Hata denetimiCRC-16 (2 byte)TCP checksum (MBAP yeterli)
Frame sınırı3.5 karakter sessizlikLength alanı
BağlantıSeri port (half-duplex)TCP socket (full-duplex)
Eşzamanlı istekHayır (tek master)Transaction ID ile mümkün
GecikmeBaud rate sınırlıEthernet (~ms altı)

Wireshark ile analiz

bash — Wireshark filtreleri
# Port bazlı filtre
tcp.port == 502

# Modbus dissector filtresi (Wireshark otomatik decode eder)
modbus

# Sadece FC03 istekleri
modbus.func_code == 3

# Hata yanıtları
modbus.exception_code

# Komut satırından yakalama
tshark -i eth0 -f "tcp port 502" -T fields \
  -e frame.time -e modbus.func_code -e modbus.reference_num

Timeout ve yeniden bağlanma

tcp_reconnect.c
#include <modbus.h>
#include <unistd.h>

int modbus_connect_with_retry(modbus_t *ctx, int max_retries, int delay_sec)
{
    for (int attempt = 0; attempt < max_retries; attempt++) {
        if (modbus_connect(ctx) == 0) {
            printf("Bağlantı başarılı (deneme %d)\n", attempt + 1);
            return 0;
        }
        fprintf(stderr, "Bağlantı hatası: %s — %d saniye bekleniyor\n",
                modbus_strerror(errno), delay_sec);
        sleep(delay_sec);
        modbus_close(ctx); /* mevcut bağlantıyı temizle */
    }
    return -1;
}

/* Timeout: 1 saniye cevap, 500 ms byte arası */
modbus_set_response_timeout(ctx, 1, 0);
modbus_set_byte_timeout(ctx, 0, 500000);

Firewall ve port yapılandırması

bash — iptables / nftables
# iptables: sadece belirli IP'den port 502'ye izin ver
sudo iptables -A INPUT -p tcp --dport 502 \
  -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 502 -j DROP

# nftables alternatifi
nft add rule ip filter input ip saddr 192.168.1.0/24 \
    tcp dport 502 accept

# Port 1502 kullan (1024 altı root gerektirir)
# libmodbus: modbus_new_tcp("0.0.0.0", 1502)

Bu bölümde

  • MBAP header alanları: transaction ID, protocol ID, length, unit ID
  • TCP ve RTU arasındaki temel farklar
  • Wireshark ile Modbus TCP trafik analizi
  • Yeniden bağlanma mantığı ve firewall yapılandırması

07 pymodbus Python

pymodbus, Python'da Modbus TCP ve RTU client/server geliştirmek için asyncio desteğiyle birlikte gelen kapsamlı bir kütüphanedir.

bash
pip install pymodbus

# Sürüm doğrulama
python3 -c "import pymodbus; print(pymodbus.__version__)"

Synchronous TCP client

modbus_tcp_client.py
from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException

client = ModbusTcpClient(host="192.168.1.100", port=502, timeout=3)

if not client.connect():
    raise ConnectionError("Sunucuya bağlanılamadı")

try:
    # FC03: holding register oku
    response = client.read_holding_registers(address=0, count=10, slave=1)
    if response.isError():
        print(f"Hata: {response}")
    else:
        print(f"Registers: {response.registers}")
        # [4660, 100, 0, 2350, 600, 0, 0, 0, 0, 850]

    # FC04: input register oku
    rr = client.read_input_registers(address=0, count=5, slave=1)
    if not rr.isError():
        print(f"Input registers: {rr.registers}")

    # FC06: tek holding register yaz
    wr = client.write_register(address=9, value=1234, slave=1)
    if wr.isError():
        print(f"Yazma hatası: {wr}")
    else:
        print("Register 9 → 1234 yazıldı")

    # FC16: birden fazla register yaz
    values = [100, 200, 300]
    wrs = client.write_registers(address=0, values=values, slave=1)

except ModbusException as exc:
    print(f"Modbus hatası: {exc}")
finally:
    client.close()

Synchronous RTU client

modbus_rtu_client.py
from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(
    port="/dev/ttyUSB0",
    baudrate=115200,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1
)

client.connect()
rr = client.read_holding_registers(address=0, count=5, slave=1)
print(rr.registers)
client.close()

Async client — asyncio ile

modbus_async_sensor.py
import asyncio
import logging
import time
from pymodbus.client import AsyncModbusTcpClient
from pymodbus.exceptions import ModbusException

logging.basicConfig(level=logging.WARNING)

SLAVE_ID = 1
POLL_INTERVAL = 5.0   # saniye
MAX_READINGS  = 20

async def read_sensor(client: AsyncModbusTcpClient) -> dict:
    """Holding register 0–4'ü oku, sözlük döndür."""
    rr = await client.read_holding_registers(address=0, count=5, slave=SLAVE_ID)
    if rr.isError():
        raise ModbusException(f"Register okuma hatası: {rr}")
    regs = rr.registers
    return {
        "temperature": regs[0] / 10.0,   # °C × 10
        "humidity"   : regs[1] / 10.0,   # % × 10
        "pressure"   : regs[2],           # hPa
        "flow_rate"  : regs[3] / 10.0,   # L/min × 10
        "status_bits": regs[4],
    }

async def main():
    async with AsyncModbusTcpClient(
        host="192.168.1.100", port=502, timeout=2
    ) as client:
        print("Bağlandı. Sensör verileri okunuyor...\n")
        for i in range(MAX_READINGS):
            try:
                data = await read_sensor(client)
                ts = time.strftime("%H:%M:%S")
                print(
                    f"[{ts}] #{i+1:04d}  "
                    f"T={data['temperature']:.1f}°C  "
                    f"H={data['humidity']:.1f}%  "
                    f"P={data['pressure']} hPa  "
                    f"F={data['flow_rate']:.1f} L/min"
                )
            except ModbusException as exc:
                print(f"[{time.strftime('%H:%M:%S')}] Hata: {exc}")
            await asyncio.sleep(POLL_INTERVAL)

asyncio.run(main())

Bu bölümde

  • pymodbus kurulumu ve ModbusTcpClient / ModbusSerialClient farkları
  • FC03, FC04, FC06, FC16 için synchronous API
  • response.isError() ile hata denetimi
  • AsyncModbusTcpClient ve asyncio ile periyodik okuma

08 Pratik: enerji sayacı okuma

SDM630 benzeri üç fazlı enerji sayaçları, Modbus RTU üzerinden voltaj, akım, güç ve enerji verilerini 32-bit float formatında sunar.

SDM630 register haritası (seçilmiş registerlar)

Adres (0-idx)ModiconAçıklamaFormatBirim
0x000030001L1 Voltajfloat32V
0x000230003L2 Voltajfloat32V
0x000430005L3 Voltajfloat32V
0x000630007L1 Akımfloat32A
0x000830009L2 Akımfloat32A
0x000A30011L3 Akımfloat32A
0x000C30013L1 Aktif Güçfloat32W
0x003430053Toplam Aktif Güçfloat32W
0x004830073Frekansfloat32Hz
0x015630343Toplam Aktif Enerji (import)float32kWh

32-bit float: iki 16-bit register birleştirme

SDM630, IEEE 754 single-precision float kullanır. Her değer iki ardışık input register'a big-endian word order ile yazılır: yüksek 16-bit önce, düşük 16-bit sonra.

float_decode.py
import struct

def regs_to_float32(high: int, low: int) -> float:
    """İki 16-bit register'dan IEEE 754 float32 oluştur (big-endian word order)."""
    packed = struct.pack(">HH", high, low)  # big-endian, 2×uint16
    return struct.unpack(">f", packed)[0]     # big-endian float32

# Örnek: register 0x0000=0x4349, 0x0001=0x8000 → 201.5 V
voltage = regs_to_float32(0x4349, 0x8000)
print(f"Voltaj: {voltage:.2f} V")  # 201.50 V

# Little-endian word order için (bazı cihazlar)
def regs_to_float32_le(low: int, high: int) -> float:
    packed = struct.pack(">HH", high, low)
    return struct.unpack(">f", packed)[0]

10 saniyede bir oku, CSV kaydet

energy_meter_logger.py
import struct
import time
import csv
import sys
from datetime import datetime
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException

# Bağlantı parametreleri
PORT      = "/dev/ttyUSB0"
BAUD      = 9600
SLAVE_ID  = 1
POLL_SEC  = 10
CSV_FILE  = "energy_log.csv"
MAX_RETRY = 3

# SDM630 register adresleri (0-indexed input registers)
REG_MAP = {
    "v_l1"       : 0x0000,
    "v_l2"       : 0x0002,
    "v_l3"       : 0x0004,
    "i_l1"       : 0x0006,
    "i_l2"       : 0x0008,
    "i_l3"       : 0x000A,
    "p_total"    : 0x0034,
    "freq"       : 0x0048,
    "energy_kwh" : 0x0156,
}

def regs_to_float32(high: int, low: int) -> float:
    return struct.unpack(">f", struct.pack(">HH", high, low))[0]

def read_float(client, address: int, retries: int = MAX_RETRY) -> float:
    """2 input register oku, float32'e dönüştür. Hata durumunda retry."""
    for attempt in range(retries):
        try:
            rr = client.read_input_registers(address=address, count=2, slave=SLAVE_ID)
            if not rr.isError():
                return regs_to_float32(rr.registers[0], rr.registers[1])
            print(f"  Hata @ 0x{address:04X} (deneme {attempt+1}): {rr}", file=sys.stderr)
        except ModbusException as exc:
            print(f"  ModbusException @ 0x{address:04X}: {exc}", file=sys.stderr)
        time.sleep(0.2)
    return float("nan")

def read_all_registers(client) -> dict:
    data = {}
    for name, addr in REG_MAP.items():
        data[name] = read_float(client, addr)
        time.sleep(0.05)  # frame'ler arası boşluk
    return data

def main():
    client = ModbusSerialClient(
        port=PORT, baudrate=BAUD, bytesize=8, parity="N", stopbits=1, timeout=2
    )
    if not client.connect():
        print("Seri porta bağlanılamadı!", file=sys.stderr)
        sys.exit(1)

    fieldnames = ["timestamp"] + list(REG_MAP.keys())
    with open(CSV_FILE, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()

        print(f"Loglama başladı → {CSV_FILE}  (Ctrl+C ile durdur)")
        try:
            while True:
                data = read_all_registers(client)
                data["timestamp"] = datetime.now().isoformat(timespec="seconds")
                writer.writerow(data)
                f.flush()
                print(
                    f"[{data['timestamp']}] "
                    f"V={data['v_l1']:.1f}V  "
                    f"I={data['i_l1']:.2f}A  "
                    f"P={data['p_total']:.1f}W  "
                    f"E={data['energy_kwh']:.3f}kWh"
                )
                time.sleep(POLL_SEC)
        except KeyboardInterrupt:
            print("\nDurduruldu.")
        finally:
            client.close()

if __name__ == "__main__":
    main()

RS-485 bağlantı adımları

  SDM630                     USB-RS485 adaptör            Linux PC
  ┌──────────┐               ┌────────────────┐           ┌───────────┐
  │  A (D+)  │───────────────│  A             │           │           │
  │  B (D-)  │───────────────│  B             │──USB──────│ /dev/ttyUSB0
  │  GND     │───────────────│  GND           │           │           │
  └──────────┘               └────────────────┘           └───────────┘
       │                                                         │
  120Ω terminasyon (her iki uçta)                          pymodbus client
DİKKAT

Enerji sayaçları şebeke voltajıyla (230 V AC) çalışır. RS-485 bağlantısını yapmadan önce sayacı enerjisiz bırakın. A/B polaritesini ters bağlamak haberleşmeyi engeller ancak cihaza zarar vermez. Yanlış GND bağlantısı potansiyel farkı nedeniyle adaptörü yakabilir.

Bu bölümde

  • SDM630 enerji sayacı register haritası ve adresleme
  • IEEE 754 float32: iki 16-bit register'dan Python struct ile dönüşüm
  • Retry mekanizmalı Modbus okuma fonksiyonu
  • 10 saniyelik periyodik okuma ve CSV loglama
  • RS-485 fiziksel bağlantı ve terminasyon