「ESP32-S3」を「DosPC」らしく仕立てようと頑張ってます。今回の改良は次の3点です。ご利用は自己責任で。
1.横80桁、縦24行(800*480ピクセル)の大画面で日本語も見易く
2.USBキーボードからの直接コマンド入力
3.UP/DOWNキーで上下スムーズスクロール可能に
環境・構成は以下です。
1.Windows 11 Pro
2.Arduino IDE 2.3.6
3.FREENOVE ESP32-S3 WROOM(Flash 8MB) "Huge APP ..."でコンパイル
「SD_MMC」ライブラリで、ボード下面の microSD スロット使用
4.4 inch 800*480 Display(NT35510 16bit Parallel 接続)
「LovyanGFX」ライブラリで表示
5.ELECOM ワイアレスキーボード TK-EDM105T BK 2.4GHz
「EspUsbHost」ライブラリで接続、使用
長くなるので詳しいことは過去の投稿をご覧ください。
スクロール動作の確認のため、次の3つのコマンドを実装しています。
・ls
- microSDカード内のファイルを全て表示
・ls フォルダー名
- 指定のフォルダー内を表示
・sd
- microSDの情報を表示
ls
またはls /
などで画面に収まりきらないほど表示させた後に、必要に応じUP
キーかDOWN
キーを押すとスクロールします。キー一発目は反応しずらいですが(ここは仕様上仕方ないところ)、数回押しているとスクロールし始めます。プログラムとしては、UP
キーが押された時のkeycode
の変化を調べ、EspUsbHost.cpp
の仮想関数を次のように実装しました。
// virtual関数の実装
class MyEspUsbHost : public EspUsbHost {
void onKeyboard(hid_keyboard_report_t report, hid_keyboard_report_t last_report) {
if (last_report.keycode[0] == 0 && report.keycode[0] == 82)
UPbegiTime = millis(); // 計測開始
if (last_report.keycode[0] == 82 && report.keycode[0] == 0)
UPkeptTime = millis() - UPbegiTime; // 経過時間を計算
if (last_report.keycode[0] == 0 && report.keycode[0] == 81)
DWbegiTime = millis(); // 計測開始
if (last_report.keycode[0] == 81 && report.keycode[0] == 0)
DWkeptTime = millis() - DWbegiTime; // 経過時間を計算
Serial.printf("modifier=[0x%02x]->[0x%02x], Key0=[0x%02x]->[0x%02x], Key1=[0x%02x]->[0x%02x], Key2=[0x%02x]->[0x%02x], Key3=[0x%02x]->[0x%02x], Key4=[0x%02x]->[0x%02x], Key5=[0x%02x]->[0x%02x] /n/r",
last_report.modifier,
report.modifier,
last_report.keycode[0],
report.keycode[0],
last_report.keycode[1],
report.keycode[1],
last_report.keycode[2],
report.keycode[2],
last_report.keycode[3],
report.keycode[3],
last_report.keycode[4],
report.keycode[4],
last_report.keycode[5],
report.keycode[5]);
}
void onKeyboardKey(uint8_t ascii, uint8_t keycode, uint8_t modifier) {
Serial.printf("a:%d ;", ascii);
Serial.printf("k:%d ;", keycode);
Serial.printf("m:%d", modifier);
Serial.println("");
if (ascii == 10 || ascii == 13) { // LF, CR
l_buffer.push_back(">" + cmd); // stack command string
++by;
//if (CurY < ROWS - 1)
++CurY;
_inputFlg = true; // break;
} else if (keycode == 82) { // [UP]key
int16_t uptime = UPkeptTime / 200;
//Serial.println(uptime);
if (uptime == 0) uptime = 1; // 初回は0となるので
while (uptime > 0) { // 複数行スクロール可能に
if (2 + by - ROWS + pastY > 0) {
pastY--; // スクロールバック
ascr();
set_pri(W - (6 * cellX), (ROWS - 1) * cellY, "l." + (String)(by - 1 + pastY));
}
uptime--;
}
} else if (keycode == 81) { // [DOWN]key
int16_t dwtime = DWkeptTime / 200;
if (dwtime == 0) dwtime = 1; // 初回は0となるので
while (dwtime > 0) { // 複数行スクロール可能に
if (pastY < 0) {
pastY++; // スクロールフォワード
ascr();
set_pri(W - (6 * cellX), (ROWS - 1) * cellY, "l." + (String)(by - 1 + pastY));
}
dwtime--;
}
} else {
if (31 < ascii && ascii < 127) {
if (pastY < 0) {
pastY = 0;
ascr();
}
cmd = cmd + (char)ascii;
} else if (ascii == 8 && cmd.length() > 0) { // BackSpace
cmd = cmd.substring(0, cmd.length() - 1);
}
Serial.println(">" + cmd);
set_pri(0, CurY * cellY, ">" + cmd); // update command string
}
}
};
スケッチと同じフォルダーに、
`myLovyanGFX.hpp`(Display専用に書いたもの)
`sd_read_write.h`(原本のまま)
`sd_read_write.cpp`(原本のまま)
`EspUsbHost.h`(付け加えたもの。上記過去投稿をご参考に)
`EspUsbHost.cpp`(原本のまま)
を置きコンパイルします。
#pragma once
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
class LGFX : public lgfx::LGFX_Device {
lgfx::Panel_NT35510 _panel_instance;
lgfx::Bus_Parallel16 _bus_instance; //16bit Parallelのインスタンス(ESP32S3)
public:
LGFX(void) { // バス制御の設定を行います。
auto cfg = _bus_instance.config(); // バス設定用の構造体を取得します。
// 16ビットパラレルバスの設定
cfg.freq_write = 20000000; // 送信クロック(最大20MHz,80MHzを整数割の値に丸める)
cfg.pin_wr = 46; // WR を接続しているピン番号
cfg.pin_rd = 9;//14; // RD を接続しているピン番号
cfg.pin_rs = 8;//3; // RS(D/C)を接続しているピン番号
cfg.pin_d0 = 18; // D0 を接続しているピン番号
cfg.pin_d1 = 21;// 0;//36; // D1 を接続しているピン番号
cfg.pin_d2 = 17; // D2 を接続しているピン番号
cfg.pin_d3 = 47;//35;//38;//37; // D3 を接続しているピン番号
cfg.pin_d4 = 16; // D4 を接続しているピン番号
cfg.pin_d5 = 45;//36;//39;//38; // D5 を接続しているピン番号
cfg.pin_d6 = 15; // D6 を接続しているピン番号s
cfg.pin_d7 = 0;//37;//40;//39; // D7 を接続しているピン番号
cfg.pin_d8 = 1;//2; // D8 を接続しているピン番号
cfg.pin_d9 = 4; // D9 を接続しているピン番号
cfg.pin_d10 = 2;//42; // D10を接続しているピン番号
cfg.pin_d11 = 5; // D11を接続しているピン番号
cfg.pin_d12 = 42;//41; // D12を接続しているピン番号
cfg.pin_d13 = 6; // D13を接続しているピン番号
cfg.pin_d14 = 41;//40; // D14を接続しているピン番号
cfg.pin_d15 = 7; // D15を接続しているピン番号
_bus_instance.config(cfg); // 設定値をバスに反映します。
_panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。
{ // 表示パネル制御の設定
auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。
cfg.pin_cs = 3;//8; // CSが接続されているピン番号 (-1 = disable)
cfg.pin_rst = -1;//9; // RSTが接続されているピン番号 (-1 = disable)
cfg.pin_busy = -1; // BUSYが接続されているピン番号 (-1 = disable)
cfg.memory_width = 480; // ドライバICがサポートしている最大の幅
cfg.memory_height =800; // ドライバICがサポートしている最大の高さ
cfg.panel_width = 480; // 実際に表示可能な幅
cfg.panel_height =800; // 実際に表示可能な高さ
cfg.offset_x = 0; // パネルのX方向オフセット量
cfg.offset_y = 0; // パネルのY方向オフセット量
cfg.offset_rotation = 0; // 回転方向の値のオフセット 0~7 (4~7は上下反転)
cfg.dummy_read_pixel = 8; // ピクセル読出し前のダミーリードのビット数
cfg.dummy_read_bits = 1; // ピクセル以外のデータ読出し前のダミーリードのビット数
cfg.readable = false; // データ読出しが可能な場合 trueに設定
cfg.invert = false; // パネルの明暗が反転してしまう場合 trueに設定
cfg.rgb_order = true; // パネルの赤と青が入れ替わってしまう場合 trueに設定
cfg.dlen_16bit = true; // データ長を16bit単位で送信するパネルの場合trueに設定
cfg.bus_shared = false; // SDカードとバス共有はtrueに設定
_panel_instance.config(cfg);
}
setPanel(&_panel_instance); // 使用するパネルをセットします。
}
};
スケッチ本体です。
#include "myLovyanGFX.hpp"
#include "EspUsbHost.h"
#include "sd_read_write.h"
#include "SD_MMC.h"
#include <string>
#include <vector>
using namespace std;
// Display Constants
#define W 800
#define H 480
#define cellX 10 //8
#define cellY 20
//#define COLS (int)(W / cellX)
#define ROWS (int)(H / cellY) - 1
#define GREY 0x7BEF
#define BLACK 0x0000
#define WHITE 0xFFFF
#define GREEN 0x03E0
#define MAGENTA 0xF81F
#define CYAN 0x07FF
#define BLUE 0x001F
#define YELLOW 0xFFE0
#define RED 0xF800
#define NAVY 0x0002
// SD_MMC FREENOVE ESP32-S3 WROOM 内蔵SDスロット使用のため
#define SD_MMC_CMD 38 // MISO. Please do not modify it.
#define SD_MMC_CLK 39 // CLK. Please do not modify it.
#define SD_MMC_D0 40 // MOSI. Please do not modify it.
File fp;
const String _rt = "/";
void ascr();
void set_pri(uint16_t x, uint16_t y, String s);
// For Disolay lines
static LGFX t;
std::vector<String> l_buffer;
String cmd;
volatile bool _inputFlg = false;
int8_t CurY = 0;
int16_t by = 0;
int16_t pastY = 0; // スクロールのため
// USB Keyboard
int16_t UPbegiTime; // UP/DOWN スクロール継続のため
int16_t UPkeptTime;
int16_t DWbegiTime;
int16_t DWkeptTime;
// virtual関数の実装
class MyEspUsbHost : public EspUsbHost {
void onKeyboard(hid_keyboard_report_t report, hid_keyboard_report_t last_report) {
if (last_report.keycode[0] == 0 && report.keycode[0] == 82)
UPbegiTime = millis(); // 計測開始
if (last_report.keycode[0] == 82 && report.keycode[0] == 0)
UPkeptTime = millis() - UPbegiTime; // 経過時間を計算
if (last_report.keycode[0] == 0 && report.keycode[0] == 81)
DWbegiTime = millis(); // 計測開始
if (last_report.keycode[0] == 81 && report.keycode[0] == 0)
DWkeptTime = millis() - DWbegiTime; // 経過時間を計算
Serial.printf("modifier=[0x%02x]->[0x%02x], Key0=[0x%02x]->[0x%02x], Key1=[0x%02x]->[0x%02x], Key2=[0x%02x]->[0x%02x], Key3=[0x%02x]->[0x%02x], Key4=[0x%02x]->[0x%02x], Key5=[0x%02x]->[0x%02x] /n/r",
last_report.modifier,
report.modifier,
last_report.keycode[0],
report.keycode[0],
last_report.keycode[1],
report.keycode[1],
last_report.keycode[2],
report.keycode[2],
last_report.keycode[3],
report.keycode[3],
last_report.keycode[4],
report.keycode[4],
last_report.keycode[5],
report.keycode[5]);
}
void onKeyboardKey(uint8_t ascii, uint8_t keycode, uint8_t modifier) {
Serial.printf("a:%d ;", ascii);
Serial.printf("k:%d ;", keycode);
Serial.printf("m:%d", modifier);
Serial.println("");
if (ascii == 10 || ascii == 13) { // LF, CR
l_buffer.push_back(">" + cmd); // stack command string
++by;
//if (CurY < ROWS - 1)
++CurY;
_inputFlg = true; // break;
} else if (keycode == 82) { // [UP]key
int16_t uptime = UPkeptTime / 200;
//Serial.println(uptime);
if (uptime == 0) uptime = 1; // 初回は0となるので
while (uptime > 0) { // 複数行スクロール可能に
if (2 + by - ROWS + pastY > 0) {
pastY--; // スクロールバック
ascr();
set_pri(W - (6 * cellX), (ROWS - 1) * cellY, "l." + (String)(by - 1 + pastY));
}
uptime--;
}
} else if (keycode == 81) { // [DOWN]key
int16_t dwtime = DWkeptTime / 200;
if (dwtime == 0) dwtime = 1; // 初回は0となるので
while (dwtime > 0) { // 複数行スクロール可能に
if (pastY < 0) {
pastY++; // スクロールフォワード
ascr();
set_pri(W - (6 * cellX), (ROWS - 1) * cellY, "l." + (String)(by - 1 + pastY));
}
dwtime--;
}
} else {
if (31 < ascii && ascii < 127) {
if (pastY < 0) {
pastY = 0;
ascr();
}
cmd = cmd + (char)ascii;
} else if (ascii == 8 && cmd.length() > 0) { // BackSpace
cmd = cmd.substring(0, cmd.length() - 1);
}
Serial.println(">" + cmd);
set_pri(0, CurY * cellY, ">" + cmd); // update command string
}
}
};
MyEspUsbHost usbHost;
void GetKey(void* args) {
while (1) {
delay(1);
usbHost.task();
}
}
// ls
int pn;
void ls_0(File fp) {
String s;
while (File file = fp.openNextFile()) {
// Loop through all the files in the root directory
if (file.isDirectory()) {
s = "/" + (String)file.name() + "/";
Serial.print(s);
} else {
s = (String)file.name() + " : size=" + (String)file.size() + "b";
Serial.print(s);
}
tString(s);
file.close(); // Close the file
}
Serial.println("--- Found Dirs/Files.");
tString("--- Found Dirs/Files.");
}
TaskHandle_t thp;
void setup(void) {
Serial.begin(115200);
Serial.println("DOS PC Console Start");
t.init();
delay(1000);
t.setRotation(1);
t.setColorDepth(16);
t.startWrite(); // ここでstartWrite()することでSpeedUp ?
t.setBaseColor(NAVY);
t.clear();
t.setFreeFont(&fonts::lgfxJapanGothic_20); //&lgfx::fonts::Font2);
t.setTextSize(1);
t.setTextWrap(false, false);
t.setTextColor(TFT_WHITE, NAVY);
SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0);
if (!SD_MMC.begin("/SD", true, true, SDMMC_FREQ_DEFAULT, 5)) {
Serial.println("Card Mount Failed");
return;
}
usbHost.begin();
usbHost.setHIDLocal(HID_LOCAL_Japan_Katakana);
xTaskCreateUniversal(GetKey, "Get Key", 4096, NULL, 3, &thp, CONFIG_ARDUINO_RUNNING_CORE);
}
void loop(void) {
cmd = "";
Serial.println(">");
set_pri(0, CurY * cellY, ">");
set_pri(W - (6 * cellX), CurY * cellY, "l." + (String)by);
_inputFlg = false;
while (_inputFlg == false) { ; }
//pri_both(">"+cmd);
if (cmd.substring(0, 2) == "sd") { // SD information
SDMMC_Info();
} else if (cmd.substring(0, 2) == "ls") { // SD information
if (cmd.length() == 2) { // list up all dirs/files
File root = SD_MMC.open(_rt);
pn = 0;
printDirectory(root, 0);
} else {
String s, fn;
if (cmd.length() == 4 && cmd.substring(3, 1) == _rt) fn = _rt;
else fn = _rt + cmd.substring(3); // "ls dir/file"
if (SD_MMC.exists(fn)) {
s = fn + " exists.";
pri_both(s);
File fp = SD_MMC.open(fn);
if (fp.isDirectory()) ls_0(fp); //そのdir内のみを表示
} else {
s = fn + " not exists.";
pri_both(s);
}
}
} else {
pri_both(cmd + " ?");
}
}
// パラメータを切り出す
int split(String data, char delimiter, String* dst, int num) {
int index = 0;
int len = data.length();
for (int i = 0; i < len; i++) {
char tmp = data.charAt(i);
if (tmp == delimiter) {
index++;
if (index > num - 1) return -1;
} else {
dst[index] += tmp;
}
}
return index + 1;
}
void set_pri(uint16_t x, uint16_t y, String s) {
t.fillRect(x, y, W - x - 2, cellY, NAVY);
t.setCursor(x, y);
t.print(s);
}
void pri_both(String s) {
Serial.println(s);
tString(s);
}
void tString(String s) {
l_buffer.push_back(s);
set_pri(0, CurY * cellY, s);
++by;
//if (CurY < ROWS - 1)
++CurY;
ascr();
}
void ascr() { // TFT下端に達したらスクロール
if (by >= ROWS) {
for (int y = 0; y < ROWS; y++) {
set_pri(0, y * cellY, l_buffer[2 + by - ROWS + pastY + y]);
}
CurY = ROWS - pastY;
}
}
void SDMMC_Info() {
uint8_t cardType = SD_MMC.cardType(); // カード情報
String s = "Card Type: ";
if (cardType == CARD_MMC) s += "MMC";
else if (cardType == CARD_SD) s += "SDSC";
else if (cardType == CARD_SDHC) s += "SDHC";
else s += "UNKNOWN";
pri_both(s);
uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024 * 1024);
s = "Card Size: " + (String)(cardSize) + "GB";
pri_both(s);
}
void printDirectory(File rt, int numTabs) {
while (File file = rt.openNextFile()) {
Serial.printf(" #%d=", ++pn);
for (uint8_t i = 0; i < numTabs; i++) Serial.print('\t');
if (file.isDirectory()) {
Serial.println((String)file.name() + "/");
tString(" #" + (String)pn + "=" + (String)file.name() + '/');
printDirectory(file, numTabs + 1);
} else { // files have sizes, directories do not
Serial.printf("%s\t\t%d\n\r", file.name(), file.size());
tString(" #" + (String)pn + "=" + (String)file.name() + " _ " + (String)file.size());
}
file.close();
}
}
全くの手探りでここまでたどり着きました。最後まで見ていただきありがとうございました。