STM32からロシアのマイクロコントローラーK1986BE92QIに渡します。 音を生成して再現します。 パート3:TIM + DMA

エントリー

以前の記事で、 DMAに初めて触れた人について話しました。 その中で、DMA + SysTickを大量に作成しました。 この記事は、経験の浅い曲がったアプローチを考慮して、非常に具体的かつ複雑であることが判明しました。 経験を積んだので、この記事では、DMAを使用するより簡単で理解しやすい方法について説明します。

主な側面

前の記事から、DMAを開始するには以下が必要であることがわかりました。 厳密に定義された時間にデータを「発行」するために、SysTickシステムタイマーを使用しました。 このため、タイマーからの割り込みに常に入り、すべてのDMA転送を再開する必要がありました。 さらに、転送を再開するたびに、データのパケット全体が送信されたかどうかも確認しました。 同意すると、このアプローチはDMAのすべての利点を完全に無効にします。 人生を複雑にするとも言えます。 結局のところ、私たちのマイクロコントローラーには素晴らしいDMA +タイマーがたくさんあります。 したがって、割り込みを入力せずに、次の値をDACに送信する時間であることを彼自身がDMAに伝えるように、タイマーを1回設定できます。 また、以前は「基本」の送信モードを使用していたことを思い出してください。 彼は私たちに絶えず構造を停止し、復元することを強制しました。 DMAにはピンポンモードがあります。 その本質は次のとおりです。一次構造(前の記事で使用したもの)を使用して、データの最初の部分(配列)を転送します。 この時点で、代替(読み取り、2番目)構造を構成して、最初の構造が終了すると、停止せずに2番目に書き込まれたものの転送をすぐに開始できるようにすることができます。 2番目の構造による転送中-最初の構造を復元または変更できます。これにより、任意の長い転送を行う機会が与えられます。

DMAの構造、DMA自体、およびタイマーを使用したリンクチャネルの構成

覚えているように、各チャネルには独自の制御構造があります(この例では2つあります)。 前の記事の説明に従って構成します。 結果の設定は2つの変数に保存されます。 これらの変数から、構造を復元します。
構造のセットアップ。
//------------------------------------------------- // . //------------------------------------------------- #define dst_src (3<<30) // - 16  (). #define src_inc (1<<26) //   16    . #define src_size (1<<24) //  16 . #define dst_size (1<<28) //  16 .       . #define n_minus_1 (49<<4) //50  (-1) DMA. #define cycle_ctrl (3<<0) //-. struct DAC_ST { uint32_t Destination_end_pointer; //   . uint32_t Source_end_pointer; //    uint32_t channel_cfg; // . uint32_t NULL; // . } __align(1024) DAC_ST; //    1024 . struct DAC_ST DAC_ST_ADC[32+32]; //     . /// = 16 , / = 16 ,  , 50 , -. uint32_t DMA_DAC_InitST_PR = dst_src|src_inc|src_size|dst_size|n_minus_1|cycle_ctrl; uint32_t DMA_DAC_InitST_ALT = dst_src|src_inc|src_size|dst_size|n_minus_1|cycle_ctrl;
      
      



お気づきかもしれませんが、32個のプライマリ構造と32個の代替構造にメモリを割り当てました。合計でRAMに1キロバイトのメモリが割り当てられます。 変位に苦しむことがないように、私はこの一歩を踏み出しました。 将来的には、オフセットを作成して2つの構造を残すことは簡単です。 次に、これらの構造に記入する必要があります。
塗りつぶし構造
 //  . DAC_ST_ADC[10-1].Destination_end_pointer = (uint32_t)C_4 + (sizeof(C_4))/2 - 1; //      (C_4 -      100 ). DAC_ST_ADC[10-1].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA); //   ( )   (  DAC). DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR); //   . DAC_ST_ADC[10-1].NULL = (uint32_t)0; // . //  . DAC_ST_ADC[10-1+32].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1; //     (C_4 -      100 ). DAC_ST_ADC[10-1+32].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA); //   ( )   (  DAC). DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT); //   . DAC_ST_ADC[10-1+32].NULL = (uint32_t)0; // .
      
      



ご覧のとおり、これら2つの構造の違いは、データソースの最終アドレスのみです。 プライマリでは、アレイの中心(送信は先頭から中央へ)、セカンダリエンドでは(中央から末尾への送信)を示します。
次にDMAを構成します。
 #define CFG_master_enable (1<<0)//   . #define PCLK_EN_DMA (1<<5)//   DMA. //  DMA. RST_CLK->PER_CLOCK|=PCLK_EN_DMA; //  DMA. DMA->CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC; //   . DMA->CFG = CFG_master_enable; //  DMA.
      
      



まあ、それはチャンネルを構成するためだけに残っています。 しかし、どれですか? 前の記事で示した表を参照できます。 しかし。 前の記事を書いた後、私は自分の間違いを知らされました。 実際、そのテーブルは完全に正しいわけではありません。
より正確な表は次のようになります。
 /** @defgroup DMA_valid_channels DMA valid channels * @{ */ #define DMA_Channel_UART1_TX ((uint8_t)(0)) #define DMA_Channel_UART1_RX ((uint8_t)(1)) #define DMA_Channel_UART2_TX ((uint8_t)(2)) #define DMA_Channel_UART2_RX ((uint8_t)(3)) #define DMA_Channel_SSP1_TX ((uint8_t)(4)) #define DMA_Channel_SSP1_RX ((uint8_t)(5)) #define DMA_Channel_SSP2_TX ((uint8_t)(6)) #define DMA_Channel_SSP2_RX ((uint8_t)(7)) #define DMA_Channel_ADC1 ((uint8_t)(8)) #define DMA_Channel_ADC2 ((uint8_t)(9)) #define DMA_Channel_TIM1 ((uint8_t)(10)) #define DMA_Channel_TIM2 ((uint8_t)(11)) #define DMA_Channel_TIM3 ((uint8_t)(12)) #define DMA_Channel_SW1 ((uint8_t)(13)) #define DMA_Channel_SW2 ((uint8_t)(14)) #define DMA_Channel_SW3 ((uint8_t)(15)) #define DMA_Channel_SW4 ((uint8_t)(16)) #define DMA_Channel_SW5 ((uint8_t)(17)) #define DMA_Channel_SW6 ((uint8_t)(18)) #define DMA_Channel_SW7 ((uint8_t)(19)) #define DMA_Channel_SW8 ((uint8_t)(20)) #define DMA_Channel_SW9 ((uint8_t)(21)) #define DMA_Channel_SW10 ((uint8_t)(22)) #define DMA_Channel_SW11 ((uint8_t)(23)) #define DMA_Channel_SW12 ((uint8_t)(24)) #define DMA_Channel_SW13 ((uint8_t)(25)) #define DMA_Channel_SW14 ((uint8_t)(26)) #define DMA_Channel_SW15 ((uint8_t)(27)) #define DMA_Channel_SW16 ((uint8_t)(28)) #define DMA_Channel_SW17 ((uint8_t)(29)) #define DMA_Channel_SW18 ((uint8_t)(30)) #define DMA_Channel_SW19 ((uint8_t)(31)) #define IS_DMA_CHANNEL(CHANNEL) (CHANNEL <= (DMA_Channels_Number - 1)) /** @} */ /* End of group DMA_valid_channels */
      
      



このテーブルは公式フォーラムで私に与えられましたが、公式ライブラリーにもあります。 タイマー1を使用します。=>チャンネルは10番目です。
 // . DMA->CHNL_ENABLE_SET = 1<<10; //  10 .
      
      



次の機能があります。
 //------------------------------------------------- // DMA    DAC. //------------------------------------------------- void DMA_to_DAC_and_TIM1 (void) { //  . DAC_ST_ADC[10-1].Destination_end_pointer = (uint32_t)C_4 + (sizeof(C_4))/2 - 1; //      (C_4 -      100 ). DAC_ST_ADC[10-1].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA); //   ( )   (  DAC). DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR); //   . DAC_ST_ADC[10-1].NULL = (uint32_t)0; // . //  . DAC_ST_ADC[10-1+32].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1; //     (C_4 -      100 ). DAC_ST_ADC[10-1+32].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA); //   ( )   (  DAC). DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT); //   . DAC_ST_ADC[10-1+32].NULL = (uint32_t)0; // . //  DMA. RST_CLK->PER_CLOCK|=PCLK_EN_DMA; //  DMA. DMA->CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC; //   . DMA->CFG = CFG_master_enable; //  DMA. // . DMA->CHNL_ENABLE_SET = 1<<10; //  10 . }
      
      



タイマーを満たす

まず、タイマーを有効にする必要があります(タック)。
 #define PER_CLOCK_TIMER1_ONCLK (1<<14) //    1. RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK; // .
      
      



そして今、実際には、理解し始める価値があります。 3つのタイマーはすべて同じ機能を備えています。 少なくとも一見。 各タイマーには非常に豊富な機能=>多くのレジスタがあります。 しかし、それらは非常に理解しやすいです。 私はタイマーの仕事を勉強する方法で熊手を会いませんでした。 タイマーで制御できるようにDMAを構成したことを覚えています。 この目的のために、一定の時間だけ待って次のデータのバッチを転送するだけで十分です。 タイマーには、4つの「比較」チャネルとメインカウンターがあります。 メインカウンターを使用すれば十分です。
タイマーのメインレジスタを見てください。




ここで、タイマーを有効にする必要があります。
 #define CNTRL_CNT_EN (1<<0) //  . TIMER1->CNTRL |= CNTRL_CNT_EN; //  ,  =  .
      
      



テストとして、0xFFFFにカウントします(後で時間間隔を扱います)。 デフォルトでは、スコアは0から始まります。
 TIMER1->ARR = 0xFFFF; // ...
      
      



次に、タイマーをDMAに関連付ける必要があります。 送信が行われるイベントとして、タイマー1でCNT == ARRを選択します。これは、CNTタイマーの現在の値とARRレジスタの番号の単純な比較です。
このために、レジスタDMA_REがあります
ここでは、カウンターが目的の値に達したときに接続を選択する必要があります。




セットアップが完了したと言えますが、そうです。 これで、タイマーは、分周器なしで8 MHzのHCLK周波数からクロックされます。 値0xFFFFはすぐに到達します。 そして、伝送を追跡することはできません。 この問題を解決するには、プリディバイダーを含める必要があります。
これらの目的のために、レジスタRST_CLK-> TIM_CLOCKがクロックブロックで使用されます。
トライアル用に、最大のディバイダーを含めました。 また、ここではタイマーにクロック信号を適用する必要があります。 タイマーがHCLKからプリディバイダーを介してカウントを開始したこと。




その結果、このような機能が得られます。
 #define PER_CLOCK_TIMER1_ONCLK (1<<14) //    1. #define TIM_CLOCK_TIM1_CLK_EN (1<<24) //     1. #define SHARE_HCLK_TIMER 7 //   HCLK ... (0 =  , 1 = /2, 2 = /4). #define TIM_CLOCK_TIM1_BRG (SHARE_HCLK_TIMER<<0) //    1  SHARE_HCLK_TIMER. #define CNTRL_CNT_EN (1<<0) //  . #define DMA_RE_CNT_ARR_EVENT_RE (1<<1) //   DMA   CNT == ARR; void Init_TIMER1_to_DMA_and_DAC2 (void) { RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK; // . TIMER1->CNTRL |= CNTRL_CNT_EN; //  ,  =  . TIMER1->ARR = 0xFFFF; // ... TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE; // "" DMA. RST_CLK->TIM_CLOCK |= TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM1_BRG; //    . }
      
      



次に、例を含めると、DAC2の電圧値レジスタの値が約1秒の間隔でどのように変化するかを観察します。 さて、最初の段階は終わりました。 確かに、プロセスは配列の途中で中断されます。 実際には、タイマーは送信を再度有効にするだけです。 しかし、配列の半分を転送するとすぐに、最初の構造を「使い果たし」ました。 今、それを復元する必要があります。 このために、2番目のタイマーを使用します。 多くの人が、「DMAからの送信を中断したり、アレイの半分を転送したりしないのはなぜですか」と尋ねます。 実際のところ、DMAでは、転送の終了時にのみ割り込みを生成できます。 中間を通過するときに割り込みを発生させる機会はありません。 しかし、ここではそれほど単純ではありません。 すべてを連続して送信するのではなく、部分的に送信することを思い出してください。 DMAはこれを理解していません。 DMAダウンタイム中に割り込みが生成されます。 つまり、DMAは、データパケット全体が送信されるのではなく、配列の1つの要素のみが送信されることを理解していません。 この事実により、DMA割り込みの使用はタスクに適さなくなります。

タイマーを構成して正弦波を生成する

先ほど言ったように、構造を「復元」するには、配列の半分(配列全体の1/4)を通過した後、次の(2番目の)タイマーで割り込みを設定する必要があります。 なぜ4分の1なのか-さらに説明する。 しかしその前に、最初のタイマーを再構成します。 DMAキックの速度を決定する必要があります。 見ます。 コントローラーは8 MHzでクロックされます。 配列には100個の要素が含まれています。 思い出すように、最初のオクターブまでの音の周波数は261.63 Hzです。 アカウントレジスタの容量は0xFFFFです(この値まではカウンターが読み取ることができます)。 8,000,000 Hz / 261.63 Hz / 100ノートを分割= 305メジャーごとにDMAを「キック」します。 これは、タイマー比較レジスタの最大値よりはるかに小さいです。 したがって、事前除数を使用する必要はありません。
両方のタイマーでクロックを供給します。
 RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK|PER_CLOCK_TIMER2_ONCLK; //   1  2.
      
      



タイマークロック(ブロック全体)をオンにした後、以前に構成されたパラメーターに従ってタイマーが動作を開始することがあります。 このため、構成する前であっても、さまざまな不具合があります。 これを防ぐには、カウント単位のクロック信号を無効にする必要があります(これにより、タイマーは、割り込みなどの原因となる値までカウントする時間を持たなくなります)。
カウントユニットのタイミングをオフにします。
 RST_CLK->TIM_CLOCK = 0; //  .
      
      



次に、DMAへのアクセス期間を指定して有効にします。
 //  8000000 /261.63/100 = 305. TIMER1->ARR = 305; // ... TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE; //  DMA.
      
      



タイマー2をセットアップします。簡単にするために、分周器なしで時計を合わせます。 中断する頻度も決定します。 同じソースから両方のタイマーをクロックするという事実により、最初の構造の最後の要素が送信された瞬間に構造値を更新することはできません。 DMAは最初の構造の最後に続く要素を転送しようとすると、その端でつまずき、自​​動的に代替構造に切り替わります。 したがって、DMAがプライマリ構造が「使い果たされた」と判断し、代替からの転送を開始するまで待つ必要があります。 これを行うには、代替構造からの少なくとも1つのパケットの送信を待ちます。 その後、プライマリを再度入力することが可能になります。 同じことを代替手段で行う必要があります。 転送が終了したら、DMAがプライマリから少なくとも1つのパケットを送信するまで待つ必要があります。その後、代替を上書きできます。 100個のアイテムを転送します。 各構造で50。 50番目のギアとともに中断を引き起こす可能性がありますが、上記の理由により、少なくとも51番目のギアを待つ必要があります。 変位の影響を受けないように、25ギアごとに両方の構造をチェックします。
したがって、タイマーの期間は305 * 25になります。
 TIMER2->ARR = 305*25; //   .
      
      



割り込みについて少し

タイマーが目的の値に達したときに割り込みが発生するためには、それが必要です。

タイマーの設定を終了します。

タイマーのパラメーターを指定した後、それらの作業を有効にし、カウンターにクロック信号を適用する必要があります。
 #define CNTRL_CNT_EN (1<<0) //  . #define TIM_CLOCK_TIM1_CLK_EN (1<<24) //     1. #define TIM_CLOCK_TIM2_CLK_EN (1<<25) //     2. TIMER1->CNTRL = CNTRL_CNT_EN; //  . TIMER2->CNTRL = CNTRL_CNT_EN; RST_CLK->TIM_CLOCK = TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM2_CLK_EN; //    .
      
      



設定の結果は関数でした。
 #define PER_CLOCK_TIMER1_ONCLK (1<<14) //    1. #define PER_CLOCK_TIMER2_ONCLK (1<<15) //    1. #define TIM_CLOCK_TIM1_CLK_EN (1<<24) //     1. #define SHARE_HCLK_TIMER1 7 //   HCLK ... (0 =  , 1 = /2, 2 = /4). #define TIM_CLOCK_TIM1_BRG (SHARE_HCLK_TIMER1<<0) //    1  SHARE_HCLK_TIMER. #define CNTRL_CNT_EN (1<<0) //  . #define CNTRL_EVENT_SEL (1<<8) //   : CNT == ARR; #define DMA_RE_CNT_ARR_EVENT_RE (1<<1) //   DMA   CNT == ARR; #define IE_CNT_ARR_EVENT_IE (1<<1) //     CNT == ARR; #define TIM_CLOCK_TIM2_CLK_EN (1<<25) //     2. #define SHARE_HCLK_TIMER2 7 //   HCLK ... (0 =  , 1 = /2, 2 = /4). #define TIM_CLOCK_TIM2_BRG (SHARE_HCLK_TIMER2<<8)//    1  SHARE_HCLK_TIMER. #define CH1_CNTRL_CAP_nPWM_Z (1<<15) //:    "". #define CH1_CNTRL_CHPSC_8 (3<<6) //:      8. #define CHy_CNTRL2_CCR1_EN (1<<2) //:   1. #define TIMERx_IE_CNT_ARR_EVENT_IE (1<<1) //:       CNT  ARR. void Init_TIMER1_to_DMA_and_DAC2 (void) { RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK|PER_CLOCK_TIMER2_ONCLK; //   1  2. RST_CLK->TIM_CLOCK = 0; //  . //  8000000 /261.63/100 = 305. TIMER1->ARR = 305; // ... TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE; //  DMA. TIMER2->ARR = 305*25; //   . TIMER2->IE = TIMERx_IE_CNT_ARR_EVENT_IE; //   . TIMER2->STATUS=0; //  . NVIC->ISER[0] = 1<<15; //    2. TIMER1->CNTRL = CNTRL_CNT_EN; //  . TIMER2->CNTRL = CNTRL_CNT_EN; RST_CLK->TIM_CLOCK = TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM2_CLK_EN; //    . }
      
      



構造を変更する機能を記述します。

中断について説明しました。 チェックインするだけです。プライマリ構造が終了し、代替構造から少なくとも1ブロックが転送された場合、プライマリ構造を上書きします。 セカンダリと同じ。 また、最初の構造の転送後-DMAがチャネルをブロックすることも忘れないでください。 送信がタイマー1からの「キック」に沿って進むように、その作業を再度許可する必要があります。
結果として生じる中断
 #define ST_Play_P (DAC_ST_ADC[10-1].channel_cfg & (1023<<4)) //       . #define ST_Play_ALT (DAC_ST_ADC[10-1+32].channel_cfg & (1023<<4)) //       . void Timer2_IRQHandler (void) // . { if ((ST_Play_P == 0) && (ST_Play_ALT <= (48<<4))) //         -   2-. DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR); if ((ST_Play_ALT == 0) && (ST_Play_P <= (48<<4))) DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT); DMA->CHNL_ENABLE_SET = 1<<10; TIMER2->STATUS=0; }
      
      



最後の例のメイン関数のコードは、新しい関数をオンにしてSysTickをオフにすることによってのみ変更されます。
彼がいる。
 int main (void) { HSE_Clock_ON(); //  HSE . HSE_Clock_OffPLL(); // ""      HSE . Buzzer_out_DAC_init(); //   . DAC_Init(); // . DMA_to_DAC_and_TIM1(); // DMA    DAC2   1. Init_TIMER1_to_DMA_and_DAC2(); //  1. while (1) { } }
      
      



プログラムを実行すると、出力に次の図が表示されます。 Githubコード。

バグについて

構造の変化の瞬間を選択することに長い間苦しんでいました。 いつも、このようなものが出てきました。




まとめると

DMAを別の方法で見て、その利点を活用することができました。 次の記事では、スキルを統合し、オルゴールの外観を作成し、その後プレーヤーを作成します。



All Articles