STM32におけるST7567 LCDドライバの実装:HALライブラリ連携と低レベルタイミング制御

ST7567コントローラを搭載した128×64ドットマトリックスLCDは、内蔵RAMと昇圧回路を備えていますが、一般的なHD44780のようなキャラクターLCDやSSD1306のようなOLEDとは駆動方式が大きく異なります。特にSTM32 HALライブラリを使用してST7567を制御する場合、その独自の通信タイミングとレジスタマッピングロジックを理解することが不可欠です。HALの抽象化層は便利な反面、ST7567のような「わずかなタイミングのずれで動作しない」デリケートなデバイスの低レベルな要件を隠蔽してしまうことがあります。本記事では、ST7567のチップデータシートに基づき、HALライブラリと組み合わせて安定したディスプレイ制御を実現するための具体的な実装方法と考慮事項を詳述します。

ST7567と一般的なディスプレイコントローラの主な相違点

ST7567をSTM32で確実に動作させるには、一般的なOLEDドライバの考え方から離れ、チップデータシートの仕様に立ち返る必要があります。ここでは、ST7567の駆動において特に注意すべき4つのポイントを挙げます。

通信プロトコルの特殊性

ST7567のSPIインターフェースは標準的なSPIとは異なるタイミング要件を持ちます。SCLKの立ち上がりエッジでデータをサンプリングしますが、CS (Chip Select) 信号は各バイト転送の開始と終了時に厳密に制御される必要があります。データシートの図には、CSがSCLKの最初の立ち下がりエッジより前に少なくともtCSS (50ns) ローレベルになり、最後のSCLK立ち上がりエッジからtCSH (50ns) 以内にハイレベルに戻る必要があると明記されています。しかし、HAL_SPI_Transmit()関数でハードウェアCS (hspi->Init.NSS = SPI_NSS_HARD) を使用すると、CSのタイミングはSPI周辺機器によって自動管理され、ST7567が許容する最大値 (通常100ns以下) を超えることがあります。さらに、ST7567の「データバイト」にはコマンドとパラメータの両方の意味が含まれるため、単一のHAL_SPI_Transmit()呼び出しで一括して送信するのではなく、単一バイトと精密なCS制御を組み合わせたアトミックな操作に分割する必要があります。

GDDRAMアドレス指定メカニズム

ST7567のGDDRAMは64行×128列のビットマップ構造ですが、SSD1306のような「ページアドレス自動インクリメントモード」は存在しません。アドレスはXアドレス (0~127)、Yアドレス (0~7)、およびCOM出力方向によって決定されます。ページアドレス設定コマンド (0xB0~0xB7) はYページ (各8行) のみ設定し、カラムアドレス設定コマンド (0x10, 0x00~0x0F) は2段階で送信する必要があります。より重要なのは、ピクセルデータを書き込むたびに、完全なX/Yアドレスコマンドシーケンスを再送信する必要がある点です。コントローラは前のアドレスを保持しないため、多くの場合、表示が左上隅の領域に限定される原因となります。

電源とバイアス設定

ST7567モジュールは通常、VDD (3.3V)、VSS (GND)、V0 (コントラスト調整)、VLCD (LCD電源) の各電源ラインを引き出しますが、VLCD電圧は内部DC-DC昇圧回路によって生成され、ソフトウェアでイネーブルする必要があります。具体的には、POWER_CONTROL (0x28)レジスタのbit2 (REGULATOR ON/OFF) を1に設定しないと、VLCDが0VとなりLCDは応答しません。また、BOOSTER_RATIO (0xF8)レジスタで昇圧比 (3x/4x/5x) を設定する必要があります。デフォルト値がモジュールの設計と一致しない場合、コントラストが極端に低くなったり、画面がちらついたりすることがあります。

リセットと初期化シーケンス

ST7567のリセットプロセスには、ハードウェアリセット (/RESピン) とソフトウェアリセット (SOFTWARE_RESET 0xE2) の両方が含まれます。しかし、データシートには、ハードウェアリセット後には少なくとも10ms待機し、その後初期化コマンドシーケンスを送信する必要があると強調されています。また、初期化コマンドの順序は厳格であり、SET_POWER_CONTROL (0x28)によるレギュレータ有効化、SET_BOOSTER_RATIO (0xF8)、SET_FOLLOWER_CONTROL (0x2A)、最後にSET_DISPLAY_ON (0xAF)の順で行う必要があります。これらのステップのいずれかが欠落したり、順序が間違っていたりすると、ディスプレイは正常に表示されません。

STM32 HAL利用時の対応策

標準的なHAL_SPI関数がST7567のタイミング要件を満たせない場合、HALライブラリに対する特定の調整が必要になります。HALライブラリのソースコードを変更せず、HAL周辺機器とビットバンギングSPIを組み合わせるアプローチが有効です。以下は、STM32 HALを使用する際に推奨される実装方法です。

CS信号のGPIOによる精密制御

まず、CubeMXでSPI1をMode 0 (CPOL=0, CPHA=0) に設定し、Baud Rate PrescalerをST7567の最大10MHz要件を満たす値 (例: 72MHzシステムで8分周 → SCLK=9MHz) に設定します。重要なのは、NSSオプションを「Software」に設定し、ハードウェアNSS機能を無効にすることです。次に、CSピンを定義し、GPIOでCS信号を直接制御する関数を実装します。

#define LCD_CS_PORT GPIOA
#define LCD_CS_PIN GPIO_PIN_4
#define LCD_SCK_PORT GPIOA
#define LCD_SCK_PIN GPIO_PIN_5
#define LCD_MOSI_PORT GPIOA
#define LCD_MOSI_PIN GPIO_PIN_7

/**
 * @brief ST7567に1バイトをGPIOビットバンギングで送信
 * @param data 送信するバイト
 */
void LcdSpiTransferByte(uint8_t data) {
    // 1. CS信号を精密にローレベルに設定
    HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
    // 短い遅延を挿入し、CS確立時間を確保 (例: NOP命令)
    __asm volatile ("nop");

    // 2. 手動SPIタイミング: SCLKアイドルはローレベル、データはSCLK立ち上がりエッジでサンプリング
    for(uint8_t i = 0; i < 8; i++) {
        // MOSI電平を設定
        if(data & 0x80) {
            HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_SET);
        } else {
            HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_RESET);
        }
        data <<= 1;

        // SCLK立ち上がりエッジを生成: まずローにし、次にハイにする
        HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_RESET);
        __asm volatile ("nop"); // SCLK LOW時間確保
        HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_SET);
        __asm volatile ("nop"); // SCLK HIGH時間確保
    }
    
    // 3. CS信号を精密にハイレベルに設定
    HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET);
    __asm volatile ("nop"); // CS解放時間確保
}

この関数はGPIOを介してSPIをエミュレートするため、速度は犠牲になりますが、タイミングの正確性は保証されます。リアルタイム性が厳しく要求される場合は、ハードウェアSPIとDMAを組み合わせ、CS制御をGPIO割り込みでトリガすることも可能ですが、最初はビットバンギングによるGPIO制御が最も理解しやすく確実です。

コマンドとデータの分離処理

ST7567はD/C#ピン (Data/Command Select) を通じてコマンドとデータを区別します。このピンもGPIOで制御し、それぞれの操作を関数としてカプセル化します。

#define LCD_DC_PORT GPIOA
#define LCD_DC_PIN GPIO_PIN_3
#define LCD_RES_PORT GPIOA
#define LCD_RES_PIN GPIO_PIN_2 // 例: リセットピンをPA2に割り当て

// D/Cピンをコマンドモードに設定 (ローレベル)
#define LcdSetCommandMode() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET)
// D/Cピンをデータモードに設定 (ハイレベル)
#define LcdSetDataMode()    HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET)

/**
 * @brief ST7567にコマンドを送信
 * @param cmd 送信するコマンドバイト
 */
void LcdWriteCommand(uint8_t cmd) {
    LcdSetCommandMode();
    LcdSpiTransferByte(cmd);
}

/**
 * @brief ST7567にデータを送信
 * @param data 送信するデータバイト
 */
void LcdWriteData(uint8_t data) {
    LcdSetDataMode();
    LcdSpiTransferByte(data);
}

/**
 * @brief ST7567にデータバッファを送信
 * @param pBuf データバッファへのポインタ
 * @param len 送信するバイト数
 */
void LcdWriteDataBlock(uint8_t *pBuf, uint16_t len) {
    LcdSetDataMode();
    for(uint16_t i = 0; i < len; i++) {
        LcdSpiTransferByte(pBuf[i]);
    }
}

初期化シーケンスの実装

ST7567の初期化は、データシートのTable 12に記載されている12ステップのプロセスに従う必要があります。この順序は非常に重要です。

/**
 * @brief ディスプレイ初期化時の遅延 (ミリ秒)
 * @param ms 遅延時間 (ミリ秒)
 */
static void DelayMs(uint32_t ms) {
    HAL_Delay(ms); // HAL_Delayを使用、DWTによる高精度遅延も後述
}

/**
 * @brief ST7567ディスプレイの初期化
 */
void LcdInitializeDisplay(void) {
    // 1. ハードウェアリセット
    HAL_GPIO_WritePin(LCD_RES_PORT, LCD_RES_PIN, GPIO_PIN_RESET);
    DelayMs(20); // データシート要件: >10ms
    HAL_GPIO_WritePin(LCD_RES_PORT, LCD_RES_PIN, GPIO_PIN_SET);
    DelayMs(20);
    
    // 2. ソフトウェアリセット
    LcdWriteCommand(0xE2);
    DelayMs(1); // 各コマンド後には短い遅延を推奨

    // 3-12. 必須の初期化コマンド (順序厳守)
    LcdWriteCommand(0x28); // Power Control: レギュレータ有効化
    DelayMs(1);
    LcdWriteCommand(0xF8); // Booster Ratio: 4x (モジュールにより調整)
    DelayMs(1);
    LcdWriteCommand(0x2A); // Follower Control: 有効化
    DelayMs(1);
    LcdWriteCommand(0x2C); // Follower Speed: Medium (モジュールにより調整)
    DelayMs(1);
    LcdWriteCommand(0x2E); // Bias System: 1/9 (モジュールにより調整)
    DelayMs(1);
    LcdWriteCommand(0xA2); // LCD Bias: 1/9
    DelayMs(1);
    LcdWriteCommand(0xC0); // ADC Select: Normal (SEG0->SEG127)
    DelayMs(1);
    LcdWriteCommand(0xA0); // SEG Direction: Normal
    DelayMs(1);
    LcdWriteCommand(0xC8); // COM Direction: Reverse (共通カソードの場合。共通アノードなら0xC0)
    DelayMs(1);
    LcdWriteCommand(0x40); // Display Start Line: 0
    DelayMs(1);
    LcdWriteCommand(0xAF); // Display On
    DelayMs(1);
    
    // 初期化後、画面をクリア
    LcdClearScreen();
}

COM方向設定コマンド (0xC8) は、多くのモジュールで共通カソード向けですが、一部の国産モジュールでは共通アノード設計の場合があり、その際は0xC0に変更する必要があります。実機で確認することを推奨します。

ディスプレイバッファの管理と更新

ST7567のGDDRAMはページ (Page) 単位で構成されており、各ページは8行×128列で128バイトです。全画面は8ページ (Page 0~7) に分割され、それぞれY座標0~7に対応します。描画データは、まずこのフレームバッファに書き込み、その後、全画面を更新する関数でディスプレイに転送します。

uint8_t g_displayBuffer[1024]; // 128*64/8 = 1024バイト

/**
 * @brief 画面全体をクリアする (バッファとディスプレイの両方)
 */
void LcdClearScreen(void) {
    memset(g_displayBuffer, 0x00, sizeof(g_displayBuffer));
    LcdRefreshScreen();
}

/**
 * @brief フレームバッファの内容をディスプレイに転送して画面を更新
 */
void LcdRefreshScreen(void) {
    for(uint8_t page = 0; page < 8; page++) {
        // ページアドレスを設定
        LcdWriteCommand(0xB0 + page); // Page 0: 0xB0, Page 1: 0xB1...
        // カラムアドレスを設定 (高位と低位)
        LcdWriteCommand(0x10); // Column MSB (High nibble)
        LcdWriteCommand(0x00); // Column LSB (Low nibble)
        // 該当ページ (128バイト) のデータを書き込む
        LcdWriteDataBlock(&g_displayBuffer[page * 128], 128);
    }
}

ST7567のCS信号は、各ページ切り替え時にリセットされる必要があるため、DMAを使用して全画面を一気に転送することは困難です。各ページごとにコマンドとデータを送信するシーケンスが必要となります。GPIOビットバンギングによるSPI転送でも、全画面更新に約180ms (9MHz SCLK時) かかりますが、これは静的なHMIアプリケーションには十分な速度です。動的な表示が必要な場合は、画面全体ではなく変化した領域のみを更新する部分更新戦略を検討してください。

実用的な機能の実装と安定化

ディスプレイを点灯させることは最初のステップに過ぎません。実際にプロジェクトに組み込むには、文字表示、図形描画、そして安定性に関する課題を解決する必要があります。

日本語フォントの描画

ST7567はグラフィックディスプレイであるため、文字を表示するにはフォントデータ (ビットマップ) が必要です。16×16ドットのような日本語フォントデータ (例: HZK16) は、全文字をバッファに格納するには大きすぎることが多いため、必要な文字データをメモリマップド方式でFlashメモリから読み込むアプローチが有効です。

// フォントグリフ情報を格納する構造体
typedef struct {
    uint16_t offset; // Flashメモリ内のデータオフセット
    uint8_t width;   // グリフの幅 (ピクセル)
    uint8_t height;  // グリフの高さ (ピクセル)
} CharGlyphInfo;

// 常用文字のフォントインデックステーブル (例として)
// 実際のアプリケーションでは、大量の文字データが必要な場合、
// 外部Flashなどから動的に読み込む戦略を検討する
const CharGlyphInfo g_fontGlyphIndex[] = {
    {0x0000, 16, 16}, // 'あ' のデータオフセットとサイズ
    {0x0020, 16, 16}, // 'い' のデータオフセットとサイズ
    // ... その他の文字
};

/**
 * @brief Flashメモリからグリフデータを読み出す (仮実装)
 * @param offset Flash内の開始オフセット
 * @param pBuffer 読み出し先のバッファ
 * @param size 読み出すバイト数
 */
void RetrieveGlyphData(uint16_t offset, uint8_t *pBuffer, uint16_t size) {
    // 実際のFlash読み出し処理をここに実装
    // 例: memcpy(pBuffer, (const uint8_t*)(FONT_FLASH_ADDRESS + offset), size);
    // あるいは外部SPI Flashなどから読み出す
}

/**
 * @brief 指定された位置に文字グリフを描画
 * @param x X座標 (ピクセル)
 * @param y Y座標 (ピクセル)
 * @param glyphIndex g_fontGlyphIndex内のインデックス
 */
void LcdDrawCharacterGlyph(uint16_t x, uint16_t y, uint8_t glyphIndex) {
    if(glyphIndex >= sizeof(g_fontGlyphIndex)/sizeof(CharGlyphInfo)) return;

    uint8_t glyph_data[32]; // 16x16ドット = 256ビット = 32バイト
    RetrieveGlyphData(g_fontGlyphIndex[glyphIndex].offset, glyph_data, 32);
    
    for(uint8_t row = 0; row < 16; row++) {
        uint8_t target_page = (y + row) / 8;
        uint8_t bit_offset_in_page = (y + row) % 8;
        
        // グリフ行データをビット単位でバッファに書き込む
        for(uint8_t col = 0; col < 16; col++) {
            // グリフデータから該当ビットを取得 (最上位ビットから)
            uint8_t bit = (glyph_data[row*2 + col/8] >> (7 - col%8)) & 0x01;
            
            if(bit) {
                // ドットをセット
                g_displayBuffer[target_page*128 + (x+col)] |= (1 << bit_offset_in_page);
            } else {
                // ドットをクリア
                g_displayBuffer[target_page*128 + (x+col)] &= ~(1 << bit_offset_in_page);
            }
        }
    }
}

フォントデータはFlashメモリの特定アドレスに配置し、実行時にそのアドレスから直接読み出すことで、RAMの使用量を抑えつつ高速な描画が可能です。__attribute__((section(".font")))のようなリンカセクション指定を使用すると、特定のフォントデータをFlashメモリの指定されたアドレスに配置できます。

耐ノイズ設計の考察

ST7567は電源ノイズや信号の完全性に非常に敏感です。以下の点に注意することで、システムの安定性を大幅に向上させることができます。

  • VLCDピンのフィルタリング: データシートでは、VLCD端に10μFのタンタルコンデンサと0.1μFのセラミックコンデンサを並列に接続することが推奨されています。これがない場合、VLCDに大きなリップルノイズが発生し、画面のちらつきや不安定な動作の原因となります。
  • SPI配線とグランドプレーン: PCB上でのSPI信号線が長すぎると、信号の反射やクロストークが発生しやすくなります。SPIラインを短く (3cm以下が理想) し、両側にグランドプレーンを配置することで、信号の完全性を確保し、SCLKエッジでのリンギングを抑制できます。
  • リセット信号の保護: /RESピンは、外部ノイズ源 (例: モータ、リレー) の影響を受けやすいことがあります。シュミットトリガ入力を持つバッファを使用したり、適切なフィルタリングを施したりすることで、意図しないリセットを防ぐことができます。

よくある問題とトラブルシューティング

ST7567のデバッグは、タイミングが重要であるため、論理アナライザが非常に有効なツールとなります。以下に一般的な問題と解決策を示します。

現象 考えられる原因 確認手順 解決策
画面が全く表示されない 1. VLCDが0V
2. /RESが解放されていない
3. CSがハイレベルになっていない
1. マルチメータでVLCD電圧を測定
2. オシロスコープで/RES波形を確認
3. CSピンの電位を測定
1. 0x28コマンドが送信されているか確認
2. リセット後の遅延を20ms以上に延長
3. アイドル時にCSがハイレベルであることを確認
表示がずれている/オフセットされている 1. X/Yアドレスがリセットされていない
2. COM方向が誤っている
1. 論理アナライザでアドレスコマンドを捕捉
2. 0xC0 / 0xC8 コマンドを切り替えて試行
1. 各ページ書き込み前に0xB0+pageと0x10/0x00を送信していることを確認
2. モジュールのデータシートでCOM極性を確認
画面の一部がちらつく 1. 画面更新のタイミング競合
2. 電源リップルが大きい
1. LcdRefreshScreen()が割り込みで中断されていないか確認
2. オシロスコープでVLCDリップルを測定
1. 画面更新中はグローバル割り込みを無効にする
2. VLCD端に10μF+0.1μFのフィルタコンデンサを追加
コントラストが極端に低い 1. 0xF8昇圧比設定が不適切
2. V0電位差計が調整されていない
1. マルチメータでVLCD電圧を測定
2. V0電位差計を調整
1. 0xF8 → 0xF9 (5x昇圧) を試す
2. V0電圧を約-2.5V (代表値) に調整

論理アナライザは、ST7567のSPI通信の波形を正確に把握し、タイミング問題を特定するための非常に強力なツールです。機器がない場合でも、主要なコマンドの前後にGPIOピンをトグルさせ、オシロスコープでその波形を観測することで、タイミングのずれを間接的に検出することができます。

HALライブラリ利用時の高度な最適化

ST7567の初期化におけるHAL_Delay()のようなミリ秒単位の遅延は、CPUをブロックし、システムのリアルタイム性を損なう可能性があります。より高精度で非ブロッキングな遅延を実現するための手法や、その他の最適化について説明します。

DWTによる高精度遅延処理

DWT (Data Watchpoint and Trace) ユニットのCYCCNTレジスタはCPUクロックサイクル数をカウントするため、これを利用することでマイクロ秒単位の精密な遅延を実現できます。STM32F103の場合、72MHzの主周波数では1サイクルが約13.9nsに相当します。

/**
 * @brief DWTサイクルカウンタを有効化
 * @note CoreDebugとDWTユニットへのアクセスを許可する
 */
void EnableDwtCounter(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // トレース機能有効化
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;             // サイクルカウンタ有効化
    DWT->CYCCNT = 0;                                 // カウンタをクリア
}

/**
 * @brief 現在のDWTサイクル数を取得
 * @return 現在のサイクル数
 */
uint32_t GetDwtCycleCount(void) {
    return DWT->CYCCNT;
}

/**
 * @brief DWTを利用したマイクロ秒単位の遅延
 * @param us 遅延時間 (マイクロ秒)
 */
void MicrosecondDelay(uint32_t us) {
    uint32_t start_cycles = GetDwtCycleCount();
    // 72MHzの場合、1us = 72サイクル
    uint32_t target_cycles = start_cycles + us * 72; 
    // オーバーフロー対応のため差分で比較
    while((GetDwtCycleCount() - start_cycles) < (target_cycles - start_cycles));
}

初期化シーケンス内のHAL_Delay(1)をMicrosecondDelay(1000)に置き換えることで、遅延の精度がミリ秒からマイクロ秒に向上し、SysTickタイマーのリソースを消費しないため、より柔軟なシステム設計が可能になります。

割り込み安全なバッファ更新戦略

割り込みサービスルーチン (ISR) 内でディスプレイバッファを更新し、メインループで画面をリフレッシュするようなシステムでは、バッファへの同時アクセスによるデータ破損を防ぐための割り込み安全な戦略が必要です。一般的には、変更フラグとクリティカルセクションを組み合わせたアプローチが推奨されます。

volatile uint8_t g_displayBufferUpdated = 0; // バッファが更新されたことを示すフラグ

// センサーデータ準備完了割り込みサービスルーチン (例)
void SensorDataReadyISR(void) {
    // ... センサーデータ処理
    // バッファへの描画処理
    // ...
    g_displayBufferUpdated = 1; // バッファ更新フラグをセット
}

// メインループ
int main(void) {
    // ... システム初期化
    EnableDwtCounter(); // DWTを有効化

    while(1) {
        if(g_displayBufferUpdated) {
            __disable_irq(); // グローバル割り込みを無効化し、クリティカルセクションに入る
            LcdRefreshScreen(); // ディスプレイを更新
            g_displayBufferUpdated = 0; // フラグをクリア
            __enable_irq();  // グローバル割り込みを有効化し、クリティカルセクションを抜ける
        }
        MicrosecondDelay(10000); // 例: 10msごとにメインループ処理
    }
}

割り込み処理中に直接LcdRefreshScreen()のような時間のかかる関数を呼び出すと、他の重要な割り込みがブロックされたり、SPI転送中にCS信号のタイミングが乱れたりして、画面が破損するリスクがあります。メインループでフラグをポーリングし、クリティカルセクション内で画面更新を行うことで、この問題を回避できます。

低消費電力モードにおける画面維持

ST7567はDISPLAY_OFF (0xAE)コマンドでスリープモードに入ることができますが、このモードから復帰するには再初期化が必要です。消費電力を抑えつつ画面表示を維持したい場合は、以下のような手法を組み合わせることができます。

  • 0x28コマンドのレギュレータ有効化ビット (bit2) を0にし、内部DC-DCを無効化して外部からVLCDを供給する。
  • SPIのSCLK周波数を1MHz程度まで下げることで、SPI通信時の消費電力を削減する。
  • 0xACコマンドで内部OSCの周波数を調整し、チップ全体のクロック消費電力を低減する。

これらの最適化により、表示品質を大きく損なうことなく、システム全体の消費電力を削減することが可能です。ST7567の駆動は、データシートの低レベルなタイミング要件を丁寧に読み解き、HALライブラリの抽象化の裏側にある物理的な信号の流れを理解することで、安定した実装に繋がります。

タグ: STM32 ST7567 LCD SPI HAL

10月26日 23:53 投稿