デジット液晶で遊ぶ

## はじめに

こちらは鈴鹿高専 Advent Calendar 2021の1日目の記事です．

https://qiita.com/advent-calendar/2021/snct



注意事項としては，ここで記載されているコードを他のプロダクトでそのまま使うのは**禁止**とします．

何かバグがあっても責任を取れないためです．

また，実際に何かを制作する際には必ず一次情報(データシートなど)を参考にしてください．



## 作ったもの

![IMG_8664_R.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/d9ed587b-2ec9-aad1-a9dc-04c0223cc0a7.jpeg)

動画をご覧ください．

https://www.nicovideo.jp/watch/sm39313027



# 使用部品

## 液晶

今回使用した液晶はこちら．

![IMG_8639_R.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/425dc910-b7fe-3194-6a9c-57239210529d.jpeg)


240x64ドットのグラフィック液晶です．

ドライバICとしてMSM5260が載っていますが，コントローラICは**非搭載**のため毎フレームごとに自分で書き換え処理を行う必要があります．

ロジック系は5Vで，他に液晶駆動用の-12Vを必要とします．

ピン配置を以下に示します．

| ピン番号 |  ピン名  |                  説明                   |
| :------: | :------: | :-------------------------------------: |
|    1     |    DI    |             表示データ入力              |
|    2     |   FRM    |            0ライン目のみHIGH            |
|    3     |   N.C.   |                 無接続                  |
|    4     |   LOAD   |        立ち下がりエッジでラッチ         |
|    5     |   CLK    |    立ち下がりエッジでデータ取り込み     |
|    6     |   N.C.   |                 無接続                  |
|    7     |   VDD    |                   +5V                   |
|    8     |   VSS    |                   GND                   |
|    9     |   VEE    |                  -12V                   |
|    10    | CONTRAST | コントラスト(VSS-VEE間に10kΩボリューム) |

通信に使用するピンは４ピンです．

## マイコン

今回使用したマイコンは**STM32F303K8T6**です．

基本的なスペックを以下に示します．

|    項目    |         説明         |
| :--------: | :------------------: |
|  電源電圧  |      2.0~3.6(V)      |
|    コア    |    ARM Cortex-M4     |
| コアサイズ |       32(bit)        |
|  クロック  |       72(MHz)        |
|    FPU     |         あり         |
|   FLASH    |        64(KB)        |
|    RAM     | 12(KB)+4(KB)(CCMRAM) |
|    GPIO    |       25(pin)        |
|    ADC     |        9(ch)         |
|    DAC     |        3(ch)         |
| UART/USART |        2(ch)         |
|    I2C     |        1(ch)         |
|    SPI     |        1(ch)         |
|    CAN     |        1(ch)         |
|   タイマ   |        8(ch)         |
| パッケージ |        QFP32         |

小ピンの割に性能が高く，また秋月で入手可能なのが決め手です．

今回のような用途ではピン数は必要としない代わりに性能が要求されるためピッタリです．

また，STM32に共通する利点としてペリフェラルが豊富で簡単に扱えるのもグッドです．

~~前にお高い公式デバッガを買ってしまったからというのも大きい~~

電源は3.3Vで使用しました．



## 周辺回路

周辺回路としては以下のようなものがあります

- 電源回路
- レベル変換回路
- USB-UART変換回路
- 負電圧生成回路

それぞれについて軽く述べていきます．

### 電源回路

入力された12Vを5V，3.3Vに降圧します．

ヒートシンクのスペースが無かったため発熱が少ないROHMのDCコンバータを使用しました．

その辺に転がっていたものを再使用したのでコストも抑えられました．



### レベル変換回路

BSS138を使用した3.3V-->5.0V変換回路を組み込んでいます．

これはドライバICのMSM5260のVIHが0.7VDD(=3.5V)と，3.3Vでは足りないためです．

部品点数も抑えられてコスト的にも非常に良いので割とよく使う回路です．



### USB-UART変換回路

今回は安くて周辺回路が少なくて済むCH340Nを採用しました．

8ピン-SOPパッケージにも関わらず，周辺回路がほとんど不要で2Mbpsまでの通信に対応してるためこれ以外使う気になりませんでした．

価格も衝撃的でした，これからずっと使い続けることになりそう...



### 負電圧生成回路

MicrochipのTC7662Bを使用しかなりシンプルにまとめています．

電圧を反転しているため，他の液晶の制御時には大元の電源電圧を変えるだけで対応が可能という副次的なメリットもあります．



# 制御

## 液晶

液晶に入力するデータの形式を以下に示します．

まず水平方向１ライン分です．

![timing-chart (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/b70cb3e6-cd80-ba36-9138-c45ce96d041d.png)


次に1フレーム分

![timing-chart (1) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/b030a975-4d94-29aa-5dce-590c5de74210.png)


全ての信号は立ち下がり時に確定されます．

これを見て勘の鋭い方はお分かりでしょうが，CLKとDIはどう見ても**SPIのモード1**です．

これならGPIOでパタパタせずともペリフェラルを使ってハードウェア的に処理ができます．

## マイコン

マイコンの制御プログラムはC言語とHALを用いて制作しました．

プログラムは主に以下の５つの要素から成り立っています．

- SapphireLCD
  - もっとも低レベルなLCD制御
- SapphireCommunication
  - ホストとの通信
- SapphireGraphics
  - グラフィックライブラリ
- SapphireMIDIParser
  - MIDIパーサー
- SapphireMIDIDisplay
  - MIDIディスプレイ

それぞれについて述べていきます．



#### コア部分

メイン関数内では各種ペリフェラルの初期化やモードに応じた処理の振り分けなどを行なっています．

また自作printfモジュールを組み込んでいます．



#### SapphireLCD

タイマー割り込みでDMA転送命令を出します．

割り込み毎に表示用バッファの内容を転送しています．

各種ピンの制御も同時に行います．

実際にロジックアナライザで観測した波形は以下の通りです．

![E92gcLnVgAom4FW.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/9f571384-f93b-39b1-d385-cc6ef8d7c796.png)

コードは以下が全てでかなりシンプルになっています．

```c
/*
 * SapphireLCD.c
 *
 *  Created on: 2021/09/03
 *      Author: Owner
 */

#include "SapphireLCD.h"

SPI_HandleTypeDef *spi_lcd;
GPIO_TypeDef *load_port_lcd;
uint16_t load_pin_lcd;
GPIO_TypeDef *frm_port_lcd;
uint16_t frm_pin_lcd;

void Initialise_LCD(SPI_HandleTypeDef *hspi, GPIO_TypeDef *load_port,
		uint16_t load_pin, GPIO_TypeDef *frm_port, uint16_t frm_pin) {
	spi_lcd = hspi;
	load_port_lcd = load_port;
	load_pin_lcd = load_pin;

	frm_port_lcd = frm_port;
	frm_pin_lcd = frm_pin;
}

void Transfer_Line(uint8_t *buffer, uint8_t isFirstLine) {
	HAL_GPIO_WritePin(frm_port_lcd, frm_pin_lcd, isFirstLine);
	HAL_SPI_Transmit_DMA(spi_lcd, &buffer[0], 30);
}

void Set_Load(void) {
	HAL_GPIO_WritePin(load_port_lcd, load_pin_lcd, 1);
	HAL_GPIO_WritePin(load_port_lcd, load_pin_lcd, 0);
}
```



#### SapphireCommunication

シリアル通信を担当します．

ホストとの通信は独自規格の4bit志向のプロトコルのため，受信したデータをここで連結し上位のレイヤーに受け渡します．

DMAを用いて受信したデータは自動的に循環バッファに格納されます．

ボーレートを自動的に検出して補正する機能もついています．

受信部分のコードを以下に示します．

```c
UART_HandleTypeDef *serial_com;
void Initialise_Serial(UART_HandleTypeDef *serial) {
	serial_com = serial;
	memset(RxBuff, 0x00, USART_RX_BUFFSIZE);
	rd_ptr = 0;

	__HAL_UART_DISABLE_IT(serial_com, UART_IT_PE);
	__HAL_UART_DISABLE_IT(serial_com, UART_IT_ERR);
	HAL_UART_Receive_DMA(serial_com, RxBuff, USART_RX_BUFFSIZE);
}

int Is_Empty_Serial(void) {
	return (rd_ptr == DMA_WRITE_PTR);
}

int Read_Serial(uint8_t *data) {
	if (rd_ptr != DMA_WRITE_PTR) {
		(*data) = RxBuff[rd_ptr++];
		rd_ptr &= 0xFF;
	} else {
		return -1;
	}
	return 0;
}

void Write_Serial(uint8_t chr) {
	HAL_UART_Transmit(serial_com, &chr, 1, 20);
}
```



#### SapphireGraphics

一番実装に手間取ったグラフィック関連の処理はここで行います．

このモジュールで実装した機能は以下の通りです．

- 埋め込んだFONTXデータからのビットマップデータ抽出
- 縦・横・４倍角文字の生成
- 文字バッファからの表示データ作成
- 表示内容の切り替え(文字面・グラフィック面)
- オーバーレイ機能(OR/XOR)

文字列を簡単に出すことを主な目的としています．

各々の機能について述べていきます．



##### 埋め込んだFONTXデータからのビットマップデータ抽出

 今回の用途ではROMが比較的余っていたため，PC-98実機から抽出したフォントをFONTX形式に変換に直接埋め込んでいます．

これにはアセンブラの命令を使用しています．

8x8のフォントで大小英文字，記号，数字，半角カタカナなどに対応しています．

```c
void Get_Font_Data(uint8_t *dest, uint8_t chr) {
	memcpy(&dest[0], &FONT[17 + chr * 8], 8);
}
```

FONTXの構造が比較的単純なためこれだけの記述で済みます．



##### 各種倍角文字の生成

ワープロなどでよく見られた倍角文字の生成も行います．

縦倍角は非常に単純です，同じバイトデータを繰り返し書くだけで済むからです．

横倍角が厄介でした．同じビットを2回繰り返す処理がなかなか出てこなくて手間取りましたが最終的には以下のようなコードに落ち着きました．

```c
void Interleave(uint8_t *dest, uint16_t src) {
	src = (src | (src << 4)) & 0x0f0f;
	src = (src | (src << 2)) & 0x3333;
	src = (src | (src << 1)) & 0x5555;
	src = (src | (src << 1));
	dest[0] = (src & 0xFF00) >> 8;
	dest[1] = (src & 0x00FF);
	return;
}
```

この処理が一番高速でした．

４倍角は横倍したデータを繰り返し書くだけです．



##### 文字バッファからの表示データ作成

240x64という解像度では30x8のキャラクタ液晶と同等の表示力を持ちます．

自作機器には最適な高密度の情報表示ができるため，容易に文字を表示できるように文字を配列に収め，それに合わせて自動でフォントを抽出し表示バッファに展開できるような処理を書きました．

これにより非常に容易に文字を出せるようになったためデバッグの効率が飛躍的に向上しました．

```c
#define CHARACTER_NORMAL									0x00
#define CHARACTER_DOUBLE_WIDTH								0x01
#define CHARACTER_DOUBLE_HEIGHT								0x02
#define CHARACTER_QUAD_SIZE									0x03

//文字列をバッファにセット
void Set_Text(uint8_t x,			//X位置
		uint8_t y,					//Y位置
		uint8_t *str,				//表示文字
		uint8_t length,				//文字列の長さ
		uint8_t flag				//倍角フラグ
		);
```

このような宣言になっており，実際に使用する際には

```c
const char msg1[] = "Initialise OK.";
Set_Text(12, 0, &msg1[0], sizeof(msg1), CHARACTER_NORMAL);
```

このように書けば良いです．



##### 表示内容の切り替え(文字面・グラフィック面)

今回制作した液晶コントローラはバッファを３面持っています．

- 表示用バッファ
- グラフィック面バッファ
- 文字面バッファ

表示内容を変更させるには，まず対象のバッファの内容を書き換えた上で表示用バッファに内容をコピーすることになります．

後で述べるオーバーレイ機能を組み合わせることにより，かなり自由度の高い画面表示が可能になっています．



##### オーバーレイ機能(OR/XOR)

これまでの機能では文字かグラフィックのどちらかしか表示できませんでしたが，オーバーレイ機能により重ね合わせることが可能になります．

ORモードとXORモードが選択でき，視認性の高いテキスト表示が可能になります．

負荷が重いため，それなりに注意して使う必要があります．

実際の処理はコードを見る方が早いでしょう．

```c
void Overlay(uint8_t flag) {
	Draw_Text_Buffer();			//文字バッファの内容からグラフィックデータを作成
	switch (flag) {
		case OVERLAY_OR:
			for (uint16_t i = 0; i < 30 * 64; i++) {
				display_buffer[i] = character_buffer[i] | graphics_buffer[i];
			}
			break;
		case OVERLAY_XOR:
			for (uint16_t i = 0; i < 30 * 64; i++) {
				display_buffer[i] = character_buffer[i] ^ graphics_buffer[i];
			}
			break;
	}
}
```

やっていることは単純ですね．

単純ですがこれだけでかなり表現力に差が出ます．



#### SapphireMIDIParser

単純なMIDIのパーサの実装です．

MIDI1.0規格書に準拠しており，基本的なメッセージを処理します．

以下のシステムエクスクルーシブにも対応しています

- GMシステムオン
- XGシステムオン
- GSリセット
- 88モードリセット

以前にもMIDIのパーサは実装したことがありましたが，今回はよりコンパクトに実装し直しました．

一番コアの受信処理のみ記述します．

```c
uint8_t Parse_Midi_Data(uint8_t data) {
	/*SysEx処理*/
	if (flag_sysex) {
		if (data == 0xF7) {
			flag_sysex = 0x00;
			return Decode_System_Exclusive();
		}
		midi_exclusive_message[exclusive_ptr++] = data;
	}

	if (data & 0x80) {

		//システムエクスクルーシブ開始
		if (data == 0xF0) {
			flag_sysex = 0x01;
			memset(midi_exclusive_message, 0x00, 32);
			exclusive_ptr = 0x00;
			return MIDI_IGNORE;
		}

		//無視する
		if (data >= 0xF8) return MIDI_IGNORE;

		running_status_buffer = data;

		flag_3bytes = 0x00;

		return MIDI_IGNORE;

	} else {

		if (flag_3bytes) {
			flag_3bytes = 0x00;
			work_midi[0] = running_status_buffer;
			work_midi[2] = data;
			return Decode_Message();

		} else {
			if (!running_status_buffer) return MIDI_IGNORE;

			if (running_status_buffer < 0xC0) {
				flag_3bytes = 0x01;
				work_midi[1] = data;
				return MIDI_IGNORE;
			}

			if (running_status_buffer < 0xE0) {
				work_midi[0] = running_status_buffer;
				work_midi[1] = data;
				return Decode_Message();
			}

			if (running_status_buffer < 0xF0) {
				flag_3bytes = 0x01;
				work_midi[1] = data;
				return MIDI_IGNORE;
			}

			if (running_status_buffer == 0xF2) {
				running_status_buffer = 0x00;
				flag_3bytes = 0x01;
			}

			if (running_status_buffer == 0xF1 || running_status_buffer == 0xF3) {
				running_status_buffer = 0x00;
				flag_3bytes = 0x00;
			}
		}
	}
}
```



#### SapphireMIDIDisplay

本来は他から扱うものでもないので要素と言えるかは微妙ですが，他のモジュールを駆使したソフトウェアとも言える存在です．

各種画面付き音源のように発音状況やパラメタを視覚的に表示することができます．

MIDIモードとシリアル制御モードは排他で，切り替えることができます．

MIDIモードの際はホストからの制御を一切必要とせず，単体で動作します．

バーグラフの表示や埋め込んだ音色画像データなどを展開，表示する機能を持っています．

当初の予定では別マイコンで処理をさせる予定でしたが，思いのほかリソースが余っていたため急遽組み込みました．

ここには収まり切らない分量なので別日に別記事として公開する予定です．

#### 工夫した点

メモリが普通に使ってると足りなくなったので今まで使ってこなかったCCMRAM領域をフルで使用しています．

CCMRAM空間はDMAの対象として選ぶことができない点には注意が必要です．

そのため，空きメモリ量がとんでもないことになってたり...

![unknown.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/4720aad2-660f-8947-1c99-863761b0c132.png)


#### 困ったこと

CubeMXの吐き出すコードの順序によりDMAが初期化されないというバグがあったため，コードを自動生成するたびに該当箇所の修正が必要となっていた．



### ホスト

今回はデバッグも大変なのでWindowsPC上で制御するプログラムを制作しました．

以前はC + Win32APIで書いていましたが，GUIを加えるのが鬼面倒だったのでC#を使用しました．

主に以下のような機能が実装されています.

- ボーレート補正機能
- 画像変換(解像度変換，モノクロ変換機能内蔵)
- 動画変換(上に加えて最適フレームレート検出＆補正機能)
- 文字列転送機能
- オーバーレイ設定

画像変換は単なる２値化ではなくフィルタをかけているのでそれなりに綺麗です，遠目だと何かわかる程度には．

![E-MpjwIVQAokClQ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/ee3e4b09-299e-7111-cb1e-285c0fa680e4.png)

UIはこんな感じ，自分しか使わないのでかなり適当です．

## 基板

### 制作方針

回路図は存在しません，脳内で組み立てつつ問題に応じて回路を柔軟に変更しています．

また，半田付けは最低限に抑えトライアンドエラーのしやすいワイヤラッピング配線を主に使用しています．

そのため，基本的には電源とクロックのみはんだメッキ線による配線を行なっています．

ラッピングポストの代わりにピンヘッダを使いましたがロジックアナライザも使いやすくて良い感じです．



### 外観

![IMG_8650_R.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/38dc674e-9f9b-323e-09c0-0424b298a423.jpeg)

![IMG_8658_R.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545164/45564afb-3e85-f551-2c4d-50a26cece15a.jpeg)

マイコンの下にはEEPROM(24FC512)とオシレータ(EXO3-16MHz)があります．

また，液晶にピッタリハマる基板は採寸の上，CADを引いて中華業者に発注しました．

コンデンサとLEDはデジットでまとめ売りされていたものから使用，全体的に割と安く仕上がっています．

## 今後について

現在開発中のハードウェアMIDI音源に組み込む予定ですが，各種半導体の枯渇につき部品の入手が困難となっているため凍結中...悲しい．



## 最後に

ご質問・ご指摘等ございましたらコメントまでお願いいたします．

最後に，Twitterでさまざまな助言をしていただいた皆様にこの場を借りて厚く御礼申し上げます．