SPRESENSE のマルチコアプログラミングで バーサライタ
ついにSPRESENSE にArduino IDEでのマルチコアプログラミング環境が誕生しました。
前回SPRESENSEを使用して製作したバーサライタ(POVディスプレイ)にマルチコアを導入してどれほどの高速動作が達成されるのか観てみました!
目次
マルチコアプログラミング
SPRESENSEは6つのCPUを搭載しております。Arduino IDE環境でのプログラミング詳細は以下の公式のドキュメントの記載の通りです。
MPライブラリというマルチコア用のライブラリが今回追加となりました。Mainコアと5つのSubコアの制御やコア間の通信を司ります。
コアごとにArduinoプログラムを用意して、コアを指定してコンパイルというなかなかの力技です(;^ω^)
構成の概要
3つのCPUを使用してバーサライタしてみました。
- Mainコア:フォトリフレクタで回転を検出して2つのSubコアに発光タイミングを送信
- Sub1, 2コア:Mainコアからのタイミングを受けてSPIテープLED Dotstarを発光
Arduino IDEコード
Mainコア
フォトリフレクタでバーサライタ装置のマーカを検出して割り込みで回転時間を測定してMP.Send関数で表示すべき画像のアドレスを各Subコアに渡しています。
コア間でメモリが共有されているので表示データのアドレスだけを渡しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
#include <MP.h> #include <SPI.h> #include "graphics.h" #define itrPin 19 int sub1 = 1; int sub2 = 2; int8_t id = 100; /* user-defined msgid */ int numRot = 0; int numDiv = 0, numDiv2 = 0; int stateDiv = 0; unsigned long rotTime, timeOld, timeNow; void setup() { //Serial.begin(115200); pinMode(16, OUTPUT); digitalWrite(16, LOW); pinMode(23, OUTPUT); digitalWrite(23, LOW); pinMode(11, OUTPUT); digitalWrite(11, LOW); pinMode(13, OUTPUT); digitalWrite(13, LOW); delay(1000); MP.begin(sub1); MP.begin(sub2); delay(1000); attachInterrupt(digitalPinToInterrupt(itrPin), RotCount, FALLING ); } void loop() { if(stateDiv == 1 && micros() - timeOld > rotTime / Div * (numDiv)){ stateDiv = 0; } if(stateDiv == 0 && micros() - timeOld < rotTime / Div * (numDiv + 1)){ stateDiv = 1; numDiv2 = numDiv+6; if(numDiv2 >= Div) numDiv2 -= Div; MP.Send(id, &pic[numRot][numDiv2][0], sub1); MP.Send(id, &pic[numRot][numDiv][1], sub2); numDiv++; if(numDiv >= Div ) numDiv = 0; } } void RotCount() { timeNow = micros(); rotTime = timeNow - timeOld; timeOld = timeNow; numRot++; if(numRot >= Frame ) numRot = 0; //Serial.println("int"); } |
LED表示画像データ graphics.h は画像やGIFからpythonで生成しています。画像の色データを極座標変換してLED用に配列にしています。またLED位置によって輝度を線型的に調整してバーサライタ表示時に明るさが均等になるようにしています。Divで1周当たりの画像分解能を指定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# -*- coding: utf-8 -*- import cv2 import os import math from PIL import Image #配列設定 Frame = 5 #抽出フレーム数指定 NUMPIXELS = 57 #LEDの数 29セル+28セル Div = 400 #1周の分割数 Bright = 40 #輝度 Led0Bright = 3 #中心LEDの輝度 [%] #ファイル作成 file = open('graphics.h', 'w') file.write('#define Frame ' + str(Frame) + '\n') file.write('#define NUMPIXELS ' + str(NUMPIXELS) + '\n') file.write('#define Div ' + str(Div) + '\n' + '\n') #file.write('#define Frame ' + str(Frame) + '\n' + '\n') file.write('uint32_t pic [Frame][Div][NUMPIXELS] = {' + '\n') # Gifファイルを読み込む # 参考 https://www.tech-tech.xyz/gif-divide.html gif_file_name = "file.gif" gif = cv2.VideoCapture(gif_file_name) #画像変換関数 def polarConv(pic, i): imgOrgin = cv2.imread(pic) #画像データ読み込み h, w, _ = imgOrgin.shape #画像サイズ取得 #画像縮小 imgRedu = cv2.resize(imgOrgin,(math.floor((NUMPIXELS * 2 -1)/h *w), NUMPIXELS * 2 -1)) #cv2.imwrite(str(i) + '-resize.jpg',imgRedu) #縮小画像中心座標 h2, w2, _ = imgRedu.shape wC = math.floor(w2 / 2) hC = math.floor(h2 / 2) #極座標変換画像準備 imgPolar = Image.new('RGB', (NUMPIXELS, Div)) #極座標変換 file.write('\t{\n') for j in range(0, Div): file.write('\t\t{') for i in range(0, hC+1): #座標色取得 #参考:http://peaceandhilightandpython.hatenablog.com/entry/2016/01/03/151320 rP = int(imgRedu[hC + math.ceil(i * math.cos(2*math.pi/Div*j)), wC - math.ceil(i * math.sin(2*math.pi/Div*j)), 2] * ((100 - Led0Bright) / NUMPIXELS * i + Led0Bright) / 100 * Bright /100) gP = int(imgRedu[hC + math.ceil(i * math.cos(2*math.pi/Div*j)), wC - math.ceil(i * math.sin(2*math.pi/Div*j)), 1] * ((100 - Led0Bright) / NUMPIXELS * i + Led0Bright) / 100 * Bright /100) bP = int(imgRedu[hC + math.ceil(i * math.cos(2*math.pi/Div*j)), wC - math.ceil(i * math.sin(2*math.pi/Div*j)), 0] * ((100 - Led0Bright) / NUMPIXELS * i + Led0Bright) / 100 * Bright /100) file.write('0x%02X%02X%02X' % (rP,gP,bP)) if i == hC: file.write('},\n') else: file.write(', ') imgPolar.putpixel((i,j), (rP, gP, bP)) file.write('\t},\n\n') # スクリーンキャプチャを保存するディレクトリを生成 dir_name = "screen_caps" if not os.path.exists(dir_name): os.mkdir(dir_name) for i in range(Frame): is_success, frame = gif.read() # 画像ファイルに書き出す img_name = str(i) + ".jpg" img_path = os.path.join(dir_name, img_name) cv2.imwrite(img_path, frame) #変換 polarConv(img_path, i) file.write('};' + '\n' + '\n') file.close() |
Sub1コア
Mainコアからの画像アドレスをMP.Recv関数で受けてLED発光させます。MP.RecvTimeout関数でデータ受信の待ちモードを指定しており、ここではMP_RECV_BLOCKINGでデータを受信するまで永久に受信待ちに入るモードにしています。
Adafruitが提供するライブラリのハードウェアSPI使用では拡張ボードのSPI4しか使用できないためAdafruit_DotStar.cpp と Adafruit_DotStar.h をメインボード用に改造して
Adafruit_DotStar_SPI5.h、Adafruit_DotStar_SPI5.cpp
をつくってSPI5でLEDテープを制御できるようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <MP.h> #include <SPI.h> #include "Adafruit_DotStar_SPI5.h" #define NUMPIXELS1 29 // Number of LEDs in strip #define Fclk 38000000 Adafruit_DotStar_SPI5 strip5 = Adafruit_DotStar_SPI5(NUMPIXELS1, DOTSTAR_BGR); void setup() { Serial.begin(115200); MP.begin(); strip5.begin(); SPI5.beginTransaction(SPISettings(Fclk, MSBFIRST, SPI_MODE0)); strip5.clear(); strip5.show(); /* Polling */ MP.RecvTimeout(MP_RECV_BLOCKING ); } void loop() { int8_t id; uint32_t rsvdata; MP.Recv(&id, &rsvdata); strip5.clear(); for(int i=0;i<NUMPIXELS1; i++){ strip5.setPixelColor(i, pgm_read_dword(rsvdata + sizeof(uint32_t) * 2 *i)); } strip5.show(); } |
Sub2コア
Mainコアからの画像アドレスをMP.Recv関数で受けてSPI4でLEDテープを発光させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <MP.h> #include <SPI.h> #include <Adafruit_DotStar.h> #define NUMPIXELS2 28 // Number of LEDs in strip #define Fclk 38000000 Adafruit_DotStar strip4 = Adafruit_DotStar(NUMPIXELS2, DOTSTAR_BGR); void setup() { Serial.begin(115200); MP.begin(); strip4.begin(); SPI.beginTransaction(SPISettings(Fclk, MSBFIRST, SPI_MODE0)); strip4.clear(); strip4.show(); /* Polling */ MP.RecvTimeout(MP_RECV_BLOCKING ); } void loop() { int8_t id; uint32_t rsvdata; MP.Recv(&id, &rsvdata); strip4.clear(); for(int i=0;i<NUMPIXELS2; i++){ strip4.setPixelColor(i, pgm_read_dword(rsvdata + sizeof(uint32_t) * 2 *i)); } strip4.show(); } |
動作
1コアから3コアにすることで1周での画像分解能が120から400にまで向上しました!わーい!!
性能指数:LED 57個 × 回転速度 730rpm × 400分解能 ÷ 100 = 166440 hPOV
3コアで1周400分解能達成。すっごいハンマカンマ#SPRESENSE #バーサライタ #Sony #ザコシGIF pic.twitter.com/j3Vk4dRuer
— HomeMadeGarbage (@H0meMadeGarbage) 2019年6月6日
ちなみに性能指数とはバーサライタの性能を示す独自の指標です。
性能指数 [hPOV(ヘクトポブ)] = LED数 × 回転数 × 一周の分解能 ÷ 100
次は今回のアップデートでフラッシュメモリへの読み書きのライブラリも追加されたので長尺の動画表示にトライしたいです。
マルチコアへの道
シングルコア
性能指数:LED 58個 × 回転速度 730rpm × 120分解能 ÷ 100 = 50808 hPOV
ザコシはソニー芸人なのでSPRESENSEとの親和性高い #SPRESENSE #バーサライタ #Sony #ザコシGIF pic.twitter.com/B3v8BuYJox
— HomeMadeGarbage (@H0meMadeGarbage) 2019年5月21日
デュアルコア
性能指数:LED 58個 × 回転速度 730rpm × 220分解能 ÷ 100 = 93148 hPOV
本日ついにSPRESENSE Arduino IDEアップデートでマルチコア対応実現。早速2コアで1周の分解能が120→220に向上を確認!!93148 hPOV達成!
Mainコア:回転検出、SPI5でLED発光
Sub1コア:SPI4でLED発光
3コアにして試してみよっと♪マジでうれしい。#SPRESENSE #バーサライタ #POVディスプレイ pic.twitter.com/67QjxnnOyo— HomeMadeGarbage (@H0meMadeGarbage) 2019年6月5日
その後の話
フラッシュ仕様検討
分解能が上がったことによって表示データが大きくなり、現状ですと5フレームしか書き込めない状態です。
SPRESENSEのコンパイルされたコードはフラッシュメモリにインストールされ、実行されるときにSRAMに展開されて実行されます。
SRAMは1.5MBのうちのMineコアが768kB、Subコアが128kB割り当てられます。
表示データに使用できる容量はSRAMサイズを超えられないのです。。。
そこで8MBもあるフラッシュを有効に使用できないか検討しました。
Fileライブラリを使用するとフラッシュにファイル形式でデータを保存できます。
そこで表示データ(LED57個 × 分解能400の32ビットデータ)をテキストデータとしてフラッシュに保存して、再度読み込んで配列にする実験をしました。
テキストデータを1文字づつ読んで32ビット分で文字列にして16進数に変換して配列化しました。
1フレーム分の配列を読み込むのに3分20秒もかかりました。。。。
マルチコアで読み込んで配列生成しながら、長尺の動画を表示したかったのですが時間がかかり過ぎのため断念しました。。。
参考
装置構成の変更
2本のLEDテープの配置を2本平行ではなく、1直線に並べました。回転の際の表示のずれを抑えるためです。
1列にして2本のLEDのずれが小さくなって良かった。しかしすでに4枚羽の十字にしたくてしかたない。キリないですわ。#SPRESENSE #バーサライタ #Sony #ザコシGIF #POVディスプレイ pic.twitter.com/DtW8Mm77le
— HomeMadeGarbage (@H0meMadeGarbage) 2019年6月10日
ずれが軽減され精度が増しました。でももっとやりたくなっちゃう。。キリがないww
シールド対策#SPRESENSE #POVディスプレイ #バーサライタ pic.twitter.com/UIU2cIXf5f
— HomeMadeGarbage (@H0meMadeGarbage) 2019年6月11日
さいごに
今回のSPRESENSEバージョンアップによってバーサライタの性能が大きく向上しました。
いつかフラッシュメモリの使用も気軽にできるようになるかと思います。今回のバージョンアップのように。
祈りを込めて。。。
そして バーサライタ装置をステージ上にそっと置き
地を引きずりながら漂う煙のようにその場を後にした。
しかし その表情はどこか誇らしげであったという