2016年06月02日 | 公開。 |
2016年06月16日 | Arduino IDE 1.6.6以降で半角カナが拡張ASCII表現になるのは、環境依存であるらしい事を追記。 マクロ定数のVERSIONよりArduino IDEのバージョンを検出して動作を切り替えるサンプルスケッチを削除。 |
2016年06月17日 | タイトルを「カナや漢字を含む文字列の内部表現がArduino IDE 1.6.6で変更される」から「Arduino IDE 1.6.6に文字コード関係のバグ?」に変更。また、それに伴い記事の内容の一部を変更。 ConvStr関数を掲載。 |
Arduino IDE 1.6.6以降において、半角カナおよび漢字を含む文字列の扱いに不具合が発生する事がある模様です。この不具合は、Arduino IDEの設定の問題なのか、あるいはバグなのかは現在のところ確認できていません。また、Arduino IDE 1.7.Xではこの不具合は発生しません。暫定的ではありますが、現在当方で確認できている問題点を報告します。
注:この記事は、当初「カナや漢字を含む文字列の内部表現がArduino IDE 1.6.6で変更される」というタイトルでした。記事を書いた当初は、Arduino IDE 1.6.6で文字コードの扱いの仕様に変更があったものと思い込んでいたのですが、記事を書いた後に「状況が再現できない」という指摘を受け、改題した上で、記事を書き直しました。なお、現在も引き続き、この不具合がどのような環境で発生するかを調査しています。皆さんの環境で、この記事で説明した不具合が再現するかどうかを教えてくださるとありがたいです。(管理人のTwitter アカウント:@h164tan1)
皆さんは、ArduinoのLiquidCrystalライブラリで、カナ表示に対応しているキャラクタLCDモジュールに半角カナの文字列を表示しようとして、文字化けに悩んだ事はありませんか?
例えばリスト1のスケッチをArduino IDE 1.0.6などで実行すると、写真1の様に文字化けします。
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
void setup()
{
lcd.begin(16,2);
lcd.print("Hello コンニチハ");
}
void loop()
{
}
この現象は、Arduinoの内部ではUTF-8という文字コードを使っているのに対し、LCDモジュール内部では8ビットに拡張されたASCIIコード(以後、拡張ASCIIコードと呼ぶ)を使っているからです。
参考:Arduino IDEのスケッチのファイルがUTF-8を使っている事については、英文ですが、arduino.ccのArduino and UTF-8という記事に解説があります。この記事では、シリアルモニタが拡張ASCIIコードを使っている事や、ディスプレイ(液晶などの表示装置)では拡張ASCIIコードを使っている物がある事が解説されています。
8ビットマイコンが全盛の頃、ASCIIコードという文字コードが広く使われていました。ASCIIコードは、コンピュータの扱う文字を、全て7ビットの整数と1対1に対応させるものです。
7ビットの整数というと、128文字しか扱えない(改行などの制御文字を除くともっと少なくなる)のですが、当時の非力なマイコンではこれで十分だったのです。英数記号が扱えれば、プログラムを組むのに困る事はありませんでした。
しかし、8ビットマイコンの性能が上がるにつれ、128種類を超える文字を扱いたいという要求が大きくなりました。また8ビットマイコンが情報を8ビット単位で扱っている事もあり、やがてASCIIコードを8ビットに拡張した文字コードが使われるようになりました。これがこの記事で拡張ASCIIコードと呼ぶものです。
文字コードが8ビットに拡張される事で、扱える文字の種類は128種類増えて256種類になりました。前半の128のコードには、ASCIIコードと同じ文字が割り当てられましたが、後半の128のコード(文字コードの最上位ビットが1になる)には、日本の場合、カナ文字やグラフィック記号が割り当てられました。(7ビットASCII+カナ文字がJIS X 0201として規格化されています。)
時代が進むと、漢字など、さらに多くの文字をコンピュータで扱うようになりました。また、インターネットの普及や社会の国際化などにより、全世界で同じ文字コードを扱う必要が出てきました。そこで生み出されたのがUnicode(ユニコード)です。
Unicodeは、世界中のすべての文字を16ビットの文字コードで表現しようという理念の元に開発が始まりました。(ただし、後に16ビットでは足らなくなって、ビット数が拡張されました)
すべての文字が16ビットの文字コードで表せるのは素晴らしい事ですが、従来のASCIIコードで書かれたドキュメントと互換性がなくなってしまいます。また、プログラムのソースコードなどの様に、ほとんどの文字が英数文字の場合、本来は7ビットで表現できる文字を16ビットも使う事になり、ファイルサイズが大きくなるという問題点も生じます。
そこで、ASCIIコードとUnicodeの両者の互換性を考慮したUTF-8という文字コード(Unicodeの表現方式の一種)ができました。ASCIIコードで表現できる範囲の文字種しか使わない場合は、ASCIIコードを使っても、UTF-8を使っても、全くファイルの内容が同じになるようにしてあります。
UTF-8では、ASCIIコードで表せない文字は、複数のバイトを使って表現します。この様に、複数のバイトを使って表現する文字をマルチバイト文字といいます。UTF-8では、マルチバイト文字を構成するすべてのバイトは、最上位ビットが1になるように工夫されています。
例えば7ビットのASCIIコードに含まれていない半角カナの"ア"は、UTF-8ではEFH、BDH、B1Hの3バイトの表現になります。
一方で、"ア"を拡張ASCIIコードで表すと、B1Hの1バイトになります。
Arduino IDEでは半角カナ文字を直接文字列中に書くと、UTF-8で表現されてしまいますから、拡張ASCIIコードで文字列を出力するには、リスト2の様に拡張ASCIIコードを16進数で書いたスケッチを作る必要があります。\xに続いて2桁の16進数を書くと、C++言語ではその16進数が示す文字コードの文字を表します。(例えば\xbaなら、文字コードがBAHの文字を表す)
リスト2をArduino IDE 1.0.6で実行した場合の画面の写真を写真2に示します。
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
void setup()
{
lcd.begin(16,2);
lcd.print("Hello \xba\xdd\xc6\xc1\xca");
}
void loop()
{
}
リスト2では、半角カナの部分を\xba\xdd\xc6\xc1\xcaと16進数の拡張アスキーコードで打ち込んでいます。この拡張ASCIIコードは、例えばMGLCDライブラリの文字コード表のページで調べられます。
文字コード表を使って"コンニチハ"の各文字の拡張ASCIIコードを調べると、表1の様になる事が分ります。それを使って"コンニチハ"を表記すると、"\xba\xdd\xc6\xc1\xca"となるのです。
文字 | 拡張ASCIIコード |
---|---|
コ | BAH |
ン | DDH |
ニ | C6H |
チ | C1H |
ハ | CAH |
確かに半角カナの拡張ASCIIコードを16進数で打ち込めば、カナは正常に表示されますが、スケッチがすごく読みにくくなります。この問題を解決するために、キャラクタLCDモジュールに文字列を表示する際にUTF-8で表現された半角カナを直接扱えるKanaLiquidCristalライブラリを開発したり、グラフィックLCDモジュール用のMGLCDライブラリにSetCodeMode関数を導入して、UTF-8で表現された半角カナを受け付けるモードを設けたりしました。
Arduino IDE 1.6.6以降で、環境によっては半角カナの文字列中の表現がUTF-8から拡張ASCIIコードに変わる不具合が発生する例が確認されました。
我が家には、32ビットのWindows 10をインストールしたマシンが2台ありますが、Arduino IDE 1.6.6および1.6.9で試したところ、1台ではリスト1のスケッチを実行すると、液晶で文字化けが起こりませんでした(不具合発生)。もう1台では、文字化けしました(不具合発生せず)。
一方で、Arduino IDE 1.0.1、1.0.6、1.6.5-r5、1.7.0、1.7.10では、2台とも文字化けしました(不具合発生せず)。
文字化けが発生しなかったマシン(不具合が発生しがマシン)で調査すると、半角カナが拡張ASCIIコードで表現されているらしい事が分りました。
なお、同一環境で半角カナがUTF-8表現されたり、拡張ASCII表現されたりと、不安定な状況になる例も報告されています。
使用中のArduino IDEで半角カナや漢字をどの様に内部表現しているかは、リスト3のスケッチで調べられます。
void setup()
{
// aは7ビットのASCIIコードで表現できる文字。
// アは8ビットの拡張ASCIIコードで表現できる文字。
// 漢はASCIIコードや拡張ASCIIコードでは表現できない文字。
char str[10]="aア漢";
Serial.begin(9600);
for(int i=0; i<strlen(str); i++) {
Serial.println(byte(str[i]),HEX); // 文字コードを1バイトごとに、16進数で出力
} // for i
} // setup
void loop()
{
} // loop
このスケッチを実行して結果をシリアルモニタで見ると、"aア漢"という文字列の文字コードを16進数で見る事ができます。
不具合が発生するマシンにおいて、リスト3をArduino IDE 1.6.5-r5で実行した結果を図1に、Arduino IDE 1.6.6で実行した結果を図2に示します。
図1と図2の実行結果を解釈するために、'a'、'ア','漢'の3文字を各種の文字コードで表わした表を表2に示します。
文字の種類 | 7ビット ASCII |
8ビット 拡張ASCII |
シフトJIS | UTF-8 |
---|---|---|---|---|
a | 61H | 61H | ― | 61H |
ア | ― | B1H | ― | EFH、BDH、B1H |
漢 | ― | ― | 8AH、BFH | E6H、BCH、A2H |
表2と図1を見比べると、Arduino IDE 1.6.5-r5では、全ての文字がUTF-8で表現されている事が分ります。
表2と図2を見比べると、Arduino IDE 1.6.6では、英数字と半角カナは拡張ASCIIコードで、漢字はシフトJISで表現されている事が分ります。この方式は、Unicodeが普及する以前にMS-DOSやWindowsの古いバージョンなどで使われていた方式です。
シリアルモニタも拡張ASCIIコード+シフトJISで動いていたような気がしたので、もしやと思い、リスト4の様なスケッチを作りました。
void setup() {
Serial.begin(9600);
Serial.println("abcカナ漢字");
}
void loop() {
}
リスト4をArduino IDE 1.6.5-r5で実行した結果を図3に、Arduino IDE 1.6.6で実行した結果を図4に示します。
思った通り、不具合の発生するマシンでArduino IDE 1.6.6を使うと、println関数の引数の文字列に直接半角カナや漢字を書いてもそのまま表示れました。
ただ、何回か試しているうちに、図5の様に文字化けする事がありました。これは、シリアルモニタの不具合の様な気がします。
Arduino IDE 1.6.6以降で、文字コードがUTF-8表現でなくなる不具合が発生すると、それ以前のArduino IDE用に書いたスケッチが正常に動作しなくなる事があります。そこで、文字コードの不具合が発生してもしなくても正常に動作するスケッチの作り方を、(1)KanaLiquidCrystalライブラリを用いてキャラクタLCDを制御する場合、(2)MGLCDライブラリを用いてグラフィックLCDを制御する場合、(3)それ以外の場合に分けて説明します。
lcd.kanaOn();
を実行すれば、それ以降のprint関数やprintln関数では、半角カナ文字がUTF-8で表されているものとして文字列を処理するようになります。
もしkanaOn関数を呼び出さないと、半角カナ文字が拡張ASCIIコードで表されるものとして文字列を処理します。
Arduino IDE 1.6.6が出る前は、文字列中では半角カナは必ずUTF-8で表現されていたので、必ずkanaOn関数を呼び出せば、文字化けのない半角カナの表示ができました。
しかし Arduino IDE 1.6.6以降では、半角カナが拡張ASCIIコードで表現される不具合が発生する可能性がありますので、そのような場合、kanaOn関数を呼び出すと、かえって文字化けを起こします。
文字コードの不具合の発生の有無にかかわらず正常に表示が行われるスケッチを作るには、リスト5をsetup関数の中に記述します。
if(strlen("ア")!=1) lcd.kanaOn();
リスト5では、"ア"という文字列が何バイトかをstrlen関数で調べて、文字コードを検出しています。もし拡張ASCIIなら1バイト、もしUTF-8なら3バイトになります。
strlen関数が1以外の数を返すとUTF-8表現であると判断してkanaOn関数を呼び出し、以後のprint関数や、println関数の呼び出しではカナがUTF-8表現されているものとして扱うように設定します。
その結果、リスト5をスケッチのsetup関数に入れておくと、それ以後、半角カナが拡張ASCII表現されていようと、UTF-8表現されていようと、文字化けせずに表示できる様になります。
文字コードの不具合の有無にかかわらずにキャラクタLCDに"コンニチハ"を表示できるスケッチの例を、リスト6に示します。
#include <LiquidCrystal.h>
#include <KanaLiquidCrystal.h>
KanaLiquidCrystal lcd(12,11,5,4,3,2); // ピン番号は、配線に応じて変更する必要あり
void setup() {
lcd.begin(16,2); // 引数は、キャラクタLCDの桁数と行数に応じて変更する必要あり
if(strlen("ア")!=1) lcd.kanaOn(); // 文字コードの不具合の発生の有無にかかわらず、以降の文字列の半角カナを正常に表示
lcd.print("コンニチハ");
}
void loop() {
}
MGLCDライブラリの場合、デフォルトでは半角カナは拡張ASCIIコードで表現されているものとして動作しますが、
MGLCD.SetCodeMode(MGLCD_CODE_UTF8);
を実行する事で、それ以降、半角カナ文字がUTF-8で表現されているものとして動作する様になります。
MGLCDライブラリのprint関数やprintln関数で、どのバージョンのArduino IDEを使う場合でも、半角カナ文字を正しく扱えるようにするには、リスト7をsetup関数に入れておきます。
if(strlen("ア")!=1) MGLCD.SetCodeMode(MGLCD_CODE_UTF8);
文字コードの不具合の有無にかかわらずにキャラクタ液晶に"コンニチハ"を表示できるスケッチの例を、リスト8に示します。
#include <MGLCD.h>
MGLCD_AQM1248A_SoftwareSPI MGLCD(MGLCD_SpiPin4(13,11,10,9)); // AQM1248Aを使う場合。ピン番号は、配線に応じて変更する必要あり
void setup() {
MGLCD.Reset();
if(strlen("ア")!=1) MGLCD.SetCodeMode(MGLCD_CODE_UTF8); // 文字コードの不具合の発生の有無にかかわらず、以降の文字列の半角カナを正常に表示
MGLCD.print("コンニチハ");
}
void loop() {
}
KanaLiquidCrystalライブラリやMGLCDライブラリを使わずにLCDに半角カナを表示する場合や、シリアルモニタに半角カナを表示する場合などは、リスト9に示すConvStr関数を使います。
注1:ConvStr関数はArduino IDE 1.0.X、1.6.X、および1.7.Xのいずれでも動作しますが、1.0.4以前の古いバージョンでは、Stringクラスのc_strメソッド(メンバ関数)が使えないために、コンパイル時にエラーになります。Arduino IDE 1.0.5以降でお使いください。
注2:ConvStr関数は、商用利用、非商用利用に関わらず、自由に利用していただけます。ただしConvStr関数の利用により何らかの問題が発生しても、当方では一切責任を取りません。自己責任での利用をお願いします。
注3:ConvStr関数を利用したスケッチ等の二次配布は自由にしてくださって結構ですが、ソースリストの形で配布される場合は、できる限りこのサイトのURLを含めたコメントも消さずに配布してください。これはライセンス事項ではありませんが、作者からの強いお願いです。
注4:ConvStr関数は、現状では半角カナのみに対応しており、漢字には対応していません。要望が多ければ、漢字のシフトJISへの変換に対応したバージョンの開発も検討します。
// ConvStr関数
// String型の引数の文字列に含まれる半角カナを拡張ASCII表現に変換する関数
// この関数は、しなぷすのハード製作記(https://synapse.kyoto/)でダウンロードできる
String ConvStr(String str)
{
struct LocalFunc{ // for defining local function
static uint8_t CodeUTF8(uint8_t ch)
{
static uint8_t OneNum=0; // Number of successive 1s at MSBs first byte (Number of remaining bytes)
static uint16_t Utf16; // UTF-16 code for multi byte character
static boolean InUtf16Area; // Flag that shows character can be expressed as UTF-16 code
if(OneNum==0) { // First byte
uint8_t c;
// Get OneNum
c=ch;
while(c&0x80) {
c<<=1;
OneNum++;
} // while
if(OneNum==1 || OneNum>6) { // First byte is in undefined area
OneNum=0;
return ch;
} else if(OneNum==0) { // 1-byte character
return ch;
} else { // Multi byte character
InUtf16Area=true;
Utf16=ch&((1<<(7-OneNum--))-1); // Get first byte
} // if
} else { // not first byte
if((ch&0xc0)!=0x80) { // First byte appears illegally
OneNum=0;
return ch;
} // if
if(Utf16&0xfc00) InUtf16Area=false;
Utf16=(Utf16<<6)+(ch&0x3f);
if(--OneNum==0) { // Last byte
return (InUtf16Area && Utf16>=0xff61 && Utf16<=0xff9f) ? Utf16-(0xff61-0xa1) // kana
: ' '; // other character
} // if
} // if
return 0;
}; // CodeUTF8
}; // LocalFunc
const char charA[]="ア";
if(*charA=='\xb1') return str;
String result="";
for(int i=0; i<str.length(); i++) {
uint8_t b=LocalFunc::CodeUTF8((uint8_t)str.c_str()[i]);
if(b) {
result+=(char)b;
} // if
} // for i
return result;
} // ConvStr
ConvStr関数はString型の文字列引数を1つ取り、渡された文字列の中の半角カナを拡張ASCII表現に変換します。Arduino IDEが半角カナをUTF-8表現にしているのか、拡張ASCII表現にしているのかを、ConvStr関数自身が自動判定します。(Arduino IDEが半角カナを元々拡張ASCII表現にしている場合は、ConvStr関数は、引数をそのまま返します)
例えばシリアルモニタに半角カナを表示する場合、
Serial.print("コンニチハ");
を
Serial.print(ConvStr("コンニチハ"));
と書き換え、一度ConvStr関数を通して文字コードの変換を行えば、文字コードの不具合の有無にかかわらず、"コンニチハ"を文字化けせずに表示できます。もちろんArduino IDE 1.6.6以降で使う可能性がない場合も、ConvStr関数を使うと、半角カナをUTF-8表現から拡張ASCII表現に変換できます。
ConvStr("Hello コンニチハ")
と、英数字と半角カナが混在した文字列を変換する場合は、半角カナの部分のみが変換の対象になります。