STM32F4 لوحة التصحيح في شكل Raspberry Pi Factor

الصورة مساء الخير عزيزي الخبروفيت! أريد أن أعرض مشروعي للجمهور - لوحة تصحيح صغيرة تستند إلى STM32 ، ولكن في شكل Raspberry Pi. يختلف عن لوحات التصحيح الأخرى في أنه يحتوي على هندسة متوافقة مع حالات Raspberry Pi ووحدة ESP8266 كمودم لاسلكي. وكذلك إضافات لطيفة على شكل موصل لبطاقة micro-SD ومكبر صوت ستيريو. للاستفادة من كل هذه الثروة ، قمت بتطوير مكتبة عالية المستوى وبرنامج تجريبي (في C ++ 11). في المقالة ، أريد أن أصف بالتفصيل كل من الأجزاء المادية والبرمجيات لهذا المشروع.


من يمكنه الاستفادة من هذا المشروع؟ ربما ، فقط لأولئك الذين يريدون لحام هذه اللوحة بنفسي ، لأنني لا أفكر في أي خيارات حتى للإنتاج على نطاق صغير. هذه هواية خالصة. في رأيي ، يغطي المجلس مجموعة واسعة إلى حد ما من المهام التي قد تنشأ في إطار الحرف المنزلية الصغيرة باستخدام WiFi والصوت.


بادئ ذي بدء ، سأحاول الإجابة على السؤال لماذا هذا كله. الدوافع الرئيسية لهذا المشروع هي كما يلي:


  • يرجع اختيار منصة STM32 إلى اعتبارات جمالية بحتة - أحب نسبة السعر / الأداء ، بالإضافة إلى مجموعة كبيرة من الأجهزة الطرفية ، بالإضافة إلى بيئة تطوير كبيرة ومريحة من الشركة المصنعة للتحكم (sw4stm ، cubeMX ، مكتبة HAL).
  • بالطبع ، هناك العديد من لوحات التصحيح من الشركة المصنعة لوحدة التحكم نفسها (Discovery ، Nucleo) ، وكذلك من الشركات المصنعة لجهات خارجية (على سبيل المثال ، Olimex). لكن تكرار العديد منهم في المنزل في شكلهم يمثل مشكلة بالنسبة لي على الأقل. في إصداري ، لدينا طوبولوجيا بسيطة مكونة من طبقتين ومكونات ملائمة للحام اليدوي.
  • لأجهزتهم ، أريد أن يكون لدي حالات لائقة من أجل إخفاء الجودة المنخفضة للإلكترونيات بالداخل. هناك على الأقل نظامان أساسيان شائعان يوجد بهما عدد كبير من الحالات الأكثر تنوعًا: Arduino و Raspberry Pi. بدا لي الثاني منهم أكثر ملاءمة من حيث موقع القواطع للموصلات. لذلك ، كمتبرع لهندسة اللوحة ، اخترت ذلك.
  • وحدة التحكم التي اخترتها على متن الطائرة مزودة بشبكة USB و SDIO و I2S. من ناحية أخرى ، فإن هذه الواجهات نفسها مفيدة أيضًا لمنصة الهوايات المنزلية. لهذا السبب ، بالإضافة إلى وحدة التحكم ذات الربط القياسي ، أضفت موصل USB وبطاقة SD ومسار صوتي (محول رقمي إلى تناظري ومضخم) ، بالإضافة إلى وحدة لاسلكية تعتمد على ESP8266.

الدائرة والمكونات


يبدو لي أن لوحة جميلة ذات الخصائص والمكونات التالية قد ظهرت:


  • وحدة تحكم STM32F405RG : ARM 32 بت Cortex-M4 مع معالج رياضي ، تردد يصل إلى 168 ميجاهرتز ، ذاكرة فلاش 1 ميجابايت ، 196 كيلوبايت من ذاكرة الوصول العشوائي.
    دبابيس تحكم مستعملة
    ربط جهاز التحكم
  • موصل SWD لبرمجة وحدة التحكم (6 دبابيس).
  • زر إعادة التشغيل لإعادة التشغيل.
  • LED ثلاثي الألوان. من ناحية ، يتم فقدان ثلاثة دبابيس تحكم. من ناحية أخرى ، ستظل مفقودة بسبب جهات الاتصال المحدودة على موصلات GPIO ، ولتصحيح مثل هذا LED ، فإن الشيء مفيد جدًا.
  • كوارتز HSE عالي التردد (16 ميجاهرتز للساعة الأساسية) و LSE بتردد منخفض (32.7680 كيلوهرتز للساعة في الوقت الفعلي).
  • تتوافق دبابيس GPIO مع خطوة 2.54 مم مع طرازات لوحة اللوح.
  • بدلاً من مقبس الصوت 3.5 مم لـ Raspberry Pi ، قمت بوضع موصل الطاقة 5 فولت. للوهلة الأولى ، القرار مثير للجدل. ولكن هناك إيجابيات. الطاقة من موصل USB موجودة اختياريًا (التفاصيل أدناه) ، ولكن هذا خيار سيئ لتصحيح الأخطاء في الدائرة ، لأن الوقت قبل حرق منفذ USB للكمبيوتر في هذه الحالة قد يكون قصيرًا جدًا.

دائرة الطاقة


  • منفذ USB صغير من ناحية ، يتم توصيله عبر شريحة الحماية STF203-22.TCT بمنفذ USB-OTG الخاص بوحدة التحكم. من ناحية أخرى ، يتم توصيل دبوس الطاقة VBUS بموصل GPIO. إذا قمت بتوصيله بدبوس + 5 فولت ، فسيتم تشغيل اللوحة من منفذ USB.

دائرة USB


  • موصل لبطاقة ذاكرة micro-SD بحزام: 47 كيلو أوم مقاومات سحب ، ترانزستور إدارة الطاقة ( P-channel MOSFET BSH205 ) ومصباح أخضر صغير على خط الطاقة.

مخطط بطاقة Micro SD


يتم توصيل بوابة الترانزستور بدبوس PA15 لوحدة التحكم. هذا هو اتصال النظام لوحدة تحكم JTDI ، وهو أمر مثير للاهتمام في أنه في الوضع الأولي يتم تكوينه كناتج بمستوى عالي (سحب) للجهد. نظرًا لاستخدام SWD بدلاً من JTAG للبرمجة ، تظل جهة الاتصال هذه مجانية ويمكن استخدامها لأغراض أخرى ، على سبيل المثال ، التحكم في الترانزستور. هذا مريح - عندما يتم تطبيق الطاقة على اللوحة ، يتم إلغاء تنشيط بطاقة الذاكرة ؛ لتمكينها ، تحتاج إلى تطبيق مستوى منخفض لتثبيت دبوس PA15.


  • محول رقمي إلى تمثيلي يعتمد على UDA1334 . لا تحتاج هذه الشريحة إلى إشارة ساعة خارجية ، مما يسهل استخدامها. يتم إرسال البيانات عبر ناقل I2S. من ناحية أخرى ، توصي ورقة البيانات باستخدام ما يصل إلى 5 مكثفات قطبية عند 47 درجة فهرنهايت. الحجم مهم في هذه الحالة. أصغرها التي تم شراؤها هي التنتالوم بحجم 1411 ، وهي ليست رخيصة حتى. ومع ذلك ، سأكتب عن السعر بمزيد من التفاصيل أدناه. بالنسبة للطاقة التناظرية ، يتم استخدام المثبت الخطي الخاص به ، ويتم تشغيل / إيقاف طاقة الجزء الرقمي بواسطة ترانزستور مزدوج.

دارة DAC


  • مضخم ثنائي القناة يعتمد على شريحتين 31AP2005 . ميزتها الرئيسية هي عدد صغير من مكونات الربط (فقط مرشحات الطاقة وفلتر الإدخال). خرج الصوت - 4 منصات مع خطوة 2.54 ملم. بالنسبة لي ، لم أقرر بعد ما هو الأفضل - مثل هذا الخيار المؤقت أو ، مثل المكونات الموجودة في التوت ، 3.5 ملم. كقاعدة عامة ، يرتبط 3.5 مم بسماعات الرأس ، في حالتنا نتحدث عن توصيل مكبرات الصوت.

دارة مكبر للصوت


  • الوحدة الأخيرة هي شال ESP11 مع ربط (طاقة ، مقبس برمجة) كمودم WiFi. يتم توصيل استنتاجات لوحة UART بجهاز التحكم وإخراجها في الوقت نفسه بموصل خارجي (للعمل مع اللوحة مباشرة من المحطة والبرمجة). يوجد مفتاح طاقة (خارجي أو تحكم دائم من متحكم). هناك LED إضافي للإشارة إلى الطاقة وموصل "FLASH" لوضع اللوحة في وضع البرمجة.

دارة ESP


بالطبع ، ESP8266 نفسها هي وحدة تحكم جيدة ، لكنها لا تزال أقل من STM32F4 في كل من الأداء والأجهزة الطرفية. نعم ، وأشار الحجم مع سعر هذه الوحدة إلى أنها كانت وحدة مودم انسكاب لأخيه الأكبر. يتم التحكم في الوحدة النمطية بواسطة USRT باستخدام بروتوكول AT نص.


زوجان من الصور:


تحضير وحدة ESP11


ESP8266 شيء معروف. أنا متأكد من أن الكثيرين على دراية بها بالفعل ، لذلك سيكون الدليل التفصيلي غير ضروري هنا. نظرًا للميزات التخطيطية لتوصيل وحدة ESP11 باللوحة ، سأقدم فقط دليلًا موجزًا ​​لأولئك الذين يرغبون في تغيير البرامج الثابتة الخاصة بها:


  • سأستخدم الأداة المساعدة esptool للعمل مع ESP. بخلاف الأداة المساعدة القياسية من الشركة المصنعة ، فإن esptool مستقل عن النظام الأساسي.
  • بادئ ذي بدء ، قم بتشغيل وضع الطاقة الخارجية باستخدام وصلة المرور ESP-PWR (نقوم بإغلاق جهات الاتصال 1 و 2) ، وقم بتوصيل الوحدة بالكمبيوتر عبر أي محول USART-USB. يتصل المحول بدبابيس GRD / RX / TD. نحن نزوّد المجلس:
  • نتأكد من أن نظام التشغيل يتعرف على المحول. في المثال الخاص بي ، أستخدم محولًا يعتمد على FT232 ، لذا مع قائمة الأجهزة ، يجب أن يكون مرئيًا مثل FT232 Serial (UART) IC:
    > lsusb ... Bus 001 Device 010: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 009: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC ... 
  • تختلف ESP8266 نفسها في حجم ذاكرة الفلاش. في الممارسة العملية ، في نفس وحدة ESP11 ، واجهت كلاً من 512 كيلوبايت (4 ميجابت) و 1 ميجابايت (8 ميجابت). لذا فإن أول شيء يجب التحقق منه هو مقدار الذاكرة في النسخة المستخدمة من الوحدة. قم بإيقاف تشغيل الطاقة من اللوحة ، ووضع الوحدة في وضع البرمجة ، وإغلاق الطائر "فلاش":


  • قم بتشغيل الطاقة ، قم بتشغيل esptool بالمعلمات التالية

 > esptool.py --port /dev/ttyUSB0 flash_id Connecting.... Detecting chip type... ESP8266 Chip is ESP8266EX Uploading stub... Running stub... Stub running... Manufacturer: e0 Device: 4014 Detected flash size: 1MB Hard resetting... 

  • تفيد esptool أننا في هذه الحالة نتعامل مع وحدة بسعة 1 ميجابايت من الذاكرة.
  • بالنسبة للإصدار الذي يحتوي على 1 ميغابايت ، يمكنك استخدام أحدث البرامج الثابتة ، على سبيل المثال ، ESP8266 AT Bin V1.6.1 . لكنها ليست مناسبة للإصدار الذي يحتوي على 4 ميغابت ، والذي تحتاج إلى استخدام شيء أقدم منه ، على سبيل المثال ، هذا . يتكون البرنامج الثابت من عدة ملفات ، يشار إلى عناوين البدء لكل ملف في الوثيقة الرسمية ESP8266 AT In Set Set . تُستخدم عناوين البدء هذه كمعلمات لأداة esptool المساعدة. على سبيل المثال ، بالنسبة لوحدة نمطية بسعة 1 ميجابايت ، ستبدو معلمات esptool على هذا النحو (يجب أولاً استخراج جميع الملفات الضرورية من أرشيف البرامج الثابتة وجمعها في دليل العمل)
     > esptool.py --port /dev/ttyUSB0 write_flash 0x00000 boot.bin 0x01000 user1.1024.new.2.bin 0x7E000 blank.bin 0xFB000 blank.bin 0xFC000 esp_init_data_default.bin 0xFE000 blank.bin 
  • نحن نزود الطاقة بالمجلس ونشغل esptool بالمعلمات المحددة.
  • بعد الانتهاء من البرنامج النصي ، قم بإيقاف تشغيل الطاقة من اللوحة ، افتح وصلة "FLASH" ، قم بتشغيل التحكم في الطاقة من وحدة التحكم الدقيقة. الوحدة جاهزة للعمل.

البرمجيات


هناك برنامج اختبار على جيثب . تقوم بما يلي:


  • يعرض وحدة التحكم عند أقصى تردد (168 ميجا هرتز)
  • ينشط ساعة الوقت الحقيقي
  • ينشط بطاقة SD ويقرأ تكوين الشبكة منه. يتم استخدام مكتبة FatFS للعمل مع نظام الملفات.
  • يؤسس اتصالاً بشبكة WLAN المحددة
  • يتصل بخادم NTP المحدد ويطلب الوقت الحالي منه. يقود الساعة.
  • يراقب حالة العديد من المنافذ المحددة. إذا تغيرت حالتهم ، يرسل رسالة نصية إلى خادم TCP المحدد.
  • عند النقر على الزر الخارجي ، فإنه يقرأ ملف * .wav المحدد من بطاقة SD ويقوم بتشغيله في وضع غير متزامن (I2S باستخدام وحدة تحكم DMA).
  • يتم تنفيذ العمل باستخدام ESP11 أيضًا في الوضع غير المتزامن (حتى الآن بدون DMA ، فقط على المقاطعات)
  • تسجيل الدخول عبر USART1 (دبابيس PB6 / PB7)
  • وبالطبع يومض مؤشر LED.

في حبري كان هناك العديد من المقالات المخصصة لبرمجة STM32 على مستوى منخفض إلى حد ما (فقط من خلال إدارة التسجيل أو CMSIS). على سبيل المثال ، من الأخير نسبيًا: واحد ، اثنان ، ثلاثة . المقالات ، بالطبع ، ذات جودة عالية جدًا ، لكن رأيي الشخصي هو أنه من أجل تطوير منتج لمرة واحدة ، ربما يبرر هذا النهج نفسه. ولكن بالنسبة لمشروع هواية طويل الأجل ، عندما تريد أن يكون كل شيء جميلًا وقابلاً للتوسيع ، فإن هذا النهج منخفض للغاية. أحد أسباب شعبية Arduino كمنصة برمجيات ، في رأيي ، هو أن مؤلفي Arduino تركوا مثل هذا المستوى المنخفض للهندسة الموجهة للكائنات. لذلك ، قررت أن أذهب في نفس الاتجاه وأضيف طبقة عالية المستوى موجهة للكائنات إلى حد ما فوق مكتبة HAL.


وبالتالي ، يتم الحصول على ثلاثة مستويات من البرنامج:


  • تشكل مكتبات المُصنِّعين (HAL ، FatFS ، في المستقبل USB-OTG) الأساس
  • تعتمد مكتبة StmPlusPlus الخاصة بي على هذا الأساس. يتضمن مجموعة من الفئات الأساسية (مثل System و IOPort و IOPin و Timer و RealTimeClock و Usart و Spi و I2S) ومجموعة من فئات برامج التشغيل للأجهزة الخارجية (مثل SdCard و Esp11 و DcfReceiver و Dac_MCP49x1 و AudioDac_UDA1334 وما شابه) ، وكذلك فئات الخدمة مثل مشغل WAV غير المتزامن.
  • بناءً على مكتبة StmPlusPlus ، يتم إنشاء التطبيق نفسه.

أما لهجة اللغة. بينما أنا قديم إلى حد ما ، ما زلت في C ++ 11. يحتوي هذا المعيار على العديد من الميزات المفيدة بشكل خاص لتطوير البرامج الثابتة: فئات التعداد واستدعاء المُنشئين ذوي الأقواس المتعرجة للتحكم في أنواع المعلمات التي تم تمريرها والحاويات الثابتة مثل std :: array. بالمناسبة ، على حبري هناك مقالة رائعة حول هذا الموضوع.


مكتبة StmPlusPlus


يمكن الاطلاع على كود المكتبة الكامل على جيثب . سأعطي هنا بعض الأمثلة الصغيرة فقط لإظهار الهيكل والفكرة والمشكلات الناتجة عن هذه الفكرة.


المثال الأول هو فئة لاستقصاء حالة دبوس بشكل دوري (على سبيل المثال ، زر) واستدعاء المعالج عندما تتغير هذه الحالة:


 class Button : IOPin { public: class EventHandler { public: virtual void onButtonPressed (const Button *, uint32_t numOccured) =0; }; Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay = 50, duration_ms _pressDuration = 300); inline void setHandler (EventHandler * _handler) { handler = _handler; } void periodic (); private: const RealTimeClock & rtc; duration_ms pressDelay, pressDuration; time_ms pressTime; bool currentState; uint32_t numOccured; EventHandler * handler; }; 

يحدد المُنشئ جميع معلمات الزر:


 Button::Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay, duration_ms _pressDuration): IOPin{name, pin, GPIO_MODE_INPUT, pull, GPIO_SPEED_LOW}, rtc{_rtc}, pressDelay{_pressDelay}, pressDuration{_pressDuration}, pressTime{INFINITY_TIME}, currentState{false}, numOccured{0}, handler{NULL} { // empty } 

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


 void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) { // state is not changed: check for periodical press event if (currentState && pressTime != INFINITY_TIME) { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d >= pressDuration) { handler->onButtonPressed(this, numOccured); pressTime = rtc.getUpTimeMillisec(); ++numOccured; } } } else if (!currentState && newState) { pressTime = rtc.getUpTimeMillisec(); numOccured = 0; } else { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d < pressDelay) { // nothing to do } else if (numOccured == 0) { handler->onButtonPressed(this, numOccured); } pressTime = INFINITY_TIME; } currentState = newState; } 

الميزة الرئيسية لهذا النهج هي تنوع المنطق والرمز لاكتشاف حدث من معالجته. لا يتم استخدام HAL_GetTick لحساب الوقت ، والذي ، بسبب نوعه (uint32_t) ، يتم إعادة تعيينه عن طريق تجاوز سعة كل 2 ^ 32 مللي ثانية (كل 49 يومًا). قمت بتطبيق فصلي RealTimeClock ، الذي يحسب بالمللي ثانية من بداية البرنامج ، أو تشغيل وحدة التحكم مثل uint64_t ، والتي تعطي حوالي 5 ^ 8 سنوات.


المثال الثاني هو العمل مع واجهة الأجهزة ، والتي يوجد منها العديد في وحدة التحكم. على سبيل المثال ، SPI. من وجهة نظر البرنامج الرئيسي ، من الملائم جدًا تحديد الواجهة المطلوبة فقط (SPI1 / SPI2 / SPI3) ، وسيتم تكوين جميع المعلمات الأخرى التي تعتمد على هذه الواجهة بواسطة مُنشئ الفئة.


 class Spi { public: const uint32_t TIMEOUT = 5000; enum class DeviceName { SPI_1 = 0, SPI_2 = 1, SPI_3 = 2, }; Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull = GPIO_NOPULL); HAL_StatusTypeDef start (uint32_t direction, uint32_t prescaler, uint32_t dataSize = SPI_DATASIZE_8BIT, uint32_t CLKPhase = SPI_PHASE_1EDGE); HAL_StatusTypeDef stop (); inline HAL_StatusTypeDef writeBuffer (uint8_t *pData, uint16_t pSize) { return HAL_SPI_Transmit(hspi, pData, pSize, TIMEOUT); } private: DeviceName device; IOPin sck, miso, mosi; SPI_HandleTypeDef *hspi; SPI_HandleTypeDef spiParams; void enableClock(); void disableClock(); }; 

يتم تخزين معلمات الدبوس ومعلمات الواجهة محليًا في الفصل. للأسف ، اخترت خيار تنفيذ غير ناجح تمامًا ، عندما يتم تنفيذ إعدادات المعلمات اعتمادًا على واجهة معينة مباشرةً:


 Spi::Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull): device(_device), sck(sckPort, sckPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), miso(misoPort, misoPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), mosi(mosiPort, mosiPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), hspi(NULL) { switch (device) { case DeviceName::SPI_1: #ifdef SPI1 sck.setAlternate(GPIO_AF5_SPI1); miso.setAlternate(GPIO_AF5_SPI1); mosi.setAlternate(GPIO_AF5_SPI1); spiParams.Instance = SPI1; #endif break; ... case DeviceName::SPI_3: #ifdef SPI3 sck.setAlternate(GPIO_AF6_SPI3); miso.setAlternate(GPIO_AF6_SPI3); mosi.setAlternate(GPIO_AF6_SPI3); spiParams.Instance = SPI3; #endif break; } spiParams.Init.Mode = SPI_MODE_MASTER; spiParams.Init.DataSize = SPI_DATASIZE_8BIT; spiParams.Init.CLKPolarity = SPI_POLARITY_HIGH; spiParams.Init.CLKPhase = SPI_PHASE_1EDGE; spiParams.Init.FirstBit = SPI_FIRSTBIT_MSB; spiParams.Init.TIMode = SPI_TIMODE_DISABLE; spiParams.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiParams.Init.CRCPolynomial = 7; spiParams.Init.NSS = SPI_NSS_SOFT; } 

نفس المخطط يطبق إجراءات enableClock و disableClock ، والتي تكون قابلة للتوسعة بشكل سيء وقابلة للنقل بشكل سيء إلى وحدات التحكم الأخرى. في هذه الحالة ، من الأفضل استخدام القوالب حيث تكون معلمة القالب هي اسم واجهة HAL (SPI1 و SPI2 و SPI3) ومعلمات الدبوس (GPIO_AF5_SPI1) وشيء يتحكم في تشغيل / إيقاف الساعة. هناك مقال مثير للاهتمام حول هذا الموضوع ، على الرغم من أنه يراجع وحدات تحكم AVR ، والتي ، مع ذلك ، لا تحدث فرقًا أساسيًا.


يتم التحكم في بداية ونهاية التحويل بطريقتين لبدء / إيقاف:


 HAL_StatusTypeDef Spi::start (uint32_t direction, uint32_t prescaler, uint32_t dataSize, uint32_t CLKPhase) { hspi = &spiParams; enableClock(); spiParams.Init.Direction = direction; spiParams.Init.BaudRatePrescaler = prescaler; spiParams.Init.DataSize = dataSize; spiParams.Init.CLKPhase = CLKPhase; HAL_StatusTypeDef status = HAL_SPI_Init(hspi); if (status != HAL_OK) { USART_DEBUG("Can not initialize SPI " << (size_t)device << ": " << status); return status; } /* Configure communication direction : 1Line */ if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } /* Check if the SPI is already enabled */ if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { /* Enable SPI peripheral */ __HAL_SPI_ENABLE(hspi); } USART_DEBUG("Started SPI " << (size_t)device << ": BaudRatePrescaler = " << spiParams.Init.BaudRatePrescaler << ", DataSize = " << spiParams.Init.DataSize << ", CLKPhase = " << spiParams.Init.CLKPhase << ", Status = " << status); return status; } HAL_StatusTypeDef Spi::stop () { USART_DEBUG("Stopping SPI " << (size_t)device); HAL_StatusTypeDef retValue = HAL_SPI_DeInit(&spiParams); disableClock(); hspi = NULL; return retValue; } 

العمل مع واجهة الأجهزة باستخدام المقاطعات . تطبق الفئة واجهة I2S باستخدام جهاز تحكم DMA. I2S (Inter-IC Sound) هو عبارة عن وظيفة إضافية للأجهزة والبرامج عبر SPI ، والتي ، على سبيل المثال ، تختار تردد الساعة وتتحكم في القنوات اعتمادًا على بروتوكول الصوت ومعدل البت.


في هذه الحالة ، يتم توريث فئة I2S من فئة "المنفذ" ، أي أن I2S عبارة عن منفذ بخصائص خاصة. يتم تخزين بعض البيانات في هياكل HAL (بالإضافة إلى الراحة ، ناقص لكمية البيانات). يتم نقل بعض البيانات من الكود الرئيسي عن طريق الروابط (على سبيل المثال ، بنية irqPrio).


 class I2S : public IOPort { public: const IRQn_Type I2S_IRQ = SPI2_IRQn; const IRQn_Type DMA_TX_IRQ = DMA1_Stream4_IRQn; I2S (PortName name, uint32_t pin, const InterruptPriority & prio); HAL_StatusTypeDef start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat); void stop (); inline HAL_StatusTypeDef transmit (uint16_t * pData, uint16_t size) { return HAL_I2S_Transmit_DMA(&i2s, pData, size); } inline void processI2SInterrupt () { HAL_I2S_IRQHandler(&i2s); } inline void processDmaTxInterrupt () { HAL_DMA_IRQHandler(&i2sDmaTx); } private: I2S_HandleTypeDef i2s; DMA_HandleTypeDef i2sDmaTx; const InterruptPriority & irqPrio; }; 

يقوم مُنشئه بتعيين جميع المعلمات الثابتة:


 I2S::I2S (PortName name, uint32_t pin, const InterruptPriority & prio): IOPort{name, GPIO_MODE_INPUT, GPIO_NOPULL, GPIO_SPEED_FREQ_LOW, pin, false}, irqPrio{prio} { i2s.Instance = SPI2; i2s.Init.Mode = I2S_MODE_MASTER_TX; i2s.Init.Standard = I2S_STANDARD_PHILIPS; // will be re-defined at communication start i2s.Init.DataFormat = I2S_DATAFORMAT_16B; // will be re-defined at communication start i2s.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE; i2s.Init.AudioFreq = I2S_AUDIOFREQ_44K; // will be re-defined at communication start i2s.Init.CPOL = I2S_CPOL_LOW; i2s.Init.ClockSource = I2S_CLOCK_PLL; i2s.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE; i2sDmaTx.Instance = DMA1_Stream4; i2sDmaTx.Init.Channel = DMA_CHANNEL_0; i2sDmaTx.Init.Direction = DMA_MEMORY_TO_PERIPH; i2sDmaTx.Init.PeriphInc = DMA_PINC_DISABLE; i2sDmaTx.Init.MemInc = DMA_MINC_ENABLE; i2sDmaTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; i2sDmaTx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; i2sDmaTx.Init.Mode = DMA_NORMAL; i2sDmaTx.Init.Priority = DMA_PRIORITY_LOW; i2sDmaTx.Init.FIFOMode = DMA_FIFOMODE_ENABLE; i2sDmaTx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; i2sDmaTx.Init.MemBurst = DMA_PBURST_SINGLE; i2sDmaTx.Init.PeriphBurst = DMA_PBURST_SINGLE; } 

يتم التحكم في بداية نقل البيانات من خلال طرق البدء ، وهي المسؤولة عن تكوين معلمات المنفذ ، وتسجيل وقت الواجهة ، وتكوين المقاطعات ، وبدء DMA ، وبدء الواجهة نفسها مع معلمات الإرسال المحددة.


 HAL_StatusTypeDef I2S::start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat) { i2s.Init.Standard = standard; i2s.Init.AudioFreq = audioFreq; i2s.Init.DataFormat = dataFormat; setMode(GPIO_MODE_AF_PP); setAlternate(GPIO_AF5_SPI2); __HAL_RCC_SPI2_CLK_ENABLE(); HAL_StatusTypeDef status = HAL_I2S_Init(&i2s); if (status != HAL_OK) { USART_DEBUG("Can not start I2S: " << status); return HAL_ERROR; } __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_LINKDMA(&i2s, hdmatx, i2sDmaTx); status = HAL_DMA_Init(&i2sDmaTx); if (status != HAL_OK) { USART_DEBUG("Can not initialize I2S DMA/TX channel: " << status); return HAL_ERROR; } HAL_NVIC_SetPriority(I2S_IRQ, irqPrio.first, irqPrio.second); HAL_NVIC_EnableIRQ(I2S_IRQ); HAL_NVIC_SetPriority(DMA_TX_IRQ, irqPrio.first + 1, irqPrio.second); HAL_NVIC_EnableIRQ(DMA_TX_IRQ); return HAL_OK; } 

إجراء التوقف يفعل عكس ذلك:


 void I2S::stop () { HAL_NVIC_DisableIRQ(I2S_IRQ); HAL_NVIC_DisableIRQ(DMA_TX_IRQ); HAL_DMA_DeInit(&i2sDmaTx); __HAL_RCC_DMA1_CLK_DISABLE(); HAL_I2S_DeInit(&i2s); __HAL_RCC_SPI2_CLK_DISABLE(); setMode(GPIO_MODE_INPUT); } 

هناك العديد من الميزات المثيرة للاهتمام هنا:


  • يتم تعريف المقاطعات المستخدمة في هذه الحالة على أنها ثوابت ثابتة. هذا ناقص لسهولة التحكم إلى وحدات تحكم أخرى.
  • يضمن تنظيم الرمز هذا أن دبابيس المنفذ تكون دائمًا في حالة GPIO_MODE_INPUT عندما لا يكون هناك إرسال. هذا زائد.
  • يتم نقل أولوية المقاطعات من الخارج ، أي أن هناك فرصة جيدة لتعيين خريطة أولوية المقاطعة في مكان واحد من الرمز الرئيسي. هذا أيضا زائد.
  • تعطيل إجراء الإيقاف DMA1. في هذه الحالة ، يمكن أن يكون لهذا التبسيط عواقب سلبية للغاية إذا استمر شخص آخر في استخدام DMA1. يتم حل المشكلة عن طريق إنشاء سجل مركزي للمستهلكين لهذه الأجهزة ، والتي ستكون مسؤولة عن التوقيت.
  • تبسيط آخر - لا يؤدي إجراء البدء إلى إعادة الواجهة إلى حالتها الأصلية في حالة حدوث خطأ (هذا ناقص ، ولكن يمكن إصلاحه بسهولة). في الوقت نفسه ، يتم تسجيل الأخطاء بمزيد من التفصيل ، وهو زائد.
  • عند استخدام هذه الفئة ، يجب أن يعترض الكود الرئيسي مقاطعات SPI2_IRQn و DMA1_Stream4_IRQn والتأكد من أن العملية المقابلة I2SInterrupt و processDmaTxInterrupt يتم استدعاؤها.

البرنامج الرئيسي


يتم كتابة البرنامج الرئيسي باستخدام المكتبة الموصوفة أعلاه بكل بساطة:


 int main (void) { HAL_Init(); IOPort defaultPortA(IOPort::PortName::A, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortB(IOPort::PortName::B, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortC(IOPort::PortName::C, GPIO_MODE_INPUT, GPIO_PULLDOWN); // System frequency 168MHz System::ClockDiv clkDiv; clkDiv.PLLM = 16; clkDiv.PLLN = 336; clkDiv.PLLP = 2; clkDiv.PLLQ = 7; clkDiv.AHBCLKDivider = RCC_SYSCLK_DIV1; clkDiv.APB1CLKDivider = RCC_HCLK_DIV8; clkDiv.APB2CLKDivider = RCC_HCLK_DIV8; clkDiv.PLLI2SN = 192; clkDiv.PLLI2SR = 2; do { System::setClock(clkDiv, FLASH_LATENCY_3, System::RtcType::RTC_EXT); } while (System::getMcuFreq() != 168000000L); MyApplication app; appPtr = &app; app.run(); } 

نقوم هنا بتهيئة مكتبة HAL ، وتهيئة جميع أطراف وحدة التحكم عن طريق الإدخال (GPIO_MODE_INPUT / PULLDOWN) بشكل افتراضي. نقوم بضبط تردد وحدة التحكم ، وبدء الساعة (بما في ذلك الساعة في الوقت الحقيقي من الكوارتز الخارجي). بعد ذلك ، في نمط Java قليلاً ، نقوم بإنشاء مثيل لتطبيقنا واستدعاء طريقة التشغيل الخاصة به ، والتي تنفذ كل منطق التطبيق.


في قسم منفصل ، يجب علينا تحديد جميع المقاطعات المستخدمة. نظرًا لأننا نكتب بلغة C ++ ، والمقاطعات هي أشياء من عالم C ، فإننا بحاجة إلى إخفاءها وفقًا لذلك:


 extern "C" { void SysTick_Handler (void) { HAL_IncTick(); if (appPtr != NULL) { appPtr->getRtc().onMilliSecondInterrupt(); } } void DMA2_Stream3_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaRxInterrupt(); } void DMA2_Stream6_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaTxInterrupt(); } void SDIO_IRQHandler (void) { Devices::SdCard::getInstance()->processSdIOInterrupt(); } void SPI2_IRQHandler(void) { appPtr->getI2S().processI2SInterrupt(); } void DMA1_Stream4_IRQHandler(void) { appPtr->getI2S().processDmaTxInterrupt(); } void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *channel) { appPtr->processDmaTxCpltCallback(channel); } ... } 

تعلن فئة MyApplication عن جميع الأجهزة المستخدمة ، ومنشئو المكالمات لجميع هذه الأجهزة ، كما تنفذ معالجات الأحداث اللازمة:


 class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler { public: static const size_t INPUT_PINS = 8; // Number of monitored input pins private: UsartLogger log; RealTimeClock rtc; IOPin ledGreen, ledBlue, ledRed; PeriodicalEvent heartbeatEvent; IOPin mco; // Interrupt priorities InterruptPriority irqPrioI2S; InterruptPriority irqPrioEsp; InterruptPriority irqPrioSd; InterruptPriority irqPrioRtc; // SD card IOPin pinSdPower, pinSdDetect; IOPort portSd1, portSd2; SdCard sdCard; bool sdCardInserted; // Configuration Config config; // ESP Esp11 esp; EspSender espSender; // Input pins std::array<IOPin, INPUT_PINS> pins; std::array<bool, INPUT_PINS> pinsState; // I2S2 Audio I2S i2s; AudioDac_UDA1334 audioDac; WavStreamer streamer; Devices::Button playButton; ... 

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


  MyApplication::MyApplication () : // logging log(Usart::USART_1, IOPort::B, GPIO_PIN_6, GPIO_PIN_7, 115200), // RTC rtc(), ledGreen(IOPort::C, GPIO_PIN_1, GPIO_MODE_OUTPUT_PP), ledBlue(IOPort::C, GPIO_PIN_2, GPIO_MODE_OUTPUT_PP), ledRed(IOPort::C, GPIO_PIN_3, GPIO_MODE_OUTPUT_PP), heartbeatEvent(rtc, 10, 2), mco(IOPort::A, GPIO_PIN_8, GPIO_MODE_AF_PP), // Interrupt priorities irqPrioI2S(6, 0), // I2S DMA interrupt priority: 7 will be also used irqPrioEsp(5, 0), irqPrioSd(3, 0), // SD DMA interrupt priority: 4 will be also used irqPrioRtc(2, 0), // SD card pinSdPower(IOPort::A, GPIO_PIN_15, GPIO_MODE_OUTPUT_PP, GPIO_PULLDOWN, GPIO_SPEED_HIGH, true, false), pinSdDetect(IOPort::B, GPIO_PIN_3, GPIO_MODE_INPUT, GPIO_PULLUP), portSd1(IOPort::C, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12, /* callInit = */false), portSd2(IOPort::D, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_2, /* callInit = */false), sdCard(pinSdDetect, portSd1, portSd2), sdCardInserted(false), // Configuration config(pinSdPower, sdCard, "conf.txt"), //ESP esp(rtc, Usart::USART_2, IOPort::A, GPIO_PIN_2, GPIO_PIN_3, irqPrioEsp, IOPort::A, GPIO_PIN_1), espSender(rtc, esp, ledRed), // Input pins pins { { IOPin(IOPort::A, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_6, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_7, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_0, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_1, GPIO_MODE_INPUT, GPIO_PULLUP) } }, // I2S2 Audio Configuration // PB10 --> I2S2_CK // PB12 --> I2S2_WS // PB15 --> I2S2_SD i2s(IOPort::B, GPIO_PIN_10 | GPIO_PIN_12 | GPIO_PIN_15, irqPrioI2S), audioDac(i2s, /* power = */ IOPort::B, GPIO_PIN_11, /* mute = */ IOPort::B, GPIO_PIN_13, /* smplFreq = */ IOPort::B, GPIO_PIN_14), streamer(sdCard, audioDac), playButton(IOPort::B, GPIO_PIN_2, GPIO_PULLUP, rtc) { mco.activateClockOutput(RCC_MCO1SOURCE_PLLCLK, RCC_MCODIV_5); } 

كمثال ، معالج الأحداث للنقر على زر يبدأ / يتوقف عن تشغيل ملف WAV:


  virtual void MyApplication::onButtonPressed (const Devices::Button * b, uint32_t numOccured) { if (b == &playButton) { USART_DEBUG("play button pressed: " << numOccured); if (streamer.isActive()) { USART_DEBUG(" Stopping WAV"); streamer.stop(); } else { USART_DEBUG(" Starting WAV"); streamer.start(AudioDac_UDA1334::SourceType:: STREAM, config.getWavFile()); } } } 

وأخيرًا ، تكمل طريقة التشغيل الرئيسية تكوين الأجهزة (على سبيل المثال ، تعيين MyApplication كمعالج للأحداث) ، وتبدأ حلقة لا نهائية ، حيث تشير بشكل دوري إلى تلك الأجهزة التي تتطلب اهتمامًا دوريًا:


 void MyApplication::run () { log.initInstance(); USART_DEBUG("Oscillator frequency: " << System::getExternalOscillatorFreq() << ", MCU frequency: " << System::getMcuFreq()); HAL_StatusTypeDef status = HAL_TIMEOUT; do { status = rtc.start(8 * 2047 + 7, RTC_WAKEUPCLOCK_RTCCLK_DIV2, irqPrioRtc, this); USART_DEBUG("RTC start status: " << status); } while (status != HAL_OK); sdCard.setIrqPrio(irqPrioSd); sdCard.initInstance(); if (sdCard.isCardInserted()) { updateSdCardState(); } USART_DEBUG("Input pins: " << pins.size()); pinsState.fill(true); USART_DEBUG("Pin state: " << fillMessage()); esp.assignSendLed(&ledGreen); streamer.stop(); streamer.setHandler(this); streamer.setVolume(1.0); playButton.setHandler(this); bool reportState = false; while (true) { updateSdCardState(); playButton.periodic(); streamer.periodic(); if (isInputPinsChanged()) { USART_DEBUG("Input pins change detected"); ledBlue.putBit(true); reportState = true; } espSender.periodic(); if (espSender.isOutputMessageSent()) { if (reportState) { espSender.sendMessage(config, "TCP", config.getServerIp(), config.getServerPort(), fillMessage()); reportState = false; } if (!reportState) { ledBlue.putBit(false); } } if (heartbeatEvent.isOccured()) { ledGreen.putBit(heartbeatEvent.occurance() == 1); } } } 

القليل من التجريب


— . — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.



. github . - , Mouser ( ). 37 . . , STM Olimex, .



. , :


  • ( ). , , . : 4 8 . PLL, .
  • , . 47 μF . , .
  • SWD . - , . .
  • . SMD , . 3 .


github GPL v3:



!

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


All Articles