子供はくじ引きが大好きで、自分でくじ引きの箱を作ってしまうほどです。
そんなくじ引き好きの子供のために、3色LEDとAttiny13Aを使ってボタンを押すたびにランダムな色にLEDが点灯するデバイスを作ってみました。
今は2026年なので、Geminiにコーディングを担当してもらいました。
プロジェクト概要
ATTiny13Aを使用し、3色LEDを「じわっと」光らせる3ビット(8通り)の電子サイコロ。単なる擬似乱数ではなく、ADCの浮動電圧(環境ノイズ)をエントロピーとして取り込み、M系列(LFSR)を撹拌することで高いランダム性を実現しています。
1. ハードウェア仕様
- MCU: ATTiny13A (1.2MHz 内蔵RCオシレータ)
- 出力: PB0, PB3, PB4 (3色LED / ソフトウェアPWM制御)
- 入力: PB1 (プッシュボタン / 内部プルアップ)
- エントロピー源: PB2 (浮動ピン / ADC入力)
- 電源: パワーダウンモードを活用し、待機電流を極限まで抑制。
2. ソフトウェアの特徴
① 物理ノイズによるエントロピー蓄積
setup_seed() 関数では、どこにも接続されていないPB2ピンの電位をADCで読み取ります。
-
飽和回避: ADC値が
0または1023(VCC/GNDに張り付き)の時は、有効なノイズが得られるまでサンプリングをリトライします。 -
ビット反転: 読み取った値の下位1ビットを用いて、既存のシード(
lfsr_state)をXORで反転させます。これにより、前回の状態を活かしつつ物理的な不確実性を累積させます。
② M系列(LFSR)擬似乱数
8ビットの線形帰還シフトレジスタ(多項式: $x^8 + x^6 + x^5 + x^4 + 1$)を採用。
- 計算負荷が極めて低く、1KBのメモリ制限下で効率的に乱数列を生成します。
- スリープ復帰ごとに物理ノイズで状態をかき混ぜるため、決定論的な周期性を打破しています。
③ 輝度補正付きソフトウェアPWM
3つのピンをソフトウェア制御することで、滑らかなフェードイン・フェードアウトを実現。
-
比視感度補正: 赤(R)に対して明るく見えやすい緑(G)と青(B)の最大輝度を係数(
SCALE_G,SCALE_B)で制限し、視覚的なバランスを整えています。
④ 鉄壁のチャタリング・誤動作対策
-
PCINT(ピン変化割り込み)の特性を考慮し、スリープ直前にGIFR(割り込みフラグ)を強制クリア。 - 「ボタンを離すまで待つ」+「ウェイト」の組み合わせにより、ボタン操作1回につき確実に1回だけ演出が実行されるよう制御。
3. ジャックポット演出
抽選結果が 000(通常は消灯)となった場合、特別な「虹色(カラーサイクリング)」演出を実行。RGBの各値を位相をずらして制御し、ゲーミングデバイスのような滑らかな色変化を提供します。
#define F_CPU 1200000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>
#define LED_MASK ((1 << PB4) | (1 << PB3) | (1 << PB0))
#define BUTTON_PIN PB1
// --- 輝度調整用パラメータ (0.0〜1.0 のイメージで調整) ---
// 赤(PB0)はそのまま、緑(PB3)と青(PB4)を絞る
#define SCALE_R 255 // 赤はフルパワー
#define SCALE_G 70 // 緑は少し控えめに (0-255で指定)
#define SCALE_B 140 // 青も控えめに (0-255で指定)
// M系列用の状態変数(0以外で初期化が必要)
uint8_t lfsr_state = 0x01;
uint8_t r_val, g_val, b_val;
uint8_t seed = 0;
int delay_ms=2;
// ADCを使って浮動ピン(PB2)からノイズを読み取りシードを作る
// ADCを使って浮動ピン(PB2)からノイズを読み取り、エントロピーを蓄積する
void setup_seed() {
ADMUX = (1 << MUX0); // ADC1 (PB2) 選択
ADCSRA = (1 << ADEN) | (1 << ADPS1) | (1 << ADPS0);
// 8ビット分(1バイト分)の有効なエントロピーが溜まるまで繰り返す
for (uint8_t i = 0; i < 8; i++) {
uint16_t val;
while (1) {
ADCSRA |= (1 << ADSC); // 変換開始
while (ADCSRA & (1 << ADSC)); // 完了待ち
val = ADC;
// 飽和チェック: 0(GND付近) または 1023(VCC付近) でなければ採用
if (val > 0 && val < 1023) {
break; // 有効なノイズが得られたのでループ脱出
}
// 飽和している場合は少し待ってからリトライ
_delay_us(100);
}
// ADCの下位1ビットが 1 の場合、現在のビットを反転(XOR)
if (val & 0x01) {
lfsr_state ^= (1 << i);
}
_delay_us(200); // サンプリング間隔を空けて独立性を高める
}
ADCSRA &= ~(1 << ADEN); // 終了後、省電力のためADC停止
}
// 8ビットM系列擬似乱数生成器
uint8_t get_lfsr_random() {
// 多項式: x^8 + x^6 + x^5 + x^4 + 1
uint8_t bit = ((lfsr_state >> 0) ^ (lfsr_state >> 2) ^ (lfsr_state >> 3) ^ (lfsr_state >> 4)) & 0x01;
lfsr_state = (lfsr_state >> 1) | (bit << 7);
return lfsr_state;
}
void soft_pwm_cycle() {
for (int i = 0; i < 255; i++) {
uint8_t port_bits = 0;
if (i < r_val) port_bits |= (1 << PB0);
if (i < g_val) port_bits |= (1 << PB3);
if (i < b_val) port_bits |= (1 << PB4);
PORTB = (PORTB & ~LED_MASK) | port_bits;
_delay_loop_1(delay_ms);
}
}
void fade_normal(uint8_t val) {
uint8_t m0 = (val & 0x01);
uint8_t m3 = (val & 0x02);
uint8_t m4 = (val & 0x04);
delay_ms=1;
for (int16_t i = 0; i <= 255; i += 2) {
// i(0-255)に対して各色の比率を掛けて上限を抑える
r_val = m0 ? ((uint16_t)i * SCALE_R / 255) : 0;
g_val = m3 ? ((uint16_t)i * SCALE_G / 255) : 0;
b_val = m4 ? ((uint16_t)i * SCALE_B / 255) : 0;
soft_pwm_cycle();
}
//_delay_ms(200);
for (int16_t i = 255; i >= 0; i -= 2) {
r_val = m0 ? ((uint16_t)i * SCALE_R / 255) : 0;
g_val = m3 ? ((uint16_t)i * SCALE_G / 255) : 0;
b_val = m4 ? ((uint16_t)i * SCALE_B / 255) : 0;
soft_pwm_cycle();
}
}
void jackpot_rainbow() {
r_val = 255; g_val = 0; b_val = 0;
delay_ms=1;
for (int step = 0; step < 6; step++) {
for (int i = 0; i < 255; i += 2) {
if (step == 0) g_val = i;
else if (step == 1) r_val = 255 - i;
else if (step == 2) b_val = i;
else if (step == 3) g_val = 255 - i;
else if (step == 4) r_val = i;
else if (step == 5) b_val = 255 - i;
soft_pwm_cycle();
//for (uint8_t r = 0; r < 2; r++) soft_pwm_cycle();
}
}
r_val = g_val = b_val = 0;
soft_pwm_cycle();
}
void go_to_sleep() {
GIMSK |= (1 << PCIE);
PCMSK |= (1 << PCINT1);
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
GIFR = (1 << PCIF);
sleep_enable();
_delay_ms(50); // チャタリング防止
sei();
sleep_cpu();
sleep_disable();
cli();
}
ISR(PCINT0_vect) {}
int main(void) {
DDRB |= LED_MASK;
DDRB &= ~(1 << BUTTON_PIN);
PORTB |= (1 << BUTTON_PIN);
while (lfsr_state == 0) {
setup_seed();
}
setup_seed(); // 起動時に一度だけ浮動電圧からシード生成
setup_seed();
setup_seed();
while (1) {
// --- 1. スリープに入る前にボタンが離れるのを待つ ---
// これを入れないと、離した瞬間の電圧変化でまた目覚めてしまいます
_delay_ms(50); // チャタリング防止
while (!(PINB & (1 << BUTTON_PIN))){
_delay_ms(50); // 離した直後のノイズも待つ
}
go_to_sleep();
while (!(PINB & (1 << BUTTON_PIN))){ // チャタリング・長押し対策
_delay_ms(50);
}
uint8_t rnd = get_lfsr_random(); // M系列から取得
uint8_t result = rnd & 0x07; // 下位3ビットを使用
if (result == 0) {
jackpot_rainbow();
} else {
fade_normal(result);
}
setup_seed();
}
}