2018年11月27日火曜日

Nucleo DCO 一旦できたことにする。

筐体を作りました。

今回は枠組みに奮発してヒノキを使いました。スギでもいいんですが、ホムセンで売ってるスギだと板厚が12mmが最薄で、ヒノキは9mmからあったのでヒノキにしました。


910x9x90mmで400円でした。組み上げは木工用ボンドだけにしました。

天板も奮発してアルミ板にしました。200mmx300mmx1mmで513円でした。アルミ板の切断はこの動画を参考にしました。


上下に当てる板はホムセンの端材(大きくても100円もしない)を買ってきて、実際やってみるとペコリときれいに切断できました。カッターもこの際奮発してでかいサイズのH型を購入して使いました。



丸穴の穿孔はドリルで一発ですが、OLEDの表示窓の角穴はまたもや妥協しました(^q^;



量産することがあればその時は、専門の業者さんに頼もうと思います。


パーツをあてがって木工用ボンドで張り合わせて、パンツのゴムで圧をかけたのですが、アルミ板の寸法が少し大きかったようで背面のハリのパーツが接着できませんでした。


裏側のハリのパーツだけクランプで圧をかけて再度接着。板材を傷めないためにはクランプの間に当て板を入れたほうがいいと思いますが、クランプの広げる幅が限界だったので養生テープのみの保護となりました。

基板の配置(底板

操作パネル(天板)

操作パネル(天板裏側

正直空中配線は地獄です。老眼で対象物にピントが合わないので(老眼鏡やルーペを駆使してますが)ほぼほぼ勘にたよってはんだ付けするしかありません。




配線

ケーブルの長さはマチマチですが、なんとか結合できました。


メモ:


入出力端子をつける裏板を製作。←プラだとベコベコするので薄いベニア板?

ヒノキの木枠を塗装する。

メンテしやすい範囲で配線材の長さを揃える。

天板につけてる木ネジは皿ネジはおさまりが悪いので、普通の丸頭のタイプがあるかな?

天板のアルミが傷だらけ。研磨する?

2018年11月24日土曜日

Nucleo DCO 追加の作り物


筐体を製作中ですが、補助用の基板をもう少し作る必要が出てきました。

ブロック図

1) Master Freq Mix


Master FrequencyはPOTによる分圧値をADCに入れていますが、これを(MIDI-CVコンバーターを使ったりして)音階を与えられるようにするものです。アクティブ・ミキサーとして製作するつもりです。

2) 3.3V Divider


3.3V電源は、Nucleo Boardのものを使っていますが、NucleoのZIO Headerには3.3V電源が1Pinしか出ていません。最低、POTとOLEDに使うので電源を分配できるようにするものです。

3) Current Limit


LED用の電流制限抵抗です。テスト中は下の写真のようにケーブルとQIピンの間に抵抗をはんだ付けして入れていましたが、ついでなのでこれも基板で実装しようと思います。


2018年11月16日金曜日

Nucleo DCOの筐体の構想

前回のNucleo F446RE→Arduino Pro MiniはI2C通信はうまくいったようにみえたのですが、マスター側をNucleo F767ZIにしたらダメでした。

I2Cの通信波形は、ちゃんとしてると思うんですが、Arduino側で拾えてないようです。

いずれNucleo F767ZIを予備で入手できたときは追試したいと思います。

ということで、ArduinoでOLEDを制御するプランは保留して、現状でFIXして筐体を作る方向で考えます。

厚紙で配置位置を確認


図面を書いて、厚紙に貼り付けてドリルで穴あけして破綻がないか確認しました。


部品取り付け

200mm × 200mm で収まりそうです。

筐体の高さ

単純にパネルを上下に重ねて置きました。最低60mmは高さが必要です。

2018年11月12日月曜日

Nucleo(mbed)からI2Cで通信してArduinoでOLEDに表示させてみる(その2)

配線は前回と同じです。主にファームウェアを検討しました。


Nucleo DCOはNucleoF767ZIをコアとして開発していますが、(貧乏で何枚もF767ZIを買う余裕がないため)テストはNucleo F446REで行いました。

IC間通信 データーフォーマット

https://github.com/ryood/Nucleo_DCO/blob/master/Nucleo-Arduino_I2C_Test/Arduino/Nucleo_DCO_I2C_Slave_OLED_print_FullBuffer_Test/DataComFormat.h

/*
* Nucleo DCO IC間通信 データーフォーマット
*
* 2018.11.11
*
*/
#ifndef _DATA_COM_FORMAT_H_
#define _DATA_COM_FORMAT_H_
#define OSC_NUM (3)
// display mode
enum {
DM_TITLE = 0,
DM_NORMAL = 1,
DM_FREQUENCY = 2,
DM_AMPLITUDE = 3,
DM_PULSE_WIDTH = 4,
DM_TITLE_STR1 = 128,
DM_TITLE_STR2 = 129,
DM_TITLE_STR3 = 130,
DM_DISPLAY_OFF = 255
};
// I2C通信用構造体
// DM_NORMAL
#pragma pack(1)
struct normalData {
uint8_t waveShape[OSC_NUM];
uint8_t frequencyRange[OSC_NUM];
uint16_t fps;
uint8_t batteryVoltage;
bool adcAvailable;
};
// DM_FREQUENCY
#pragma pack(1)
struct frequencyData {
uint16_t rate[OSC_NUM];
int16_t detune[OSC_NUM];
};
// DM_AMPLITUDE
#pragma pack(1)
struct amplitudeData {
int16_t amplitude[OSC_NUM];
int16_t masterAmplitude;
uint8_t clip;
};
// DM_PULSE_WIDTH
#pragma pack(1)
struct pulseWidthData {
int16_t pulseWidth[OSC_NUM];
};
#endif //_DATA_COM_FORMAT_H_
view raw DataComFormat.h hosted with ❤ by GitHub
I2Cではバイト単位の送受信を行うので、送受信するデータをまとめるために構造体を定義しました。
#pragma pack(1)
とすると構造体のアラインメントが1Byte単位になります。

I2Cマスター Nucleo F446RE(mbed)


https://github.com/ryood/Nucleo_DCO/tree/master/Nucleo-Arduino_I2C_Test/mbed/I2C_Master_Arduino_OLED_Test01

/*
* Nucleo DCO I2C Slave / OLED Module Test.
*
* 2018.11.05
*
*/
#include "mbed.h"
#include "DataComFormat.h"
#define UART_TRACE (0)
#define I2C_CLOCK (400000)
#define I2C_ARDUINO_ADDR (0x08 << 1) // 8bit address
#define TITLE_STR1 ("I2C OLED Test")
#define TITLE_STR2 (__DATE__)
#define TITLE_STR3 (__TIME__)
#define DEBOUNCE_DELAY (50000) // usec
int displayMode = DM_TITLE;
I2C I2cArduino(PB_9, PB_8); // SDA, SCL
InterruptIn UserButton(PC_13);
DigitalOut CheckPin1(PA_10);
Timeout debouncer;
#if (UART_TRACE)
Serial pc(USBTX, USBRX);
#endif
// parameter
volatile int waveShape[OSC_NUM];
volatile int frequencyRange[OSC_NUM];
volatile int fps;
volatile int batteryVoltage;
volatile bool adcAvailable;
volatile double drate[OSC_NUM];
volatile double detune[OSC_NUM];
volatile float amplitude[OSC_NUM];
volatile float masterAmplitude;
volatile bool isClip;
volatile int pulseWidth[OSC_NUM];
volatile bool isDirty = true;
int x = 0;
//-------------------------------------------------------------------------------------------------
// I2C 通信
//
void displayTitle()
{
#if (UART_TRACE)
pc.printf("displayTitle()\r\n");
#endif
const int len = 32;
char strBuffer[len];
// Title文字列を送信
strncpy(strBuffer, TITLE_STR1, len);
printf("%s %d %d\r\n", strBuffer, len, strlen(strBuffer));
uint8_t mode = DM_TITLE_STR1;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, strBuffer, len, false) != 0) {
printf("%d I2C failure: TitleStr1\r\n", x);
}
strncpy(strBuffer, TITLE_STR2, len);
printf("%s %d %d\r\n", strBuffer, len, strlen(strBuffer));
mode = DM_TITLE_STR2;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, strBuffer, len, false) != 0) {
printf("%d I2C failure: TitleStr2\r\n", x);
}
strncpy(strBuffer, TITLE_STR3, len);
printf("%s %d %d\r\n", strBuffer, len, strlen(strBuffer));
mode = DM_TITLE_STR3;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, strBuffer, len, false) != 0) {
printf("%d I2C failure: TitleStr3\r\n", x);
}
wait_ms(1);
// Title表示指示を送信
mode = DM_TITLE;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, false) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
}
void displayNormal()
{
#if (UART_TRACE)
pc.printf("displayNormal()\r\n");
#endif
struct normalData d;
d.waveShape[0] = waveShape[0];
d.waveShape[1] = waveShape[1];
d.waveShape[2] = waveShape[2];
d.frequencyRange[0] = frequencyRange[0];
d.frequencyRange[1] = frequencyRange[1];
d.frequencyRange[2] = frequencyRange[2];
d.fps = fps;
d.batteryVoltage = batteryVoltage;
d.adcAvailable = adcAvailable;
#if (UART_TRACE)
pc.printf("WaveShape1: %d\r\n", d.waveShape[0]);
pc.printf("WaveShape2: %d\r\n", d.waveShape[1]);
pc.printf("WaveShape3: %d\r\n", d.waveShape[2]);
pc.printf("FreqRange1: %d\r\n", d.frequencyRange[0]);
pc.printf("FreqRange2: %d\r\n", d.frequencyRange[1]);
pc.printf("FreqRange3: %d\r\n", d.frequencyRange[2]);
pc.printf("FPS: %d\r\n", d.fps);
pc.printf("BattVoltage: %d\r\n", d.batteryVoltage);
pc.printf("ADCAvailable: %d\r\n", d.adcAvailable);
#endif
uint8_t mode = DM_NORMAL;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&d, sizeof(d), false) != 0) {
printf("%d I2C failure: normalData\r\n", x);
}
}
void displayFrequency()
{
#if (UART_TRACE)
pc.printf("displayFrequency()\r\n");
#endif
struct frequencyData d;
d.rate[0] = drate[0] * 10;
d.rate[1] = drate[1] * 10;
d.rate[2] = drate[2] * 10;
d.detune[0] = detune[0] * 1000;
d.detune[1] = detune[1] * 1000;
d.detune[2] = detune[2] * 1000;
#if (UART_TRACE)
pc.printf("Rate1: %d\r\n", d.rate[0]);
pc.printf("Rate2: %d\r\n", d.rate[1]);
pc.printf("Rate3: %d\r\n", d.rate[2]);
pc.printf("Detune1: %d\r\n", d.detune[0]);
pc.printf("Detune2: %d\r\n", d.detune[1]);
pc.printf("Detune3: %d\r\n", d.detune[2]);
#endif
uint8_t mode = DM_FREQUENCY;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&d, sizeof(d), false) != 0) {
printf("%d I2C failure: frequencyData\r\n", x);
}
}
void displayAmplitude()
{
#if (UART_TRACE)
pc.printf("displayAmplitude()\r\n");
#endif
struct amplitudeData d;
d.amplitude[0] = amplitude[0] * 1000;
d.amplitude[1] = amplitude[1] * 1000;
d.amplitude[2] = amplitude[2] * 1000;
d.masterAmplitude = masterAmplitude * 1000;
d.clip = isClip;
#if (UART_TRACE)
pc.printf("Amplitude1: %d\r\n", d.amplitude[0]);
pc.printf("Amplitude2: %d\r\n", d.amplitude[1]);
pc.printf("Amplitude3: %d\r\n", d.amplitude[2]);
pc.printf("MasterAmplitude: %d\r\n", d.masterAmplitude);
pc.printf("Clip: %d\r\n", d.clip);
#endif
uint8_t mode = DM_AMPLITUDE;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&d, sizeof(d), false) != 0) {
printf("%d I2C failure: amplitudeData\r\n", x);
}
}
void displayPulseWidth()
{
#if (UART_TRACE)
pc.printf("displayPulseWidth()\r\n");
#endif
struct pulseWidthData d;
d.pulseWidth[0] = ((float)pulseWidth[0] / UINT16_MAX) * 1000;
d.pulseWidth[1] = ((float)pulseWidth[1] / UINT16_MAX) * 1000;
d.pulseWidth[2] = ((float)pulseWidth[2] / UINT16_MAX) * 1000;
#if (UART_TRACE)
pc.printf("PulseWidth1: %d\r\n", d.pulseWidth[0]);
pc.printf("PulseWidth2: %d\r\n", d.pulseWidth[1]);
pc.printf("PulseWidth2: %d\r\n", d.pulseWidth[2]);
#endif
uint8_t mode = DM_PULSE_WIDTH;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&d, sizeof(d), false) != 0) {
printf("%d I2C failure: pulseWidthData\r\n", x);
}
}
void displayOff()
{
#if (UART_TRACE)
pc.printf("displayOff()\r\n");
#endif
uint8_t mode = DM_DISPLAY_OFF;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&mode, 1, true) != 0) {
printf("%d I2C failure: mode %d\r\n", x, mode);
}
uint8_t message = false;
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&message, 1, false) != 0) {
printf("%d I2C failure: displayOff message\r\n", x);
}
}
//-------------------------------------------------------------------------------------------------
// 入力処理
//
void changeDisplayMode()
{
if (UserButton.read() == 0) {
displayMode++;
if (displayMode > DM_DISPLAY_OFF) {
displayMode = 0;
}
else if (displayMode > DM_PULSE_WIDTH) {
displayMode = DM_DISPLAY_OFF;
}
isDirty = true;
}
}
void debounce()
{
debouncer.attach_us(&changeDisplayMode, DEBOUNCE_DELAY);
}
//-------------------------------------------------------------------------------------------------
// Main loop
//
int main()
{
#if (UART_TRACE)
pc.baud(115200);
pc.printf("\r\n%s\r\n", TITLE_STR1);
pc.printf("%s\r\n", TITLE_STR2);
pc.printf("%s\r\n", TITLE_STR3);
wait(1.0);
#endif
I2cArduino.frequency(I2C_CLOCK);
UserButton.fall(&debounce);
// initialize parameter (dummy)
waveShape[0] = 1;
waveShape[1] = 2;
waveShape[2] = 3;
frequencyRange[0] = 4;
frequencyRange[1] = 5;
frequencyRange[2] = 6;
fps = 600; // 60.0fps
batteryVoltage = 90; // 9.0V
adcAvailable = true;
drate[0] = 440.1;
drate[1] = 880.2;
drate[2] = 1320.3;
detune[0] = 0.0;
detune[1] = -0.543;
detune[2] = 0.543;
amplitude[0] = 0.123f;
amplitude[1] = 0.456f;
amplitude[2] = 0.567f;
masterAmplitude = 1.000f;
isClip = true;
pulseWidth[0] = 0;
pulseWidth[1] = 32768;
pulseWidth[2] = 65536;
while(1) {
if (isDirty) {
CheckPin1.write(1);
switch (displayMode) {
case DM_TITLE:
displayTitle();
break;
case DM_NORMAL:
displayNormal();
break;
case DM_FREQUENCY:
displayFrequency();
break;
case DM_AMPLITUDE:
displayAmplitude();
break;
case DM_PULSE_WIDTH:
displayPulseWidth();
break;
case DM_DISPLAY_OFF:
displayOff();
break;
}
//isDirty = false;
wait_ms(1);
CheckPin1.write(0);
x++;
}
}
}
view raw main.cpp hosted with ❤ by GitHub

マスター側のNucleo(mbed)のプログラムは、ダミーデータを送信するものです。

ボードに付いている「User Button」を押すと、「display mode」が切り替わり、スレーブに送るデータが切り替わります。

I2C通信の仕方は、1Byte目に1Byteの「display mode」を送信し、それ以降のデータのフォーマットを識別できるようにしました。

困った点。


「void displayTitle()」は「プログラム名」、「コンパイル日付」、「コンパイル時刻」の3つの固定長文字列(32byte)を送信します。ここは、I2Cの流儀に従って、

[I2C Address][display mode(DM_TITLE)][プログラム名(32Byte)][コンパイル日付(32Byte)][コンパイル時刻(32Byte)][STOP]
と、まとめて送信したかったのですが、スレーブのArduinoから冒頭の「I2C Address」の時点でACKが返ってきません。

[I2C Address][display mode(DM_TITLE)][プログラム名(32Byte)][コンパイル日付(32Byte)][STOP]

と、文字列2個なら正常に通信できました。原因はわかりません。

しかたがないので、

[I2C Address][display mode(DM_TITLE_STR1)][プログラム名(32Byte)][STOP]
[I2C Address][display mode(DM_TITLE_STR2)][コンパイル日付(32Byte)][STOP]
[I2C Address][display mode(DM_TITLE_STR3)][コンパイル時刻(32Byte)][STOP]
wait_ms(1);
[I2C Address][display mode(DM_TITLE)][STOP]

と分離して送信するようにしました。また、途中に「wait_ms(1)」を入れないとかえって通信速度が遅くなります。Arduino側のI2Cのバッファサイズかなにかが影響しているのかもしれません。

I2Cスレーブ Arduino Pro mini 8MHz/3.3V 中華製

https://github.com/ryood/Nucleo_DCO/tree/master/Nucleo-Arduino_I2C_Test/Arduino/Nucleo_DCO_I2C_Slave_OLED_print_FullBuffer_Test

/*
* Nucleo DCO I2C Slave / OLED Module Test.
* use u8g2::print()
*
* 2018.11.11
*
*/
#include <Wire.h>
#include <U8g2lib.h>
#include "DataComFormat.h"
#define UART_TRACE (0) // UART_TRACEを有効化(1)にすると、メモリーオーバーのためコンパイルできません。
#define TITLE_STR1 ("I2C Slave Test")
#define TITLE_STR2 ("20181111")
#define I2C_ADDR (0x08)
#define I2C_CLOCK (400000)
#define MASTER_TITLE_STR_LEN (32)
//U8G2_SSD1306_128X32_UNIVISION_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // Adafruit ESP8266/32u4/ARM Boards + FeatherWing OLED
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);
const int CheckPin1 = 2;
// Const strings
const char* waveShapeName[] = {
"SIN",
"TRI",
"SWU",
"SWD",
"SQR",
"NOS",
"XXX"
};
const char* frequencyRangeName[] = {
"A-1",
" A0",
" A1",
" A2",
" A3",
" A4",
" A5",
" A6",
" A7",
" A8"
};
char masterTitleStr1[MASTER_TITLE_STR_LEN] = "TitleStr1";
char masterTitleStr2[MASTER_TITLE_STR_LEN] = "TitleStr2";
char masterTitleStr3[MASTER_TITLE_STR_LEN] = "TitleStr3";
volatile int displayMode = DM_TITLE;
volatile struct normalData normalData;
volatile struct frequencyData frequencyData;
volatile struct amplitudeData amplitudeData;
volatile struct pulseWidthData pulseWidthData;
volatile bool displayOffMessage;
volatile bool isDirty = true;
void setup()
{
#if (UART_TRACE)
Serial.begin(9600); // start serial for output
Serial.println();
Serial.println(TITLE_STR1);
Serial.println(TITLE_STR2);
#endif
pinMode(CheckPin1, OUTPUT);
// OLED
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13_tf);
u8g2.setCursor(0, 10);
u8g2.print(TITLE_STR1);
u8g2.setCursor(0, 25);
u8g2.print(TITLE_STR2);
u8g2.sendBuffer();
// I2C
Wire.begin(I2C_ADDR); // join i2c bus with address #8
pinMode(A4, INPUT); // disable pullup
pinMode(A5, INPUT); // disable pullup
Wire.setClock(I2C_CLOCK);
Wire.onReceive(receiveEvent); // register event
delay(2000);
}
//-------------------------------------------------------------------------------------------------
// OLED Display
//
void displayTitle()
{
#if (UART_TRACE)
Serial.println("displayTitle()");
#endif
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_mf);
u8g2.setCursor(0, 16);
u8g2.print(masterTitleStr1);
u8g2.setCursor(0, 32);
u8g2.print(masterTitleStr2);
u8g2.setCursor(0, 48);
u8g2.print(masterTitleStr3);
u8g2.sendBuffer();
}
void displayNormal()
{
#if (UART_TRACE)
Serial.println("displayNormal()");
#endif
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_mf);
u8g2.setCursor(4, 16);
u8g2.print(waveShapeName[normalData.waveShape[0]]);
u8g2.print(' ');
u8g2.print(waveShapeName[normalData.waveShape[1]]);
u8g2.print(' ');
u8g2.print(waveShapeName[normalData.waveShape[2]]);
u8g2.setCursor(4, 32);
u8g2.print(frequencyRangeName[normalData.frequencyRange[0]]);
u8g2.print(' ');
u8g2.print(frequencyRangeName[normalData.frequencyRange[1]]);
u8g2.print(' ');
u8g2.print(frequencyRangeName[normalData.frequencyRange[2]]);
u8g2.setCursor(4, 48);
u8g2.print("FPS:");
u8g2.print((float)normalData.fps/10, 1);
u8g2.setCursor(4, 64);
u8g2.print("BAT:");
u8g2.print((float)normalData.batteryVoltage/10, 1);
u8g2.print("V ");
u8g2.print(normalData.adcAvailable ? "xx" : "AD");
u8g2.sendBuffer();
}
void displayFrequency()
{
#if (UART_TRACE)
Serial.println("displayFrequency()");
#endif
u8g2.clearBuffer();
u8g2.setCursor(0, 16);
u8g2.print("F1:");
u8g2.print((float)frequencyData.rate[0]/10, 1);
u8g2.print(" Hz");
u8g2.setCursor(0, 32);
u8g2.print("F2:");
u8g2.print((float)frequencyData.rate[1]/10, 1);
u8g2.print(" Hz");
u8g2.setCursor(0, 48);
u8g2.print("F3:");
u8g2.print((float)frequencyData.rate[2]/10, 1);
u8g2.print(" Hz");
u8g2.setCursor(0, 64);
u8g2.print((float)frequencyData.detune[1]/1000, 3);
u8g2.print(' ');
u8g2.print((float)frequencyData.detune[2]/1000, 3);
u8g2.sendBuffer();
}
void displayAmplitude()
{
#if (UART_TRACE)
Serial.println("displayAmplitude()");
#endif
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_mf);
u8g2.setCursor(0, 16);
u8g2.print("AMP1: ");
u8g2.print((float)amplitudeData.amplitude[0]/1000, 3);
u8g2.setCursor(0, 32);
u8g2.print("AMP2: ");
u8g2.print((float)amplitudeData.amplitude[1]/1000, 3);
u8g2.setCursor(0, 48);
u8g2.print("AMP3: ");
u8g2.print((float)amplitudeData.amplitude[2]/1000, 3);
u8g2.setCursor(0, 64);
u8g2.print("MAMP: ");
u8g2.print((float)amplitudeData.masterAmplitude/1000, 3);
u8g2.sendBuffer();
}
void displayPulseWidth()
{
#if (UART_TRACE)
Serial.println("displayPulseWidth()");
#endif
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_mf);
u8g2.setCursor(0, 16);
u8g2.print("PW1: ");
u8g2.print((float)pulseWidthData.pulseWidth[0]/1000, 3);
u8g2.setCursor(0, 32);
u8g2.print("PW2: ");
u8g2.print((float)pulseWidthData.pulseWidth[1]/1000, 3);
u8g2.setCursor(0, 48);
u8g2.print("PW3: ");
u8g2.print((float)pulseWidthData.pulseWidth[2]/1000, 3);
u8g2.sendBuffer();
}
void displayOff()
{
#if (UART_TRACE)
Serial.println("displayOff()");
#endif
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_mf);
u8g2.setCursor(4, 24);
u8g2.print("DISPLAY OFF");
u8g2.sendBuffer();
}
//-------------------------------------------------------------------------------------------------
// Main loop
//
void loop()
{
if (isDirty) {
isDirty = false;
switch (displayMode) {
case DM_TITLE:
displayTitle();
break;
case DM_NORMAL:
displayNormal();
break;
case DM_FREQUENCY:
displayFrequency();
break;
case DM_AMPLITUDE:
displayAmplitude();
break;
case DM_PULSE_WIDTH:
displayPulseWidth();
break;
case DM_DISPLAY_OFF:
displayOff();
break;
}
}
#if (UART_TRACE)
switch (displayMode) {
case DM_TITLE:
Serial.println("******* Display Title *********");
Serial.println(masterTitleStr1);
Serial.println(masterTitleStr2);
Serial.println(masterTitleStr3);
Serial.println("*******************************");
break;
case DM_NORMAL:
Serial.println("*********** Normal ************");
Serial.print("WaveShpe1: "); Serial.println(normalData.waveShape[0]);
Serial.print("WaveShpe2: "); Serial.println(normalData.waveShape[1]);
Serial.print("WaveShpe3: "); Serial.println(normalData.waveShape[2]);
Serial.print("FreqRange1: "); Serial.println(normalData.frequencyRange[0]);
Serial.print("FreqRange2: "); Serial.println(normalData.frequencyRange[1]);
Serial.print("FreqRange3: "); Serial.println(normalData.frequencyRange[2]);
Serial.print("Fps: "); Serial.println(normalData.fps);
Serial.print("Fps: "); Serial.print(normalData.fps/10); Serial.print("."); Serial.println(normalData.fps%10);
Serial.print("BattVoltage: "); Serial.println(normalData.batteryVoltage);
Serial.print("BattVoltage: "); Serial.print(normalData.batteryVoltage/10); Serial.print("."); Serial.println(normalData.batteryVoltage%10);
Serial.print("ADCAvilable: "); Serial.println(normalData.adcAvailable);
Serial.println("*******************************");
break;
case DM_FREQUENCY:
Serial.println("********** Frequency **********");
Serial.print("Rate1: "); Serial.println(frequencyData.rate[0]);
Serial.print("Rate1: "); Serial.print(frequencyData.rate[0]/10); Serial.print("."); Serial.println(frequencyData.rate[0]%10);
Serial.print("Rate2: "); Serial.println(frequencyData.rate[1]);
Serial.print("Rate2: "); Serial.print(frequencyData.rate[1]/10); Serial.print("."); Serial.println(frequencyData.rate[1]%10);
Serial.print("Rate3: "); Serial.println(frequencyData.rate[2]);
Serial.print("Rate3: "); Serial.print(frequencyData.rate[2]/10); Serial.print("."); Serial.println(frequencyData.rate[2]%10);
Serial.print("Detune1: "); Serial.println(frequencyData.detune[0]);
Serial.print("Detune1: "); Serial.print(frequencyData.detune[0]/1000); Serial.print("."); Serial.println(frequencyData.detune[0]%1000);
Serial.print("Detune2: "); Serial.println(frequencyData.detune[1]);
//Serial.print("Detune2: "); Serial.print(frequencyData.detune[1]/1000); Serial.print("."); Serial.println(frequencyData.detune[1]%1000);
Serial.print("Detune2: "); Serial.println((float)(frequencyData.detune[1])/1000.0f, 3);
Serial.print("Detune3: "); Serial.println(frequencyData.detune[2]);
Serial.print("Detune3: "); Serial.print(frequencyData.detune[2]/1000); Serial.print("."); Serial.println(frequencyData.detune[2]%1000);
Serial.println("*******************************");
break;
case DM_AMPLITUDE:
Serial.println("********** Amplitude **********");
Serial.print("Amplitude1: "); Serial.println(amplitudeData.amplitude[0]);
Serial.print("Amplitude1: "); Serial.println((float)(amplitudeData.amplitude[0])/1000.0f, 3);
Serial.print("Amplitude2: "); Serial.println(amplitudeData.amplitude[1]);
Serial.print("Amplitude2: "); Serial.println((float)(amplitudeData.amplitude[1])/1000.0f, 3);
Serial.print("Amplitude3: "); Serial.println(amplitudeData.amplitude[2]);
Serial.print("Amplitude3: "); Serial.println((float)(amplitudeData.amplitude[2])/1000.0f, 3);
Serial.print("MasterAmplitude: "); Serial.println(amplitudeData.masterAmplitude);
Serial.print("MasterAmplitude: "); Serial.println((float)(amplitudeData.masterAmplitude)/1000.0f, 3);
Serial.print("Clip: "); Serial.println(amplitudeData.clip);
Serial.println("*******************************");
break;
case DM_PULSE_WIDTH:
Serial.println("********* Pulse Width *********");
Serial.print("PulseWidth1: "); Serial.println(pulseWidthData.pulseWidth[0]);
Serial.print("PulseWidth1: "); Serial.println((float)(pulseWidthData.pulseWidth[0])/1000.0f, 3);
Serial.print("PulseWidth2: "); Serial.println(pulseWidthData.pulseWidth[1]);
Serial.print("PulseWidth2: "); Serial.println((float)(pulseWidthData.pulseWidth[1])/1000.0f, 3);
Serial.print("PulseWidth3: "); Serial.println(pulseWidthData.pulseWidth[2]);
Serial.print("PulseWidth3: "); Serial.println((float)(pulseWidthData.pulseWidth[2])/1000.0f, 3);
Serial.println("*******************************");
break;
case DM_DISPLAY_OFF:
Serial.println("********* Display Off *********");
Serial.print("DisplayOffMessage: "); Serial.println(displayOffMessage);
Serial.println("*******************************");
}
delay(1000);
#endif
}
//-------------------------------------------------------------------------------------------------
// I2C Event
//
void receiveEvent(int byteN)
{
#if (UART_TRACE)
Serial.println("receiveEvent()");
#endif
displayMode = Wire.read();
#if (UART_TRACE)
Serial.print("displayMode: ");
Serial.println(displayMode);
#endif
digitalWrite(CheckPin1, HIGH);
uint8_t* p;
switch (displayMode) {
case DM_TITLE:
break;
case DM_TITLE_STR1:
for (int i = 0; i < MASTER_TITLE_STR_LEN; i++) {
masterTitleStr1[i] = Wire.read();
}
break;
case DM_TITLE_STR2:
for (int i = 0; i < MASTER_TITLE_STR_LEN; i++) {
masterTitleStr2[i] = Wire.read();
}
break;
case DM_TITLE_STR3:
for (int i = 0; i < MASTER_TITLE_STR_LEN; i++) {
masterTitleStr3[i] = Wire.read();
}
break;
case DM_NORMAL:
p = (uint8_t *)&normalData;
for (int i = 0; i < sizeof(struct normalData); i++) {
p[i] = Wire.read();
}
case DM_FREQUENCY:
p = (uint8_t *)&frequencyData;
for (int i = 0; i < sizeof(struct frequencyData); i++) {
p[i] = Wire.read();
}
break;
case DM_AMPLITUDE:
p = (uint8_t *)&amplitudeData;
for (int i = 0; i < sizeof(struct amplitudeData); i++) {
p[i] = Wire.read();
}
break;
case DM_PULSE_WIDTH:
p = (uint8_t *)&pulseWidthData;
for (int i = 0; i < sizeof(struct pulseWidthData); i++) {
p[i] = Wire.read();
}
break;
case DM_DISPLAY_OFF:
displayOffMessage = Wire.read();
break;
}
isDirty = true;
digitalWrite(CheckPin1, LOW);
}
最終的にu8g2のフルバッファで表示できました。u8g2のページバッファは消費RAM量が少なくて済むのですが、OLEDの表示がダラダラした感じになるので見栄えのためにはフルバッファがいいと思います。

u8g2のフルバッファを使うとRAMを1024バイト消費するようです。ATMega328pのArduinoボードは2048バイトしか使えるRAMがなく、デバッグ用に使っているSerialでPCにUART通信する文字列の容量が大きく、「UART_TRACE」を有効にするとメモリーオーバーしてコンパイルできません。

Arduino IDEのメッセージは以下の通りです。

#define UART_TRACE (0)
最大30720バイトのフラッシュメモリのうち、スケッチが17392バイト(56%)を使っています。
最大2048バイトのRAMのうち、グローバル変数が1866バイト(91%)を使っていて、ローカル変数で182バイト使うことができます。
スケッチが使用できるメモリが少なくなっています。動作が不安定になる可能性があります。
#define UART_TRACE (1)
最大30720バイトのフラッシュメモリのうち、スケッチが21210バイト(69%)を使っています。
最大2048バイトのRAMのうち、グローバル変数が2689バイト(131%)を使っていて、ローカル変数で-641バイト使うことができます。
スケッチが使用するメモリが足りません。メモリを節約する方法については、以下のURLのページを参照してください。http://www.arduino.cc/en/Guide/Troubleshooting#size
ボードArduino Pro or Pro Miniに対するコンパイル時にエラーが発生しました。
スケッチで、
Serial.print("xxxx");
としているところを
Serial.print(F("xxxx"));
と「F()」マクロを使えば「"xxxx"」の文字列がRAMではなく、Flash Memoryに置かれてRAMの消費量が節約できるそうですが、試していません。

困った点というか助かった点。


Arduinoの「sprintf()」は浮動小数点が使えません。u8g2には「print()」メンバ関数が用意されていて、これを使えば「Serial.print()」と同じ様に
u8g2.print(<浮動小数点数>, <小数点以下の桁数>)
として浮動小数点数が表示できました。

タイミングの測定


ch1:Arduino(D2) ch2:I2C_SCL

ch1はArduinoのスケッチで、I2C割り込みがかかったとき(receiveEvent()が呼ばれる)、処理の最初と最後でH/Lしています。

これを見ると、I2C通信が終わったあとにI2C割り込みがかかっているようです。

やはりI2Cは難解です(T_T;

2018年11月9日金曜日

Nucleo(mbed)からI2Cで通信してArduinoでOLEDに表示させてみる。

mbedのSSD1306のライブラリはいくつかありますが、フォントが貧弱で、きれいな文字を表示しようと思うとやはりUoogやu8g2を使いたくなります。

U8g2はArduinoでは使いやすいですが、mbedでは少々やっかいです。

そこで、U8g2を使うためだけにArduinoを一個用意するという贅沢なことを思いつきました。

Nucleo本体は波形生成に専念し、パラメーターに変更があった場合にOLED表示用のAruduinoにI2C経由でデーターを投げるというイメージです。

I2Cは仕様が複雑でほんとは使いたくないんですが、AVRにもSTM32にも専用のハードウェアが用意されているのでパフォーマンスを考えると使わざるを得ないと思います。なにせ信号線2本で双方向通信できますから。

SSD1306 OLEDも、やりくりすればI2Cタイプでもかまわないのですが(Nucleo - Arduino間の通信と信号線を共用)、速度と手数を減らすためにSPIタイプのものを使いました。

配線図

I2Cマスター側 Nucleo(mbed)のプログラムは前回と同じです。

https://os.mbed.com/users/ryood/code/Nucleo_i2c_master_writer/

I2Cスレーブ側 Arduino Pro mini (3.3V/8MHz駆動)

/*
* Nucleo DCO I2C Slave / OLED Module Test.
*
* 2018.11.07
*
*/
#include <Wire.h>
#include <U8g2lib.h>
#define TITLE_STR1 ("I2C Slave Test")
#define TITLE_STR2 ("20181107")
#define I2C_ADDR (0x08)
#define I2C_CLOCK (400000)
//U8G2_SSD1306_128X32_UNIVISION_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // Adafruit ESP8266/32u4/ARM Boards + FeatherWing OLED
U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);
void setup()
{
Serial.begin(9600); // start serial for output
Serial.println();
Serial.println(TITLE_STR1);
Serial.println(TITLE_STR2);
// OLED
u8g2.begin();
u8g2.clearBuffer();
u8g2.firstPage();
u8g2.setFont(u8g2_font_8x13_tf);
do {
u8g2.drawStr(0,10,TITLE_STR1);
u8g2.drawStr(0,25,TITLE_STR2);
} while (u8g2.nextPage());
u8g2.sendBuffer();
// I2C
Wire.begin(8); // join i2c bus with address #8
pinMode(A4, INPUT); // disable pullup
pinMode(A5, INPUT); // disable pullup
Wire.setClock(I2C_CLOCK);
Wire.onReceive(receiveEvent); // register event
delay(2000);
}
void loop()
{
delay(100);
}
void receiveEvent(int howMany)
{
char buff1[20];
char buff2[30];
int i = 0;
while (1 < Wire.available()) {
char c = Wire.read();
Serial.print(c);
if (i < 18) {
buff1[i] = c;
i++;
}
buff1[i] = '\0';
}
int x = Wire.read();
Serial.println(x);
sprintf(buff2, "%s%d", buff1, x);
u8g2.firstPage();
u8g2.setFont(u8g2_font_10x20_mf);
do {
u8g2.drawStr(0,32, buff2);
} while (u8g2.nextPage());
}

I2C経由で、マスター側が生成したデータをスレーブ側のOLEDで表示することができました。

I2Cクロック=400kHzの場合

ch1:Nucleo(D2) ch2:SCL

ch1はNucleo(マスター)側でI2C通信の開始時、終了時でH/Lしています。

I2C通信に要した時間: 220u秒~230u秒程度。

I2Cクロック=100kHzの場合

ch1:Nucleo(D2) ch2:SCL

I2C通信に要した時間: 675u秒

I2Cクロックは、Nucleo(マスター)側で変更すれば、Arduino(スレーブ)側は400kHzの設定のままで大丈夫でした。

2018年11月4日日曜日

ArduinoとNucleo(mbed)でI2C通信してみる。

Nucleo(mbed)をマスター、ArduinoをスレーブとしてI2C通信してみました。

配線図


スレーブ側は中華製のArduino Pro Mini(3.3V/8MHz版)でPCとUART通信するために、秋月の「FT232RL USBシリアル変換モジュール」をつないでいます

Nucleo(マスター側)
https://os.mbed.com/users/ryood/code/Nucleo_i2c_master_writer/
#include "mbed.h"
#define I2C_ARDUINO_ADDR (0x08 << 1) // 8bit address
I2C I2cArduino(PB_9, PB_8); // SDA, SCL
int main()
{
I2cArduino.frequency(400000);
uint8_t x = 0;
while(1) {
if (I2cArduino.write(I2C_ARDUINO_ADDR, "x is ", 5, true) != 0) {
printf("I2C failure");
}
if (I2cArduino.write(I2C_ARDUINO_ADDR, (char *)&x, 1) != 0) {
printf("I2C failure");
}
x++;
wait_ms(500);
}
}
view raw gistfile1.txt hosted with ❤ by GitHub

Arduino(スレーブ側)はArduino-ArduinoのI2C通信の場合と同じです。
https://github.com/ryood/Arduino_I2C/tree/master/Arduino/Wire_Slave_Resiver_NoPullup

#include <Wire.h>
void setup() {
Wire.begin(8); // join i2c bus with address #8
pinMode(A4, INPUT); // disable pullup
pinMode(A5, INPUT); // disable pullup
Wire.setClock(400000);
Wire.onReceive(receiveEvent); // register event
Serial.begin(9600); // start serial for output
}
void loop() {
delay(100);
}
// function that executes whenever data is received from master
// this function is registered as an event, see setup()
void receiveEvent(int howMany) {
while (1 < Wire.available()) { // loop through all but the last
char c = Wire.read(); // receive byte as a character
Serial.print(c); // print the character
}
int x = Wire.read(); // receive byte as an integer
Serial.println(x); // print the integer
}
Arduinoのスケッチでは、

  pinMode(A4, INPUT);           // disable pullup
  pinMode(A5, INPUT);           // disable pullup

として、内部プルアップを無効化しています。配線図のように外付けの2.2kΩの抵抗でプルアップしています。

ArduinoのI2Cアドレスは7bitですが、mbedでは8bitです。mbedではアドレスを1bit左シフトします。

mbedの「I2C::write()」の引数の最後に「bool repeated=false」があります。このrpeatedをtureにすると、I2C通信で「STOPコンディション」が送信されません。

repeated=trueの場合

ch1:SDA ch2:SCL

repeated=falseの場合

ch1:SDA ch2:SCL

「repeated=false」の場合は最後の1バイトを送出する前に「STOPコンディション」が入っています。

「repeated=false」の場合は受信側のArduinoのスケッチでは正常に受信できず、コンソールには「x is32」と表示され続けました。

「repeated=true」の場合は前回のArduino同士のI2C通信のように、8bitカウンタがインクリメントされる様子が表示されます。

「STOPコンディション」は、SCLがHの間に、SDAがL→Hに変化するものです。


ch1:SDA ch2:SCL

2018年11月1日木曜日

Arduino同士でI2C通信をしてみる。

ArduinoのI2Cはあまり使ったことがなかったので、スケッチ例を試してみました。

Master Writer/Slave Receiver (https://www.arduino.cc/en/Tutorial/MasterWriter)

2つのArduinoの一方をI2C Master、もう一方をI2C Slaveとして互いに通信するものです。

回路図

この回路図を見るまで知らなかったんですが、プルアップ抵抗が入っていません。

I2Cはオープンドレインでプルアップ抵抗を入れないと普通は動作しません。

オープンドレインの概略図

左側の「Open Drain」がI2Cの(大雑把な)出力です。「OUT」端子はMOS-FET Q1のドレインそのままで、これでは信号を取り出せません。

右側の「Open Drain Pullup」は、MOS-FET Q2のドレインをR1でVddにプルアップしてOUTから信号を取り出します。Q2のGateがHになったときQ2がONし、R1に電流が流れて電圧降下し(「OUT」のポイントがGNDに短絡される)、「OUT」はLレベルになります。Q2のGateがLのときはQ2がOffし、VddからR1を通してOUTに電流が流れ、「OUT」はHレベルになります。

R1の抵抗値が高すぎるとOUTにつながった容量成分に流れ込む電流が少なくなり、Hレベルになるまでに時間がかかり信号波形がなまることになります。

ArduinoのWire(I2C)の内部プルアップ


今まで知らなかったんですが、ArduinoのWireクラスはデフォルトで内部プルアップが行われているそうです。

参考「ArduinoでI2Cする際のプルアップ抵抗について

「<Arduinoのインストール先>\hardware\arduino\avr\libraries\Wire\src\utility\twi.c」の「void twi_init(void)」関数を見ると、

  // activate internal pullups for twi.
  digitalWrite(SDA, 1);
  digitalWrite(SCL, 1);

とWireクラスの中で内部プルアップが行われています。

Arduinoではデフォルトで内部プルアップされているとすると、このBlogのI2Cタグの記事を見直してみても、I2Cのプルアップがらみでいろいろ不思議に思っていた現象に説明が付きます。

内部プルアップを無効にするには、Wire.begin()したあとに

  pinMode(A4, INPUT);  // disable pullup
  pinMode(A5, INPUT);  // disable pullup

とすればピンの状態がHi-Zになりオープンドレインになります。Master/Slave両方でArduinoを使う場合、内部プルアップを完全に無効にするためには、Master/Slave両方で行って無効化する必要があります。

波形の測定


最近よく使っている中華製のArduino Pro Mini 8MHz/3.3V版で実験してみました。

Arduinoのスケッチ
https://github.com/ryood/Arduino_I2C/tree/master/Arduino

ブレッドボード配線図

ブレッドボード配線図の、下の「Direct」は内部プルアップを使う場合の配線、上の「Pullup」は外付けのプルアップ抵抗を使う場合の配線です。

MasterのI2Cのピン(A4、A5)に(Slaveにつながず)直接オシロのプローブを当てて測定しました。

電源電圧:3.29V


ch1:A4(SDA) ch2:A5(SCL)

内部プルアップが効いているので信号が出力されています。スレーブに接続していないので、SDAの9ビット目がLに引っ張られていません。

前述のように内部プルアップを無効化すると


ch1:A4(SDA) ch2:A5(SCL)

信号が現れません。Slaveに接続せず、信号線をVDD(3.3V電源)に2.2kΩの抵抗でプルアップすると


ch1:A4(SDA) ch2:A5(SCL)

信号が出力されているのを測定できました。しかも、内部プルアップの場合より波形がきれい(立ち上がり、立ち下がりが速い)です。これは2.2kΩとプルアップ抵抗の抵抗値が低いためです。

スレーブに接続して、プルアップ抵抗の抵抗値を変えてみる。


プルアップ抵抗値を変えてI2Cの信号波形を測定しました。

プルアップ抵抗:2.2kΩ

ch1:A4(SDA) ch2:A5(SCL)

Slaveに接続したので、SDAの9ビット目がLに引っ張られて(ACK)、以降の通信が成功しています。

プルアップ抵抗:10kΩ

ch1:A4(SDA) ch2:A5(SCL)

プルアップ抵抗:47kΩ

ch1:A4(SDA) ch2:A5(SCL)

プルアップ抵抗:100kΩ

ch1:A4(SDA) ch2:A5(SCL)

プルアップ抵抗値を上げていくてと、立ち上がりが遅くなっています。このスケッチでは、ざっと見た感じ、いずれもプログラム通りSlaveのSerialから文字列が出力されていますが、100kΩの場合はSCL/SDAの電圧やタイミングがかなり怪しくなっています。信号路の状態によっては通信エラーが発生する可能性が高いと思います。

10kΩあたりまでが無難でしょうか。プルアップ抵抗値を下げるとLレベルになった場合、電流が多く流れるので(特にバッテリー駆動の場合)なんでもかんでも抵抗値を下げればいいというわけでもありません。

I2Cクロックを上げてみる。


ArduinoのデフォルトではI2Cクロックは100kHz(Standard Mode)ですが、ATMega328Pを8MHz駆動すると400kHz(Fast Mode)まで動作させられます。

スケッチで

  Wire.setClock(400000);

と指定すれば400kHzのFast Modeになります。Fast Modeの場合もプルアップ抵抗値を変えて信号波形を測定しました。

プルアップ抵抗:10kΩ

ch1:A4(SDA) ch2:A5(SCL)

プルアップ抵抗:4.7kΩ

ch1:A4(SDA) ch2:A5(SCL)

プルアップ抵抗:2.2kΩ

ch1:A4(SDA) ch2:A5(SCL)

I2Cクロックを上げると波形の崩れにさらに注意する必要がありそうです。

メモ:


ArduinoのWireクラスは難解なI2Cの仕様をうまくラッピングしていて、AVRを直に使う場合と比べてとても楽だと思います。

ただ、うまく動作している場合はいいんですが、そうでない場合は・・・

Nucleo DCO 音出しテスト

筐体やOLED表示などまだ開発途中ですが、Nucleoと出力部を結合して音出ししてみました。

ブロック図

1OSCの波形をチェック後、3OSCを適当にMIXしました。


Audio I/F: TASCAM US-144 MKII
キャプチャ: XSplit