STM32F3xx + FreeRTOS. Modbus RTU dengan perangkat keras RS485 dan CRC tanpa timer dan semaphore

Halo! Relatif baru-baru ini, setelah lulus dari universitas, saya masuk ke sebuah perusahaan kecil yang bergerak di bidang pengembangan elektronik. Salah satu tugas pertama yang saya hadapi adalah kebutuhan untuk mengimplementasikan protokol Modbus RTU Slave menggunakan STM32. Dengan setengah dosa, saya menulisnya kemudian, tetapi saya mulai memenuhi protokol ini dari proyek ke proyek dan saya memutuskan untuk merefaktor dan mengoptimalkan lib menggunakan FreeRTOS.



pengantar



Dalam proyek saat ini, saya sering menggunakan bundel STM32F3xx + FreeRTOS, jadi saya memutuskan untuk memanfaatkan kemampuan perangkat keras pengontrol ini. Khususnya:



  • Menerima / mengirim menggunakan DMA
  • Kemungkinan perhitungan CRC perangkat keras
  • Dukungan perangkat keras RS485
  • Deteksi akhir paket melalui kemampuan perangkat keras USART, tanpa menggunakan pengatur waktu


Saya akan segera membuat reservasi, di sini saya tidak menjelaskan spesifikasi protokol Modbus dan bagaimana master bekerja dengannya, Anda dapat membaca tentang ini di sini dan di sini .



file konfigurasi



Untuk memulainya, saya memutuskan untuk menyederhanakan tugas mentransfer kode antar proyek, setidaknya dalam keluarga pengontrol yang sama. Jadi saya memutuskan untuk menulis file conf.h kecil yang memungkinkan saya mengkonfigurasi ulang bagian utama implementasi dengan cepat.



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 */




Paling sering, menurut saya, hal-hal berikut berubah:



  • Alamat perangkat dan ukuran ruang alamat
  • Frekuensi clock dan parameter pin USART (pin, port, rcc, irq)
  • Parameter saluran DMA (rcc, irq)
  • Aktifkan / Nonaktifkan CRC Hardware dan RS485


Konfigurasi besi



Dalam implementasi ini, saya menggunakan CMSIS biasa, bukan karena keyakinan agama, hanya lebih mudah bagi saya dan lebih sedikit ketergantungan. Saya tidak akan menjelaskan pengaturan port, Anda dapat melihatnya di tautan ke github yang ada di bawah ini.



Mari mulai dengan menyiapkan USART:



Konfigurasi 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;




Ada beberapa poin di sini:



  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 dikonfigurasi dengan dua bitfield: USART_CR1_DEAT dan USART_CR1_DEDT . Bitfield ini memungkinkan Anda menyetel waktu untuk melepas dan menyetel sinyal DE sebelum dan sesudah mengirim dalam 1/16 atau 1/8 bit, tergantung pada parameter oversampling dari modul USART. Tetap hanya untuk mengaktifkan fungsi di register CR3 dengan bit USART_CR3_DEM , perangkat keras akan mengurus sisanya.


Pengaturan DMA:



Penyiapan 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);




Karena Modbus beroperasi dalam mode permintaan-respons, kami menggunakan satu buffer untuk penerimaan dan transmisi. Diterima di buffer, diproses di sana dan dikirim darinya. Tidak ada masukan yang diterima selama pemrosesan. Saluran Rx DMA menempatkan data dari register penerima USART (RDR) ke dalam buffer, saluran Tx DMA, sebaliknya, dari buffer ke register kirim (TDR). Kita perlu mengganggu saluran Tx untuk menentukan bahwa jawabannya hilang dan kita dapat beralih ke mode terima.



Menginterupsi saluran Rx pada dasarnya tidak perlu, karena kami berasumsi bahwa paket Modbus tidak boleh lebih dari 256 byte, tetapi bagaimana jika ada gangguan pada saluran dan seseorang mengirim byte secara acak? Untuk melakukan ini, saya membuat buffer sebesar 257 byte, dan jika terjadi interupsi Rx DMA, itu berarti seseorang "mengotori" baris, dan kami membuang saluran Rx ke awal buffer dan mendengarkan lagi.



Penangan interupsi:



Penangan interupsi
/*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);
    }
}




Penangan DMA cukup sederhana: mengirim semuanya - membersihkan bendera, beralih ke mode terima, menerima 257 byte - kesalahan bingkai, membersihkan kelembapan, beralih ke mode terima lagi.



Prosesor USART memberi tahu kita bahwa sejumlah data masuk dan kemudian hening. Bingkai sudah siap, kami menentukan jumlah byte yang diterima (jumlah maksimum byte yang diterima DMA - jumlah yang tersisa untuk diterima), matikan penerimaan, bangun tugas.



Satu peringatan, saya dulu menggunakan semaphore biner untuk membangunkan tugas, tetapi pengembang FreeRTOS merekomendasikan menggunakan TaskNotification :

Membuka blokir tugas RTOS dengan pemberitahuan langsung 45% lebih cepat dan menggunakan lebih sedikit RAM daripada membuka blokir tugas dengan semafor biner

Terkadang fungsi xTaskGetCurrentTaskHandle () tidak disertakan dalam rakitan di FreeRTOS_Config.h , dalam hal ini Anda perlu menambahkan baris ke file ini:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


Tanpa menggunakan semaphore, firmware telah kehilangan hampir 1 kB. Agak, tentu saja, tapi bagus.



Fungsi kirim dan terima:



Kirim dan terima
/*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;
}


Kedua fungsi tersebut menginisialisasi ulang saluran DMA. Saat menerima, fungsi pelacakan waktu tunggu di register CR2 diaktifkan oleh bit USART_CR2_RTOEN .



CRC



Mari beralih ke penghitungan CRC hardcore. Fungsi pengontrol mata ini selalu mengganggu saya, tetapi entah bagaimana itu tidak pernah berhasil, dalam beberapa seri tidak mungkin untuk mengatur polinomial sewenang-wenang, dalam beberapa seri tidak mungkin untuk mengubah dimensi polinomial, dan seterusnya. Di F3, semuanya baik-baik saja, dan atur polinomial dan ubah ukurannya, tetapi saya harus melakukan satu squat:



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;
}


Ternyata tidak mungkin untuk membuang byte-by-byte ke dalam register DR - itu akan salah membaca, Anda harus menggunakan byte-access. Saya telah bertemu "orang aneh" seperti itu di STM dengan modul SPI di mana saya ingin menulis byte-by-byte.



Tugas



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();
        }
    }
}


Di dalamnya, kami menginisialisasi penunjuk ke tugas, ini diperlukan untuk menggunakannya untuk membuka kunci melalui TaskNotification, menginisialisasi perangkat keras dan menunggu hingga kami tidur hingga pemberitahuan tiba. Jika perlu, alih-alih portMAX_DELAY , Anda dapat meletakkan nilai waktu tunggu untuk menentukan bahwa tidak ada sambungan untuk waktu tertentu. Jika notifikasi sudah sampai, kita proses parsel, form respon dan kirimkan, tapi jika frame sudah sampai rusak atau salah alamat, kita tunggu saja yang berikutnya.



/*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;
}


Pawang itu sendiri tidak memiliki kepentingan khusus: memeriksa panjang frame / alamat / CRC dan menghasilkan respon atau kesalahan. Implementasi ini mendukung tiga fungsi utama: 0x03 - Read Registers, 0x06 - Write register, 0x10 - Write Multiple Registers. Biasanya, fungsi-fungsi ini cukup bagi saya, tetapi jika mau, Anda dapat memperluas fungsionalitasnya tanpa masalah.



Nah, mulailah:



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


Agar tugas dapat berfungsi, tumpukan dengan ukuran 32 x uint32_t (atau 128 byte) sudah cukup ; ini adalah ukuran yang saya tetapkan dalam definisi configMINIMAL_STACK_SIZE . Untuk referensi: awalnya saya salah berasumsi bahwa configMINIMAL_STACK_SIZE disetel dalam byte, jika saya tidak menambahkan cukup, namun, bekerja dengan pengontrol F0, di mana ada lebih sedikit RAM, saya harus menghitung tumpukan sekali dan ternyata configMINIMAL_STACK_SIZE disetel dalam dimensi jenis portSTACK_TYPE , yang didefinisikan di file portmacro.h

#define portSTACK_TYPE    uint32_t


Kesimpulan



Implementasi Modbus RTU ini memanfaatkan kemampuan perangkat keras mikrokontroler STM32F3xx secara optimal.



Bobot firmware keluaran bersama dengan OS dan optimasi -o2 adalah: Ukuran program: 5492 Bytes, Ukuran data: 112 bytes. Dengan latar belakang 6 KB, kehilangan 1 KB dari semaphore terlihat signifikan.



Portabilitas ke keluarga lain dimungkinkan, misalnya F0 mendukung waktu tunggu dan RS485, tetapi ada masalah dengan perangkat keras CRC, sehingga Anda dapat bertahan dengan metode penghitungan perangkat lunak. Mungkin juga ada perbedaan dalam penangan interupsi DMA, di mana keduanya digabungkan.



Link ke github



Mungkin akan berguna bagi seseorang.



Link yang berguna:






All Articles