التين. أنا كيكوصحة جيدة للجميع!
ربما تتذكر حكاية ملتحة ، وربما قصة حقيقية عن كيفية سؤال الطالب عن طريقة لقياس ارتفاع المبنى باستخدام مقياس. استشهد الطالب ، في رأيي ، بحوالي 20 أو 30 طريقة ، دون ذكر المباشر (من خلال الاختلاف في الضغط) الذي توقعه المعلم.
في نفس السياق تقريبًا ، أرغب في متابعة مناقشة استخدام C ++ لأجهزة التحكم الدقيقة والنظر في طرق العمل مع السجلات التي تستخدم C ++. وأود أن أشير إلى أنه من أجل تحقيق الوصول الآمن إلى السجلات ، لن تكون هناك طريقة سهلة. سأحاول إظهار كل إيجابيات وسلبيات الأساليب. إذا كنت تعرف المزيد من الطرق ، فقم برميها في التعليقات. لذلك دعونا نبدأ:
الطريقة الأولى: من الواضح أنها ليست الأفضل
الطريقة الأكثر شيوعًا ، والتي تُستخدم أيضًا في C ++ ، هي استخدام وصف بنيات السجل من ملف الرأس من الشركة المصنعة. من أجل العرض التوضيحي ، سوف آخذ اثنين من سجلات المنفذ A (ODR - سجل بيانات الإخراج و IDR - سجل بيانات المدخلات) من متحكم STM32F411 حتى أتمكن من القيام بـ "Hello" في عالم التطريز - وميض مؤشر LED.
int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ;
دعونا نرى ما يحدث هنا وكيف يعمل هذا التصميم. يحتوي رأس المعالج الدقيق على بنية
GPIO_TypeDef
وتعريف مؤشر لهيكل
GPIOA
. يبدو مثل هذا:
typedef struct { __IO uint32_t MODER;
لوضعها بكلمات بشرية بسيطة ، فإن البنية
GPIO_TypeDef
لنوع
GPIO_TypeDef
"تقع" على العنوان
GPIOA_BASE
، وعند الوصول إلى حقل معين من البنية ، فإنك تشير أساسًا إلى عنوان هذه البنية + الإزاحة إلى عنصر من هذه البنية. إذا قمت بإزالة
#define GPIOA
،
#define GPIOA
الرمز كما يلي:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ;
بالنسبة إلى لغة البرمجة C ++ ، يتم تحويل عنوان عدد صحيح إلى نوع مؤشر إلى بنية
GPIO_TypeDef
. ولكن في C ++ ، عند استخدام التحويل C ، يحاول المحول البرمجي إجراء التحويل بالتسلسل التالي:
- const_cast
- static_cast
- static_cast بجانب const_cast ،
- reinterpret_cast
- reinterpret_cast بجانب const_cast
أي إذا تعذر على المحول البرمجي تحويل النوع باستخدام const_cast ، فسيحاول تطبيق static_cast وما إلى ذلك. نتيجة لذلك ، الدعوة:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
لا يوجد شيء مثل:
reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ;
في الواقع ، بالنسبة لتطبيقات C ++ ، سيكون من الصحيح "سحب" الهيكل على العنوان مثل هذا:
GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ;
في أي حال ، بسبب نوع التحويل ، هناك ناقص كبير لهذا الأسلوب لـ C ++. يتكون في حقيقة أنه لا يمكن استخدام
reinterpret_cast
لا في
constexpr
ووظائف
constexpr
، أو في معلمات القالب ، وهذا يقلل بشكل كبير من استخدام ميزات C ++ لأجهزة التحكم الصغيرة.
سأشرح هذا مع الأمثلة. من الممكن القيام بذلك:
struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; }
لكن لا يمكنك فعل ذلك بالفعل:
template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() {
وبالتالي ، فإن الاستخدام المباشر لهذا النهج يفرض قيودًا كبيرة على استخدام C ++. لن نتمكن من تحديد موقع الكائن الذي يريد استخدام
GPIOA
في ROM باستخدام أدوات اللغة ، ولن نتمكن من الاستفادة من metaprogramming لمثل هذا الكائن.
بالإضافة إلى ذلك ، بشكل عام ، هذه الطريقة ليست آمنة (كما يقول شركاؤنا الغربيون). بعد كل شيء ، فمن الممكن تماما لجعل بعض NON-FUN
فيما يتعلق بما سبق ، نلخص:
الأشياء الجيدة
- يتم استخدام العنوان من الشركة المصنعة (تم التحقق منه ، لا يوجد لديه أخطاء)
- لا توجد إيماءات وتكاليف إضافية تتخذها وتستخدمها
- سهولة الاستخدام
- الجميع يعرف ويفهم هذه الطريقة.
- لا النفقات العامة
سلبيات
- استخدام محدود من metaprogramming
- عدم القدرة على استخدامها في بناة constexpr
- عند استخدام برامج الالتفاف في الفئات ، فإن الاستهلاك الإضافي لذاكرة الوصول العشوائي هو مؤشر لكائن من هذه البنية
- يمكنك جعل غبي
الآن دعونا نلقي نظرة على الطريقة رقم 2
طريقة 2. وحشية
من الواضح أن كل مبرمج مدمج يضع في الاعتبار عناوين جميع السجلات لجميع ميكروكنترولر ، لذلك يمكنك ببساطة استخدام الطريقة التالية ، والتي تتبع من الأولى:
*reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ;
في أي مكان في البرنامج ، يمكنك دائمًا استدعاء التحويل إلى عنوان التسجيل
volatile uint32_t
وتثبيت شيء على الأقل هناك.
لا يوجد أي إيجابيات خاصة هنا ، ولكن بالنسبة لتلك السلبيات التي تضاف إزعاج للاستخدام والحاجة إلى كتابة عنوان كل سجل في ملف منفصل بنفسك. لذلك ، ننتقل إلى الطريقة رقم 3.
الطريقة الثالثة: من الواضح والأكثر صحة
إذا حدث الوصول إلى السجلات من خلال حقل البنية ، فبدلاً من مؤشر إلى كائن الهيكل ، يمكنك استخدام عنوان البنية الصحيح. عنوان الهياكل موجود في ملف الرأس من الشركة المصنعة (على سبيل المثال ، GPIOA_BASE لـ GPIOA) ، لذلك لا تحتاج إلى تذكره ، ولكن يمكنك استخدامه في القوالب وفي تعبيرات constexpr ، ثم "تراكب" البنية على هذا العنوان.
template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() {
لا توجد سلبيات خاصة ، من وجهة نظري. من حيث المبدأ ، خيار العمل. ولكن لا يزال ، دعونا نلقي نظرة على طرق أخرى.
الطريقة 4. التفاف Exoteric
لخبراء الكود المفهوم ، يمكنك عمل غلاف على السجل بحيث يكون ملائماً للوصول إليهم وتبدو "جميلة" ، وإنشاء مُنشئ ، وإعادة تعريف المشغلين:
class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr;
كما ترون ، مرة أخرى ، عليك إما أن تتذكر العناوين الصحيحة لجميع السجلات ، أو أن تضبطها في مكان ما ، وعليك أيضًا تخزين مؤشر على عنوان السجل. ولكن ما هو غير جيد للغاية مرة أخرى ، يحدث
reinterpret_cast
مرة أخرى في المنشئ
بعض العيوب ، وحقيقة أنه في الإصدار الأول والثاني تمت إضافة الحاجة لكل سجل يستخدم لتخزين مؤشر إلى 4 بايت في ذاكرة الوصول العشوائي. بشكل عام ، ليس خيارا. نحن ننظر إلى ما يلي.
طريقة 4،5. التفاف Exoteric مع نمط
نضيف حبة من metaprogramming ، ولكن ليس هناك فائدة كبيرة من هذا. تختلف هذه الطريقة عن الطريقة السابقة فقط حيث يتم نقل العنوان ليس إلى المنشئ ، ولكن في معلمة القالب ، نقوم بحفظ بعض السجلات عند نقل العنوان إلى المنشئ ، إنها جيدة بالفعل:
template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
وهكذا ، نفس أشعل النار ، وجهة نظر جانبية.
طريقة 5. معقولة
من الواضح أنك بحاجة إلى التخلص من المؤشر ، لذلك دعونا نفعل الشيء نفسه ، ولكن قم بإزالة المؤشر غير الضروري من الفصل.
template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
يمكنك البقاء هنا والتفكير قليلا. هذه الطريقة تحل على الفور مشكلتين سبق أن ورثتا عن الطريقة الأولى. أولاً ، الآن يمكنني استخدام المؤشر إلى كائن
Register
في القالب ، وثانياً ، يمكنني تمريره إلى مُنشئ
constexrp
.
template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ;
بالطبع ، من الضروري مرة أخرى ، إما أن يكون لديك ذاكرة eidetic لعناوين السجلات ، أو لتحديد جميع عناوين السجلات يدويًا في مكان ما في ملف منفصل ...
الأشياء الجيدة
- سهولة الاستخدام
- القدرة على استخدام metaprogramming
- القدرة على استخدامها في بناة constexpr
سلبيات
- لا يتم استخدام ملف الرأس الذي تم التحقق منه من الشركة المصنعة
- يجب عليك ضبط جميع عناوين السجلات بنفسك
- تحتاج إلى إنشاء كائن من فئة التسجيل
- يمكنك جعل غبي
عظيم ، ولكن لا يزال هناك الكثير من السلبيات ...
الطريقة 6. أكثر ذكاء من المعقول
في الطريقة السابقة ، من أجل الوصول إلى السجل ، كان من الضروري إنشاء كائن من هذا السجل ، وهذا هو مضيعة غير ضرورية من ذاكرة الوصول العشوائي و ROM ، لذلك نحن نفعل التفاف مع أساليب ثابتة.
template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5);
وأضاف واحد زائد
- لا النفقات العامة. رمز مضغوط سريع ، كما هو الحال في الخيار 1 (عند استخدام الأغلفة في الفئات ، لا توجد تكلفة RAM إضافية ، نظرًا لعدم إنشاء الكائن ، ولكن يتم استخدام الأساليب الثابتة دون إنشاء كائنات)
المضي قدما ...
طريقة 7. إزالة الغباء
من الواضح أنني أقوم دائمًا بعمل NON-FUNNY في الكود وأكتب شيئًا في السجل ، وهو في الواقع ليس مخصصًا للكتابة. لا بأس ، بالطبع ، لكن الغباء يجب حظره. دعنا نمنع القيام بهذا الهراء. للقيام بذلك ، نقدم هياكل مساعدة:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {};
الآن يمكننا تعيين السجلات للكتابة ، والسجلات للقراءة فقط:
template<uint32_t addr, typename RegisterType> class Register { public:
الآن ، دعونا نحاول تجميع
Idr
ونرى أن الاختبار لا يتم
Idr
، لأن المشغل
^=
Idr
غير موجود:
int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ;
لذلك ، الآن هناك المزيد من الإيجابيات ...
الأشياء الجيدة
- سهولة الاستخدام
- القدرة على استخدام metaprogramming
- القدرة على استخدامها في بناة constexpr
- رمز مضغوط سريع ، كما في الخيار 1
- عند استخدام برامج الالتفاف في الفئات ، لا توجد تكلفة RAM إضافية ، حيث لا يتم إنشاء الكائن ، ولكن يتم استخدام الأساليب الثابتة دون إنشاء كائنات
- لا يمكنك أن تفعل الغباء
سلبيات
- لا يتم استخدام ملف الرأس الذي تم التحقق منه من الشركة المصنعة
- يجب عليك ضبط جميع عناوين السجلات بنفسك
- تحتاج إلى إنشاء كائن من فئة التسجيل
لذلك دعونا نزيل الفرصة لإنشاء فصل لتوفير المزيد
الطريقة 8. بدون NONSENSE وبدون كائن فئة
كود على الفور:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ;
نضيف واحد زائد ، ونحن لا ننشئ كائنًا. ولكن المضي قدما ، لا يزال لدينا سلبيات
الطريقة 9. الطريقة 8 مع تكامل البنية
في الطريقة السابقة ، تم تعريف الحالة فقط. ولكن في الطريقة الأولى ، يتم دمج جميع السجلات في هياكل بحيث يمكنك الوصول إليها بسهولة بواسطة الوحدات النمطية. لنقم بذلك ...
namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>;
هنا ناقص هو أن الهياكل سوف تحتاج إلى تسجيل من جديد ، ويجب أن نتذكر إزاحة جميع السجلات وتحديدها في مكان ما. سيكون من الرائع أن يتم تعيين الإزاحات من قبل المترجم ، وليس من قبل الشخص ، ولكن هذا لاحقًا ، لكن الآن سننظر في طريقة أخرى مثيرة للاهتمام اقترحها زميلي.
الطريقة 10. التفاف فوق السجل من خلال مؤشر إلى عضو في البنية
يستخدم مثل هذا المفهوم كمؤشر لعضو البنية
والوصول إليها .
template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ;
الأشياء الجيدة
- سهولة الاستخدام
- القدرة على استخدام metaprogramming
- القدرة على استخدامها في بناة constexpr
- رمز مضغوط سريع ، كما في الخيار 1
- عند استخدام برامج الالتفاف في الفئات ، لا توجد تكلفة RAM إضافية ، حيث لا يتم إنشاء الكائن ، ولكن يتم استخدام الأساليب الثابتة دون إنشاء كائنات
- يتم استخدام ملف الرأس الذي تم التحقق منه من الشركة المصنعة.
- لا حاجة لتعيين جميع عناوين التسجيل بنفسك
- لا حاجة لإنشاء كائن من فئة التسجيل
سلبيات
- يمكنك جعل الحماقة وحتى التكهن بشأن شمولية الكود.
طريقة 10.5. الجمع بين الطريقة 9 و 10
لمعرفة تحول السجل بالنسبة إلى بداية البنية ، يمكنك استخدام المؤشر لعضو البنية: عضو
volatile uint32_t T::*member
، سيعود إزاحة عضو الهيكل بالنسبة إلى بدايته بالبايت. على سبيل المثال ، لدينا بنية
GPIO_TypeDef
، ثم العنوان
&GPIO_TypeDef::ODR
سيكون 0x14.
لقد تغلبنا على هذه الفرصة ونحسب عناوين السجلات من الطريقة 9 ، باستخدام المترجم:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ;
يمكنك العمل مع السجلات بشكل أكثر غرابة:
using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ;
من الواضح ، هنا يجب إعادة كتابة جميع الهياكل مرة أخرى. يمكن القيام بذلك تلقائيًا ، بواسطة بعض البرامج النصية في Phyton ، عند إدخال شيء مثل stm32f411xe.h عند إخراج ملفك مع بنيات للاستخدام في C ++.
في أي حال ، هناك العديد من الطرق المختلفة التي قد تعمل في مشروع معين.
مكافأة. نحن نقدم امتداد لغة ورمز parsim باستخدام Phyton
كانت مشكلة العمل مع السجلات في C ++ موجودة لبعض الوقت. يحلها الناس بطرق مختلفة. بالطبع سيكون من الرائع أن تدعم اللغة شيئًا ما مثل إعادة تسمية الفئات في وقت الترجمة. حسنًا ، دعنا نقول ، ماذا لو كان مثل هذا:
template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; }
لكن لسوء الحظ هذه اللغة لا تدعم. لذلك ، فإن الحل الذي يستخدمه الناس هو تحليل الكود باستخدام Python. أي تم تقديم بعض امتدادات اللغة. يتم تغذية الكود ، باستخدام هذا الامتداد ، إلى محلل Python ، والذي يترجمه إلى رمز C ++. يشبه هذا الرمز ما يلي: (مثال مأخوذ من مكتبة المودم ؛ فيما
يلي المصادر الكاملة ):
%% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } }
تحديث: مكافأة. ملفات SVD ومحلل على Phyton
نسيت أن أضيف خيار آخر. يقوم ARM بإصدار ملف وصف تسجيل لكل مُصنع SVD. يمكنك من خلالها إنشاء ملف C ++ مع وصف للسجلات. قام بول أوزبورن بتجميع كل هذه الملفات على
جيثب . كما كتب سيناريو بيثون لتحليلهم.
هذا كل شيء ... مخيلتي استنفدت. إذا كنت لا تزال لديك أفكار ، فلا تتردد في. مثال على كل الأساليب
يكمن هنا.مراجع
Typesafe تسجيل الوصول في C ++جعل الأشياء تفعل أشياء - الوصول إلى الأجهزة من C ++جعل الأشياء تفعل أشياء - الجزء 3جعل الأشياء تفعل أشياء ، هيكل تراكب