مساء الخير عزيزي الخبروفيت! أريد أن أعرض مشروعي للجمهور - لوحة تصحيح صغيرة تستند إلى 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.


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

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

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

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


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

- قم بتشغيل الطاقة ، قم بتشغيل 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...
البرمجيات
هناك برنامج اختبار على جيثب . تقوم بما يلي:
- يعرض وحدة التحكم عند أقصى تردد (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} {
إذا لم يكن التعامل مع مثل هذه الأحداث يمثل أولوية ، فمن الواضح أن استخدام المقاطعات غير ضروري. لذلك ، يتم تنفيذ سيناريوهات الضغط المختلفة (على سبيل المثال ، ضغطة واحدة أو تعليق) في الإجراء الدوري ، والذي يجب استدعاؤه بشكل دوري من رمز البرنامج الرئيسي. يحلل بشكل دوري تغيير الحالة ويستدعي بشكل متزامن المعالج الظاهري onButtonPressed ، والذي يجب تنفيذه في البرنامج الرئيسي:
void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (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; } if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { __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;
يتم التحكم في بداية نقل البيانات من خلال طرق البدء ، وهي المسؤولة عن تكوين معلمات المنفذ ، وتسجيل وقت الواجهة ، وتكوين المقاطعات ، وبدء 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);
نقوم هنا بتهيئة مكتبة 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;
هذا ، في الواقع ، يتم الإعلان عن جميع الأجهزة المستخدمة بشكل ثابت ، مما قد يؤدي إلى زيادة في الذاكرة المستخدمة ، ولكنه يبسط إلى حد كبير الوصول إلى البيانات. في مُنشئ فئة MyApplication ، من الضروري الاتصال بمصممي جميع الأجهزة ، وبعد ذلك ، عند بدء إجراء التشغيل ، سيتم تهيئة جميع أجهزة التحكم الدقيقة المستخدمة:
MyApplication::MyApplication () :
كمثال ، معالج الأحداث للنقر على زر يبدأ / يتوقف عن تشغيل ملف 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:
!