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ı
| Özellik | Modbus RTU | Modbus ASCII | Modbus TCP |
|---|---|---|---|
| Fiziksel katman | RS-232 / RS-485 | RS-232 / RS-485 | Ethernet (TCP/IP) |
| Kodlama | Binary (ham byte) | ASCII hex karakterleri | Binary (MBAP + PDU) |
| Hata denetimi | CRC-16 | LRC (Longitudinal RC) | TCP checksum (CRC yok) |
| Frame sınırı | 3.5 karakter boşluk | : ile başlar, CR+LF ile biter | Length alanı |
| Hız | Yüksek | Düşük (ASCII 2x byte) | En yüksek (Ethernet) |
| Mesafe | 1200 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ık | En yaygın | Eski sistemler | Modern endüstriyel |
Kullanım alanları
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.
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ür | Kısaltma | Boyut | Erişim | Açıklama |
|---|---|---|---|---|
| Coil | 0x (00001–09999) | 1 bit | Okuma + Yazma | Dijital çıkış. Röle, solenoid, LED. |
| Discrete Input | 1x (10001–19999) | 1 bit | Sadece okuma | Dijital giriş. Buton, limit switch, sensör. |
| Input Register | 3x (30001–39999) | 16-bit word | Sadece okuma | Analog giriş. Sıcaklık, basınç ölçümü. |
| Holding Register | 4x (40001–49999) | 16-bit word | Okuma + Yazma | Konfigü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.
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 Adresi | Protokol Adresi | Açıklama | Birim / Ölçek |
|---|---|---|---|
| 40001 | 0x0000 | Anlık akış hızı | L/min × 10 |
| 40002 | 0x0001 | Toplam akış (high word) | L |
| 40003 | 0x0002 | Toplam akış (low word) | L |
| 40004 | 0x0003 | Sıcaklık | °C × 10 |
| 40005 | 0x0004 | Alarm durumu bit field | bitmask |
| 40010 | 0x0009 | Ölçüm birimi seçimi | 0=L/min, 1=m³/h |
| 40011 | 0x000A | Alarm üst sınırı | L/min × 10 |
| 40100 | 0x0063 | Slave ID | 1–247 |
| 40101 | 0x0064 | Baud rate kodu | 3=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.
/* 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) | İsim | Veri Türü | İşlem |
|---|---|---|---|
| FC01 (0x01) | Read Coils | Coil | 1–2000 coil oku |
| FC02 (0x02) | Read Discrete Inputs | Discrete Input | 1–2000 discrete input oku |
| FC03 (0x03) | Read Holding Registers | Holding Register | 1–125 register oku |
| FC04 (0x04) | Read Input Registers | Input Register | 1–125 register oku |
| FC05 (0x05) | Write Single Coil | Coil | Tek coil yaz (0xFF00=ON, 0x0000=OFF) |
| FC06 (0x06) | Write Single Register | Holding Register | Tek register yaz |
| FC15 (0x0F) | Write Multiple Coils | Coil | 1–1968 coil yaz |
| FC16 (0x10) | Write Multiple Registers | Holding Register | 1–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
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:
01 → Slave ID
83 → FC03 | 0x80 = 0x83 → hata yanıtı
02 → Exception Code: Illegal Data Address
XX XX → CRC-16
Exception code tablosu
| Kod | İsim | Açıklama |
|---|---|---|
| 0x01 | Illegal Function | Slave bu function code'u desteklemiyor. |
| 0x02 | Illegal Data Address | İstenen adres aralığı slave'de mevcut değil. |
| 0x03 | Illegal Data Value | Data alanındaki değer geçersiz (örn. count=0). |
| 0x04 | Server Device Failure | Slave işlemi gerçekleştirirken iç hata oluştu. |
| 0x05 | Acknowledge | Uzun işlem kabul edildi; sonucu için tekrar sor. |
| 0x06 | Server Device Busy | Slave meşgul; isteği tekrar gönder. |
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
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.
/* 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.
#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.
# 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
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.
# 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
Okuma fonksiyonları
Yazma fonksiyonları
Tam çalışan C client örneği
#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ı
/* 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:
errnovemodbus_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.
/* 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
#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;
}
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 tahsisimodbus_mapping_tstruct alanlarına doğrudan erişimselect()ile çoklu TCP client yönetimimodbus_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ı
| Özellik | Modbus RTU | Modbus TCP |
|---|---|---|
| Hata denetimi | CRC-16 (2 byte) | TCP checksum (MBAP yeterli) |
| Frame sınırı | 3.5 karakter sessizlik | Length alanı |
| Bağlantı | Seri port (half-duplex) | TCP socket (full-duplex) |
| Eşzamanlı istek | Hayır (tek master) | Transaction ID ile mümkün |
| Gecikme | Baud rate sınırlı | Ethernet (~ms altı) |
Wireshark ile analiz
# 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
#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ı
# 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.
pip install pymodbus
# Sürüm doğrulama
python3 -c "import pymodbus; print(pymodbus.__version__)"
Synchronous TCP client
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
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
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/ModbusSerialClientfarkları - FC03, FC04, FC06, FC16 için synchronous API
response.isError()ile hata denetimiAsyncModbusTcpClientve 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) | Modicon | Açıklama | Format | Birim |
|---|---|---|---|---|
| 0x0000 | 30001 | L1 Voltaj | float32 | V |
| 0x0002 | 30003 | L2 Voltaj | float32 | V |
| 0x0004 | 30005 | L3 Voltaj | float32 | V |
| 0x0006 | 30007 | L1 Akım | float32 | A |
| 0x0008 | 30009 | L2 Akım | float32 | A |
| 0x000A | 30011 | L3 Akım | float32 | A |
| 0x000C | 30013 | L1 Aktif Güç | float32 | W |
| 0x0034 | 30053 | Toplam Aktif Güç | float32 | W |
| 0x0048 | 30073 | Frekans | float32 | Hz |
| 0x0156 | 30343 | Toplam Aktif Enerji (import) | float32 | kWh |
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.
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
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
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
structile dönüşüm - Retry mekanizmalı Modbus okuma fonksiyonu
- 10 saniyelik periyodik okuma ve CSV loglama
- RS-485 fiziksel bağlantı ve terminasyon