أشعل النار الأكثر شيوعًا عند استخدام printf في برامج ميكروكنترولر

من وقت لآخر في مشاريعي ، يجب أن أستخدم printf بالاقتران مع منفذ تسلسلي (UART أو تجريد عبر USB يحاكي منفذ تسلسلي). وكالعادة ، يمر الكثير من الوقت بين تطبيقاته وأتمكن من نسيان جميع الفروق الدقيقة التي يجب مراعاتها تمامًا بحيث يعمل بشكل طبيعي في مشروع كبير.

في هذه المقالة ، قمت بتجميع أفضل الفروق الدقيقة التي أثيرها عند استخدام printf في برامج ميكروكنترولر ، مرتبة حسب الأدلة من الأكثر وضوحًا إلى غير الواضح تمامًا.

مقدمة موجزة


في الحقيقة ، من أجل استخدام printf في برامج أجهزة التحكم الدقيقة ، يكفي:
  • تضمين ملف الرأس في رمز المشروع ؛
  • إعادة تعريف وظيفة نظام _write لإخراجها إلى المنفذ التسلسلي ؛
  • وصف روتين مكالمات النظام التي يتطلبها الرابط (_fork ، _wait ، وغيرها) ؛
  • استخدام دعوة printf في المشروع.

في الواقع ، ليس كل شيء في غاية البساطة.

وصف جميع بذرة ، وليس فقط تلك المستخدمة.


وجود مجموعة من الروابط الغامضة في تخطيط المشروع في البداية أمر مفاجئ ، ولكن بعد قراءة بعض الشيء ، يصبح من الواضح ماذا ولماذا. في جميع مشاريعي ، أقوم بتوصيل هذا النموذج الفرعي . وبالتالي ، في المشروع الرئيسي ، أعيد تحديد الطرق التي أحتاجها فقط (كتابة فقط في هذه الحالة) ، والباقي لم تتغير.

من المهم أن نلاحظ أن جميع كعب الروتين يجب أن تكون وظائف C. لا C ++ (أو ملفوفة في الخارج "C"). خلاف ذلك ، سيفشل التخطيط (تذكر تغيير الاسم أثناء التجميع باستخدام G ++).

في _write يأتي حرف واحد


على الرغم من حقيقة أن النموذج الأولي لطريقة _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; } 


مثل هذا التطبيق له العيوب التالية:
  • إنتاجية منخفضة
  • تدفق انعدام الأمن ؛
  • عدم القدرة على استخدام المنفذ التسلسلي لأغراض أخرى ؛


أداء منخفض


يرجع الأداء البطيء إلى إرسال وحدات البايت باستخدام موارد المعالج: يجب عليك مراقبة سجل الحالة بدلاً من استخدام نفس DMA. لحل هذه المشكلة ، يمكنك تحضير المخزن المؤقت للإرسال مسبقًا ، وعند تلقي حرف نهاية السطر (أو تعبئة المخزن المؤقت) ، أرسل. تتطلب هذه الطريقة ذاكرة مؤقتة ، ولكنها تحسن الأداء بشكل كبير من خلال الإرسال المتكرر.
مثال تنفيذ _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 ، مسؤول عن الإرسال مباشرةً باستخدام dma. يستخدم الكائن أساليب FreeRTOS لمنع وصول الجهات الخارجية إلى الكائن في وقت إرسال البيانات من المخزن المؤقت (التقاط وإرجاع كائن المزامنة). وبالتالي ، لا يمكن لأحد استخدام كائن uart أثناء الإرسال من مؤشر ترابط آخر.
بعض الروابط:
  • _write رمز الوظيفة كجزء من مشروع حقيقي هنا
  • واجهة فئة uart هنا
  • تنفيذ واجهة فئة uart تحت stm32f4 هنا وهنا
  • إنشاء مثيل لفئة uart كجزء من المشروع هنا


تدفق انعدام الأمن


يظل هذا التطبيق أيضًا غير محمي ، حيث لا أحد يزعج في دفق FreeRTOS المجاور لبدء إرسال خط آخر إلى printf وبالتالي طحن المخزن المؤقت الذي يتم إرساله حاليًا (كائن المزامنة داخل uart يحمي الكائن من الاستخدام في تدفقات مختلفة ، لكن البيانات غير المرسلة إليهم ). في حالة وجود خطر في أن يتم استدعاء printf لمؤشر ترابط آخر ، عندئذٍ يلزم تنفيذ كائن طبقة يحظر الوصول إلى printf بالكامل. في حالتي الخاصة ، يتفاعل مؤشر ترابط واحد فقط مع printf ، وبالتالي فإن المضاعفات الإضافية ستقلل من الأداء فقط (التقاط ثابت وإطلاق mutex داخل الطبقة).

عدم القدرة على استخدام المنفذ التسلسلي لأغراض أخرى


نظرًا لأننا نرسل فقط بعد استلام السلسلة بأكملها (أو امتلاء المخزن المؤقت) ، بدلاً من كائن uart ، يمكنك استدعاء طريقة المحول إلى واجهة المستوى الأعلى لنقل الحزمة اللاحقة (على سبيل المثال ، التسليم مع ضمان وفقًا لبروتوكول النقل مماثل للحزم modbus المعاملة). سيتيح لك ذلك استخدام uart واحدًا على حد سواء لعرض معلومات تصحيح الأخطاء ، وعلى سبيل المثال ، لتفاعل المستخدم مع وحدة التحكم بالإدارة (إذا كان ذلك متاحًا على الجهاز). يكفي كتابة برنامج إلغاء ضغط على جانب المستلم.

بشكل افتراضي ، لا يعمل إخراج الطفو


إذا كنت تستخدم newlib-nano ، فلن تدعم printf افتراضيًا (وكذلك جميع مشتقاتها مثل sprintf / snprintf ... وغيرها) إخراج القيم الطافية. يمكن حل هذا بسهولة عن طريق إضافة إشارات linker التالية إلى المشروع.
 SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_") 

انظر القائمة الكاملة للأعلام هنا .

يتجمد البرنامج في مكان ما في أحشاء printf


هذا هو عيب آخر في أعلام رابط. من أجل تكوين البرنامج الثابت مع الإصدار المطلوب من المكتبة ، يجب عليك تحديد معلمات المعالج بشكل صريح.
 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_") 

انظر القائمة الكاملة للأعلام هنا .

printf يفرض على متحكم للوصول إلى خطأ ثابت


قد يكون هناك سببان على الأقل:
  • القضايا كومة.
  • مشاكل مع _sbrk ؛

كومة القضايا


هذه المشكلة تتجلى بالفعل عند استخدام FreeRTOS أو أي نظام تشغيل آخر. المشكلة هي استخدام المخزن المؤقت. قال الفقرة الأولى أنه في _write يأتي 1 بايت لكل. لكي يحدث هذا ، يجب أن تحظر استخدام التخزين المؤقت في التعليمات البرمجية قبل استخدام printf لأول مرة.
 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 */ 

ومع ذلك ، يمكن أن يؤدي ذلك إلى تجاوز سعة مكدس المهام (أو المقاطعة إذا كنت فجأة شخصًا سيئًا جدًا يستدعي printf من المقاطعات).

تقنيًا بحتًا ، من الممكن ترتيب مداخن بعناية فائقة لكل تدفق ، ولكن هذا النهج يتطلب تخطيطًا دقيقًا ومن الصعب اكتشاف الأخطاء التي يحملها. حل أبسط بكثير هو تلقي بايت واحد ، وتخزينها في مخزن مؤقت خاص بها ، ثم إخراجها بالتنسيق المطلوب ، والذي تم تحليله مسبقًا.

مشاكل مع _sbrk


كانت هذه المشكلة بالنسبة لي شخصيا الأكثر ضمنا. وماذا نعرف عن _سبرك؟
  • كعب آخر يجب تنفيذه لدعم جزء كبير من المكتبات القياسية ؛
  • مطلوب لتخصيص الذاكرة على الكومة ؛
  • المستخدمة من قبل جميع أنواع أساليب المكتبة مثل malloc ، مجانا.

شخصياً ، في مشاريعي في 95٪ من الحالات ، أستخدم FreeRTOS مع إعادة تعريف الطرق الجديدة / delete / malloc التي تستخدم مجموعة من FreeRTOS. لذلك عندما أقوم بتخصيص الذاكرة ، أنا متأكد من أن التخصيص موجود على كومة الذاكرة المؤقتة FreeRTOS ، والتي تشغل مقدارًا محددًا مسبقًا من الذاكرة في منطقة bss. يمكنك إلقاء نظرة على الطبقة هنا . لذلك ، من الناحية الفنية البحتة ، يجب ألا تكون هناك مشكلة. وظيفة ببساطة لا ينبغي أن يسمى. لكن دعونا نفكر ، إذا اتصلت ، فأين ستحاول الحصول على ذاكرتها؟

أذكر تصميم ذاكرة الوصول العشوائي للمشروع "الكلاسيكي" للميكروكونترولر:
  • .data.
  • .bss.
  • مساحة فارغة
  • كومة الأولي.

في البيانات ، لدينا البيانات الأولية للكائنات العمومية (المتغيرات ، الهياكل ، وحقول المشروع العالمية الأخرى). في bss ، الحقول العالمية التي لها قيمة صفرية مبدئية ، وبعناية ، مجموعة من FreeRTOS. انها مجرد مجموعة في الذاكرة. التي تعمل من خلالها الأساليب من ملف heap_x.c. التالي هو المساحة الفارغة ، وبعدها (أو بالأحرى من النهاية) المكدس. لأن يستخدم FreeRTOS في مشروعي ، ثم يتم استخدام هذه المكدس فقط حتى يبدأ المجدول. وبالتالي ، يقتصر استخدامه ، في معظم الحالات ، على collobyte (في الواقع ، بحد أقصى 100 بايت).

ولكن أين ، إذن ، يتم تخصيص الذاكرة باستخدام _sbrk؟ ألقِ نظرة على المتغيرات التي تستخدمها من البرنامج النصي للرابط.
 void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ... 

الآن نعثر عليها في البرنامج النصي linker (يختلف النص الخاص بي قليلاً عن النص الذي يوفره st ، ولكن هذا الجزء هو نفسه تقريباً هناك):
 __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 كيلو بايت RAM) و bss.

يفهم. لكنه كان يعيد تعريف أساليب malloc ، مجانا وغيرها. استخدم _sbrk بعد كل شيء ليس ضروريا؟ كما اتضح ، لا بد منه. علاوة على ذلك ، لا تستخدم هذه الطريقة printf ، ولكن طريقة تعيين وضع التخزين المؤقت - setvbuf (أو بالأحرى _malloc_r ، والتي لم يتم الإعلان عنها كدالة ضعيفة في المكتبة. على عكس malloc ، والتي يمكن استبدالها بسهولة).

نظرًا لأنني كنت متأكدًا من عدم استخدام sbrk ، فقد وضعت مجموعة من FreeRTOS (قسم bss) بالقرب من المكدس (لأنني كنت متأكدًا من أن المكدس كان يستخدم 10 مرات أقل من المطلوب).

حلول للمشكلة 3:
  • المسافة البادئة بين bss والمكدس؛
  • تجاوز _malloc_r بحيث لا يتم استدعاء _sbrk (افصل طريقة واحدة عن المكتبة) ؛
  • أعد كتابة sbrk عبر malloc و مجانًا.

لقد استقرت على الخيار الأول ، لأنه لم ينجح في استبدال المعيار _malloc_r (الموجود داخل libg_nano.a (lib_a-nano-mallocr.o)) (لم يتم الإعلان عن الطريقة كـ __attribute__ ((ضعيف)) ، ولكن لاستبعاد وظيفة واحدة فقط من المكتبة الثنائية لم ينجح في الارتباط). أنا حقا لا أريد إعادة كتابة sbrk لمكالمة واحدة.

كان الحل النهائي هو تخصيص أقسام منفصلة في ذاكرة الوصول العشوائي للمكدس الأولي و _sbrk. هذا يضمن أن الأقسام غير مكدسة فوق بعضها البعض أثناء مرحلة الإعداد. داخل sbrk هناك أيضا الاختيار للخروج من القسم. اضطررت لإجراء تصحيح صغير حتى عند الكشف عن انتقال إلى الخارج ، فإن التدفق سيعلق في حلقة زمنية (لأن استخدام sbrk يحدث فقط في المرحلة الأولية من التهيئة ويجب معالجته في مرحلة تصحيح الجهاز).
mem.ld المعدلة
 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 } 


التغييرات في section.ld
 __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP); 

يمكنك إلقاء نظرة على mem.ld و section.ld في مشروع صندوق الحماية الخاص بي في هذا الالتزام .

UPD 07/12/2019: إصلاح قائمة الإشارات للعمل printf مع القيم العائمة. لقد قمت بتصحيح الرابط إلى قوائم CMakeLists العاملة مع أعلام تجميع وتخطيط مصححة (كانت هناك فروق دقيقة مع حقيقة أنه يجب إدراج العلامات واحدة تلو الأخرى ومن خلال "؛" ، بينما لا يهم الأمر في سطر واحد أو على خطوط مختلفة).

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


All Articles