キャラクター液晶をつけて遊ぶその2


さて、ただ液晶をつけても面白くないので、新幹線についているようなスクロールメッセージ表示器を作ってみることにした。
ArduinoのLiquidCrystalライブラリーにはscrollDisplayLeff()、scrollDisplayRight()というメソッドがある。
これは液晶ドライバ(HD44780)の内部の表示バッファーと、実際の表示位置をずらすものだ。
もう少し調べてみると、どうもこの液晶ドライバには40x2文字分の表示バッファーがあるようだ。
このCL1602の場合には(0,0)から(15,0)が一行目の表示エリア、(1,0)から(1,15)までが二行目の表示エリアになっていて、後の部分は見えなくなっているだけのようだ。
試しに上記のscrollDisplayLeff()を使ってみると1列ずれて、(16,0)の表示バッファーの文字が(15,0)の位置に表示された。


この機能を使ってスクロール表示をする場合には、Arduinoの中に液晶ドライバの内部バッファーを利用したキュー(リングバッファー)作りこめばよい。
まんどくさい。
しかも新しい文字を入力するすべも、Arduino単体ではない。
面白くない。
なので、ホストPCとシリアル通信させて、ホストから送信するデータ自身を変えて表示をスクロールさせることにした。

まずホスト側はこんなだ。


namespace ArduinoCLCDTest
{
public partial class Form1 : Form
{
static int NUM_CLCD_WIDTH = 16; // CLCD QC1602 width
static int QUEUE_INPUT_MAX = 64 + 1; // 64 characters
static int QUEUE_OUTPUT_MAX = NUM_CLCD_WIDTH + 1;
static int QUEUE_EMPTY = -1;
static int QUEUE_FULL = -1;
static int OK_ENQUEUE = 0;
static byte ASCII_SPC_CODE = (byte)32;
static byte LINE_SYNC_SYMBOL = 0x0d;
//
byte[] queueInput;
int queueInputFirst;
int queueInputLast;
//
byte[] queueOutput;
int queueOutputFirst;
int queueOutputLast;
//
byte[] bufferOutput;
//
public Form1()
{
InitializeComponent();
//
textBoxInput.Text = string.Empty;
textBoxInputQueue.Text = string.Empty;
textBoxOutputQueue.Text = string.Empty;
textBoxOutputBuffer.Text = string.Empty;
//
queueInput = new byte[QUEUE_INPUT_MAX];
queueOutput = new byte[QUEUE_OUTPUT_MAX];
queueInputFirst = 0;
queueInputLast = 0;
queueOutputFirst = 0;
queueOutputLast = 0;
bufferOutput = null;
//
openSerialport();
}

private int enqueuQueueInput(byte bufferInput)
{
if ((queueInputLast + 1) % QUEUE_INPUT_MAX == queueInputFirst)
{
return QUEUE_FULL;
}
else
{
queueInput[queueInputLast] = bufferInput;
queueInputLast = (queueInputLast + 1) % QUEUE_INPUT_MAX;
return OK_ENQUEUE;
}
}

private int dequeueQueueInput()
{
if (queueInputFirst == queueInputLast) return QUEUE_EMPTY;
else
{
int returnValue = queueInput[queueInputFirst];
queueInputFirst = (queueInputFirst + 1) % QUEUE_INPUT_MAX;
return returnValue;
}
}

private int enqueuQueueOutput(byte bufferInput)
{
if ((queueOutputLast + 1) % QUEUE_OUTPUT_MAX == queueOutputFirst)
{
return QUEUE_FULL;
}
else
{
queueOutput[queueOutputLast] = bufferInput;
queueOutputLast = (queueOutputLast + 1) % QUEUE_OUTPUT_MAX;
return OK_ENQUEUE;
}
}

private int dequeueQueueOutput()
{
if (queueOutputFirst == queueOutputLast) return QUEUE_EMPTY;
else
{
int returnValue = queueOutput[queueOutputFirst];
queueOutputFirst = (queueOutputFirst + 1) % QUEUE_OUTPUT_MAX;
return returnValue;
}
}

private void onButtonSendClicked(object sender, EventArgs e)
{
byte[] bufferInput = System.Text.Encoding.ASCII.GetBytes(textBoxInput.Text + " "); // add 1 white space
int lengthInput = textBoxInput.Text.Length + 1; // add 1 for the white space
for (int count = 0; count < lengthInput; count++)
{
if (enqueuQueueInput(bufferInput[count]) == QUEUE_FULL)
{
MessageBox.Show("Queue is full");
break;
}
}
}

private void onTimer1Tick(object sender, EventArgs e)
{
int count;
if (bufferOutput == null)
{
bufferOutput = new byte[sizeof(byte) + NUM_CLCD_WIDTH]; // the first character is a line sync symbol code
bufferOutput[0] = LINE_SYNC_SYMBOL; // the first character is a line sync symbol (CR)
for(count = 0; count < NUM_CLCD_WIDTH; count++)
{
enqueuQueueOutput(ASCII_SPC_CODE);
}
}
else
{
dequeueQueueOutput();
int bufferInput2Output = dequeueQueueInput();
if ( bufferInput2Output == QUEUE_EMPTY)
{
enqueuQueueOutput(ASCII_SPC_CODE);
}
else
{
enqueuQueueOutput((byte) bufferInput2Output);
}
}
for(count = 0; count < NUM_CLCD_WIDTH; count++)
{
// the first character is a line sync symbol (CR)
bufferOutput[sizeof(byte) + count] = queueOutput[(queueOutputFirst + count) % QUEUE_OUTPUT_MAX];
}
// for monitor
textBoxInputQueue.Text = System.Text.Encoding.ASCII.GetString(queueInput);
textBoxOutputQueue.Text = System.Text.Encoding.ASCII.GetString(queueOutput);
textBoxOutputBuffer.Text = System.Text.Encoding.ASCII.GetString(bufferOutput);
//
serialPortArduino.Write(bufferOutput, 0, sizeof(byte) + NUM_CLCD_WIDTH);
}

void openSerialport()
{
serialPortArduino.PortName = "COM3";
serialPortArduino.BaudRate = 9600;
serialPortArduino.DataBits = 8;
serialPortArduino.Parity = System.IO.Ports.Parity.None;
serialPortArduino.StopBits = System.IO.Ports.StopBits.One;
serialPortArduino.Handshake = System.IO.Ports.Handshake.None;
serialPortArduino.Open();
}

private void onForm1FormClosing(object sender, FormClosingEventArgs e)
{
serialPortArduino.Close();
}
}
}

アルゴリズムはこんなだ。

  • 2つのキュー(リングバッファー)を持たせ
  • 入力があれば、1つ目のキューに詰める(エンキューする)
  • タイマーで1つ目のキューから取り出(デキュー)した文字を、2つ目のキューに詰める(エンキューする)
    • 2つ目のキューは、1つ目のキューが空の時にはスペースを詰めるので、いつも満杯だ
  • タイマーの最後で2つ目のキューの最初から最後までを読みだして、表示バッファーにして、シリアルで転送する

若干冗長だが、PC側はリソースが余りまくっているので、デバッグしやすい構成にした。


次にArduino側だ。


#include
#define LINE_SYNC_SYMBOL 0x0d
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
int countBufferPosition;
char buffer;

void setup()
{
Serial.begin(9600);
lcd.begin(16, 2);
lcd.home();
//
countBufferPosition = 0;
}

void loop()
{
if(Serial.available())
{
buffer = Serial.read();
if ( buffer == LINE_SYNC_SYMBOL )
{
countBufferPosition = 0;
}
else
{
lcd.setCursor(countBufferPosition, 0);
lcd.print( buffer );
countBufferPosition++;
}
}
}

最初は実はloopがこんなだった。

int CLCD_WIDTH = 16;
byte bufferCLCD[CLCD_WIDTH];
//
void loop()
{
int count = 0;
lcd.setCursor(0, 0);
if(Serial.available())
{
for( count = 0; count < CLCD_WIDTH )
{
bufferCLCD[ count ] = Serial.read();
}
lcd.print( bufferCLCD );
}
}
シリアルから来た16文字をbufferCLCDに取り込んで、lcdにprintする。
どこが悪い?
悪くないじゃん。
・・・
いやね、シリアルは一気に16文字送ってくるとは限らないですぜ、旦那。
なので、その回のシリアルのセッションで受信された文字だけを液晶に表示する。
なのでいつまでたっても1行16文字すべては埋まらない。
しかも一回シリアル通信が始まると、16文字送信されていようがなかろうが、Serial.read()を16回叩く。
へたすりゃ、デッドロックです。
駄目です。


このためシリアル通信の中身にラインシンクシンボル(LINE_SYNC_SYMBOL 0x0d)によるハンドシェークを導入した。
ホストPC側は送信するデータの頭にラインシンクシンボルをつけて、Arduinoはこれを受信したら液晶の表示位置を行の頭に戻すようにした。
これで動作するようになった。

Arduino Uno Rev3

Arduino Uno Rev3