皆さま こんにちは。
今回は LL UART通信の送信をDMAを使って行ってみます。
LLってな~に?という方は HALとLL の記事をご覧ください。
投稿時の開発環境を記しておきます。
PC:Windows10 OS
IDE: STM32CubeIDE Version1.5.0
Configurator: STM32CubeMX Version6.1.0
Board: STM32Nucleo-F401RE
DMAとは?
DMAは Direct Memory Accessの略です。
通常はペリフェラル(周辺I/O)からCPUを介してメモリーにデータを読み込んだり、メモリーからCPUを介してペリフェラルにデータを書き込みます。
DMAを使うと文字通りダイレクトにメモリーにアクセスしてくれるのでCPUを介す必要がなくなり、その時間を他の処理に回すことができるというわけです。
前回、前々回は 割り込み、ポーリング送信について記事を書きましたが、
DMA > 割り込み > ポーリング の順で、DMAが最もCPUの使用効率が良いでしょう。
プロジェクトを作成する
IDEを起動し、File- New – STM32 Project を選択し、Target Selection ウィンドウが出たら Board Selector タブを選択し Boards List から NUCLEO-F401RE を選択し Next ボタンを押します。
Project 名に F401UartLLTxDMA と入力し、Finishボタンを押します。
Initialize all peripherals with their default Mode ? と聞いてくるので Yesを押します。
UARTをLLで使う準備をする
Project Managerタブを選択し Advanced Settings の USART を HAL (初期値)から LL に変更します。(コンボボックスで選択する)
HALの部分をクリックするとコンボボックスのリストが表示されるので、LLを選択します。
Project – Build All を選択します。
Do you want Generate Code ? と聞いてくるのでRemenber my decision のチェックボックスにチェックを入れて OKを押します。
This action can be associated with C/C++ perspective.Do you want to open this perspective now?
と聞いてくることがあれば Remenber my decision のチェックボックスにチェックを入れて Yesを押します。
UARTのパラメーターを変更する
Project Explorer から F401UartLL.ioc をダブルクリックして GUIの画面を開きます。
Pinout & Configuration – Connectivity – USART2 を選択します。
Modeのところは Asynchronus が選択されていて使える状態になっています。(使わない場合は Disable)
今回もパリティを使ってみます。
Configuration – Parameter Settings のところで以下のように設定します。
(デフォルトからの変更はParityを None -> Even にするだけです)
Baud Rate : 115200
Word Length : 8bits
Parity : Even
Stop Bits : 1
割り込みの設定をおこなう
Configuration – NVIC Settings の USART2 global interrupt の Enabled にチェックを入れます。
DMAの設定を行う
Configuration – DMA Settings で Addボタンを押して、Selectコンボボックスから USART2_TX を選択します。
ここで一度ビルドしてコードを確認してみましょう。
Project – Build All を選択します。
STM32のDMA
この先LLやDMAを使っていくにはペリフェラルのレジスタについて理解していく必要があります。
そのために、このボードで使っているマイコンのリファレンスマニュアル RM0368 を こちら からダウンロードしておいてください。
今回は DMAでUART を動かしますのでまず 9項の DMA と 19項の USART の部分を良くご覧になっておいてください。
UARTについては前回、前々回に詳しく説明して来ましたので、ここではDMAについてわかる範囲で説明していきます。
以下にDMAのブロック図を貼っておきます。
DMAのペリフェラルは1と2があって、更に複数のチャンネルとストリームがあります。
どれを選択して使えばいいのだろうか?と思いましたけれど・・・
この下の図を見るとスッキリ解決します。
(リファレンスマニュアルでは、この図の下にもうひとつ使わないDMA2の図があるので、そちらについても目を通して確認しておいてください)
各ペリフェラルによって使うことができるストリームとチャンネルの組み合わせが決まっているようです。
USART2の場合について表にすると以下のようになります。
今回は送信ですから、DMAペリフェラルは1、ストリームは6、チャンネルは4を使うことになります。
DMAペリフェラル | ストリーム | チャンネル | |
---|---|---|---|
USART2 TX | DMA1 | 6 | 4 |
USART2 RX | DMA1 | 5 | 4 |
ストリームは「流れ」という意味で、通信の経路と考えておけば良いでしょう。
DMAのレジスタ解説
設定レジスタ DMA_SxCR (x=0-7)
まず設定レジスタについて見ていきます。
Sx の部分はストリームの番号です。先に説明した通り、今回はS6に設定することになります。
リファレンスマニュアルを読んでみた内容をもとに説明していきます。
ビット27,26,25 : CHSEL チャンネル選択
上で説明したとおり、4
ビット24,23 : MBURST メモリバースト転送設定
シングル転送で 0
ビット22,21 : PBURST ペリフェラルバースト転送
シングル転送で 0
ビット19 : CT 現在のターゲット
シングルバッファを使うので 0
現在のターゲットメモリはメモリ 0(DMA_SxM0AR ポインタによってアドレス指定)
ビット18 : DBM ダブルバッファモード
シングルバッファを使うので 0
ビット17,16 : PL 優先順位レベル
低 0
ビット15 : PINCOS ペリフェラルインクリメントオフセットサイズ
使わないので 0
(PINC=0の場合、このビットは意味を持たない)
ビット14,13 : MSIZE メモリデータサイズ
バイト 0
ビット12,11 : PSIZE ペリフェラルデータサイズ
バイト 0
ビット10 : MINC メモリインクリメントモード
インクリメントするので 1
ビット9 : PINC ペリフェラルインクリメントモード
インクリメントしないので 0
ビット8 : CIRC サーキュラモード
無効 0
ビット7,6 : DIR データ転送方向
メモリーからペリフェラル 1
ビット5 : PFCTRL ペリフェラルフローコントローラ
DMAがフローコントローラ 0
ビット4 : TCIE 送信完了割り込み有効化
有効にする 1
ビット3 : HTIE 1/2転送割り込み有効化
使わないので 0
ビット2 : TEIE 転送エラー割り込み有効化
使わないので 0
ビット1 : DMEIE ダイレクトモードエラー割り込み有効化
使わないので 0
ビット0 : ストリーム有効化
設定時は 0, 送信時は 1
初期設定を終えた段階で DMA1_S6CR の値は 0x08000450 になるようにします。
データレジスタ DMA_SxNDTR (x=0-7)
この部分のリファレンスマニュアルは誤植のようです。HIFCRはフラグクリアレジスタですので。。
赤で書き直しました。
ここには送信するバイト数である、配列 buffer のサイズ sizeof(buffer) を書きます。
ここに書いた値は送るたびにデクリメントされるので、送信時に毎回書き込む必要があります。
ペリフェラルアドレスレジスタ
ここには USART2のDRを指定します。
メモリ0アドレスレジスタ
ここにはDMA転送元のメモリーアドレスを指定します。
この後の メモリ1アドレスレジスタはシングルバッファモードでは使用しないので省略します。
FiFo制御レジスタも使用しないので省略します。
コーディングする
main.c
MX_USART2_UART_Init()内にはDMAに関する初期化コードも生成されますが、最後の2行は追加する必要があります。
初期化の各関数が何を行っているのかステップ実行して確認しながら動かしてみると理解が深まると思います。
各レジスタの値は Window – Show View – SFRs – DMA1 から確認することができます。
static void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 0 */
/* USER CODE END USART2_Init 0 */
LL_USART_InitTypeDef USART_InitStruct = {0};
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
/* Peripheral clock enable */
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USART2);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
/**USART2 GPIO Configuration
PA2 ------> USART2_TX
PA3 ------> USART2_RX
*/
GPIO_InitStruct.Pin = LL_GPIO_PIN_2|LL_GPIO_PIN_3;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
GPIO_InitStruct.Alternate = LL_GPIO_AF_7;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART2 DMA Init */
/* USART2_TX Init */
LL_DMA_SetChannelSelection(DMA1, LL_DMA_STREAM_6, LL_DMA_CHANNEL_4);
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_STREAM_6, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
LL_DMA_SetStreamPriorityLevel(DMA1, LL_DMA_STREAM_6, LL_DMA_PRIORITY_LOW);
LL_DMA_SetMode(DMA1, LL_DMA_STREAM_6, LL_DMA_MODE_NORMAL);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_STREAM_6, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_STREAM_6, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_STREAM_6, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_STREAM_6, LL_DMA_MDATAALIGN_BYTE);
LL_DMA_DisableFifoMode(DMA1, LL_DMA_STREAM_6);
/* USART2 interrupt Init */
NVIC_SetPriority(USART2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));
NVIC_EnableIRQ(USART2_IRQn);
/* USER CODE BEGIN USART2_Init 1 */
/* USER CODE END USART2_Init 1 */
USART_InitStruct.BaudRate = 115200;
USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_8B;
USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
USART_InitStruct.Parity = LL_USART_PARITY_EVEN;
USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
USART_InitStruct.OverSampling = LL_USART_OVERSAMPLING_16;
LL_USART_Init(USART2, &USART_InitStruct);
LL_USART_ConfigAsyncMode(USART2);
LL_USART_Enable(USART2);
/* USER CODE BEGIN USART2_Init 2 */
// ここからは自分で追加したコード
// 毎回やる必要はないので、ここに書いてみた
// 転送元、転送先、方向
LL_DMA_ConfigAddresses(DMA1, LL_DMA_STREAM_6, (uint32_t)&buffer, LL_USART_DMA_GetRegAddr(USART2), LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
// 送信完了割り込みを有効にする
LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);
/* USER CODE END USART2_Init 2 */
}
main()関数の前あたりにデータを送る関数 DMA_TransmitData() を書きます。
/* USER CODE BEGIN 0 */
#define _CR 0x0d
#define _LF 0x0a
uint8_t buffer[] =
{
'#', 'a', 'b', 'c', 'd', 'e', _CR, _LF
};
void DMA_TransmitData();
void DMA_TransmitData()
{
LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);
LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_6, sizeof(buffer));
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_6);
LL_USART_EnableDMAReq_TX(USART2);
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
while()ループ内
while (1)
{
DMA_TransmitData();
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
stm32f4xx_it.c
DMA1_Stream6_IRQHandler()は DMA1 ストリーム6の割り込みハンドラです。
送信完了のフラグが立っていたらクリアする処理を追加します。
フラグをクリアしておかないと次回の送信が行われません。
void DMA1_Stream6_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream6_IRQn 0 */
if( LL_DMA_IsActiveFlag_TC6(DMA1) == 1){
LL_DMA_ClearFlag_TC6(DMA1);
}
/* USER CODE END DMA1_Stream6_IRQn 0 */
/* USER CODE BEGIN DMA1_Stream6_IRQn 1 */
/* USER CODE END DMA1_Stream6_IRQn 1 */
}
Tera Termの設定
何度も出てきているので画像は省略します。
以下の要領で設定します。
COMポート : 新しい接続でシリアルを選択し、STLink Virtual COMのポートを選択する。
設定 – シリアルポートより
スピード : 115200
データ : 7bit
パリティ : even
ストップビット : 1bit
フロー制御 : none
端末の設定でローカルエコーにチェックを入れます。
プログラムの概要
DMAに関する初期設定は、コンフィグレーターが大半を行ってくれるので助かりました。
追加した2行について
まず
LL_DMA_ConfigAddresses(DMA1, LL_DMA_STREAM_6, (uint32_t)&buffer, LL_USART_DMA_GetRegAddr(USART2), LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
でメモリーとペリフェラルのそれぞれのアドレスと方向を設定します。
割り込みの設定は別途必要だったので
LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);
で送信完了割り込みを有効にしました。
DMA_TransmitData()では
送信するバイト数を都度、設定します。
設定にはストリームを無効にしておく必要があります。
その後、LL_USART_EnableDMAReq_TX() でリクエストすることにより送信が始まります。
ビルドして実行する
ビルドしてエラーがないことを確認します。
パソコン側で Tera Term の設定を行い、プログラムを実行します。
1秒毎に #abcde が表示されれば成功です。
いかがでしたか?
DMAを使ったUARTの送信はうまく動きましたか?
お疲れさまでした。
関連記事
コメントを書く