STM32G4ベースの0.96インチOLEDディスプレイドライバ(HALライブラリ)、ハード/ソフトI2C対応

STM32G474ベースの0.96インチOLED(SSD1306)ディスプレイドライバ(4ピンI2Cインターフェース)、ハードウェアIIC/ソフトウェアIIC対応、HALライブラリ版。

このドライバは比較的充実しており、英語、整数、浮動小数点数、漢字、画像、2進数、16進数などの表示に加え、点・直線・矩形・円・楕円・三角形の描画が可能。複数のフォントに対応し、簡易版グラフィックスライブラリと言える。

本プログラムは江協科技のコードを二次改修したもの。元版はSTM32F103ベースでソフトI2Cのみ対応だったが、ハードI2Cにも対応し、マクロ定義を変更すればソフトI2Cにも切り替え可能。

テストハードウェア:NUCLEO-G474RE開発ボード

OLEDの駆動原理とドライバの使い方は江協科技の動画を参照:https://url.zeruns.com/L7j6y

電子/マイコン技術交流グループ:2169025065

効果図

I2Cプロトコル概要

I2C通信プロトコル(Inter-Integrated Circuit)はPhilips社が開発。ピン数が少なく、ハードウェア実装が簡単で拡張性が高く、USARTやCANのような外部送受信デバイス(レベル変換IC)が不要なため、現在ではシステム内複数IC間通信に広く使われている。

I2Cはデータ総線SDA(Serial Data Line)が1本のみで、シリアルデータ総線なので1ビットずつデータを送信し、半二重通信を採用。

半二重通信:双方向通信が可能だが、同時に双方向を行えず、交互に切り替える必要がある。本質的には方向切替可能な単一通信と考えられ、同一時刻には一方向の転送のみであり、データ線は1本で済む。

I2C通信プロトコルは物理層とプロトコル層に分けられる。物理層は通信システムの機械・電子的機能部分(ハードウェア)を規定し、物理メディアでの生データ転送を保証。プロトコル層は通信ロジックを規定し、送受信側のデータパック/アンパック標準(ソフトウェアレベル)を統一する。

I2C物理層

I2C通信デバイス間の一般的な接続方式

(1) 複数デバイスをサポートするバス。「バス」とは複数デバイスが共用する信号線のこと。1つのI2C通信バスには複数のI2C通信デバイスを接続でき、複数の通信マスター及びスレーブをサポート。

(2) I2Cバスは2本のバスラインのみを使用:双方向シリアルデータ線SDA(Serial Data Line)とシリアルクロック線SCL(Serial Clock Line)。データ線はデータを表し、クロック線はデータ送受信の同期に使う。

(3) バスはプルアップ抵抗で電源に接続。I2Cデバイスがアイドル時はハイインピーダンス出力となり、すべてのデバイスがアイドルでハイインピーダンス出力のとき、プルアップ抵抗がバスをハイレベルに引き上げる。

I2C通信時、マイコンのGPIOピンは開漏れ出力に設定する必要があり、そうでないとショートする恐れがある。

STM32のI2C詳細情報と使用方法はこちら:https://url.zeruns.com/JC0Ah

江協科技のSTM32入門チュートリアル:https://www.bilibili.com/video/BV1th411z7sn?p=31

ここでは詳細は割愛。

使用手順

デフォルトはハードIICで、I2C3を使用。SCLはPA8、SDAはPC9。

ハードI2C

STM32CubeMX設定:使用したいI2Cペリフェラルのピンを見つけ、ピン機能をSCLとSDAに設定。下図はI2C3のSCL。

次にI2Cペリフェラルを設定:対応するI2Cペリフェラルを有効化、速度モードを Fast Mode Plus にし、速度を1000に変更、他はデフォルト。

GPIO設定:上記設定後、自動的に2ピンが複用開漏れ出力モードになる。IO出力速度を Very High に変更、GPIOラベル(User Label)をそれぞれ I2C3_SCL と I2C3_SDA に定義。別のI2Cを使う場合は別値でも可だが、コード側も修正。設定後コード生成をクリック。

OLED.cファイルで、#define OLED_USE_SW_I2C をコメントアウト、#define OLED_USE_HW_I2C のコメントを外す。別のピンをI2Cピンとして使い、別名を定義している場合はコード内の I2C3_SCL と I2C3_SDA も変更。

ソフトI2C

STM32CubeMX設定:I2CのSCL・SDA信号線として2ピンを設定。IOの User Label をそれぞれ I2C3_SCL と I2C3_SDA に変更(別名にした場合はコードも修正)。IOモードを開漏れ出力、デフォルト出力レベルをハイ、プルアップ出力、速度を最高に設定。下図参照。設定後コード生成。

OLED.cファイルで、#define OLED_USE_HW_I2C をコメントアウト、#define OLED_USE_SW_I2C のコメントを外す。別のピンをI2Cピンとして使い、別名を定義している場合はコード内の I2C3_SCL と I2C3_SDA も変更。

必要な部品

プログラム

完全なプロジェクトのダウンロード先:

百度网盘:リンク: https://url.zeruns.com/0CQJG 抽出コード: 0169

123网盘(速度制限なし):https://www.123pan.com/s/2Y9Djv-O0cvH.html 抽出コード:vvDt

Giteeオープンソースアドレス:https://gitee.com/zeruns/STM32-HAL-OLED-I2C

GitHubオープンソースアドレス:https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL

Starをクリックしてください

プロジェクトはKeil5で作成され、Vscode+EIDEで開発されています。どちらのソフトウェアでもこのプロジェクトを開けます。

プロジェクトファイルはすべてUTF-8エンコーディングです。開いた際に文字化けが発生する場合は、エディタのエンコーディングをUTF-8に変更してください。

主要ファイル OLED.c:


/***************************************************************************************
 * 本プログラムは江協科技によって作成され、無料でオープンソースとして共有されています
 * 自由に閲覧、使用、修正が可能で、自身のプロジェクトに適用できます
 * プログラムの著作権は江協科技に帰属し、いかなる個人・組織も無断で占有することはできません
 *
 * プログラム名:      0.96インチOLEDディスプレイドライバ(4ピンI2Cインターフェース)
 * プログラム作成日:    2023.10.24
 * 現在のプログラムバージョン: V1.1
 * 現在のバージョン公開日:  2023.12.8
 *
 * 江協科技公式ウェブサイト:  jiangxiekeji.com
 * 江協科技公式淘宝ショップ: jiangxiekeji.taobao.com
 * プログラムの紹介と更新情報: jiangxiekeji.com/tutorial/oled.html
 *
 * プログラムにバグや誤記を発見した場合、メールでフィードバックしてください:[email protected]
 * メールを送信する前に、更新情報ページで最新のプログラムをご確認ください。すでに修正されている場合は、メールを送信する必要はありません
 ***************************************************************************************
 */

/*
 * 本プログラムはzerunsによって改変されています
 * 改変内容: 標準ライブラリ版をHALライブラリ版に変更し、ハードウェアI2Cのサポートを追加。マクロ定義を変更することでハードウェアI2Cの有効/無効を選択可能
 * 改変日:  2024.3.16
 * ブログ:  https://blog.zeruns.com
 * B站ホームページ:https://space.bilibili.com/8320520
*/

#include "main.h"
#include "OLED.h"
#include <str
```ing.h>
#include <math.h>
#include <stdio.h>
#include <stdarg.h>

// 中国語を使用する場合、コンパイラに --no-multibyte-chars オプションを追加する必要があります(AC6コンパイラを使用する場合は追加不要)

/*
OLED駆動方式を選択。デフォルトはハードウェアI2C。ソフトウェアI2Cを使用する場合は、ハードウェアI2Cの行のマクロ定義をコメントアウトし、ソフトウェアI2Cの行のコメントを解除してください。
両方を同時にコメント解除することはできません!
stm32cubemxで初期化する際、SCLとSDAピンの「user lable」をそれぞれI2C3_SCLおよびI2C3_SDAに設定する必要があります。
*/
#define OLED_USE_HW_I2C	// ハードウェアI2C
//#define OLED_USE_SW_I2C	// ソフトウェアI2C

/*ピン定義。ここでI2C通信ピンを変更可能*/
#define OLED_SCL            I2C3_SCL_Pin // SCL
#define OLED_SDA            I2C3_SDA_Pin // SDA
#define OLED_SCL_GPIO_Port  I2C3_SCL_GPIO_Port
#define OLED_SDA_GPIO_Port  I2C3_SDA_GPIO_Port

/*STM32G474チップのハードウェアI2C3: PA8 -- SCL; PC9 -- SDA */

#ifdef OLED_USE_HW_I2C
/*I2Cインターフェース。OLEDスクリーンがどのI2Cインターフェースを使用するか定義*/
#define OLED_I2C            hi2c3
extern  I2C_HandleTypeDef   hi2c3;	//HALライブラリ使用。ハードウェアIICインターフェースを指定
#endif

/*OLEDスレーブアドレス*/
#define OLED_ADDRESS 0x3C << 1	// 0x3CはOLEDの7ビットアドレス。左に1ビットシフトして最下位ビットを読み書きビットにし、0x78にする

/*I2Cタイムアウト時間*/
#define OLED_I2C_TIMEOUT 10
/*ソフトウェアI2C用の遅延時間。以下の値は170MHz動作時の遅延値。動作周波数が異なる場合は調整してください。100MHz以内の場合は0にしてください*/
#define Delay_time 3

/**
 * データ格納形式:
 * 縦8ドット、上位ビットが下。左から右、上から下の順
 * 各ビットが1ピクセルに対応
 *
 *      B0 B0                  B0 B0
 *      B1 B1                  B1 B1
 *      B2 B2                  B2 B2
 *      B3 B3  ------------->  B3 B3 --
 *      B4 B4                  B4 B4  |
 *      B5 B5                  B5 B5  |
 *      B6 B6                  B6 B6  |
 *      B7 B7                  B7 B7  |
 *                                    |
 *  -----------------------------------
 *  |
 *  |   B0 B0                  B0 B0
 *  |   B1 B1                  B1 B1
 *  |   B2 B2                  B2 B2
 *  --> B3 B3  ------------->  B3 B3
 *      B4 B4                  B4 B4
 *      B5 B5                  B5 B5
 *      B6 B6                  B6 B6
 *      B7 B7                  B7 B7
 *
 * 座標軸定義:
 * 左上が(0, 0)点
 * 横方向右がX軸、範囲:0~127
 * 縦方向下がY軸、範囲:0~63
 *
 *       0             X軸           127
 *      .------------------------------->
 *    0 |
 *      |
 *      |
 *      |
 *  Y軸 |
 *      |
 *      |
 *      |
 *   63 |
 *      v
 *
 */

/*グローバル変数*********************/
/**
 * OLED表示バッファ配列
 * すべての表示関数はこの表示バッファ配列に対して読み書きのみ行う
 * その後OLED_Update関数またはOLED_UpdateArea関数を呼び出すことで
 * バッファ配列のデータをOLEDハードウェアに送信して表示する
 */
uint8_t OLED_DisplayBuf[8][128];
/*********************グローバル変数*/

#ifdef OLED_USE_SW_I2C
/**
 * @brief OLED_SCLに高低電圧を書き込む
 * BitValueの値に基づき、OLED_SCLをHIGHまたはLOWに設定
 * @param BitValue ビット値、0または1
 */
void OLED_W_SCL(uint8_t BitValue)
{
	/*BitValueの値に基づき、SCLをHIGHまたはLOWに設定*/
	HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
	/*マイコン速度が速すぎる場合、適切な遅延を追加してI2C通信の最大速度を超えないようにする*/
    for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}

/**
  * @brief OLEDにSDA高低電圧を書き込む
  * @param SDAに書き込む電圧値、範囲:0/1
  * @return なし
  * @note 上位関数がSDAを書き込む必要があるとき、この関数が呼ばれる
  *           パラメータの値に基づき、SDAをHIGHまたはLOWに設定
  *           パラメータが0のときSDAをLOW、1のときSDAをHIGHに設定
  */
void OLED_W_SDA(uint8_t BitValue)
{
	/*BitValueの値に基づき、SDAをHIGHまたはLOWに設定*/
	HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
	/*マイコン速度が速すぎる場合、適切な遅延を追加してI2C通信の最大速度を超えないようにする*/
	for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}
#endif

/**
 * @brief OLEDピン初期化
 * @param  なし
 * @retval なし
 * @note 使用前にこの初期化関数を呼び出す必要がある
 */
void OLED_GPIO_Init(void)
{
    uint32_t i, j;

    /*初期化前に適切な遅延を入れ、OLEDの電源が安定するのを待つ*/
    for (i = 0; i < 1000; i++) {
        for (j = 0; j < 1000; j++)
            ;
    }
#ifdef OLED_USE_SW_I2C
    __HAL_RCC_GPIOC_CLK_ENABLE();		// GPIOCクロックを有効化
    __HAL_RCC_GPIOA_CLK_ENABLE();       // GPIOAクロックを有効化
	GPIO_InitTypeDef GPIO_InitStruct = {0};              // GPIO設定用構造体を定義
 	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;	        // GPIOモードをオープンドレイン出力に設定
    GPIO_InitStruct.Pull = GPIO_PULLUP;                 // 内部プルアップ抵抗
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  // GPIO速度を非常に高速に設定
	GPIO_InitStruct.Pin = I2C3_SDA_Pin;                 // ピンを設定
 	HAL_GPIO_Init(I2C3_SDA_GPIO_Port, &GPIO_InitStruct);// GPIOを初期化

    GPIO_InitStruct.Pin = I2C3_SCL_Pin;
    HAL_GPIO_Init(I2C3_SCL_GPIO_Port, &GPIO_InitStruct);

	/*SCLとSDAを解放*/
	OLED_W_SCL(1);
	OLED_W_SDA(1);
#endif
}

// https://blog.zeruns.com

/*通信プロトコル*********************/

/**
 * @brief I2Cスタート
 * @param  なし
 * @return なし
 */
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
	OLED_W_SDA(1);		// SDAを解放し、HIGHを確保
	OLED_W_SCL(1);		// SCLを解放し、HIGHを確保
	OLED_W_SDA(0);		// SCLがHIGHの間にSDAをLOWにしてスタート信号を生成
	OLED_W_SCL(0);		// スタート後にSCLもLOWにしてバスを占有し、タイミング連結を容易にする
#endif
}

/**
 * @brief I2Cストップ
 * @param  なし
 * @return なし
 */
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
	OLED_W_SDA(0);		// SDAをLOWにして確実にLOWを確保
	OLED_W_SCL(1);		// SCLを解放してHIGHにする
	OLED_W_SDA(1);		// SCLがHIGHの間にSDAを解放してストップ信号を生成
#endif
}

/**
 * @brief I2Cで1バイト送信
 * @param Byte 送信する1バイトデータ、範囲:0x00~0xFF
 * @return なし
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
	uint8_t i;
	/*8回ループし、ホストがデータの各ビットを順に送信*/
	for (i = 0; i < 8; i++)
	{
		/*マスクを使ってByteの指定ビットを取り出しSDAラインに書き込む*/
		/*二重の!は、0以外の値を1にするため*/
		OLED_W_SDA(!!(Byte & (0x80 >> i)));
		OLED_W_SCL(1);	// SCLを解放し、スレーブがSCLハイ期間にSDAを読み取る
		OLED_W_SCL(0);	// SCLをLOWにし、ホストが次のビットを送信開始
	}
	OLED_W_SCL(1);		// 追加のクロック、ACK信号は処理しない
	OLED_W_SCL(0);
#endif
}

/**
 * @brief OLEDにコマンドを書き込む
 * @param Command 書き込むコマンド値、範囲:0x00~0xFF
 * @return なし
 */
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
    OLED_I2C_Start();           // I2Cスタート
	OLED_I2C_SendByte(0x78);		// OLEDのI2Cスレーブアドレスを送信
	OLED_I2C_SendByte(0x00);	// コントロールバイト、0x00を指定するとコマンド書き込みを示す
    OLED_I2C_SendByte(Command); // 指定コマンドを書き込む
    OLED_I2C_Stop();            // I2Cストップ
#elif defined(OLED_USE_HW_I2C)
    uint8_t TxData[2] = {0x00, Command};
    HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, 2, OLED_I2C_TIMEOUT);
#endif 
}

/**
 * @brief OLEDにデータを書き込む
 * @param Data 書き込むデータの先頭アドレス
 * @param Count 書き込むデータ数
 * @return なし
 */
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
    uint8_t i;
#ifdef OLED_USE_SW_I2C
    OLED_I2C_Start();        // I2Cスタート
	OLED_I2C_SendByte(0x78);		// OLEDのI2Cスレーブアドレスを送信

    OLED_I2C_SendByte(0x40); // コントロールバイト、0x40を指定するとデータ書き込みを示す
    /*Count回ループし連続データ書き込みを実行*/
    for (i = 0; i < Count; i++) {
        OLED_I2C_SendByte(Data[i]); // Dataの各データを順に送信
    }
    OLED_I2C_Stop(); // I2Cストップ
#elif defined(OLED_USE_HW_I2C)
    uint8_t TxData[Count + 1]; // 新しい配列を確保、サイズはCount + 1
    TxData[0] = 0x40; // 開始バイト
    // Dataが指すデータをTxData配列の残り部分にコピー
    for (i = 0; i < Count; i++) {
        TxData[i + 1] = Data[i];
    }
    HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, Count + 1, OLED_I2C_TIMEOUT);
#endif    
}

/*********************通信プロトコル*/

/*ハードウェア設定*********************/

/**
 * @brief OLED初期化
 * @param なし
 * @return なし
 * @note 使用前にこの初期化関数を呼び出す必要がある
 */
void OLED_Init(void)
{
    OLED_GPIO_Init(); // まず下位層のポート初期化を呼ぶ

    /*一連のコマンドを書き込み、OLEDを初期化設定*/
    OLED_WriteCommand(0xAE); // 表示ON/OFF設定、0xAEでOFF、0xAFでON

    OLED_WriteCommand(0xD5); // 表示クロック分周比/発振周波数設定
    OLED_WriteCommand(0x80); // 0x00~0xFF

    OLED_WriteCommand(0xA8); // マルチプレクス比設定
    OLED_WriteCommand(0x3F); // 0x0E~0x3F

    OLED_WriteCommand(0xD3); // 表示オフセット設定
    OLED_WriteCommand(0x00); // 0x00~0x7F

    OLED_WriteCommand(0x40); // 表示開始行設定、0x40~0x7F

    OLED_WriteCommand(0xA1); // 左右方向設定、0xA1で通常、0xA0で左右反転

    OLED_WriteCommand(0xC8); // 上下方向設定、0xC8で通常、0xC0で上下反転

    OLED_WriteCommand(0xDA); // COMピンハードウェア設定
    OLED_WriteCommand(0x12);

    OLED_WriteCommand(0x81); // コントラスト設定
    OLED_WriteCommand(0xCF); // 0x00~0xFF

    OLED_WriteCommand(0xD9); // プリチャージ周期設定
    OLED_WriteCommand(0xF1);

    OLED_WriteCommand(0xDB); // VCOMHデシレクトレベル設定
    OLED_WriteCommand(0x30);

    OLED_WriteCommand(0xA4); // 全体表示ON/OFF設定

    OLED_WriteCommand(0xA6); // 通常/反転表示設定、0xA6で通常、0xA7で反転

    OLED_WriteCommand(0x8D); // チャージポンプ設定
    OLED_WriteCommand(0x14);

    OLED_WriteCommand(0xAF); // 表示ON

    OLED_Clear();  // 表示バッファ配列をクリア
    OLED_Update(); // 表示を更新し、画面をクリア。初期化後に内容がないときの画面乱れを防ぐ
}

/**
 * @brief OLED表示カーソル位置設定
 * @param Page カーソルがあるページを指定、範囲:0-7
 * @param X カーソルがあるX座標を指定、範囲:0-127
 * @return なし
 * @note OLEDのデフォルトY軸は8ビット単位でしか書き込めない、つまり1ページ=8Y座標
 */
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
    /*1.3インチOLEDディスプレイをこのプログラムで駆動する場合は以下のコメントを解除*/
    /*1.3インチOLEDのドライバIC(SH1106)は132列あるため*/
    /*画面の開始列が第2列に接続されており、第0列ではない*/
    /*そのためXに2を加えることで正常に表示される*/
    //	X += 2;

    /*コマンドでページアドレスと列アドレスを設定*/
    OLED_WriteCommand(0xB0 | Page);              // ページ位置を設定
    OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // X位置上位4ビットを設定
    OLED_WriteCommand(0x00 | (X & 0x0F));        // X位置下位4ビットを設定
}/*********************ハードウェア設定*/

/*ユーティリティ関数*********************/

/*ユーティリティ関数は内部の一部の関数でのみ使用されます*/

/**
 * @brief 累乗関数
 * @param X 基数
 * @param Y 指数
 * @return XのY乗
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1; // 結果の初期値は1
    while (Y--)          // Y回乗算
    {
        Result *= X; // 毎回Xを結果に乗算
    }
    return Result;
}

/**
 * @brief 指定された点が指定された多角形の内部にあるかどうかを判定
 * @param nvert 多角形の頂点数
 * @param vertx verty 多角形の頂点のxおよびy座標を含む配列
 * @param testx testy テスト点のXおよびy座標
 * @return 指定された点が指定された多角形の内部にあるかどうか、1:内部、0:外部
 */
uint8_t OLED_pnpoly(uint8_t nvert, int16_t *vertx, int16_t *verty, int16_t testx, int16_t testy)
{
    int16_t i, j, c = 0;

    /*このアルゴリズムはW. Randolph Franklinによって提案されました*/
    /*参照リンク:https://wrfranklin.org/Research/Short_Notes/pnpoly.html*/
    for (i = 0, j = nvert - 1; i < nvert; j = i++) {
        if (((verty[i] > testy) != (verty[j] > testy)) &&
            (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
            c = !c;
        }
    }
    return c;
}

/**
 * @brief 指定された点が指定された角度内にあるかどうかを判定
 * @param X Y 指定された点の座標
 * @param StartAngle EndAngle 開始角度と終了角度、範囲:-180-180
 *           水平右方向が0度、水平左方向が180度または-180度、下方向が正、上方向が負、時計回り
 * @return 指定された点が指定された角度内にあるかどうか、1:内部、0:外部
 */
uint8_t OLED_IsInAngle(int16_t X, int16_t Y, int16_t StartAngle, int16_t EndAngle)
{
    int16_t PointAngle;
    PointAngle = atan2(Y, X) / 3.14 * 180; // 指定された点のラジアンを計算し、角度に変換
    if (StartAngle < EndAngle)             // 開始角度が終了角度より小さい場合
    {
        /*指定された角度が開始角度と終了角度の間にある場合、指定された点が指定された角度内と判定*/
        if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
            return 1;
        }
    } else // 開始角度が終了角度より大きい場合
    {
        /*指定された角度が開始角度より大きいか終了角度より小さい場合、指定された点が指定された角度内と判定*/
        if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
            return 1;
        }
    }
    return 0; // 上記の条件を満たさない場合、指定された点が指定された角度内にないと判定
}

/*********************ユーティリティ関数*/

/*機能関数*********************/

/**
 * @brief OLEDフレームバッファ配列をOLEDスクリーンに更新
 * @param なし
 * @return なし
 * @note すべての表示関数は、OLEDフレームバッファ配列の読み書きのみを行います
 *           その後、OLED_Update関数またはOLED_UpdateArea関数を呼び出します
 *           フレームバッファ配列のデータがOLEDハードウェアに送信され、表示されます
 *           したがって、表示関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_Update(void)
{
    uint8_t j;
    /*各ページを順番に処理*/
    for (j = 0; j < 8; j++) {
        /*カーソル位置を各ページの最初の列に設定*/
        OLED_SetCursor(j, 0);
        /*128個のデータを連続で書き込み、フレームバッファ配列のデータをOLEDハードウェアに書き込む*/
        OLED_WriteData(OLED_DisplayBuf[j], 128);
    }
}

/**
 * @brief OLEDフレームバッファ配列の一部をOLEDスクリーンに更新
 * @param X 指定領域の左上隅のX座標、範囲:0-127
 * @param Y 指定領域の左上隅のY座標、範囲:0-63
 * @param Width 指定領域の幅、範囲:0-128
 * @param Height 指定領域の高さ、範囲:0-64
 * @return なし
 * @note この関数は、パラメータで指定された領域を少なくとも更新します
 *           更新領域のY軸がページの一部のみを含む場合、同じページの残りの部分も一緒に更新されます
 * @note すべての表示関数は、OLEDフレームバッファ配列の読み書きのみを行います
 *           その後、OLED_Update関数またはOLED_UpdateArea関数を呼び出します
 *           フレームバッファ配列のデータがOLEDハードウェアに送信され、表示されます
 *           したがって、表示関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t j;

    /*パラメータチェック、指定された領域がスクリーン範囲を超えないようにする*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    /*指定された領域に関連するページを順番に処理*/
    /*(Y + Height - 1) / 8 + 1の目的は(Y + Height) / 8を切り上げること*/
    for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
        /*カーソル位置を関連するページの指定された列に設定*/
        OLED_SetCursor(j, X);
        /*Width個のデータを連続で書き込み、フレームバッファ配列のデータをOLEDハードウェアに書き込む*/
        OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
    }
}

/**
 * @brief OLEDフレームバッファ配列をすべてクリア
 * @param なし
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_Clear(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // 8ページを順番に処理
    {
        for (i = 0; i < 128; i++) // 128列を順番に処理
        {
            OLED_DisplayBuf[j][i] = 0x00; // フレームバッファ配列のデータをすべてクリア
        }
    }
}

/**
 * @brief OLEDフレームバッファ配列の一部をクリア
 * @param X 指定領域の左上隅のX座標、範囲:0-127
 * @param Y 指定領域の左上隅のY座標、範囲:0-63
 * @param Width 指定領域の幅、範囲:0-128
 * @param Height 指定領域の高さ、範囲:0-64
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*パラメータチェック、指定された領域がスクリーン範囲を超えないようにする*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    for (j = Y; j < Y + Height; j++) // 指定されたページを順番に処理
    {
        for (i = X; i < X + Width; i++) // 指定された列を順番に処理
        {
            OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // フレームバッファ配列の指定されたデータをクリア
        }
    }
}

/**
 * @brief OLEDフレームバッファ配列をすべて反転
 * @param なし
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_Reverse(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // 8ページを順番に処理
    {
        for (i = 0; i < 128; i++) // 128列を順番に処理
        {
            OLED_DisplayBuf[j][i] ^= 0xFF; // フレームバッファ配列のデータをすべて反転
        }
    }
}

/**
 * @brief OLEDフレームバッファ配列の一部を反転
 * @param X 指定領域の左上隅のX座標、範囲:0-127
 * @param Y 指定領域の左上隅のY座標、範囲:0-63
 * @param Width 指定領域の幅、範囲:0-128
 * @param Height 指定領域の高さ、範囲:0-64
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*パラメータチェック、指定された領域がスクリーン範囲を超えないようにする*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    for (j = Y; j < Y + Height; j++) // 指定されたページを順番に処理
    {
        for (i = X; i < X + Width; i++) // 指定された列を順番に処理
        {
            OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // フレームバッファ配列の指定されたデータを反転
        }
    }
}

/**
 * @brief OLEDに1文字を表示
 * @param X 指定された文字の左上隅のX座標、範囲:0-127
 * @param Y 指定された文字の左上隅のY座標、範囲:0-63
 * @param Char 表示する文字、範囲:ASCIIコードの表示可能文字
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16\t\t幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8\t\t幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
    if (FontSize == OLED_8X16) // フォントが幅8ピクセル、高さ16ピクセルの場合
    {
        /*ASCIIフォントライブラリOLED_F8x16の指定されたデータを8*16の画像形式で表示*/
        OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
    } else if (FontSize == OLED_6X8) // フォントが幅6ピクセル、高さ8ピクセルの場合
    {
        /*ASCIIフォントライブラリOLED_F6x8の指定されたデータを6*8の画像形式で表示*/
        OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
    }
}

/**
 * @brief OLEDに文字列を表示
 * @param X 指定された文字列の左上隅のX座標、範囲:0-127
 * @param Y 指定された文字列の左上隅のY座標、範囲:0-63
 * @param String 表示する文字列、範囲:ASCIIコードの表示可能文字で構成される文字列
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16\t\t幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8\t\t幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i++) // 文字列の各文字を順番に処理
    {
        /*OLED_ShowChar関数を呼び出し、各文字を順番に表示*/
        OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
    }
}

/**
 * @brief OLEDに数字を表示(10進数、正の整数)
 * @param X 指定された数字の左上隅のX座標、範囲:0-127
 * @param Y 指定された数字の左上隅のY座標、範囲:0-63
 * @param Number 表示する数字、範囲:0-4294967295
 * @param Length 数字の長さ、範囲:0-10
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16\t\t幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8\t\t幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ShowNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; i < Length; i++) // 数字の各桁を順番に処理
    {
        /*OLED_ShowChar関数を呼び出し、各数字を順番に表示*/
        /*Number / OLED_Pow(10, Length - i - 1) % 10は10進数で数字の各桁を抽出できます*/
        /*+ '0'で数字を文字形式に変換*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief OLEDに符号付き数字を表示(10進数、整数)
 * @param X 指定された数字の左上隅のX座標、範囲:0-127
 * @param Y 指定された数字の左上隅のY座標、範囲:0-63
 * @param Number 表示する数字、範囲:-2147483648-2147483647
 * @param Length 数字の長さ、範囲:0-10
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16\t\t幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8\t\t幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには、更新関数を呼び出す必要があります
 */
void OLED_ShowSignedNum(uint8_t X, uint8_t Y, int32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    uint32_t Number1;

    if (Number >= 0) // 数字が0以上の場合
    {
        OLED_ShowChar(X, Y, '+', FontSize); // +記号を表示
        Number1 = Number;                   // Number1はNumberと等しい
    } else                                  // 数字が0未満の場合
    {
        OLED_ShowChar(X, Y, '-', FontSize); // -記号を表示
        Number1 = -Number;                  // Number1はNumberの負の値
    }

}for (i = 0; i < Length; i++) // 各桁を走査
    {
        /* OLED_ShowChar関数を呼び出して、各数字を順に表示 */
        /* Number1 / OLED_Pow(10, Length - i - 1) % 10 で十進数の各桁を抽出 */
        /* + '0' で数字を文字に変換 */
        OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief OLEDに16進数を表示(16進、正の整数)
 * @param X 数字左上のX座標、範囲:0~127
 * @param Y 数字左上のY座標、範囲:0~63
 * @param Number 表示する数字、範囲:0x00000000~0xFFFFFFFF
 * @param Length 数字の桁数、範囲:0~8
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16 幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8  幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_ShowHexNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i, SingleNumber;
    for (i = 0; i < Length; i++) // 各桁を走査
    {
        /* 16進数で各桁を抽出 */
        SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;

        if (SingleNumber < 10) // 1桁が10未満
        {
            /* OLED_ShowChar関数を呼び出して数字を表示 */
            /* + '0' で数字を文字に変換 */
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
        } else // 1桁が10以上
        {
            /* OLED_ShowChar関数を呼び出して数字を表示 */
            /* + 'A' で10以上をAから始まる16進文字に変換 */
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
        }
    }
}

/**
 * @brief OLEDに2進数を表示(2進、正の整数)
 * @param X 数字左上のX座標、範囲:0~127
 * @param Y 数字左上のY座標、範囲:0~63
 * @param Number 表示する数字、範囲:0x00000000~0xFFFFFFFF
 * @param Length 数字の桁数、範囲:0~16
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16 幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8  幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_ShowBinNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; i < Length; i++) // 各桁を走査
    {
        /* OLED_ShowChar関数を呼び出して各数字を順に表示 */
        /* Number / OLED_Pow(2, Length - i - 1) % 2 で2進数の各桁を抽出 */
        /* + '0' で数字を文字に変換 */
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
    }
}

/**
 * @brief OLEDに浮動小数点数を表示(10進、小数)
 * @param X 数字左上のX座標、範囲:0-127
 * @param Y 数字左上のY座標、範囲:0-63
 * @param Number 表示する数字、範囲:-4294967295.0~4294967295.0
 * @param IntLength 整数部の桁数、範囲:0-10
 * @param FraLength 小数部の桁数、範囲:0-9、小数は四捨五入して表示
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16 幅8ピクセル、高さ16ピクセル
 *                  OLED_6X8  幅6ピクセル、高さ8ピクセル
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_ShowFloatNum(uint8_t X, uint8_t Y, double Number, uint8_t IntLength, uint8_t FraLength, uint8_t FontSize)
{
    uint32_t PowNum, IntNum, FraNum;

    if (Number >= 0) // 数字が0以上
    {
        OLED_ShowChar(X, Y, '+', FontSize); // +記号を表示
    } else                                  // 数字が負
    {
        OLED_ShowChar(X, Y, '-', FontSize); // -記号を表示
        Number = -Number;                   // Numberを正にする
    }

    /* 整数部と小数部を抽出 */
    IntNum = Number;                  // 整数部を抽出
    Number -= IntNum;                 // 整数部を引いて小数部のみに
    PowNum = OLED_Pow(10, FraLength); // 小数桁数に応じた倍率
    FraNum = round(Number * PowNum);  // 小数を整数にし四捨五入
    IntNum += FraNum / PowNum;        // 繰り上がりがあれば整数部に加算

    /* 整数部を表示 */
    OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);

    /* 小数点を表示 */
    OLED_ShowChar(X + (IntLength + 1) * FontSize, Y, '.', FontSize);

    /* 小数部を表示 */
    OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}

/**
 * @brief OLEDに漢字列を表示
 * @param X 漢字列左上のX座標、範囲:0-127
 * @param Y 漢字列左上のY座標、範囲:0-63
 * @param Chinese 表示する漢字列、範囲:全て漢字または全角文字、半角文字は含めない
 *           表示する漢字はOLED_Data.c内のOLED_CF16x16配列で定義されている必要がある
 *           該当漢字が見つからない場合はデフォルト図形(四角に?)が表示される
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_ShowChinese(uint8_t X, uint8_t Y, char *Chinese)
{
    uint8_t pChinese = 0;
    uint8_t pIndex;
    uint8_t i;
    char SingleChinese[OLED_CHN_CHAR_WIDTH + 1] = {0}; // UTF8エンコーディングは3バイト、+1は\0終端のため

    for (i = 0; Chinese[i] != '\0'; i++) // 漢字列を走査
    {
        SingleChinese[pChinese] = Chinese[i]; // 漢字データを単体配列に抽出
        pChinese++;                           // カウント増加

        /* OLED_CHN_CHAR_WIDTHに達したら1漢字完了 */
        if (pChinese >= OLED_CHN_CHAR_WIDTH) {
            SingleChinese[pChinese + 1] = '\0'; // 終端文字を追加
            pChinese                    = 0;    // カウントリセット

            /* 漢字フォントテーブルを走査して一致を探す */
            /* 最後の漢字(空文字)に達したら定義なしと判断 */
            for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
                /* 一致した漢字を発見 */
                if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
                    break; // ループ脱出、pIndexが該当漢字のインデックス
                }
            }

            /* OLED_CF16x16の該当データを16×16画像として表示 */
            OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
        }
    }
}

/**
 * @brief OLEDに画像を表示
 * @param X 画像左上のX座標、範囲:0-127
 * @param Y 画像左上のY座標、範囲:0-63
 * @param Width 画像の幅、範囲:0-128
 * @param Height 画像の高さ、範囲:0-64
 * @param Image 表示する画像データ
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
    uint8_t i, j;

    /* パラメータチェック、画面外に出ないよう制限 */
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /* 画像領域をクリア */
    OLED_ClearArea(X, Y, Width, Height);

    /* 画像に関連するページを走査 */
    /* (Height - 1) / 8 + 1 はHeight / 8の切り上げ */
    for (j = 0; j < (Height - 1) / 8 + 1; j++) {
        /* 画像に関連する列を走査 */
        for (i = 0; i < Width; i++) {
            /* 境界外ならスキップ */
            if (X + i > 127) { break; }
            if (Y / 8 + j > 7) { return; }

            /* 現在のページに画像内容を表示 */
            OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);

            /* 境界外なら次へ */
            /* continueは次ページでも続行するため */
            if (Y / 8 + j + 1 > 7) { continue; }

            /* 次のページに画像内容を表示 */
            OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
        }
    }
}

/**
 * @brief OLEDでprintf形式の書式化文字列を表示
 * @param X 文字列左上のX座標、範囲:0-127
 * @param Y 文字列左上のY座標、範囲:0-63
 * @param FontSize フォントサイズ
 *           範囲:OLED_8X16 幅8ピクセル、高さ16ピクセル
 *                 OLED_6X8  幅6ピクセル、高さ8ピクセル
 * @param format 表示する書式化文字列、範囲:ASCII可視文字のみ
 * @param ... 書式化文字列の引数リスト
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
    char String[30];                         // 文字配列定義
    va_list arg;                             // 可変引数リスト型変数arg
    va_start(arg, format);                   // format以降の引数をargに受け取る
    vsprintf(String, format, arg);           // vsprintfで書式化文字列と引数を文字配列に出力
    va_end(arg);                             // arg終了
    OLED_ShowString(X, Y, String, FontSize); // OLEDに文字列を表示
}

/**
 * @brief 指定位置に点を描画
 * @param X 点のX座標、範囲:0-127
 * @param Y 点のY座標、範囲:0-63
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
    /* パラメータチェック、画面外に出ないよう制限 */
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /* 表示バッファの該当ビットを1にセット */
    OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}

/**
 * @brief 指定位置の点の状態を取得
 * @param X 点のX座標、範囲:0-127
 * @param Y 点のY座標、範囲:0-63
 * @return 点が点灯しているか、1:点灯、0:消灯
 */
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
    /* パラメータチェック、画面外なら0を返す */
    if (X > 127) { return 0; }
    if (Y > 63) { return 0; }

    /* 該当ビットを判定 */
    if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
        return 1; // 1なら1を返す
    }

    return 0; // そうでなければ0
}

/**
 * @brief OLEDに線を描画
 * @param X0 一端のX座標、範囲:0-127
 * @param Y0 一端のY座標、範囲:0-63
 * @param X1 もう一端のX座標、範囲:0-127
 * @param Y1 もう一端のY座標、範囲:0-63
 * @return なし
 * @note この関数を呼び出した後、実際に画面に反映するには更新関数を呼ぶ必要がある
 */
void OLED_DrawLine(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1)
{
    int16_t x, y, dx, dy, d, incrE, incrNE, temp;
    int16_t x0 = X0, y0 = Y0, x1 = X1, y1 = Y1;
    uint8_t yflag = 0, xyflag = 0;if (y0 == y1) // 横线は個別に処理
    {
        /* 点0のX座標が点1のX座標より大きい場合、両点のX座標を交換 */
        if (x0 > x1) {
            temp = x0;
            x0   = x1;
            x1   = temp;
        }

        /* X座標を走査 */
        for (x = x0; x <= x1; x++) {
            OLED_DrawPoint(x, y0); // 順次点を描画
        }
    } else if (x0 == x1) // 縦線は個別に処理
    {
        /* 点0のY座標が点1のY座標より大きい場合、両点のY座標を交換 */
        if (y0 > y1) {
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        /* Y座標を走査 */
        for (y = y0; y <= y1; y++) {
            OLED_DrawPoint(x0, y); // 順次点を描画
        }
    } else // 斜線
    {
        /* Bresenhamアルゴリズムを使用して直線を描画し、時間のかかる浮動小数点演算を回避して効率を向上 */
        /* 参考ドキュメント:https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf */
        /* 参考チュートリアル:https://www.bilibili.com/video/BV1364y1d7Lo */

        if (x0 > x1) // 点0のX座標が点1のX座標より大きい
        {
            /* 両点の座標を交換 */
            /* 交換後も線の描画に影響はなく、描画方向が第一・二・三・四象限から第一・四象限に変わる */
            temp = x0;
            x0   = x1;
            x1   = temp;
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        if (y0 > y1) // 点0のY座標が点1のY座標より大きい
        {
            /* Y座標を負にする */
            /* 負にすると描画に影響するが、描画方向が第一・四象限から第一象限に変わる */
            y0 = -y0;
            y1 = -y1;

            /* フラグyflagを設定し、現在の変換を記憶して後で実際の描画時に座標を元に戻す */
            yflag = 1;
        }

        if (y1 - y0 > x1 - x0) // 描画する線の傾きが1より大きい
        {
            /* X座標とY座標を入れ替える */
            /* 入れ替えると描画に影響するが、描画方向が第一象限の0~90度から0~45度に変わる */
            temp = x0;
            x0   = y0;
            y0   = temp;
            temp = x1;
            x1   = y1;
            y1   = temp;

            /* フラグxyflagを設定し、現在の変換を記憶して後で実際の描画時に座標を元に戻す */
            xyflag = 1;
        }

        /* 以下、Bresenhamアルゴリズムによる直線描画 */
        /* アルゴリズムの要求:描画方向は第一象限の0~45度でなければならない */
        dx     = x1 - x0;
        dy     = y1 - y0;
        incrE  = 2 * dy;
        incrNE = 2 * (dy - dx);
        d      = 2 * dy - dx;
        x      = x0;
        y      = y0;

        /* 開始点を描画し、フラグを判定して座標を元に戻す */
        if (yflag && xyflag) {
            OLED_DrawPoint(y, -x);
        } else if (yflag) {
            OLED_DrawPoint(x, -y);
        } else if (xyflag) {
            OLED_DrawPoint(y, x);
        } else {
            OLED_DrawPoint(x, y);
        }

        while (x < x1) // X軸の各点を走査
        {
            x++;
            if (d < 0) // 次の点は現在の点の東
            {
                d += incrE;
            } else // 次の点は現在の点の北東
            {
                y++;
                d += incrNE;
            }

            /* 各点を描画し、フラグを判定して座標を元に戻す */
            if (yflag && xyflag) {
                OLED_DrawPoint(y, -x);
            } else if (yflag) {
                OLED_DrawPoint(x, -y);
            } else if (xyflag) {
                OLED_DrawPoint(y, x);
            } else {
                OLED_DrawPoint(x, y);
            }
        }
    }
}

/**
 * @brief OLED長方形
 * @param X 長方形の左上隅のX座標を指定、範囲:0~127
 * @param Y 長方形の左上隅のY座標を指定、範囲:0~63
 * @param Width 長方形の幅を指定、範囲:0~128
 * @param Height 長方形の高さを指定、範囲:0~64
 * @param IsFilled 長方形を塗りつぶすかどうかを指定
 *           範囲:OLED_UNFILLED\t\t塗りつぶさない
 *                 OLED_FILLED\t\t\t塗りつぶす
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには更新関数を呼ぶ必要がある
 */
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
    uint8_t i, j;
    if (!IsFilled) // 長方形を塗りつぶさない指定
    {
        /* 上下のX座標を走査し、長方形の上下2本の線を描画 */
        for (i = X; i < X + Width; i++) {
            OLED_DrawPoint(i, Y);
            OLED_DrawPoint(i, Y + Height - 1);
        }
        /* 左右のY座標を走査し、長方形の左右2本の線を描画 */
        for (i = Y; i < Y + Height; i++) {
            OLED_DrawPoint(X, i);
            OLED_DrawPoint(X + Width - 1, i);
        }
    } else // 長方形を塗りつぶす指定
    {
        /* X座標を走査 */
        for (i = X; i < X + Width; i++) {
            /* Y座標を走査 */
            for (j = Y; j < Y + Height; j++) {
                /* 指定領域に点を描画し、長方形を塗りつぶす */
                OLED_DrawPoint(i, j);
            }
        }
    }
}

/**
 * @brief OLED三角形
 * @param X0 第1端点のX座標を指定、範囲:0-127
 * @param Y0 第1端点のY座標を指定、範囲:0-63
 * @param X1 第2端点のX座標を指定、範囲:0-127
 * @param Y1 第2端点のY座標を指定、範囲:0-63
 * @param X2 第3端点のX座標を指定、範囲:0-127
 * @param Y2 第3端点のY座標を指定、範囲:0-63
 * @param IsFilled 三角形を塗りつぶすかどうかを指定
 *           範囲:OLED_UNFILLED\t\t塗りつぶさない
 *                 OLED_FILLED\t\t\t塗りつぶす
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには更新関数を呼ぶ必要がある
 */
void OLED_DrawTriangle(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t X2, uint8_t Y2, uint8_t IsFilled)
{
    uint8_t minx = X0, miny = Y0, maxx = X0, maxy = Y0;
    uint8_t i, j;
    int16_t vx[] = {X0, X1, X2};
    int16_t vy[] = {Y0, Y1, Y2};

    if (!IsFilled) // 三角形を塗りつぶさない指定
    {
        /* 画線関数を呼び出し、3点を直線で結ぶ */
        OLED_DrawLine(X0, Y0, X1, Y1);
        OLED_DrawLine(X0, Y0, X2, Y2);
        OLED_DrawLine(X1, Y1, X2, Y2);
    } else // 三角形を塗りつぶす指定
    {
        /* 3点の最小X、Y座標を見つける */
        if (X1 < minx) { minx = X1; }
        if (X2 < minx) { minx = X2; }
        if (Y1 < miny) { miny = Y1; }
        if (Y2 < miny) { miny = Y2; }

        /* 3点の最大X、Y座標を見つける */
        if (X1 > maxx) { maxx = X1; }
        if (X2 > maxx) { maxx = X2; }
        if (Y1 > maxy) { maxy = Y1; }
        if (Y2 > maxy) { maxy = Y2; }

        /* 最小~最大座標間の長方形が塗りつぶしが必要な可能性のある領域 */
        /* この領域内のすべての点を走査 */
        /* X座標を走査 */
        for (i = minx; i <= maxx; i++) {
            /* Y座標を走査 */
            for (j = miny; j <= maxy; j++) {
                /* OLED_pnpolyを呼び出し、指定点が指定三角形内にあるか判定 */
                /* 内側なら点を描画、外側なら何もしない */
                if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
            }
        }
    }
}

/**
 * @brief OLED円描画
 * @param X 円の中心X座標を指定、範囲:0~127
 * @param Y 円の中心Y座標を指定、範囲:0~63
 * @param Radius 円の半径を指定、範囲:0~255
 * @param IsFilled 円を塗りつぶすかどうかを指定
 *           範囲:OLED_UNFILLED\t\t塗りつぶさない
 *                 OLED_FILLED\t\t\t塗りつぶす
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには更新関数を呼ぶ必要がある
 */
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /* Bresenhamアルゴリズムを使用して円を描画し、時間のかかる浮動小数点演算を回避して効率を向上 */
    /* 参考ドキュメント:https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf */
    /* 参考チュートリアル:https://www.bilibili.com/video/BV1VM4y1u7wJ */

    d = 1 - Radius;
    x = 0;
    y = Radius;

    /* 各8分円の円弧の開始点を描画 */
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X + y, Y + x);
    OLED_DrawPoint(X - y, Y - x);

    if (IsFilled) // 円を塗りつぶす指定
    {
        /* 開始点のY座標を走査 */
        for (j = -y; j < y; j++) {
            /* 指定領域に点を描画し、円の一部を塗りつぶす */
            OLED_DrawPoint(X, Y + j);
        }
    }

    while (x < y) // X軸の各点を走査
    {
        x++;
        if (d < 0) // 次の点は現在の点の東
        {
            d += 2 * x + 1;
        } else // 次の点は現在の点の南東
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /* 各8分円の円弧の点を描画 */
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X + y, Y + x);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - y, Y - x);
        OLED_DrawPoint(X + x, Y - y);
        OLED_DrawPoint(X + y, Y - x);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X - y, Y + x);

        if (IsFilled) // 円を塗りつぶす指定
        {
            /* 中央部分を走査 */
            for (j = -y; j < y; j++) {
                /* 指定領域に点を描画し、円の一部を塗りつぶす */
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }

            /* 両側部分を走査 */
            for (j = -x; j < x; j++) {
                /* 指定領域に点を描画し、円の一部を塗りつぶす */
                OLED_DrawPoint(X - y, Y + j);
                OLED_DrawPoint(X + y, Y + j);
            }
        }
    }
}

/**
 * @brief OLED楕円描画
 * @param X 楕円の中心X座標を指定、範囲:0~127
 * @param Y 楕円の中心Y座標を指定、範囲:0~63
 * @param A 楕円の水平半軸長を指定、範囲:0~255
 * @param B 楕円の垂直半軸長を指定、範囲:0~255
 * @param IsFilled 楕円を塗りつぶすかどうかを指定
 *           範囲:OLED_UNFILLED\t\t塗りつぶさない
 *                 OLED_FILLED\t\t\t塗りつぶす
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには更新関数を呼ぶ必要がある
 */
void OLED_DrawEllipse(uint8_t X, uint8_t Y, uint8_t A, uint8_t B, uint8_t IsFilled)
{
    int16_t x, y, j;
    int16_t a = A, b = B;
    float d1, d2;

    /* Bresenhamアルゴリズムを使用して楕円を描画し、一部の時間のかかる浮動小数点演算を回避して効率を向上 */
    /* 参考リンク:https://blog.csdn.net/myf_666/article/details/128167392 */

    x  = 0;
    y  = b;
    d1 = b * b + a * a * (-b + 0.5);```c
if (IsFilled) // 楕円の塗りつぶし指定
    {
        /*開始点Y座標を走査*/
        for (j = -y; j < y; j++) {
            /*指定領域に点を描画し、楕円の一部を塗りつぶす*/
            OLED_DrawPoint(X, Y + j);
            OLED_DrawPoint(X, Y + j);
        }
    }

    /*楕円弧の開始点を描画*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X - x, Y + y);
    OLED_DrawPoint(X + x, Y - y);

    /*楕円の中央部分を描画*/
    while (b * b * (x + 1) < a * a * (y - 0.5)) {
        if (d1 <= 0) // 次の点は現在の点の東
        {
            d1 += b * b * (2 * x + 3);
        } else // 次の点は現在の点の南東
        {
            d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
            y--;
        }
        x++;

        if (IsFilled) // 楕円の塗りつぶし指定
        {
            /*中央部分を走査*/
            for (j = -y; j < y; j++) {
                /*指定領域に点を描画し、楕円の一部を塗りつぶす*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*楕円中央部分の円弧を描画*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X + x, Y - y);
    }

    /*楕円の左右部分を描画*/
    d2 = b * b * (x + 0.5) * (x + 0.5) + a * a * (y - 1) * (y - 1) - a * a * b * b;

    while (y > 0) {
        if (d2 <= 0) // 次の点は現在の点の東
        {
            d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
            x++;

        } else // 次の点は現在の点の南東
        {
            d2 += a * a * (-2 * y + 3);
        }
        y--;

        if (IsFilled) // 楕円の塗りつぶし指定
        {
            /*左右部分を走査*/
            for (j = -y; j < y; j++) {
                /*指定領域に点を描画し、楕円の一部を塗りつぶす*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*楕円左右部分の円弧を描画*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X + x, Y - y);
    }
}

/**
 * @brief OLED円弧描画
 * @param X 円弧の中心X座標を指定、範囲:0~127
 * @param Y 円弧の中心Y座標を指定、範囲:0~63
 * @param Radius 円弧の半径を指定、範囲:0~255
 * @param StartAngle 円弧の開始角度を指定、範囲:-180~180
 *           水平右を0度、水平左を180度または-180度、下を正、上を負、時計回り
 * @param EndAngle 円弧の終了角度を指定、範囲:-180~180
 *           水平右を0度、水平左を180度または-180度、下を正、上を負、時計回り
 * @param IsFilled 円弧の塗りつぶしを指定、塗りつぶすと扇型になる
 *           範囲:OLED_UNFILLED\t\t塗りつぶしなし
 *                 OLED_FILLED\t\t\t塗りつぶしあり
 * @return なし
 * @note この関数を呼び出した後、実際に画面に表示するには更新関数を呼ぶ必要がある
 */
void OLED_DrawArc(uint8_t X, uint8_t Y, uint8_t Radius, int16_t StartAngle, int16_t EndAngle, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /*この関数はBresenhamアルゴリズムの円描画を借用*/

    d = 1 - Radius;
    x = 0;
    y = Radius;

    /*円の各点を描画時に、指定点が指定角度内にあるか判定し、あれば点を描画、なければ処理なし*/
    if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
    if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
    if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
    if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }

    if (IsFilled) // 円弧の塗りつぶし指定
    {
        /*開始点Y座標を走査*/
        for (j = -y; j < y; j++) {
            /*塗りつぶし円の各点を描画時に、指定点が指定角度内にあるか判定し、あれば点を描画、なければ処理なし*/
            if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
        }
    }

    while (x < y) // X軸の各点を走査
    {
        x++;
        if (d < 0) // 次の点は現在の点の東
        {
            d += 2 * x + 1;
        } else // 次の点は現在の点の南東
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*円の各点を描画時に、指定点が指定角度内にあるか判定し、あれば点を描画、なければ処理なし*/
        if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
        if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
        if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
        if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
        if (OLED_IsInAngle(x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y - y); }
        if (OLED_IsInAngle(y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y - x); }
        if (OLED_IsInAngle(-x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + y); }
        if (OLED_IsInAngle(-y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + x); }

        if (IsFilled) // 円弧の塗りつぶし指定
        {
            /*中央部分を走査*/
            for (j = -y; j < y; j++) {
                /*塗りつぶし円の各点を描画時に、指定点が指定角度内にあるか判定し、あれば点を描画、なければ処理なし*/
                if (OLED_IsInAngle(x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + j); }
                if (OLED_IsInAngle(-x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + j); }
            }

            /*左右部分を走査*/
            for (j = -x; j < x; j++) {
                /*塗りつぶし円の各点を描画時に、指定点が指定角度内にあるか判定し、あれば点を描画、なければ処理なし*/
                if (OLED_IsInAngle(-y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + j); }
                if (OLED_IsInAngle(y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + j); }
            }
        }
    }
}

/*********************機能関数*/

/*****************江協科技|著作権所有****************/
/*****************jiangxiekeji.com*****************/

おすすめ記事