STM32F3xx + FreeRTOS。タイマーとセマフォなしのハードウェアRS485およびCRCを備えたModbusRTU

こんにちは!比較的最近、高校を卒業した後、電子機器の開発に携わっている小さな会社に入りました。私が最初に直面したタスクの1つは、STM32を使用してModbusRTUスレーブプロトコルを実装する必要性でした。罪が半分になったので、それを書きましたが、プロジェクトからプロジェクトへとこのプロトコルを満たし始め、FreeRTOSを使用してlibをリファクタリングして最適化することにしました。



前書き



現在のプロジェクトでは、STM32F3xx + FreeRTOSバンドルをよく使用するため、このコントローラーのハードウェア機能を最大限に活用することにしました。特に:



  • DMAを使用した送受信
  • ハードウェアCRC計算の可能性
  • RS485ハードウェアサポート
  • タイマーを使用せずに、USARTハードウェア機能を介した小包検出の終了


すぐに予約します。ここでは、Modbusプロトコルの仕様と、マスターがどのように動作するかについては説明していませんこれについては、ここここで読むことができます



構成ファイル



まず、少なくとも同じコントローラーファミリー内で、プロジェクト間でコードを転送するタスクを簡素化することにしました。そこで、実装の主要部分をすばやく再構成できる小さなconf.hファイルを作成することにしました。



ModbusRTU_conf.h
#ifndef MODBUSRTU_CONF_H_INCLUDED
#define MODBUSRTU_CONF_H_INCLUDED
#include "stm32f30x.h"

extern uint32_t SystemCoreClock;

/*Registers number in Modbus RTU address space*/
#define MB_REGS_NUM             4096
/*Slave address*/
#define MB_SLAVE_ADDRESS        0x01

/*Hardware defines*/
#define MB_USART_BAUDRATE       115200
#define MB_USART_RCC_HZ         64000000

#define MB_USART                USART1
#define MB_USART_RCC            RCC->APB2ENR
#define MB_USART_RCC_BIT        RCC_APB2ENR_USART1EN
#define MB_USART_IRQn           USART1_IRQn
#define MB_USART_IRQ_HANDLER    USART1_IRQHandler

#define MB_USART_RX_RCC         RCC->AHBENR
#define MB_USART_RX_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_RX_PORT        GPIOA
#define MB_USART_RX_PIN         10
#define MB_USART_RX_ALT_NUM     7

#define MB_USART_TX_RCC         RCC->AHBENR
#define MB_USART_TX_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_TX_PORT        GPIOA
#define MB_USART_TX_PIN         9
#define MB_USART_TX_ALT_NUM     7

#define MB_DMA                  DMA1
#define MB_DMA_RCC              RCC->AHBENR
#define MB_DMA_RCC_BIT          RCC_AHBENR_DMA1EN

#define MB_DMA_RX_CH_NUM        5
#define MB_DMA_RX_CH            DMA1_Channel5
#define MB_DMA_RX_IRQn          DMA1_Channel5_IRQn
#define MB_DMA_RX_IRQ_HANDLER   DMA1_Channel5_IRQHandler

#define MB_DMA_TX_CH_NUM        4
#define MB_DMA_TX_CH            DMA1_Channel4
#define MB_DMA_TX_IRQn          DMA1_Channel4_IRQn
#define MB_DMA_TX_IRQ_HANDLER   DMA1_Channel4_IRQHandler

/*Hardware RS485 support
1 - enabled
other - disabled 
*/  
#define MB_RS485_SUPPORT        0
#if(MB_RS485_SUPPORT == 1)
#define MB_USART_DE_RCC         RCC->AHBENR
#define MB_USART_DE_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_DE_PORT        GPIOA
#define MB_USART_DE_PIN         12
#define MB_USART_DE_ALT_NUM     7
#endif

/*Hardware CRC enable
1 - enabled
other - disabled 
*/  
#define MB_HARDWARE_CRC     1

#endif /* MODBUSRTU_CONF_H_INCLUDED */




ほとんどの場合、私の意見では、次のことが変わります。



  • デバイスアドレスとアドレススペースサイズ
  • USARTピンのクロック周波数とパラメーター(ピン、ポート、rcc、irq)
  • DMAチャネルパラメータ(rcc、irq)
  • ハードウェアCRCおよびRS485を有効/無効にする


鉄の構成



この実装では、宗教的な信念のためではなく、通常のCMSISを使用します。これは、私にとっては簡単で、依存関係が少なくなります。ポートの構成については説明しません。以下のgithubへのリンクで確認できます。



USARTを設定することから始めましょう:



USART構成
    /*Configure USART*/
    /*CR1:
    -Transmitter/Receiver enable;
    -Receive timeout interrupt enable*/
    MB_USART->CR1 = 0;
    MB_USART->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_RTOIE);
    /*CR2:
    -Receive timeout - enable
    */
    MB_USART->CR2 = 0;

    /*CR3:
    -DMA receive enable
    -DMA transmit enable
    */
    MB_USART->CR3 = 0;
    MB_USART->CR3 |= (USART_CR3_DMAR | USART_CR3_DMAT);

#if (MB_RS485_SUPPORT == 1)
    /*Cnfigure RS485*/
     MB_USART->CR1 |= USART_CR1_DEAT | USART_CR1_DEDT;
     MB_USART->CR3 |= USART_CR3_DEM;
#endif

     /*Set Receive timeout*/
     //If baudrate is grater than 19200 - timeout is 1.75 ms
    if(MB_USART_BAUDRATE >= 19200)
        MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;
    else
        MB_USART->RTOR = 35;
    /*Set USART baudrate*/
     /*Set USART baudrate*/
    uint16_t baudrate = MB_USART_RCC_HZ / MB_USART_BAUDRATE;
    MB_USART->BRR = baudrate;

    /*Enable interrupt vector for USART1*/
    NVIC_SetPriority(MB_USART_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);
    NVIC_EnableIRQ(MB_USART_IRQn);

    /*Enable USART*/
    MB_USART->CR1 |= USART_CR1_UE;




ここにはいくつかのポイントがあります。



  1. F3, F0, , - . . , F1 , . USART_CR1_RTOIE R1. , USART , RM!
  2. RTOR. , 3.5 , 35 (1 — 8 + 1 + 1 ). 19200 / 1.75 , :
    MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;
  3. OC, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY , FreeRTOS FromISR , . FreeRTOS_Config.h,
  4. RS485は、USART_CR1_DEATUSART_CR1_DEDTの2つのビットフィールドで構成されますこれらのビットフィールドを使用すると、USARTモジュールのオーバーサンプリングパラメータに応じて、1/16または1/8ビットで送信する前後にDE信号を削除および設定する時間を設定できます。USART_CR3_DEMビットを使用してCR3レジスタの機能を有効にするだけで、残りはハードウェアが処理します。


DMA設定:



DMAセットアップ
    /*Configure DMA Rx/Tx channels*/
    //Rx channel
    //Max priority
    //Memory increment
    //Transfer complete interrupt
    //Transfer error interrupt
    MB_DMA_RX_CH->CCR = 0;
    MB_DMA_RX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_TEIE);
    MB_DMA_RX_CH->CPAR = (uint32_t)&MB_USART->RDR;
    MB_DMA_RX_CH->CMAR = (uint32_t)MB_Frame;

    /*Set highest priority to Rx DMA*/
    NVIC_SetPriority(MB_DMA_RX_IRQn, 0);
    NVIC_EnableIRQ(MB_DMA_RX_IRQn);

    //Tx channel
    //Max priority
    //Memory increment
    //Transfer complete interrupt
    //Transfer error interrupt
    MB_DMA_TX_CH->CCR = 0;
    MB_DMA_TX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE | DMA_CCR_TEIE);
    MB_DMA_TX_CH->CPAR = (uint32_t)&MB_USART->TDR;
    MB_DMA_TX_CH->CMAR = (uint32_t)MB_Frame;

     /*Set highest priority to Tx DMA*/
    NVIC_SetPriority(MB_DMA_TX_IRQn, 0);
    NVIC_EnableIRQ(MB_DMA_TX_IRQn);




Modbusは要求応答モードで動作するため、受信と送信の両方に1つのバッファーを使用します。バッファで受信され、そこで処理され、そこから送信されます。処理中の入力は受け付けられません。 Rx DMAチャネルは、USART受信レジスタ(RDR)からのデータをバッファに入れ、逆に、Tx DMAチャネルは、バッファから送信レジスタ(TDR)にデータを入れます。回答がなくなったことを確認するためにTxチャネルを中断する必要があり、受信モードに切り替えることができます。



Modbusパッケージは256バイトを超えることはできないと想定しているため、Rxチャネルの中断は基本的に不要ですが、回線にノイズがあり、誰かがランダムにバイトを送信している場合はどうなりますか?これを行うために、257バイトのバッファーを作成しました。これは、Rx DMA割り込みが発生した場合、誰かが回線を「散らかしている」ことを意味し、Rxチャネルをバッファーの先頭にスローして再度リッスンします。



割り込みハンドラー:



割り込みハンドラ
/*DMA Rx interrupt handler*/
void MB_DMA_RX_IRQ_HANDLER(void)
{
    if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2));
    if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2));
    /*If error happened on transfer or MB_MAX_FRAME_SIZE bytes received - start listening*/
    MB_RecieveFrame();
}

/*DMA Tx interrupt handler*/
void MB_DMA_TX_IRQ_HANDLER(void)
{
    MB_DMA_TX_CH->CCR &= ~(DMA_CCR_EN);
    if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2));
    if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2));
    /*If error happened on transfer or transfer completed - start listening*/
    MB_RecieveFrame();
}

/*USART interrupt handler*/
void MB_USART_IRQ_HANDLER(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if(MB_USART->ISR & USART_ISR_RTOF)
    {
        MB_USART->ICR = 0xFFFFFFFF;
        //MB_USART->ICR |= USART_ICR_RTOCF;
        MB_USART->CR2 &= ~(USART_CR2_RTOEN);
        /*Stop DMA Rx channel and get received bytes num*/
        MB_FrameLen = MB_MAX_FRAME_SIZE - MB_DMA_RX_CH->CNDTR;
        MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN;
        /*Send notification to Modbus Handler task*/
        vTaskNotifyGiveFromISR(MB_TaskHandle, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}




DMAハンドラーは非常に単純です。すべてを送信します-フラグをクリーンアップし、受信モードに切り替え、257バイトを受信しました-フレームエラー、湿気をクリーンアップし、再び受信モードに切り替えます。



USARTプロセッサは、一定量のデータが入力され、その後無音になったと通知します。フレームの準備ができたら、受信したバイト数(DMA受信バイトの最大数-残りの受信量)を決定し、受信をオフにして、タスクをウェイクアップします。



1つの注意点として、以前はバイナリセマフォを使用してタスクをウェイクアップしましたが、FreeRTOS開発者はTaskNotificationの使用を推奨しています

直接通知を使用してRTOSタスクのブロックを解除すると、バイナリセマフォを使用してタスクのブロックを解除するよりも45%速く、使用するRAMが少なくなります。

時々にFreeRTOS_Config.h xTaskGetCurrentTaskHandle()関数は、アセンブリに含まれていない、あなたはこのファイルに行を追加する必要があり、その場合には:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


セマフォを使用しない場合、ファームウェアはほぼ1kBを失っています。もちろん、ささいなことですが、いいです。



送受信機能:



送信および受信
/*Configure DMA to receive mode*/

void MB_RecieveFrame(void)
{
    MB_FrameLen = 0;
    //Clear timeout Flag*/
    MB_USART->CR2 |= USART_CR2_RTOEN;
    /*Disable Tx DMA channel*/
    MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN;
    /*Set receive bytes num to 257*/
    MB_DMA_RX_CH->CNDTR = MB_MAX_FRAME_SIZE;
    /*Enable Rx DMA channel*/
    MB_DMA_RX_CH->CCR |= DMA_CCR_EN;
}

/*Configure DMA in tx mode*/
void MB_SendFrame(uint32_t len)
{
    /*Set number of bytes to transmit*/
    MB_DMA_TX_CH->CNDTR = len;
    /*Enable Tx DMA channel*/
    MB_DMA_TX_CH->CCR |= DMA_CCR_EN;
}


どちらの機能もDMAチャネルを再初期化します。受信時に、CR2レジスタのタイムアウトを追跡する機能はUSART_CR2_RTOENビットによって有効になります



CRC



筋金入りのCRC計算に移りましょう。アイコントローラーのこの機能はいつも私を悩ませましたが、どういうわけかうまくいきませんでした。シリーズによっては任意の多項式を設定できなかったり、多項式の次元を変更できなかったりしました。F3では、すべてが正常で、多項式を設定してサイズを変更しますが、1つのスクワットを実行する必要がありました。



uint16_t MB_GetCRC(uint8_t * buffer, uint32_t len)
{
    MB_CRC_Init();
    for(uint32_t i = 0; i < len; i++)
        *((__IO uint8_t *)&CRC->DR) = buffer[i];
    return CRC->DR;
}


DR レジスタにバイトごとにスローすることは不可能であることが判明しました-読み取るのは間違っているので、バイトアクセスを使用する必要があります。私はすでに、バイトごとに書きたいSPIモジュールを使用してSTMでそのような「フリーク」に遭遇しました。



仕事



void MB_RTU_Slave_Task(void *pvParameters)
{
    MB_TaskHandle = xTaskGetCurrentTaskHandle();
    MB_HWInit();
    while(1)
    {
        if(ulTaskNotifyTake(pdTRUE, portMAX_DELAY))
        {
            uint32_t txLen = MB_TransactionHandler(MB_GetFrame(), MB_GetFrameLen());
            if(txLen)
                MB_SendFrame(txLen);
            else
                MB_RecieveFrame();
        }
    }
}


その中で、タスクへのポインタを初期化します。これは、TaskNotificationを介してロックを解除し、ハードウェアを初期化し、通知が到着するまでスリープするまで待機するために必要です。必要に応じて、portMAX_DELAYの代わりに、タイムアウト値を設定して、特定の時間接続がなかったことを確認できます。通知が到着した場合は、小包を処理して応答を作成して送信しますが、フレームが壊れているか間違ったアドレスに到着した場合は、次のメッセージを待つだけです。



/*Handle Received frame*/
static uint32_t MB_TransactionHandler(uint8_t * frame, uint32_t len)
{
    uint32_t txLen = 0;
    /*Check frame length*/
    if(len < MB_MIN_FRAME_LEN)
        return txLen;
    /*Check frame address*/
    if(!MB_CheckAddress(frame[0]))
        return txLen;
    /*Check frame CRC*/
    if(!MB_CheckCRC(*((uint16_t*)&frame[len - 2]), MB_GetCRC(frame, len - 2)))
        return txLen;
    switch(frame[1])
    {
        case MB_CMD_READ_REGS : txLen = MB_ReadRegsHandler(frame, len); break;
        case MB_CMD_WRITE_REG : txLen = MB_WriteRegHandler(frame, len); break;
        case MB_CMD_WRITE_REGS : txLen = MB_WriteRegsHandler(frame, len); break;
        default : txLen = MB_ErrorHandler(frame, len, MB_ERROR_COMMAND); break;
    }
    return txLen;
}


ハンドラー自体は特に重要ではありません。フレーム/アドレス/ CRCの長さをチェックし、応答またはエラーを生成します。この実装は、次の3つの主要な機能をサポートします。0x03-レジスタの読み取り、0x06-レジスタの書き込み、0x10-複数のレジスタの書き込み。通常はこれらの機能で十分ですが、必要に応じて問題なく機能を拡張できます。



さて、始めましょう:



int main(void)
{
    NVIC_SetPriorityGrouping(3);
    xTaskCreate(MB_RTU_Slave_Task, "MB", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
    vTaskStartScheduler();
}


タスクが機能するには、32 x uint32_t(または128バイト)のサイズのスタックで十分です。これは、configMINIMAL_STACK_SIZE定義で設定したサイズです参考:当初、configMINIMAL_STACK_SIZEはバイト単位で設定されていると誤解していましたが、十分に追加しなかった場合、RAMが少ないF0コントローラーで作業する場合、スタックを1回カウントする必要があり、configMINIMAL_STACK_SIZEがportSTACK_TYPEタイプのディメンションで設定されていることがわかりました。ファイルportmacro.h

#define portSTACK_TYPE    uint32_t


結論



このModbusRTU実装は、STM32F3xxマイクロシステムのハードウェア機能を最適に利用します。



OSおよび-o2最適化を含む出力ファームウェアの重みは次のとおりです。プログラムサイズ:5492バイト、データサイズ:112バイト。6 KBを背景に、セマフォから1KBを失うことは重要に見えます。



他のファミリへの移植も可能です。たとえば、F0はタイムアウトとRS485をサポートしていますが、ハードウェアCRCに問題があるため、ソフトウェアの計算方法で問題を解決できます。DMA割り込みハンドラーには、それらが組み合わされている場所で違いがある場合もあります。



githubへのリンク



おそらくそれは誰かに役立つでしょう。



便利なリンク:






All Articles