المقدمة
في المشاريع الحالية ، غالبًا ما أستخدم حزمة STM32F3xx + FreeRTOS ، لذلك قررت تحقيق أقصى استفادة من إمكانات الأجهزة الخاصة بوحدة التحكم هذه. خاصه:
- الاستلام / الإرسال باستخدام DMA
- إمكانية حساب CRC للأجهزة
- دعم الأجهزة RS485
- اكتشاف نهاية الطرد عبر إمكانيات أجهزة USART ، دون استخدام مؤقت
سوف تبدي تحفظا على الفور، وأنا هنا لا يصف مواصفات بروتوكول مودبوس وكيفية عمل رئيسية مع ذلك، يمكنك أن تقرأ عن هذا هنا و هنا .
ملف الضبط
بادئ ذي بدء ، قررت تبسيط مهمة نقل الكود بين المشاريع ، على الأقل ضمن نفس عائلة وحدات التحكم. لذلك قررت كتابة ملف 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 (pin ، port ، 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;
هناك عدة نقاط هنا:
- F3, F0, , - . . , F1 , . USART_CR1_RTOIE R1. , USART , RM!
- RTOR. , 3.5 , 35 (1 — 8 + 1 + 1 ). 19200 / 1.75 , :
MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1; - OC, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY , FreeRTOS FromISR , . FreeRTOS_Config.h,
- تم تكوين RS485 مع حقلي بت : USART_CR1_DEAT و USART_CR1_DEDT . تتيح لك حقول البت هذه ضبط وقت إزالة إشارة DE وتعيينها قبل وبعد الإرسال بمعدل 1/16 أو 1/8 بت ، اعتمادًا على معلمة زيادة العينات في وحدة USART. يبقى فقط لتمكين الوظيفة في سجل CR3 باستخدام بت USART_CR3_DEM ، وسيتولى الجهاز الباقي.
إعداد 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 يعمل في وضع استجابة الطلب ، فإننا نستخدم مخزنًا مؤقتًا واحدًا لكل من الاستقبال والإرسال. يتم استلامها في المخزن المؤقت ومعالجتها هناك وإرسالها منه. لا يتم قبول أي إدخال أثناء المعالجة. تضع قناة Rx DMA البيانات من سجل استقبال USART (RDR) في المخزن المؤقت ، قناة Tx DMA ، على العكس من ذلك ، من المخزن المؤقت إلى سجل الإرسال (TDR). نحتاج إلى مقاطعة قناة Tx لنحدد أن الإجابة قد اختفت ويمكننا التبديل إلى وضع الاستلام.
مقاطعة قناة Rx غير ضرورية بشكل أساسي ، لأننا نفترض أن حزمة Modbus لا يمكن أن تكون أكثر من 256 بايت ، ولكن ماذا لو كان هناك ضوضاء على الخط وقام شخص ما بإرسال البايت بشكل عشوائي؟ للقيام بذلك ، قمت بعمل مخزن مؤقت من 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 - المقدار المتبقي ليتم استلامه) ، ونوقف الاستقبال ، وننبه المهمة.
تحذير واحد ، كنت أستخدم إشارة ثنائية لإيقاظ المهمة ، لكن مطوري FreeRTOS يوصون باستخدام TaskNotification :
يعد إلغاء حظر مهمة RTOS بإشعار مباشر أسرع بنسبة 45٪ ويستخدم ذاكرة وصول عشوائي أقل من إلغاء حظر مهمة باستخدام إشارة ثنائيةفي بعض الأحيان في FreeRTOS_Config.h و xTaskGetCurrentTaskHandle () لا يتم تضمين وظيفة في الجمعية ، وفي هذه الحالة تحتاج إلى إضافة خط إلى هذا الملف:
#define INCLUDE_xTaskGetCurrentTaskHandle 1
بدون استخدام إشارة ، فقد البرنامج الثابت 1 كيلو بايت تقريبًا. تافه ، بالطبع ، لكنها لطيفة.
إرسال واستقبال الوظائف:
أرسل واستقبل
/*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 المتشدد. لطالما أزعجتني هذه الوظيفة لوحدة التحكم بالعين ، لكن بطريقة ما لم تنجح أبدًا ، في بعض السلاسل كان من المستحيل تعيين متعدد الحدود التعسفي ، في بعض السلاسل كان من المستحيل تغيير بُعد كثير الحدود ، وما إلى ذلك. في F3 ، كل شيء على ما يرام ، وقم بتعيين كثير الحدود وتغيير الحجم ، لكن كان علي القيام بقرفصاء واحدة:
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 - سيكون من الخطأ القراءة ، يجب عليك استخدام وصول البايت. لقد قابلت بالفعل مثل هؤلاء "النزوات" في STM باستخدام وحدة SPI التي أريد أن أكتب فيها بايتًا بايت.
مهمة
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 وإنشاء استجابة أو خطأ. يدعم هذا التطبيق ثلاث وظائف رئيسية: 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 × uint32_t (أو 128 بايت) ؛ هذا هو الحجم الذي حددته في تعريف configMINIMAL_STACK_SIZE . للرجوع إليها: في البداية افترضت خطأً أن configMINIMAL_STACK_SIZE تم تعيينه بالبايت ، إذا لم أقم بإضافة ما يكفي ، ومع ذلك ، عند العمل مع وحدات تحكم F0 ، حيث توجد ذاكرة وصول عشوائي أقل ، اضطررت إلى حساب المكدس مرة واحدة واتضح أن configMINIMAL_STACK_SIZE تم تعيينه في أبعاد من نوع المنفذ STACK_TYPE ، والذي تم تعريفه في ملف portmacro.h
#define portSTACK_TYPE uint32_t
خاتمة
يستخدم تطبيق Modbus RTU هذا الاستخدام الأمثل لقدرات الأجهزة الخاصة بالمتحكم الدقيق STM32F3xx.
كان وزن البرنامج الثابت الناتج مع نظام التشغيل والتحسين -o2: حجم البرنامج: 5492 بايت ، حجم البيانات: 112 بايت. على خلفية 6 كيلوبايت ، يبدو أن فقدان 1 كيلوبايت من الإشارات أمرًا مهمًا.
إمكانية النقل إلى عائلات أخرى ، على سبيل المثال ، يدعم F0 المهلة و RS485 ، ولكن هناك مشكلة في CRC للأجهزة ، لذا يمكنك التعامل مع طريقة حساب البرنامج. قد تكون هناك أيضًا اختلافات في معالجات مقاطعة DMA ، حيث يتم دمجها في مكان ما.
ارتباط إلى جيثب
ربما يكون مفيدًا لشخص ما.
روابط مفيدة: