1-Wire デバイスを使ってみよう
手軽にRaspberry pi やArduino等で扱えるシリアル通信技術の一つに、I2CやSPIと並んで、1-Wireというのがあります。信号線とGND線というわずか2本で接続が行える、そして複数接続が行えるという、とても便利な技術です。1-Wireを持ったデバイスにはメモリや温度センサーなどありますが、今回はDS28E07という1024bitのメモリデバイスを4つ手に入れたので、この4つのDS28E07とArduinoを接続しDS28E07にデータを読み書きしてみたので、それを紹介する記事です。
きっかけは、インクジェットプリンターのインクカートリッジの備え付けられているデバイスは、1-Wireまたは相当の通信技術で制御されており、例えば消耗品管理等で仕事上これを活用する場面を想定して勉強しておきたいなと思っていたところにあります。 ただし、本稿では1-Wireのハードウエア仕様や、通信の仕組み、デバイス検索のアルゴリズムなどの技術的な話にはほぼ触れておらず、ライブラリを活用して1-Wireデバイスへのアクセス方法を概観する範囲に留まります。
1-Wireとは
1-Wireは、Dallas Semiconductor(Maxim Integratedに買収され、その後Maxim IntegratedはAnalog Devicesに買収された。)によって開発された通信プロトコルです。1本のデータ線と1本のグランド線を使って、デバイス間でデータの送受信を行います。1-Wireは、低速でシンプルな通信を提供し、特にセンサやメモリなどの小型デバイスに向いています。主な特徴は次のとおりです。
- シンプルな構成: 通信には基本的に1本のデータ線とグランド線があれば良い。データ線は、データの送受信と電力供給の両方に使用される。
- 電力供給: 多くの1-Wireデバイスは「パラサイトパワー」と呼ばれる方式で動作し、データ線から電力を得ることができます。デイバス内のコンデンサにより電荷がチャージされ電源として用いる。このため、外部電源を必要としない場合がある。
- マスタ/スレーブ構造: 1-Wireネットワークは、1つのマスタデバイスと1つ以上のスレーブデバイスで構成される。マスタは通信の制御を行い、スレーブはマスタからの指令に従う。
- ユニークな識別コード: 各1-Wireデバイスには、64ビットのユニークな識別コード(ROMコード)が割り当てられており、ネットワーク上の複数のデバイスを区別するために使用される。うち1バイトはデバイスのファミリーコードを示している。
- 低速通信: 1-Wireのデータ転送速度は比較的低速で、通常は15.3kbpsか、オーバードライブモードで125kbps程度である。
1-Wireデバイスにはどんなものがあるか
1-Wireは、センサ、メモリ、リアルタイムクロックなど、多くの小型デバイスで広く使用されています。たとえば、温度センサのDS18B20や、DS28E07のようなメモリデバイスなどが一般的な1-Wireデバイスです。データの改ざんが困難な仕組みを持つため、プリンタのカートリッジや医療用の 消耗品の識別/使用状況の監視、ラック・カードやプリント基板の識別、医療用センサーのキャリブレーション・データの保存、アクセサリ/ペリフェラルの識別等の活用に実績があるようです。この技術を採用する(プリンターや医療機器)メーカーの採用理由としては、自社のビジネスを守るという観点と自社の提供するサービスの信頼性を守ることで顧客を守るという観点と、大きくは2つあるでしょう。その課題解決に1-wireが役に立っているようです。
詳しい解説は、Analog Devicesのこちらの記事にあります。(1-Wire技術の概要、その用途)
今回の実行環境
・Arduino Uno
・Arduino IDE version 1.18.19
・OneWireライブラリ version 2.3.7
・Arduino Unoの10番ピンを1-Wireの信号線として使用
・Arduino Unoの5vまたは3.3vと上記信号ラインに1kΩを接続
・DS28E07を4つ、信号ラインとGNDラインで数珠つなぎ(上の写真のとおりです)
作成したプログラムでできること
Onewireライブラリを活用しています。デバイスの検索、指定したデバイスのメモリ内データの読み込みと表示、指定したデバイスへのデータ書き込み(0x00や0xFF)が行えます。Arduino起動後シリアルモニタにより実行したいメニューの選択と、実行結果の確認を行うことができます。
以下に実行の様子を示します。見つかった4つのデバイスのROM IDが表示されています。メニュー画面が表示された後で、Menu番号として、"2"を送信して、続いてIndex numberとして"0"を送信した結果、このプログラム上におけるIndex number = 0に該当するROM IDのデバイスのユーザー領域である128バイト分のデータが表示されます。(注意:このプログラムでは数字の送信時に改行コードなどのコードは送信しないようにしています。)
続いては、メニューから"3"(データ書き込み)を選んで、続いてIndex number "0"を選び、書き込みが実行された後で、Menu"2"を実行して正しく書かれたかどうかを確認した様子を示しています。0x00で埋めるようにしたので、先ほどの0xAAのところが0x00に変わってるのを確認できます。
TA1 で始まる行が複数あって、これは何のことかわからないかと思います。後述します。
上記のArduinoのコードを貼っておきます。
#include <OneWire.h>
OneWire ds(10); // on pin 10
#define ONE_WIRE_MAX_SEARCH_NUM 10
#define ONE_WIRE_ID_LENGTH 8
int i,p,r;
struct OneWireDevice {
uint8_t id[ONE_WIRE_ID_LENGTH];
uint8_t data;
};
uint8_t foundDeviceNum=0;
uint8_t writeDataBuffer[8] = {0x00};
OneWireDevice OneWireDev[ONE_WIRE_MAX_SEARCH_NUM];
uint8_t SearchDevice()
{
foundDeviceNum=0;
uint8_t rdata[8]={0x00};
String id="";
if(foundDeviceNum>=ONE_WIRE_MAX_SEARCH_NUM)
return false;
while(1){
/*デバイスの検索*/
if ( !ds.search(rdata)) {
if(foundDeviceNum==0){
Serial.println("Can not find the device");
ds.reset_search();
delay(5000);
} else {
Serial.print(foundDeviceNum);
Serial.println("devices were found.");
}
break;
} else {
id= String(rdata[0], 16) + ":"
+ String(rdata[1], 16) + ":"
+ String(rdata[2], 16) + ":"
+ String(rdata[3], 16) + ":"
+ String(rdata[4], 16) + ":"
+ String(rdata[5], 16) + ":"
+ String(rdata[6], 16) + ":"
+ String(rdata[7], 16) ;
Serial.print("Found Device:");
Serial.print(id);
Serial.print(" :Managed as index number:");
Serial.println(foundDeviceNum);
OneWireDev[foundDeviceNum].id[0]=rdata[0];
OneWireDev[foundDeviceNum].id[1]=rdata[1];
OneWireDev[foundDeviceNum].id[2]=rdata[2];
OneWireDev[foundDeviceNum].id[3]=rdata[3];
OneWireDev[foundDeviceNum].id[4]=rdata[4];
OneWireDev[foundDeviceNum].id[5]=rdata[5];
OneWireDev[foundDeviceNum].id[6]=rdata[6];
OneWireDev[foundDeviceNum].id[7]=rdata[7];
foundDeviceNum++;
}
}
return foundDeviceNum;
}
void display_menu()
{
Serial.println("");
Serial.println("// Menu ");
Serial.println("// 1: Serch Device");
Serial.println("// 2: Read data from Device(128bytes)");
Serial.println("// 3: Write data to Device(128bytes) ");
Serial.println("// 4: Change the writing data");
Serial.println("// 5: Debug");
Serial.println("// Select menu:");
}
void menu(byte value)
{
switch (value) {
case '1':
option_1();
break;
case '2':
option_2();
break;
case '3':
option_3();
break;
case '4':
option_4();
break;
case '5':
option_5();
break;
default:
Serial.println("unexpected command, try again");
break;
}
}
void option_1()
{
Serial.println("SercheDevice");
SearchDevice();
}
void option_2()
{
Serial.println("Read data from Device(128bytes)");
Serial.println("Input the index number of target device:");
while(1)
{
if (Serial.available() > 0) {
uint8_t ascii = Serial.read();
uint8_t index = ascii - '0';
if(index > (foundDeviceNum-1))
{
Serial.println("The number is out of range. Please try again.");
return;
}
read_eeprom(index, 0x00);
return;
}
delay(10);
}
}
void option_3()
{
Serial.println("Write data to Device(128bytes)");
Serial.println("Input the index number of target device:");
while(1)
{
if (Serial.available() > 0) {
uint8_t ascii = Serial.read();
uint8_t index = ascii - '0';
if(index > (foundDeviceNum-1))
{
Serial.println("The number is out of range. Please try again.");
return;
}
Serial.println(" ");
Serial.print("Selected Index =");
Serial.print(index);
Serial.print(", ROM ID =");
for (int i = 0; i < 8; i++) {
Serial.print(OneWireDev[index].id[i], HEX);
Serial.print(" ");
}
Serial.println(" ");
write_allusermemoy(index, writeDataBuffer);
return;
}
delay(10);
}
}
void option_5()
{
Serial.println("Menu no.5 execution..");
for(int index=0; index < 4; index++){
for (int i = 0; i < 8; i++) {
Serial.print(OneWireDev[index].id[i], HEX);
Serial.print(" ");
}
Serial.println(" ");
}
}
void option_4(){
uint8_t dataBuffer_zero[8] = {0x00};
uint8_t dataBuffer_incri[8] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};
uint8_t dataBuffer_AA[8] = {0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA};
uint8_t dataBuffer_FF[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
Serial.println("The data to be written can be changed.The default is all 0x00");
Serial.println("1. all 0x000");
Serial.println("2. incremental data 0x00 - 0x07");
Serial.println("3. all 0xAA");
Serial.println("4. all 0xFF");
Serial.println("Input number : ");
Serial.println(" ");
while(1)
{
if (Serial.available() > 0) {
byte value = Serial.read();
switch (value) {
case '1':
memcpy(writeDataBuffer, dataBuffer_zero, sizeof(dataBuffer_zero));
Serial.println("Changed to all 0x00");
return;
case '2':
memcpy(writeDataBuffer, dataBuffer_incri, sizeof(dataBuffer_incri));
Serial.println("Changed toa incremental data 0x00 -0x07");
return;
case '3':
memcpy(writeDataBuffer, dataBuffer_AA, sizeof(dataBuffer_AA));
Serial.println("Changed to all 0xAA");
return;
case '4':
memcpy(writeDataBuffer, dataBuffer_FF, sizeof(dataBuffer_FF));
Serial.println("Changed to all 0xFF");
return;
default:
Serial.println("unexpected command");
return;
}
}
}
delay(10);
}
void setup(void) {
delay(1000);
Serial.begin(9600);
Serial.println("Start communicating with 1-Wire Device");
SearchDevice();
delay(1000);
}
void loop(void) {
if(foundDeviceNum){
display_menu();
// wait for inputting ascii data from serial port
while(1)
{
if (Serial.available() > 0) {
byte value = Serial.read();
menu(value);
break;
}
delay(10);
}
delay(200);
}
delay(200);
}
/*************************************************************************/
void write_allusermemoy(uint8_t index, uint8_t* dataBuffer){
Serial.println("Write data to the User memory area(128byte) 8bytes at a time");
for(byte address=0x00; address<=0x78;address=address+0x08){
write_eeprom(index, address, dataBuffer);
delay(100);
}
}
/************************** READ_EEPROM ***********************************/
void read_eeprom(uint8_t index, uint8_t address){
uint8_t romid[8] = {0x00};
uint8_t readdata[128] = {0x00};
// TargetのROM IDを設定
for (int i = 0; i < 8; i++) {
romid[i] = OneWireDev[index].id[i];
}
Serial.println(" ");
Serial.print("Selected ROM ID =");
for (int i = 0; i < 8; i++) {
Serial.print(romid[i], HEX);
Serial.print(" ");
}
Serial.println(" ");
// Read sequence
ds.reset();
ds.write(0x55); // match command send
ds.write_bytes(romid, 8); // rom id send
ds.write(0xf0); // eeprom read command send
/*
The memory address must be specified in 2 bytes. The lower byte is sent first, and the upper byte is sent later.
The upper byte shall be fixed at 0x00.
*/
ds.write(address); //adrees
ds.write(0x00); //adrees
/*Read data from EEPROM*/
Serial.println("User memory Area of this EEPROM DATA (128byte) = ");
for (int i = 0; i < 128; i++) {
readdata[i] = ds.read();
}
/*読み取ったEEPROMの表示*/
for (int i = 0; i < 128; i++) {
Serial.print(readdata[i], HEX);
Serial.print(" ");
if(i % 32 == 31) { // 32個の値ごとに改行
Serial.println("");
}
}
Serial.println(" ");
}
/************************** WRITE_EEPROM ***********************************/
void write_eeprom(uint8_t index, uint8_t address, uint8_t* dataBuffer){
uint8_t romid[8] = {0x00};
uint8_t readScratchedData[8] = {0x00};
// TargetのROM IDを設定
for (int i = 0; i < 8; i++) {
romid[i] = OneWireDev[index].id[i];
}
/*データをスクラッチパットへの書き込むシーケンス*/
ds.reset();
ds.write(0x55); // match command send
ds.write_bytes(romid, 8); // rom id send
ds.write(0x0f); //scratchpad_write
/*
The memory address must be specified in 2 bytes. The lower byte is sent first, and the upper byte is sent later.
The upper byte shall be fixed at 0x00.
*/
ds.write(address); //write address TA1 書き込み先アドレス
ds.write(0x00); //write address TA2
delay(10);
/*8byteのデータの書き込み*/
for ( i = 0; i <= 7; i++){
ds.write(dataBuffer[i]);
}
Serial.println("");
delay(10);
/*スクラッチパットデータの読み込み(書き込むべきデータのチェックのために行う)*/
ds.reset();
ds.write(0x55); // match command send
ds.write_bytes(romid, 8); // rom id send
ds.write(0xaa); //scratchpad_read
delay(10);
/*レジスタ部の読み込み*/
byte T1=ds.read();
byte T2=ds.read();
byte E_S=ds.read();
/*(読み取ったレジスタの表示)*/
Serial.print("TA1=");
Serial.print(T1,HEX);
Serial.print(" TA2=");
Serial.print(T2,HEX);
Serial.print(" E/S=");
Serial.print(E_S,HEX);
delay(10);
/*(スクラッチパットのデータ読み込み)*/
Serial.print(", read scratchpad deta= ");
for ( i = 0; i <= 7; i++){
readScratchedData[i] = ds.read();
}
/*(読み取ったスクラッチパットの表示)*/
for ( i = 0; i <= 7; i++) {
Serial.print(readScratchedData[i], HEX);
Serial.print(" ");
}
delay(500);
/*スクラッチパットのコピーをEEPROMに書き込み*/
ds.reset();
ds.write(0x55); // match command send
ds.write_bytes(romid, 8); // rom id send
ds.write(0x55); //scratchpad_copy
/*
The memory address must be specified in 2 bytes. The lower byte is sent first, and the upper byte is sent later.
The upper byte shall be fixed at 0x00.
*/
ds.write(address); //write address TA1
ds.write(0x00); //write address TA2
ds.write(E_S); //write address e/s
delay(5);//書き終わるまで待つ
}
さて、コードの中身ですが、デバイスからデータを読む関数read_eepromとデータを書き込むwrite_eepromが主要なところです。このコードを見る前に、1-Wireデバイスのメモリ空間へのリード、ライトの作法をつかんでおきましょう。そうするとコードを理解し易くなるでしょう。
1-Wireデバイスへのアクセス手順
おおきく2つに分けることができます。ROM Command と Function Commandです。どちらも複数コマンドがあってその総称ですが、ROM Commandは1-Wireバス上の1ーWireデバイス全体へのコマンド群であり、Function Commandは、特定の1-Wireデバイスへのコマンド群です。いきなりFunction Command群のコマンドを発行しても機能しません。次のステップを踏むことになります。
☆1-Wireデバイスアクセスのための基本シーケンス
・1-Wire のバスリセット
・ROM Command 実行
・Function Command 実行
ROM Command と Function Command
今回扱う1-Wireデバイス(DS28E07)のデータシート中身を見ていきます。
☆ROM Commandの種類
・READ ROM (33h)バス上のデバイスが一つの時 デバイスの固有IDを読む。
・MATCH ROM(55h) 既知のROM IDによって、バス上のデバイスを特定する。
・SEARCH ROM(F0h) バス上のデバイスを見つける。
・SKIP ROM(CCh) バス上のデバイスが一つの時 デバイスの選択をスキップする。
・RESUME(A5h) 選択したデバイスとの通信を再開する。
・OVERDRIVE-SKIP(3Ch) ROM :デバイスの選択をスキップして、デバイスをオーバードライブする。
・OVOERDRIVE-MATCH(69h) ROM :デバイスを指定して、デバイスをオーバードライブする。
今回使用したのはMATCH ROMコマンドです。(デバイス検索の細かい手順は活用したライブラリ関数がやってくれるので、SEARCH ROMを自身では使いませんでした。) デバイス検索をしてデバイスが見つかると、ROM IDという8バイトの固有データを取得できます。1-Wireバス上にデバイスが一つしか無い場合はROM IDを用いる必要はありませんが、今回の環境はデバイスが複数あるため、デバイスへのデータ書き込みや読み込みの際にはデバイスを指定するためにこのROM IDを用いることになります。具体的には、上述リストのうちの、MATCH ROMコマンドを発行して、続けてROM IDを送信します。そうすることで、その後発行するFunction Commandがどのデバイスであるのか指定されたことになります。
☆Function Command
Function Commandの種類はデバイスが持つ機能によって異なるようです。DS28E07では次の4つのFunction Commandがありました。
・WRITE SCRATCHPAD(0Fh) :スクラッチパットにデータを書き込む。
・READ SCRATCHPAD(AAh) :スクラッチパットにデータを読み込む。
・COPY SCRATCHPAD(55h) :スクラッチパットに書き込んだデータをEEPROMに書き込む。
・READ MEMORY(F0h) :EEPROMのデータを読み込む。
スクラッチパッド
意図したデータが正しくデバイスに伝わったことを確認した上で、メモリを上書きする仕組みがあります。これがスクラッチパッドであり、データを書き込む際は、いったんスクラッチパッドにデータを書き込み、続いてTAというレジスタにデータを書き込む先頭アドレスを書き込みます。(2バイトで指定)そして、TAというレジスタとE/Sというレジスタ(後者はステータスを示す)の中と、スクラッチパッドにあるデータを確認して、問題なければ続いてCOPY SCRATCHEPAD COMMANDを発行することで、スクラッチパッドのデータがTAで示されるアドレスを先頭に実際のメモリに書き込みが実行されます。よって、データの書き込みは、WRITE SCRATCHPAD COMMAND、READ SCRATCHEDPAD COMMAND、COPY SCRATCHEDPAD COMMANDをセットで用います。このデバイスではSCRATCHPADのデータサイズは8バイトです。よって、128バイトのメモリ空間全体にデータを書き込むにあたって、8バイトずつスクラッチパッドへのデータ書き込みを繰り返しました。それが上述のデータライトの様子です。(TAで始まる行が複数あるところ)
これらコマンドの発行以外に、データの書き込みや、データの読み込みというコードがありますが上記概要を把握した上で、コードを見ると動作を容易に理解できると思います。
リファレンス
こちらの記事を読み、公開されているコードに改造を加えたのが上述のコードになります。大変勉強になりました。1-Wireのコマンドシーケンスなるものを理解するのに助かりました。記事を読んだ上でデータシートを読むことで理解しやすくなりました。ありがとうございます。
特記
Arduinoって、Serial.printを使うのに大量にメモリーを消費するようで使用率が73%を越えたあたりから、動作が不安定になります。今回Serial.printを使ったメニュー画面なんか設けたものだから、こういう問題に気づきました。
1-Wireの通信を行うにあたって、信号ラインには電源へのプルアップ抵抗が必要なのですが、これは電源電圧と使うデバイスによって適正値が異なるため、1-Wireデバイスを扱った記事で使われいてる抵抗値そのまま使うのでは無く、自身が使う電源電圧やデバイスのデータシート上の記載を確認した上で抵抗値を選びましょう。