STM32 LittlevGLを使う

03s_255_1lvp.jpg
LittlevGLは、組み込み機器のGUI向けのグラフィックライブラリです。これを使いこなせれば、きれいな画面や使いやすい操作が実現できると思われます。公式の導入方法ではわかりにくい部分があるので、デモの実行方法をまとめておくことにしました。使用したNucleoボードはNUCLEO-F401REです。※メモリ使用量はRAM46kB、FLASH407kBとなったので、それなりに容量が必要となります。



<準備>
まずは以前の記事(STM32 SPI接続タッチパネル付ディスプレイを使う)の手順を行って、ディスプレイの動作確認をします。そしてそのプロジェクトをそのまま利用し、以下の操作をしていきます。



<ダウンロード>
  • LittlevGL本体(v6.1.1)
    右側の [Clone or download] ボタンからZIPダウンロード、解凍
    →フォルダ名「lvgl-master」を「lvgl」へ変更後、プロジェクトのIncフォルダへフォルダごとコピー

  • LittlevGLサンプル集
    右側の [Clone or download] ボタンからZIPダウンロード、解凍
    →フォルダ名「lv_examples-master」を「lv_examples」へ変更後、プロジェクトのIncフォルダへフォルダごとコピー



<LittlevGLコンフィグファイル編集>
  • lvglフォルダ内のlv_conf_template.hを開き、下記3か所編集
    10行目 #if 1 /*Set it to "1" to enable content*/
    23行目 #define LV_HOR_RES_MAX (240)
    36行目 #define LV_COLOR_16_SWAP 1
    →ファイル名「lv_conf_template.h」を「lv_conf.h」へ変更後、プロジェクトのIncフォルダへ移動

  • lv_examplesフォルダ内のlv_ex_conf_templ.hを開き、下記2か所編集
    9行目 #if 1 /*Set it to "1" to enable content*/
    42行目 #define LV_USE_DEMO 1
    →ファイル名「lv_ex_conf_templ.h」を「lv_ex_conf.h」へ変更後、プロジェクトのIncフォルダへ移動
    ※壁紙画像不使用にするとメモリ使用量を減らせます。→44行目 #define LV_DEMO_WALLPAPER 0



<main.cファイル編集>
以前の記事(STM32 SPI接続タッチパネル付ディスプレイを使う)で記述したものは消去し、下記4か所に記載
(参考ページ→How start with the LittlevGL on the Disco F746NG with Mbed

▼/* USER CODE BEGIN Includes */ の下
#include "ili9341.h"
#include "ili9341_touch.h"
#include "lvgl/lvgl.h"
#include "lv_examples/lv_apps/demo/demo.h"

▼/* USER CODE BEGIN 0 */ の下
lv_disp_buf_t disp_buf;               // ディスプレイバッファの構造体
lv_color_t buf_1[ILI9341_WIDTH * 10]; // ディスプレイバッファ1
lv_color_t buf_2[ILI9341_WIDTH * 10]; // ディスプレイバッファ2

lv_disp_drv_t disp_drv; // ディスプレイドライバの構造体
lv_indev_drv_t indev_drv; // タッチパッドドライバの構造体

void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) // 画面描画
{
int32_t w = area->x2 - area->x1 + 1;
int32_t h = area->y2 - area->y1 + 1;
ILI9341_DrawImage(area->x1, area->y1, w, h, (uint16_t*)color_p);
lv_disp_flush_ready(disp_drv); // 描画終了をライブラリへ知らせる
}

bool my_input_read(lv_indev_drv_t * drv, lv_indev_data_t * data) // タッチパッドのタッチ検出とタッチ位置取得
{
static uint16_t x = 0, last_x = 0;
static uint16_t y = 0, last_y = 0;
if(ILI9341_TouchPressed())
{
ILI9341_TouchGetCoordinates(&x, &y);
data->point.x = x;
data->point.y = ILI9341_HEIGHT - y; // 上下逆さま
last_x = data->point.x;
last_y = data->point.y;
data->state = LV_INDEV_STATE_PR; // タッチあり
}
else
{
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL; // タッチなし
}
return false;
}

void init() // ディスプレイ初期化
{
ILI9341_Unselect();
ILI9341_TouchUnselect();
ILI9341_Init();
}
my_flush_cb関数に画面描画を行うコードを、my_input_read関数にタッチパッドに関するコードを記述します。他のディスプレイを使う場合でも、ここに該当するコードを記述すればうまく動作するはずです。

▼/* USER CODE BEGIN 2 */ の下
  init();    // ディスプレイ初期化
lv_init(); // ライブラリ初期化
lv_disp_buf_init(&disp_buf, buf_1, buf_2, ILI9341_WIDTH*10); // ディスプレイバッファ初期化

lv_disp_drv_init(&disp_drv); // ディスプレイドライバ初期化
disp_drv.buffer = &disp_buf; // ディスプレイバッファ登録
disp_drv.flush_cb = my_flush_cb; // 画面描画関数登録
lv_disp_drv_register(&disp_drv); // ディスプレイドライバ登録

lv_indev_drv_init(&indev_drv); // タッチパッドドライバ初期化
indev_drv.type = LV_INDEV_TYPE_POINTER; // 入力タイプをタッチパッドに指定
indev_drv.read_cb = my_input_read; // タッチ検出関数登録
lv_indev_drv_register(&indev_drv); // タッチパッドドライバ登録

demo_create(); // デモ呼び出し

▼/* USER CODE BEGIN 3 */ の下
    HAL_Delay(100);
lv_tick_inc(100); // ライブラリへ経過時間を知らせる
lv_task_handler(); // ライブラリ実行(画面更新)
メインループ内で100msごとに画面を更新します。画面描画が速ければもっと短い間隔で更新してもよいでしょう。ライブラリへ経過時間を知らせるのは、アニメーションの動作に関係していると思われます。

ビルドには1分程度かかる場合があります。SPIの通信速度が遅いので、アニメーションはさすがに無理がありそうです。狭い範囲の表示更新については特に問題なさそうな感じです。

タグ : マイコン 

Owm Pedal H7 ソフトウェア編

自作デジタルエフェクター「Owm Pedal H7」の各設定、内部プログラミングをしていきます。
Owm Pedal H7 ハードウェア編はこちら
内部データはGitHub

<開発環境について>
STM32CubeIDEのバージョン1.1.0ではクロック設定がうまく動作しなかったので、STM32CubeMXを併用していました。バージョン1.2.1では修正されているようで、今のところ問題は起きていません。

main.cファイルを「main.cpp」にリネームすればC++を使うことができます。main.cppに追加記載しているのは下記3か所です。主にuser_main.cppを編集することになります。
 「/* USER CODE BEGIN Includes */」の下→#include "user_main.hpp"
 「/* USER CODE BEGIN 2 */」の下→mainInit();
 「/* USER CODE BEGIN 3 */」の下→mainLoop();

コンパイラの最適化設定を高速動作向けに変更しておきます。
Project→Properties→C/C++ Build→Settings→Tool Settingsタブ
→MCU G++ Compiler→Optimization→Optimization level [Optimize for speed (-Ofast)]



<STM32CubeIDE iocファイル設定>
・ST-LINK接続(結線はOwm Pedal ソフトウェア編参照)
Trace and Debug→DEBUG→Debug [Serial Wire]

・処理高速化のためDキャッシュを有効化
System Core→CORTEX_M7→Cortex Interface Settings→CPU DCache : Enabled

・クロック源として外部水晶振動子を使用
System Core→RCC→High Speed Clock (HSE) [Crystal/Ceramic Resonator]

・動作モード設定(480MHz動作のために必要)
System Core→RCC→Power Parameters→Power Regulator Voltage Scale : Power Regulator Voltage Scale 0

▽SPI設定
タッチパネル付ディスプレイの接続はSPIです。詳細は以前の記事(STM32 SPI接続タッチパネル付ディスプレイを使う)をご参照ください。
※通信速度を上げ過ぎるとタッチ位置をうまく取得できないので、本来はディスプレイとタッチを別々のSPIにした方がよいと思われます。

▽SAI設定
03s_254_1owmHSAI.png
Multimedia→SAI1→SAI A→Mode [Asynchronous Slave] I2S/PCM Protocol にチェック
同SAI B→Mode [Synchronous Slave] I2S/PCM Protocol にチェック
SAI A Data Size : 32 Bits
SAI A FIFO Threshold : Half Full
SAI B Audio Mode : Slave Transmit
SAI B Data Size : 32 Bits
SAI B FIFO Threshold : Half Full

NVIC Settingsタブ→SAI1 global interrupt→Enabledにチェック(2020年3月16日追記)

※STM32H7でDMAを利用するのは大変そうなので、今回は利用していません。
→参考ページ:STM32H7でキャッシュ一貫性を保持したDMA転送(Memory-to-Peripheral)

▽クロック設定図
03s_254_2owmHCK.png
SAIクロックは、オーディオサンプリングレート44.1kHzに対する誤差が少なくなるような設定としました(44.1×2048=90316.8)。

▽全ピン設定図
03s_254_3owmHPin.png



<信号処理>
Owm Pedalと同じように半分ずつデータ処理しようとしましたが、HalfCpltCallbackが呼び出されないようなので、2つのバッファ配列を準備しました(下図ではaとb、ソースコード内では[0]と[1])。
03s_254_4owmHI2S.png
mainInit処理の中で最初のSAI受信呼び出しを行います(HAL_SAI_Receive_IT)。その後、受信完了時にHAL_SAI_RxCpltCallbackが呼び出され、その中で次の受信とSAI送信(HAL_SAI_Transmit_IT)を行います。送受信にFIFOを設定しておかないと、処理が連続的にならず途切れ途切れになってしまうことがあります。遅延時間(レイテンシ)実測値は、ブロックサイズ32で2.5ms程度でした。まとめて信号処理ではなく、1サンプルずつ処理するようなやり方をすればバッファ配列は1つで済むかもしれません。

<SAIエラー対処>
Owm Pedalのときの反省を生かし、オーディオコーデックV4220Mをマイコンからリセットできるようにしました。mainInit処理内でSAIのエラーが起こった場合、リセット処理するようになっています。ただ、今のところエラーが発生したことがなく、正しい処理なのかわかりません。

<スイッチ・タッチ検出>
SAIの割り込みは32ブロックサイズ(約0.7ms)ごとに起こるので、これをタイマーのように利用しています。割り込みが起こるたびにGPIOの状態をチェックし、スイッチ操作やタッチを検出するというやり方です。タッチ検出時はすぐにタッチ位置を算出したいところですが、SPI通信にそれなりに時間がかかるので、タッチ位置関係処理は割り込み内ではなくメインループ処理内に入れています。現状の操作方法は下記の通りです。
・画面上部:パラメータページ切替、長押しでデータ保存(→STM32 内蔵フラッシュメモリへのデータ保存
・各ノブ部:上側で増加、下側で減少(長押しで10ずつ変化)
・画面下部:長押しでエフェクト切替
・スイッチ:エフェクトオン・オフ

<エフェクト処理>
とりあえず6つのエフェクト(オーバードライブ、ディレイ、トレモロ、イコライザ、コーラス、リバーブ)を実装していますが、細かい音質については検討していません。今後別記事で紹介していきたいと考えています。

タグ : 自作エフェクター マイコン 

Owm Pedal H7 ハードウェア編

03s_253_1owmHp.jpg
STM32F405を搭載した「Owm Pedal」に引き続き、STM32H753とタッチパネル付ディスプレイを備えたデジタルエフェクターを製作しました。今回も穴加工はTabby's工房さんにお願いしました。(Owm Pedal H7 ソフトウェア編はこちら

▽入出力・電源部 回路図
03s_253_2owmHas.png
4回路入りオペアンプOPA1679を使用することにしたので、以前と違い入力部のローパスフィルタが2次になっています。デジタル電源は絶縁型DC-DCコンバータで分離するようにしました。コンバータなしにもできるようにしましたが、ノイズ面でよくないと思われるので非推奨です。

▽マイコン・オーディオコーデック部 回路図
03s_253_3owmHds.png
当初はSTM32F722を使っていたのですが、さらなる発展を見据えSTM32H7を使うことにしました。注文時の在庫の関係でSTM32H753にしましたが、STM32H743でも問題ないと思います。今回はオーディオサンプリングレート44.1kHzで、SAIペリフェラルを使います。

▽基板レイアウトについて(KiCadデータはGitHubへ)
上側のマイコン基板は4層にしてみました。JLCPCBの場合、50mm×50mm以内であれば4層でも13ドルで済みます。マイコンは可能な限りピンを引き出していますが、全部は使わないので内側は小さいパッドです。GNDは基本一面プレーンで、基板が2枚に分かれているのでコネクタ部分でGNDを接続することになります。DC-DCコンバータは高さを低くするため寝かせて配置しました。

▽ディスプレイ・スイッチ基板レイアウト
03s_253_4owmHl.png
念のためダンピング抵抗を入れています。消費電流がそれなりにあるので、マイコン用とは別に3.3Vを準備しました。ディスプレイの詳細は以前の記事(STM32 SPI接続タッチパネル付ディスプレイを使う)をご参照ください。下写真のように基板を合体させます。
03s_253_5owmHi.jpg

▽周波数特性・ノイズ
ZOOM MS-50Gと特性を比較しました。MS-50GはParaEQを6個並べて+20dBになるよう調節しています。ノイズは1kHz正弦波入力時のものです。
03s_253_6owmHf.gif
Owm PedalはMS-50Gより高域のノイズが多いように見えますが、MS-50Gはもともと高域が下がっているので、それを加味するとほぼ同じノイズレベルといってよいと思います。Owm Pedal H7では1dB程度ノイズが多いですが原因はよくわかりません。基板設計としては、部品配置に注意すればOwm PedalのようにGNDを1面プレーンにしても問題ないとわかりました。入出力のフィルタも、簡単な1次ローパスフィルタで充分かもしれません。

タグ : 自作エフェクター レイアウト 回路図 マイコン 周波数特性 

STM32 内蔵フラッシュメモリへのデータ保存

HALライブラリを利用し、STM32の内蔵フラッシュメモリへデータを保存します。これにより電源を切ってもデータを残したままにすることができます。下記参考ページにも詳細な内容がありますので、併せてご参照ください。
STM32 + HAL Flashの書き込み・読み込み



フラッシュメモリへのデータ保存時には、単純に書き込み関数を呼び出せばよいわけではなく、あらかじめ書き込む場所を消去しておく必要があります。さらに、消去等の操作前にはロックを解除しなければいけません。つまり下記の流れとなります。
・ロック解除→消去→書き込み→再度ロック

フラッシュメモリはセクタという単位に分割されており、消去操作時にはセクタごとに消去します。STM32F401REのセクタ7の場合、フラッシュメモリの終わり側128kB分が全て消えることになります。

<STM32F4、F7での書き込みサンプルコード>
テストとして配列を書き込みます。NUCLEO-F401REで動作確認しました。セクタの開始アドレスはそれぞれのICのリファレンスマニュアルをご確認ください。
const uint32_t flash_addr = 0x08060000; // データ保存先(セクタ7)開始アドレス
uint16_t testData[8] = {1, 3, 5, 7, 2, 4, 6, 8}; // 書き込むデータ

void saveData()
{
/* フラッシュ ロック解除 */
HAL_FLASH_Unlock();

/* フラッシュ 消去 */
FLASH_EraseInitTypeDef erase; // 消去に関する構造体を定義
uint32_t error = 0; // エラーコードを格納する変数
erase.TypeErase = FLASH_TYPEERASE_SECTORS; // 消去方法: セクタ消去
erase.Sector = FLASH_SECTOR_7; // 消去するセクタ: セクタ7
erase.NbSectors = 1; // 消去するセクタの数: 1
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 電圧設定
HAL_FLASHEx_Erase(&erase, &error); // 消去

/* フラッシュ 書き込み */
uint32_t addr = flash_addr; // 書き込み先アドレスを代入
for (int i = 0; i < 8; i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, testData[i]); // 書き込み
addr += 2; // 2バイト(16ビット)分アドレスを進める
}

/* フラッシュ ロック */
HAL_FLASH_Lock();
}
今回は16ビットのデータを書き込んでいますが、8ビットであれば「FLASH_TYPEPROGRAM_BYTE」を使い、アドレスは1バイトずつ進めます(32ビットであれば「FLASH_TYPEPROGRAM_WORD」を使い、アドレスは4バイトずつ進めます)。

<データ読み出し>
保存したデータを読み出す場合は特別な操作は必要なく、下記のように記載します。
uint32_t addr = flash_addr;
testData[0] = *((uint16_t*)addr);
「(uint16_t*)」で書き込み先アドレスをポインタ型へキャストし、そのポインタ(アドレス)にあるデータを「*」で読み出しています。何も書き込まれていない場合は、ビットが全て1のデータ(uint16_tなら65535)が読み出されます。



<STM32H7での書き込みサンプルコード>
セクタの他にバンク1と2があるため、バンク指定が追加になっています。また、書き込みはFLASHWORD(256ビット)単位限定で、256ビットのデータを準備しなければいけないようです。データ読み出しについては上記と同じです。NUCLEO-H743ZI2で動作確認しました。
const uint32_t flash_addr = 0x081E0000; // データ保存先(バンク2 セクタ7)開始アドレス
uint16_t testData[16] = {1, 3, 5, 7, 2, 4, 6, 8, 9, 7, 5, 3, 0, 8, 6, 4}; // 書き込むデータ

void saveData()
{
/* フラッシュ ロック解除 */
HAL_FLASH_Unlock();

/* フラッシュ 消去 */
FLASH_EraseInitTypeDef erase; // 消去に関する構造体を定義
uint32_t error = 0; // エラーコードを格納する変数
erase.TypeErase = FLASH_TYPEERASE_SECTORS; // 消去方法: セクタ消去
erase.Banks = FLASH_BANK_2; // 消去するバンク: バンク2
erase.Sector = FLASH_SECTOR_7; // 消去するセクタ: セクタ7
erase.NbSectors = 1; // 消去するセクタの数: 1
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 電圧設定
HAL_FLASHEx_Erase(&erase, &error); // 消去

/* フラッシュ 書き込み */
uint32_t addr = flash_addr; // 書き込み先アドレスを代入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, addr, (uint32_t)testData); // 書き込み
addr += 32; // 続けて他のデータを書き込む場合は32バイト(256ビット)分アドレスを進める

/* フラッシュ ロック */
HAL_FLASH_Lock();
}



<リンカスクリプトについて>
内蔵フラッシュメモリにはプログラムが書き込まれています。万が一大きなプログラムとなった場合には、データ保存したいフラッシュ領域と重なってしまうおそれがあります。そこで、プログラムの領域とデータ保存の領域を分けておきます。

STM32CubeIDEのプロジェクトフォルダ内に、リンカスクリプトと呼ばれるSTM32○○○○_FLASH.ldというファイルがあります。このファイル内のMEMORYの記述を変更すればOKです。例えばSTM32F401REのセクタ7(開始アドレス0x08060000)をデータ保存領域(DATA)とする場合は下記の記述とします。
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 384K
DATA (rx) : ORIGIN = 0x08060000, LENGTH = 128K
}

タグ : マイコン 

STM32 計算速度データ

デジタルエフェクターでリアルタイムにデジタル信号処理をする場合、計算が間に合わずまともな音が出なくなるという可能性があります。そこで、様々な計算方法についてどの程度処理時間がかかるのか確認しておくことにしました。使ったのはNUCLEO-F401REで、FPU(浮動小数点演算処理装置)を備えています。開発環境はSTM32CubeIDE(1.0.2)です。



経過時間を確認するには、CPUのクロックサイクルのカウント数(以下サイクル数)を利用すると便利です。下記の記載をしておくと、サイクル数を確認できるようになります。(参考ページ→ARMのData Watchpoint and Trace Unitを使って処理時間計測をしてみよう
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

そして下記のようにコードを書きます。「DWT->CYCCNT」に現在のサイクル数が入ります。コンパイル時に最適化されないように、volatileを入れ、サイクル数を変数として使うことにします。
volatile uint32_t start = 0;
volatile uint32_t stop = 0;
volatile uint32_t ivar[50] = {};
volatile uint32_t cnt[50] = {};

start = DWT->CYCCNT; // 開始時サイクル数
ivar[0] = start; // 計測したい処理
stop = DWT->CYCCNT; // 終了時サイクル数
cnt[0] = stop - start; // 処理にかかったサイクル数

上記「変数の代入」を基準として、処理を追加した場合にどのぐらいサイクル数が増えるかを確認しました。例えば乗算の場合、「ivar[1] = start * stop;」のようにして、変数の乗算を1つ追加するといった具合です。定数を使うと事前に計算されてしまう場合があるため、全て変数を使います。

コンパイル時の最適化オプション「O0(最適化なし)」と「Ofast(最速最適化)」での違いも確認します。1000回平均値ですが結果が結構変わるときがあり、あまり正確ではなさそうです。



<32ビット整数(uint32_t)>

サイクル数(O0)サイクル数(Ofast)
何もしない84
代入(=)※基準167
加算(+) 減算(-) 乗算(*)53
除算(/)97
剰余演算(%)118
各ビット演算(& | ! ^ >> <<)53
変数の呼び出しに数サイクル必要なのか、代入だけで結構時間がかかるようです。剰余演算がそんなに遅くないというのが意外でした。



<32ビット浮動小数点数(float)>

サイクル数(O0)サイクル数(Ofast)
代入(=)※基準167
加算(+) 減算(-) 乗算(*)54
除算(/)1717
定数での除算の場合は最適化で乗算と同じ速さになりますが、変数だと遅いままとなってしまいます。

C言語標準ライブラリmath.hにある数学の関数については以下の通りです。
サイクル数(O0)サイクル数(Ofast)
sqrtf5116
logf189189
expf216207
powf565571
sinf8279
sinhf286283
atanf189178
デシベル計算に使うpowfがかなり遅いので、ルックアップテーブルの利用を考える必要がありそうです。

CMSIS-DSPライブラリを導入し、とりあえず2つの関数を使ってみました。(参考ページ→STM32 CubeIDE環境で、CMSIS-DSPを使う方法
サイクル数(O0)サイクル数(Ofast)
arm_sqrt_f329113
arm_sin_f325652
今後、BiQuadフィルタやフーリエ変換等の活用について検討したいと思います。

タグ : マイコン 

管理人

管理人

自己紹介のページ
記事一覧
Twitter
GitHub

ブログ内検索
カテゴリー
タグ

自作エフェクター   レイアウト   回路図   歪み   周波数特性   マイコン   PureData   波形・倍音   RaspberryPi   エレキギター   アンプ   歪率   エフェクター自作方法   エレキベース   真空管   コーラス   ピックアップ   静音ギター   ヘッドフォンアンプ   擬似ギター出力   市販エフェクター   アコースティックギター   ブースター   コンデンサ   ソロギター   ポールピース   イコライザー   コンプレッサー   ビブラート   フェイザー   トレモロ   TAB譜   ディレイ   DIY_Layout_Creator   ワウ   オートワウ   バッファー   

最近の記事
最新コメント
Twitter
RSS
メールフォーム
当ブログに関するお問い合わせはこちらからお願いします。 ※FAQ(よくある質問)もお読みください。

お名前
メールアドレス
件名
本文

アクセスカウンター