概要
近年のマイコン用ディスプレイとしては液晶やOLEDが一般的ですが、昔ながらの蛍光表示管(VFD, Vacuum Fluorescent Display)も味があって良いものです。液晶やOLEDに比べて物理的な質感があり、ドットの1粒1粒が輝いているのがとても味わい深いです。
今回は、Noritake Itorn製のドットマトリクスVFDモジュール(GU-3000シリーズ)をobnizに接続するためのパーツライブラリとデモ用のブラウザアプリを作成しました。(最後の言い訳のところにも書いていますが、UARTのシリアル接続なのでちょっと遅いです。)
デモ動画
デモでは下記を実行しています。
- テキストの描画(obnizからVFDモジュールに文字列を送信して、VFDモジュールが文字を描画)
- Canvas上に描画したテキストの表示(Canvas上のビットマップイメージをobnizからVFDモジュールに送信して表示)
- ランダムな点、線、四角形、塗りつぶし四角形の描画(obnizが座標を送信してVFDモジュールが描画)
- 画像の描画(URLで指定した画像をCanvas上に描画し、obnizがCanvas上のビットマップイメージをVFDモジュールに送信して表示)
- 輝度の変更(obnizからVFDモジュールにコマンドを送信)
部品
項番 | 品名 | 型番 | 数量 |
---|---|---|---|
1 | obniz | obniz Board 1Y | 1 |
2 | VFDモジュール | Noritake Itron GU256x128C-3100 | 1 |
3 | RS232Cインターフェース | Texas Instruments MAX202 | 1 |
4 | 配線材料 | 適宜 |
高画素数のVFDモジュールは一般的には高価で入手困難なのですが、デッドストック品をいくつか入手することができたので、ラズパイ用インターフェースとサンプルソフト付きで1万円前後で販売させていただいております。(ヤフオクで「noritake vfd」で検索すると出てきます。数に限りがありますのでお早めにどうぞ。)
接続図
VFDモジュールのシリアルポートのインターフェースがRS-232C仕様なので、obnizのI/Oとの間にインターフェース用のICを入れました。
ソースコード
パーツライブラリ(VFD_GU3000_Serial.js)
"use strict";
class VFD_GU3000_Serial {
constructor() {
this.width = 0;
this.height = 0;
this.defaultBrightness = 4; // 0 to 8
this.FB = [];
this.keys = ["vcc", "gnd", "tx", "rx", "cts", "rts", "baud"];
this.requiredKeys = ["tx", "rx", "cts", "rts"];
}
static info() {
return {
name: "VFD_GU3000_Serial",
};
}
wired(obniz) {
if (obniz.isValidIO(this.params.vcc)) {
obniz.getIO(this.params.vcc).output(true);
}
if (obniz.isValidIO(this.params.gnd)) {
obniz.getIO(this.params.gnd).output(false);
}
this.params.flowcontrol = "rts-cts";
if(this.params.baud == null){
this.params.baud = 38400;
}
this.uart = obniz.getFreeUart();
this.uart.start(this.params)
}
init(width, height) {
this.width = width;
this.height = height;
this.heightByte = height / 8;
this.allocFB(width, height);
this.initModule();
}
initModule() {
this.write([0x1b, 0x40]); // Initialize VFD
this.setCursor(0, 0);
this.setBrightness(this.defaultBrightness);
this.write("Module initialized"); // for test
}
setBrightness(val){ // 0 to 8
if(val < 0){
val = 0;
} else if(val > 8){
val = 8;
}
this.write([0x1f, 0x58, 0x10+val]); // set brightness
this.brightness = val;
}
increaseBrightness(){
this.setBrightness(this.brightness + 1);
}
decreaseBrightness(){
this.setBrightness(this.brightness - 1);
}
clear(){
this.write(0x0c);
}
write(message){
this.uart.send(message);
}
writeWord(w){
this.uart.send([w & 0xff, (w>>8) & 0xff]);
}
allocFB(width, height) {
this.FB = new Array(width * height / 8);
}
clearFB(){
for(let i = 0; i < this.FB.length; i++){
this.FB[i] = 0;
}
}
test(){
}
setCursor(x, y){
this.write([0x1f, 0x24]);
this.writeWord(x);
this.writeWord(y);
}
drawDot(x, y, pen){
this.write([0x1f, 0x28, 0x64, 0x10, pen]);
this.writeWord(x);
this.writeWord(y);
}
drawLine(x1, y1, x2, y2, pen){
this.write([0x1f, 0x28, 0x64, 0x11, 0x00, pen]);
this.writeWord(x1);
this.writeWord(y1);
this.writeWord(x2);
this.writeWord(y2);
}
drawBox(x1, y1, x2, y2, pen){
this.write([0x1f, 0x28, 0x64, 0x11, 0x01, pen]);
this.writeWord(x1);
this.writeWord(y1);
this.writeWord(x2);
this.writeWord(y2);
}
drawFillBox(x1, y1, x2, y2, pen){
this.write([0x1f, 0x28, 0x64, 0x11, 0x02, pen]);
this.writeWord(x1);
this.writeWord(y1);
this.writeWord(x2);
this.writeWord(y2);
}
showFB(){
this.setCursor(0, 0);
this.write([0x1f, 0x28, 0x66, 0x11]);
this.writeWord(this.width);
this.writeWord(this.heightByte);
this.write(0x01);
this.write(this.FB);
}
drawCanvas(ctx) {
const imageData = ctx.getImageData(0, 0, this.width, this.height);
const data = imageData.data;
this.clearFB();
for(let x = 0; x < this.width; x++){
for(let y = 0; y < this.height; y++){
let i = y * this.width + x;
let j = x * this.height + y;
let threshold = 0.0;
const brightness = 0.299 * data[i*4] + 0.587 * data[i*4 + 1] + 0.114 * data[i*4 + 2];
const bits = j & 0x7;
// 2x2 dithering for color image
if(i % 2 == 0){
if(j % 2){
threshold = 0.671 * 0xff;
} else {
threshold = 0.2025 * 0xff;
}
} else {
if(j % 2){
threshold = 0.0772 * 0xff;
} else {
threshold = 0.3913 * 0xff;
}
}
if (brightness > threshold) {
this.FB[j>>3] |= 0x80 >> bits;
}
}
}
this.showFB();
}
}
if (typeof module === 'object') {
module.exports = VFD_GU3000_Serial;
}
ブラウザアプリ(VFDdemo.html)
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" />
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/obniz@3.x/obniz.js" crossorigin="anonymous" ></script>
<script src="https://obniz.com/users/4792/repo/VFD_GU3000_Serial.js"></script>
</head>
<body>
<div id="obniz-debug"></div>
<div class="container">
<div class="text-center">
<h3>VFD Module Parts Library Demo on obniz</h3>
<input type="text" id="text" value="Hello World!" />
<button class="btn btn-primary" id="showtime">Print text</button>
<button class="btn btn-primary" id="test_canvas">Print via canvas</button>
<br><br>
<button class="btn btn-primary" id="test_brightness1">+brightness</button>
<button class="btn btn-primary" id="test_brightness2">-brightness</button>
<button class="btn btn-primary" id="test_init">init</button>
<button class="btn btn-primary" id="test_clear">clear</button>
<br><br>
<button class="btn btn-primary" id="test_dot">random dot</button>
<button class="btn btn-primary" id="test_line">random line</button>
<button class="btn btn-primary" id="test_box">random box</button>
<button class="btn btn-primary" id="test_fillbox">random fillbox</button>
<br><br>
<input type="text" id="imagefile" value="https://raw.githubusercontent.com/ryomuk/gu3000/main/src/examples/showbmp/256128sample.bmp" />
<button class="btn btn-primary" id="test_showimage">Show imagefile</button>
</div>
</div>
<script>
//put your obniz ID
var obniz = new Obniz("OBNIZ_ID_HERE");
//during obniz connection
obniz.onconnect = async function() {
Obniz.PartsRegistrate(VFD_GU3000_Serial);
const vfd = obniz.wired("VFD_GU3000_Serial", {tx: 0, rx: 1, cts: 2, rts: 3, baud: 38400});
vfd.init(256, 128);
//init switch state on obniz
$("#print").text(await obniz.switch.getWait());
//if the "Print on obniz" clicked
$("#showtime").on("click", function() {
const message = $("#text").val();
//show input text on obniz display
obniz.display.clear();
obniz.display.print(message);
vfd.write(message);
});
$("#test_brightness1").on("click", function() {
vfd.increaseBrightness();
vfd.setCursor(0, 0);
vfd.write("Brightness="+vfd.brightness);
});
$("#test_brightness2").on("click", function() {
vfd.decreaseBrightness();
vfd.setCursor(0, 0);
vfd.write("Brightness="+vfd.brightness);
});
$("#test_init").on("click", function() {
vfd.initModule();
});
$("#test_clear").on("click", function() {
vfd.clear();
});
$("#test_dot").on("click", function() {
for(let i = 0; i < 100; i++){
const x = Math.floor(Math.random()*vfd.width);
const y = Math.floor(Math.random()*vfd.height);
vfd.drawDot(x, y, 1);
}
});
$("#test_line").on("click", function() {
for(let i = 0; i < 10; i++){
const x1 = Math.floor(Math.random()*vfd.width);
const y1 = Math.floor(Math.random()*vfd.height);
const x2 = Math.floor(Math.random()*vfd.width);
const y2 = Math.floor(Math.random()*vfd.height);
vfd.drawLine(x1, y1, x2, y2, 1);
}
});
$("#test_box").on("click", function() {
for(let i = 0; i < 10; i++){
const x1 = Math.floor(Math.random()*vfd.width);
const y1 = Math.floor(Math.random()*vfd.height);
const x2 = Math.floor(Math.random()*vfd.width);
const y2 = Math.floor(Math.random()*vfd.height);
vfd.drawBox(x1, y1, x2, y2, 1);
}
});
$("#test_fillbox").on("click", function() {
for(let i = 0; i < 10; i++){
const x1 = Math.floor(Math.random()*vfd.width);
const y1 = Math.floor(Math.random()*vfd.height);
const x2 = Math.floor(Math.random()*vfd.width);
const y2 = Math.floor(Math.random()*vfd.height);
const pen = Math.floor(Math.random()*2);
vfd.drawFillBox(x1, y1, x2, y2, pen);
}
});
$("#test_canvas").on("click", function() {
const ctx = obniz.util.createCanvasContext(vfd.width, vfd.height);
const message = $("#text").val();
ctx.fillStyle = "black";
ctx.fillRect(0, 0, vfd.width, vfd.height);
ctx.fillStyle = "white";
ctx.font = "32px sans-serif";
ctx.fillText(message, 0, 40);
vfd.drawCanvas(ctx);
});
$("#test_showimage").on("click", function() {
const ctx = obniz.util.createCanvasContext(vfd.width, vfd.height);
let imagefile = $("#imagefile").val();
//imagefile = "https://raw.githubusercontent.com/ryomuk/gu3000/main/images/colorbar.png";
vfd.write(imagefile);
const image = new Image();
image.src= imagefile;
image.crossOrigin = "Anonymous";
image.onload = function() {ctx.drawImage(image, 0, 0); vfd.drawCanvas(ctx);};
});
};
</script>
</body>
</html>
まとめ(言い訳など)
当初のもくろみとしては、以前にRaspberry Pi用にC++で作成したもの(リンクはこちら)を移植しようとしたのですが、簡単にはいかなかったので仕様を変更して実装しました。
ラズパイ用のライブラリはパラレル接続のグラフィックDMAモードを使用した高速なものだったのですが、そのままJavaScriptに置き換えただけでは速度が1/100ぐらいで動作も不安定でブラウザもフリーズしてしまい、お手上げな状況になってしまいました。というわけで、今回はUARTでシリアル接続して制御する低速なものになっています。なにぶんJavaScriptは初めてなので、obnizの性能を十分に発揮させることができていないと思われます。
obniz上で動作するプログラムをC++で簡単に開発できるような環境とガイドがあると嬉しいと思いました。(もしかしたら既にあるのかもしれませんが見つけられませんでした。)
-
MukaiLab
さんが
2021/05/10
に
編集
をしました。
(メッセージ: 初版)
-
MukaiLab
さんが
2021/05/11
に
編集
をしました。
Opening
je8vgn
2022/12/30
ログインしてコメントを投稿するVFDの光って良いですよね。私もVFDの光に魅せられています!