لغة سي و متحكماتمقالات

هل يجب أن نستخدم ++C عوضاً عن C في النظم المضمنة؟

الجواب لدى كتاب

تٌرجمت هذه المقالة من الإنكليزية -> العربية بواسطة: Nour Taweel

 

القصة باختصار

 

بحسب ما تبيّن العديد من الاحصائيات (كاحصائية Eclipse IoT Developer واحصائية IEEE Spectrum) وكما هو واضح في الوضع الراهن فإن لغة embedded C لازالت اللغة المهيمنة في مجال تطوير النظم المضمنة، كما لا تزال معظم الشركات المصنعة للمتحكمات توفّر بيئات التطوير SDK بلغة C حصراً فهي اللغة الاكثر دعماً في معظم الأجهزة المضمنة.

و لعل المنصّة/بيئة التطوير الأكثر شيوعاً والتي تدعم لغة ++C الى جانب C هي Arduino Core حيث أن ++C مستخدمة بكثرة في نواة المنصة core وكذلك في المكتبات المتعددة libraries.

يفضل المطورون لعدّة أسباب لغة C على ++C في تطبيقات النظم المضمنة وربما أهم هذه الأسباب هو أن لغة C لا تستهلك الموارد (وأعني الذاكرة تحديداً وبشكل غير مباشر سرعة التنفيذ) كما هو الحال مع ++C وهذا له أهمية كبيرة وخاصة عند التعامل مع المعالجات المصغرة microcontrollers حيث يسعى المطورون لتوفير كل بايت ممكن في ذواكر الـ RAM او ROM

لكن استخدام C بالمقابل قد يكون مصدراً لثغرات خطيرة وأخص بالذكر عمليات القَصْر typecasting ففي مثل هذه الحالات تتولى ++C مهام التحقق safety بالاضافة للكثير من الميزات الأخرى. كما أنه لا يمكننا تطبيق قواعد تصميم البرمجيات في لغة C إذ أنها لا توفر ميزات الـ overloading و الـtemplates  و البرمجة غرضية التوجه OOP وغيرها وهي ميزة أخرى في صالح ++C.

الفيديو التالي يظهر العديد من الميزات التي يراها مبرمجي لغة C عن لغة ++C

 

شخصيا لست مناصراً للغة C بحد ذاتها ولكنني أفضل أن أكون على دراية بتفاصيل اللغة التي أعمل بها في المستوى الأدنى وأن أعلم كيف يتم استهلاك الموارد. رأيت الكثير من المقالات والدروس التعليمية هنا وهناك عن الموضوع إلا أنني صادفت مؤخراً كتاباً صنع الفرق بالنسبة لي بكمية المعلومات التي أضافها لي وجعلني أعيد التفكير في امكانية استخدام ++C بدلا من embedded C. ربما مقولة (الإنسان عدو ما يجهل) تناسب سياقنا هنا ولهذا الكتاب إمكانية كبيرة في توضيح الكثير من الأمور المجهولة التي تهم مطوري النظم المضمّنة حول لغة ++C.

هذه ليست مقالة تقنية بقدر ماهي مراجعة لكتاب يجيب على سؤال: هل علَيْ كمطور للنظم المضمنة أن أحب أم أكره لغة ++C ؟

فلنبدأ

اسم الكتاب “Real-Time C++: Efficient Object-Oriented and Template Microcontroller Programming” وله ثلاث طبعات

 

Real-Time C++ book cover

 

عن الكتاب

 

يقدم الكتاب لغة ++C للقارئ المبتدئ في الفصل الأول باستخدام مثال وهو عبارة عن صف class يقوم بتبديل حالة LED من خلال منفذ الأغراض العامة GPIO. الفصل الثاني “Working with a Real-Time C++ Program on a Board” يبين كيفية نقل الكود السابق الى معالج فعلي ATmega من عائلة AVR باستخدام مجموعة أدوات AVR-GCC والبناء باستخدام سكريبت build.bat في بيئة ويندوز.

الجيد في هذا الكتاب هو أنه مزوّد بمستودع غني في Github حيث يمكنك استكشافه حتى ولو لم تمتلك الكتاب نفسه. يشرح هذا الفصل عملية البناء خطوة بخطوة وصولا الى شرح كل flag تم استخدامه في تعليمات البناء (على سبيل المثال اختيار c++11 بتفعيل -std=c++11 في تعليمة avr-g++ أثناء عملية ال compilation) . بعد ذلك يقوم الكاتب بإجراء بعض التحسينات على الكود الاصلي وذلك لتخفيض المساحة التي يحتلها التطبيق في ذاكرة الفلاش من ٣٦ بايت الى ١٦ وكذلك من ٢ بايت في ذاكرة RAM إلى لا شيء.

الفصل الثالث بعنوان “An Easy Jump Start in Real-Time C++” يلخّص ميزات ++C الأكثر استخداما لمبرمجي لغة embedded C الذين يودون استخدام ++C بأسرع وقت ممكن. ويصف الكاتب ذلك بقوله : “قد يود المطورون حديثي العهد ب ++C في الزمن الحقيقي إنجاز بعض النتائج الفعلية قبل تخصيص الوقت اللازم لاحتراف لغة ++C بكل التفاصيل … قسم صغير من اللغة يمكن استخدامها في كثير من الحالات”.

يناقش الفصل الرابع “Object-Oriented Techniques for Microcontrollers” كيفية استخدام الميزات الغرضية التوجه كالصفوف classes والوراثة inheritance في الكود المضمّن. يقترح الكاتب هنا بنية صفوف مستمدة من  LED class السابق تحتوي على الصف led_base وصفين آخرين أحدهما مخصص للتعامل مع منافذ الاستخدام العام GPIO تحت المسمى led_port وصف آخر باسم led_pwm للتعامل مع LED التي تعمل بإشارات PWM . تم ذكر العديد من التفاصيل المتعلقة بالفعالية efficiency عند استخدام البرمجة الغرضية التوجه فعلى سبيل المثال تم التطرق لتعدد الأشكال الديناميكي dynamic polymorphism وكم يضيف على حجم الذاكرة حال استخدامه.

 

الفصل الخامس يحمل عنوان “C++ Templates for Microcontrollers” إلا أنني وجدته يحتوي نقاشاً عاماً حول استخدام القوالب templates لكن مبدأ البرمجة الوصفية metaprogramming المذكور فيه هو مفهوم يساعد في تنفيذ بعض الأمور خلال الترجمة compile time بدلاً من الكود المضمن.

الفصل السادس “Optimized C++ Programming for Microcontrollers” وهو الفصل الأخير في الجزء الاول من الكتاب ويتحدث عن تحسينات GCC المتوافرة مثل -O2 و -O3 و -Os بالإضافة لبعض التقنيات كاستخدام لغة التجميع assembly في المهام التي تتطلب تحكم دقيق بالزمن time critical، وأيضا استخدام ذاكرة الـ ROM الفارغة لتخزين المتحولات بدلا من استخدام RAM التي قد تكون ممتلئة بالاضافة لاستخدام البرمجة الوصفية.

إن التحسينات التي يقترحها الكاتب هنا ليست محصورة بالتحسينات المتعلقة بالزمن أو المساحة فقط وإنما طريقة كتابة الكود وترتيبه و كتابة التوثيق documentation أيضاً.

الجزء الثاني “Components for Real-Time C++” يبدأ بالفصل السابع “Accessing Microcontroller Registers” وهو فصل خفيف يتحدث عن كيفية الولوج الى المسجلات المنخفضة المستوى بطريقة لغة ++C وذلك باستخدام عمليات القصر الآمن safety typecasting والقوالب templates. بعد ذلك يقدم الكتاب الكود المبدئي في الفصل الثامن “The Right Start” وهو فعلياً إعادة كتابة للكود لكن باستخدام ++C وهو موجود في مستودع الكتاب. كما يحتوي الفصل التالي “Low-Level Hardware Drivers in C++” على العديد من الأمثلة عن كيفية كتابة برامج القيادة drivers في المستوى المنخفض ل GPIO و PWM و SPI باستخدام ++C. يناقش الفصلان الأخيران من الجزء الثاني آلية تنفيذ بعض الميزات مثل الإدارة الديناميكية للذاكرة بطريقة تناسب المعالج المستهدف وذلك في الفصل العاشر “Custom Memory Management”

ويذكر الفصل الحادي عشر“C++ Multitasking” توصيفاً مجدول متعدد المهام بسيط بلغة ++C. يحتوي الجزء الثالث “Mathematics and Utilities for Real-Time C++” ستة فصول عن معالجة الاشارة و بعض الادوات الرياضية في C++ وهي خارج مجال اهتمامنا في هذا المقال/المراجعة.

وأخيرا يذيّل الكتاب مجموعة من الملاحق التي تحتوي عدة مقالات تعليمية قصيرة حول مواضيع ذات علاقة.

 

الدارة العمليّة المستخدمة في الفصل الثاني

 

أظن أن ما يميز هذا الكتاب هو تركيزه على الجانب العملي حيث حاول الكاتب الالتزام بالأمثلة المستمدة من النظم المضمنة قدر المستطاع.

 

بعض الأفكار والملاحظات من الكتاب

 

استخدام الكلمات المفتاحية const و constexpr و define مع الثوابت

 

ان const هي بديل define# في ++C عند التعامل مع الثوابت حيث تقوم بنفس العمل بالإضافة إلى التحقق من النوع بينما لاتقوم define# بذلك. هناك أيضاً constexpr التي يضيفها الإصدار c++11 والتي لها نفس العمل كما const بالإضافة لجعل القيم الثابتة العددية تستخدم وتُحسب في وقت الترجمة compilation فقط، وهذا يفيد بجعل المترجم يقوم بالعمليات الحسابية عوضاً عن جعل البرنامج firmware يقوم بذلك. ويفيد ذلك أيضا بجعل بعض الثوابت في الصف class ثوابت وقت الترجمة compile-time وهذا بالتأكيد سيقلص من حجم الصف في ذاكرة الرام. فيما يلي مثال من الكتاب:

//Author: Christopher Kormanyos
class led_template
{
public:
led_template()
{
// Set the port pin value to low.
*reinterpret_cast<volatile bval_type*>(port)
&= static_cast<bval_type>(~bval);
// Set the port pin direction to output.
*reinterpret_cast<volatile bval_type*>(pdir)
|= bval;
}
static void toggle()
{
// Toggle the LED.
*reinterpret_cast<volatile bval_type*>(port)
^= bval;
}
private:
static constexpr port_type pdir = port - 1U;
};

بإلقاء النظر على الجزء الـ private
private:
static constexpr port_type pdir = port - 1U;

هنا المتحول pdir لا يُحتاج سوى عند تعريف الغرض من  led ولن يتم تغيير قيمته لاحقاً.

 

العناصر الـ static

 

إذا تم تعريف أحد التوابع في صف ما كتابع من نوع static فإنه سيقلل من حجم الصف في الذاكرة حيث أن ذلك يقلل من كلفة الاستدعاء. يمكنك التوسع في موضوع الاعضاء الـ static في هذه المقالة التعليمية. كما أن تعريف أحد المتحولات كمتحول مشترك بين كل المتغيرات المشتقة من الصف كـ static يوفر استهلاك الذاكرة بشكل ملحوظ. ذلك لأن هذا المتحول سيكون له نسخة واحدة وليس عدد من النسخ بعدد الأغراض objects المشتقة من الصف. يبين المثال التالي استخدام static و constexpr في صف LED:

//Author: Christopher Kormanyos
 #include <cstdint>
#include "mcal_reg.h"
class led
{
public:
// Use convenient class-specific typedefs.
typedef std::uint8_t port_type;
typedef std::uint8_t bval_type;
// The led class constructor.
led(const port_type p,
const bval_type b) : port(p),
bval(b)
{
// Set the port pin value to low.
*reinterpret_cast<volatile bval_type*>(port)
&= static_cast<bval_type>(~bval);
// Set the port pin direction to output.
// Note that the address of the port direction
// register is one less than the address
// of the port value register.
const port_type pdir = port - 1U;
*reinterpret_cast<volatile bval_type*>(pdir)
|= bval;
}

void toggle() const
{
// Toggle the LED via direct memory access.
*reinterpret_cast<volatile bval_type*>(port)
^= bval;
}
private:
// Private member variables of the class.
const port_type port;
const bval_type bval;
};

بينما النسخة المحسنة بالشكل:
//Author: Christopher Kormanyos
template<typename port_type,
typename bval_type,
const port_type port,
const bval_type bval>
class led_template
{
public:
led_template()
{
// Set the port pin value to low.
*reinterpret_cast<volatile bval_type*>(port)
&= static_cast<bval_type>(~bval);
// Set the port pin direction to output.
*reinterpret_cast<volatile bval_type*>(pdir)
|= bval;
}
static void toggle()
{
// Toggle the LED.
*reinterpret_cast<volatile bval_type*>(port)
^= bval;
}
private:
static constexpr port_type pdir = port - 1U;
};

والنتيجة قبل وبعد التحسين:

 

الاستدعاء بالمؤشر أم الاستدعاء بالقيمة؟

 

هذا الموضوع لايمكن تصنيفه على أنه متعلق ب ++C فقط لكنني وجدته حيلة مفيدة، حيث أن الكاتب ينصح باستخدام الاستدعاء بالمؤشر لتقليل الوقت الذي يستهلكه المعالج لوضع القيم واستخراجها من المكدّس stack

“ … C++ references are heavily used because this can be advantageous for small microcontrollers. Consider an 8–bit microcontroller. The work of copying subroutine parameters or the work of pushing them onto the stack for anything wider than 8 bits can be significant. This workload can potentially be reduced by using references.”

 

كلفة تعدد الأشكال الديناميكي Dynamic polymorphism

 

يفضّل مبرمج النظم المدمجة المتمرّس على الدوام معاينة تكلفة أي تقنية برمجية يستخدمها على الذاكرة والمعالج وقد تفهّم الكتاب هذا الموضوع بحيث أنه شرح التكلفة المرافقة للعديد من التقنيات البرمجية مثل تعدد الأشكال الديناميكي Dynamic polymorphism.

يتحدث الكتاب عن التوابع الـ virtual التي يمكن أن تأخذ تعريفاً مختلفاً في كل صف مشتق على حدة. ثم استخدام ذلك في تعريف الصف الأب led_base والصفين المشتقين منه led_pwm و led_port والتابع toggle هو تابع virtual حيث أن كل صف من الصفين المشتقين يعرف هذا التابع بطريقته الخاصة أي أن الصف led_port يقوم بتعريفه وفق منطق المنفذ ذو الاغراض العامة GPIO بينما الصف led_pwm يعرفه وفق منطق الاشارات pwm.

الكود التالي يبين كيفية استخدام هذه الطريقة في الزمن الحقيقي:

//Author: Christopher Kormanyos
void led_toggler(led_base& led)
{
// Use dynamic polymorphism to toggle
// a base class reference.
led.toggle();
}
void do_something()
{
led_toggler(led0); // Toggle an led_port.
led_toggler(led1); // Toggle an led_port.
led_toggler(led2); // Toggle an led_pwm.
led_toggler(led3); // Toggle an led_pwm.
}

ويمكن شرح الكلفة المرافقة كما يلي:

تقوم العديد من المترجمات بحفظ عناوين التوابع ال virtual في جدول إما في ذاكرة الرام الستاتيكية أو في ذاكرة البرنامج فكل تابع من نوع virtual يكلف فقط مقدار من الذاكرة يتسع لحفظ المؤشر لتلك الدالة. بفرض المنصة التي نعمل عليها بأساس 8-bit والمؤشر على الدالة يحتاج 2byte والصف المشتق يحتوي على ثلاث توابع من هذا النوع فبالتالي يحتاج إلى 6byte لتخزين الجدول المذكور.

ولكن ما الغاية من هذا الجدول؟

إن استدعاء الدالة سيكون فعالاً بهذه الحالة حيث أنه كل ما يجب فعله هو انتقاء السطر المناسب من الجدول واستدعاء هذا العنوان ولربما هو أبطأ قليلا من الاستدعاء التقليدي.

 

البرمجة الوصفية : تشغيل الدالة خلال الترجمة

 

هذه فكرة كبيرة ولربما يحتاج القارئ لبعض الدراسة لها من مصادر أخرى مثل C++ template metaprogramming أو صفحة ويكيبيديا أو درس من موقع geeksforgeeks ولكن الفكرة العامة هي جعل المترجم يقوم ببعض الحسابات التي ليست بالضرورة القيام بها خلال وقت التنفيذ run-time مثل حساب N!

N! ≡ N (N – 1) · · · 2 · 1

//Author: Christopher Kormanyos
template<const std::uint32_t N>
struct factorial
{
// Multiply N * (N - 1U) with template recursion.
static constexpr std::uint32_t value
= N * factorial<N - 1U>::value;
};
template<>
struct factorial<0U>
{
// Zero’th specialization terminates the recursion.
static constexpr std::uint32_t value = 1U;
};

والان لاستدعاء القالب
constexpr std::uint32_t fact5 = factorial<5U>::value;

هنا سيقوم المترجم بحساب القيمة وليس الكود وهي طريقة ممتازة لتقليص حجم الكود في العمليات التي لاتحتاج لأن تكون في وقت التنفيذ.

 

تغيير اسماء التوابع mangling والعملية المعاكسة demangling

 

هو مفهوم آخر ليس بحكر على ++C لكن يستحق الذكر هنا. إن كنت قد جربت استخدام أحد الأدوات التي تقوم باستخراج الرموز من ملف (.out)  مثل nm للتحقق من أسماء التوابع في خرج المترجم … إن كنت جربت ذلك فهذا يعني أنك رأيت أسماء التوابع وقد أُضيفت إليها بعض المحارف الأخرى، فمثلاً التابع get_event قد تصبح بالشكل :

__ZN2os9get_eventENS_17enum_task_id_typeE

لماذا؟ هذا لأن المترجم يحتاج لأن تكون أسماء التوابع والمتغيرات فريدة خاصة عندما نستعمل المتحولات والتوابع الـstatic والتي قد تستخدم نفس الأسم ولكن بتوابع مختلفة أو ملفات مختلفة فعلى المترجم أن يميّز بينها. ونفس الفكرة تنطبق على الـ overloading في ++C كذلك.

لترى الرموز التي نتكلم عنها يمكنك استخدام السطر التالي:

nm --numeric-sort yourappname.elf 

لاستعادة الاسم الاصلي de-mangle تستخدم ++C الأداة c++filt

للتعمق في الموضوع انظر لهذا الدرس من embeddedrelated .

 

استخدم ROM ووفّر RAM!

 

غالبا ما تمتلئ ذاكرة ال RAM وذلك لأنها أصغر من ROM في الحجم. أحد الاقتراحات في هذا الكتاب هو استخدام ذاكرة ROM عندما تكون متاحة. فعلى سبيل المثال يمكن تخزين رقم نسخة البرنامج كثابت في ذاكرة الـ ROM بدلا من RAM.

استخدمت هذه الحيلة مرة لتوفير مساحة الـ RAM التي كانت مشغولة برسائل الـ log بينما كان لدي وفرة في مساحة الـ ROM.

عليك التأكد من ملف map بأن المترجم سيقوم بوضع الثابت في ROM.

 

كود أكثر موثوقية

 

استخدام الصفوف والميزات البرمجية الأخرى من ++C يمنح الكود مستوى اعلى من الوثوقية للكود. فعلى سبيل المثال، المتغير من نوع communication وبما أن الصف pwm و port كل منهما مربوط مع طرفية محددة فإنه عند نسخ الغرض لغرض آخر من نفس الصف باستخدام الإسناد المباشر أو باستخدام تابع البناء فإنه سيتسبّب بأن يصبح الغرضان يشيران الى نفس المكان.

يمكن التخلص من هذا باستخدام تابع بناء من نوع private واستخدام ال overloading كما يلي

 

//Author: Christopher Kormanyos
class led_base
{
public:
// ...
private:
// Private non-implemented copy constructor.
led_base(const led_base&) = delete;
// Private non-implemented copy assignment operator.
const led_base& operator=(const led_base&) = delete;
};


 

كيفية تعريف جزء مخصص في RAM

 

وهي أيضا خاصية أخرى ليست متعلقة فقط بـ ++C. وهي عبارة عن كيفية الوصول لعنوان منطقة section محددة في linker script من البرنامج وهذا مهم جداً في حالة استخدم المبرمج خيار من linker script لتلك المنطقة وهو NOLOAD .

في أحد المرات احتجت لمسح منطقة section بالكامل وكنت قد استخدمت تلك المنطقة للحفاظ على المعلومات يُحافظ عليها بين عمليات إعادة الإقلاع البرمجية software reset وعند ظرف معين احتجت لمسحه، فبدلاً من الوصول لكل متحوّل على حدى لمسحه يمكننا وضع حلقة تستخدم بعناوين البداية والنهاية للمنطقة كما يلي:

MEMORY
{
/*...*/
  NOINIT(rwx) :  ORIGIN = 0xXXXXXXXX, LENGTH = 0xXXXX
/*...*/
}

SECTIONS
{
/*...*/
  .noinit (NOLOAD):
  {
    PROVIDE(__start_noinit_data = .);
    KEEP(*(.noinit))
    PROVIDE(__stop_noinit_data = .);
  } >NOINIT
/*...*/
} INSERT AFTER .data;

ويمكن للبرنامج الوصول بتعريف عناوين البداية والنهاية كمتحولات extern خارجية من ملف الرابط (في المثال السابق هي  __start_noinit_data و __stop_noinit_data)
extern std::uintptr_t __start_noinit_data;
extern std::uintptr_t __stop_noinit_data;
void init_noinit()
{
// Clear the noinit segment.
std::fill(&__start_noinit_data, &__stop_noinit_data, 0U);
}


 

ما علاقة تابع البناء بكود التهيئة startup code

 

بفرض انك قد عرّفت أحد الاغراض كـ static حينئذ على المترجم أن يقوم باستدعاء تابع البناء لجعل هذا الغرض جاهزاً قبل استدعاء التابع main.

يقول المؤلف : أغلب مترجمات ++C تولّد دالّة فرعية بداخلها كود البناء لكل غرض من هذه الأغراض ويحتفظ بعناوين هذه الدوال في مكان مخصص في الرابط linker. يقوم كود التهيئة باستدعاء هذه التوابع، وقد وضع الكاتب نسخته من كود التهيئة و linker script

// Author: Christopher Kormanyos
// Linker-defined begin and end of the ctors.
extern function_type* _ctors_begin[];
extern function_type* _ctors_end[];
void init_ctors()
{
std::for_each(_ctors_begin,
_ctors_end,
[](const function_type pf)
{
pf();
});
}

 

في الخاتمة

 

هناك العديد من الملاحظات التي جمعتها خلال قراءتي لهذا الكتاب ولكنني ذكرت الأكثر أهمية منها. كذلك فالكاتب قد أرفق في كل فصل من الكتاب بعدد من المصادر المهمة وكذلك فصل كامل بعنوان additional reading للاستزادة.

قرر أحد قرّاء عتاديات، فهد حسين، شراء الكتاب بعد قراءة مراجعتنا له وقام بأخذ هذه اللقطة المبدعة

في النهاية إن كنت قد قرأت أو صادفت كتاباً أو مقالة عن نفس الموضوع لاتتردّد في ذكره بتعليق.

 

Yahya Tawil

يحيى هو مدير التحرير في عتاديات ويؤمن بأهمية المحتوى المكتوب المجاني والنوعي والعملي. خبرته في مجال النظم المضمّنة تتركز في كتابة البرامج المضمنة وتصميم الدارات المطبوعة والنظرية وإنشاء المحتوى.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

هذا الموقع يستخدم Akismet للحدّ من التعليقات المزعجة والغير مرغوبة. تعرّف على كيفية معالجة بيانات تعليقك.

زر الذهاب إلى الأعلى