diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b81a84 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +out/* diff --git a/23_akatonbo/README.md b/23_akatonbo/README.md new file mode 100644 index 0000000..a3ff22d --- /dev/null +++ b/23_akatonbo/README.md @@ -0,0 +1,65 @@ + +`22_buzzer/`を応用して、簡単なオルゴール赤とんぼを作成しました。この作例では、ブザー(圧電サウンダー)を使ってメロディを奏でる仕組みを使用しています。 + +## 内容 +- **ブザーによるメロディ再生**: 22_buzzerをベースに、赤とんぼを演奏します。 +- **サンプル提供**: 動作確認用の画像と動画を用意しました。 + +## 使用方法 + +ロータリエンコーダーを押し込んで、クリックしてください。演奏が開始され、画面に歌詞が表示されます。 + +## サンプル + +![サンプル画像](image.png) + +- **動画**: [サンプル動画](https://x.com/rdon_key/status/1932290594363379796) + + +## ハードウェアの接続 +- EX1と3V3(もしくはGND)をブザー(圧電サウンダー)に接続してください。 + +## ビルド方法 + +```bash +tinygo flash -target waveshare-rp2040-zero --size short . +``` + +- 必要に応じて、`-port`オプションを指定してください(例: `-port com5`)。 + +## やってみてね! +以下のチャレンジを試して、プロジェクトをさらに進化させてみましょう! + +1. **「赤とんぼ」の続きを実装しよう!** + 現在、「夕焼け小焼けの赤とんぼ 負われて見たのは何時の日か」まで実装済みです。以下の歌詞を追加で実装してみましょう! + +``` +山の畑の、桑の実を、小籠に、つんだは、まぼろしか。 +十五で、姐(ねえ)やは、嫁にゆき、 +お里の、たよりも、たえはてた。 +夕やけ、小やけの、赤とんぼ。 +とまっているよ、竿の先。 +``` + + **ヒント**: `main.go`の`func getSong()`に曲データがあります。この関数を編集してメロディを追加しよう! + +2. **他の曲を入れよう!** + 「赤とんぼ」以外の好きな曲に変更してみましょう! + + **ヒント**: 「ドレミ付き楽譜」で検索すると、入力しやすい楽譜がたくさん見つかります。`func getSong()`に新しい曲データを追加して試してみて! + +3. **ゴーファー君を画面に出して踊らせよう!** + ゴーファー君をディスプレイに表示して、動きをつけてみましょう! + + **ヒント**: 作例`09_oled_tinyfont`でゴーファー君を表示する方法を確認できます。アニメーションを追加して踊らせてみて! + +**挑戦したら、ぜひDiscordで成果を報告してね!** + +## 制作者 +- **あーるどん** ([@rdon_key](https://x.com/rdon_key)) + +何かご質問やフィードバックがあれば、[@rdon_key](https://x.com/rdon_key)までご連絡ください! +ネガティブ・フィードバックも歓迎します! + + + diff --git a/23_akatonbo/display.go b/23_akatonbo/display.go new file mode 100644 index 0000000..84f6e5b --- /dev/null +++ b/23_akatonbo/display.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "image/color" + "machine" + "time" + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ssd1306" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/shnm" +) + +// 画面レイアウト定数 +const ( + STATUS_Y = 12 // ステータス行のテキスト表示位置 + STATUS_AREA_END = 19 // ステータス行エリアの終端 + SEPARATOR_Y = 16 // 区切り線の位置 + SCROLL_START_Y = 30 // スクロール領域の開始位置 + SCROLL_CLEAR_Y = 20 // スクロール領域のクリア開始位置 + LINE_HEIGHT = 12 // 行の高さ + SCROLL_MAX_Y = 60 // スクロール領域の最大Y位置 + MAX_SCROLL_LINES = 3 // スクロール表示可能行数 +) + +var white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} +var black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} + +// Display 構造体 - 画面制御とスクロール管理 +type Display struct { + device *ssd1306.Device + currentLine int // 現在の表示行(自動スクロール用) + lines [MAX_SCROLL_LINES]string // 表示中の行を保存 +} + +// InitDisplay ディスプレイを初期化する関数 +func InitDisplay() *Display { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 2.8 * machine.MHz, + SDA: machine.GPIO12, + SCL: machine.GPIO13, + }) + + device := ssd1306.NewI2C(machine.I2C0) + device.Configure(ssd1306.Config{ + Address: 0x3C, + Width: 128, + Height: 64, + }) + + device.SetRotation(drivers.Rotation180) + device.ClearDisplay() + time.Sleep(50 * time.Millisecond) + + return &Display{ + device: &device, + currentLine: 0, + lines: [MAX_SCROLL_LINES]string{}, // 空文字列で初期化 + } +} + +// UpdateStatus ステータス行を更新する関数 +func (d *Display) UpdateStatus(status string) { + // ステータス行のエリアをクリア + for y := 0; y < STATUS_AREA_END; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + + // ステータス行に表示 + tinyfont.WriteLine(d.device, &shnm.Shnmk12, 0, STATUS_Y, status, white) + + // 区切り線を描画 + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), SEPARATOR_Y, white) + } + + d.device.Display() +} + +// ClearScrollArea ステータス行以外をクリアする関数 +func (d *Display) ClearScrollArea() { + for y := SCROLL_CLEAR_Y; y < 64; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + d.currentLine = 0 // 表示行をリセット + // 行バッファもクリア + for i := range d.lines { + d.lines[i] = "" + } + d.device.Display() +} + +// PrintLine 一行表示する関数(必要に応じて自動スクロール) +func (d *Display) PrintLine(message string) { + // デバッグ情報をコンソールに出力 + fmt.Printf("PrintLine: currentLine=%d, message='%s'\n", d.currentLine, message) + + // 表示可能行数を超えた場合は画面をスクロール + if d.currentLine >= MAX_SCROLL_LINES { + fmt.Println("スクロール実行中...") + d.scrollUp() + d.currentLine = MAX_SCROLL_LINES - 1 + fmt.Printf("スクロール後: currentLine=%d\n", d.currentLine) + } + + // 新しいメッセージを配列に保存 + d.lines[d.currentLine] = message + fmt.Printf("行[%d]に保存: '%s'\n", d.currentLine, message) + + // 画面を再描画(スクロール領域のみ) + d.redrawScrollArea() + d.currentLine++ + fmt.Printf("currentLine更新: %d\n", d.currentLine) +} + +// scrollUp 画面を1行上にスクロールする(内部関数) +func (d *Display) scrollUp() { + fmt.Println("scrollUp開始") + // デバッグ: スクロール前の状態を表示 + fmt.Print("スクロール前の行: ") + for i := 0; i < MAX_SCROLL_LINES; i++ { + fmt.Printf("[%d]='%s' ", i, d.lines[i]) + } + fmt.Println() + + // 行を1つずつ上に移動 + for i := 0; i < MAX_SCROLL_LINES-1; i++ { + d.lines[i] = d.lines[i+1] + } + // 最後の行をクリア + d.lines[MAX_SCROLL_LINES-1] = "" + + // デバッグ: スクロール後の状態を表示 + fmt.Print("スクロール後の行: ") + for i := 0; i < MAX_SCROLL_LINES; i++ { + fmt.Printf("[%d]='%s' ", i, d.lines[i]) + } + fmt.Println() +} + +// redrawScrollArea スクロール領域を再描画する(内部関数) +func (d *Display) redrawScrollArea() { + fmt.Println("redrawScrollArea開始") + + // スクロール領域をクリア + for y := SCROLL_CLEAR_Y; y < 64; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + + // 保存されている全ての行を再描画 + for i := 0; i < MAX_SCROLL_LINES; i++ { + if d.lines[i] != "" { + y := int16(i*LINE_HEIGHT + SCROLL_START_Y) + fmt.Printf("描画: 行[%d] Y=%d '%s'\n", i, y, d.lines[i]) + if y >= SCROLL_START_Y && y <= SCROLL_MAX_Y { + tinyfont.WriteLine(d.device, &shnm.Shnmk12, 0, y, d.lines[i], white) + } else { + fmt.Printf("描画範囲外: Y=%d (範囲: %d-%d)\n", y, SCROLL_START_Y, SCROLL_MAX_Y) + } + } + } + + d.device.Display() + fmt.Println("redrawScrollArea完了") +} + +// GetDevice デバイスへの直接アクセス(必要な場合) +func (d *Display) GetDevice() *ssd1306.Device { + return d.device +} diff --git a/23_akatonbo/image.png b/23_akatonbo/image.png new file mode 100644 index 0000000..232028e Binary files /dev/null and b/23_akatonbo/image.png differ diff --git a/23_akatonbo/main.go b/23_akatonbo/main.go new file mode 100644 index 0000000..ad64614 --- /dev/null +++ b/23_akatonbo/main.go @@ -0,0 +1,304 @@ +package main + +// Please connect a piezo buzzer to the 3V3 and EX01 pins on the back terminal. +// +// | EX01 | EX03 | 3V3 | SDA0 | 3V3 | 3V3 | | GROVE | +// | EX02 | EX04 | GND | SCL0 | GND | GND | - - | GND | 3V3 | SDA0 | SCL0 | + +import ( + "fmt" + "machine" + "time" + "tinygo.org/x/drivers/tone" +) + +// タクトスイッチのピン定義 +const BUTTON_PIN = machine.GPIO2 + +// ボタンの初期化 +func initButton() { + BUTTON_PIN.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +} + +// ボタンが押されたかチェック(チャタリング対策付き) +func isButtonPressed() bool { + if !BUTTON_PIN.Get() { // プルアップなので押されると false + time.Sleep(50 * time.Millisecond) // チャタリング対策 + if !BUTTON_PIN.Get() { // 再確認 + // ボタンが離されるまで待つ + for !BUTTON_PIN.Get() { + time.Sleep(10 * time.Millisecond) + } + time.Sleep(50 * time.Millisecond) // 離した後のチャタリング対策 + return true + } + } + return false +} + +type NoteWithDuration struct { + Note tone.Note + Duration time.Duration +} + +var pinToPWM = map[machine.Pin]tone.PWM{ + machine.GPIO14: machine.PWM7, // for EX01 +} + +// ブザーを初期化する関数 +func initBuzzer() (tone.Speaker, error) { + bzrPin := machine.GPIO14 + pwm := pinToPWM[bzrPin] + speaker, err := tone.New(pwm, bzrPin) + if err != nil { + return tone.Speaker{}, err + } + return speaker, nil +} + +func getSong() []interface{} { + const bpm = 100 + beat := time.Minute / bpm + eighth := beat / 2 + quarter := beat + dottedQuarter := beat * 3 / 2 + dottedHalf := beat * 3 + + return []interface{}{ + "夕焼け", + NoteWithDuration{tone.G4, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, dottedQuarter}, + NoteWithDuration{tone.D5, eighth}, + + "小焼けの", + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.C6, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, quarter}, + + "赤とんぼ", + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, quarter}, + NoteWithDuration{tone.D5, quarter}, + NoteWithDuration{tone.E5, dottedHalf}, + + "負われて", + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, dottedQuarter}, + NoteWithDuration{tone.A5, eighth}, + + "見たのは", + NoteWithDuration{tone.C6, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.E5, eighth}, + + "何時の日か", + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.D5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, dottedHalf}, + } +} + +// 音符名と周波数を取得する関数(全オクターブ対応) +func getNoteName(note tone.Note) string { + switch note { + // 3オクターブ + case tone.C3: + return "ド(C3) 131Hz" + case tone.CS3: + return "ド#(C#3) 139Hz" + case tone.D3: + return "レ(D3) 147Hz" + case tone.DS3: + return "レ#(D#3) 156Hz" + case tone.E3: + return "ミ(E3) 165Hz" + case tone.F3: + return "ファ(F3) 175Hz" + case tone.FS3: + return "ファ#(F#3) 185Hz" + case tone.G3: + return "ソ(G3) 196Hz" + case tone.GS3: + return "ソ#(G#3) 208Hz" + case tone.A3: + return "ラ(A3) 220Hz" + case tone.AS3: + return "ラ#(A#3) 233Hz" + case tone.B3: + return "シ(B3) 247Hz" + + // 4オクターブ + case tone.C4: + return "ド(C4) 262Hz" + case tone.CS4: + return "ド#(C#4) 277Hz" + case tone.D4: + return "レ(D4) 294Hz" + case tone.DS4: + return "レ#(D#4) 311Hz" + case tone.E4: + return "ミ(E4) 330Hz" + case tone.F4: + return "ファ(F4) 349Hz" + case tone.FS4: + return "ファ#(F#4) 370Hz" + case tone.G4: + return "ソ(G4) 392Hz" + case tone.GS4: + return "ソ#(G#4) 415Hz" + case tone.A4: + return "ラ(A4) 440Hz" + case tone.AS4: + return "ラ#(A#4) 466Hz" + case tone.B4: + return "シ(B4) 494Hz" + + // 5オクターブ + case tone.C5: + return "ド(C5) 523Hz" + case tone.CS5: + return "ド#(C#5) 554Hz" + case tone.D5: + return "レ(D5) 587Hz" + case tone.DS5: + return "レ#(D#5) 622Hz" + case tone.E5: + return "ミ(E5) 659Hz" + case tone.F5: + return "ファ(F5) 698Hz" + case tone.FS5: + return "ファ#(F#5) 740Hz" + case tone.G5: + return "ソ(G5) 784Hz" + case tone.GS5: + return "ソ#(G#5) 831Hz" + case tone.A5: + return "ラ(A5) 880Hz" + case tone.AS5: + return "ラ#(A#5) 932Hz" + case tone.B5: + return "シ(B5) 988Hz" + + // 6オクターブ + case tone.C6: + return "ド(C6) 1047Hz" + case tone.CS6: + return "ド#(C#6) 1109Hz" + case tone.D6: + return "レ(D6) 1175Hz" + case tone.DS6: + return "レ#(D#6) 1245Hz" + case tone.E6: + return "ミ(E6) 1319Hz" + case tone.F6: + return "ファ(F6) 1397Hz" + case tone.FS6: + return "ファ#(F#6) 1480Hz" + case tone.G6: + return "ソ(G6) 1568Hz" + case tone.GS6: + return "ソ#(G#6) 1661Hz" + case tone.A6: + return "ラ(A6) 1760Hz" + case tone.AS6: + return "ラ#(A#6) 1865Hz" + case tone.B6: + return "シ(B6) 1976Hz" + + default: + return "不明な音" + } +} + +// 楽曲を演奏する関数(画面表示付き) +func playSong(speaker tone.Speaker, song []interface{}, display *Display) { + noteIndex := 0 + for _, element := range song { + switch v := element.(type) { + case string: + display.UpdateStatus(v) + case NoteWithDuration: + noteIndex++ + noteName := getNoteName(v.Note) + display.PrintLine(fmt.Sprintf("%d: %s", noteIndex, noteName)) + + speaker.SetNote(v.Note) + time.Sleep(v.Duration) + speaker.Stop() + time.Sleep(10 * time.Millisecond) + } + } +} + +// デモ用のスクロール表示(削除) + +func main() { + fmt.Println("プログラム開始") + + // ディスプレイの初期化 + display := InitDisplay() + fmt.Println("ディスプレイ初期化完了") + display.PrintLine("ディスプレイ初期化完了") + + // ボタンの初期化 + initButton() + fmt.Println("ボタン初期化完了") + display.PrintLine("ボタン初期化完了") + + // ステータス行に「テスト音楽」を表示 + display.UpdateStatus("テスト音楽") + fmt.Println("ステータス行表示完了") + display.PrintLine("ステータス行表示完了") + + // ブザーの初期化 + speaker, err := initBuzzer() + if err != nil { + fmt.Println("failed to configure PWM") + display.PrintLine("PWM設定エラー") + return + } + fmt.Println("ブザー初期化完了") + display.PrintLine("ブザー初期化完了") + + // 楽曲データの取得 + song := getSong() + fmt.Println("楽曲データ取得完了") + display.PrintLine("楽曲データ取得完了") + + // 演奏回数カウンター + playCount := 0 + + // メインループ + display.PrintLine("ボタンを押す") + fmt.Println("ボタン待機中...") + display.UpdateStatus("待機中") + + for { + if isButtonPressed() { + playCount++ + fmt.Printf("ボタンが押されました - 演奏回数: %d\n", playCount) + display.PrintLine(fmt.Sprintf("演奏開始 (%d回目)", playCount)) + display.UpdateStatus("演奏中...") + + playSong(speaker, song, display) + + fmt.Printf("演奏完了 - %d回目\n", playCount) + display.PrintLine(fmt.Sprintf("演奏完了 (%d回目)", playCount)) + display.PrintLine("ボタンを押す") + display.UpdateStatus("待機中") + } + time.Sleep(100 * time.Millisecond) // CPU負荷軽減 + } +} diff --git a/Makefile b/Makefile index 022ce6f..18b2744 100644 --- a/Makefile +++ b/Makefile @@ -25,4 +25,5 @@ smoketest: tinygo build -o ./out/20_rotary_gopher.uf2 --target waveshare-rp2040-zero --size short ./20_rotary_gopher tinygo build -o ./out/21_midi2.uf2 --target waveshare-rp2040-zero --size short ./21_midi2 tinygo build -o ./out/22_buzzer.uf2 --target waveshare-rp2040-zero --size short ./22_buzzer + tinygo build -o ./out/23_akatonbo.uf2 --target waveshare-rp2040-zero --size short ./23_akatonbo tinygo build -o ./out/80_checker.uf2 --target waveshare-rp2040-zero --size short ./80_checker diff --git a/README.md b/README.md index 9528d15..5f3fdde 100644 --- a/README.md +++ b/README.md @@ -929,7 +929,8 @@ COM7 2E8A:0003 waveshare-rp2040-zero * https://x.com/Ryu_07_29/status/1847921967070163377 * [./19_redkey/](./19_redkey/) * [./20_rotary_gopher](./20_rotary_gopher/) -* [./21_midi2](./21_midi2//) +* [./21_midi2](./21_midi2/) +* [./23_akatonbo](./23_akatonbo/) * https://github.com/conejoninja/midikeeb * https://x.com/triring/status/1891448348818776323 * https://github.com/sago35/koebiten diff --git a/README_EN.md b/README_EN.md index d54c6d2..e35b580 100644 --- a/README_EN.md +++ b/README_EN.md @@ -864,6 +864,8 @@ If not recognized, disconnect the microcontroller from the PC and reconnect it. * https://x.com/Ryu_07_29/status/1847921967070163377 * [./19_redkey/](./19_redkey/) * [./20_rotary_gopher](./20_rotary_gopher/) +* [./21_midi2](./21_midi2/) +* [./23_akatonbo](./23_akatonbo/) * https://github.com/conejoninja/midikeeb * https://x.com/triring/status/1891448348818776323 * https://github.com/sago35/koebiten @@ -876,4 +878,4 @@ If not recognized, disconnect the microcontroller from the PC and reconnect it. I wrote a technical book "Learning TinyGo Embedded Development from the Basics" (released on 2022/11/12) using TinyGo 0.26 + Wio Terminal. Please check it along with this page. -* https://sago35.hatenablog.com/entry/2022/11/04/230919 \ No newline at end of file +* https://sago35.hatenablog.com/entry/2022/11/04/230919