ATtiny202/402で動く超軽量I²Cコアを自作する(Wire不使用・約300バイト)

薄いブルー背景にATtiny202マイコンとI2C接続されたLCDモジュール、LEDが配置されたI2C解説用アイキャッチ画像ATtiny202/402で動く超軽量I²Cコアを自作する(Wire不使用・約300バイト)
スポンサーリンク

tinyAVR 0/1/2シリーズは非常に小型で高性能ですが、
ATtiny202はFlash 2KB / RAM 128B しかありません。
通常の Wire ライブラリを使うと、すぐにメモリを圧迫してしまいます。
そこで今回は、

  • VPORTを使った高速制御
  • ソフトウェアI²C
  • ACK検出付き
  • 約100kHz動作
  • 実機でAQM1602表示確認済み

という 超軽量I²Cコア を作ります。
対象:

  • ATtiny202
  • ATtiny402
  • megaTinyCore環境
スポンサーリンク

🔵 設計方針

今回の設計思想は次の通りです。

✔ ハードTWIは使わない
✔ VPORT直接操作
✔ オープンドレイン擬似動作
✔ ACK取得可能
✔ 100kHz固定

tinyAVRはVPORTが使えるため、非常に高速にピン制御できます。

🔧 低レベルI²Cコア

プログラムの書き込み方法についてはこちらのブログで確認してください。

#include <avr/io.h>
#include <util/delay.h>

#define SDA 1  //PA1 番ピン
#define SCL 2  //PA2 番ピン

static inline void sda_high(void) { VPORTA.DIR &= ~(1<<SDA); }
static inline void sda_low(void)  { VPORTA.OUT &= ~(1<<SDA); VPORTA.DIR |= (1<<SDA); }
static inline void scl_high(void) { VPORTA.DIR &= ~(1<<SCL); }
static inline void scl_low(void)  { VPORTA.OUT &= ~(1<<SCL); VPORTA.DIR |= (1<<SCL); }

static inline void i2c_delay(void) { _delay_us(4); }  // 約100kHz

なぜ OUT を先に0にするのか?

VPORTA.OUT &= ~(1<<SDA);
VPORTA.DIR |= (1<<SDA);

先にOUTをLowにしてから出力に切り替えないと、
瞬間的にHighが出る可能性があります。

これがI²Cでは重要です。

🔵 基本I²C動作

Start / Stop / Write / Read を実装します。

#include <avr/io.h>
#include <util/delay.h>
#define SDA 1  //PA1
#define SCL 2  //PA2
#define LED 3  //動作確認用LED 
/* ======== 低レベル制御(VPORT) ======== */
static inline void sda_high(void) { VPORTA.DIR &= ~(1<<SDA); }
static inline void sda_low(void)  { VPORTA.OUT &= ~(1<<SDA); VPORTA.DIR |= (1<<SDA); }
static inline void scl_high(void) { VPORTA.DIR &= ~(1<<SCL); }
static inline void scl_low(void)  { VPORTA.OUT &= ~(1<<SCL); VPORTA.DIR |= (1<<SCL); }
static inline void i2c_delay(void) { _delay_us(4); }  // 約100kHz

/* ======== I2C基本 ======== */
void i2c_init(void)
{
    VPORTA.OUT |= (1<<SDA) | (1<<SCL);
    VPORTA.DIR &= ~((1<<SDA) | (1<<SCL));
}
void i2c_start(void)
{
    sda_high(); scl_high(); i2c_delay();
    sda_low();  i2c_delay(); scl_low();
}
void i2c_stop(void)
{
    sda_low();  scl_high(); i2c_delay();
    sda_high(); i2c_delay();
}

/* ======== 1バイト送信(ACK返す) ======== */
uint8_t i2c_write(uint8_t data)
{
    for(uint8_t i=0;i<8;i++)
    {
        if(data & 0x80) sda_high();
        else            sda_low();

        scl_high(); i2c_delay();scl_low();  i2c_delay();
        data <<= 1;
    }
    // ACK受信
    sda_high();                // SDA開放
    scl_high(); i2c_delay();

    uint8_t ack = !(VPORTA.IN & (1<<SDA));  // 0ならACK

    scl_low();
    return ack;
}

/* ======== 1バイト受信 ======== */
uint8_t i2c_read(uint8_t ack)
{
    uint8_t data = 0;
    sda_high();   // 入力

    for(uint8_t i=0;i<8;i++)
    {
        data <<= 1;
        scl_high(); i2c_delay();

        if(VPORTA.IN & (1<<SDA))
            data |= 1;
        scl_low(); i2c_delay();
    }

    // ACK/NACK送信
    if(ack) sda_low();   // ACK=0
    else    sda_high();  // NACK=1
    scl_high(); i2c_delay();scl_low();sda_high();
    return data;
}

LED判定プログラム

AQM1602のアドレスは 0x3E
I²Cでは左に1bitシフトするので

uint8_t ack = i2c_write(0x3E << 1);

アドレス判定プログラムです

void setup(){
    i2c_init();

    VPORTA.DIR |= (1<<LED);

    i2c_start();
    uint8_t ack = i2c_write(0x3E << 1);
    i2c_stop();

    if(ack)
        VPORTA.OUT |= (1<<LED);   // ACK成功 → 点灯
    else
        VPORTA.OUT &= ~(1<<LED);  // NACK → 消灯

    while(1);
}

void loop(){}

動作結果の見方

LED状態意味
点灯AQM1602が応答している
消灯配線ミス / プルアップ無し / アドレス違い

このテストのメリット

✔ シリアル不要
✔ LCD初期化不要
✔ I²Cの基本確認だけできる
✔ 配線チェックに最適


もしLEDが点灯しない場合

  1. SDA/SCLに4.7kΩプルアップがあるか
  2. アドレスが0x3Eか
  3. 配線が正しいか
  4. I²Cクロックが速すぎないか

LCDを動かす前に、まず「アドレスにACKが返るか」を確認するのが正しい順番です。
これだけでデバッグ効率が劇的に上がります。

AQM1602で動作確認

今回はI²C LCDモジュール,AQM1602 で確認しました。
アドレス:0x3E
SDA PA1(4) ピン
SCL PA2(5) ピン

プログラム完成版

#include <avr/io.h>
#include <util/delay.h>
#define SDA 1  //PA1  pin
#define SCL 2  //PA2  pin
#define LED 3
/* ======== 低レベル制御(VPORT) ======== */
static inline void sda_high(void) { VPORTA.DIR &= ~(1<<SDA); }
static inline void sda_low(void)  { VPORTA.OUT &= ~(1<<SDA); VPORTA.DIR |= (1<<SDA); }
static inline void scl_high(void) { VPORTA.DIR &= ~(1<<SCL); }
static inline void scl_low(void)  { VPORTA.OUT &= ~(1<<SCL); VPORTA.DIR |= (1<<SCL); }
static inline void i2c_delay(void) { _delay_us(4); }  // 約100kHz

/* ======== I2C基本 ======== */
void i2c_init(void)
{
    VPORTA.OUT |= (1<<SDA) | (1<<SCL);
    VPORTA.DIR &= ~((1<<SDA) | (1<<SCL));
}
void i2c_start(void)
{
    sda_high(); scl_high(); i2c_delay();
    sda_low();  i2c_delay(); scl_low();
}
void i2c_stop(void)
{
    sda_low();  scl_high(); i2c_delay();
    sda_high(); i2c_delay();
}

/* ======== 1バイト送信(ACK返す) ======== */
uint8_t i2c_write(uint8_t data)
{
    for(uint8_t i=0;i<8;i++)
    {
        if(data & 0x80) sda_high();
        else            sda_low();

        scl_high(); i2c_delay();scl_low();  i2c_delay();
        data <<= 1;
    }
    // ACK受信
    sda_high();                // SDA開放
    scl_high(); i2c_delay();

    uint8_t ack = !(VPORTA.IN & (1<<SDA));  // 0ならACK

    scl_low();
    return ack;
}

/* ======== 1バイト受信 ======== */
uint8_t i2c_read(uint8_t ack)
{
    uint8_t data = 0;
    sda_high();   // 入力

    for(uint8_t i=0;i<8;i++)
    {
        data <<= 1;
        scl_high(); i2c_delay();

        if(VPORTA.IN & (1<<SDA))
            data |= 1;
        scl_low(); i2c_delay();
    }

    // ACK/NACK送信
    if(ack) sda_low();   // ACK=0
    else    sda_high();  // NACK=1
    scl_high(); i2c_delay();scl_low();sda_high();
    return data;
}
void lcd_command(uint8_t cmd)
{
    i2c_start();
    i2c_write(0x3E << 1);
    i2c_write(0x00);   // コマンドモード
    i2c_write(cmd);
    i2c_stop();
    _delay_ms(2);
}

void lcd_init(void)
{
    _delay_ms(40);
    lcd_command(0x38);
    lcd_command(0x39);
    lcd_command(0x14);
    lcd_command(0x70);
    lcd_command(0x56);
    lcd_command(0x6C);
    _delay_ms(200);
    lcd_command(0x38);
    lcd_command(0x0C);
    lcd_command(0x01);
    _delay_ms(2);
}
void lcd_data(uint8_t data)
{
    i2c_start();
    i2c_write(0x3E << 1);
    i2c_write(0x40);   // データモード
    i2c_write(data);
    i2c_stop();
}
void lcd_print(const char *str)
{
    while(*str)
    {
        lcd_data(*str++);
    }
}

void lcd_set_cursor(uint8_t row, uint8_t col)
{
    uint8_t addr = (row == 0) ? 0x00 : 0x40;
    addr += col;
    lcd_command(0x80 | addr);
}

void setup()
{
    i2c_init();
    lcd_init();

    lcd_set_cursor(0,0);
    lcd_print("Hello");

    lcd_set_cursor(1,0);
    lcd_print("ATtiny202");
}

void loop()
{
    
}

問題なく表示されました。

attiny202 表示画像

メモリ使用量

ATtiny202でのビルド結果:

Wireライブラリを使わずに、I²C + LCD表示が可能です。

まとめ

ATtiny202のような小容量マイコンでも、

  • VPORT直接制御
  • シンプルなソフトI²C
  • 必要最小限の設計

で十分実用レベルに到達できます。
むしろ自作することで、

✔ I²Cの内部動作理解
✔ メモリ制御の感覚
✔ ハード制御の基礎

が身につきます。

次回はこのI²Cコアを

  • ライブラリ化
  • Arduinoから簡単に呼び出せる形に整理
  • DHT20などのセンサ対応

まで発展たいと思います。

タイトルとURLをコピーしました