今回は、CPU使用率が極めて低く、応答速度が極めて速く、様々な無駄な消費を回避できる、他のシングルチップマイコンにも応用可能なシリアルポート可変長データ受信方式を紹介したいと思います。これは私が実際のプロジェクトで見つけたものですが、このアプローチはインターネット上ではあまり見かけません。
まず、このシナリオに必要な基本について説明します。
1. IDLE中断
シングルチップマイコンのシリアルポートアイドル割り込みは、シリアルポートがデータを受信してアイドル状態になると発生する特殊な割り込みで、シングルチップマイコンは受信したデータを時間内に処理できます。この割り込みにより、シリアル通信においてマイコンがデータ受信待ち状態となりシステムが応答しなくなる事態を回避できます。原理は、シリアル ポートからデータを受信するときに MCU がタイマーを設定することです。シリアル ポートがデータを受信すると、タイマーがトリガーされます。タイマーの期限が切れると、アイドル割り込みがトリガーされ、MCU はすぐに受信を停止できます。データを受信し、対応する割り込みを実行するサービスプログラムにより、受信データのタイムリーな処理を実現します。
2.DMA
DMA (Direct Memory Access) は、ハードウェアを介してデータ転送を制御する技術です。DMA を使用すると、シングルチップ マイクロコンピュータのデータ送信プロセス中に CPU の時間とリソースが解放され、システムの効率とパフォーマンスが向上します。従来のシングルチップ マイクロコンピュータ システムでは、データ転送は通常 CPU を介して実現され、CPU は周辺機器からデータを連続的に読み取ってメモリに書き込む、またはその逆を行う必要があり、これにより多くの CPU 時間とメモリが消費されます。リソース 。DMA テクノロジを使用すると、DMA は CPU の介入なしでハードウェアを介してデータ転送を制御できるため、この状況を回避できます。データ転送が完了すると、DMA は CPU に割り込み要求を送信して、データ転送が完了したことを CPU に通知します。これにより、CPU は他のタスクを実行し続けることができるため、システムの効率とパフォーマンスが向上します。
3.RTOS
RTOS(リアルタイムオペレーティングシステム、リアルタイムオペレーティングシステム)は、リアルタイムアプリケーション専用のオペレーティングシステムです。タスクのスケジューリング、割り込み処理、メモリ管理、プロセス間通信など、いくつかの重要な機能を提供します。私は FreeRTOS リアルタイム オペレーティング システムを使用していますが、RTT や UCOS などの他のオペレーティング システムも同じです
4. 通知する
FreeRTOS のタスク通知は、異なるタスク間の通信と同期に使用できる軽量のプロセス間通信メカニズムです。タスク通知により、タスク間のより柔軟な情報転送が可能になり、より複雑なシステム動作を実現できます。タスク通知は、送信者が受信者にシグナルを送信するメカニズムです。送信者が受信者にシグナルを送信する必要がある場合、タスク通知 API を使用して通知値を受信者に送信できます。受信者は、いつでも通知値を待つことができ、通知値が到着すると、受信者はウェイクアップされ、通知値に従って対応する動作を実行できます。タスク通知はバイナリ セマフォやカウント セマフォをある程度置き換えることができ、タスク通知はより高速かつ軽量です。当局によると、タスク通知はバイナリ セマフォより少なくとも 45% 高速です (他のオペレーティング システムにタスク通知があるかどうかはわかりません)。ただし、原理は似ており、本質はバイナリ セマフォの効果を実現することです)
プログラム全体の動作は次のとおりです。
まずシリアル ポートを送受信するように設定し、シリアル ポートの初期化を完了し、アイドル割り込みを有効にしてから、dma を設定し、シリアル ポート ペリフェラルの dma チャネルを指定し (dma 割り込みを有効にしないように注意してください)、シリアル ポート データ処理タスク (タスクの終了時) ループ内で通知を待ちます。シリアル ポートがデータのフレームを受信して割り込みをトリガーすると、割り込み処理関数でいくつかのことを行う必要があります: dma チャネルの停止、リセット割り込みフラグ、データ サイズの計算、タスク通知の送信、dma カウントのリセット、再起動 dma チャネルを有効にする。これらの作業が完了すると、割り込みが終了し、シリアル ポート データ処理タスクが通知を受け取り、処理を開始します。データを直接処理します
(
タスク通知が送信されるときにデータ長が dma から取得されてタスクに渡されるため、非常に多くのメリットがもたらされます)
1. 割り込み発生時に1回だけ計算するので計算回数が少ない 従来方式では割り込み中カウントを蓄積し続ける必要があった 2. データ処理タスクは他の処理を介さずにデータ長に応じて直接データを処理
できる
3. データ受信用のバッファ配列 正確なデータサイズが得られるため、memset を使い切るたびにクリアする必要がなく、範囲外での動作も問題ありません。
CPUにとっては夢のようなもので、突然誰かに電話がかかってきて、目が覚めるとそのデータが口の中に送り込まれていることに気づく
)
コード:
1. オペレーティングシステムの移植
STM32はCUBEを使って直接生成できます(とても良い)、CH32の一部シリーズは公式移植された完全なプロジェクトを使用できます(非常に良い)、GD32や他のシングルチップマイコンはオンラインチュートリアルを参照して自分自身を移植できます(笑)
2. シリアル構成
void USARTx_CFG(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {
0};
USART_InitTypeDef USART_InitStructure = {
0};
NVIC_InitTypeDef NVIC_InitStructure = {
0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
/* USART1 TX-->A.9 RX-->A.10 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
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_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_Cmd(DMA1_Channel5, ENABLE); /* USART1 Rx */
DMA_Cmd(DMA1_Channel4, ENABLE);
USART_Cmd(USART1, ENABLE);
}
このコードは、主に次の部分を含む USART1 シリアル ポート モジュールを初期化するために使用されます。
1. GPIO、USART、NVIC の初期化用に構造体変数 GPIO_InitStructure、USART_InitStructure、NVIC_InitStructure を定義して初期化します。
2. GPIOA および USART1 のクロックを有効にします。
3. GPIOA の 9 番ピンを USART1 の送信ピンとして構成し、GPIOA の 10 番ピンを USART1 の受信ピンとして構成します。
4. USART1 のボー レート、データ ビット長、ストップ ビット、パリティ ビット、ハードウェア フロー制御、および送受信モードを設定します。
5. USART1 のアイドル割り込みを有効にします。
6. 割り込み優先順位を設定し、USART1 の割り込みを有効にします。
7. USART1 の送受信用に DMA1 のチャネル 5 とチャネル 4 を有効にします。(ただし、チャネル 4 は実際には使用されません)
8. USART1 モジュールを有効にします。
3. DMA チャネル構成
void DMA_INIT(void)
{
DMA_InitTypeDef DMA_InitStructure = {
0};
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&USART1->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)u1_r_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = sizeof(u1_r_buf);
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_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
}
このコードは、DMA1 チャネル 5 を初期化し、DMA 初期化用の構造体変数 DMA_InitStructure を定義および初期化するために使用されます。
4. 割り込みハンドラ
void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
まず、ファイルの先頭で割り込み処理関数を宣言します。これが宣言形式です。次の __attribute__ 宣言を追加する必要があります。追加しないと、GCC コンパイラはこれが割り込み関数であることが認識されません。他の MCU の宣言方法は異なります。だから自分で調べる必要がある
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
DMA_Cmd(DMA1_Channel5, DISABLE);
USART1->STATR;
USART1->DATAR;
xTaskNotifyFromISR(Task1Task_Handler, sizeof(u1_r_buf) - DMA_GetCurrDataCounter(DMA1_Channel5), eSetValueWithOverwrite, NULL);
DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(u1_r_buf));
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
この機能を実装するには、まず dma チャネルを無効にし、次にシリアル ポートの送信レジスタと受信レジスタを読み取ります。この手順では、割り込みフラグ ビットをリセットします。これを行わないと、割り込みフラグ ビットをリセットできません。次のステップは、タスクを送信することです。通知には、
xTaskNotifyFromISR 関数を使用します。
パラメーターは (ターゲット タスク ハンドル、通知値 (ここでは、送信されるデータのサイズ)、通知方法を指定する) です。タスクを更新するとNULL)
、通知タスクを送信しても割り込みに引っかかったままで実行できません(笑)
そこでdmaカウントをリセット(例えばdma送信データサイズを20に設定)最初はカウント値を設定し、3データ受信後に割り込みを発生させます。このときのカウント値は17なので、毎回カウント値を元に戻す必要があります。そうしないと次の計算でエラーになります。) 最後
に、dma チャネルを再度開きます (この手順はメモリ アドレスをリセットすることです。そうしないと、データを正しいメモリに配置できなくなり、深刻なメモリ範囲外の問題が発生します)。
5. データ処理タスク
void task1_task(void *pvParameters)
{
unsigned long int size = 0;
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
while (1)
{
xTaskNotifyWait(0, 0, &size, portMAX_DELAY);
for (int i = 0; i < size; i++)
{
printf("%c", u1_r_buf[i]);
}
printf("\r\n");
}
}
まずタスク通知値 (シリアル ポート データ サイズ) を格納する変数を定義し、次にシリアル ポート DMA インターフェイスを有効にしてから、xTaskNotifyWait 関数を使用して無限ループでタスクをブロックします。このタスクは、タスクを実行せずに常にここでスタックします。パラメータ as (通知入力時、
通知値の演算(ここでは0を設定、演算しない)、通知終了時の通知値の演算(0を設定、演算しない)、通知値を受け取る変数アドレス、タイムアウト時間(最大タイムアウト)) データを受信した後、
送信通知を中断することで、ここでブロックが解除され、次のコードを実行できます。ここに必要なデータを入れてコードを出力するだけです。単に出力するだけです受信したデータをそのまま
追記:
この方法は良いのですが、実際には少し複雑です。第一に、これはオペレーティング システムに基づいているためです。ベアメタル プログラムの場合は使用できません。第二に、ほとんどのマイクロコントローラーにはコード生成ツールがありません。 (CUBE) st と同様、さまざまな設定プロセスが面倒で複雑で、間違いを犯しやすいですが、stm32 プロセスを使用すると、はるかに簡単になります
オペレーティング システムはマルチ スレッドの利便性を提供しますが、オペレーティング システム レベルでの問題も引き起こします。割り込みでオペレーティング システム API を使用する場合は、FromISR で終わる API を使用する必要があります。そうしないと、MCU がクラッシュする原因になります。割り込みの優先度がオペレーティング システムのマスカブル割り込みの優先度よりも低い場合、割り込みのネストが間違っており、クラッシュが発生します。
完全なコードを参考のために以下に示します。
#include "debug.h"
#include "FreeRTOS.h"
#include "task.h"
/* Global define */
#define TASK1_TASK_PRIO 5
#define TASK1_STK_SIZE 256
#define TASK2_TASK_PRIO 5
#define TASK2_STK_SIZE 256
/* Global Variable */
TaskHandle_t Task1Task_Handler;
TaskHandle_t Task2Task_Handler;
unsigned char u1_r_buf[20] = {
0};
unsigned char u1_t_buf[] = {
"abcdefg\r\n"};
void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void GPIO_Toggle_INIT(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {
0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
void task1_task(void *pvParameters)
{
unsigned long int size = 0;
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
while (1)
{
xTaskNotifyWait(0, 0, &size, portMAX_DELAY);
for (int i = 0; i < size; i++)
{
printf("%c", u1_r_buf[i]);
}
printf("\r\n");
}
}
void task2_task(void *pvParameters)
{
while (1)
{
// log_i("task2 entry");
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
vTaskDelay(500);
GPIO_SetBits(GPIOA, GPIO_Pin_1);
vTaskDelay(500);
}
}
void USARTx_CFG(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {
0};
USART_InitTypeDef USART_InitStructure = {
0};
NVIC_InitTypeDef NVIC_InitStructure = {
0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
/* USART1 TX-->A.9 RX-->A.10 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
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_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_Cmd(DMA1_Channel5, ENABLE); /* USART1 Rx */
DMA_Cmd(DMA1_Channel4, ENABLE);
USART_Cmd(USART1, ENABLE);
}
void DMA_INIT(void)
{
DMA_InitTypeDef DMA_InitStructure = {
0};
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&USART1->DATAR); /* USART2->DATAR:0x40004404 */
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)u1_r_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = sizeof(u1_r_buf);
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_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
SystemCoreClockUpdate();
Delay_Init();
DMA_INIT();
USARTx_CFG();
printf("SystemClk:%d\r\n", SystemCoreClock);
printf("ChipID:%08x\r\n", DBGMCU_GetCHIPID());
printf("FreeRTOS Kernel Version:%s\r\n", tskKERNEL_VERSION_NUMBER);
GPIO_Toggle_INIT();
/* create two task */
xTaskCreate((TaskFunction_t)task2_task, (const char *)"task2", (uint16_t)TASK2_STK_SIZE, (void *)NULL, (UBaseType_t)TASK2_TASK_PRIO, (TaskHandle_t *)&Task2Task_Handler);
xTaskCreate((TaskFunction_t)task1_task, (const char *)"task1", (uint16_t)TASK1_STK_SIZE, (void *)NULL, (UBaseType_t)TASK1_TASK_PRIO, (TaskHandle_t *)&Task1Task_Handler);
vTaskStartScheduler();
while (1)
{
printf("shouldn't run at here!!\n");
}
}
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
DMA_Cmd(DMA1_Channel5, DISABLE);
USART1->STATR;
USART1->DATAR;
xTaskNotifyFromISR(Task1Task_Handler, sizeof(u1_r_buf) - DMA_GetCurrDataCounter(DMA1_Channel5), eSetValueWithOverwrite, NULL);
DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(u1_r_buf));
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}