Modbusで利用するケース

最終更新日:2020年2月7日

2020/2/7 変更版を作成しました ->

ModbusSlaveデバイスを簡単に構築出来ないかと思い検討しました。
手元にはテクノウエーブ社のLANX-I2219をModbus/TCPSlaveに仕立てたモノがあります。このデバイスを使用して、LabVIEWのホスト側コードを書きデバッグしてきました。その過程で、もっとユーザ環境に近いModbusデバイスが簡単に構築出来ないかなと思いました。
このデバイスですとDIOやアナログ信号の入出力は出来ますが、センサデバイスをコントロールしようとすると二の足を踏みます。
今ではArduinoとかRasberyPiを利用してセンサデバイスを含むSlaveデバイスを構築出来るかと思いますので、これを利用して自在なModbusSlavedeviceの構築を検討しました。
ターゲットは構築しやすいArduinoIDE環境で利用できるデバイスを検討したいです。ただ、AVR328ではSRAMが足りないためModbusTCPとして構築するのが困難です。Mega256で有れば足りるのですが、折角なのでBluePillデバイスを使う事にします。

想定としては幾つかのブロック毎にセンサ類が配置されていて、それをSCADAで管理する様なイメージです。


ModbusのマスタはLabVIEWで構築しました。基本的にブラウザは使用しませんがSetupの為には使用します。そのためSketchにもhttp用のコードが含まれています。
Modbusライブラリは Plasmionique Modbus Master v1.3.5.3 を採用しました。Plasmionique社から無償提供されています。
https://lavag.org/files/file/286-plasmionique-modbus-master/

Modbusはアドレスに決まりがあります。データ・アドレスには4種類有り区別しています。

  1. コイル(Coil)
    コイルは、フィールドへの ON / OFF 出力である DO(Discrete Output)やスレーブデバイスの状態やモードを変更する為にスイッチとして用いられます。
    参照・変更が可能な2値のデータで、可能アドレス範囲は 1 から 9999 という仕様です。
    このプログラムではDOとして設定出来る4bit分のみを割り当てています。1から4です。

  2. 入力ステータス(Input Status)
    入力ステータスは、フィールドからの ON / OFF 入力である DI(Discrete Input)やスレーブデバイスの状態入力として用いられます。
    参照のみで変更はできない 2 値のデータで、可能アドレス範囲は10001 から 19999 という仕様です。
    このプログラムではDIとして4bit用意しました。1から4です。

  3. 入力レジスタ(Input Register)
    入力レジスタは、フィールドからのAI(Analog Input)やスレーブデバイス内の情報として用いられます。
    16ビット長のデータで、参照するのみで変更はできません。
    可能アドレス範囲は30001から39999です。
    複数の連続したアドレスを割当てることにより、単精度実数、倍精度実数などのデータを扱うこともできます。
    このプログラムではAIの4ポートとI2Cで取得した温度、湿度、照度を割り当てています。1から7です。
    ※ソースコードは公開していますので適宜修正してください。


  4. 保持レジスタ(Holding Register)
    保持レジスタは、フィールドからの AO(Analog Output)やスレーブデバイス内の設定情報として用いられます。
    16 ビット長のデータで、参照・変更ができます。可能アドレス範囲は 40001 から 49999です。
    複数の連続したアドレスを割当てることによって、単精度実数、倍精度実数などのデータを扱うこともできます。
    SRAMに余りがあれば配列を確保して読み書きできるようにすればいいです。一応このプログラムでは16アドレス分確保してあります。

上記評価プログラムはSTM32MINIShieldEvaluationボードに以下のような実装をして動作するようにしています。
ただ、Witte Software社のModbus Slaveと云うソフトを使用して1台のPCでSelfSendして確認することも可能です。

Setupの画面は殆ど変わりません。Macアドレス及びネットワーク環境の設定をはじめに実施する必要があります。ブラウザで実現出来ますのでLINUX/MAC/WINDOWSいずれのOSからでも設定可能です。

STM32duinoソースコード(Sketch)はこんな感じになります。必ず、“MgsModbus.h”“MgsModbus.cpp”を同じフォルダに入れてください。
“MgsModbus.h”“MgsModbus.cpp”はオリジナルコードをレタッチしています。オリジナルコードの作者はMarco Gerritse氏[V-0.1.1著作権(C)2013]です。断りを入れているわけではないです。Arduino用のコードはどうしてもうまく実現できませんでした。

LabVIEWのソースコードはLV2014版をここにリンクしておきます。

このコードを実行するに当たり追加が必要なライブラリは、

/*
 * 20191219 T.Wanibe STM32MINIShieldEvaluation用に書き直しました。
 * CoilData:16bit 4bit接続
 * DiscreteData:16bit 4bit接続
 * InputData:8ワード 4ポートAI、3ポートI2C
 * HoldData:16ワード 未使用
 * I2Cデバイスが接続されていなくても動く様に対策していますが、必ずPulUp抵抗は実装してください。
 * STM32MINIShieldEvaluation基板の場合R8/R9に10kΩ実装することになります。
 * 
 * コンパイル結果を添えておきます。
 * 最大131072バイトのフラッシュメモリのうち、スケッチが60312バイト(46%)を使っています。
 * 最大20480バイトのRAMのうち、グローバル変数が5096バイト(24%)を使っていて、ローカル変数で15384バイト使うことができます。
 */
#include <SPI.h>
#include <Ethernet3.h>                  //WizNetからライブラリを取得する
#include "MgsModbus.h"                  //このSketchと同じ階層に置く
#include <EEPROM.h>
#include <avr/pgmspace.h>
#include <TextFinder.h>                 //WebSetting
#include <Wire.h>
#include "Adafruit_HTU21DF.h"           //GY21  温度・湿度モジュールのドライバ
#include <SparkFunTSL2561.h>            //GY2161 照度モジュールのドライバ
#define DEBUG                   1
#define OK                      0
#define NG                      1
#define W550io_Rst              PA8     //STM32MINIShieldEvaluation基板に準拠
#define SPI1_NSS_PIN            PA4     //SPI_1
#define ModBusPort              502     //決まり事
#define MbCoilDataLen           2       // length of the MbCoilDataLen array (Equivalent16bit*2)
                                        //実際には4bitしか実装していません。
#define MbDiscreteDataLen       2       // length of the MbDiscreteDataLen array(Equivalent16bit*2)
                                        //実際には4bitしか実装していません。
#define MbHoldDataLen           16      // length of the MbHoldDataLen array
#define MbInputDataLen          8       // length of the MbInputDataLen array
                                        //実際には7つ実装しています。AI×4、温度(x100[℃])湿度(x100[%])照度(x1[lux])
#define GY21ADRS                0x40    //HTU21DF_ADRS
#define GY2561ADRS              0x39    //TSL2561_ADRS
#define HttpPort                80
#define LED                     PC13
MgsModbus Mb;
int     inByte          =       0;      // incoming serial byte
// Ethernet settings (depending on MAC and Local network)
byte mac[]              = {0x00,0x08,0xDC,0x54,0x4D,0xD0};      //WIZNET
byte ip[]               = {192, 168, 0, 200};
byte dns_server[]       = {192, 168, 0, 1};
byte gateway[]          = {192, 168, 0, 1};
byte subnet[]           = {255, 255, 255, 0};
int dOsPins[]           = {PB12, PB13, PB14, PB15};
int dIsPins[]           = {PB11, PB10, PB1, PB0};
//int aOsPins[]         = {};
int aIsPins[]           = {PA0,PA1,PA2,PA3};
//配列要素数を求めておく
int DObits              = sizeof(dOsPins)/sizeof(int);
int DIbits              = sizeof(dIsPins)/sizeof(int);
int AIbits              = sizeof(aIsPins)/sizeof(int);
char buf1[32];
EthernetServer webServer(HttpPort);
EthernetClient client;
Adafruit_HTU21DF htu = Adafruit_HTU21DF();
String HTTP_req;                                        // stores the HTTP request
boolean         gain;                                   // HTU用Gain setting, 0 = X1, 1 = X16;
unsigned int    ms;                                     // HTU用Integration ("shutter") time in milliseconds
unsigned int    data0, data1;                           // HTU用
SFE_TSL2561     light;                                  // Light用
// これは、HTMLコードを「流す」ためのバッファーです。
// ””を含む最長の文字チェーン+1と同じ大きさでなければなりません。
char buffer[100];
// これはすべて切り詰められたHTMLコードです。 エディターでHTMLコードを作成し、バッファに収まるように切り分けます。
// HTMLをすべての場所で切り取ることができますが、\ "部分ではできません。HTML内でsimple"の代わりに\ "を
// 使用する必要があります。そうしないと機能しません。
const byte ID = 0x92;
/*
        EEPROMの有効なデータが「知っている」ビットであるかどうかを識別するために使用されます。
        これがEEPROMに書き込まれていれば、スケッチは前に実行されました
        これを使用するので、最初にこのスケッチを実行すると、上記の値が使用されます。 
        どのデータにどのEEPROMアドレスを使用しているかを定義する
 */
const char titleStr[]   = "STM32MINIShieldEvaluation";
byte    ErrorHTU,ErrorLight;
//----------------------------------
void LANSetup(){
        int idcheck = EEPROM.read(0);
        if (idcheck != ID){
                //idがIDの値ではない場合、
                //このスケッチがシールドを設定する前に使用されていなかったことを意味します
                //スケッチの始めに書かれた値を使用するだけです
        }
        if (idcheck == ID){
                //idがIDと同じ値の場合、
                //これは、このスケッチがシールドを設定するために使用されたことを意味します。
                //EERPOMの値を読み取ってシールドを設定します。
                for (int i = 0; i < 6; i++){
                        mac[i] = EEPROM.read(i+1);
                }
                for (int i = 0; i < 4; i++){
                        ip[i] = EEPROM.read(i+7);
                }
                for (int i = 0; i < 4; i++){
                        subnet[i] = EEPROM.read(i+11);
                }
                for (int i = 0; i < 4; i++){
                        gateway[i] = EEPROM.read(i+15);
                }
        }
        Ethernet.begin(mac, ip);
//      Ethernet.begin(mac);                            //DHCPの場合
//      Ethernet.begin(mac, ip, subnet);                //SubnetMaskを意識した場合。
//      Ethernet.begin(mac, ip, subnet, gateway);       //gatewayを意識した場合。
}
//--------------------------
void SetWebPage( EthernetClient client){
        client.print(F("<!DOCTYPE HTML PUBLIC \"\"><html><HEAD><META http-equiv=\"Content-Type\" charset=UTF-8\">"));
        client.print(F("<META http-equiv=\"Content-Style-Type\"><TITLE>"));
        client.print(titleStr);
        client.print(F(" Setup Page</TITLE></HEAD>"));
        client.print(F("<BODY marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\"><BLOCKQUOTE><BLOCKQUOTE>"));
        client.print(F("<table bgcolor=\"#17A1A5\" border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;color:#ffffff;font-size:12px;\">"));
        client.print(F("<tr><td>&nbsp;"));
        client.print(titleStr);
        client.print(F(" Setup Page</td></tr></table><br>"));
        //
        client.print(F("<script>function hex2num (s_hex) {eval(\"var n_num=0X\" + s_hex);return n_num;}</script>"));
        client.print(F("<FORM><input type=\"hidden\" name=\"SBM\" value=\"1\"><table><tr><td>MAC:</td><td>"));
        client.print(F("<input id=\"T1\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT1\" value=\""));
        client.print(mac[0],HEX);
        client.print(F("\">.<input id=\"T3\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT2\" value=\""));
        client.print(mac[1],HEX);
        client.print(F("\">.<input id=\"T5\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT3\" value=\""));
        client.print(mac[2],HEX);
        client.print(F("\">.<input id=\"T7\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT4\" value=\""));
        client.print(mac[3],HEX);
        client.print(F("\">.<input id=\"T9\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT5\" value=\""));
        client.print(mac[4],HEX);
        client.print(F("\">.<input id=\"T11\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT6\" value=\""));
        client.print(mac[5],HEX);
        //
        client.print(F("\"><input id=\"T2\" type=\"hidden\" name=\"DT1\"><input id=\"T4\" type=\"hidden\" name=\"DT2"));
        client.print(F("\"><input id=\"T6\" type=\"hidden\" name=\"DT3\"><input id=\"T8\" type=\"hidden\" name=\"DT4"));
        client.print(F("\"><input id=\"T10\" type=\"hidden\" name=\"DT5\"><input id=\"T12\" type=\"hidden\" name=\"DT6"));
        client.print(F("\"></td></tr><tr><td>IP:</td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT7\" value=\""));
        client.print(ip[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT8\" value=\""));
        client.print(ip[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT9\" value=\""));
        client.print(ip[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT10\" value=\""));
        client.print(ip[3],DEC);
        //
        client.print(F("\"></td></tr><tr><td>MASK: </td><td><input type= \"text\" size=\"3\" maxlength=\"3\" name=\"DT11\" value=\""));
        client.print(subnet[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT12\" value=\""));
        client.print(subnet[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT13\" value=\""));
        client.print(subnet[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT14\" value=\""));
        client.print(subnet[3],DEC);
        //
        client.print(F("\"></td></tr><tr><td>GW: </td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT15\" value=\""));
        client.print(gateway[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT16\" value=\""));
        client.print(gateway[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT17\" value=\""));
        client.print(gateway[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT18\" value=\""));
        client.print(gateway[3],DEC);
        client.print(F("\"></td></tr><tr><td><br></td></tr><tr><td><input id=\"button1\"type=\"submit\" value=\"SUBMIT\" "));
        //
        client.print(F("Onclick=\"document.getElementById('T2').value "));
        client.print(F("= hex2num(document.getElementById('T1').value);"));
        client.print(F("document.getElementById('T4').value = hex2num(document.getElementById('T3').value);"));
        client.print(F("document.getElementById('T6').value = hex2num(document.getElementById('T5').value);"));
        client.print(F("document.getElementById('T8').value = hex2num(document.getElementById('T7').value);"));
        client.print(F("document.getElementById('T10').value = hex2num(document.getElementById('T9').value);"));
        client.print(F("document.getElementById('T12').value = hex2num(document.getElementById('T11').value);\""));
        //
        client.print(F("></td><td></td></tr></form></table></BLOCKQUOTE></BLOCKQUOTE></BODY></html>"));
}
//--------------------------
void checkWebPage( EthernetClient client)
{
        Serial.println(F("new webClient"));
        if (client) {
                TextFinder  finder(client );
                while (client.connected()) {      
                        if (client.available()) {
                                //この部分はすべてのテキスト検索を行います。
                                if( finder.find("GET /") ) {
                                        // 「setup」とい語が見つかった場合は、さらに探してください。
                                        // その単語が見つからない場合は、検索を停止して先に進みます。
                                        // これにより、後でスケッチに独自のWebページを配置できます。
                                        if (finder.findUntil("setup", "\n\r")){
                                                // 「SBM」という単語が見つかった場合は、さらに探し続けます。
                                                // その言葉が見つからない場合は、探して停止します。
                                                // SUBMITボタンが押されていない、何も押されていないことを意味します
                                                // セットアップページが構築されている場所に移動し、クライアントのブラウザに表示します。
                                                if (finder.findUntil("SBM", "\n\r")){
                                                        byte SET = finder.getValue();
                                                        // これで、「DT」という文字を探している間に、「DT」の後ろにあるすべての数字を覚えて、
                                                        // 値を一致させて、mac、ip、subnet、およびgatewayに入れる必要があります。
                                                        while(finder.findUntil("DT", "\n\r")){
                                                                int val = finder.getValue();
                                                                // 「DT」のvalが1?6の場合、対応する値はMAC値でなければなりません。
                                                                if(val >= 1 && val <= 6) {
                                                                        mac[val - 1] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが7?10の場合、対応する値はIP値でなければなりません。
                                                                if(val >= 7 && val <= 10) {
                                                                        ip[val - 7] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが11?14の場合、対応する値はMASK値でなければなりません。
                                                                if(val >= 11 && val <= 14) {
                                                                        subnet[val - 11] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが15?18の場合、対応する値はGW値でなければなりません。
                                                                if(val >= 15 && val <= 18) {
                                                                        gateway[val - 15] = finder.getValue();
                                                                }
                                                        }
                                                        // すべてのデータを取得したので、EEPROMに保存できます
                                                        for (int i = 0 ; i < 6; i++){
                                                                EEPROM.write(i + 1,mac[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 7, ip[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 11, subnet[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 15, gateway[i]);
                                                        }
                                                        // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                                                        EEPROM.write(0, ID); 
                                                        // すべてのデータがEEPROMに書き込まれている場合、arduinoをリセットする必要があります。
                                                        //ハードウェアリセットボタンを使用する必要があります。
                                                }
                                                // この時点から、セットアップページの構築を開始し、クライアントのブラウザーに表示できます。
                                                client.println("HTTP/1.1 200 OK");
                                                client.println("Content-Type: text/html");
                                                client.println();
                                                //
                                                SetWebPage(client);
                                                
                                                break;
                                        }
                                }
                                client.println("HTTP/1.1 200 OK");
                                client.println("Content-Type: text/html");
                                client.println();
                                // put your own html from here on
                                client.print("IT WORKS: go to ");
                                client.print(ip[0],DEC);
                                for (int i= 1; i < 4; i++){
                                        client.print(".");
                                        client.print(ip[i],DEC);
                                }
                                client.print("/setup");
                                // put your own html until here 
                                break;  
                        }
                }
                delay(1);
                client.stop();
        }     
}
//----------------------------
int     GetDeviceInfo(){
        for (int i=0;i<DIbits;i++){
                Mb.SetBit(MB_FC_READ_DISCRETE_INPUT,i,!digitalRead(dIsPins[i]));        //bit 論理反転
        }
        for (int i=0;i<AIbits;i++){
                Mb.MbInputData[i]=analogRead(aIsPins[i]);                               //U16
        }
        return OK;            
}
//----------------------------
int     SetDeviceInfo(){
        for (int i=0;i<DObits;i++){          
                digitalWrite(dOsPins[i],!Mb.GetBit(MB_FC_READ_COILS,i));                 //LEDはLOWで点灯                            
        }
        return OK;
}
//----------------------------
int     SetI2CDeviceInfo(){
        if(!ErrorHTU){
                //HOLDREGISTERのIndex=0に温度をセット INTにする必要があるので100倍して整数化 25.65->2565
                Mb.MbInputData[4]        = int(htu.readTemperature()*100);
                //HOLDREGISTERのIndex=1に湿度をセット INTにする必要があるので100倍して整数化 57.88->5788
                Mb.MbInputData[5]        = int(htu.readHumidity()*100);
        }
        if(!ErrorLight){        
                //HOLDREGISTERのIndex=2に照度をセット INT化する 
                double lux;                             // Resulting lux value
                boolean good;                           // True if neither sensor is saturated
                if (light.getData(data0,data1)){        // getData() returned true, communication was successful
                        good = light.getLux(gain,ms,data0,data1,lux);   // Perform lux calculation:
                        if(good) Mb.MbInputData[6]=int(lux);else Mb.MbHoldData[6]=0xFFFF;
                }
        }     
}
//--------------------------
void printError(byte error){
        // If there's an I2C error, this function will
        // print out an explanation.
        Serial.print(F("I2C error: "));
        Serial.print(error,DEC);
        Serial.print(F(", "));
  
        switch(error){
                case 0:
                        Serial.println(F("success"));
                        break;
                case 1:
                        Serial.println(F("data too long for transmit buffer"));
                        break;
                case 2:
                        Serial.println(F("received NACK on address (disconnected?)"));
                        break;
                case 3:
                        Serial.println(F("received NACK on data"));
                        break;
                case 4:
                        Serial.println(F("other error"));
                        break;
                default:
                        Serial.println(F("unknown error"));
        }
}
//----------------------------
void setup()
{
        // serial setup
        Serial.begin(115200);
        delay(1000);
        Serial.println(F("Serial interface started"));
        SPI.begin();                                                    //Initialize the SPI_1 port.
        SPI.setBitOrder(MSBFIRST);                                      // Set the SPI_1 bit order
        SPI.setClockDivider(SPI_CLOCK_DIV4);                            //SPI_CLOCK_DIV16 (72 / 16 = 4.5 MHz SPI_1 speed)
                                                                        //SPI_CLOCK_DIV4 (72 / 4 = 18 MHz SPI_1 speed)
        //Ethernet3で使用可能なAPI
        pinMode(SPI1_NSS_PIN, OUTPUT);                                  //NIC_CS出力設定
        Ethernet.setCsPin(SPI1_NSS_PIN);                                //NIC_CSアサイン
        Ethernet.setRstPin(W550io_Rst);                                 //NIC_RSTアサイン
        //NICリセット処理
        pinMode(W550io_Rst, OUTPUT);
        digitalWrite(W550io_Rst, LOW);
        delay(10);                                                      //10msecパルス幅で初期化
        digitalWrite(W550io_Rst, HIGH);
        LANSetup();
        Serial.println(F("Ethernet interface started")); 
        Serial.print(F("My IP address: "));Serial.println(Ethernet.localIP());// print your local IP address:
        webServer.begin();
        //I2Cは接続されているかどうかチェックしてから対応することにした。
        Wire.begin();
        Serial.print(F("I2C_Start\n"));
        //ErrorHTUcheck
        Wire.beginTransmission(GY21ADRS);
        Serial.print(F("trans_Start\n"));
        ErrorHTU = Wire.endTransmission();
        Serial.println(ErrorHTU);
        if(!ErrorHTU){
                Serial.print(F("HTU_Start\n"));
                htu.begin();
        }
        Serial.print(F("htu_CheckEnd\n"));
        delay(100); 
        //ErrorLight
        Wire.beginTransmission(GY2561ADRS);
        Serial.print(F("trans_Start\n"));
        ErrorLight = Wire.endTransmission();
        Serial.println(ErrorLight);
        if(!ErrorLight){
                Serial.print(F("Light_Start\n"));
                light.begin();
        }
        Serial.print(F("light_CheckEnd\n"));
        //
        unsigned char ID;
        if(!ErrorLight){
                if (light.getID(ID)){
                        Serial.print(F("Got factory ID: 0X"));
                        Serial.print(ID,HEX);
                        Serial.println(F(", should be 0X5X"));
                }else{
                        // Most library commands will return true if communications was successful,
                        // and false if there was a problem. You can ignore this returned value,
                        // or check whether a command worked correctly and retrieve an error code:
                        byte error = light.getError();
                        printError(error);
                }
                gain = 0;                                               //Gain setting, 0 = X1, 1 = X16;
                unsigned char time = 2;                                 //Integration ("shutter") time in milliseconds
                light.setTiming(gain,time,ms);
                light.setPowerUp();
        }
        //ModBusメモリ 初期化
        for(int i = 0;i<MbCoilDataLen;i++)      Mb.MbCoilData[i]=0;     //メモリ初期化
        for(int i = 0;i<MbDiscreteDataLen;i++)  Mb.MbDiscreteData[i]=0; //メモリ初期化
        for(int i = 0;i<MbHoldDataLen;i++)      Mb.MbHoldData[i]=0;     //メモリ初期化
        for(int i = 0;i<MbInputDataLen;i++)     Mb.MbInputData[i]=0;    //メモリ初期化
        for (int i=0;i<DObits;i++){          
                Mb.SetBit(MB_FC_READ_COILS,i,false);                    //メモリマップはMb.MbData[0]
                pinMode(dOsPins[i],OUTPUT);
                digitalWrite(dOsPins[i], HIGH);                         //LEDはLOWで点灯                            
        }
        for (int i=0;i<DIbits;i++){
                pinMode(dIsPins[i],INPUT);
                Mb.SetBit(MB_FC_READ_DISCRETE_INPUT,i,!digitalRead(dIsPins[i]));        //bit メモリマップはMb.MbData[1]
        }
        for (int i=0;i<AIbits;i++){
                Mb.MbInputData[i] = analogRead(aIsPins[i]);             //U16
        }
        Serial.println(F("EndSetUp"));
}
//----------------------------
int LoopCount = 0;
void loop()
{
        if(!LoopCount++)        Serial.println(F("LoopStart"));
        client = webServer.available();
        if(client) checkWebPage(client);
        client.stop();                                  // コネクションを閉じる。
        GetDeviceInfo();
        Mb.MbsRun();                                    //SlaveServer
        SetDeviceInfo();
        SetI2CDeviceInfo();
        if(!(LoopCount%1000))   Serial.print(F("."));
        if(!(LoopCount%40000))  Serial.println(F(""));
}
//----------------------------

setup画面のHTMLコードですが、参考としたコードは PROGMEM =”” を採用していました。この場合レタッチがとても大変です。
obj.println(F("")); で書いた方が制限が少なくて扱いやすいです。必ずF("")を遣う必要があります。そうしないとSRAMが足りなくなります。

Setupを実行したら必ずBluePillをリセットしてください。起動した時点で内容が更新されます。


保持レジスタに書き込まれた値を元にアナログ出力値を変更するコードを書きました。
I2Cのモジュール・MCP4725を接続しました。コネクタの関係で、I2CモジュールをBME280とMCP4725に変更しています。

ホスト側パネルも一寸弄りましたがコード自体は弄っていません。
スレーブ側のコードはデバイスを変更した部分を弄っています。また、LED&KEYで押したボタンに相当するデータを7セグLEDで表示出来るようにしました。

  1. コイル(Coil)
    1. LED1(PB8)
    2. LED2(PB9)

  2. 入力ステータス(Input Status)
    1. AI0(PA0) DAC(MCP4725)の出力を入力
    2. BME280 温度データ x100
    3. BME280 湿度データ x100
    4. BME280 大気圧データ ÷100 [mbar]

  3. 入力レジスタ(Input Register)
    1. IN0(PB11)
    2. IN1(PB10)
    3. IN2(PB1)
    4. IN3(PB0)

  4. 保持レジスタ(Holding Register)
    1. DAC(MCP4725) 0..4095

S1:温度表示[℃]
S2:湿度表示[%]
S3:大気圧表示[Pa]
S5:DAC設定値
S6:ADC測定値

ソースコードも添えておきます。
このコードを実行するに当たり追加が必要なライブラリは、

/*
 * 20200206 T.Wanibe STM32MINIShieldEvaluation用に書き直しました。
 * CoilData:16bit 4bit接続
 * DiscreteData:16bit 4bit接続
 * InputData:8ワード アドレス1:MCP4735の値を読み込む Address2:MCP280 温度 Address3:rerserved Address4:MCP280 気圧
 * HoldData:16ワード アドレス1の値が書き込まれたときにMCP4725のEEPROMを書き換え
 * I2Cデバイスが接続されていなくても動く様に対策していますが、必ずPulUp抵抗は実装してください。
 * 実際に動かしていて幾つか修正を思い立ちました。
 * ●クライアントがコネクト中はBuiltINLEDを転倒させる
 * ●LED&KEYのLEDをに割り付ける
 * ●LED&KEYで表示更新し続けるか設定出来るようにする。DO7のbitをフラグにしてみます。
 * iPhoneで提供されているModbusアプリがいくつかあり、アクセスしたところ、コネクトは普通に出来ます。ただ、通信がちゃんと出来ているのか確認出来るだけのMasterがないです。残念です。
 * STM32MINIShieldEvaluation基板の場合R8/R9に10kΩ実装することになります。
 * 
 * コンパイル結果を添えておきます。
 * 最大131072バイトのフラッシュメモリのうち、スケッチが58672バイト(44%)を使っています。
 * 最大20480バイトのRAMのうち、グローバル変数が5312バイト(25%)を使っていて、ローカル変数で15168バイト使うことができます。
 */
#include <SPI.h>
#include <Ethernet3.h>                  //WizNetからライブラリを取得する
#include "MgsModbus.h"                  //このSketchと同じ階層に置く
#include <EEPROM.h>
#include <avr/pgmspace.h>
#include <TextFinder.h>                 //WebSetting
#include <Wire.h>
//#include "Adafruit_HTU21DF.h"           //GY21  温度・湿度モジュールのドライバ
//#include <SparkFunTSL2561.h>            //GY2161 照度モジュールのドライバ
#include <MCP4725.h>                    //DAC
#include <BME280I2C.h>                  //温湿度 気圧センサ
#include <TM1638.h>
#define dataPin                 PB4
#define clockPin                PB3
#define strobePin               PA15
#define DEBUG                   1
#define OK                      0
#define NG                      1
#define W550io_Rst              PA8     //STM32MINIShieldEvaluation基板に準拠
#define SPI1_NSS_PIN            PA4     //SPI_1
#define ModBusPort              502     //決まり事
#define MbCoilDataLen           2       // length of the MbCoilDataLen array (Equivalent16bit*2)
                                        //実際には4bitしか実装していません。
#define MbDiscreteDataLen       2       // length of the MbDiscreteDataLen array(Equivalent16bit*2)
                                        //実際には4bitしか実装していません。
#define MbHoldDataLen           16      // length of the MbHoldDataLen array
#define MbInputDataLen          8       // length of the MbInputDataLen array
                                        //実際には7つ実装しています。AI×4、温度(x100[℃])湿度(x100[%])照度(x1[lux])
#define HttpPort                80
#define LED                     PC13
#define LED1                    PB8     //LED出力1
#define LED2                    PB9     //LED出力2
#define DAC_REF_VOLTAGE         3.3     //dac supply-reference voltage
#define I2C_BUS_SPEED           400000  //i2c bus speed, 100 000Hz or 400 000Hz
int     inByte          =       0;      // incoming serial byte
// Ethernet settings (depending on MAC and Local network)
byte mac[]              = {0x00,0x08,0xDC,0x54,0x4D,0xD2};      //WIZNET
byte ip[]               = {192, 168, 0, 202};
byte dns_server[]       = {192, 168, 0, 1};
byte gateway[]          = {192, 168, 0, 1};
byte subnet[]           = {255, 255, 255, 0};
int dOsPins[]           = {LED1,LED2};
int dIsPins[]           = {PB11, PB10, PB1, PB0};
//int aOsPins[]         = {};
int aIsPins[]           = {PA0};
//配列要素数を求めておく
int DObits              = sizeof(dOsPins)/sizeof(int);
int DIbits              = sizeof(dIsPins)/sizeof(int);
int AIbits              = sizeof(aIsPins)/sizeof(int);
char buf1[32];
byte    ErrorTMP        = false;
byte    ErrorHUM        = false;
byte    ErrorPRS        = false;
uint16_t value          = 0;
float   voltage         = 0;
float   temp(NAN), hum(NAN), pres(NAN);
EthernetServer  webServer(HttpPort);
EthernetClient  client;
TM1638          LEDandKEY(dataPin, clockPin, strobePin);
BME280I2C       bme;                                            // Default : forced mode, standby time = 1000 ms
MCP4725 dac(MCP4725A0_ADDR_A00, DAC_REF_VOLTAGE);
String          HTTP_req;                                       // stores the HTTP request
MgsModbus       Mb;
word            preData = Mb.MbHoldData[0];                     //初期値確認
byte            lastKey;
// これは、HTMLコードを「流す」ためのバッファーです。
// ””を含む最長の文字チェーン+1と同じ大きさでなければなりません。
char    buffer[100];
// これはすべて切り詰められたHTMLコードです。 エディターでHTMLコードを作成し、バッファに収まるように切り分けます。
// HTMLをすべての場所で切り取ることができますが、\ "部分ではできません。HTML内でsimple"の代わりに\ "を
// 使用する必要があります。そうしないと機能しません。
const byte      ID = 0x92;
/*
        EEPROMの有効なデータが「知っている」ビットであるかどうかを識別するために使用されます。
        これがEEPROMに書き込まれていれば、スケッチは前に実行されました
        これを使用するので、最初にこのスケッチを実行すると、上記の値が使用されます。 
        どのデータにどのEEPROMアドレスを使用しているかを定義する
 */
const char      titleStr[]   = "STM32MINIShield";
char            outputSTR[9];
//----------------------------------
void LANSetup(){
        int idcheck = EEPROM.read(0);
        if (idcheck != ID){
                //idがIDの値ではない場合、
                //このスケッチがシールドを設定する前に使用されていなかったことを意味します
                //スケッチの始めに書かれた値を使用するだけです
        }
        if (idcheck == ID){
                //idがIDと同じ値の場合、
                //これは、このスケッチがシールドを設定するために使用されたことを意味します。
                //EERPOMの値を読み取ってシールドを設定します。
                for (int i = 0; i < 6; i++){
                        mac[i] = EEPROM.read(i+1);
                }
                for (int i = 0; i < 4; i++){
                        ip[i] = EEPROM.read(i+7);
                }
                for (int i = 0; i < 4; i++){
                        subnet[i] = EEPROM.read(i+11);
                }
                for (int i = 0; i < 4; i++){
                        gateway[i] = EEPROM.read(i+15);
                }
        }
        Ethernet.begin(mac, ip);
//      Ethernet.begin(mac);                            //DHCPの場合
//      Ethernet.begin(mac, ip, subnet);                //SubnetMaskを意識した場合。
//      Ethernet.begin(mac, ip, subnet, gateway);       //gatewayを意識した場合。
}
//--------------------------
void SetWebPage( EthernetClient client){
        client.print(F("<!DOCTYPE HTML PUBLIC \"\"><html><HEAD><META http-equiv=\"Content-Type\" charset=UTF-8\">"));
        client.print(F("<META http-equiv=\"Content-Style-Type\"><TITLE>"));
        client.print(titleStr);
        client.print(F(" Setup Page</TITLE></HEAD>"));
        client.print(F("<BODY marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\"><BLOCKQUOTE><BLOCKQUOTE>"));
        client.print(F("<table bgcolor=\"#17A1A5\" border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;color:#ffffff;font-size:12px;\">"));
        client.print(F("<tr><td>&nbsp;"));
        client.print(titleStr);
        client.print(F(" Setup Page</td></tr></table><br>"));
        //
        client.print(F("<script>function hex2num (s_hex) {eval(\"var n_num=0X\" + s_hex);return n_num;}</script>"));
        client.print(F("<FORM><input type=\"hidden\" name=\"SBM\" value=\"1\"><table><tr><td>MAC:</td><td>"));
        client.print(F("<input id=\"T1\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT1\" value=\""));
        client.print(mac[0],HEX);
        client.print(F("\">.<input id=\"T3\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT2\" value=\""));
        client.print(mac[1],HEX);
        client.print(F("\">.<input id=\"T5\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT3\" value=\""));
        client.print(mac[2],HEX);
        client.print(F("\">.<input id=\"T7\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT4\" value=\""));
        client.print(mac[3],HEX);
        client.print(F("\">.<input id=\"T9\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT5\" value=\""));
        client.print(mac[4],HEX);
        client.print(F("\">.<input id=\"T11\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT6\" value=\""));
        client.print(mac[5],HEX);
        //
        client.print(F("\"><input id=\"T2\" type=\"hidden\" name=\"DT1\"><input id=\"T4\" type=\"hidden\" name=\"DT2"));
        client.print(F("\"><input id=\"T6\" type=\"hidden\" name=\"DT3\"><input id=\"T8\" type=\"hidden\" name=\"DT4"));
        client.print(F("\"><input id=\"T10\" type=\"hidden\" name=\"DT5\"><input id=\"T12\" type=\"hidden\" name=\"DT6"));
        client.print(F("\"></td></tr><tr><td>IP:</td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT7\" value=\""));
        client.print(ip[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT8\" value=\""));
        client.print(ip[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT9\" value=\""));
        client.print(ip[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT10\" value=\""));
        client.print(ip[3],DEC);
        //
        client.print(F("\"></td></tr><tr><td>MASK: </td><td><input type= \"text\" size=\"3\" maxlength=\"3\" name=\"DT11\" value=\""));
        client.print(subnet[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT12\" value=\""));
        client.print(subnet[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT13\" value=\""));
        client.print(subnet[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT14\" value=\""));
        client.print(subnet[3],DEC);
        //
        client.print(F("\"></td></tr><tr><td>GW: </td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT15\" value=\""));
        client.print(gateway[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT16\" value=\""));
        client.print(gateway[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT17\" value=\""));
        client.print(gateway[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT18\" value=\""));
        client.print(gateway[3],DEC);
        client.print(F("\"></td></tr><tr><td><br></td></tr><tr><td><input id=\"button1\"type=\"submit\" value=\"SUBMIT\" "));
        //
        client.print(F("Onclick=\"document.getElementById('T2').value "));
        client.print(F("= hex2num(document.getElementById('T1').value);"));
        client.print(F("document.getElementById('T4').value = hex2num(document.getElementById('T3').value);"));
        client.print(F("document.getElementById('T6').value = hex2num(document.getElementById('T5').value);"));
        client.print(F("document.getElementById('T8').value = hex2num(document.getElementById('T7').value);"));
        client.print(F("document.getElementById('T10').value = hex2num(document.getElementById('T9').value);"));
        client.print(F("document.getElementById('T12').value = hex2num(document.getElementById('T11').value);\""));
        //
        client.print(F("></td><td></td></tr></form></table></BLOCKQUOTE></BLOCKQUOTE></BODY></html>"));
}
//--------------------------
void checkWebPage( EthernetClient client)
{
        Serial.println(F("new webClient"));
        if (client) {
                TextFinder  finder(client );
                while (client.connected()) {
                        //digitalWrite(LED, HIGH);
                        if (client.available()) {
                                //この部分はすべてのテキスト検索を行います。
                                if( finder.find("GET /") ) {
                                        // 「setup」とい語が見つかった場合は、さらに探してください。
                                        // その単語が見つからない場合は、検索を停止して先に進みます。
                                        // これにより、後でスケッチに独自のWebページを配置できます。
                                        if (finder.findUntil("setup", "\n\r")){
                                                // 「SBM」という単語が見つかった場合は、さらに探し続けます。
                                                // その言葉が見つからない場合は、探して停止します。
                                                // SUBMITボタンが押されていない、何も押されていないことを意味します
                                                // セットアップページが構築されている場所に移動し、クライアントのブラウザに表示します。
                                                if (finder.findUntil("SBM", "\n\r")){
                                                        byte SET = finder.getValue();
                                                        // これで、「DT」という文字を探している間に、「DT」の後ろにあるすべての数字を覚えて、
                                                        // 値を一致させて、mac、ip、subnet、およびgatewayに入れる必要があります。
                                                        while(finder.findUntil("DT", "\n\r")){
                                                                int val = finder.getValue();
                                                                // 「DT」のvalが1?6の場合、対応する値はMAC値でなければなりません。
                                                                if(val >= 1 && val <= 6) {
                                                                        mac[val - 1] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが7?10の場合、対応する値はIP値でなければなりません。
                                                                if(val >= 7 && val <= 10) {
                                                                        ip[val - 7] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが11?14の場合、対応する値はMASK値でなければなりません。
                                                                if(val >= 11 && val <= 14) {
                                                                        subnet[val - 11] = finder.getValue();
                                                                }
                                                                // 「DT」のvalが15?18の場合、対応する値はGW値でなければなりません。
                                                                if(val >= 15 && val <= 18) {
                                                                        gateway[val - 15] = finder.getValue();
                                                                }
                                                        }
                                                        // すべてのデータを取得したので、EEPROMに保存できます
                                                        for (int i = 0 ; i < 6; i++){
                                                                EEPROM.write(i + 1,mac[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 7, ip[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 11, subnet[i]);
                                                        }
                                                        for (int i = 0 ; i < 4; i++){
                                                                EEPROM.write(i + 15, gateway[i]);
                                                        }
                                                        // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                                                        EEPROM.write(0, ID); 
                                                        // すべてのデータがEEPROMに書き込まれている場合、arduinoをリセットする必要があります。
                                                        //ハードウェアリセットボタンを使用する必要があります。
                                                }
                                                // この時点から、セットアップページの構築を開始し、クライアントのブラウザーに表示できます。
                                                client.println("HTTP/1.1 200 OK");
                                                client.println("Content-Type: text/html");
                                                client.println();
                                                //
                                                SetWebPage(client);
                                                
                                                break;
                                        }
                                }
                                client.println("HTTP/1.1 200 OK");
                                client.println("Content-Type: text/html");
                                client.println();
                                // put your own html from here on
                                client.print("IT WORKS: go to ");
                                client.print(ip[0],DEC);
                                for (int i= 1; i < 4; i++){
                                        client.print(".");
                                        client.print(ip[i],DEC);
                                }
                                client.print("/setup");
                                // put your own html until here 
                                break;  
                        }
                }
                //digitalWrite(LED, LOW);
                delay(1);
                client.stop();
        }     
}
//----------------------------
int     GetDeviceInfo(){
        for (int i=0;i<DIbits;i++){
                Mb.SetBit(MB_FC_READ_DISCRETE_INPUT,i,!digitalRead(dIsPins[i]));        //bit 論理反転
        }
        for (int i=0;i<AIbits;i++){
                Mb.MbInputData[i]=analogRead(aIsPins[i]);                               //U16
        }
        return OK;            
}
//----------------------------
int     SetDeviceInfo(){
        for (int i=0;i<DObits;i++){          
                digitalWrite(dOsPins[i],!Mb.GetBit(MB_FC_READ_COILS,i));                 //LEDはLOWで点灯                            
        }
        if(Mb.MbHoldData[0] != preData){
                preData =       Mb.MbHoldData[0];
                dac.setValue(preData, MCP4725_FAST_MODE, MCP4725_POWER_DOWN_OFF);
                Serial.print(F("SetDAC = "));Serial.println(preData);
        }
        return OK;
}
//----------------------------
int     SetI2CDeviceInfo(){
        //BME280計測
        double rf;
        BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
        BME280::PresUnit presUnit(BME280::PresUnit_Pa);
        bme.read(pres, temp, hum, tempUnit, presUnit);
        //InputDataのIndex=0に温度をセット INTにする必要があるので100倍して整数化 25.65->2565
        if(!ErrorTMP)   Mb.MbInputData[1]        = int(temp*100);
        //InputDataのIndex=1に湿度をセット INTにする必要があるので100倍して整数化 57.88->5788
        if(!ErrorHUM)   Mb.MbInputData[2]        = int(hum*100);
        //InputDataのIndex=1に湿度をセット INTにする必要がある[mbar]101300->1013 i16 に収めるため
        if(!ErrorPRS)   Mb.MbInputData[3]        = int(pres/100);
}
//--------------------------
void printError(byte error){
        // If there's an I2C error, this function will
        // print out an explanation.
        Serial.print(F("I2C error: "));
        Serial.print(error,DEC);
        Serial.print(F(", "));
  
        switch(error){
                case 0:
                        Serial.println(F("success"));
                        break;
                case 1:
                        Serial.println(F("data too long for transmit buffer"));
                        break;
                case 2:
                        Serial.println(F("received NACK on address (disconnected?)"));
                        break;
                case 3:
                        Serial.println(F("received NACK on data"));
                        break;
                case 4:
                        Serial.println(F("other error"));
                        break;
                default:
                        Serial.println(F("unknown error"));
        }
}
//----------------------------
void setup()
{
        // serial setup
        Serial.begin(115200);
        delay(1000);
        Serial.println(F("Serial interface started"));
        pinMode(LED_BUILTIN, OUTPUT);digitalWrite(LED_BUILTIN, HIGH);
        SPI.begin();                                                    //Initialize the SPI_1 port.
        SPI.setBitOrder(MSBFIRST);                                      // Set the SPI_1 bit order
        SPI.setClockDivider(SPI_CLOCK_DIV4);                            //SPI_CLOCK_DIV16 (72 / 16 = 4.5 MHz SPI_1 speed)
                                                                        //SPI_CLOCK_DIV4 (72 / 4 = 18 MHz SPI_1 speed)
        //Ethernet3で使用可能なAPI
        pinMode(SPI1_NSS_PIN, OUTPUT);                                  //NIC_CS出力設定
        Ethernet.setCsPin(SPI1_NSS_PIN);                                //NIC_CSアサイン
        Ethernet.setRstPin(W550io_Rst);                                 //NIC_RSTアサイン
        //NICリセット処理
        pinMode(W550io_Rst, OUTPUT);
        digitalWrite(W550io_Rst, LOW);
        delay(10);                                                      //10msecパルス幅で初期化
        digitalWrite(W550io_Rst, HIGH);
        LANSetup();
        Serial.println(F("Ethernet interface started")); 
        Serial.print(F("My IP address: "));Serial.println(Ethernet.localIP());// print your local IP address:
        webServer.begin();
        //I2Cは接続されているかどうかチェックしてから対応することにした。
        Wire.begin();
        Serial.print(F("I2C_Start\n"));
        //BME280初期化 BMP280の対策
        while(!bme.begin())
        {
                Serial.println(F("Could not find BME280 sensor!"));
                delay(1000);
        }
        // bme.chipID(); // Deprecated. See chipModel().
        switch(bme.chipModel()){
                case BME280::ChipModel_BME280:
                        Serial.println(F("Found BME280 sensor! Success."));
                        break;
                case BME280::ChipModel_BMP280:
                        Serial.println(F("Found BMP280 sensor! No Humidity available."));
                        ErrorHUM = true;
                        break;
                default:
                        Serial.println(F("Found UNKNOWN sensor! Error!"));
                        ErrorTMP = true;
                        ErrorHUM = true;
                        ErrorPRS = true;
        }
        //DAC(MCP4725)の初期化
        while (dac.begin() != true){
                Serial.println(F("MCP4725 is not connected"));          //(F())文字列をフラッシュに保存し、動的メモリを解放します
                delay(5000);
        }
        Serial.println(F("MCP4725 is OK"));
        Wire.setClock(I2C_BUS_SPEED);                                   //i2c bus speed 100kです。パタン出力したりするときは400K        
        //ModBusメモリ 初期化
        for(int i = 0;i<MbCoilDataLen;i++)      Mb.MbCoilData[i]=0x0000;//メモリ初期化
        for(int i = 0;i<MbDiscreteDataLen;i++)  Mb.MbDiscreteData[i]=0; //メモリ初期化
        for(int i = 0;i<MbHoldDataLen;i++)      Mb.MbHoldData[i]=0;     //メモリ初期化
        for(int i = 0;i<MbInputDataLen;i++)     Mb.MbInputData[i]=0;    //メモリ初期化
        //出力LED初期化
        for (int i=0;i<DObits;i++){          
                Mb.SetBit(MB_FC_READ_COILS,i,false);                    //メモリマップはMb.MbData[0]
                pinMode(dOsPins[i],OUTPUT);
                digitalWrite(dOsPins[i], LOW);                         //LEDはHIGHで点灯                            
        }
        //入力pin初期化
        for (int i=0;i<DIbits;i++){
                pinMode(dIsPins[i],INPUT);
                Mb.SetBit(MB_FC_READ_DISCRETE_INPUT,i,!digitalRead(dIsPins[i]));        //bit メモリマップはMb.MbData[1]
        }
        for (int i=0;i<AIbits;i++){
                Mb.MbInputData[i] = analogRead(aIsPins[i]);             //U16
        }
        Serial.println(F("EndSetUp"));
}
//----------------------------
int LoopCount = 0;
void loop()
{
        if(!LoopCount++)        Serial.println(F("LoopStart"));
        client = webServer.available();
        if(client){
                //digitalWrite(LED, LOW);
                checkWebPage(client);
        }else{
                //digitalWrite(LED, HIGH);           
        }
        client.stop();                                                  // コネクションを閉じる。
        GetDeviceInfo();
        Mb.MbsRun();                                                    //SlaveServer
        SetDeviceInfo();
        SetI2CDeviceInfo();
        if(!(LoopCount%1000))   Serial.print(F("."));
        if(!(LoopCount%40000))  Serial.println(F(""));
        byte keys = LEDandKEY.getButtons();
        switch(keys){
                case 0x01:
                        LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[1]),4,false);
                        lastKey = keys;
                        Serial.print(F("keys=0x"));Serial.println(lastKey,HEX);
                        break;
                case 0x02:
                        LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[2]),4,false);
                        lastKey = keys;
                        Serial.print(F("keys=0x"));Serial.println(lastKey,HEX);
                        break;
                case 0x04:
                        LEDandKEY.setDisplayToDecNumber(int(pres),0,false);
                        lastKey = keys;
                        Serial.print(F("keys=0x"));Serial.println(lastKey,HEX);
                        break;
                case 0x10:
                        LEDandKEY.setDisplayToDecNumber(int(Mb.MbHoldData[0]),0,false);
                        lastKey = keys;
                        Serial.print(F("keys=0x"));Serial.println(lastKey,HEX);
                        break;
                case 0x20:
                        LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[0]),0,false);
                        lastKey = keys;
                        Serial.print(F("keys=0x"));Serial.println(lastKey,HEX);
                        break;
        } 
        LEDandKEY.setLEDs(Mb.MbCoilData[0]>>8);                   //LED&KEYのLED点灯  
        if(Mb.GetBit(MB_FC_READ_COILS,7) && lastKey){
                switch(lastKey){
                        case 0x01:
                                LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[1]),4,false);
                                break;
                        case 0x02:
                                LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[2]),4,false);
                                break;
                        case 0x04:
                                LEDandKEY.setDisplayToDecNumber(int(pres),0,false);
                                break;
                        case 0x10:
                                LEDandKEY.setDisplayToDecNumber(int(Mb.MbHoldData[0]),0,false);
                                break;
                        case 0x20:
                                LEDandKEY.setDisplayToDecNumber(int(Mb.MbInputData[0]),0,false);
                                break;
                }
        }        
}
//----------------------------


ホスト側ModbusTCPソフトはiPod/iPhoneでも提供されています。iAppでModbusソフトを探してみました。
残念ながら汎用品は無いようです。どれも専用のスレーブデバイス用のようです。
そこで、どのようにして専用化しているのか調査してみました。具体的にはSTM32MINIShieldに実際にアクセスした際にどのようなコマンドを投げかけているのか調査です。結果をお知らせします。
とりあえず制限はあるもののmTCPは使える様です。

Sealevel Modbus Connect

一瞬接続して、すぐに勝手にdisconnectされてしまいます。原因不明

  • “PING”機能があります。PINGは問題なく成功しました。
  • 接続しようとするとSealevelデバイスでは無いと云われます。
    REQUEST:00 03 00 00 00 02 f7 45
    というメッセージが流れてくるのです。他のソフトの場合、コネクト時にメッセージは送られてこないのです。多分このメッセージに応答出来ないので Sealevel デバイスではないという解釈をされて
    いると考えます。
    このメッセージの解釈ですが、
    解釈として、
    00 03 は単にシリアルカウント値かと思います。回数を追う毎にインクリメントされます。
    0x0000は開始アドレスかと思われます。
    0x0002はメッセージサイズかと思います。
    0xf7は指定したUnitIDのようです。ID=247を意味します。
    0x45はデリミタでしょうか?判りません。

独自の処理についてはこれ以上追わないことにします。

irBoard Lite 主旨が違いました
CS Modbus 接続出来ました。
コネクトすると設定条件に応じた要求メッセージを送り、応答メッセージをコンソールに書き込んでいるようです。ただ、これでは解釈できないです。
mTCP これちゃんと接続及び読み込みできました。
https://apps.apple.com/jp/app/mtcp/id1467488841#?platform=iphone
書込なのですが、Holdレジスタのみ書込が出来る事を確認しました。
Coilの書込方法が判りません
TCP ModBus 接続出来ました。
コマンドが3と6しか無いです。
Q-view 主旨が違うような、、
ET-7000: a Modbus TCP connect tool for the ET-7000/ PET-7000 serious modules of ICP DAS 接続出来ませんでした。Slaveデバイスを選択してからアクセスするのですが、詳細わからず。
Connectを実行したところ以下の要求コマンドが発信されました。
REQUEST:01 02 00 00 00 06 01 13 df e4 00 00
7バイト目の0x01は要求IDであることは判りました。それ以外は
8バイト目の0x13は固定でしょうか?
9バイト目10バイト目は指定ターゲットに起因するようです。
ModComm: Peek & Poke 作者は日本人の方のようです。Kei Sakaguchiとあります。
使ってみました。これ期待通り操作できます。
まず読込を実行し、値を確認しますこのとき範囲指定が枠内で無いとエラーが出るところも有りがたいです。
Coilと保持レジスタは値を変更して書込が出来ます。ちゃんと反映しました。
無償です。使えます。


オブジェクト自体はベクター殿のストレージをお借りしています。

https://www.vector.co.jp/vpack/browse/person/an051501.html



免責事項

本ソフトウエアは、あなたに対して何も保証しません。本ソフトウエアの関係者(他の利用者も含む)は、あなたに対して一切責任を負いません。
あなたが、本ソフトウエアを利用(コンパイル後の再利用など全てを含む)する場合は、自己責任で行う必要があります。

本ソフトウエアの著作権はToolsBoxに帰属します。
本ソフトウエアをご利用の結果生じた損害について、ToolsBoxは一切責任を負いません。
ToolsBoxはコンテンツとして提供する全ての文章、画像等について、内容の合法性・正確性・安全性等、において最善の注意をし、作成していますが、保証するものではありません。
ToolsBoxはリンクをしている外部サイトについては、何ら保証しません。
ToolsBoxは事前の予告無く、本ソフトウエアの開発・提供を中止する可能性があります。

商標・登録商標

Microsoft、Windows、WindowsNTは米国Microsoft Corporationの米国およびその他の国における登録商標です。
Windows Vista、Windows XPは、米国Microsoft Corporation.の商品名称です。
LabVIEW、National Instruments、NI、ni.comはNational Instrumentsの登録商標です。
I2Cは、NXP Semiconductors社の登録商標です。
その他の企業名ならびに製品名は、それぞれの会社の商標もしくは登録商標です。
すべての商標および登録商標は、それぞれの所有者に帰属します。