MSP430G2553 - MODBUS
概要
MODBUS (Modbus Protocol) は、産業用シリアル通信プロトコルであり、PLCや計測機器、センサ等の制御機器間でデータを送受信するために広く使用されている。
1979年にModicon社 (現在のSchneider Electric社) により開発されたオープンな通信プロトコルであり、産業用ネットワークにおけるデファクトスタンダードとして世界中で採用されている。
下表に、MODBUSプロトコルの特徴を示す。
| 項目 | 説明 |
|---|---|
| マスタ・スレーブ方式 | 1台のマスタ (親機) が複数のスレーブ (子機) を制御する通信方式 |
| アドレス指定 | 各スレーブ機器に1~247のアドレスを割り当てて個別に通信 |
| ファンクションコード | 読み取り、書き込み等の命令を定義 (01h~17h等) |
| オープン仕様 | 仕様が公開されており、誰でも実装可能 |
| 物理層の選択肢 | RS-232、RS-485、TCP/IP等の物理層に対応 |
| 2つのモード | ASCII モードとRTU モードの2種類の伝送モードを提供 |
MODBUS RTU は、バイナリ形式で効率的な通信が可能であり、産業用途で広く使用されている。
MODBUS ASCII は、可読性が高く、デバッグが容易であるが、伝送効率は低い。
RS-485トランシーバを使用することにより、長距離通信とノイズ耐性を実現できる。
終端抵抗とバイアス抵抗を適切に配置して、信号品質を確保することが重要である。
MODBUSは、単純で実装しやすい構造を持つため、組み込みシステムやマイコンでの実装に適しており、
MSP430G2553マイコンでは、内蔵のUSCI_A0 UARTモジュールを使用して、MODBUSプロトコルを設計することができる。
MODBUSプロトコル
MODBUSプロトコルには、伝送モードとして、ASCIIモード と RTUモードの2種類がある。
どちらのモードも同じファンクションコードと通信手順を使用するが、データの符号化方式とフレーム構造が異なる。
MODBUS RTU
MODBUS RTU (Remote Terminal Unit) は、バイナリ形式でデータを送信するモードである。
データ伝送効率が高いため、産業用途で最も広く使用されている。
下表に、MODBUS RTUの特徴を示す。
| フィールド | バイト数 | 説明 |
|---|---|---|
| スレーブアドレス | 1 | 通信先スレーブ機器のアドレス (1~247) |
| ファンクションコード | 1 | 実行する命令コード (01h~17h等) |
| データ | N | 送受信するデータ (可変長) |
| CRC16 | 2 | 巡回冗長検査 (CRC-16/MODBUS) チェックサム |
MODBUS RTUでは、3.5文字時間以上の無通信状態をフレームの区切りとして認識する。
通信速度が9600[bps]、8N1の場合、3.5文字時間は約3.64[ms]となる。
エラー検出には、CRC-16/MODBUS アルゴリズムが使用される。
CRC16は、多項式 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x8005} (x^{16} + x^{15} + x^{2} + 1)}
を使用して、初期値 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0xFFFF}}
から計算される。
例 :
スレーブアドレス : 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x01}}
ファンクションコード : 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x03}}
開始アドレス : 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x0000}}
レジスタ数 : 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x0001}}
のフレーム
CRC16は、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x84}}
、 となる。
計算過程 :
初期値 から開始して、各バイトとXORを取り、8回のシフトと条件付きXOR () を繰り返すことにより、
最終的なCRC値 が得られる。
これをリトルエンディアン形式で、、 として送信する。
MODBUS ASCII
MODBUS ASCII は、データをASCII文字列として送信するモードである。
可読性が高く、デバッグが容易であるため、開発時やトラブルシューティングで有用である。
下表に、MODBUS ASCIIの特徴を示す。
| フィールド | 文字数 | 説明 |
|---|---|---|
| スタート文字 | 1 | ':' (コロン、0x3A) でフレーム開始を示す |
| スレーブアドレス | 2 | アドレスを2桁のASCII 16進数で表現 ('01'~'F7') |
| ファンクションコード | 2 | 命令コードを2桁のASCII 16進数で表現 |
| データ | 2N | データを2桁ずつのASCII 16進数で表現 (可変長) |
| LRC | 2 | 縦方向冗長検査 (LRC) チェックサム |
| エンド文字 | 2 | CR (0x0D) とLF (0x0A) でフレーム終了を示す |
MODBUS ASCIIでは、各バイトのデータを2文字のASCII 16進数文字 ('0'~'9'、'A'~'F') に変換して送信する。
例えば、0x5Aは、'5' と 'A' の2文字として送信される。
エラー検出には、LRC (Longitudinal Redundancy Check) が使用される。
LRCは、スレーブアドレスからデータまでの全バイトを加算して、2の補数を取ることにより計算される。
計算式は、 であり、8ビット演算では、 となる。
例 : スレーブアドレス : ファンクションコード : 開始アドレス : 、 レジスタ数 : 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x00}} 、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x01}} のフレーム 合計は、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x01} + \mbox{0x03} + \mbox{0x00} + \mbox{0x00} + \mbox{0x00} + \mbox{0x01} = \mbox{0x05}} となる。 この合計値の2の補数を取ると、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{LRC} = (\sim\mbox{0x05} + 1) \& \mbox{0xFF} = \mbox{0xFA} + 1 = \mbox{0xFB}} となり、 最終的なLRC値は、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0xFB}} である。
MODBUS RTU と MODBUS ASCII の比較
MODBUS RTUの方が伝送効率とエラー検出精度の点で優れているため、産業用途で広く使用されている。
MODBUS ASCIIは、開発初期段階やデバッグ時に有用であるが、実運用ではRTUモードを使用することが推奨される。
| 項目 | MODBUS RTU | MODBUS ASCII |
|---|---|---|
| データ符号化 | バイナリ形式 | ASCII 16進数文字列 |
| 伝送効率 | 高い (バイナリ) | 低い (2倍のデータ量) |
| エラー検出 | CRC-16 (高精度) | LRC (低精度) |
| フレーム区切り | 3.5文字時間の無通信 | スタート文字 ':' とエンド文字 'CR+LF' |
| 可読性 | 低い | 高い (デバッグが容易) |
| 使用頻度 | 非常に高い | 限定的 |
| 通信速度 | 9600~115200[bps]が一般的 | 9600~19200[bps]が一般的 |
ハードウェア構成
MSP430G2553でMODBUSを使用する場合、物理層としてRS-485を使用することが一般的である。
RS-485は、差動信号方式を採用しており、長距離通信 (最大1200[m]) と ノイズ耐性 に優れている。


必要なハードウェア
下表に、MSP430G2553でMODBUS通信を行うために必要なハードウェアコンポーネントを示す。
| コンポーネント | 型番例 | 説明 |
|---|---|---|
| マイコン | MSP430G2553 | TI社 16ビットRISCマイコン |
| RS-485トランシーバ | MAX485、MAX3485 | 半二重通信用トランシーバIC |
| RS-485トランシーバ | ADM485、SN75176 | MAX485互換の代替品 |
| 終端抵抗 | 120[Ω] | バスの両端に配置 |
| バイアス抵抗 | 560[Ω]〜20[kΩ] × 2 | フェイルセーフバイアス用 |
| 保護素子 | TVSダイオード | サージ保護用 (オプション) |
RS-485トランシーバとの接続
下表に、MSP430G2553とMAX3485を使用したピン接続例を示す。
| MSP430G2553 | 機能 | MAX3485 | ピン番号 |
|---|---|---|---|
| P1.1 | UART TXD (送信) | DI (Driver Input) | Pin 4 |
| P1.2 | UART RXD (受信) | RO (Receiver Output) | Pin 1 |
| P1.3 | GPIO (方向制御) | DE (Driver Enable) | Pin 3 |
| P1.3 | GPIO (方向制御) | /RE (Receiver Enable) | Pin 2 |
| GND | グラウンド | GND | Pin 5 |
| - | 電源 | VCC 5[V] | Pin 8 |
| - | - | A (非反転出力) | RS-485バス |
| - | - | B (反転出力) | RS-485バス |
MAX485のDE (Driver Enable) と /RE (Receiver Enable) ピンは、共通の制御線 (P1.3) に接続する。
P1.3をHIGHにすると送信モードとなり、LOWにすると受信モードとなる。
RS-485バスには、通信線AとBの2本の差動信号線を使用する。
バスの両端には、120[Ω]の終端抵抗を配置して、信号の反射を防止する必要がある。
バイアス抵抗とフェイルセーフ
RS-485バスには、通信していない時の不定状態を防ぐために、バイアス抵抗を追加することが推奨される。
バイアス抵抗により、通信していない時でも差動電圧が発生して、受信側が確定した論理レベルを認識できるようになる。
これにより、誤動作を防止することができる。
| 抵抗値 | 接続先 | 目的 |
|---|---|---|
| 560[Ω] | VCC → A端子 (プルアップ) | アイドル時にA端子を高電位に保持 |
| 560[Ω] | B端子 → GND (プルダウン) | アイドル時にB端子を低電位に保持 |
UART設定
MSP430G2553のUSCI_A0 UARTモジュールを使用して、MODBUS通信を行うための設定手順を示す。
クロック設定
UARTの動作には、安定したクロックソースが必要である。
通常は、SMCLK (Sub-Main Clock) をクロックソースとして使用する。
以下の例では、MSP430G2553のキャリブレーション済みDCOを使用して、1[MHz]のクロックを生成している。
// クロック設定 (1[MHz])
DCOCTL = 0; // DCOをリセット
BCSCTL1 = CALBC1_1MHZ; // 1[MHz]キャリブレーション値を設定
DCOCTL = CALDCO_1MHZ; // 1[MHz]キャリブレーション値を設定
UART初期化
USCI_A0 UARTモジュールの初期化手順を示す。
MODBUSでは、8N1 (8ビットデータ、パリティなし、1ストップビット) が標準的な設定である。
// UART初期化 (9600[bps]、8N1)
void UART_Init(void)
{
// GPIO設定: P1.1とP1.2をUART機能に割り当て
P1SEL |= BIT1 + BIT2; // P1.1 = TXD, P1.2 = RXD
P1SEL2 |= BIT1 + BIT2;
// USCI_A0をリセット状態にする
UCA0CTL1 |= UCSWRST;
// UART設定
UCA0CTL1 |= UCSSEL_2; // SMCLK (1[MHz]) をクロックソースに選択
// ボーレート設定 (9600[bps])
// N = SMCLK / Baudrate = 1000000 / 9600 = 104.166
UCA0BR0 = 104; // 下位バイト
UCA0BR1 = 0; // 上位バイト
UCA0MCTL = UCBRS0; // モジュレーションステージ (0x01)
// データフォーマット設定 (8N1)
UCA0CTL0 &= ~UCPEN; // パリティなし
UCA0CTL0 &= ~UC7BIT; // 8ビットデータ
UCA0CTL0 &= ~UCSPB; // 1ストップビット
// USCI_A0を動作状態にする
UCA0CTL1 &= ~UCSWRST;
// 受信割り込み有効化
IE2 |= UCA0RXIE;
}
ボーレート設定
下表に、MODBUSで使用される一般的なボーレートと、1[MHz] SMCLK時の設定値を示す。
ボーレート設定の計算式は、構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{N} = \dfrac{\mbox{SMCLK}}{\mbox{Baudrate}}}
である。
UCAxBR0とUCAxBR1は、Nの整数部を格納して、UCAxMCTLは、小数部の補正を行う。
| ボーレート | UCAxBR0 | UCAxBR1 | UCAxMCTL | 備考 |
|---|---|---|---|---|
| 9600[bps] | 104 | 0 | UCBRS0 (0x01) | 標準的な設定 |
| 19200[bps] | 52 | 0 | UCBRS0 (0x01) | 2倍速 |
| 38400[bps] | 26 | 0 | UCBRS0 (0x01) | 4倍速 |
| 115200[bps] | 8 | 0 | UCBRS7+UCBRS6 (0xC0) | 高速通信 |
RS-485方向制御
RS-485は半二重通信であるため、送信と受信を切り替える方向制御が必要である。
以下の例では、P1.3を使用して、MAX485のDE/REピンを制御している。
// RS-485方向制御用GPIO設定
#define RS485_DIR_BIT BIT3 // P1.3を方向制御に使用
#define RS485_TX_MODE() (P1OUT |= RS485_DIR_BIT) // 送信モード (DE=1, /RE=1)
#define RS485_RX_MODE() (P1OUT &= ~RS485_DIR_BIT) // 受信モード (DE=0, /RE=0)
void RS485_GPIO_Init(void)
{
P1DIR |= RS485_DIR_BIT; // P1.3を出力に設定
RS485_RX_MODE(); // 初期状態は受信モード
}
送信前にRS485_TX_MODE()を呼び出して送信モードに切り替えて、送信完了後にRS485_RX_MODE()を呼び出して受信モードに戻す必要がある。
MODBUS RTU実装
MODBUS RTUの実装では、バイナリ形式のデータ送受信とCRC-16エラー検出が必要である。
CRC-16/MODBUS計算
MODBUS RTUでは、CRC-16/MODBUSアルゴリズムを使用してエラー検出を行う。
CRC-16は、多項式 構文解析に失敗 (SVG (ブラウザーのプラグインで MathML を有効にできます): サーバー「https://wikimedia.org/api/rest_v1/」から無効な応答 ("Math extension cannot connect to Restbase."):): {\displaystyle \mbox{0x8005} (x^{16} + x^{15} + x^{2} + 1)}
を使用して計算される。
CRC-16計算の実装例を以下に示す。
/*
* CRC-16/MODBUS計算関数
* 入力: data - データ配列, length - データ長
* 出力: CRC-16値 (16ビット)
*/
unsigned int CRC16_MODBUS(unsigned char *data, unsigned int length)
{
unsigned int crc = 0xFFFF; // 初期値は0xFFFF
unsigned int i, j;
for (i = 0; i < length; i++) {
crc ^= (unsigned int)data[i]; // データとXOR
for (j = 0; j < 8; j++) {
if (crc & 0x0001) { // 最下位ビットが1の場合
crc >>= 1; // 右シフト
crc ^= 0xA001; // 多項式とXOR (ビット反転版)
}
else {
crc >>= 1; // 右シフトのみ
}
}
}
return crc;
}
計算されたCRC-16値は、下位バイトを先に送信するリトルエンディアン形式でフレームに付加する。
MODBUS RTUフレーム送信
MODBUS RTUフレームの送信関数の実装例を以下に示す。
/*
* 1バイト送信関数
*/
void UART_SendByte(unsigned char data)
{
while (!(IFG2 & UCA0TXIFG)); // 送信バッファが空になるまで待機
UCA0TXBUF = data; // データを送信
}
/*
* MODBUS RTUフレーム送信関数
* 入力 : slave_addr - スレーブアドレス
* func_code - ファンクションコード
* data - データ配列
* data_length - データ長
*/
void MODBUS_RTU_SendFrame(unsigned char slave_addr, unsigned char func_code, unsigned char *data, unsigned int data_length)
{
unsigned char frame[256];
unsigned int frame_length = 0;
unsigned int crc;
unsigned int i;
// フレーム構築
frame[frame_length++] = slave_addr; // スレーブアドレス
frame[frame_length++] = func_code; // ファンクションコード
for (i = 0; i < data_length; i++) {
frame[frame_length++] = data[i]; // データ部
}
// CRC-16計算
crc = CRC16_MODBUS(frame, frame_length);
frame[frame_length++] = (unsigned char)(crc & 0xFF); // CRC下位バイト
frame[frame_length++] = (unsigned char)((crc >> 8) & 0xFF); // CRC上位バイト
// 送信モードに切替
RS485_TX_MODE();
__delay_cycles(100); // 切替待ち時間
// フレーム送信
for (i = 0; i < frame_length; i++) {
UART_SendByte(frame[i]);
}
// 送信完了待ち
while (UCA0STAT & UCBUSY); // UART送信完了待ち
__delay_cycles(1000); // 最終ビット送信完了待ち
// 受信モードに戻す
RS485_RX_MODE();
}
MODBUS RTUフレーム受信
MODBUS RTUフレームの受信には、3.5文字時間のタイムアウト検出が必要である。
以下の例では、タイマを使用して、フレーム区切りを判定している。
// 受信バッファと変数
#define RX_BUFFER_SIZE 256
unsigned char rx_buffer[RX_BUFFER_SIZE];
unsigned int rx_index = 0;
volatile unsigned char frame_complete = 0;
/*
* Timer_A0初期化 (3.5文字時間タイムアウト検出用)
* 9600[bps]、8N1の場合、1文字時間 = 1.04[ms]
* 3.5文字時間 = 3.64[ms]
*/
void Timer_Init(void)
{
TA0CTL = TASSEL_2 + MC_0 + TACLR; // SMCLK、停止モード、クリア
TA0CCR0 = 3640; // 3.64[ms] (1[MHz] SMCLK時)
TA0CCTL0 = CCIE; // タイマ割り込み有効
}
/*
* UART受信割り込みハンドラ
*/
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void)
{
unsigned char rx_data = UCA0RXBUF; // 受信データを読み込み
if (rx_index < RX_BUFFER_SIZE) {
rx_buffer[rx_index++] = rx_data; // バッファに格納
}
// タイマをリスタート (3.5文字時間タイムアウトをリセット)
TA0CTL |= TACLR; // タイマクリア
TA0CTL |= MC_1; // アップモードで開始
}
/*
* Timer_A0割り込みハンドラ (3.5文字時間タイムアウト)
*/
#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer0_A0_ISR(void)
{
TA0CTL &= ~MC_3; // タイマ停止
if (rx_index > 0) {
frame_complete = 1; // フレーム受信完了フラグをセット
}
}
/*
* MODBUS RTUフレーム受信処理
*/
void MODBUS_RTU_ProcessFrame(void)
{
unsigned int crc_calculated;
unsigned int crc_received;
unsigned char slave_addr;
unsigned char func_code;
if (!frame_complete || rx_index < 4) {
return; // フレーム未完了または短すぎる
}
// CRC検証
crc_calculated = CRC16_MODBUS(rx_buffer, rx_index - 2);
crc_received = (unsigned int)rx_buffer[rx_index - 2] |
((unsigned int)rx_buffer[rx_index - 1] << 8);
if (crc_calculated != crc_received) {
// CRCエラー
rx_index = 0;
frame_complete = 0;
return;
}
// フレーム解析
slave_addr = rx_buffer[0];
func_code = rx_buffer[1];
// 自分宛のフレームかチェック
if (slave_addr == MY_SLAVE_ADDRESS) {
// ファンクションコードに応じた処理
switch (func_code) {
case 0x03: // Read Holding Registers
// 処理定義
break;
case 0x06: // Write Single Register
// 処理定義
break;
// その他のファンクションコード
}
}
// バッファクリア
rx_index = 0;
frame_complete = 0;
}
ファンクションコード例
下表に、MODBUSで使用される主なファンクションコードを示す。
| コード | 名称 | 説明 |
|---|---|---|
| 0x01 | Read Coils | コイル (ビット単位) の読み取り |
| 0x02 | Read Discrete Inputs | 離散入力 (ビット単位) の読み取り |
| 0x03 | Read Holding Registers | 保持レジスタ (16ビット単位) の読み取り |
| 0x04 | Read Input Registers | 入力レジスタ (16ビット単位) の読み取り |
| 0x05 | Write Single Coil | 単一コイルへの書き込み |
| 0x06 | Write Single Register | 単一レジスタへの書き込み |
| 0x0F | Write Multiple Coils | 複数コイルへの書き込み |
| 0x10 | Write Multiple Registers | 複数レジスタへの書き込み |
MODBUS ASCIIの使用
MODBUS ASCIIを使用した通信制御では、ASCII文字列への変換とLRCエラー検出が必要である。
LRC (縦方向冗長検査) 計算
MODBUS ASCIIでは、LRC (Longitudinal Redundancy Check) を使用してエラー検出を行う。
LRCは、全バイトを加算して、2の補数を取ることにより計算される。
以下の例では、LRC計算を定義している。
/*
* LRC計算関数
* 入力 : data - データ配列, length - データ長
* 出力 : LRC値 (8ビット)
*/
unsigned char LRC_Calculate(unsigned char *data, unsigned int length)
{
unsigned char lrc = 0;
unsigned int i;
for (i = 0; i < length; i++) {
lrc += data[i]; // 全バイトを加算
}
lrc = (~lrc) + 1; // 2の補数を取る
return lrc;
}
16進数からASCII変換
MODBUS ASCIIでは、1バイトのデータを2文字のASCII 16進数文字に変換する必要がある。
以下の例では、変換関数を定義している。
/*
* 16進数1桁をASCII文字に変換
*/
unsigned char HexToASCII(unsigned char hex)
{
if (hex <= 9) {
return '0' + hex; // '0'~'9'
}
else {
return 'A' + (hex - 10); // 'A'~'F'
}
}
/*
* ASCII文字を16進数1桁に変換
*/
unsigned char ASCIIToHex(unsigned char ascii)
{
if (ascii >= '0' && ascii <= '9') {
return ascii - '0'; // '0'~'9'
}
else if (ascii >= 'A' && ascii <= 'F') {
return ascii - 'A' + 10; // 'A'~'F'
}
else if (ascii >= 'a' && ascii <= 'f') {
return ascii - 'a' + 10; // 'a'~'f'
}
else {
return 0; // 無効な文字
}
}
MODBUS ASCIIフレーム送信
以下の例では、MODBUS ASCIIフレームの送信関数を定義している。
/*
* MODBUS ASCIIフレーム送信関数
*/
void MODBUS_ASCII_SendFrame(unsigned char slave_addr, unsigned char func_code, unsigned char *data, unsigned int data_length)
{
unsigned char binary_frame[128];
unsigned int binary_length = 0;
unsigned char lrc;
unsigned int i;
// バイナリフレーム構築
binary_frame[binary_length++] = slave_addr;
binary_frame[binary_length++] = func_code;
for (i = 0; i < data_length; i++) {
binary_frame[binary_length++] = data[i];
}
// LRC計算
lrc = LRC_Calculate(binary_frame, binary_length);
binary_frame[binary_length++] = lrc;
// 送信モードに切替
RS485_TX_MODE();
__delay_cycles(100);
// スタート文字 ':' を送信
UART_SendByte(':');
// バイナリデータをASCII 16進数に変換して送信
for (i = 0; i < binary_length; i++) {
UART_SendByte(HexToASCII((binary_frame[i] >> 4) & 0x0F)); // 上位4ビット
UART_SendByte(HexToASCII(binary_frame[i] & 0x0F)); // 下位4ビット
}
// エンド文字 'CR + LF' を送信
UART_SendByte(0x0D); // CR
UART_SendByte(0x0A); // LF
// 送信完了待ち
while (UCA0STAT & UCBUSY);
__delay_cycles(1000);
// 受信モードに戻す
RS485_RX_MODE();
}
MODBUS ASCIIフレーム受信
以下の例では、MODBUS ASCIIフレームの受信処理を定義している。
// ASCII受信バッファと変数
unsigned char ascii_rx_buffer[512];
unsigned int ascii_rx_index = 0;
volatile unsigned char ascii_frame_start = 0;
volatile unsigned char ascii_frame_complete = 0;
/*
* UART受信割り込みハンドラ (ASCII用)
*/
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR_ASCII(void)
{
unsigned char rx_data = UCA0RXBUF;
if (rx_data == ':') {
// スタート文字検出
ascii_frame_start = 1;
ascii_rx_index = 0;
}
else if (ascii_frame_start) {
if (rx_data == 0x0A) {
// LF検出
ascii_frame_complete = 1;
ascii_frame_start = 0;
}
else if (rx_data != 0x0D && ascii_rx_index < 512) {
// CR以外
ascii_rx_buffer[ascii_rx_index++] = rx_data;
}
}
}
/*
* MODBUS ASCIIフレーム処理
*/
void MODBUS_ASCII_ProcessFrame(void)
{
unsigned char binary_data[256];
unsigned int binary_length = 0;
unsigned char lrc_calculated;
unsigned char lrc_received;
unsigned int i;
if (!ascii_frame_complete || ascii_rx_index < 6) {
return;
}
// ASCII 16進数からバイナリに変換
for (i = 0; i < ascii_rx_index; i += 2) {
binary_data[binary_length] = (ASCIIToHex(ascii_rx_buffer[i]) << 4) | ASCIIToHex(ascii_rx_buffer[i + 1]);
binary_length++;
}
// LRC検証
lrc_received = binary_data[binary_length - 1];
lrc_calculated = LRC_Calculate(binary_data, binary_length - 1);
if (lrc_calculated != lrc_received) {
// LRCエラー
ascii_rx_index = 0;
ascii_frame_complete = 0;
return;
}
// フレーム解析
unsigned char slave_addr = binary_data[0];
unsigned char func_code = binary_data[1];
// なんらかの処理
// ...略
// バッファクリア
ascii_rx_index = 0;
ascii_frame_complete = 0;
}
サンプルコード
以下の例では、MSP430G2553でMODBUS RTUスレーブを使用して通信制御している。
MODBUS RTU スレーブとして動作して、ファンクションコード 0x03 (Read Holding Registers) と 0x06 (Write Single Register) に対応している。
10個の保持レジスタを持ち、マスタからの要求に応答する。
/*
* MSP430G2553 MODBUS RTU スレーブ実装例
*
* ハードウェア接続:
* P1.1 (TXD) -> MAX485 DI
* P1.2 (RXD) <- MAX485 RO
* P1.3 (GPIO) -> MAX485 DE, /RE
*
* ボーレート: 9600[bps], 8N1
* スレーブアドレス: 0x01
*/
#include <msp430g2553.h>
// 定数
#define MY_SLAVE_ADDRESS 0x01
#define RS485_DIR_BIT BIT3
#define RX_BUFFER_SIZE 256
// マクロ定義
#define RS485_TX_MODE() (P1OUT |= RS485_DIR_BIT)
#define RS485_RX_MODE() (P1OUT &= ~RS485_DIR_BIT)
// グローバル変数
unsigned char rx_buffer[RX_BUFFER_SIZE];
unsigned int rx_index = 0;
volatile unsigned char frame_complete = 0;
// 保持レジスタ (例 : 10個)
unsigned int holding_registers[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 関数プロトタイプ宣言
void Clock_Init(void);
void GPIO_Init(void);
void UART_Init(void);
void Timer_Init(void);
void UART_SendByte(unsigned char data);
unsigned int CRC16_MODBUS(unsigned char *data, unsigned int length);
void MODBUS_SendResponse(unsigned char *response, unsigned int length);
void MODBUS_ProcessFrame(void);
/*
* メイン関数
*/
int main(void)
{
WDTCTL = WDTPW + WDTHOLD; // ウォッチドッグタイマ停止
Clock_Init(); // クロック初期化
GPIO_Init(); // GPIO初期化
UART_Init(); // UART初期化
Timer_Init(); // タイマ初期化
__bis_SR_register(GIE); // 全体割り込み有効
// メインループ
while (1) {
if (frame_complete) {
MODBUS_ProcessFrame(); // MODBUSフレーム処理
}
__bis_SR_register(LPM0_bits); // 低消費電力モード0
}
}
/*
* クロック初期化
*/
void Clock_Init(void)
{
DCOCTL = 0;
BCSCTL1 = CALBC1_1MHZ; // 1[MHz]キャリブレーション
DCOCTL = CALDCO_1MHZ;
}
/*
* GPIO初期化
*/
void GPIO_Init(void)
{
// UART端子設定
P1SEL |= BIT1 + BIT2;
P1SEL2 |= BIT1 + BIT2;
// RS-485方向制御端子
P1DIR |= RS485_DIR_BIT;
RS485_RX_MODE(); // 初期状態は受信モード
}
/*
* UART初期化
*/
void UART_Init(void)
{
UCA0CTL1 |= UCSWRST; // リセット状態に
UCA0CTL1 |= UCSSEL_2; // SMCLK選択
UCA0BR0 = 104; // 9600[bps] (1[MHz])
UCA0BR1 = 0;
UCA0MCTL = UCBRS0; // モジュレーション
UCA0CTL0 &= ~UCPEN; // パリティなし
UCA0CTL0 &= ~UC7BIT; // 8ビットデータ
UCA0CTL0 &= ~UCSPB; // 1ストップビット
UCA0CTL1 &= ~UCSWRST; // リセット解除
IE2 |= UCA0RXIE; // 受信割り込み有効
}
/*
* タイマ初期化 (3.5文字時間タイムアウト検出)
*/
void Timer_Init(void)
{
TA0CTL = TASSEL_2 + MC_0 + TACLR; // SMCLK、停止モード
TA0CCR0 = 3640; // 3.64[ms] @ 1[MHz]
TA0CCTL0 = CCIE; // 割り込み有効
}
/*
* 1バイト送信
*/
void UART_SendByte(unsigned char data)
{
while (!(IFG2 & UCA0TXIFG));
UCA0TXBUF = data;
}
/*
* CRC-16/MODBUS計算
*/
unsigned int CRC16_MODBUS(unsigned char *data, unsigned int length)
{
unsigned int crc = 0xFFFF;
unsigned int i, j;
for (i = 0; i < length; i++) {
crc ^= (unsigned int)data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
}
else {
crc >>= 1;
}
}
}
return crc;
}
/*
* MODBUS応答送信
*/
void MODBUS_SendResponse(unsigned char *response, unsigned int length)
{
unsigned int crc;
unsigned int i;
// CRC計算
crc = CRC16_MODBUS(response, length);
// 送信モードに切替
RS485_TX_MODE();
__delay_cycles(100);
// レスポンス送信
for (i = 0; i < length; i++) {
UART_SendByte(response[i]);
}
// CRC送信 (リトルエンディアン)
UART_SendByte((unsigned char)(crc & 0xFF));
UART_SendByte((unsigned char)((crc >> 8) & 0xFF));
// 送信完了待ち
while (UCA0STAT & UCBUSY);
__delay_cycles(1000);
// 受信モードに戻す
RS485_RX_MODE();
}
/*
* MODBUSフレーム処理
*/
void MODBUS_ProcessFrame(void)
{
unsigned int crc_calculated, crc_received;
unsigned char slave_addr, func_code;
unsigned char response[256];
unsigned int response_length = 0;
// CRC検証
if (rx_index < 4) {
goto cleanup; // フレームが短すぎる
}
crc_calculated = CRC16_MODBUS(rx_buffer, rx_index - 2);
crc_received = (unsigned int)rx_buffer[rx_index - 2] | ((unsigned int)rx_buffer[rx_index - 1] << 8);
if (crc_calculated != crc_received) {
goto cleanup; // CRCエラー
}
// フレーム解析
slave_addr = rx_buffer[0];
func_code = rx_buffer[1];
// 自分宛かチェック
if (slave_addr != MY_SLAVE_ADDRESS) {
goto cleanup;
}
// ファンクションコード処理
switch (func_code) {
case 0x03: // Read Holding Registers
{
unsigned int start_addr = ((unsigned int)rx_buffer[2] << 8) | rx_buffer[3];
unsigned int num_regs = ((unsigned int)rx_buffer[4] << 8) | rx_buffer[5];
unsigned int i;
// 範囲チェック
if (start_addr + num_regs > 10) {
// エラー応答 (例外コード 0x02: 不正なデータアドレス)
response[response_length++] = slave_addr;
response[response_length++] = func_code | 0x80;
response[response_length++] = 0x02;
break;
}
// 正常応答
response[response_length++] = slave_addr;
response[response_length++] = func_code;
response[response_length++] = (unsigned char)(num_regs * 2); // バイト数
for (i = 0; i < num_regs; i++) {
response[response_length++] = (unsigned char)((holding_registers[start_addr + i] >> 8) & 0xFF);
response[response_length++] = (unsigned char)(holding_registers[start_addr + i] & 0xFF);
}
break;
}
case 0x06: // Write Single Register
{
unsigned int reg_addr = ((unsigned int)rx_buffer[2] << 8) | rx_buffer[3];
unsigned int reg_value = ((unsigned int)rx_buffer[4] << 8) | rx_buffer[5];
// 範囲チェック
if (reg_addr >= 10) {
// エラー応答
response[response_length++] = slave_addr;
response[response_length++] = func_code | 0x80;
response[response_length++] = 0x02;
break;
}
// レジスタ書き込み
holding_registers[reg_addr] = reg_value;
// 正常応答 (エコーバック)
response[response_length++] = slave_addr;
response[response_length++] = func_code;
response[response_length++] = rx_buffer[2];
response[response_length++] = rx_buffer[3];
response[response_length++] = rx_buffer[4];
response[response_length++] = rx_buffer[5];
break;
}
default:
// サポートされていないファンクションコード
response[response_length++] = slave_addr;
response[response_length++] = func_code | 0x80;
response[response_length++] = 0x01; // 例外コード: 不正なファンクション
break;
}
// 応答送信
if (response_length > 0) {
MODBUS_SendResponse(response, response_length);
}
cleanup:
rx_index = 0;
frame_complete = 0;
}
/*
* UART受信割り込みハンドラ
*/
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void)
{
unsigned char rx_data = UCA0RXBUF;
if (rx_index < RX_BUFFER_SIZE) {
rx_buffer[rx_index++] = rx_data;
}
// タイマリスタート
TA0CTL |= TACLR;
TA0CTL |= MC_1; // アップモードで開始
__bic_SR_register_on_exit(LPM0_bits); // 低消費電力モード解除
}
/*
* Timer_A0割り込みハンドラ (3.5文字時間タイムアウト)
*/
#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer0_A0_ISR(void)
{
TA0CTL &= ~MC_3; // タイマ停止
if (rx_index > 0) {
frame_complete = 1;
}
__bic_SR_register_on_exit(LPM0_bits); // 低消費電力モード解除
}
トラブルシューティング
通信エラーの診断
| 症状 | 原因 | 対処方法 |
|---|---|---|
| 応答がない | ボーレート不一致 | マスタとスレーブのボーレート設定を確認 |
| 応答がない | スレーブアドレス不一致 | スレーブアドレスの設定を確認 |
| CRCエラー頻発 | ノイズの影響 | シールドケーブルの使用 終端抵抗の確認 |
| CRCエラー頻発 | 配線の問題 | A/B線の接続を確認 グラウンドを共通化 |
| データ化け | RS-485方向制御の不良 | DE/RE制御タイミングを見直す |
| 文字落ち | 受信バッファオーバーフロー | バッファサイズを増やす 割り込み優先度を上げる |
| 断続的なエラー | 終端抵抗なし | バスの両端に120[Ω]の終端抵抗を配置 |
| 通信開始時のエラー | バイアス抵抗なし | フェイルセーフバイアス抵抗を追加 |
デバッグ方法
MODBUS通信のデバッグには、以下に示す方法が有効である。
| 方法 | 説明 |
|---|---|
| LEDインジケータ | 送受信状態をLEDで表示して、動作を目視確認 |
| ロジックアナライザ | TXD、RXD、DE/RE信号をキャプチャして、タイミングを確認 |
| USB-RS485変換器 | PCとRS-485バス間でデータをモニタ |
| MODBUS ASCIIモード | デバッグ時はASCIIモードで通信内容を可視化 |
| CRCチェック無効化 | 開発初期はCRC検証を一時的にスキップして動作確認 |
RS-485バスの配線ガイドライン
RS-485バスの配線では、以下に示す事柄に注意する必要がある。
| 項目 | 推奨値 | 説明 |
|---|---|---|
| 最大通信距離 | 1200[m] | ボーレートとケーブルの品質に依存 |
| 最大ノード数 | 32台 | 標準ドライバの場合 (最大247台まで拡張可能) |
| ケーブル特性インピーダンス | 120[Ω] | ツイストペアケーブルを使用 |
| 終端抵抗 | 120[Ω] | バスの両端に配置 |
| バイアス抵抗 (プルアップ) | 470~680[Ω] | A端子をVCCに接続 |
| バイアス抵抗 (プルダウン) | 470~680[Ω] | B端子をGNDに接続 |
| 推奨ケーブル | ツイストペアシールド | 外部ノイズ対策 |
省電力モードの注意点
MSP430G2553の省電力モードを使用する場合、UART受信時にマイコンを確実に起動させる必要がある。
そのため、受信割り込みで低消費電力モードを解除するようにしたほうがよい。
// 受信割り込み内で低消費電力モード解除
__bic_SR_register_on_exit(LPM0_bits);