Skip to content

Instantly share code, notes, and snippets.

@masuidrive
Last active September 22, 2024 14:16
Show Gist options
  • Save masuidrive/4d8304a0495452ee09d227e5de1490eb to your computer and use it in GitHub Desktop.
Save masuidrive/4d8304a0495452ee09d227e5de1490eb to your computer and use it in GitHub Desktop.
自分用のキーボードにI2Cで接続するマウスをM5Unifiedで作った
/*
# M5Unified Touch Circles Documentation
## 概要
このプログラムは、M5Unifiedデバイス上でタッチ操作を検出し、視覚的フィードバックを提供すると同時に、I2C経由で外部デバイスにマウス操作をシミュレートします。
一般的なUSBやシリアルのプロトコルではなく、専用のI2Cプロトコルを話します。
## 主要機能
1. タッチ操作の検出と分類(タップ vs 移動)
2. 視覚的フィードバック(円の描画)
3. I2C経由でのマウス操作シミュレーション
## 重要な実装詳細
### 回転設定
- `ROTATION`定数で画面の回転を設定可能(0: 0°, 1: 90°, 2: 180°, 3: 270°)
- これにより、デバイスの物理的な向きに合わせてソフトウェアを調整可能
### 動的移動スケーリング
- `calculateMoveScale`関数でCカーブを使用
- タッチの移動速度に応じて移動量を非線形にスケーリング
- 低速度では細かい制御が可能で、高速度では素早い移動が可能
### タップ検出とボタンシミュレーション
- タップ検出時、200ミリ秒間I2C経由でボタン押下をシミュレート
- これにより、タップ操作が外部デバイスに明確に伝わる
### 視覚的フィードバック
- タップ操作:拡大する中空の円
- 移動操作:固定サイズの塗りつぶされた円
- どちらもフェードアウト効果あり
### I2C通信
- ボタン状態、X/Y座標の相対移動量を送信
- 注意:I2Cの座標系はデバイスの座標系と逆になっている
## 設定パラメータ
- `MIN_MOVE_SCALE`と`MAX_MOVE_SCALE`:移動量のスケーリング範囲
- `SPEED_CALC_INTERVAL`:移動速度計算の間隔
- `TAP_THRESHOLD`:タップと判定する最大時間
- `MOVE_THRESHOLD`:移動と判定する最小距離
## 注意点
1. パフォーマンス考慮:
- 円の描画数に上限(`MAX_CIRCLES`)を設定
- 古い円を自動的に削除
2. タイミング管理:
- タップ検出、移動速度計算、視覚効果のタイミングは慎重に調整が必要
3. I2C通信:
- 外部デバイスとの互換性確保のため、通信プロトコルの変更には注意が必要
4. スプライトとメモリ管理:
- フルスクリーンサイズのスプライト作成には大量のメモリが必要
- メインRAMでの作成が失敗する場合、PSRAMの使用を検討
- スプライト作成時のメモリ確保順序:
a) メインRAMでフルサイズ試行
b) PSRAMが利用可能な場合、PSRAMでフルサイズ試行
c) 上記が失敗した場合、サイズを少し縮小して再試行
- PSRAMを使用する場合、アクセス速度がメインRAMより遅い可能性を考慮
- アプリケーション起動時に利用可能なメモリ量(ヒープとPSRAM)をチェックし、
リソース使用を適宜調整することを推奨
5. デバイス互換性:
- M5Stackの異なるモデル間でメモリ構成が異なる可能性があるため、
複数のデバイスでのテストを推奨
*/
#include <M5Unified.h>
#include <Wire.h>
#define MAX_CIRCLES 50
#define FADE_SPEED 10
#define INITIAL_SIZE_MOVE 10
#define INITIAL_SIZE_TAP 16
#define FINAL_SIZE_TAP 96
#define TAP_THRESHOLD 100 // タップと判断する最大時間(ミリ秒)
#define MOVE_THRESHOLD 5 // 移動と判断する最小距離(ピクセル)
//#define MIN_MOVE_SCALE 2.0f
//#define MAX_MOVE_SCALE 4.0f
#define MIN_MOVE_SCALE 1.0f
#define MAX_MOVE_SCALE 2.0f
#define SPEED_CALC_INTERVAL 300 // 速度計算の間隔(ミリ秒)
#define TAP_BUTTON_DURATION 200 // タップ時のボタン押下時間(ミリ秒)
// 回転設定(0: 0°, 1: 90°, 2: 180°, 3: 270°)
const int ROTATION = 0;
// I2Cレジスタアドレスの定義
const uint8_t REG_BUTTON = 0x01;
const uint8_t REG_X = 0x02;
const uint8_t REG_Y = 0x03;
// I2Cスレーブデバイスのアドレス
#define SLAVE_ADDRESS 0x32
// I2CのSDAとSCLピンを指定
//#define SDA_PIN 13
//#define SCL_PIN 15
enum TouchType {
NONE,
TAP,
MOVE
};
struct Circle {
float x;
float y;
uint8_t alpha;
float size;
TouchType type;
};
struct TouchInfo {
bool isPressed;
float x;
float y;
unsigned long startTime;
float startX;
float startY;
float lastSentX;
float lastSentY;
float lastX;
float lastY;
unsigned long lastMoveTime;
float currentSpeed;
};
Circle circles[MAX_CIRCLES];
int circleCount = 0;
LGFX_Sprite* sprite = nullptr;
TouchInfo currentTouch = {false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint8_t currentButtons = 0;
unsigned long tapStartTime = 0;
// 色をブレンドする関数
uint16_t blendColor(uint16_t bg, uint16_t fg, uint8_t alpha) {
uint8_t fg_r = (fg >> 11) & 0x1F;
uint8_t fg_g = (fg >> 5) & 0x3F;
uint8_t fg_b = fg & 0x1F;
uint8_t bg_r = (bg >> 11) & 0x1F;
uint8_t bg_g = (bg >> 5) & 0x3F;
uint8_t bg_b = bg & 0x1F;
uint8_t r = ((fg_r * alpha) + (bg_r * (255 - alpha))) / 255;
uint8_t g = ((fg_g * alpha) + (bg_g * (255 - alpha))) / 255;
uint8_t b = ((fg_b * alpha) + (bg_b * (255 - alpha))) / 255;
return ((r & 0x1F) << 11) | ((g & 0x3F) << 5) | (b & 0x1F);
}
// I2C通信ヘルパー関数
void i2cWriteAbsolute(uint8_t reg, uint8_t value) {
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
}
void i2cWriteRelativeInt8(uint8_t reg, int8_t delta) {
uint8_t command = 0x40 | (reg & 0x1F);
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(command);
Wire.write((uint8_t)delta);
Wire.endTransmission();
}
void updateButtonState(bool pressed) {
uint8_t buttons = pressed ? 0x01 : 0x00;
if (buttons != currentButtons) {
currentButtons = buttons;
i2cWriteAbsolute(REG_BUTTON, currentButtons);
}
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.println("Initializing...");
sprite = new LGFX_Sprite(&M5.Display);
bool spriteCreated = false;
// フルスクリーンサイズでスプライトを作成
if (sprite->createSprite(M5.Display.width(), M5.Display.height())) {
Serial.println("Sprite created successfully with full screen size");
spriteCreated = true;
} else {
Serial.println("Failed to create sprite with full screen size");
// PSRAMを使用して再試行
if (psramFound()) {
sprite->setPsram(true);
if (sprite->createSprite(M5.Display.width(), M5.Display.height())) {
Serial.println("Sprite created successfully using PSRAM");
spriteCreated = true;
} else {
Serial.println("Failed to create sprite using PSRAM");
}
} else {
Serial.println("PSRAM not found");
}
// それでも失敗した場合、サイズを少し小さくして再試行
if (!spriteCreated) {
if (sprite->createSprite(M5.Display.width() - 8, M5.Display.height() - 8)) {
Serial.println("Sprite created successfully with slightly reduced size");
spriteCreated = true;
} else {
Serial.println("All attempts to create sprite failed");
}
}
}
if (spriteCreated) {
sprite->fillScreen(TFT_BLACK);
sprite->pushSprite(0, 0);
} else {
Serial.println("Failed to create sprite. Using direct drawing on display.");
M5.Display.fillScreen(TFT_BLACK);
}
// 画面の回転設定
M5.Display.setRotation(ROTATION);
// I2Cマスターとして初期化
#ifdef SDA_PIN
Wire.begin(SDA_PIN, SCL_PIN);
#else
Wire.begin(M5.Ex_I2C.getSDA(), M5.Ex_I2C.getSCL());
#endif
// 初期状態としてボタンレジスタを0に設定
i2cWriteAbsolute(REG_BUTTON, 0x00);
Serial.println("Initialization complete");
}
float calculateMoveScale(float speed) {
// Cカーブでスケールを計算
float normalizedSpeed = speed / 1000.0f; // 速度を0-1の範囲に正規化
float t = (normalizedSpeed < 0.5f) ? 2.0f * normalizedSpeed * normalizedSpeed : 1.0f - pow(-2.0f * normalizedSpeed + 2.0f, 2.0f) / 2.0f;
return MIN_MOVE_SCALE + (MAX_MOVE_SCALE - MIN_MOVE_SCALE) * t;
}
TouchType recognizeTouch() {
M5.update();
TouchType recognizedType = NONE;
if (M5.Touch.getCount()) {
auto touch = M5.Touch.getDetail();
float x = touch.x;
float y = touch.y;
if (touch.isPressed()) {
if (!currentTouch.isPressed) {
currentTouch.isPressed = true;
currentTouch.startTime = millis();
currentTouch.startX = x;
currentTouch.startY = y;
currentTouch.lastSentX = x;
currentTouch.lastSentY = y;
currentTouch.lastX = x;
currentTouch.lastY = y;
currentTouch.lastMoveTime = millis();
currentTouch.currentSpeed = 0;
}
currentTouch.x = x;
currentTouch.y = y;
float dx = currentTouch.x - currentTouch.startX;
float dy = currentTouch.y - currentTouch.startY;
float distance = sqrt(dx * dx + dy * dy);
if (distance > MOVE_THRESHOLD) {
recognizedType = MOVE;
unsigned long currentTime = millis();
unsigned long timeDiff = currentTime - currentTouch.lastMoveTime;
if (timeDiff >= SPEED_CALC_INTERVAL) {
float moveDistance = sqrt(
(currentTouch.x - currentTouch.lastX) * (currentTouch.x - currentTouch.lastX) + (currentTouch.y - currentTouch.lastY) * (currentTouch.y - currentTouch.lastY));
currentTouch.currentSpeed = moveDistance / (timeDiff / 1000.0f);
currentTouch.lastX = currentTouch.x;
currentTouch.lastY = currentTouch.y;
currentTouch.lastMoveTime = currentTime;
}
float moveScale = calculateMoveScale(currentTouch.currentSpeed);
float deltaX = currentTouch.x - currentTouch.lastSentX;
float deltaY = currentTouch.y - currentTouch.lastSentY;
int8_t scaled_dx = constrain((int8_t)(deltaX * moveScale), -127, 127);
int8_t scaled_dy = constrain((int8_t)(deltaY * moveScale), -127, 127);
i2cWriteRelativeInt8(REG_X, scaled_dx);
i2cWriteRelativeInt8(REG_Y, scaled_dy);
currentTouch.lastSentX = currentTouch.x;
currentTouch.lastSentY = currentTouch.y;
}
} else if (currentTouch.isPressed) {
unsigned long duration = millis() - currentTouch.startTime;
float dx = currentTouch.x - currentTouch.startX;
float dy = currentTouch.y - currentTouch.startY;
float distance = sqrt(dx * dx + dy * dy);
if (duration < TAP_THRESHOLD && distance <= MOVE_THRESHOLD) {
recognizedType = TAP;
tapStartTime = millis(); // タップ開始時間を記録
updateButtonState(true); // ボタンを押下状態に
}
currentTouch.isPressed = false;
}
}
return recognizedType;
}
void addCircle(float x, float y, TouchType type) {
if (circleCount < MAX_CIRCLES) {
circles[circleCount] = { x, y, 255, type == TAP ? INITIAL_SIZE_TAP : INITIAL_SIZE_MOVE, type };
circleCount++;
} else {
for (int i = 0; i < MAX_CIRCLES - 1; i++) {
circles[i] = circles[i + 1];
}
circles[MAX_CIRCLES - 1] = { x, y, 255, type == TAP ? INITIAL_SIZE_TAP : INITIAL_SIZE_MOVE, type };
}
}
void updateCircles() {
for (int i = 0; i < circleCount; i++) {
if (circles[i].alpha > FADE_SPEED) {
circles[i].alpha -= FADE_SPEED;
if (circles[i].type == TAP) {
float progress = 1.0f - (float)circles[i].alpha / 255.0f;
circles[i].size = INITIAL_SIZE_TAP + (FINAL_SIZE_TAP - INITIAL_SIZE_TAP) * progress;
}
} else if (circles[i].alpha > 0) {
circles[i].alpha = 0;
if (circles[i].type == TAP) {
circles[i].size = FINAL_SIZE_TAP;
}
}
}
int i = 0;
while (i < circleCount) {
if (circles[i].alpha == 0) {
for (int j = i; j < circleCount - 1; j++) {
circles[j] = circles[j + 1];
}
circleCount--;
} else {
i++;
}
}
}
void drawCircles() {
sprite->fillScreen(TFT_BLACK);
for (int i = 0; i < circleCount; i++) {
if (circles[i].alpha > 0) {
uint16_t circleColor = blendColor(TFT_BLACK, TFT_WHITE, circles[i].alpha);
if (circles[i].type == TAP) {
sprite->drawCircle(circles[i].x, circles[i].y, circles[i].size, circleColor);
} else {
sprite->fillCircle(circles[i].x, circles[i].y, circles[i].size, circleColor);
}
}
}
sprite->pushSprite(0, 0);
}
int ii = 0;
void loop() {
TouchType touchType = recognizeTouch();
if (touchType != NONE) {
addCircle(currentTouch.x, currentTouch.y, touchType);
}
// タップ後のボタン解放処理
if (tapStartTime > 0 && millis() - tapStartTime >= TAP_BUTTON_DURATION) {
updateButtonState(false); // ボタンを解放状態に
tapStartTime = 0; // タイマーをリセット
}
updateCircles();
drawCircles();
delay(10);
}

キーボードとLCD、トラックパッド統合のためのI2Cマスタースレーブ通信ガイド

目次

  1. はじめに
  2. I2Cスレーブアドレス
  3. コマンド構造
  4. レジスタマップ
  5. キーボードステータスの読み取り
  6. レジスタへの書き込み
  7. レジスタからの読み取り
  8. 16ビットレジスタアクセス
  9. マスターコードの例
  10. 注意事項とベストプラクティス
  11. 付録: キーボードステータスバイト

はじめに

このドキュメントは、I2Cマスター側の開発者向けに、LCDディスプレイやトラックパッドを搭載したカスタムキーボードデバイスとの通信方法を詳細に説明します。このキーボードはI2Cスレーブデバイスとして動作し、マスター(LCDコントローラーやトラックパッドコントローラーなど)はキーボードのステータスを読み取ったり、マウスやスクロールデータを送信したりすることができます。


I2Cスレーブアドレス

キーボードデバイスのI2Cスレーブアドレスは以下の通りです:

  • スレーブアドレス: 0x32

注記: 他のI2Cデバイスとアドレスが競合しないことを確認してください。必要に応じて、キーボードのファームウェアでアドレスを変更してください。


コマンド構造

マスターからスレーブ(キーボード)に対してコマンドを送信し、レジスタに対して読み書き操作を行います。コマンドは、まずコマンドバイトを送信し、その後に必要なデータバイトを送ります。コマンドには絶対値書き込みと相対値書き込みの2種類があります。

コマンドカテゴリ

  1. 0x00 - 0x1F: 絶対値書き込みコマンド

    • 用途: レジスタに1バイトの即値(uint8_t)を直接書き込みます。
  2. 0x20 - 0x3F: 相対値書き込みコマンド(uint8_t メモリ)

    • 用途: レジスタに対して、符号付き1バイト(int8_t)の増減値を加えます。レジスタは uint8_t として扱います。
  3. 0x40 - 0x5F: 相対値書き込みコマンド(int8_t メモリ)

    • 用途: レジスタに対して、符号付き1バイト(int8_t)の増減値を加えます。レジスタは int8_t として扱います。
  4. 0x60 - 0x7F: 相対値書き込みコマンド(uint16_t メモリ)

    • 用途: レジスタペアに対して、符号付き2バイト(int16_t)の増減値を加えます。レジスタペアは uint16_t として扱います(リトルエンディアン)。
  5. 0x80 - 0x9F: 相対値書き込みコマンド(int16_t メモリ)

    • 用途: レジスタペアに対して、符号付き2バイト(int16_t)の増減値を加えます。レジスタペアは int16_t として扱います(リトルエンディアン)。

書き込みコマンド

書き込み操作を行うには、マスターはまずコマンドバイトを送信し、その後に必要なデータバイトを送信します。コマンドカテゴリに応じて、絶対値または相対値を書き込みます。

相対値コマンド

相対値コマンドは、現在のレジスタの値に指定した増減値を加算します。これにより、現在の値を事前に知ることなく、値をインクリメントやデクリメントすることが可能です。


レジスタマップ

キーボードのメモリは32個のレジスタ(0x00 から 0x1F)に分かれています。それぞれのレジスタには特定の機能が割り当てられています。

レジスタ0: キーボードステータス

  • アドレス: 0x00
  • サイズ: 1バイト (uint8_t)
  • R/W: 読み取り専用
  • 説明: キーボードの修飾キーとレイヤーの状態を保持しています。

ビット割り当て:

ビット 機能 説明
7 Shift 1 = Shiftキーが押されています
6 Ctrl 1 = Ctrlキーが押されています
5 Cmd/GUI 1 = Cmd/GUIキーが押されています
4 Opt/Alt 1 = Option/Altキーが押されています
3-2 予約 現在は使用されていません
1-0 レイヤー 現在のアクティブレイヤー(0〜3)を示します

レジスタ1-5: マウスとスクロールデータ

レジスタ 説明 サイズ R/W
0x01 ボタン状態 uint8 1 書き込み
0x02 X座標 int8 1 書き込み
0x03 Y座標 int8 1 書き込み
0x04 水平スクロール int8 1 書き込み
0x05 垂直スクロール int8 1 書き込み

ボタンレジスタ(0x01)ビット割り当て:

ビット 機能 説明
0 左ボタン 1 = 左ボタンが押されています
1 右ボタン 1 = 右ボタンが押されています
2 中ボタン 1 = 中ボタンが押されています
3-7 予約 現在は使用されていません

マウス・スクロールデータの説明:

  • X座標 (0x02): 水平方向のマウス移動量を示します。符号付き8ビット整数(int8)で表現されます。
  • Y座標 (0x03): 垂直方向のマウス移動量を示します。符号付き8ビット整数(int8)で表現されます。
  • 水平スクロール (0x04): 水平方向のスクロール量を示します。符号付き8ビット整数(int8)で表現されます。
  • 垂直スクロール (0x05): 垂直方向のスクロール量を示します。符号付き8ビット整数(int8)で表現されます。

レジスタ6-31: 予約領域

これらのレジスタは将来の拡張(追加の周辺機器や機能の拡張)用に予約されています。現在は未使用であり、アクセスしないでください。


キーボードステータスの読み取り

キーボードのステータスは、レジスタ0x00に格納されています。このレジスタを読み取ることで、修飾キーの状態やアクティブなレイヤーを取得できます。

読み取り手順:

  1. 読み取り要求の送信: マスターはスレーブのレジスタ0x00から1バイトを要求します。
  2. データの解釈: 受信したバイトを解析し、各修飾キーの状態とレイヤー番号を取得します。

:

#include <Wire.h>

#define SLAVE_ADDRESS 0x32

void setup() {
    Wire.begin(); // I2Cマスターとしてバスに参加
    Serial.begin(9600);
}

void loop() {
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x00); // レジスタ0x00: キーボードステータス
    Wire.endTransmission();

    Wire.requestFrom(SLAVE_ADDRESS, 1); // 1バイト要求
    if (Wire.available()) {
        uint8_t data = Wire.read();
        bool shift = data & 0x80;
        bool ctrl  = data & 0x40;
        bool cmd   = data & 0x20;
        bool opt   = data & 0x10;
        uint8_t layer = data & 0x03;

        Serial.print("Shift: "); Serial.println(shift);
        Serial.print("Ctrl: ");  Serial.println(ctrl);
        Serial.print("Cmd: ");   Serial.println(cmd);
        Serial.print("Opt: ");   Serial.println(opt);
        Serial.print("Layer: "); Serial.println(layer);
    }

    delay(1000); // 1秒待機
}

レジスタへの書き込み

レジスタへの書き込みは、コマンドバイトを送信し、その後に必要なデータバイトを送信することで行います。コマンドの種類に応じて、絶対値または相対値の書き込みを行います。

絶対値書き込み

目的: レジスタに対して特定の値を直接設定します。

コマンド範囲: 0x00 - 0x1F

手順:

  1. コマンドバイトの送信: 書き込み先のレジスタを示すコマンドバイトを送信します。
  2. データバイトの送信: 即値のデータバイトを送信します。

: レジスタ0x02(X座標)に値10を設定する。

uint8_t command = 0x02; // 0x00-0x1F: 絶対値書き込みコマンド
uint8_t value = 10;

Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(command);
Wire.write(value);
Wire.endTransmission();

相対値書き込み

相対値書き込みは、現在のレジスタの値に対して指定した増減値を加算します。これにより、現在の値を事前に知る必要なく、値を変更できます。

相対値書き込みカテゴリ

  1. 0x20 - 0x3F: uint8_t メモリ用相対値書き込み
  2. 0x40 - 0x5F: int8_t メモリ用相対値書き込み
  3. 0x60 - 0x7F: uint16_t メモリ用相対値書き込み
  4. 0x80 - 0x9F: int16_t メモリ用相対値書き込み

手順:

  1. コマンドバイトの送信: 操作対象のレジスタを示すコマンドバイトを送信します。
  2. データバイトの送信: 増減値のデータバイトを送信します。

: レジスタ0x03(Y座標)を-5だけ減少させる。

int8_t delta = -5;
uint8_t command = 0x40 | (0x03 & 0x1F); // 0x40-0x5F: int8_t メモリ用相対値書き込み

Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(command);
Wire.write((uint8_t)delta); // 符号付き値をuint8_tにキャスト
Wire.endTransmission();

注記: 相対値書き込みでは、適切なキャストを行い、データが正しく送信されるようにしてください。


レジスタからの読み取り

現時点では、レジスタ1-5は書き込み専用であり、読み取りはサポートされていません。主にレジスタ0x00(キーボードステータス)からの読み取りが行われます。

レジスタ0x00の読み取り

レジスタ0x00からキーボードのステータスを読み取る方法は以下の通りです。

手順:

  1. 読み取り要求の送信: マスターはスレーブのレジスタ0x00から1バイトを要求します。
  2. データの受信と解釈: 受信したバイトを解析し、修飾キーの状態とレイヤー番号を取得します。

:

#include <Wire.h>

#define SLAVE_ADDRESS 0x32

void setup() {
    Wire.begin(); // I2Cマスターとしてバスに参加
    Serial.begin(9600);
}

void loop() {
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x00); // レジスタ0x00: キーボードステータス
    Wire.endTransmission();

    Wire.requestFrom(SLAVE_ADDRESS, 1); // 1バイト要求
    if (Wire.available()) {
        uint8_t data = Wire.read();
        bool shift = data & 0x80;
        bool ctrl  = data & 0x40;
        bool cmd   = data & 0x20;
        bool opt   = data & 0x10;
        uint8_t layer = data & 0x03;

        Serial.print("Shift: "); Serial.println(shift);
        Serial.print("Ctrl: ");  Serial.println(ctrl);
        Serial.print("Cmd: ");   Serial.println(cmd);
        Serial.print("Opt: ");   Serial.println(opt);
        Serial.print("Layer: "); Serial.println(layer);
    }

    delay(1000); // 1秒待機
}

16ビットレジスタアクセス

現在の実装では、16ビットレジスタアクセスは使用していませんが、将来の拡張のためにサポートされています。

説明

  • レジスタ0x00 - 0x1F: 各レジスタは1バイト(uint8_t)ですが、2つの連続したレジスタを組み合わせて16ビット値(uint16_t または int16_t)として扱うことができます。リトルエンディアン形式です。

使用ガイドライン

  • アドレスペアリング: 16ビット値を扱うには、2つの連続した8ビットレジスタを使用します。例えば、レジスタ0x060x07を組み合わせて16ビット値とします。
  • エンディアンネス: 下位バイトは低アドレスに配置されます。

: レジスタ0x060x07を組み合わせて16ビット符号付き整数として扱う。

uint8_t low_byte = i2c_slave_read_uint8(0x06);
uint8_t high_byte = i2c_slave_read_uint8(0x07);
int16_t combined = (int16_t)(high_byte << 8) | low_byte;

注記: 現在のファームウェアでは16ビットアクセスは実装されていませんが、将来のアップデートで使用される可能性があります。


マスターコードの例

以下に、I2Cマスター側のコード例を示します。これらの例は、ArduinoおよびPython(smbus)を使用して、キーボードデバイスと通信する方法を示しています。

Arduinoの例

#include <Wire.h>

#define SLAVE_ADDRESS 0x32

void setup() {
    Wire.begin(); // I2Cマスターとしてバスに参加
    Serial.begin(9600);
}

void loop() {
    // キーボードステータスの読み取り
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x00); // レジスタ0x00: キーボードステータス
    Wire.endTransmission();

    Wire.requestFrom(SLAVE_ADDRESS, 1); // 1バイト要求
    if (Wire.available()) {
        uint8_t status = Wire.read();
        bool shift = status & 0x80;
        bool ctrl  = status & 0x40;
        bool cmd   = status & 0x20;
        bool opt   = status & 0x10;
        uint8_t layer = status & 0x03;

        Serial.print("Shift: "); Serial.println(shift);
        Serial.print("Ctrl: ");  Serial.println(ctrl);
        Serial.print("Cmd: ");   Serial.println(cmd);
        Serial.print("Opt: ");   Serial.println(opt);
        Serial.print("Layer: "); Serial.println(layer);
    }

    // マウスとスクロールデータの書き込み
    uint8_t buttons = 0b00000101; // 例: 左ボタンと中ボタンが押されている
    int8_t x = 10; // X方向に10移動
    int8_t y = -5; // Y方向に-5移動
    int8_t h = 2;  // 水平スクロールに2移動
    int8_t v = -3; // 垂直スクロールに-3移動

    // ボタン状態の書き込み
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x01); // レジスタ0x01: ボタン状態
    Wire.write(buttons);
    Wire.endTransmission();

    // X座標の書き込み
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x02); // レジスタ0x02: X座標
    Wire.write((uint8_t)x);
    Wire.endTransmission();

    // Y座標の書き込み
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x03); // レジスタ0x03: Y座標
    Wire.write((uint8_t)y);
    Wire.endTransmission();

    // 水平スクロールの書き込み
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x04); // レジスタ0x04: 水平スクロール
    Wire.write((uint8_t)h);
    Wire.endTransmission();

    // 垂直スクロールの書き込み
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(0x05); // レジスタ0x05: 垂直スクロール
    Wire.write((uint8_t)v);
    Wire.endTransmission();

    delay(1000); // 1秒待機
}

Python (smbus) の例

import smbus
import time

# I2Cの初期化
bus = smbus.SMBus(1)  # Raspberry Piでは通常1を使用

SLAVE_ADDRESS = 0x32

def read_keyboard_status():
    # レジスタ0x00の読み取り
    bus.write_byte(SLAVE_ADDRESS, 0x00)
    status = bus.read_byte(SLAVE_ADDRESS)
    
    shift = bool(status & 0x80)
    ctrl  = bool(status & 0x40)
    cmd   = bool(status & 0x20)
    opt   = bool(status & 0x10)
    layer = status & 0x03
    
    return {
        'shift': shift,
        'ctrl': ctrl,
        'cmd': cmd,
        'opt': opt,
        'layer': layer
    }

def write_mouse_scroll(buttons, x, y, h, v):
    # ボタン状態の書き込み
    bus.write_byte_data(SLAVE_ADDRESS, 0x01, buttons)
    
    # X座標の書き込み
    bus.write_byte_data(SLAVE_ADDRESS, 0x02, x & 0xFF)
    
    # Y座標の書き込み
    bus.write_byte_data(SLAVE_ADDRESS, 0x03, y & 0xFF)
    
    # 水平スクロールの書き込み
    bus.write_byte_data(SLAVE_ADDRESS, 0x04, h & 0xFF)
    
    # 垂直スクロールの書き込み
    bus.write_byte_data(SLAVE_ADDRESS, 0x05, v & 0xFF)

def main():
    while True:
        # キーボードステータスの読み取り
        status = read_keyboard_status()
        print(f"Shift: {status['shift']}, Ctrl: {status['ctrl']}, Cmd: {status['cmd']}, Opt: {status['opt']}, Layer: {status['layer']}")

        # マウスとスクロールデータの書き込み
        buttons = 0b00000101  # 例: 左ボタンと中ボタンが押されている
        x = 10                # X方向に10移動
        y = -5                # Y方向に-5移動
        h = 2                 # 水平スクロールに2移動
        v = -3                # 垂直スクロールに-3移動

        write_mouse_scroll(buttons, x, y, h, v)
        print("マウスとスクロールデータを送信しました。")

        time.sleep(1)  # 1秒待機

if __name__ == "__main__":
    main()

注意事項とベストプラクティス

  1. I2Cプルアップ抵抗: I2Cバスには適切なプルアップ抵抗(通常4.7kΩ)がSDAおよびSCLラインに接続されていることを確認してください。

  2. アドレス競合の回避: 同一I2Cバス上に同じスレーブアドレスを持つデバイスが存在しないことを確認してください。必要に応じて、キーボードのスレーブアドレスを変更してください。

  3. バス速度の設定: 標準のI2C速度(100kHz)を使用してください。マスターとスレーブの両方が選択した速度をサポートしていることを確認してください。

  4. エラーハンドリング: バスエラー、NACK応答、タイムアウトなどのシナリオに対するエラーハンドリングを実装してください。

  5. 同期の確保: マスターとスレーブがコマンド構造を正しく理解し、データの誤解釈を防ぐために同期を取ってください。

  6. データの整合性: 重要なアプリケーションでは、データの整合性を確認するためにチェックサムやCRCメカニズムを実装してください。

  7. 電力管理: マスター側のデバイスがバッテリー駆動の場合、消費電力に注意してください。

  8. 予約レジスタの扱い: 予約レジスタ(0x06 - 0x1F)にはアクセスしないでください。将来の拡張のために予約されています。


付録: キーボードステータスバイト

キーボードのステータスは、レジスタ0x00に格納された1バイト(uint8_t)で表現されます。このバイトは、修飾キーの状態とアクティブなレイヤーをリアルタイムで提供します。

ビット 機能 説明
7 Shift 1 = Shiftキーが押されています
6 Ctrl 1 = Ctrlキーが押されています
5 Cmd/GUI 1 = Cmd/GUIキーが押されています
4 Opt/Alt 1 = Option/Altキーが押されています
3 予約 将来の使用のために予約されています
2 予約 将来の使用のために予約されています
1 レイヤービット1 レイヤー番号の一部(0〜3)
0 レイヤービット0 レイヤー番号の一部(0〜3)

レイヤー番号: ビット1とビット0を組み合わせて、アクティブなレイヤーを表します(00 = レイヤー0、01 = レイヤー1、10 = レイヤー2、11 = レイヤー3)。

例の解釈:

Wire.requestFrom(SLAVE_ADDRESS, 1); // 1バイトを要求
if (Wire.available()) {
    uint8_t data = Wire.read();
    bool shift = data & 0x80;   // Shiftキーが押されているか
    bool ctrl  = data & 0x40;   // Ctrlキーが押されているか
    bool cmd   = data & 0x20;   // Cmd/GUIキーが押されているか
    bool opt   = data & 0x10;   // Opt/Altキーが押されているか
    uint8_t layer = data & 0x03; // レイヤー番号(0~3)
}

このガイドに従うことで、開発者はI2Cマスター側のファームウェアを効果的に実装し、カスタムキーボードデバイスとの通信を円滑に行うことができます。コマンド構造やレジスタマップを正しく理解し、適切な読み書き操作を実装してください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment