ラベル STM32F1 の投稿を表示しています。 すべての投稿を表示
ラベル STM32F1 の投稿を表示しています。 すべての投稿を表示

2016年7月9日土曜日

タイマのレジスタ

STM32F1のタイマ周りのデバッグでレジスタを読みたくなった。複数のタイマの、複数のレジスタにまたがってのデバッグなので、いちいちデバッグ用のコマンドを実装するのも面倒なので、レジスタのポインタを返す関数を作った。

volatile uint16_t* GetTimRegPtr(int idx, const char* regname) {
    TIM_TypeDef* TIM;
    
    switch (idx) {
    case 1: TIM = TIM1; break;
    case 2: TIM = TIM2; break;
    case 3: TIM = TIM3; break;
    case 4: TIM = TIM4; break;
    case 5: TIM = TIM5; break;
    case 8: TIM = TIM8; break;
    default: return(0);
    }
    
    if (!strcmp(regname, "CR1")) { return(&TIM->CR1); }
    if (!strcmp(regname, "CR2")) { return(&TIM->CR2); }
    if (!strcmp(regname, "SMCR")) { return(&TIM->SMCR); }
    if (!strcmp(regname, "DIER")) { return(&TIM->DIER); }
    if (!strcmp(regname, "SR")) { return(&TIM->SR); }
    if (!strcmp(regname, "EGR")) { return(&TIM->EGR); }
    if (!strcmp(regname, "CCMR1")) { return(&TIM->CCMR1); }
    if (!strcmp(regname, "CCMR2")) { return(&TIM->CCMR2); }
    if (!strcmp(regname, "CCER")) { return(&TIM->CCER); }
    if (!strcmp(regname, "CNT")) { return(&TIM->CNT); }
    if (!strcmp(regname, "PSC")) { return(&TIM->PSC); }
    if (!strcmp(regname, "ARR")) { return(&TIM->ARR); }
    if (!strcmp(regname, "RCR")) { return(&TIM->RCR); }
    if (!strcmp(regname, "CCR1")) { return(&TIM->CCR1); }
    if (!strcmp(regname, "CCR2")) { return(&TIM->CCR2); }
    if (!strcmp(regname, "CCR3")) { return(&TIM->CCR3); }
    if (!strcmp(regname, "CCR4")) { return(&TIM->CCR4); }
    if (!strcmp(regname, "BDTR")) { return(&TIM->BDTR); }
    if (!strcmp(regname, "DCR")) { return(&TIM->DCR); }
    if (!strcmp(regname, "DMAR")) { return(&TIM->DMAR); }

    return(0);
}

とりあえずSTBeeで使ってるのでTIMは1,2,3,4,5,8を使えるようにしている。TIMxはdefineで宣言されているので、ifdefでcaseを囲ってもいい。
引数はタイマの番号と、レジスタの名前を文字列で渡す。不正な番号orレジスタ名を渡した場合は0(ぬるぽ)を返す。それぞれが正常だった場合はvolatile uint16_t*を返す。
USARTから文字列を受け取って、TIMの番号とレジスタ名を読み込んで、それを引数に渡して、ポインタ内の値を表示する、みたいな機能を作ればレジスタの値を読み取ることができる。
ポインタに値を設定すればレジスタを変更できる。



PWMとADCを高精度にタイミング合わせて走らせたいけど、結構面倒っぽい。あとSTM32F1を72MHzで走らせてるとADCは1Mspsではサンプリングできず、およそ0.85Mspsくらいが上限っぽい。コアを56MHzで動かせば1Mspsで取れるらしいが。
それとADC1とADC2を交互にサンプリングする方法も謎。おそらく高速インターリーブモードでできるんじゃないかと思うが。これはトリガ1個でどんどん変換していくので、止める方法が不明。
まぁいろいろな方法がありそう。期限があるわけでもないのでゆっくりと調べつつ。頭を休めつつ。

2016年7月8日金曜日

TIM-DMA-DAC

STM32F1のDACから正弦波を出しました。





若干高調波がありますが、かなり綺麗な波形です。DACが2chあるので、時間方向に正弦波の強さを変えつつ、逆位相を出力したりできます。なのでAM変調波とかも出力できます。ま、100Hz当たりが限界で、送信出力も極めて弱いので普通のラジオ程度だと受信できないですけど。


TIM_TimeBaseInitTypeDef TIM_InitStructure;
DMA_InitTypeDef DMA_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

TIM_InitStructure.TIM_Period = 72 - 1;
TIM_InitStructure.TIM_Prescaler = 40 - 1;
TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStructure.TIM_RepetitionCounter = 0;

TIM_TimeBaseInit(TIM4, &TIM_InitStructure);


DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&DAC->DHR12RD);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)(DAC_Buffers);
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = sizeof(DAC_Buffers) / sizeof(DAC_Buffers[0]);
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

DMA_DeInit(DMA1_Channel7);
DMA_Init(DMA1_Channel7, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel7, ENABLE);
TIM_DMACmd(TIM4, TIM_DMA_Update, ENABLE);

//TIM_Cmd(TIM4, ENABLE);

とりあえずTIM4とDMA1-7を使いました。予めuint32_tでバッファを宣言しておく必要があります。今回は500サンプルのバッファを作りました。500サンプルで50Hzになるように、TIM4を初期化します。TIM4はタイミングを作るだけなのでNVICなどは必要ありません。
次にDMAの初期化ですが、これも他の用途と全く同じです。ただし、普通はSTM32でペリフェラルとDMA転送する場合はByteが多く、たまにHalfWordを使う程度ですが、今回は珍しくWord転送です。これはDACに2chを一気に渡すために32bitのレジスタを使うためです。
とりあえずこの時点でDMA自体は有効にしておき、TIMのDMAトリガ出力も有効にしますが、TIM自体は後ほど有効化します(データを作ってから動かします)。

int i, l = sizeof(DAC_Buffers) / sizeof(DAC_Buffers[0]);
const float pi = 3.14159265f;
for (i = 0; i < l; i++) {
    float f = (float)i / l;
    f = sinf(f * pi * 2);
    
    uint16_t da1 = (uint16_t)(2047 + (2047 * +f));
    uint16_t da2 = (uint16_t)(2047 + (2047 * -f));
    DAC_Buffers[i] = da1 | da2 << 16;
}

TIM_Cmd(TIM4, ENABLE);

正弦波を作るに当たり、今回はsinf関数を使用しました。そのためmath.hをインクルードしておく必要があります。マイコン内で浮動小数点を扱えなかったり、扱いたくない場合はExcel等でテーブルを作ってやることもできますが、コピペしたりデータの管理が面倒なので、今回はマイコン内で作りました。どうせ起動時に1回実行するだけですし、動作確認用に動かすだけですから、ちょっと時間食うくらいは許容します。DAC1とDAC2では位相を逆にしています。
DACのDHR12RDは右詰め12bitで、下位16bitがDAC1、上位16bitがDAC2に出力されています。ということで32bitのデータバッファにビットシフトして与えています。
この辺りは趣味でやる以上はある程度個人の好みに実装して下さい。例えば16bitの2次元配列でDAC1とDAC2を分離するといったやり方もアリだと思います。

最後にTIM_Cmdでタイマを走らせて終了です。


STM32F1のペリフェラルは結構いろいろな組み合わせ方ができて、特にTIMはタイミングを作ったり、他にもいろいろな機能があります。場当たり的に必要な機能を実装していくとあっという間に使い切るので、うまく使いまわせるところは共通で使っていく必要があります。

TIMの機能
・タイマ割り込み
・PWM出力
・パルス幅計測
・DMAタイミング生成
・他のタイマの開始/停止
・その他

タイマの開始/停止は、例えばTIM2でPWMを作り、PWMがHighの時だけTIM3を走らせる、みたいな事ができます。5kHzのPWMを正確に50パルスだけ出したい、というような使い方ができます。
期限とか気にせずに考えてる時は楽しいんですけどね。いざ使おうとすると大変です。

STM32F1のDAC

ADCを試したいが、適当なアナログソースが無いので、内蔵しているDACから波形を出力することにした。

まず初期化。

/*
    DACを使う場合、GPIOはAINで初期化すること
    (RM0008日本語訳Rev11, 12.2(p251)による)
*/

GPIO_InitTypeDef GPIO_InitStructure;
DAC_InitTypeDef DAC_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);


DAC_InitStructure.DAC_Trigger = DAC_Trigger_None;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_TriangleAmplitude_4095;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;

DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Init(DAC_Channel_2, &DAC_InitStructure);

DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_Cmd(DAC_Channel_2, ENABLE);

とりあえずDAC1, DAC2の両方を初期化してみる。
DACの出力ピンは固定で、リマップとかはできない。
値を出力するにはDAC_SetChannel1Data(DAC_Align_12b_R, val);のような関数を使う。この例では右詰めだが、左詰めにすると16bitの下位4bit切り捨てと同じような動作になるので、16bitのWAVEデータを出したりするには左詰めが便利。

DACのOutputBuffが有効なときは最小で5kΩ、無効なときは最大で15kΩになるらしい。有効な時の最大インピーダンスは書かれていない。バッファを有効にしてもせいぜい2倍とか3倍しか変わらないが、ないよりはマシかな。


次はを実装する。正弦波あたりがほしいので、TIMとDMAで流し込む感じになるのかな。

超音波

STM32F1 TIM1で40kHzのPWMを作ってみた。TIM1は逆位相波を出力できるので、だいたい6Vppくらいのパルスを作れてる予定。オシロではGND基準の1ピンしか見てないからVDDの3.3Vあたりだけど。
FreeRTOSのタスクで5mSくらいPWMを出した後に500mSディレイという感じで出力。




超音波の送信と受信はだいたい18-20cmくらい離して向かい合わせでおいてある。20cmで音速だと600us近く。送信(赤)と受信(黄)の違いはだいたいそれくらいだと思う。
下の画像は水色が無風時の位相、黄色はR側からTに向かって軽く息を吹いてる。だいたい1usくらい遅れてる。STM32F1は確か2Mspsが最高だから、分解能0.5息速度くらい。

STM32F4だと2.4Mspsが最高で、並列だとその倍くらいかな?。やはり超音波風速計を作るならF4が必要かも、と思いつつ、僕はF4を使ったことがないので、今からF4を始めるくらいならF3を使ったほうがいいかもしれない。まぁどうせならPSoCのほうがいいような気もするが。
どっちにしろアナログ回路はある程度必要だから、いっそのこと数MSPSでサンプリングできるADCを買ったほうが早いかもしれない。


使用するオペアンプについて
とりあえずざっとTRの位置を変えた感じでは、最小で50mVppくらいが出てきてる。最大100倍くらいのゲインがあれば十分かも。これくらいならオペアンプ1段で十分だと思う。4回路オペアンプなら4ch受けれる。超音波距離計の回路では数千倍-数万倍くらいに増幅している例もあるみたいだけど、それは往復で10mくらいを飛ばすからであって、風速計みたいな最大でも40-50cm程度ならそんなにゲインは必要ない。
そもそもTR間が15cmくらいなら増幅無しでADCに繋いでもいいんじゃないかってくらいの電圧がある。インピーダンス高すぎてダメだろうけども、試す価値はあるかも。

STM32F1のリソースだと、出力が3系等、受信が8系統くらいまでかな。
TR間の距離を予めキャリブレーションして、音速は別のセンサで温度を計測する、みたいな構成なら送受信1対1で3セットあれば足りるかもしれない。おそらく市販の超音波風速計はこの構成だと思う。常識的な温度範囲なら熱の変形とかはあまり気にならないだろうし。


とりあえず、まずは送受信1対1で位相差を計測するところから始めるかな。位相差をF1の最大性能で測るとなると、F1のペリフェラルをキッチリ設定してやる必要がある。まずそこが難関。

2016年7月7日木曜日

STM32F1のUSART(DMA)

STM32F1にFreeRTOSをポーティングしたついでにUSARTもいじってる。他のサンプルを見てないのでどういうやり方が正しいOSの使い方なのかわからないけど。

ハードウェアの初期化部分は通常の非OS環境と同じように使っていて、非OS環境でも使えるので貼っておく。


2016年7月6日水曜日

FreeRTOSのポーティング

STBeeにFreeRTOSをポーティングしてみる。
メモ帳代わりに変更点をブログエディタに書いてるので時系列にはなってるけど内容はまとまってない。暇な時の読み物にでも。


開発環境はGCCを使用している。
とりあえず普段使っている雛形を元に、OSを追加していく形で実装した。
この雛形について。USART1による通信やLチカを実装済み。協調的マルチタスクを自前で実装済みで、USARTからのコマンドの受け取りとかはマルチスレッドで行うことができる。センサロガーとかとして使う場合はタイマ割り込みの中でSPI通信とかを叩いていた。とりあえずマイコンの動作はできているのが確認できている。

FreeRTOSは各自で探してもらうとして、執筆時点ではv9.0.0が落ちてきた。RTOSなるものは初めて使うのにいきなりメジャーアップデート直後とかこの時点で不安が。

今回は初めてRTOSを使うことも有り、動作するかもわからないのでOSはプロジェクトのフォルダの中にコピーした。ちゃんと使うときは、OS自体はプロジェクトとは別においておくのがいいらしい。

プロジェクトのディレクトリの中にinc, lib, srcのディレクトリが有り、他にmakefileが入っている。StdPriph_Driverとかはlibに入っている。とりあえずlibの中にFreeRTOSディレクトリを作り、その中にFreeRTOS/Sourceの*.cとincludeディレクトリをコピーする。更にportableディレクトリを作り、Source/portableのMemMangをコピーする。
OSの機能はこれで足りるらしい。

とりあえずこの時点でmakefileに./lib/FreeRTOS/includeをインクルードパスに追加する。それと./lib/FreeRTOS/*.cをソースファイルとして追加する。

次にmakeする。すると予想通り「FreeRTOSConfig.hが見つからないぞ」とエラーが出る。あとはエラーを消していく作業をすれば、最低限ビルドが通るようにはなるはず。

まずはFreeRTOS/Demo/CORTEX_STM32F103_GCC_Rowley/FreeRTOSConfig.hを./inc/にコピーしてビルド。portmacro.hが無いというエラーが出た。
FreeRTOS/Source/portable/GCC/ARM_CM3のportmacro.hをincにコピー。
これでビルドが通った。警告も特に出ている様子はない。

ここでバイナリをSTBeeに書き込む。RTOSを追加しただけなので、望む動作は雛形と変わりない。とりあえずデバッグメッセージやLチカは正常に動作している。

次はRTOSを動作させる番。

まずFreeRTOS.hとtask.hをインクルードする。すでにインクルードパスにincludeディレクトリを追加しているので問題なくビルドが通るはず。


とりあえずxTaskCreateとvTaskStartSchedulerを試す。てきとーな関数を作ってTaskCreateに渡す。その後でTaskStartを叩く。ビルドするといろいろ未定義エラーが出る。とりあえず真っ先に目につくのがメモリ関係かな。
ソースファイルにFreeRTOS/portable/MemMang/heap_1.cを追加する。メモリ(Malloc,Free)関係の未定義エラーはおおよそ消えた。あとはpxPortInitialseStack, xPortStartScheduler, vPortEnterCritical, vPortExitCriticalが未定義となっている。
potable/GCC/ARM_CM3/port.cを./lib/FreeRTOS/にコピーしてmakefileに追加する。
これでビルドエラーが消えた。STBeeにバイナリを書いて実行。xTaskCreateは通ったが、vTaskStartSchedulerを叩いてもタスクのデバッグメッセージは出てこない。
例外ハンドラに文字列出力を入れたところ、HardFault_Handlerが呼ばれているらしい。

FreeRTOSをSTM32F4にポーティングする - あくまで個人的メモ用ブログに#defineを消すとHardFaultする、というのが書いてあったので、#defineを3行追加する。
このマクロの意図するところは、FreeRTOSの関数名を変更するらしい。例えばxPortSysTickHandlerはマクロによりSysTick_Handlerという名前になり、これはCMSISのSysTick割り込み名となる。
ただしこの3個の関数はstm32f10x_it.cですでに作成されているため、ビルドエラーとなる。とりあえずit.cの方をコメントアウトすることで対応した。
それをビルドしたところ、今度はvApplicationStackOverflowHook関数が無いとエラーが出た。ということでとりあえず引数なし、戻り値なしで中で無限ループする関数を作り、UARTでHookした旨を表示するようにした。
これでビルドが通ったのでSTBeeに書いてみる。すると2個登録したタスクの内1個を実行し、StakOverflowHookが呼ばれた旨の表示が出た。

これの原因は簡単にわかった。以前に読んだページで、「printfは大量にメモリ使うからstack足りなくなるよ」と書いてあったのを覚えていたため。タスクの動作テストにはprintfで文字列を出していたから、それが原因だろうと思い、printfをputsに変更したところStackOverflowHookは引っかからなくなった。


タスク2個の内容はほぼ同じで、違いはデバッグメッセージだけ。中身は文字列を出力した後、while(1);で無限ループとする。協調的マルチタスクでこのコードを実行すると、最初に呼ばれた関数の中で無限ループに入り、次のタスクへ処理がわたらなくなる。
今回のFreeRTOSもまだSysTickの初期化は行っていないから、実質的には協調的マルチタスクとなっている、はずだった。しかしなぜかTask1もTask2もデバッグメッセージを出力している。
port.cのなかでSysTick関係のレジスタを叩いているようなので、この中で初期化を行っているのかもしれない。

Task1のループで500ms待って文字出力、Task2のループで1000ms待って文字出力、という機能を追加したところ、おおよそそれくらいのタイミングで文字が出てきている。ということでマルチタスクの動作はできているらしい。


とりあえず最低限のFreeRTOSの動作は確認できた。USART周りは低レベルで書いてあったりするけど、動作していることに違いはない。あとは低レベルな部分をRTOSで書き直したりすれば良いのかな。
RTOSを使えば協調的マルチタスクと違って、勝手にOSがマルチタスクっぽくやってくれるので、気を使うところが少し減るかも。OSはOSで面倒な部分もあるんだろうけど。
でも最低限動作はしてるんだから、少しずつ改善していけば使えるようになるはず。



今回ハマったところまとめ
・FreeRTOSのデモを使わなくても、すでにあるコードに追加して動作させられる
→GCC使ってる人はデモ使うの面倒らしいし
・OSが使う割り込み3個はマクロで実装

以外に少ない。思ってたより簡単だったかも。

2016年3月17日木曜日

リミット有りのエンコーダ入力

前回のエントリでは、エンコーダの位置を絶対位置で取り出しました。しかし、この方法では音量調整に使おうとすると、音量を下げて0を通過すると最大に戻るという問題点があります。ということで一定範囲に数値を収める方法について。

まずエンコーダの初期化(RCC,GPIO,TIM)は基本的に前回と同じです。ただし、TIM_SetAutoreloadの引数には0xFFFFを渡します。そうすることによりTIMの出力は0-65535の範囲を取ります。
次にカウント値を取り出すわけですが、TIMx->CNTはint_16でキャストして取り出します。そうすると正回転の時に正の値、逆回転の時に負の値を取り出すことができます。後はこの符号ありの数値を積算していけば、電源ONの時点からの絶対位置を取り出すことができます。
しかしそのままではせいぜい32bitや64bitの範囲でしか動作しません。なので積算後に範囲外に出た場合は範囲内に戻るようにします。
また、CNT値は取り出した後にちゃんと0に初期化してやります。そうすれば次回に取り出すときは、今回から次回の間に変更されたパルス数を読むことができます。

static int Value = 0;

if (TIM3->CNT) {
    Value += (int16_t)TIM3->CNT;
    TIM3->CNT = 0;

    if (Value > +100) { Value = +100; }
    if (Value < -100) { Value = -100; }

    printf("value:%4d\n", Value);
}

実際にはこのような処理になると思います。適当なタイマ割り込みやスレッドでこのような処理を行えば、上の例では+100から-100の間の数値として読み込むことができます。
他の使用方法として、タイマ割り込みなど正確な周期でTIMx->CNTの読み込みとリセットを行えば、周期とカウント値で速度を計測することも可能です。

STM32F1のエンコーダインターフェースモード

STM32F1のTIMxについてるエンコーダインターフェースボードを試してみた。

テストに使用したエンコーダはこれ
http://akizukidenshi.com/catalog/g/gP-00292/
1回転24パルスの機械接点タイプのエンコーダだ。

ソースコード
void TIM3_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TIM_SetAutoreload(TIM3, 24 * 2 - 1);
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI1, TIM_ICPolarity_Falling, TIM_ICPolarity_Falling);
    TIM_Cmd(TIM3, ENABLE);
}

最初はRCCでクロックの供給を行う。次にGPIOを初期化する。今回はマイコンでプルアップを行い、エンコーダはGNDに落とした。TIM3のCH1、CH2はGPIOA6、7に接続されているので、GPIOAの6と7をIPUとして初期化する。次にTIM_SetAutoreloadでカウンタの上限を指定する。またTIM_EncoderInterfaceConfigで動作モードを指定する。最後にTIM_Cmdで動作を開始させる。

TIM_EICの引数にはカウントモード(TIM_EncoderMode_TIhoge)と極性(TIM_ICPolarity_hoge)を渡す。
カウンタモードはTI1ならCH1のエッジでカウント、TI2ならCH2のエッジでカウント、TI12ならCH1とCH2それぞれのエッジでカウントを行う。
極性はRisingで立ち上がり、Fallingで立ち下がりエッジを使うことになると思うが、カウンタは立ち上がり、立ち下がりエッジの両方でカウントを行うため、実質的にはあまり違いはないと思う。ただ今回はプルアップで最初のエッジが立ち下がりのため、Fallingを指定した。

それとTIM_SAで指定した24*2-1という数字だが、これはエンコーダのパルス数(1回転24パルス)に立ち上がりと立ち下がりエッジで2回分、それから0始まりのために-1となる。カウントモードでTI12を指定した場合は24*4-1とする。この設定でわかるように、エンコーダは1回転24パルス出力でも、マイコンからはその2倍(あるいは4倍)として認識することができる。1回転24パルスのエンコーダでも実際には96パルスという比較的高分解能として使えるわけだ(ただしヒューマンインターフェースの場合は1クリックの途中で値が変わるのは非常に気持ち悪いが)。

エンコーダでカウントアップしていき上限に達した場合は0にリセットされ、カウントダウンで0を下回った場合はTIM_SAで指定したカウント値に飛ぶ。そのためカウント値の監視がカウンタのリセットに間に合わない場合にはもっと大きな数値を指定するとか工夫する必要があると思う。音量の調整などに使用するために範囲を0-50とかにすると、音量を下げて0になった次の瞬間に50に飛んで最大音量となるため、この辺りはうまく処理する必要があると思う。


機械式のロータリーエンコーダはチャタリングが酷く、以前にソフトウェアで読んだ時も素早く回すととても使いものにならないような結果になった。今回も同じような結果を予想していたが、CRフィルタも何も使用せず、しかもマイコンの外部部品はエンコーダのみでプルアップ抵抗もマイコン内蔵という悪条件ながら、ほとんど読み込みエラーが発生しなかった。操作ダイヤルなど、相対的な動作で構わない用途にはほぼ問題なく使用できると思われる。モーターの絶対位置を計測したい等の用途では何らかの対策が必要だと思うが、モーターの回転数を測るような、0.5パルスずれた程度ではあまり問題にならない用途では十分だと思われる。


ところで、STM32F1リファレンスマニュアルには「エンコーダインタフェースモードは、単に方向選択を含む外部クロックとして動作します」と書いてある。ということはCH1のパルスでカウントし、CH2でインクリメントとデクリメントを切り替えられるのか?と思って試してみたが、そういう動作はできないようだ。少なくともエンコーダインターフェースボードではノイズとして処理されてしまうらしい。
もしもこのような動作が可能であればステッピングモータドライバに対するパルス出力(回転方向の1bitとステップ数のパルス)のような信号を受け取ることができると思ったんだけど、それはできないみたい。

2016年1月10日日曜日

STM32F1のGPIO

STM32F1のGPIOマッピング。STM32F103VE向け。STM32F103CBでも使えるはず。

一部のペリフェラルに対するピン。

ハイフンはリマップが存在しない、スラッシュはピン割当が存在しない事を表す。
リファレンスマニュアルによるとSPI3にもリマップが存在するようだが、103VEのPDFには書かれていなかったのでここでも表記していない。

ピンに対するペリフェラル。


STM32F1のGPIOは結構ペリフェラル割り当てられてる気がするけど、表で見ると意外とスカスカ。


STBee F4も発売されたことだし、僕も本格的にF4に移行したいかなと思いつつ、まだSTBeeで足りてるのでわざわざ移行する気力が。もっとも、F4のほうが容量多くて早いのに数百円安いので、64ピンで足りるならF4使ったほうが良いかもしれないけど。

2016年1月9日土曜日

STM32F1のUSARTを割り込みで送信

USARTの送信をポーリングで行う場合、115.2kbaudで32文字送った場合、2.8msec程度かかる。わずかといえばわずかだが、この時間はほとんど無駄にループさせているため、CPUのリソースが浪費されている。一旦データをバッファにコピーし、送信処理を終了してから、割り込みなどを利用してデータを送ればこの時間を200usec未満にすることができる。

1) 変数を用意する

#define USART1_TX_Buff_Size (128)
uint8_t USART1_TX_Buff[USART1_TX_Buff_Size];
uint16_t USART1_TX_Buff_SetCounter = 0;
volatile uint16_t USART1_TX_Buff_GetCounter = 0; // 割り込みで更新するのでvolatileをつける 

USART1_TX_Buff_Sizeでバッファサイズを設定する。その後USART1_TX_Buffでバッファを用意し、SetCounterとGetCounterを用意する。Setは挿入位置、Getは取り出し位置を記録する。GetCounterは割り込みの中で変更されるため、コンパイラの最適化によってwhileによる監視等が削除される場合がある。そのためGetにはvolatileをつけて最適化を回避する。

2) 下位関数を用意する

uint16_t USART1_TX_Buff_SetCounter_Inc(void) {
    uint16_t counter = USART1_TX_Buff_SetCounter;

    counter++;

    if (counter == USART1_TX_Buff_Size) {
        counter = 0;
    }

    return(counter);
}

uint16_t USART1_TX_Buff_GetCounter_Inc(void) {
    uint16_t counter = USART1_TX_Buff_GetCounter;

    counter++;

    if (counter == USART1_TX_Buff_Size) {
        counter = 0;
    }

    return(counter);
}

SetCounter_IncとGetCounter_Incはその名の通りSetとGetをインクリメントする。ただし関数内では変数に反映せず、戻り値として値を返すので関数を呼び出したところで責任をもって変数に反映する必要がある。

3) 割り込みを有効にする

    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_ITConfig(USART1, USART_IT_TC, ENABLE);

USARTを初期化する際にNVICも初期化する。USART自体の初期化は前回のエントリを参照。

4) 割り込みハンドラを用意する

void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_TC))
    {
        if (USART1_TX_Buff_SetCounter == USART1_TX_Buff_GetCounter) {
            USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        } else {
            USART_SendData(USART1, USART1_TX_Buff[USART1_TX_Buff_GetCounter]);

            USART1_TX_Buff_GetCounter = USART1_TX_Buff_GetCounter_Inc();

            USART_ClearITPendingBit(USART1, USART_IT_TC);
        }
    }
}

内部処理としてはSetとGetの差が0なら割り込みを停止し、割り込み自体はクリアせずに終了する。データが存在する場合はUSART_SendDataで送信を行い、Getをインクリメントする。その後割り込みをクリアして終了する。

5) 送信用の関数を用意する

void USART1_putc(unsigned char ch) {
    uint16_t inc;
    
    do {
        inc = USART1_TX_Buff_SetCounter_Inc();
    } while (inc == USART1_TX_Buff_GetCounter);

    USART1_TX_Buff[USART1_TX_Buff_SetCounter] = ch;

    USART1_TX_Buff_SetCounter = inc;

    USART_ITConfig(USART1, USART_IT_TC, ENABLE);
}

putcの中ではバッファへのコピーのみを行う。ただし未送信のデータを破壊しないようにdo whileで待機する。その後にバッファへコピーを行い、SetCounterを更新する。最後に割り込みを有効にする。


割り込みはデータの流れが面倒になるのと、ソフトウェアもそれなりに複雑になる。毎度のことだがほんとうに必要かどうかを考えてから実装しよう。

STM32F1のUSARTをDMAで受信

USARTを受信するにはポーリングでは不都合。とりあえずタイマ割り込みを使うのが手っ取り早いが、タイマ割り込みではデータが来るたびに割り込み処理を行う必要がある。DMAを使うとソフトウェアではほとんど処理する必要が無いため、気分的に早くなってる気がする。

1) 変数を用意する

#define USART1_RX_Buff_Size (256)
uint8_t USART1_RX_Buff[USART1_RX_Buff_Size];
uint16_t USART1_Rx_Buff_GetCounter = USART1_RX_Buff_Size;

今回はバッファサイズを256としたが、USART1_RX_Buff_SizeをGCCのオプションで設定すればmakefileとかで変更することもできると思う。バッファサイズを小さくし過ぎると取りこぼすデータが発生するかもしれない。上限は数十kbyteあたりだが、実用上は数百バイトもあれば十分だろう。

2) USARTその他を初期化する

void USART1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);


    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Init(USART1, &USART_InitStructure);


    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)(USART1_RX_Buff);
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = sizeof(USART1_RX_Buff) / sizeof(USART1_RX_Buff[0]);
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_DeInit(DMA1_Channel5);
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel5, ENABLE);

    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);


    USART_Cmd(USART1, ENABLE);
}

まずGPIOとUSARTを初期化する。これはポーリングでの受信や割り込みでの受信と全く同じ。TXのPin9はAF_PP、RXのPin10はPullUpで初期化する。
ペリフェラルを初期化したらDMAの初期化を行う。DMAの初期化も特に変なところはないが、ModeをCircularに設定する。優先度はMediumとしたが、例えばSPIを最大速度でVeryHighにした場合はUSARTを取りこぼすかもしれない。USARTは非同期で送信されるため、可能なかぎり優先度を上げておくほうがいい。
それと大事なことだが、DMAを初期化する前にDMAのクロックを有効にしておく必要がある(RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);)。

3) 受信関数

int USART1_getc(void) {
    int counter = DMA_GetCurrDataCounter(DMA1_Channel5);

    if (counter == USART1_Rx_Buff_GetCounter) {
        return(-1);
    }

    uint8_t ch = USART1_RX_Buff[USART1_RX_Buff_Size - USART1_Rx_Buff_GetCounter--];

    if (USART1_Rx_Buff_GetCounter == 0) {
        USART1_Rx_Buff_GetCounter = USART1_RX_Buff_Size;
    }

    return(ch);
}

この関数ではDMAを確認し、データがない場合は-1を返す。データが有る場合は最初に受信したデータを返す。この関数はバッファからデータを読む機能しかない。1行を受信したい場合は上位の関数で改行文字を検出したりする必要がある。



今回はUSARTの受信をDMAで行った。割り込みを使う場合と比べてちょっとくらい早くなった気がする。
USARTの受信はそんなに大変じゃないけど、送信は結構面倒。ただ送信をバッファリングできれば数msecとか数十msec単位で高速化できたりするので試してみる価値はある。

STM32F1のDMA

STM32F1のDMA接続一覧。ソースはRM0008のRev11日本語版。書き間違えてたらごめんなさい。





1枚目がDMAに対するペリフェラルの関係、2枚目がペリフェラルに対するDMAの関係。

例えばデータロガーを作る場合、SDカードを接続するSPI1とセンサを接続するSPIはDMA使用で、コンソールに使用するUARTもDMAを使用したい、という場合、SPI1,SPI2を使用し、UARTは4を使用することになると思う。
まずSPIだが、とりあえずSDカードはSPI1に固定しておいたほうが良い。これはSPI1では36MHzまで、それ以外では18MHzまでという制限があるため、なるべく早く通信したいSDカードはSPI1に接続する。センサはSTMの場合10MHzまでの場合が多いので、SPI2の9MHzでも十分という判断。
次にUART4を使用する理由だが、USART2のDMAは1_6と1_7が割り当てられている。しかし1_6,1_7にはI2C1も割り当てられており、I2CをDMA化したい場合に困ることになる。一方UART4なら他にはADC3とタイマしか割り当てられていないため、競合する可能性は低いと判断した。またUART4のピンにはリマップでUSART3を割り当てることが可能であり、UART4で足りない場合はUSART3を使用することが可能となる(もちろんDMAはSPI1と競合するため割り込みでしか送受信できないが)。

その他のDMAソースはADCとDAC、それからTIMしかなく、ADCはDMA1_1を割り当てることが可能、ただしDAC2は競合がTIMのみだが、DAC1はUART4_RXと競合しているため、DACを2ch使う場合は選択する必要がある。もっとも、DACを使いたい場合も大半はDMAソースにTIMを使うと思うけど。

定期的にデータを送りたい場合(GPIOでパルスを出したりDACでアナログ波を出したり)はタイマに同期してDMAを蹴ることになると思うが、タイマに割り当てられたDMAは結構数があるとはいえ、他のペリフェラルを目一杯使っているとかなり厳しいことになりそう。
ただしADCの場合はADCのトリガをTIMに接続し、DMAのトリガはADCに接続するため、DMAとTIMの接続は不要。


プログラムは実際に動作するハードウェアに合わせて作ることになると思うが、とりあえず上記のようなSPI1/SPI2/I2C1/UART4あたりをDMA化するライブラリを作っておけば互いに競合せずに後々便利かもしれない。ただしSTBee Miniに使用されているSTM32F103xCにはUART4は搭載されていない。STBee Miniを使う場合はSPI2のDMAを使用せずにUSART1を使うか、I2C1のDMAを使用せずにUSART2を使用することになるはず。
結局大抵の組み合わせのドライバを作っておけば便利だよ!ってなっちゃうんだなぁ。。。

今回はUARTは1本のみの想定だが、缶サットの場合は 1)テレメ 2)GPS 3)JPEGカメラ のように複数のUARTを使用することになると思う。そういう場合はまたいろいろ考える必要がある。
例えばJPEGカメラやGPSはコンスタントにデータが流れ込んでくるために受信はDMA化しておくと都合がいい。しかしGPSに送信する必要はあまり無く、JPEGカメラでもせいぜい数十バイトを送れば十分なので、送信はDMA化する必要はない。対してテレメの場合、受信はあまり多くないだろうが、送信はある程度のデータ量になるのでDMA化しておくと良いかもしれない。

まぁ行き当たりばったりで作るとろくな事にならないので、最初にどんな機能がほしいかを考え、それを作るためにどういう配置をすればいいかというのをしっかりと検証しておくと言いと思う。

2016年1月6日水曜日

FatFsでBack Write

FatFsでBack Writeっぽいのを実装してみました。つまりCMD24/CMD25のデータをRAMにキャッシングしておき、DMA(と転送完了割り込み)で処理し、その間は他の処理が可能、ということです。Back Writeを使う利点は、ROMにデータを転送している間の時間を他の処理に使用することが可能という点です。



BW転送中のキャプチャです。SPIバスも表示されていますが、SPIクロックが36MHzに対してロジアナは2MHzですからデータは破壊されています。「信号が動いていれば何か転送してるかも」くらいです。もちろん2MHzでは見れない瞬間的な波形は表示されません。

一番下の黄色はメインループでGPIOをトグルしています。その上のオレンジは1msecの割り込みでGPIOをトグルしており、だいたい500Hzになっています。この1kHz割り込みはビジーチェックや転送のキックにも使用しています。

上の画像では4KiBを転送しています。オレンジの波形と比較するとおおよそ30msecで転送終了しているので、転送速度は135Kbyte/sec程度です。これはCMD25のビジーチェックが不適当なためだと思われます。適切に実装すればあと6msecくらいは早くなる気がします。それでも170kByte/sec程度ですけど。
メインループに戻ってメインループが動作しているのは8msecくらいです。転送に必要な30msecのうちの8msecが他に使えるということですから、25%くらいを別に使用可能ということになります。

以前のエントリに書いている転送速度は「f_write + f_syncの時間」です。対して今回の速度はf_mountでFATを開くところから、f_mount(null)でFATを閉じるところまでの時間です。またファイルはFA_CREATE_ALWAYSで開いているので、ファイルサイズを0に変更するための処理も必要になります。データロガーなどファイルは開きっぱなしで後ろに書き加えるだけの場合はもっと早くなると思います。

BWを使用しない場合は転送時間は33msecくらいです。BW使用時はビジーチェックが不適当だと仮定すると、BWを使用しないほうが転送時間は短いはずですが、1割ほど余計に時間がかかっています。BW使用時は30msec-8msecで22msec、BW未使用時は33msecとすると3割ほどCPUリソースを有効活用できるようです。

BWを使用した場合、大量のRAM(大半の時間はこのRAMは未使用)を消費したり、Write中にReadが発生しないように(あるいはその逆が起こらないように)するために様々な配慮が必要だったり、割り込み処理が非常に面倒だったり、とソフトウェアの面ではかなり不利になります。またプログラムも1.3Kほど増えるようです。
BWを有効に活用できるのは非常にメモリに余裕があり、ソフトウェアの開発も十分に専念して行える場合に限られるでしょう。
SDカードの書き込み速度は8KiBくらいをまとめて書く場合で、データロガーの場合はダブルバッファにして文字通り「書き込みが遅れた場合のバッファを確保する」必要があるでしょう。またBWに使用するメモリはFAT操作などもありますから実データ+数ブロック分が必要と思われます。そのためBWを十分に活用するにはRAMはバッファ領域だけでも30Kほど欲しくなります。

今回Back Writeを実装してみて、かなりいろいろな工夫が必要なことがわかりました。この工夫は直接書き込む場合には必要ありませんから、実装にはかなりのコストが必要になります。バックグラウンドで書き込もうとする場合には本当にその必要があるかを考えたほうがいいと思います。

2016年1月3日日曜日

STM32F1でメモリ間コピー



ちょっと思う所ありメモリ間コピーを試してみた。上から 1)memcpy関数 2)forでコピー 3)DMAでM2M 4)DMAでM2M(2本並列で動作) という感じ。
転送サイズは2KiB。

1はそのままその通り、string.hのmemcpy関数を使用した。だいたい25usくらい。
2は32bitポインタ間で512回forコピー。67usくらい。
3はDMAでM2M。優先度はVeryHigh、チャンネルは2-5。8usくらい。
4は3とほとんど同じ、ただし2-5で優先度はLow、そして2-4のVeryHighを同時に走らせる。これはDMAが競合した状態でどうなるかを見ている。9usくらいかな。

感想:memcpy早い。どんな最適化してるんだよ。

とりあえず、DMAのM2Mを使うのが最速っぽい。条件によってはコアからは干渉が不要で、初期化の数usec以外はほっといても勝手に転送してくれたりもする。DMAの初期化を含めても2KiB程度なら10usec未満で完了するらしい。

SDカードの速度

SDカードの書き込み速度を計測しなおしてみた。



SPI接続で3種類のSDカードの速度を計測してみた。

・TS 32GB
Transcend製
32GB/クラス10/UHS-I/300xとか書いてあって早そうなパッケージ
多分これ:http://www.amazon.co.jp/dp/B00APCMME0/

TO 4GB
東芝製
4GB/クラス4
これ:http://www.amazon.co.jp/dp/B00K187GP8/

TO 8GB
東芝製
8GB/クラス10/UHS-I
これ:http://www.amazon.co.jp/dp/B00K187ILA/


SDの規格作ったグループに東芝いるんだから早いだろ、と思って東芝の2種類を買ってみたけど、意外と遅かった。これは「Transcendすげぇ!」なのか、「技術の進歩すげぇ!」なのか、判断に迷うところ(おそらく8GBや4GBはかなり古い設計)。
あと8GBは2000円程度、4GBでも1500円程度する。32GBは現在1400円で売ってるので、小容量じゃないとダメという訳の分からない理由でも無い限りは32GBのメディアで良さそう。書き込み早いし、安いし、大容量だし。

//***

念のためにロジアナで確認してみる。上がTS32で4ブロックのマルチブロックライト。下がTO8で4ブロックのマルチブロックライト。





1回しかキャプチャしてないし、誤差だろ と言われればそれまでだけど、Transcendは1ブロック目のビジーがかなり長い。対して東芝はあまり間をおかずに2ブロック目を転送している。対して転送終了後のビジーは切れてて申し訳ないけど、Transcendは1.7msecほど、東芝は1.9msecほどと、Transcendのほうが早い。しかしCSがLの時間は東芝が2.65msec、Transcendが3.37msecと、東芝のほうが2割少し早い。
現在使っているロジアナはこのシリーズではかなりメモリが大きい方だけど、それでもクロック100MHzだと12msecくらいしかキャプチャできない。SDカードへの転送はもっと時間がかかるので、全体を見渡すことができない。

//***

次にもう一つ比較。





1ブロック書き込みの時の波形。Transcendは1トランザクションが2.9msecほどで終了している。対して東芝では27msecを超えており、ロジアナのメモリには入りきらない。FATで数ブロックを書き換える場合はマルチブロックライト以外にもFATテーブルを書き換えるためのシングルブロックライトがかなりの割合を占める。そのため、シングルブロックライトに数十msecを必要としている場合、マルチブロックWが数割早い程度では回収できないほどの遅れを生じてしまう。Transcendが倍くらい早いのは、TranscendがシングルブロックWをうまくバッファリングしてるのか、東芝がシンブルブロックWが下手なのか、または双方かもしれないけど。

念のため、シングルブロックがアホな処理をやっている、という可能性を潰すために512バイトでもマルチブロックライトで書き込んでみた、ちゃんと計ったわけではないが、8KiBを書いて0.39Mbyte/secほどだった。しかしロジアナで見える範囲では数十msecのビジーが発生しているようには見えない。マルチブロックライトで1ブロックだけの場合は内部でシングルブロックと等価な処理を行っているのかもしれないが、ビジーはそんなに長くない、しかし総合的な処理はシングルブロックライトと変わらないし、というちょっと謎な挙動となった。これ以上を調べるのは手持ちの機材ではつらそうだ。

//***

ロジアナでキャプチャしながらブログ書いてるけど、最初はこんなに長く書く気はなかった。でもなんだかムキになってきてもうちょっと続く(計画性がないからこんなことになるんだ)。

//***





こちらは10MHzでサンプリングした波形。SPIクロックは36MHzなのでデータとしての正しさはあまりない。ただCSの形はおおよそわかるので、1トランザクションの長さはある程度信頼できると思う。
が、波形が見えたところでどの波形がどういう通信なのかがわからないのであまり意味が無い。一応双方とも同じデータが流れているはずで、MISO(一番下の茶色)でLowが続いているのはビジー状態だと思う。そう考えると東芝の方はビジーが長い気がする。

//**





Transcendは10MHz、東芝は5MHzで4ブロック書き込みをキャプチャ。東芝のカードは1個目のシングルブロックで85msec以上のビジーが発生しており、これがネックっぽい。というかこれはひどい。。。

//***

ところで、SDカードでは少し不思議な挙動がある。東芝の場合は何回も書いていると数割くらいの確率で書き込み速度が倍増くらいになる。Transcendの場合は、最初の数回がものすごく遅くても、何回も書いていればちゃんと速度が出るようになる。



このキャプチャは東芝8GBで16ブロック(8KiB)を書いた時のキャプチャ。この時には0.6Mbyte/secを超えている。相変わらずTranscendには劣るが、それでもかなり早くなった。

ちなみにSDカード初期化直後のキャプチャはこれ。



シングルもマルチも関係なく512バイト転送ごとに70msec前後のビジーが発生している。明らかに駄目な感じ。この時は0.021Mbyte/secくらい。21.5kByte/secである。ISDNの2.6倍くらいの速度。遅すぎ。

//***

なんか「実は電源が貧弱で安定動作していません」みたいなオチがしっくりきそうな気がしてきた。
現在はマイコンボードのLDOで3.3Vを作って、そこからちょっと引き回して470uFの電コン(これしかなかった)に突っ込んで、microSDブレークアウトボードの下で0.1uFという構成。
もうわけがわからないよ。

2015年12月23日水曜日

SPIの速度を変更する

STM32F1のSPIはCR1のBRに設定する。これはStdPerphLibではSPI_Initのみで変更できる。ということで自前で関数を作る必要がある。
static uint16_t SPI_Baud = 0;

static void SPI_BaudLow(void) {
    SPI_Baud = SPI1->CR1 & 0x0038;

    SPI_Cmd(SPI1, DISABLE);
    SPI1->CR1 = (SPI1->CR1 & 0xFFC7) | 0x0038;
    SPI_Cmd(SPI1, ENABLE);
}

static void SPI_BaudHigh(void) {
    SPI_Cmd(SPI1, DISABLE);
    SPI1->CR1 = (SPI1->CR1 & 0xFFC7) | SPI_Baud;
    SPI_Cmd(SPI1, ENABLE);
}

SPI_BaudLowではSPIを最低の速度で駆動する。72MHzなら281.25kHzとなる。またSPI_Baudに速度をバックアップする。
SPI_BaudHighではバックアップされたボーレートをレジスタに書き戻す。これはSPI_Initで設定した値になる。またSPI_BaudLowを呼ばれていない場合はSPI_Baudには0が入っており、これは分周比2となる。

それとボーレートの設定は通信中に変更してはいけないらしいのでSPI_Cmdでペリフェラルを終了させている。
ちなみにSPI_Cmdの中ではCR1のSPEを変更するだけなので、本来のSPI停止方法ではない。STM32F1のリファレンスマニュアルによると、RXバッファに値が入るまで待つ→RXバッファを読む→TXバッファが空になるまで待つ→ビジー状態でなくなるまで待つ→SPIを無効にする、という手順を踏む必要がある。ただこの方法は結構手間なのと、そもそも通信中にSPIを停止させるための手段なので、非通信時に変更したいのでSPI_Cmdだけで問題ないと判断した。

SDカードをSPIで使用するには、初期化時には100-400kbaudにする必要があるらしい。とりあえず36MHzでも問題なく初期化できているが、どうせ初期化するには200msecほどかかるので280kbaudでも36Mbaudでも大して変わりはない。

2015年12月22日火曜日

DMAでFatFs









上から
1) ソフトウェアでファイル読み込み
2) DMAでファイル読み込み
3) ソフトウェアでファイル書き込み
4) DMAでファイル書き込み
です。

1がおよそ11msec、2がおよそ3.5msec、3がおよそ22msec、4がおよそ12msec、という感じです。読み込みでは3倍、書き込みでは1.8倍 くらいでしょうか。書き込みではあまり高速化していませんが、1ブロック書き込むごとにビジーが2.7msecほどあって、これが2回あるので、ビジー待機だけで5msec以上になります。このビジー待機はおよそ1回の書き込みごとに1回の待機となりますから、1回に数kbyteを書くのも512byteを書くのも同じ程度の時間になります。つまり書き込みは一度に大量に書くほど早くなります。


ということでSDカードの書き込み動作もできました。あとはマルチブロックR/Wを作れば大容量を転送した場合のデータレートを計測できるようになります。
エラーチェックはほとんどやっていないし、挿抜検出も行っていないので実用には程遠いですが。
それと本来の目的はDMAによるバックグラウンド書き込みをやりたいので、スタート地点にも立っていないといえますが。


ところで、今回検証に使用しているメモリはTranscendの32GB/C10のmicroSDです。すぐ見つかる場所にあったのがこのカードだけなので、他のメーカーとの比較は行っていません。個人的には東芝のカードが早そうな気がするので試したいのですが、何かのついでに購入したあとになります。

STM32F1のSPIをDMAで双方向

STM32F1のSPI1を分周比2(バスクロック36MHz)で双方向通信した時のキャプチャです。



2バイトか3バイト程度を送ったところで少し隙間が空いてしまいます。これはDMAの帯域が足りないために、転送に遅延が発生していると推測できます。

F1のSPIはDMA1_3にTXが、DMA1_2にRXが設定されており、双方向で通信する場合にはDMA1の2chを使用することになります。36Mbit/secは4.5Mbyte/secですから、それが双方向で9Mbyte/secとなります。
一方、DMAは1転送に8クロック必要らしいので、72MHz動作では9Mbyte/secが限界となります。あれ、帯域足りてるじゃん。

おそらく1chだけを転送する場合は9Mbyte/secがフルに出るのですが、複数の転送を行う場合は次のデータを選ぶための比較が発生するためにフルで使用することができないのでしょう。

気になるデータの取りこぼしですが、先に書いたとおりSPIのTXが3、RXが2で、DMAは数字の小さいほうが優先されますから、受信のほうが優先度が高くなります。そのため受信データが転送されていない場合は送信が一時的に停止されるはずです。

ということで、「SPIをDMA転送したらきっちりSPI帯域使い切れるぜ!」と思ったらDMAの帯域が足りないというオチでした。それでもソフトウェアでフラグをポーリングして1バイトずつ送るよりは早いんですが。
もちろん送信だけの一方向通信なら受信データを読む必要はありませんから、SDカードへデータを書く場合は帯域をフルに使えます。バス速度的には 書き込み速度>読み込み速度 というちょっと不思議な感じになりそうです。

STM32F1のSPIをDMAで転送

今回は送信だけ、と受信だけ、の処理です。全二重の送受信処理は以下の関数を変形するだけで作れます。

送信は一方的に送りつけるだけなので楽ですが、受信する場合はデータを送信する必要がなくても送信用のDMAを設定する必要があります。何もデータを送る必要が無いのに大容量のバッファを確保して初期化するのも馬鹿らしいので、1バイトの配列に初期値(0xFF)を設定し、MemoryIncをDisableにして送信してやります。

それから転送が終わったら責任をもってDMA関連をDisableにしてやりましょう。さもないと次にSPIで通信を行った時に謎の動作をします(謎というか、ハングアップするだけですが)。

ソフトSPIとハードSPI

STM32F1のSPI1にmicroSDソケットを接続し、FatFsで読みだしてみました。分周比は2でバスクロックは36MHzです。





この画像はSDカードのシングルブロックリードコマンドの時のキャプチャです。上がDMA使用、下がソフトウェアで通信しています。
おおよその目安ですが、ソフトウェアでは1ブロックを読むのに1.8msec程必要です。対してDMAでは1ブロックの読み込みに460-600usec程度です。およそ3倍の速度差があります。前回のエントリではDMAとソフトでは10倍ほどの差がありましたが、今回はSDカードの内部処理待ちが含まれるために差が少なくなったのだと思われます。

今回はシングルブロックリードですが、マルチブロックでのR/Wであればだいぶパフォーマンスが向上すると思います。まだSDカードの書き込みコマンドや、マルチブロックの処理のコマンドは作っていないので、そのあたりを作ったらもう一度比較してみたいと思います。