この記事ははJLCPCBの提供でお送りします。
JLCPCBとは
jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)
JLCPCBとは、プリント基板製造などで有名な香港の企業です。
日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。
値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。
この記事の作例もJLCPCBに基板を発注して実現しました。
Raspberry Pi Picoを使った携帯ゲーム機とは?
文字通り、Raspberry Pi Picoを搭載した携帯ゲーム機です。 1枚の基板で色々なことが試せるように工夫しました。
- ディプレイは以下から1つ
- I2C接続OLED
- SPI接続OLED
- SPI接続カラーTFT(128px*128px)
- スピーカー
- 3.5mmオーディオジャック端子
- ボリューム
- 電源は以下から1つ
- 単4電池2つ
- LiPOバッテリー(充電モジュールは既製品を想定)
- MicroSDカードソケット
- 十字ボタン、A,Bボタン
- 電源用スライドスイッチ
- Raspberry Pi Picoのすべての端子を引き出した拡張端子
タクトスイッチは6mm角の表面実装の物、コンデンサは0603(1608M)を使っています
複数のディスプレイ、複数の電源への対応
まぁ単純にフットプリントを重ねて配置しただけです。どれか1つしか使わない想定なので、フットプリント自体が重なることは問題がないため、スルーホール穴が重ならないように配置しました。
電源も、乾電池ホルダーに重ねてLiPoバッテリーの充電モジュールを配置し、どちらか一方を利用できるようにしました。
発注
今回は基板の色は黒で発注しました。JLCPCBの黒の基板はツヤ消しマットのような質感で、高級感があり良いものだと思いました。
基板サイズは10cm*10cm以内なので、JLCPCBの最安料金で発注できました。 (1注文当たり5枚の基板が1つだけ割引になるため、1注文なら5枚で$2+送料)
今回の失敗
今回も凡ミスをしてしまいました・・
Raspberry Pi PicoのGNDが他のGNDと繋がっておらず、電源を入れても、Raspberry Pi Picoに電源が供給されない状態でした。
まぁ、修正は簡単で、適当なGNDの来ているピンからRaspberry Pi PicoのGNDピンにジャンパ線を取り付けて修正しました。
毎回ミスはありますね・・
単4電池2つでRaspberry Pi Pico?!
Raspberry Pi Picoは3.3V駆動なので単4電池2つでは少し電圧が足りないと思って、DC-DCコンバータを別途取り付けようと思っていたのですが、よく仕様を見るとRaspberry Pi PicoにはDC-DCコンバータが搭載されており、1.8~5.5Vを印加すると、いい感じに3.3Vにしてくれるようで、外部部品が不要であることに気づきました。
MicroPythonでのプログラミング
とりあえず I2C接続OLED、単4電池2つの構成で組み立てました。
Raspberry Pi Picoを使ったゲーム機の動作テスト
— ina_ani@3歳児のパパ (@ina_ani) 2023年6月26日
MicroPythonでもここまで速度が出る
基板は #JLCPCB で作った。
ブログ記事、もうすぐ公開します。https://t.co/lf2W6eXcL8 pic.twitter.com/aTmKjxbhCj
動作確認がてらMicroPythonでプログラミングしてみました。 以下のソースコードで、キー入力を取得し、画面に文字を表示し、音を鳴らすことができます。
""" Raspberry Pi Pico Sequencer test Copyright (c) 2023 inajob This software is released under the MIT License. http://opensource.org/licenses/mit-license.php """ from machine import Pin, I2C, PWM, Timer import ssd1306 import time buzzer = PWM(Pin(9)) freqLis = [262, 294, 330, 349, 392, 440, 494, 523] idx = 0 i2c = I2C(0, sda=Pin(4), scl=Pin(5) , freq=400000) BTNA = Pin(15, Pin.IN, Pin.PULL_UP) BTNB = Pin(14, Pin.IN, Pin.PULL_UP) BTNU = Pin(19, Pin.IN, Pin.PULL_UP) BTND = Pin(17, Pin.IN, Pin.PULL_UP) BTNL = Pin(18, Pin.IN, Pin.PULL_UP) BTNR = Pin(16, Pin.IN, Pin.PULL_UP) display = ssd1306.SSD1306_I2C(128, 64, i2c) count = 0 pos = [0,0] MAX_STEP = int(128/4) MAX_TONE = int(64/4) data = [[False] * MAX_TONE for _ in range(MAX_STEP)] playPos = 0; def draw(): display.fill_rect(0,0,128,64,0) for step in range(128/4): for tone in range(64/4): if data[step][tone]: display.fill_rect(step*4, tone*4, 4, 4, 1) display.fill_rect(pos[0]*4, pos[1]*4, 4, 4, 1) display.fill_rect(playPos*4, 0, 1, 64, 1) def tick(timer): global idx global count global pos global playPos draw() count = count + 1 if count > 2: count = 0 playPos = playPos + 1 if playPos >= 8: playPos = 0 display.show() if(BTNA.value() == 0): data[pos[0]][pos[1]] = True if(BTNB.value() == 0): data[pos[0]][pos[1]] = False if(BTNU.value() == 0): pos[1] = pos[1] - 1 if pos[1] < 0: pos[1] = 0 if(BTND.value() == 0): pos[1] = pos[1] + 1 if(BTNL.value() == 0): pos[0] = pos[0] - 1 if pos[0] < 0: pos[0] = 0 if(BTNR.value() == 0): pos[0] = pos[0] + 1 f = False for i in range(MAX_TONE): if data[playPos][i]: buzzer.freq(freqLis[i]) buzzer.duty_u16(256*16) f = True if not(f): buzzer.duty_u16(0) tim = Timer() tim.init(period=100, mode=Timer.PERIODIC, callback=tick)
MicroPythonの開発にはThony( https://thonny.org/ )を利用しました。
MicroPythonはコンパイルすることなく、パソコン上のIDEからプログラムを転送し、実行できるので、Arduinoなどに比べるとトライ&エラーがやりやすいと感じました。
Arduinoでのプログラミング
MicroPythonで作曲ソフトを作りましたが、これでは和音に対応できません。 ということでArduinoを使って和音も利用できる作曲ソフトを作ってみました。
和音が鳴らしたかったのでArduinoにも挑戦 pic.twitter.com/dXihQI4i1f
— ina_ani@3歳児のパパ (@ina_ani) 2023年6月26日
OLEDの制御はArduino UNOでよく利用しているu8g2というライブラリがそのまま動いたので、簡単でした。
ソースコードは雑ですが、、こんな感じです。
/** Raspberry Pi Pico Sequencer test Copyright (c) 2023 inajob This software is released under the MIT License. http://opensource.org/licenses/mit-license.php */ #define NOTE_B0 31 #define NOTE_C1 33 #define NOTE_CS1 35 #define NOTE_D1 37 #define NOTE_DS1 39 #define NOTE_E1 41 #define NOTE_F1 44 #define NOTE_FS1 46 #define NOTE_G1 49 #define NOTE_GS1 52 #define NOTE_A1 55 #define NOTE_AS1 58 #define NOTE_B1 62 #define NOTE_C2 65 #define NOTE_CS2 69 #define NOTE_D2 73 #define NOTE_DS2 78 #define NOTE_E2 82 #define NOTE_F2 87 #define NOTE_FS2 93 #define NOTE_G2 98 #define NOTE_GS2 104 #define NOTE_A2 110 #define NOTE_AS2 117 #define NOTE_B2 123 #define NOTE_C3 131 #define NOTE_CS3 139 #define NOTE_D3 147 #define NOTE_DS3 156 #define NOTE_E3 165 #define NOTE_F3 175 #define NOTE_FS3 185 #define NOTE_G3 196 #define NOTE_GS3 208 #define NOTE_A3 220 #define NOTE_AS3 233 #define NOTE_B3 247 #define NOTE_C4 262 #define NOTE_CS4 277 #define NOTE_D4 294 #define NOTE_DS4 311 #define NOTE_E4 330 #define NOTE_F4 349 #define NOTE_FS4 370 #define NOTE_G4 392 #define NOTE_GS4 415 #define NOTE_A4 440 #define NOTE_AS4 466 #define NOTE_B4 494 #define NOTE_C5 523 #define NOTE_CS5 554 #define NOTE_D5 587 #define NOTE_DS5 622 #define NOTE_E5 659 #define NOTE_F5 698 #define NOTE_FS5 740 #define NOTE_G5 784 #define NOTE_GS5 831 #define NOTE_A5 880 #define NOTE_AS5 932 #define NOTE_B5 988 #define NOTE_C6 1047 #define NOTE_CS6 1109 #define NOTE_D6 1175 #define NOTE_DS6 1245 #define NOTE_E6 1319 #define NOTE_F6 1397 #define NOTE_FS6 1480 #define NOTE_G6 1568 #define NOTE_GS6 1661 #define NOTE_A6 1760 #define NOTE_AS6 1865 #define NOTE_B6 1976 #define NOTE_C7 2093 #define NOTE_CS7 2217 #define NOTE_D7 2349 #define NOTE_DS7 2489 #define NOTE_E7 2637 #define NOTE_F7 2794 #define NOTE_FS7 2960 #define NOTE_G7 3136 #define NOTE_GS7 3322 #define NOTE_A7 3520 #define NOTE_AS7 3729 #define NOTE_B7 3951 #define NOTE_C8 4186 #define NOTE_CS8 4435 #define NOTE_D8 4699 #define NOTE_DS8 4978 int tones[] = { NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4, NOTE_C5, NOTE_D5, NOTE_E5, NOTE_F5, NOTE_G5, NOTE_A5, NOTE_B5, NOTE_C6, NOTE_D6, NOTE_E6, NOTE_F6, NOTE_G6, NOTE_A6, NOTE_B6 }; #include "hardware/pwm.h" #include <U8g2lib.h> U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); #define PWM_AUDIO_L (9) //#define PWM_AUDIO_R (28) #define RANDOMBIT (*((uint *)(ROSC_BASE + ROSC_RANDOMBIT_OFFSET)) & 1) #define PWM_RANGE_BITS (10) #define PWM_RANGE (1<<PWM_RANGE_BITS) #define NUM_PSG (4) #define PSG_VOL_MAX (PWM_RANGE / NUM_PSG - 1) #define SAMPLE_RATE (125000000 / PWM_RANGE) #define OMEGA_UNIT (FIXED_1_0 / SAMPLE_RATE) #define FIXED_0_5 (0x40000000) #define FIXED_1_0 (0x7fffffff) typedef uint32_t fixed; // 1.0=0x7fffffff, 0.0=0x0 enum psg_type {OSC_SQUARE, OSC_SAW, OSC_TRI, OSC_NOISE}; struct psg_t { volatile fixed phi; // 0..FIXED_1_0 fixed step; // 0..FIXED_1_0 volatile int sound_vol; // 0..PSG_VOL_MAX enum psg_type type; }; static struct psg_t psg[NUM_PSG]; void psg_freq(int i, float freq) { assert(i < NUM_PSG); psg[i].step = freq * OMEGA_UNIT; } void psg_vol(int i, int value) { assert(i < NUM_PSG); if (value < 0) { value = 0; } psg[i].sound_vol = value % (PSG_VOL_MAX + 1); } void psg_type(int i, enum psg_type type) { assert(i < NUM_PSG); psg[i].type = type; } static inline uint psg_value(int i) { assert(i < NUM_PSG); uint result; if (psg[i].type == OSC_SQUARE) { result = (psg[i].phi > FIXED_0_5) ? psg[i].sound_vol : 0; } else if (psg[i].type == OSC_SAW) { result = ((psg[i].phi >> (31 - PWM_RANGE_BITS)) * psg[i].sound_vol) >> PWM_RANGE_BITS; } else if (psg[i].type == OSC_TRI) { result = ((psg[i].phi >> (30 - PWM_RANGE_BITS)) * psg[i].sound_vol) >> PWM_RANGE_BITS; result = (result < psg[i].sound_vol) ? result : psg[i].sound_vol * 2 - result; } else { // OSC_NOISE result = RANDOMBIT * psg[i].sound_vol; } return result; } static inline void psg_next() { for(int i = 0; i < NUM_PSG; i++) { psg[i].phi += psg[i].step; if (psg[i].phi > FIXED_1_0) { psg[i].phi -= FIXED_1_0; } } } void on_pwm_wrap() { uint sum = 0; pwm_clear_irq(pwm_gpio_to_slice_num(PWM_AUDIO_L)); #ifdef PWM_AUDIO_R pwm_clear_irq(pwm_gpio_to_slice_num(PWM_AUDIO_R)); #endif psg_next(); for(int i = 0; i < NUM_PSG; i++) { sum += psg_value(i); } pwm_set_gpio_level(PWM_AUDIO_L, sum); #ifdef PWM_AUDIO_R pwm_set_gpio_level(PWM_AUDIO_R, sum); #endif } void psg_pwm_config() { gpio_set_function(PWM_AUDIO_L, GPIO_FUNC_PWM); #ifdef PWM_AUDIO_R gpio_set_function(PWM_AUDIO_R, GPIO_FUNC_PWM); #endif uint slice_num = pwm_gpio_to_slice_num(PWM_AUDIO_L); pwm_clear_irq(slice_num); pwm_set_irq_enabled(slice_num, true); irq_set_exclusive_handler(PWM_IRQ_WRAP, on_pwm_wrap); irq_set_enabled(PWM_IRQ_WRAP, true); pwm_config config = pwm_get_default_config(); pwm_config_set_clkdiv_int(&config, 1); pwm_config_set_wrap(&config, PWM_RANGE); pwm_init(slice_num, &config, true); #ifdef PWM_AUDIO_R slice_num = pwm_gpio_to_slice_num(PWM_AUDIO_R); pwm_init(slice_num, &config, true); #endif } void psg_init() { for(int i = 0; i < NUM_PSG; i++) { psg[i].phi = 0; psg[i].step = 0; psg[i].sound_vol = PSG_VOL_MAX / 4; psg[i].type = OSC_SQUARE; } psg_pwm_config(); } void psg_all_vol(int value) { for(int i = 0 ; i < NUM_PSG; i++) { psg_vol(i, value); } } int channel = 0; int cursorX = 0; int cursorY = 0; int pos = 0; #define SIZE 16 bool scoreData[SIZE][32]; int trigger[6]; void setup() { psg_init(); psg_type(0, OSC_SQUARE); psg_type(1, OSC_SQUARE); psg_all_vol(0); psg_vol(channel, PSG_VOL_MAX / 4); psg_vol(1, PSG_VOL_MAX / 4); psg_freq(channel, 220); psg_freq(1, 220); pinMode(14, INPUT_PULLUP); pinMode(15, INPUT_PULLUP); pinMode(16, INPUT_PULLUP); pinMode(17, INPUT_PULLUP); pinMode(18, INPUT_PULLUP); pinMode(19, INPUT_PULLUP); u8g2.begin(); u8g2.setFont(u8g2_font_ncenB08_tr); for(int i = 0; i < 128/SIZE; i ++){ for(int j = 0; j < 32; j ++){ scoreData[i][j] = false; } } for(int i = 0; i < 6; i ++){ trigger[i] = 0; } } void draw(){ u8g2.drawLine(pos*128/SIZE, 0, pos*128/SIZE, 63); for(int i = 0; i < SIZE; i ++){ for(int j = 0; j < 32; j ++){ if(scoreData[i][j]){ u8g2.drawBox(i*128/SIZE, j*2, 128/SIZE, 2); } } } u8g2.drawBox(cursorX * 128/SIZE, cursorY*2, 128/SIZE, 2); } int count = 0; void loop() { count ++; if(count > 3){ count = 0; pos ++; if(pos > SIZE - 1){pos = 0;} for(int i = 0; i < NUM_PSG; i ++){ psg_freq(i, 0); psg_vol(i, 0); } int freeChan = 0; for(int j = 0; j < 32; j ++){ if(scoreData[pos][j]){ psg_vol(freeChan, PSG_VOL_MAX); psg_freq(freeChan, tones[j]); freeChan ++; if(freeChan >= NUM_PSG){ freeChan = 0; } } } } if(!digitalRead(16)){trigger[0] ++;}else{trigger[0] = -1;} if(!digitalRead(17)){trigger[1] ++;}else{trigger[1] = -1;} if(!digitalRead(18)){trigger[2] ++;}else{trigger[2] = -1;} if(!digitalRead(19)){trigger[3] ++;}else{trigger[3] = -1;} if(!digitalRead(15)){trigger[4] ++;}else{trigger[4] = -1;} if(!digitalRead(14)){trigger[5] ++;}else{trigger[5] = -1;} if(trigger[0] == 3){cursorX ++;} if(trigger[1] == 3){cursorY ++;} if(trigger[2] == 3){cursorX --;} if(trigger[3] == 3){cursorY --;} if(trigger[4] == 3){scoreData[cursorX][cursorY] = true;} if(trigger[5] == 3){scoreData[cursorX][cursorY] = false;} for(int i = 0; i < 6; i ++){ if(trigger[i] == -1)trigger[i] = 0; } if(cursorX < 0)cursorX = 0; if(cursorY < 0)cursorY = 0; if(cursorX > SIZE - 1)cursorX = SIZE - 1; if(cursorY > 64/2 - 1)cursorY = 64/2 - 1; sleep_ms(3); u8g2.clearBuffer(); draw(); u8g2.sendBuffer(); }
参考にしたもの
以下のRP2040を使ったゲーム機の回路を参考にしており、SPIのOLEDを搭載した場合は、同じように動作するように設計しました(試していないですが・・)
和音を使った演奏については以下の記事を参考にしました
まとめ
この手のゲーム機といえば、Arduboy( https://www.arduboy.com/ )が有名で、自分も本物を持っていますし、互換機を作ったこともあります、それに比べるとこのRaspberry Pi Picoを使ったゲーム機は、性能がケタ違いに高いものとなっており、様々なゲームを動かすポテンシャルがあります。
また、半導体不足の影響もあり、おそらくRaspberry Pi Picoを使ったこのゲーム機の方が安上がりで作れそうです。
まぁ、Arduboyほどのスタイリッシュなデザインにするのは、かなり大変で、Arduboyの価値はそちらの方にあると思うのですが、自作ゲーム機界隈も日進月歩だなと感じています。
最後に、この基板が数枚余っていますので、もしほしい人がいたら連絡ください。 (可能なら、ボツ基板交換的なノリで、何か自作の面白い基板と交換してもらえると嬉しいです。 私が提供できるボツ基板はこちらにまとめています。)