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が点灯しない場合
- SDA/SCLに4.7kΩプルアップがあるか
- アドレスが0x3Eか
- 配線が正しいか
- 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でのビルド結果:
最大2048バイトのフラッシュメモリのうち、スケッチが642バイト(31%)を使っています。
最大128バイトのRAMのうち、グローバル変数が10バイト(7%)を使っていて、ローカル変数で118バイト使うことができます。
Wireライブラリを使わずに、I²C + LCD表示が可能です。
まとめ
ATtiny202のような小容量マイコンでも、
- VPORT直接制御
- シンプルなソフトI²C
- 必要最小限の設計
で十分実用レベルに到達できます。
むしろ自作することで、
✔ I²Cの内部動作理解
✔ メモリ制御の感覚
✔ ハード制御の基礎
が身につきます。
次回はこのI²Cコアを
- ライブラリ化
- Arduinoから簡単に呼び出せる形に整理
- DHT20などのセンサ対応
まで発展たいと思います。


