माइक्रोकंट्रोलर्स के लिए कार्यक्रमों में प्रिंटफ का उपयोग करते समय सबसे आम रेक

मेरी परियोजनाओं में समय-समय पर मुझे एक सीरियल पोर्ट (UART या USB पर एक एब्सट्रैक्ट जो एक सीरियल पोर्ट की नकल करता है) के साथ मिलकर प्रिंटफ का उपयोग करना पड़ता है। और, हमेशा की तरह, इसके अनुप्रयोगों के बीच बहुत समय गुजरता है और मैं उन सभी बारीकियों को पूरी तरह से भूल जाने का प्रबंधन करता हूं जिन्हें ध्यान में रखना आवश्यक है ताकि यह एक बड़ी परियोजना में सामान्य रूप से काम करे।

इस लेख में, मैंने अपने स्वयं के शीर्ष बारीकियों को संकलित किया है जो कि माइक्रोकंट्रोलर्स के लिए कार्यक्रमों में प्रिंटफ का उपयोग करते समय उत्पन्न होते हैं, जो कि सबसे स्पष्ट से पूरी तरह से गैर-स्पष्ट करने के लिए सबूतों द्वारा छांटे गए हैं।

संक्षिप्त परिचय


वास्तव में, माइक्रोकंट्रोलर के लिए कार्यक्रमों में प्रिंट का उपयोग करने के लिए, यह पर्याप्त है:
  • प्रोजेक्ट कोड में हेडर फ़ाइल शामिल करें;
  • सीरियल पोर्ट के आउटपुट के लिए _write सिस्टम फ़ंक्शन को फिर से परिभाषित करें;
  • सिस्टम कॉल के स्टब्स का वर्णन करें कि लिंकर की आवश्यकता है (_fork, _wait, और अन्य);
  • परियोजना में प्रिंटफ कॉल का उपयोग करें।

वास्तव में, सब कुछ इतना सरल नहीं है।

सभी स्टब्स का वर्णन करें, न कि केवल इस्तेमाल किए गए।


पहली बार में एक प्रोजेक्ट बनाते समय अस्पष्ट लिंक के झुंड की उपस्थिति आश्चर्यजनक है, लेकिन थोड़ा पढ़ने के बाद, यह स्पष्ट हो जाता है कि क्या और क्यों। मेरी सभी परियोजनाओं में मैं इस सबमॉडल को जोड़ रहा हूँ। इस प्रकार, मुख्य परियोजना में, मुझे केवल उन तरीकों को फिर से परिभाषित करना है जिनकी मुझे ज़रूरत है (इस मामले में केवल _write), और बाकी सभी अपरिवर्तित रहते हैं।

यह ध्यान रखना महत्वपूर्ण है कि सभी स्टब्स को सी फ़ंक्शन होना चाहिए। नहीं सी ++ (या बाहरी "सी" में लिपटे)। अन्यथा, लेआउट विफल हो जाएगा (जी ++ के साथ विधानसभा के दौरान नाम परिवर्तन याद रखें)।

_लेख में 1 वर्ण आता है


इस तथ्य के बावजूद कि _write पद्धति के प्रोटोटाइप में एक तर्क है जो प्रदर्शित संदेश की लंबाई से गुजरता है, इसका मूल्य 1 है (वास्तव में, हम खुद इसे हमेशा 1 बना देंगे, लेकिन बाद में उस पर अधिक)।
int _write (int file, char *data, int len) { ... } 

इंटरनेट पर आप अक्सर इस पद्धति के ऐसे कार्यान्वयन को देख सकते हैं:
_Write फ़ंक्शन का लगातार कार्यान्वयन
 int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; /* For example, output string by UART */ for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; } 


इस तरह के कार्यान्वयन के निम्नलिखित नुकसान हैं:
  • कम उत्पादकता;
  • स्ट्रीमिंग असुरक्षा;
  • अन्य उद्देश्यों के लिए सीरियल पोर्ट का उपयोग करने में असमर्थता;


कम प्रदर्शन


प्रोसेसर संसाधनों का उपयोग करके बाइट्स भेजने के कारण धीमा प्रदर्शन होता है: आपको उसी डीएमए का उपयोग करने के बजाय स्थिति रजिस्टर की निगरानी करनी होगी। इस समस्या को हल करने के लिए, आप अग्रिम में भेजने के लिए बफर तैयार कर सकते हैं, और लाइन के अंत का चरित्र प्राप्त करते समय (या बफर को भरते समय) भेज सकते हैं। इस पद्धति के लिए एक बफर मेमोरी की आवश्यकता होती है, लेकिन अक्सर भेजने के साथ प्रदर्शन में सुधार होता है।
एक बफर के साथ _write का उदाहरण कार्यान्वयन
 #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { //      uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty  \r\n    //    . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } int _write (int file, char *data, int len) { len = len; //   . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } //     //   \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } } 

यहाँ, डार्ट का उपयोग करके सीधे भेजने के लिए uart ऑब्जेक्ट, uart_1 जिम्मेदार है। ऑब्जेक्ट बफर से डेटा भेजने के समय ऑब्जेक्ट को थर्ड-पार्टी एक्सेस ब्लॉक करने के लिए FreeRTOS तरीकों का उपयोग करता है (म्यूटेक्स ले रहा है और वापस कर रहा है)। इस प्रकार, कोई भी दूसरे धागे से भेजते समय उर्ट ऑब्जेक्ट का उपयोग नहीं कर सकता है।
कुछ लिंक:
  • एक वास्तविक परियोजना के भाग के रूप में _iteite फ़ंक्शन कोड
  • uart क्लास इंटरफ़ेस यहाँ है
  • यहाँ और यहाँ stm32f4 के तहत uart क्लास इंटरफ़ेस का कार्यान्वयन
  • यहां परियोजना के हिस्से के रूप में उरट वर्ग की तात्कालिकता


स्ट्रीमिंग असुरक्षा


यह क्रियान्वयन भी असुरक्षित बना हुआ है, क्योंकि पड़ोसी FreeRTOS स्ट्रीम में कोई भी व्यक्ति प्रिंटफ को एक और लाइन भेजना शुरू नहीं करता है और इस प्रकार जो बफर वर्तमान में भेजा जा रहा है उसे पीस लें। )। यदि ऐसा कोई जोखिम होता है, तो किसी अन्य थ्रेड के प्रिंटफ को कॉल किया जाएगा, फिर एक लेयर ऑब्जेक्ट को लागू करना आवश्यक है जो पूरी तरह से प्रिंट करने के लिए पहुंच को ब्लॉक कर देगा। मेरे विशेष मामले में, केवल एक धागा प्रिंटफ के साथ बातचीत करता है, इसलिए अतिरिक्त जटिलताएं केवल प्रदर्शन को कम कर देंगी (परत के अंदर म्यूटेक्स की निरंतर कब्जा और रिहाई)।

अन्य उद्देश्यों के लिए सीरियल पोर्ट का उपयोग करने में असमर्थता


चूंकि हम पूरी स्ट्रिंग प्राप्त होने के बाद ही भेजते हैं (या बफ़र पूरा भरा हुआ है), इसके बजाय uart ऑब्जेक्ट के बाद, आप कन्वर्टर मेथड को बाद के पैकेट ट्रांसफर के लिए कुछ टॉप-लेवल इंटरफेस में कॉल कर सकते हैं (उदाहरण के लिए, ट्रांसमिशन प्रोटोकॉल के अनुसार गारंटी के साथ डिलीवरी पैकेट के समान है। लेनदेन modbus)। यह आपको डिबगिंग जानकारी प्रदर्शित करने के लिए, और उदाहरण के लिए, प्रबंधन कंसोल के साथ उपयोगकर्ता इंटरैक्शन (यदि कोई डिवाइस पर उपलब्ध है) दोनों के लिए एक uart दोनों का उपयोग करने की अनुमति देगा। यह प्राप्तकर्ता पक्ष पर एक डिकम्प्रेसर लिखने के लिए पर्याप्त होगा।

डिफ़ॉल्ट रूप से, फ्लोट आउटपुट काम नहीं करता है


यदि आप newlib-nano का उपयोग करते हैं, तो डिफ़ॉल्ट प्रिंटफ (साथ ही उनके सभी डेरिवेटिव जैसे स्प्रिंट / स्नप्रिंट ... और अन्य) फ्लोट वैल्यू के आउटपुट का समर्थन नहीं करते हैं। परियोजना के लिए निम्न लिंकर झंडे जोड़कर इसे आसानी से हल किया गया है।
 SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_") 

झंडे की पूरी सूची यहां देखें

कार्यक्रम कहीं न कहीं प्रिंटफ की आंतों में जम जाता है


यह लिंकर झंडे में एक और दोष है। फर्मवेयर को लाइब्रेरी के वांछित संस्करण के साथ कॉन्फ़िगर करने के लिए, आपको प्रोसेसर मापदंडों को स्पष्ट रूप से निर्दिष्ट करना होगा।
 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_") 

झंडे की पूरी सूची यहां देखें

प्रिंटफ माइक्रोकंट्रोलर को एक कठिन गलती के लिए मजबूर करता है


कम से कम दो कारण हो सकते हैं:
  • स्टैक मुद्दों;
  • _sbrk के साथ समस्याएं;

ढेर मुद्दे


यह समस्या वास्तव में FreeRTOS या किसी अन्य OS का उपयोग करते समय स्वयं प्रकट होती है। समस्या बफ़र का उपयोग कर रहा है। पहले पैराग्राफ में कहा गया है कि _write में 1 बाइट आता है। ऐसा होने के लिए, आपको पहली बार प्रिंटफ़ का उपयोग करने से पहले अपने कोड में बफरिंग के उपयोग पर प्रतिबंध लगाना होगा।
 setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); 

फ़ंक्शन के विवरण से यह निम्नानुसार है कि निम्न मानों में से एक को उसी तरीके से सेट किया जा सकता है:
 #define _IOFBF 0 /* setvbuf should set fully buffered */ #define _IOLBF 1 /* setvbuf should set line buffered */ #define _IONBF 2 /* setvbuf should set unbuffered */ 

हालाँकि, इससे कार्य स्टैक का ओवरफ़्लो हो सकता है (या यदि आप अचानक बहुत बुरे व्यक्ति हैं जो इंटरप्ट से प्रिंटफ़ को कॉल करते हैं)।

विशुद्ध रूप से तकनीकी रूप से, प्रत्येक स्ट्रीम के लिए बहुत सावधानी से स्टैक्स की व्यवस्था करना संभव है, लेकिन इस दृष्टिकोण के लिए सावधानीपूर्वक नियोजन की आवश्यकता होती है और उन त्रुटियों को पकड़ना मुश्किल होता है जो इसे वहन करती हैं। एक बहुत सरल उपाय एक बाइट प्राप्त करना है, इसे अपने स्वयं के बफर में संग्रहीत करना है, और फिर आवश्यक प्रारूप में इसे पार्स करना है।

_Sbrk के साथ समस्या


यह समस्या मेरे लिए व्यक्तिगत रूप से सबसे अधिक निहित थी। और इसलिए हम _sbrk के बारे में क्या जानते हैं?
  • एक और ठूंठ जिसे मानक पुस्तकालयों के काफी हिस्से का समर्थन करने के लिए लागू करने की आवश्यकता है;
  • ढेर पर स्मृति आवंटित करने के लिए आवश्यक;
  • मॉलॉक जैसे सभी प्रकार के पुस्तकालय विधियों द्वारा उपयोग किया जाता है, मुफ्त।

निजी तौर पर, 95% मामलों में अपनी परियोजनाओं में मैं नए तरीकों को नए सिरे से परिभाषित / हटाने / मॉलोक के साथ फ्रीआरटीओएस का उपयोग करता हूं जो फ्रीआरटीओएस का एक गुच्छा का उपयोग करते हैं। इसलिए जब मैं मेमोरी आवंटित करता हूं, तो मुझे यकीन है कि आवंटन FreeRTOS ढेर पर है, जो bss क्षेत्र में मेमोरी की पूर्व निर्धारित मात्रा लेता है। आप यहां की परत को देख सकते हैं । तो, विशुद्ध रूप से तकनीकी रूप से, कोई समस्या नहीं होनी चाहिए। एक फ़ंक्शन को बस नहीं बुलाया जाना चाहिए। लेकिन चलिए सोचते हैं, अगर वह फोन करती है, तो वह उसकी याद दिलाने की कोशिश कहां करेगी?

Microcontrollers के लिए "क्लासिक" परियोजना की रैम के लेआउट को याद करें:
  • .data;
  • .bss;
  • खाली जगह
  • प्रारंभिक स्टैक।

डेटा में, हमारे पास वैश्विक ऑब्जेक्ट्स (चर, संरचनाएं और अन्य वैश्विक प्रोजेक्ट फ़ील्ड) का प्रारंभिक डेटा है। बीएसएस में, वैश्विक क्षेत्र जिनका प्रारंभिक शून्य मान है और, ध्यान से, फ्रीआरटीओएस का एक गुच्छा। यह स्मृति में सिर्फ एक सरणी है। जिसके साथ heap_x.c फ़ाइल से विधियाँ काम करती हैं। अगला खाली जगह है, जिसके बाद (या अंत से बल्कि) स्टैक है। क्योंकि FreeRTOS का उपयोग मेरी परियोजना में किया जाता है, फिर इस स्टैक का उपयोग केवल तब तक किया जाता है जब तक कि शेड्यूलर शुरू नहीं हो जाता। और, इस प्रकार, इसका उपयोग, ज्यादातर मामलों में, बोलचाल तक सीमित है (वास्तव में, आमतौर पर 100 बाइट की सीमा)।

लेकिन फिर, स्मृति को _sbrk का उपयोग करके आवंटित किया गया है? एक नज़र डालें कि लिंकर स्क्रिप्ट से वह किस चर का उपयोग करता है।
 void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ... 

अब हम उन्हें लिंकर स्क्रिप्ट में ढूंढते हैं (मेरी स्क्रिप्ट जो प्रदान करता है उससे थोड़ी अलग है, हालांकि यह हिस्सा उसी के बारे में है:
 __stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ...  flash,    ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit; 

यही है, यह स्टैक के बीच मेमोरी (1 केबी से 0x20020000 नीचे 128 केबी रैम के साथ) और बीएसएस का उपयोग करता है।

समझ गए। लेकिन उनके पास मेधाव, मुक्त और अन्य तरीकों के पुनर्परिभाषित थे। सभी आवश्यक नहीं है के बाद _sbrk का उपयोग करें? जैसा कि यह निकला, एक चाहिए। इसके अलावा, यह विधि प्रिंटफ का उपयोग नहीं करती है, लेकिन बफरिंग मोड सेट करने के लिए विधि - सेटवॉफ (या बल्कि _malloc_r, जिसे लाइब्रेरी में एक कमजोर फ़ंक्शन के रूप में घोषित नहीं किया गया है। मॉलोक के विपरीत, जिसे आसानी से बदला जा सकता है)।

चूंकि मुझे यकीन था कि sbrk का उपयोग नहीं किया गया था, इसलिए मैंने स्टैक के करीब FreeRTOS (bss सेक्शन) का एक गुच्छा रखा (क्योंकि मुझे पता था कि स्टैक को आवश्यकता से 10 गुना कम इस्तेमाल किया गया था)।

समस्या 3 के समाधान:
  • बीएसएस और स्टैक के बीच इंडेंट;
  • _malloc_r को ओवरराइड करें ताकि _sbrk को न कहा जाए (लाइब्रेरी से अलग विधि);
  • मॉलॉक और मुक्त के माध्यम से फिर से लिखना।

मैं पहले विकल्प पर बस गया, क्योंकि यह मानक _malloc_r (जो libg_nano.a (lib_a-nano-mallocr.o के अंदर है) को सुरक्षित रूप से बदलना संभव नहीं था) (विधि को __attribute__ (कमजोर) के रूप में घोषित नहीं किया गया था, लेकिन द्वि-पुस्तकालय से केवल एक फ़ंक्शन को बाहर करने के लिए। मैं लिंक करने में सफल नहीं हुआ)। मैं वास्तव में एक कॉल के लिए sbrk को फिर से लिखना नहीं चाहता था।

अंतिम समाधान प्रारंभिक स्टैक और _sbrk के लिए RAM में अलग-अलग विभाजन आवंटित करना था। यह सुनिश्चित करता है कि सेटअप चरण के दौरान अनुभाग एक दूसरे के शीर्ष पर स्टैक्ड नहीं हैं। अंदर sbrk भी अनुभाग से बाहर जाने के लिए एक जाँच है। मुझे एक छोटा सुधार करना था ताकि विदेश में एक संक्रमण का पता लगाने के दौरान, प्रवाह थोड़ी देर के लूप में लटका हो (क्योंकि sbrk का उपयोग केवल प्रारंभिक चरण के प्रारंभिक चरण में होता है और डिवाइस को डीबग करने के चरण में संसाधित किया जाना चाहिए)।
संशोधित मेम
 MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K } 


अनुभाग में परिवर्तन
 __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP); 

आप इस कमेटी में मेरे सैंडबॉक्स प्रोजेक्ट में मेम.ल्ड और सेक्शन को देख सकते हैं

UPD 07/12/2019: फ्लोट वैल्यू के साथ प्रिंटफ काम करने के लिए झंडे की सूची तय की। मैंने सही संकलन और लेआउट झंडे के साथ काम कर रहे CMakeLists के लिंक को सही किया (इस तथ्य के साथ बारीकियां थीं कि झंडे को एक के बाद एक और "के माध्यम से सूचीबद्ध किया जाना चाहिए?", जबकि एक पंक्ति में या विभिन्न लाइनों पर यह कोई फर्क नहीं पड़ता)।

Source: https://habr.com/ru/post/hi459420/


All Articles