نحوه راه اندازی گوشی های هوشمند و رایانه های شخصی. پرتال اطلاعاتی
  • خانه
  • اخبار
  • STM32F407(STM32F4-DISCOVERY) - رویکرد غیر استاندارد - کتابخانه استاندارد قسمت 1.

STM32F407(STM32F4-DISCOVERY) - رویکرد غیر استاندارد - کتابخانه استاندارد قسمت 1.

نرم افزار مورد نیاز برای توسعه در این مقاله به شما خواهم گفت که چگونه آن را به درستی پیکربندی و وصل کنید. همه محیط‌های تجاری مانند IAR EWARM یا Keil uVision معمولاً این یکپارچه‌سازی را خودشان انجام می‌دهند، اما در مورد ما همه چیز باید به صورت دستی پیکربندی شود و زمان زیادی را صرف آن کنید. مزیت این است که شما فرصتی دارید که بفهمید همه چیز از درون چگونه کار می کند، و در آینده، به طور انعطاف پذیر همه چیز را برای خود سفارشی کنید. قبل از شروع راه اندازی، بیایید به ساختار محیطی که در آن کار خواهیم کرد نگاه کنیم:

Eclipse برای ویرایش راحت فایل های پیاده سازی تابع ( ج), فایل های هدر (.h، و همچنین فایل های اسمبلر ( .S). منظور من از "راحت" استفاده از تکمیل کد، برجسته سازی نحو، تغییر شکل، پیمایش در توابع و نمونه های اولیه آنهاست. فایل ها به طور خودکار به کامپایلرهای لازم داده می شوند که کد شی (در فایل ها) را تولید می کنند .o). تا کنون این کد شامل نمی شود آدرس های مطلقمتغیرها و توابع و بنابراین برای اجرا مناسب نیستند. فایل های شی به دست آمده توسط یک پیوند دهنده با هم مونتاژ می شوند. برای دانستن اینکه کدام بخش از فضای آدرس باید استفاده شود، گردآورنده از یک فایل خاص استفاده می کند ( .ld) که به آن اسکریپت پیوند دهنده می گویند. معمولاً شامل تعریفی از آدرس‌های بخش و اندازه آن‌ها است (بخش کد نگاشت شده به فلش، بخش متغیر نگاشت شده به RAM و غیره).

در نهایت، پیوند دهنده یک فایل .elf (قالب اجرایی و قابل پیوند) تولید می‌کند که علاوه بر دستورالعمل‌ها و داده‌ها، اطلاعات اشکال‌زدایی مورد استفاده توسط دیباگر را نیز شامل می‌شود. این فرمت برای فلش کردن سیستم عامل معمولی با برنامه vsprog مناسب نیست، زیرا این به یک فایل تصویری حافظه اولیه تری نیاز دارد (به عنوان مثال، Intel HEX - .hex). برای تولید آن، ابزاری از مجموعه Sourcery CodeBench (arm-none-eabi-objcopy) نیز وجود دارد و با استفاده از پلاگین ARM نصب شده، کاملاً در eclipse ادغام می شود.

برای انجام خود اشکال زدایی، از سه برنامه استفاده می شود:

  1. خود eclipse، که به برنامه نویس اجازه می دهد تا به صورت "بصری" از اشکال زدایی استفاده کند، از میان خطوط عبور کند، ماوس را روی متغیرها نگه دارد تا مقادیر آنها را مشاهده کند، و سایر امکانات
  2. arm-none-eabi-gdb - سرویس گیرنده GDB یک اشکال زدایی است که در پاسخ به اقدامات مشخص شده در مرحله 1 توسط eclips (از طریق stdin) به طور مخفی کنترل می شود. به نوبه خود، GDB به سرور OpenOCD Debug متصل می شود و تمام دستورات ورودی توسط دیباگر GDB به دستورات قابل فهم برای OpenOCD ترجمه می شوند. کانال GDB<->OpenOCD با استفاده از پروتکل TCP پیاده سازی می شود.
  3. OpenOCD یک سرور دیباگ است که می تواند مستقیماً با برنامه نویس ارتباط برقرار کند. جلوی کلاینت اجرا می شود و منتظر اتصال TCP می ماند.

این طرح ممکن است برای شما کاملاً بی فایده به نظر برسد: چرا از کلاینت و سرور به طور جداگانه استفاده کنید و ترجمه دستورات غیر ضروری را انجام دهید، اگر همه اینها را می توان با یک اشکال زدا انجام داد؟ واقعیت این است که چنین معماری از نظر تئوری امکان تبادل راحت مشتری و سرور را فراهم می کند. به عنوان مثال، اگر به جای versaloon نیاز به استفاده از برنامه نویس دیگری دارید که از OpenOCD پشتیبانی نمی کند، اما از سرور Debug ویژه دیگری پشتیبانی می کند (به عنوان مثال، texane/stlink برای برنامه نویس stlink - که در تابلوی اشکال زدایی STM32VLDiscovery قرار دارد)، سپس شما به سادگی OpenOCD را به جای راه اندازی سرور مورد نظر اجرا می کنید و همه چیز باید بدون هیچ مرحله اضافی کار کند. در عین حال، وضعیت برعکس ممکن است: فرض کنید می‌خواهید از محیط IAR EWARM همراه با versaloon به جای ترکیب Eclipse + CodeBench استفاده کنید. IAR دارای سرویس گیرنده Debug داخلی خود است که با موفقیت با OpenOCD تماس گرفته و آن را مدیریت می کند و همچنین داده های لازم را در پاسخ دریافت می کند. با این حال، همه اینها گاهی اوقات فقط در تئوری باقی می ماند، زیرا استانداردهای ارتباط بین مشتری و سرور به طور دقیق تنظیم نشده است و ممکن است در برخی جاها متفاوت باشد، اما تنظیماتی که با st-link+eclipse و IAR+versaloon مشخص کردم برای من کار کرد.

معمولاً کلاینت و سرور روی یک دستگاه اجرا می شوند و اتصال به سرور در آن صورت می گیرد لوکال هاست: 3333(برای openocd)، یا لوکال هاست: 4242(برای texane/stlink st-util). اما هیچ کس مانع باز کردن پورت 3333 یا 4242 شما نمی شود (و این پورت را روی روتر به شبکه خارجی فوروارد می کنید) و همکاران شما از شهر دیگری می توانند سخت افزار شما را متصل کرده و اشکال زدایی کنند. این ترفند اغلب توسط جاسازی‌کنندگانی که در سایت‌های راه دور کار می‌کنند که دسترسی محدود است، استفاده می‌شود.

بیا شروع کنیم

eclipse را راه اندازی کنید و File->New->C Project را انتخاب کنید، نوع پروژه ARM Linux GCC (Sorcery G++ Lite) و نام "stm32_ld_vl" را انتخاب کنید (اگر STV32VLDiscovery دارید، پس منطقی تر است که نام آن را "stm32_md_vl" بگذارید). :

روی Finish کلیک کنید و پنجره خوش آمدگویی را کوچک یا بسته کنید. بنابراین، پروژه ایجاد شده است و پوشه stm32_ld_vl باید در فضای کاری شما ظاهر شود. اکنون باید با کتابخانه های لازم پر شود.

همانطور که از نام پروژه متوجه شدید، من یک پروژه برای نمای خط کش ایجاد خواهم کرد خط مقدار کم چگالی(LD_VL). برای ایجاد پروژه برای سایر میکروکنترلرها باید تمامی فایل ها و تعریف هایی را که به نام آنها وجود دارد جایگزین کنید _LD_VL (یا_ld_vl) به موارد مورد نیاز مطابق با جدول:

نوع خط کش تعیین میکروکنترلرها (x ممکن است تغییر کند)
خط مقدار کم چگالی _LD_VL STM32F100x4 STM32F100x6
کم چگالی _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
خط مقدار با چگالی متوسط _MD_VL STM32F100x8 STM32F100xB
با چگالی متوسط
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
خط ارزش با چگالی بالا _HD_VL STM32F100xC STM32F100xD STM32F100xE
تراکم بالا _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
چگالی XL _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
خط اتصال _CL STM32F105xx و STM32F107xx

برای درک منطق پشت جدول، باید با برچسب STM32 آشنا باشید. یعنی اگر VLDiscovery دارید، پس باید همه چیز مرتبط با _LD_VL را با _MD_VL جایگزین کنید، زیرا تراشه STM32F100RB، که متعلق به خط مقدار با چگالی متوسط ​​است، در اکتشاف لحیم شده است.

افزودن کتابخانه استاندارد لوازم جانبی CMSIS و STM32F10x به پروژه

CMSIS(Cortex Microcontroller Software Interface Standard) یک کتابخانه استاندارد شده برای کار با میکروکنترلرهای Cortex است که سطح HAL (Hardware Abstraction Layer) را پیاده سازی می کند، یعنی به شما امکان می دهد از جزئیات کار با ثبات ها، جستجوی آدرس های ثبت با استفاده از دیتاشیت ها انتزاعی بگیرید. و غیره. کتابخانه مجموعه ای از کدهای منبع در C و Asm است. بخش اصلی کتابخانه برای همه Cortex (خواه ST، NXP، ATMEL، TI یا هر شخص دیگری) یکسان است و توسط ARM توسعه داده شده است. بخش دیگر کتابخانه مسئولیت تجهیزات جانبی را بر عهده دارد که طبیعتاً از سازنده ای به سازنده دیگر متفاوت است. بنابراین، در نهایت، کتابخانه کامل همچنان توسط سازنده توزیع می شود، اگرچه بخش هسته همچنان می تواند به طور جداگانه از وب سایت ARM دانلود شود. این کتابخانه شامل تعاریف آدرس، کد مقداردهی اولیه تولید کننده ساعت (به راحتی قابل تنظیم با تعریف) و هر چیز دیگری است که برنامه نویس را از معرفی دستی آدرس های انواع ثبات های جانبی و تعیین بیت های مقادیر در پروژه های خود نجات می دهد. این ثبت ها

اما بچه های ST فراتر رفتند. علاوه بر پشتیبانی CMSIS، آنها کتابخانه دیگری را برای STM32F10x به نام ارائه می کنند کتابخانه استاندارد لوازم جانبی(SPL) که علاوه بر CMSIS قابل استفاده است. این کتابخانه دسترسی سریع‌تر و راحت‌تری به لوازم جانبی فراهم می‌کند و همچنین (در برخی موارد) عملکرد صحیح دستگاه‌های جانبی را کنترل می‌کند. از همین رو داده های کتابخانه ایاغلب مجموعه ای از درایورها برای ماژول های جانبی نامیده می شود. همراه با مجموعه‌ای از نمونه‌ها است که به دسته‌هایی برای تجهیزات جانبی مختلف تقسیم می‌شود. این کتابخانه نه تنها برای STM32F10x، بلکه برای سری های دیگر نیز در دسترس است.

می‌توانید کل SPL+CMSIS نسخه 3.5 را از اینجا دانلود کنید: STM32F10x_StdPeriph_Lib_V3.5.0 یا در وب‌سایت ST. آرشیو را از حالت فشرده خارج کنید. پوشه های CMSIS و SPL را در پوشه پروژه ایجاد کنید و شروع به کپی کردن فایل ها در پروژه خود کنید:

چه چیزی را کپی کنیم

کجا کپی کنیم (با توجه به
که پوشه پروژه stm32_ld_vl است)

توضیحات فایل
کتابخانه ها/CMSIS/CM3/
CoreSupport/ core_cm3.c
stm32_ld_vl/CMSIS/ core_cm3.c توضیحات هسته Cortex M3
کتابخانه ها/CMSIS/CM3/
CoreSupport/ هسته_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h سرصفحه های توضیحات هسته

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c توابع مقداردهی اولیه و
کنترل ساعت
کتابخانه ها/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h سرصفحه های این توابع
کتابخانه ها/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h توضیحات اولیه در مورد لوازم جانبی
کتابخانه ها/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! پسوند فایل CAPITAL S)
فایل وکتور جدول
وقفه ها و init-s در asm
Project/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h قالب برای سفارشی سازی
ماژول های جانبی

inc/ *
stm32_ld_vl/SPL/inc/ * فایل های هدر SPL
Libraries/STM32F10x_StdPeriph_Driver/
src/ *
stm32_ld_vl/SPL/src/ * اجرای SPL

پس از کپی کردن، به Eclipse بروید و در منوی زمینه پروژه Refresh را انجام دهید. در نتیجه، در Project Explorer باید همان ساختاری را که در تصویر سمت راست مشاهده می کنید، دریافت کنید.

شاید متوجه شده باشید که در پوشه Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ پوشه هایی برای IDE های مختلف وجود دارد (IDE های مختلف از کامپایلرهای متفاوتی استفاده می کنند). من Ride7 IDE را انتخاب کردم زیرا از ابزار GNU برای کامپایلر ARM Embedded استفاده می کند که با Sourcery CodeBench ما سازگار است.

کل کتابخانه با استفاده از یک پیش پردازنده (با استفاده از تعریف) پیکربندی شده است، این به شما امکان می دهد تمام شاخه های لازم را در مرحله کامپایل (یا بهتر است بگوییم، حتی قبل از آن) حل کنید و از بار در عملکرد خود کنترل کننده جلوگیری کنید (که می تواند مشاهده می شود اگر پیکربندی در RunTime انجام شده باشد). به عنوان مثال، همه تجهیزات برای خطوط مختلف متفاوت است، و بنابراین برای اینکه کتابخانه بداند از کدام خط می خواهید استفاده کنید، از شما خواسته می شود که در فایل از نظر حذف کنید. stm32f10x.hیکی از تعریف ها (مرتبط با خط شما):

/* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */

و غیره...

اما من انجام این کار را توصیه نمی کنم. ما فعلاً به فایل های کتابخانه دست نخواهیم داد و بعداً با استفاده از تنظیمات کامپایلر در Eclipse آن را تعریف خواهیم کرد. و سپس Eсlipse کامپایلر را با کلید فراخوانی می کند -D STM32F10X_LD_VL، که برای پیش پردازنده کاملاً معادل وضعیتی است که اگر کامنت نگذارید "#define STM32F10X_LD_VL". بنابراین، ما کد را تغییر نمی دهیم؛ در نتیجه، اگر بخواهید، روزی می توانید کتابخانه را به یک فهرست جداگانه منتقل کنید و آن را در پوشه هر پروژه جدید کپی نکنید.

اسکریپت پیوند دهنده

در منوی زمینه پروژه، New->File->Other->General->File، Next را انتخاب کنید. پوشه اصلی پروژه (stm32_ld_vl) را انتخاب کنید. نام فایل "stm32f100c4.ld" (یا "stm32f100rb.ld" را برای کشف) وارد کنید. حالا در eclipse کپی و پیست کنید:

ENTRY(Reset_Handler) MEMORY ( FLASH (rx) : ORIGIN = 0x08000000، LENGTH = 16K RAM (xrw): ORIGIN = 0x20000000، LENGTH = 4K ) _estack =MTH ORIGIN(MTH) MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECTIONS (/* جدول برداری وقفه */ .isr_vector: ( . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); ) >FLASH /* کد برنامه و سایر داده ها به FLASH می رود * / متن و Thumb->ARM کد چسب */ *(.glue_7) *(.glue_7t) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; ) >FLASH . ARM.extab: ( *(ARM.extab* .gnu.linkonce.armextab.*) ) >FLASH .ARM: ( __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; ) >FLASH .ARM. ویژگی ها: ( *(ARM.attributes) ) > FLASH .preinit_array: ( PROVIDE_HIDDEN (__preinit_array_start = .)؛ KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .:ID_EN PROVIDE_RAY_END = .:ID_EN PROVIA ray_start = .)؛ KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .)؛ ) >FLASH .fini_array: ( PROVIDE_HIDDEN (__fini_array_start =*). (.fini_array*)) KEEP (*(SORT(.fini_array.*))) PROVIDE_HIDDEN (__fini_array_end = .); ) >FLASH_sidata = .; /* داده های اولیه */ .data: AT (_sidata) (. = ALIGN(4)؛ _sdata = .؛ /* یک نماد سراسری در شروع داده ایجاد کنید */ *(.data) *(.data*) . = ALIGN (4)؛ _edata = .؛ /* یک نماد سراسری در انتهای داده تعریف کنید */ ) >RAM /* داده های اولیه */. = ALIGN(4); .bss: ( /* این توسط راه اندازی به منظور مقداردهی اولیه بخش .bss استفاده می شود */ _sbss = .; /* یک نماد جهانی در شروع bss تعریف می کند */ __bss_start__ = _sbss؛ *(bss.) *(.bss. *) *(COMMON) .= ALIGN(4)؛ _ebss = .؛ /* یک نماد سراسری در انتهای bss تعریف کنید */ __bss_end__ = _ebss; ) >RAM PROVIDE(end = _ebss); PROVIDE(_end = _ebss); PROVIDE(__HEAP_START = _ebss); /* بخش User_heap_stack، برای بررسی اینکه رم کافی باقی مانده است */._user_heap_stack: ( . = ALIGN(4); . = . + MIN_HEAP_SIZE؛ . = . + MIN_STACK_SIZE؛ . = ALIGN(4); ) >RAM / DISCARD/ : ( libc.a(*) libm.a(*) libgcc.a(*) )

این ل اسکریپت جوهر به طور خاص برای کنترلر STM32F100C4 (که دارای 16 کیلوبایت فلش و 4 کیلوبایت رم است) در نظر گرفته شده است، اگر اسکریپت دیگری دارید، باید پارامترهای LENGTH مناطق FLASH و RAM را در ابتدای صفحه تغییر دهید. فایل (برای STM32F100RB که در Discovery: Flash 128K و RAM 8K).

فایل را ذخیره کنید.

تنظیم ساخت (C/C++ Build)

به Project->Properties->C/C++ Build->Settings->Tool Settings بروید و شروع به تنظیم ابزارهای ساخت کنید:

1) پیشرو هدف

ما انتخاب می کنیم که کامپایلر برای کدام هسته Cortex کار کند.

  • پردازنده: cortex-m3

2) ARM Sourcery Linux GCC C Compiler -> Preprocessor

با عبور دادن آنها از سوییچ -D به کامپایلر، دو تعریف اضافه می کنیم.

  • STM32F10X_LD_VL - خط کش را تعریف می کند (در مورد این تعریف در بالا نوشتم)
  • USE_STDPERIPH_DRIVER - به کتابخانه CMSIS می گوید که باید از درایور SPL استفاده کند.

3) ARM Sourcery Linux GCC C Compiler -> Directories

اضافه کردن مسیرها به کتابخانه شامل.

  • "$(workspace_loc:/$(ProjName)/CMSIS)"
  • "$(workspace_loc:/$(ProjName)/SPL/inc)"

حالا مثلاً اگر بنویسیم:

#include "stm32f10x.h

سپس کامپایلر باید ابتدا فایل را جستجو کند stm32f10x.hدر دایرکتوری پروژه (او همیشه این کار را انجام می دهد)، آن را در آنجا پیدا نمی کند و شروع به جستجو در پوشه CMSIS، مسیری که به آن اشاره کردیم، می کند و آن را پیدا می کند.

4) ARM Sourcery Linux GCC C Compiler -> Optimization

بیایید بهینه سازی عملکرد و داده را فعال کنیم

  • -function-sections
  • بخش‌های fdata

در نتیجه، تمام توابع و عناصر داده در بخش‌های جداگانه قرار می‌گیرند و جمع‌کننده می‌تواند بفهمد کدام بخش‌ها استفاده نمی‌شوند و به سادگی آنها را دور می‌اندازند.

5) ARM Sourcery Linux GCC C Compiler -> General

مسیر را به اسکریپت پیوند دهنده ما اضافه کنید: "$(workspace_loc:/$(ProjName)/stm32f100c4.ld)" (یا هر چیزی که شما آن را می‌نامید).

و گزینه ها را تنظیم کنید:

  • از فایل های شروع استاندارد استفاده نکنید - استفاده نکنید فایل های استانداردراه اندازی.
  • حذف بخش های استفاده نشده - حذف بخش های استفاده نشده

همین، راه اندازی کامل شد. خوب.

از زمان ایجاد پروژه، کارهای زیادی انجام داده‌ایم، و مواردی وجود دارد که Eclipse ممکن است از قلم افتاده باشد، بنابراین باید به آن بگوییم که ساختار فایل پروژه را تجدید نظر کند. برای انجام این کار، از منوی زمینه پروژه باید انجام دهید Index -> Rebuild.

سلام LED در STM32

زمان ایجاد فایل اصلی پروژه فرا رسیده است: File -> New -> C/C++ -> Source File. بعد. نام فایل فایل منبع: main.c.

موارد زیر را کپی و در فایل پیست کنید:

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPBEN؛ // فعال کردن ساعت پریف PORTB RCC->APB1ENR |= RCC_APB1ENR_TIM2EN؛ // فعال کردن ساعت پریف TIM2 // غیرفعال کردن JTAG برای انتشار LED PIN RCCAFREN->APBCCAFREN->APBCCAFREN2 | AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGغیرفعال؛ // پاک کردن بیت های ثبت کنترلی PB4 و PB5 GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 به عنوان P.4.B. و پیکربندی GPIONF فشار کششخروجی در حداکثر 10 مگاهرتز GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0؛ TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 تیک/ثانیه TIM2->ARR = 1000; // 1 وقفه/1 ثانیه TIM2->DIER |= TIM_DIER_UIE; // فعال کردن وقفه tim2 TIM2->CR1 |= TIM_CR1_CEN; // تعداد شروع NVIC_EnableIRQ(TIM2_IRQn); // فعال کردن IRQ while(1); // حلقه بی نهایت ) void TIM2_IRQHandler(void) (TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag if (1 == (i++ & 0x1)) (GPIOB->BSRR = GPIO_BSRR_BS4; // تنظیم PB4 GPIOB4 ->BSRR = GPIO_BSRR_BR5؛ // بازنشانی بیت PB5 ) دیگری (GPIOB->BSRR = GPIO_BSRR_BS5؛ // تنظیم PB5 بیت GPIOB->BSRR = GPIO_BSRR_BR4؛ // بازنشانی بیت PB4 )

اگرچه ما کتابخانه SPL را گنجانده ایم، اما در اینجا استفاده نشده است. همه تماس‌ها با فیلدهایی مانند RCC->APB2ENR به طور کامل در CMSIS توضیح داده شده‌اند.

می توانید Project -> Build All را انجام دهید. اگر همه چیز درست شد، فایل stm32_ld_vl.hex باید در پوشه Debug پروژه ظاهر شود. توسط ابزارهای داخلی به طور خودکار از elf تولید شد. ما فایل را فلش می کنیم و می بینیم که چگونه LED ها با فرکانس یک بار در ثانیه چشمک می زنند:

Vsprog -sstm32f1 -ms -oe -owf -I /home/user/workspace/stm32_ld_vl/Debug/stm32_ld_vl.hex -V "tvcc.set 3300"

طبیعتاً به جای /home/user/workspace/ باید مسیر خود را به فضای کاری وارد کنید.

برای STM32VLDdiscovery

کد کمی متفاوت از کدی است که در بالا برای برد اشکال زدایی ارائه کردم. تفاوت در پین هایی است که LED ها روی آنها "آویزان" هستند. اگر روی برد من PB4 و PB5 بود، در دیسکاوری PC8 و PC9 بود.

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPCEN؛ // فعال کردن ساعت محیطی PORTC RCC->APB1ENR |= RCC_APB1ENR_TIM2EN؛ // فعال کردن ساعت پریف TIM2 // پاک کردن رجیسترهای PC8 و PC9 & ~ CRH (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9)؛ // PC8 و PC9 را به‌عنوان خروجی Push Pull در حداکثر 10 مگاهرتز پیکربندی کنید GPIOC->CRH |= GPIO_CRH_MODE8_CRH |= GPIO_CRH_MODE_CRH |= GPIO_CRH_MODE8_0; 1000 - 1؛ // 1000 تیک در ثانیه TIM2->ARR = 1000؛ // 1 وقفه/ثانیه (1000/100) TIM2->DIER |= TIM_DIER_UIE؛ // فعال کردن وقفه tim2 TIM2->CR1 |= TIM_CR1_CEN؛ // شروع شمارش NVIC_EnableIRQ(TIM2); //_ فعال کردن IRQ while(1)؛ // حلقه بی نهایت ) void TIM2_IRQHandler(void) (TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag if (1 == (i++ & 0x1)) (GPIOC->BSRR = GPIO_BSRR_BS ؛ // تنظیم PC8 بیت GPIOC->BSRR = GPIO_BSRR_BR9؛ // بازنشانی بیت PC9 ) دیگری (GPIOC->BSRR = GPIO_BSRR_BS9؛ // تنظیم PC9 بیت GPIOC->BSRR = GPIO_BSRR_BR8؛ // بازنشانی بیت PC8))

در ویندوز، می‌توانید هگز (/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) حاصل را با استفاده از ابزار ST فلش کنید.

خوب، زیر ابزار لینوکس st-flash. ولی!!! این ابزار از فرمت هگز اینتل (که به طور پیش فرض تولید می شود) استفاده نمی کند، بنابراین انتخاب فرمت باینری در تنظیمات ایجاد تصویر Flash بسیار مهم است:

پسوند فایل تغییر نمی کند (هگزا به همان شکلی که بود باقی می ماند)، اما فرمت فایل تغییر می کند. و تنها پس از آن می توانید انجام دهید:

St-flash write v1 /home/user/workspace/stm32_md_vl/Debug/stm32_md_vl.hex 0x08000000

ضمناً در مورد پسوند و فرمت: معمولاً فایل‌های باینری با پسوند bin مشخص می‌شوند، در حالی که فایل‌های با فرمت Intel HEX پسوند .hex نامیده می‌شوند. تفاوت در این دو فرمت بیشتر فنی است تا کاربردی: فرمت باینری به سادگی حاوی بایت‌هایی از دستورالعمل‌ها و داده‌ها است که به سادگی توسط برنامه‌نویس «همانطور که هست» برای کنترل‌کننده نوشته می‌شود. از طرف دیگر، IntelHEX دارای فرمت باینری نیست، بلکه یک قالب متنی است: دقیقاً همان بایت ها به 4 بیت تقسیم می شوند و کاراکتر به کاراکتر در فرمت ASCII ارائه می شوند و فقط از کاراکترهای 0-9، A-F (bin و hex) استفاده می شود. سیستم‌های عددی با پایه‌های متعدد هستند، یعنی 4 بیت در هر bin را می‌توان به صورت یک رقم هگزا نشان داد. بنابراین فرمت ihex بیش از 2 برابر اندازه معمولی است فایل باینری(هر 4 بیت با یک بایت + شکست خط برای خواندن آسان جایگزین می شود)، اما می توان آن را در یک ویرایشگر متن معمولی خواند. بنابراین، اگر قصد دارید این فایل را برای شخصی ارسال کنید یا از آن در برنامه های برنامه نویسی دیگر استفاده کنید، بهتر است نام آن را به stm32_md_vl.bin تغییر دهید تا کسانی که به نام آن نگاه می کنند گمراه نشوند.

بنابراین ما ساخت سیستم عامل را برای stm32 تنظیم کردیم. دفعه بعد بهت میگم چطوری

هنگام ایجاد اولین برنامه خود بر روی میکروکنترلر STM32، می توانید از راه های مختلفی استفاده کنید. اولین مورد، کلاسیک، توضیحات دقیق کنترلر را در وب سایت www.st.com، که تحت نام "راهنمای مرجع" ظاهر می شود، می گیریم و شرح رجیسترهای جانبی را می خوانیم. سپس سعی می کنیم آن ها را ضبط کنیم و ببینیم که دستگاه های جانبی چگونه کار می کنند. خواندن این سند بسیار مفید است، اما در اولین مرحله از تسلط بر میکروکنترلر، می توانید از این امر خودداری کنید، هر چند ممکن است عجیب به نظر برسد. مهندسان STMicroelectronics یک کتابخانه درایور برای تجهیزات جانبی استاندارد نوشته اند. علاوه بر این، آنها نمونه های زیادی از استفاده از این درایورها نوشته اند، که می تواند برنامه نویسی برنامه شما را به فشار دادن کلیدهای Ctrl+C و Ctrl+V کاهش دهد، و به دنبال آن نمونه استفاده از درایور را مطابق با نیاز شما ویرایش جزئی انجام دهد. بنابراین، اتصال یک کتابخانه درایور جانبی به پروژه شما دومین روش ساخت یک برنامه کاربردی است. علاوه بر سرعت نوشتن، مزایای دیگری نیز برای این روش وجود دارد: جهانی بودن کد و استفاده از کتابخانه های اختصاصی دیگر مانند USB، Ethernet، کنترل درایو و غیره که در کد منبع ارائه شده و استفاده می شود. یک درایور استاندارد جانبی ایرادات این روشهمچنین وجود دارد: جایی که می توانید با یک خط کد به نتیجه برسید، درایور استاندارد STM32 محیطی 10 را می نویسد. خود کتابخانه جانبی نیز به صورت فایل های منبع ارائه شده است، بنابراین می توانید ردیابی کنید که کدام بیت از ثبات توسط کدام یک تغییر کرده است. این یا آن تابع در صورت تمایل، می توانید از روش دوم نوشتن یک برنامه به روش اول با نظر دادن بخشی از کدی که از کتابخانه استاندارد استفاده می کند، تغییر دهید، که مستقیماً رجیستر جانبی را کنترل می کند. در نتیجه این عمل، سرعت کنترل، مقدار رم و رام را به دست خواهید آورد، اما تطبیق پذیری کد را از دست خواهید داد. در هر صورت، مهندسان Promelektronika توصیه می کنند حداقل در مرحله اول از کتابخانه تجهیزات جانبی استاندارد استفاده کنید.

هنگام اتصال کتابخانه به پروژه خود، بزرگترین مشکلات در انتظار توسعه دهنده است. اگر نمی دانید چگونه این کار را انجام دهید، می توانید زمان زیادی را صرف این فعالیت کنید که با ایده استفاده از درایور آماده در تضاد است. مطالب به اتصال کتابخانه استاندارد به هر خانواده STM32 اختصاص داده شده است.

هر خانواده STM32 کتابخانه مخصوص خود را از لوازم جانبی استاندارد دارد. این به دلیل این واقعیت است که خود حاشیه متفاوت است. به عنوان مثال، حاشیه کنترلرهای STM32L دارای عملکرد صرفه جویی در مصرف انرژی به عنوان یکی از وظایف آن است که مستلزم افزودن عملکردهای کنترلی است. یک مثال کلاسیک ADC است که در STM32L توانایی خاموش کردن سخت افزار را در صورت عدم وجود طولانی فرمان تبدیل دارد - یکی از عواقب کار صرفه جویی در انرژی. ADCهای کنترلرهای خانواده STM32F چنین عملکردی ندارند. در واقع به دلیل وجود تفاوت های سخت افزاری در دستگاه های جانبی، کتابخانه های درایور متفاوتی داریم. علاوه بر تفاوت آشکار در عملکرد کنترلر، بهبود در لوازم جانبی نیز وجود دارد. بنابراین، لوازم جانبی خانواده کنترلرها که بعداً منتشر شدند، ممکن است قابل تأمل تر و راحت تر باشند. به عنوان مثال، تجهیزات جانبی کنترلرهای STM32F1 و STM32F2 تفاوت هایی در کنترل دارند. به نظر نویسنده، مدیریت لوازم جانبی STM32F2 راحت تر است. و واضح است که چرا: خانواده STM32F2 بعداً منتشر شد و این به توسعه دهندگان اجازه داد تا برخی از تفاوت های ظریف را در نظر بگیرند. بر این اساس، برای این خانواده ها کتابخانه های کنترل محیطی فردی وجود دارد. ایده فوق ساده است: در صفحه میکروکنترلری که قرار است استفاده کنید، یک کتابخانه جانبی مناسب برای آن وجود دارد.

با وجود تفاوت در وسایل جانبی در خانواده ها، رانندگان 90 درصد از تفاوت ها را در خود پنهان می کنند. به عنوان مثال، عملکرد پیکربندی ADC ذکر شده در بالا برای همه خانواده ها یکسان به نظر می رسد:

void ADC_Init (ADC_Nom، ADC_Param),

که در آن ADC_Nom عدد ADC به شکل ADC1، ADC2، ADC3 و غیره است.

ADC_Param - نشانگر ساختار داده، نحوه پیکربندی ADC (از چه چیزی شروع شود، از چند کانال دیجیتالی شود، آیا آن را به صورت چرخه ای انجام دهیم و غیره)

10 درصد تفاوت بین خانواده ها، در در این مثال، که باید هنگام انتقال از یک خانواده STM32 به خانواده دیگر اصلاح شود، در ساختار ADC_Param پنهان هستند. بسته به خانواده، تعداد فیلدهای این ساختار ممکن است متفاوت باشد. قسمت کلی دارای همان نحو است. بنابراین، انتقال یک برنامه برای یک خانواده STM32، که بر اساس کتابخانه های استاندارد محیطی نوشته شده است، به دیگری بسیار ساده است. از نظر جهانی سازی راه حل ها در میکروکنترلرها، STMicroelectronics مقاومت ناپذیر است!

بنابراین، ما کتابخانه STM32 مورد استفاده خود را دانلود کردیم. بعدش چی؟ در مرحله بعد باید یک پروژه بسازیم و فایل های مورد نیاز را به آن متصل کنیم. بیایید به ایجاد یک پروژه با استفاده از محیط توسعه IAR Embedded Workbench به عنوان مثال نگاه کنیم. محیط توسعه را راه اندازی کنید و به تب "پروژه" بروید، مورد "ایجاد پروژه" را برای ایجاد یک پروژه انتخاب کنید:

در پروژه جدیدی که ظاهر می شود، با نگه داشتن مکان نما روی نام پروژه، کلیک راست و انتخاب گزینه "Options" از منوی کشویی، تنظیمات را وارد کنید:

مناطق حافظه RAM و ROM:

هنگامی که روی دکمه "ذخیره" کلیک می کنید، محیط پیشنهاد می کند که یک فایل توضیح کنترلر جدید در پوشه پروژه بنویسد. نویسنده توصیه می کند برای هر پروژه یک فایل *.icp مجزا ایجاد کنید و آن را در پوشه پروژه ذخیره کنید.

اگر می خواهید پروژه خود را در مدار اشکال زدایی کنید، که توصیه می شود، نوع اشکال زدایی مورد استفاده را وارد کنید:

در زبانه دیباگر انتخاب شده، رابط را برای اتصال دیباگر (در مورد ما ST-Link انتخاب شده است) به کنترلر نشان می دهیم:



از این مرحله به بعد، پروژه ما بدون کتابخانه آماده کامپایل و بارگذاری در کنترلر است. محیط های دیگری مانند Keil uVision4، Resonance Ride7 و غیره به همین مراحل نیاز دارند.

اگر خط را در فایل main.c بنویسید:

#include "stm32f10x.h" یا

#include "stm32f2xx.h" یا

#include "stm32f4xx.h" یا

#include "stm32l15x.h" یا

#include "stm32l10x.h" یا

#include "stm32f05x.h"

نشان دهنده مکان این فایل، یا با کپی کردن این فایل در پوشه پروژه، برخی از مناطق حافظه با ثبات های جانبی خانواده مربوطه مرتبط می شوند. خود فایل در پوشه استاندارد کتابخانه لوازم جانبی در بخش: \CMSIS\CM3\DeviceSupport\ST\STM32F10x (یا نامی مشابه برای خانواده‌های دیگر) قرار دارد. از این به بعد آدرس رجیستر جانبی را به صورت شماره با نام آن جایگزین می کنید. حتی اگر قصد استفاده از توابع استاندارد کتابخانه را ندارید، توصیه می شود چنین ارتباطی را ایجاد کنید.

اگر قصد دارید در پروژه خود از وقفه استفاده کنید، توصیه می شود فایل start را با پسوند *.s که در مسیر \CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\iar یا مشابه آن قرار دارد، اضافه کنید. خانواده های دیگر ذکر این نکته ضروری است که هر محیطی فایل مخصوص به خود را دارد. بر این اساس اگر از IAR EWB استفاده کنیم باید فایل را از پوشه IAR بگیریم. این به دلیل تفاوت های جزئی در نحو محیط ها است. بنابراین، برای اینکه پروژه بلافاصله شروع شود، مهندسان STMicroelectronics چندین نسخه از فایل‌های راه‌اندازی را برای چندین مورد از محبوب‌ترین محیط‌های توسعه نوشتند. اکثر خانواده های STM32 یک فایل دارند. خانواده STM32F1 چندین فایل ماشه دارد:

  • startup_stm32f10x_cl.s – برای میکروکنترلرهای STM32F105/107
  • startup_stm32f10x_xl.s - برای میکروکنترلرهای STM32F101/STM32F103 768kb و بیشتر
  • startup_stm32f10x_hd.s - برای میکروکنترلرهای STM32F101/STM32F103 c فلش مموری 256-512 کیلوبایت
  • startup_stm32f10x_md.s - برای میکروکنترلرهای STM32F101/STM32F102/STM32F103 با حافظه فلش 64-128 کیلوبایت
  • startup_stm32f10x_ld.s - برای میکروکنترلرهای STM32F101/STM32F102/STM32F103 با حافظه فلش کمتر از 64 کیلوبایت
  • startup_stm32f10x_hd_vl.s برای میکروکنترلرهای STM32F100 با حافظه فلش 256-512 کیلوبایت
  • startup_stm32f10x_md_vl.s برای میکروکنترلرهای STM32F100 با حافظه فلش 64-128 کیلوبایت
  • startup_stm32f10x_ld_vl.s برای میکروکنترلرهای STM32F100 با حافظه فلش 32 کیلوبایت یا کمتر

بنابراین، بسته به خانواده، زیرخانواده و محیط توسعه، فایل راه اندازی را به پروژه اضافه می کنیم:

این جایی است که میکروکنترلر با شروع برنامه به پایان می رسد. وقفه SystemInit() و سپس __iar_program_start را به ترتیب فراخوانی می کند. تابع دوم از قبل بازنشانی یا می نویسد مقادیر را تنظیم کنیدمتغیرهای سراسری، پس از آن به برنامه کاربر main(). تابع SystemInit() ساعت میکروکنترلر را پیکربندی می کند. او کسی است که به سوالات پاسخ می دهد:

  • آیا باید به کریستال خارجی (HSE) سوئیچ کنم؟
  • چگونه فرکانس را از HSI/HSE ضرب کنیم؟
  • آیا اتصال صف بار فرمان لازم است؟
  • تأخیر لازم هنگام بارگذاری یک فرمان چقدر است (به دلیل سرعت کم فلش کار میکنهحافظه)
  • چگونه کلاکینگ اتوبوس های جانبی را تقسیم کنیم؟
  • آیا کد باید در رم خارجی قرار داده شود؟

تابع SystemInit() می تواند به صورت دستی در پروژه شما نوشته شود. اگر این تابع را خالی طراحی کنید، کنترلر روی یک نوسان ساز داخلی RC با فرکانس حدود 8 مگاهرتز (بسته به نوع خانواده) کار می کند. گزینه 2 - فایل system_stm32f10x.c (یا یک نام مشابه بسته به نوع خانواده مورد استفاده) را که در کتابخانه در امتداد مسیر قرار دارد به پروژه متصل کنید: Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x. این فایل حاوی تابع SystemInit() می باشد. به فرکانس کریستال خارجی HSE_VALUE توجه کنید. این پارامتردر فایل هدر stm32f10x.h تنظیم شده است. مقدار استاندارد بسته به خانواده STM32 8 و 25 مگاهرتز است. وظیفه اصلی تابع SystemInit() این است که کلاکینگ را به کوارتز خارجی تغییر داده و به روشی خاص ضرب کند. فرکانس داده شده. چه اتفاقی می افتد اگر مقدار HSE_VALUE به عنوان 8 مگاهرتز مشخص شود، هسته باید روی 72 مگاهرتز کلاک شود، اما در واقع برد دارای کریستال 16 مگاهرتز است؟ در نتیجه چنین اقدامات نادرستی، هسته یک ساعت 144 مگاهرتز دریافت می کند که ممکن است خارج از عملکرد تضمین شده سیستم در STM32 باشد. آن ها هنگام اتصال فایل system_stm32f10x.c، باید مقدار HSE_VALUE را مشخص کنید. همه اینها به این معنی است که فایل های system_stm32f10x.c، system_stm32f10x.h و stm32f10x.h (یا نام های مشابه برای خانواده های دیگر) باید برای هر پروژه جداگانه باشد. و

مهندسان STMicroelectronics ابزار پیکربندی ساعت را ایجاد کرده اند که به شما امکان می دهد ساعت سیستم را به درستی پیکربندی کنید. این فایل اکسل، که پس از تعیین پارامترهای ورودی و خروجی سیستم، فایل system_stm32xxx.c (از نظر نام مشابه برای خانواده معینی از خانواده ها) تولید می کند. بیایید به عملکرد آن با استفاده از خانواده STM32F4 به عنوان مثال نگاه کنیم.

گزینه ها: نوسان ساز داخلی RC، نوسان ساز داخلی RC با ضرب فرکانس، یا کوارتز خارجی با ضرب فرکانس. پس از انتخاب منبع ساعت، پارامترهای پیکربندی سیستم مورد نظر را وارد می کنیم، مانند فرکانس ورودی (هنگام استفاده از کوارتز خارجی)، فرکانس ساعت هسته، تقسیم کننده فرکانس ساعت باس محیطی، عملیات بافر واکشی فرمان و غیره. با کلیک بر روی دکمه "Generate" یک پنجره دریافت می کنیم


شامل فایل system_stm32f4xx.c و آنالوگ های آن مستلزم اتصال یک فایل کتابخانه محیطی استاندارد دیگر است. برای کنترل کلاک، مجموعه کاملی از توابع وجود دارد که از فایل system_stm32xxxxxx.c فراخوانی می شوند. این توابع در فایل stm32f10x_rcc.c و هدر آن قرار دارند. بر این اساس، هنگام اتصال فایل system_stm32xxxxxx.c به پروژه، لازم است که stm32f10x_rcc.c را نیز درج کنید، در غیر این صورت پیوند دهنده محیط عدم وجود شرح توابع را با نام RCC_xxxxxxx گزارش می کند. فایل مشخص شده در کتابخانه جانبی در مسیر: Libraries\STM32F10x_StdPeriph_Driver\src قرار دارد و هدر آن \Libraries\STM32F10x_StdPeriph_Driver\inc است.

فایل‌های هدر درایورهای جانبی در فایل stm32f10x_conf.h گنجانده شده است که توسط stm32f10x.h ارجاع داده شده است. فایل stm32f10x_conf.h به سادگی مجموعه‌ای از فایل‌های هدر برای درایورهای تجهیزات جانبی کنترلر خاص است که باید در پروژه گنجانده شوند. در ابتدا، تمام سرصفحه های "#include" به عنوان نظر علامت گذاری می شوند. اتصال یک فایل هدر جانبی شامل حذف نام فایل مربوطه است. در مورد ما، این خط #include "stm32f10x_rcc.h" است. بدیهی است که فایل stm32f10x_conf.h برای هر پروژه جداگانه است، زیرا پروژه های مختلف از تجهیزات جانبی متفاوتی استفاده می کنند.

و یک چیز آخر شما باید چندین دستورالعمل برای پیش پردازشگر کامپایلر و مسیرهای فایل های هدر را مشخص کنید.



مسیرهای فایل‌های سرصفحه ممکن است متفاوت باشد، بسته به محل کتابخانه جانبی نسبت به پوشه پروژه، اما وجود "USE_STDPERIPH_DRIVER" هنگام اتصال درایورهای استاندارد محیطی کتابخانه الزامی است.

بنابراین، ما کتابخانه استاندارد را به پروژه متصل کرده ایم. علاوه بر این، ما یکی از درایورهای استاندارد محیطی را به پروژه متصل کردیم که ساعت سیستم را کنترل می کند.

ما یاد گرفتیم که ساختار کتابخانه از داخل چگونه به نظر می رسد، اکنون چند کلمه در مورد اینکه از بیرون چگونه به نظر می رسد.



بنابراین، اتصال فایل هدر stm32f10x.h در برنامه مستلزم اتصال سایر فایل‌های هدر و فایل‌های کد است. برخی از مواردی که در شکل نشان داده شده است در بالا توضیح داده شده است. چند کلمه در مورد بقیه. فایل های STM32F10x_PPP.x فایل های درایور جانبی هستند. نمونه ای از اتصال چنین فایلی در بالا نشان داده شده است؛ این RCC است - محیط کنترل ساعت سیستم. اگر بخواهیم درایورهای دستگاه های جانبی دیگر را به هم وصل کنیم، نام فایل های متصل با جایگزین کردن «PPP» با نام دستگاه جانبی به دست می آید، به عنوان مثال، ADC - STM32F10x_ADC.c، یا پورت های I/O STM32F10x_GPIO.c، یا DAC - STM32F10x_DAC.c. به طور کلی، به طور مستقیم مشخص است که هنگام اتصال یک دستگاه جانبی معین، کدام فایل باید متصل شود. فایل های "misc.c"، "misc.h" هستند روی هم رفتههمان STM32F10x_PPP.x، فقط آنها هسته را کنترل می کنند. به عنوان مثال، راه اندازی بردارهای وقفه، که در هسته تعبیه شده است، یا مدیریت تایمر SysTick، که بخشی از هسته است. فایل‌های xxxxxxx_it.c بردارهای وقفه غیرقابل ماسک کنترلر را توصیف می‌کنند. آنها را می توان با بردارهای وقفه محیطی تکمیل کرد. فایل core_m3.h هسته CortexM3 را توصیف می کند. این هسته استاندارد شده است و در میکروکنترلرهای تولیدکنندگان دیگر یافت می شود. برای جهانی سازی بین پلتفرمی، STMicroelectronics برای ایجاد یک کتابخانه هسته CortexM جداگانه کار کرد، پس از آن ARM آن را استاندارد کرد و آن را به سایر تولیدکنندگان میکروکنترلر توزیع کرد. بنابراین انتقال به STM32 از کنترل‌کننده‌های تولیدکنندگان دیگر با هسته CortexM کمی آسان‌تر خواهد بود.

بنابراین، ما می توانیم کتابخانه جانبی استاندارد را به هر خانواده STM32 متصل کنیم. کسی که یاد بگیرد چگونه این کار را انجام دهد یک جایزه دریافت می کند: برنامه نویسی بسیار ساده میکروکنترلرها. علاوه بر درایورهایی که در قالب فایل های منبع وجود دارند، این کتابخانه شامل نمونه های زیادی از استفاده از تجهیزات جانبی است. به عنوان مثال، بیایید ایجاد یک پروژه شامل خروجی های مقایسه تایمر را در نظر بگیریم. با رویکرد سنتی، شرح رجیسترهای این ابزار جانبی را به دقت مطالعه خواهیم کرد. اما اکنون می توانیم متن برنامه در حال اجرا را مطالعه کنیم. به پوشه نمونه های لوازم جانبی استاندارد می رویم که در مسیر ProjectSTM32F10x_StdPeriph_Examples قرار دارد. در اینجا پوشه هایی از نمونه هایی با نام تجهیزات جانبی استفاده شده وجود دارد. به پوشه "TIM" بروید. تایمرها در STM32 عملکردها و تنظیمات زیادی دارند، بنابراین نمی توان قابلیت های کنترلر را تنها با یک مثال نشان داد. بنابراین، در دایرکتوری مشخص شده نمونه های زیادی از استفاده از تایمر وجود دارد. ما علاقه مند به تولید سیگنال PWM توسط تایمر هستیم. به پوشه "7PWM_Output" بروید. در داخل توضیحات برنامه به زبان انگلیسی و مجموعه ای از فایل ها وجود دارد:

main.c stm32f10x_conf.h stm32f10x_it.h stm32f10x_it.c system_stm32f10x.c

اگر پروژه وقفه نداشته باشد، محتوا به طور کامل در فایل main.c قرار دارد. این فایل ها را در دایرکتوری پروژه کپی کنید. پس از کامپایل کردن پروژه، برنامه ای برای STM32 دریافت خواهیم کرد که تایمر و پورت های ورودی/خروجی را برای تولید 7 سیگنال PWM از تایمر 1 پیکربندی می کند. به عنوان مثال، تعداد سیگنال های PWM را کاهش دهید، چرخه وظیفه، جهت شمارش و غیره را تغییر دهید. توابع و پارامترهای آنها به خوبی در فایل stm32f10x_stdperiph_lib_um.chm توضیح داده شده است. نام توابع و پارامترهای آنها برای کسانی که کمی انگلیسی می دانند به راحتی با هدف آنها مرتبط است. برای وضوح بخشی از کد مثال در اینجا آمده است:

/* پیکربندی Time Base */ TIM_TimeBaseStructure.TIM_Prescaler = 0; // بدون پیش انتخاب پالس های شمارش (رجیستر 16 بیتی) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // جهت شمارش بالا است TIM_TimeBaseStructure.TIM_Period = TimerPeriod; // شمارش تا مقدار TimerPeriod (ثابت در برنامه) TIM_TimeBaseStructure.TIM_ClockDivision = 0; // هیچ تقسیم پیش شماری وجود ندارد TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // شمارنده سرریز برای تولید رویدادها (در برنامه استفاده نمی شود) TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // وارد کردن مقادیر TimeBaseStructure در رجیسترهای تایمر 1 (وارد کردن داده ها در این متغیر // در بالا است) /* پیکربندی کانال 1، 2،3 و 4 در حالت PWM */ // تنظیم خروجی های PWM TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; // حالت کار PWM2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // فعال کردن خروجی سیگنال های تایمر PWM TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // فعال کردن خروجی تایمر PWM تکمیلی TIM_OCInitStructure.TIM_Pulse = Channel1Pulse; // عرض پالس Channel1Pulse – ثابت در برنامه TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // تنظیم قطب خروجی TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; // تنظیم قطبیت خروجی مکمل TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; // تنظیم وضعیت ایمن خروجی PWM TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset; // تنظیم وضعیت ایمن خروجی PWM تکمیلی TIM_OC1Init(TIM1, &TIM_OCInitStructure); // وارد کردن مقادیر متغیر TIM_OCInitStructure در رجیسترهای PWM کانال 1 // تایمر 1 TIM_OCInitStructure.TIM_Pulse = Channel2Pulse; // پهنای پالس را در متغیر OCInitStructure تغییر دهید و آن را در TIM_OC2Init وارد کنید(TIM1, &TIM_OCInitStructure); // ثبت کانال PWM 2 timer1 TIM_OCInitStructure.TIM_Pulse = Channel3Pulse; // پهنای پالس را در متغیر OCInitStructure تغییر دهید و آن را در TIM_OC3Init وارد کنید(TIM1, &TIM_OCInitStructure); // ثبت کانال PWM 3 timer1 TIM_OCInitStructure.TIM_Pulse = Channel4Pulse; // پهنای پالس را در متغیر OCInitStructure تغییر دهید و آن را در TIM_OC4Init وارد کنید(TIM1, &TIM_OCInitStructure); // کانال PWM 4 timer1 را ثبت می کند /* شمارنده TIM1 فعال می کند */ TIM_Cmd(TIM1, ENABLE); // start timer1 /* TIM1 خروجی اصلی فعال کردن */ TIM_CtrlPWMOoutputs(TIM1, ENABLE); // فعال کردن عملکرد خروجی های مقایسه تایمر 1

در سمت راست، نویسنده برای هر خط از برنامه به زبان روسی نظر گذاشته است. اگر همین مثال را در توضیح توابع کتابخانه stm32f10x_stdperiph_lib_um.chm باز کنیم، خواهیم دید که تمام پارامترهای تابع استفاده شده پیوندی به توضیحات خود دارند که در آنجا نشان داده می شوند. مقادیر ممکن. خود توابع نیز پیوندی به توضیحات و کد منبع خود دارند. این بسیار مفید است زیرا ... با دانستن اینکه یک تابع چه کاری انجام می‌دهد، می‌توانیم نحوه انجام آن، کدام بیت‌های رجیسترهای جانبی و نحوه تأثیرگذاری آن را ردیابی کنیم. این اولاً منبع دیگری از اطلاعات برای تسلط بر کنترلر بر اساس استفاده عملی از کنترلر است. آن ها ابتدا مشکل فنی را حل می کنید و سپس خود راه حل را مطالعه می کنید. ثانیا این یک زمینه برای بهینه سازی برنامه برای کسانی است که از نظر سرعت و حجم کد از کتابخانه راضی نیستند.



خوب، تا اینجا همه چیز خوب پیش می رود، اما فقط لامپ ها و دکمه ها آماده هستند. اکنون زمان آن رسیده است که تجهیزات جانبی سنگین تری - USB، UART، I2C و SPI را در اختیار بگیرید. تصمیم گرفتم با USB شروع کنم - دیباگر ST-Link (حتی نمونه واقعی از Discovery) سرسختانه از اشکال زدایی برد من خودداری کرد، بنابراین اشکال زدایی روی چاپ ها از طریق USB تنها روش اشکال زدایی است که در دسترس من است. البته می توانید از طریق UART، اما این یک دسته سیم اضافی است.

دوباره رفتم راه طولانی- خالی های مربوطه را در STM32CubeMX ایجاد کرد، USB Middleware را از بسته STM32F1Cube به پروژه من اضافه کرد. فقط باید ساعت USB را فعال کنید، کنترل کننده های وقفه USB مربوطه را تعریف کنید و چیزهای کوچک را جلا دهید. بیشتر تنظیمات مهم ماژول USBمن آن را از STM32GENERIC کپی کردم، با این تفاوت که تخصیص حافظه را کمی تغییر دادم (آنها از malloc استفاده کردند و من از تخصیص استاتیک استفاده کردم).

در اینجا چند قطعه جالب است که من ربودم. به عنوان مثال، برای اینکه هاست (رایانه) بفهمد که چیزی به آن وصل شده است، دستگاه خط USB D+ (که به پایه A12 متصل است) را "تغییر" می کند. با مشاهده این موضوع، میزبان شروع به بازجویی از دستگاه در مورد اینکه چه کسی است، چه رابط هایی می تواند اداره کند، با چه سرعتی می خواهد ارتباط برقرار کند و غیره شروع می کند. من واقعاً نمی دانم که چرا این کار باید قبل از مقداردهی اولیه USB انجام شود، اما در stm32duino تقریباً به همین روش انجام می شود.

تکان خوردن USB

USBD_HandleTypeDef hUsbDeviceFS. void Reenumerate() (// راه‌اندازی پین PA12 GPIO_InitTypeDef pinInit؛ pinInit.Pin = GPIO_PIN_12؛ pinInit.Mode = GPIO_MODE_OUTPUT_PP؛ pinInit.Speed ​​= GPIO_SPEED_FREQ_LOWA; برای شمارش دستگاه های USB در اتوبوس HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); for(unsigned int i=0; i<512; i++) {}; // Restore pin mode pinInit.Mode = GPIO_MODE_INPUT; pinInit.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pinInit); for(unsigned int i=0; i<512; i++) {}; } void initUSB() { Reenumerate(); USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); }


نکته جالب دیگر پشتیبانی از بوت لودر stm32duino است. برای آپلود سیستم عامل، ابتدا باید کنترلر را در بوت لودر راه اندازی مجدد کنید. ساده ترین راه این است که دکمه تنظیم مجدد را فشار دهید. اما برای انجام راحت‌تر این کار، می‌توانید از تجربه آردوینو استفاده کنید. هنگامی که درختان جوان بودند، کنترل‌کننده‌های AVR هنوز پشتیبانی USB را روی برد نداشتند؛ یک آداپتور USB-UART روی برد وجود داشت. سیگنال DTR UART به تنظیم مجدد میکروکنترلر متصل است. هنگامی که میزبان سیگنال DTR را ارسال می کند، میکروکنترلر در بوت لودر راه اندازی مجدد می شود. مانند بتن مسلح عمل می کند!

در مورد استفاده از USB، ما فقط یک پورت COM را شبیه سازی می کنیم. بر این اساس، باید خودتان در بوت لودر راه اندازی مجدد کنید. بوت لودر stm32duino، علاوه بر سیگنال DTR، در هر صورت، انتظار یک ثابت جادویی ویژه را نیز دارد (1EAF - ارجاع به Leaf Labs)

استاتیک int8_t CDC_Control_FS (uint8_t cmd، uint8_t* pbuf، uint16_t طول) (... مورد CDC_SET_CONTROL_LINE_STATE: dtr_pin++؛ //DTR پین فعال است، شکسته می شود؛ ... ایستا int8_t CDC_Receive_FS* (uint16_3) بایت بسته جادویی "1EAF" است که MCU را در بوت لودر قرار می دهد. */ if(*Len >= 4) ( /** * بررسی کنید آیا ورودی حاوی رشته "1EAF" است. * اگر بله، بررسی کنید که آیا DTR تنظیم شده است، تا MCU را در حالت بوت لودر قرار دهید. */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&& (Buf == "F")) (HAL_NVIC_SystemReset(); ) dtr_pin = 0; ) ) ... )

بازگشت: MiniArduino

در کل USB کار کرد. اما این لایه فقط با بایت ها کار می کند نه رشته ها. به همین دلیل است که چاپ های دیباگ بسیار زشت به نظر می رسند.

CDC_Transmit_FS((uint8_t*)"Ping\n"، 5); // 5 یک strlen ("پینگ") + صفر بایت است
آن ها هیچ پشتیبانی از خروجی فرمت شده وجود ندارد - شما نمی توانید یک عدد را چاپ کنید یا یک رشته را از قطعات جمع کنید. گزینه های زیر ظاهر می شوند:

  • چاپگر کلاسیک را پیچ کنید. به نظر می رسد این گزینه خوب است، اما به +12 کیلوبایت سیستم عامل نیاز دارد (من به نوعی تصادفاً sprintf را نامیدم)
  • پیاده سازی printf خود را از مخفیگاه خود بیابید. من یک بار برای AVR نوشتم، به نظر می رسد این پیاده سازی کوچکتر بود.
  • کلاس Print را از آردوینو به پیاده سازی STM32GENERIC متصل کنید
من گزینه دوم را انتخاب کردم زیرا کد کتابخانه Adafruit GFX نیز به Print متکی است، بنابراین هنوز باید آن را ببندم. علاوه بر این، من قبلاً کد STM32GENERIC را در دست داشتم.

من یک دایرکتوری MiniArduino در پروژه خود با هدف قرار دادن حداقل کد مورد نیاز برای پیاده سازی قطعات رابط arduino مورد نیاز خود ایجاد کردم. من شروع به کپی کردن یک فایل در یک زمان کردم و به دنبال وابستگی های دیگری بودم. بنابراین من یک کپی از کلاس Print و چندین فایل صحافی را به دست آوردم.

اما این کافی نیست. همچنان لازم بود تا کلاس Print را به نحوی با توابع USB متصل کنیم (مثلاً CDC_Transmit_FS()). برای این کار باید در کلاس SerialUSB بکشیم. در امتداد کلاس Stream و بخشی از مقداردهی اولیه GPIO کشیده شد. مرحله بعدی اتصال UART بود (من یک GPS به آن وصل شده ام). بنابراین کلاس SerialUART را نیز وارد کردم، که با آن یک لایه دیگر از مقداردهی اولیه محیطی از STM32GENERIC کشید.

به طور کلی خودم را در شرایط زیر یافتم. من تقریباً تمام فایل‌ها را از STM32GENERIC در MiniArduino کپی کردم. من هم کپی خودم از کتابخانه های USB و FreeRTOS داشتم (باید کپی هایی از HAL و CMSIS هم داشتم، اما خیلی تنبل بودم). در عین حال یک ماه و نیم است که زمان را علامت گذاری می کنم - قطعات مختلف را وصل و قطع می کنم، اما در عین حال حتی یک خط کد جدید ننوشته ام.

روشن شد که ایده اصلی من این بود که کنترل همه چیز را در دست بگیرم بخش سیستمخیلی خوب نمی شود. به هر حال، بخشی از کد اولیه در STM32GENERIC زندگی می کند و به نظر می رسد در آنجا راحت تر است. البته، می‌توان تمام وابستگی‌ها را قطع کرد و کلاس‌های wrapper خود را برای وظایف خود نوشت، اما این کار من را برای یک ماه دیگر کند می‌کرد - این کد هنوز باید اشکال‌زدایی شود. البته، این برای شرایط اضطراری خودتان خوب است، اما باید به جلو بروید!

بنابراین، من تمام کتابخانه های تکراری و تقریباً کل لایه سیستم خود را بیرون انداختم و به STM32GENERIC بازگشتم. این پروژه کاملاً پویا در حال توسعه است - چندین تعهد در روز به طور مداوم. علاوه بر این، در این یک ماه و نیم بسیار مطالعه کردم، بیشتر کتابچه راهنمای مرجع STM32 را خواندم، به نحوه ساخت کتابخانه های HAL و بسته بندی های STM32GENERIC نگاه کردم و در درک توصیفگرهای USB و لوازم جانبی میکروکنترلر پیشرفت کردم. به طور کلی، من اکنون به STM32GENERIC بسیار بیشتر از قبل اعتماد داشتم.

معکوس: I2C

با این حال، ماجراهای من به همین جا ختم نشد. هنوز UART و I2C وجود داشت (نمایشگر من در آنجا زندگی می کند). با UART همه چیز بسیار ساده بود. من فقط تخصیص حافظه پویا را حذف کردم، و برای اینکه UARTهای استفاده نشده این حافظه را نخورند، به سادگی آنها را توضیح دادم.

اما پیاده سازی I2C در STM32GENERIC کمی مشکل ساز بود. در آن زمان بسیار جالب بود، اما حداقل 2 شب برایم وقت گرفت. خوب، یا 2 شب اشکال‌زدایی سخت روی چاپ‌ها انجام دادید - اینطوری به آن نگاه می‌کنید.

به طور کلی، اجرای نمایش شروع نشد. در سبک سنتی از قبل، کار نمی کند و بس. چه چیزی کار نمی کند مشخص نیست. به نظر می‌رسد که کتابخانه خود نمایشگر (Adafruit SSD1306) روی پیاده‌سازی قبلی آزمایش شده است، اما اشکالات تداخل هنوز هم نباید رد شوند. شک بر HAL و اجرای I2C از STM32GENERIC است.

برای شروع، من تمام صفحه نمایش و کد I2C را کامنت گذاشتم و یک مقدار اولیه I2C بدون هیچ کتابخانه ای، در HAL خالص نوشتم.

مقداردهی اولیه I2C

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_HIGH; HAL_GPIO_Init(GPIOB، &GPIO_InitStruct); __I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed ​​= 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c1);


من وضعیت رجیسترها را بلافاصله پس از مقداردهی اولیه ریختم. من همان دامپ را در یک نسخه کاری در stm32duino ساختم. این چیزی است که من دریافت کردم (با نظراتی برای خودم)

خوب (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: وقفه خطا فعال است، 36 مگاهرتز
40005408: 0 0 0 0 - I2C_OAR1: آدرس خود صفر

40005410: 0 0 0 AF - I2C_DR: ثبت داده

40005418: 0 0 0 0 - I2C_SR2: ثبت وضعیت

بد (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: فعال کردن محیطی
40005404: 0 0 0 24 - I2C_CR2: 36 مگاهرتز
40005408: 0 0 40 0 ​​- I2C_OAR1: !!! بیت در مجموعه ثبت آدرس شرح داده نشده است
4000540C: 0 0 0 0 - I2C_OAR2: ثبت آدرس شخصی
40005410: 0 0 0 0 - I2C_DR: ثبت داده
40005414: 0 0 0 0 - I2C_SR1: ثبت وضعیت
40005418: 0 0 0 2 - I2C_SR2: مجموعه بیت مشغول
4000541C: 0 0 80 1E - I2C_CCR: حالت 400 کیلوهرتز
40005420: 0 0 0 B - I2C_TRISE

اولین تفاوت بزرگ، 14 بیت تنظیم شده در ثبات I2C_OAR1 است. این بیت اصلا در دیتاشیت توضیح داده نشده و در قسمت رزرو شده قرار می گیرد. درست است، با این هشدار که هنوز باید یکی را در آنجا بنویسید. آن ها این یک اشکال در libmaple است. اما از آنجایی که همه چیز در آنجا کار می کند، پس مشکل این نیست. بیایید بیشتر حفاری کنیم.

تفاوت دیگر این است که بیت اشغال تنظیم شده است. در ابتدا هیچ اهمیتی برای او قائل نبودم، اما با نگاه کردن به آینده، می گویم که این او بود که مشکل را نشان داد!.. اما اول همه چیز.

من کد مقداردهی اولیه را بدون هیچ کتابخانه ای اضافه کردم.

مقداردهی اولیه نمایشگر

void sendcommand (i2c_handletypedef * دسته ، uint8_t cmd) (Serialusb.print ("فرمان ارسال") ؛ Serialusb.println (cmd ، 16) ؛ uint8_t xbuffer ؛ xbuffer = 0x00 ؛ xbuffer = cmd ؛ cmd ؛ hal_i2c_master_mastransmit (<<1, xBuffer, 2, 10); } ... sendCommand(handle, SSD1306_DISPLAYOFF); sendCommand(handle, SSD1306_SETDISPLAYCLOCKDIV); // 0xD5 sendCommand(handle, 0x80); // the suggested ratio 0x80 sendCommand(handle, SSD1306_SETMULTIPLEX); // 0xA8 sendCommand(handle, 0x3F); sendCommand(handle, SSD1306_SETDISPLAYOFFSET); // 0xD3 sendCommand(handle, 0x0); // no offset sendCommand(handle, SSD1306_SETSTARTLINE | 0x0); // line #0 sendCommand(handle, SSD1306_CHARGEPUMP); // 0x8D sendCommand(handle, 0x14); sendCommand(handle, SSD1306_MEMORYMODE); // 0x20 sendCommand(handle, 0x00); // 0x0 act like ks0108 sendCommand(handle, SSD1306_SEGREMAP | 0x1); sendCommand(handle, SSD1306_COMSCANDEC); sendCommand(handle, SSD1306_SETCOMPINS); // 0xDA sendCommand(handle, 0x12); sendCommand(handle, SSD1306_SETCONTRAST); // 0x81 sendCommand(handle, 0xCF); sendCommand(handle, SSD1306_SETPRECHARGE); // 0xd9 sendCommand(handle, 0xF1); sendCommand(handle, SSD1306_SETVCOMDETECT); // 0xDB sendCommand(handle, 0x40); sendCommand(handle, SSD1306_DISPLAYALLON_RESUME); // 0xA4 sendCommand(handle, SSD1306_DISPLAYON); // 0xA6 sendCommand(handle, SSD1306_NORMALDISPLAY); // 0xA6 sendCommand(handle, SSD1306_INVERTDISPLAY); sendCommand(handle, SSD1306_COLUMNADDR); sendCommand(handle, 0); // Column start address (0 = reset) sendCommand(handle, SSD1306_LCDWIDTH-1); // Column end address (127 = reset) sendCommand(handle, SSD1306_PAGEADDR); sendCommand(handle, 0); // Page start address (0 = reset) sendCommand(handle, 7); // Page end address uint8_t buf; buf = 0x40; for(uint8_t x=1; x<17; x++) buf[x] = 0xf0; // 4 black, 4 white lines for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<1, buf, 17, 10); }


پس از مدتی تلاش، این کد برای من کار کرد (در این مورد، راه راه کشید). این بدان معنی است که مشکل در لایه I2C STM32GENERIC است. من شروع به حذف تدریجی کدم کردم و آن را با قسمت های مناسب از کتابخانه جایگزین کردم. اما به محض اینکه کد اولیه سازی پین را از پیاده سازی خود به کتابخانه ای تغییر دادم، کل انتقال I2C شروع به اتمام زمان کرد.

سپس به یاد بیت مشغول افتادم و سعی کردم بفهمم چه زمانی رخ می دهد. معلوم شد که به محض اینکه کد اولیه، ساعت I2c را روشن می کند، پرچم مشغول ظاهر می شود. آن ها ماژول روشن می شود و بلافاصله کار نمی کند. جالب هست.

ما در مقداردهی اولیه قرار می گیریم

uint8_t * pv = (uint8_t*)0x40005418; //رجیستر I2C_SR2. به دنبال پرچم BUSY SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // 0 را چاپ می کند __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); //چاپ 2


بالای این کد فقط مقداردهی اولیه پین ​​ها وجود دارد. خوب، چه باید کرد - اشکال زدایی را با چاپ در سراسر خط و آنجا بپوشانید

راه اندازی پین های STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instance, GPIO_TypeDef *port, uint32_t pin, uint32_t mode, uint32_t pull) ( ... GPIO_InitTypeDef GPIO_IntStrucit; Struct.Mode = حالت؛ GPIO_InitStruct.Pull = کشش؛ GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_VERY_HIGH؛ HAL_GPIO_Init(پورت، &GPIO_InitStruct)؛ ...)


اما بدشانسی - GPIO_InitStruct به درستی پر شده است. فقط مال من کار میکنه ولی این کار نمیکنه راستی عارف!!! همه چیز طبق کتاب درسی است، اما هیچ چیز کار نمی کند. من خط به خط کد کتابخانه را مطالعه کردم و به دنبال هر چیز مشکوکی بودم. در نهایت با این کد مواجه شدم ( تابع بالا را فراخوانی می کند)

یک قطعه دیگر از مقداردهی اولیه

void stm32AfI2CInit(const I2C_TypeDef *instance, ...) ( stm32AfInit(chip_af_i2c_sda, ...); stm32AfInit(chip_af_i2c_scl, ...)؛ )


آیا اشکالی در آن می بینید؟ و او هست! من حتی پارامترهای غیر ضروری را حذف کردم تا مشکل واضح تر شود. به طور کلی، تفاوت این است که کد من هر دو پین را در یک ساختار و کد STM32GENERIC را یک به یک مقداردهی اولیه می کند. ظاهرا کد اولیه پین ​​به نوعی بر سطح این پین تأثیر می گذارد. قبل از مقداردهی اولیه، چیزی روی این پین خروجی نمی شود و مقاومت سطح را به یک می رساند. در لحظه اولیه سازی، به دلایلی، کنترل کننده صفر را روی پایه مربوطه قرار می دهد.

این واقعیت به خودی خود بی ضرر است. اما مشکل اینجاست که پایین آوردن خط SDA در حین بالا بردن خط SCL شرط شروع برای گذرگاه i2c است. به همین دلیل، گیرنده کنترلر دیوانه می شود، پرچم BUSY را تنظیم می کند و شروع به انتظار برای داده می کند. من تصمیم گرفتم که کتابخانه را تخلیه نکنم تا توانایی مقداردهی اولیه چندین پین را به طور همزمان اضافه کنم. در عوض، من به سادگی این 2 خط را عوض کردم - مقداردهی اولیه نمایشگر موفقیت آمیز بود. اصلاح در STM32GENERIC پذیرفته شد.

به هر حال، در libmaple مقداردهی اولیه گذرگاه به روش جالبی انجام می شود. قبل از شروع به راه اندازی اولیه لوازم جانبی i2c در اتوبوس، ابتدا یک تنظیم مجدد انجام دهید. برای انجام این کار، کتابخانه پین‌ها را به حالت عادی GPIO تغییر می‌دهد و چندین بار این پاها را تکان می‌دهد و دنباله‌های شروع و توقف را شبیه‌سازی می‌کند. این به احیای دستگاه های گیر کرده در اتوبوس کمک می کند. متاسفانه چیزی مشابه در HAL وجود ندارد. گاهی اوقات نمایشگر من گیر می کند و سپس تنها راه حل این است که برق را خاموش کنم.

راه اندازی i2c از stm32duino

/** * @brief یک گذرگاه I2C را بازنشانی کنید. * * بازنشانی با کلاک کردن پالس‌ها انجام می‌شود تا زمانی که هر برد آویزان * SDA و SCL را آزاد کند، سپس یک شرط START و سپس یک شرط STOP * ایجاد می‌کند. * * @param dev I2C دستگاه */ void i2c_bus_reset(const i2c_dev *dev) ( /* هر دو خط را آزاد کنید */ i2c_master_release_bus(dev)؛ /* * با کلاک کردن گذرگاه تا زمانی که هر بردی گذرگاه * را آزاد کند، مطمئن شوید که اتوبوس آزاد است. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* منتظر بمانید تا هر ساعتی که در حال کشیده شدن است تمام شود */ در حالی که (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ؛ delay_us(10) /* پایین بکشید */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Release high again */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* شرط شروع و سپس توقف ایجاد کنید */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us (10)؛ gpio_write_bit (scl_port (dev), dev->scl_pin, 1؛ delay_us (10); gpio_write_bit (sda_port (dev), dev->sda_pin, 1);

دوباره آنجا: UART

خوشحالم که بالاخره به برنامه نویسی برگشتم و ویژگی های نوشتن را ادامه دادم. قطعه بزرگ بعدی اتصال کارت SD از طریق SPI بود. این خود یک فعالیت هیجان انگیز، جالب و دردناک است. قطعا در مقاله بعدی به طور جداگانه در مورد آن صحبت خواهم کرد. یکی از مشکلات بار بالای CPU (بیش از 50 درصد) بود. این موضوع کارایی انرژی دستگاه را زیر سوال برد. و استفاده از دستگاه ناراحت کننده بود، زیرا ... رابط کاربری به طرز وحشتناکی احمقانه بود.

با درک موضوع دلیل این مصرف منابع را پیدا کردم. همه کارها با کارت SD بایت به بایت و با استفاده از پردازنده اتفاق افتاد. اگر لازم بود بلوکی از داده ها را روی کارت بنویسیم، برای هر بایت تابع ارسال بایت فراخوانی می شود

برای (uint16_t i = 0; i< 512; i++) { spiSend(src[i]);
نه جدی نیست! DMA وجود دارد! بله، کتابخانه SD (مجموعه ای که با آردوینو عرضه می شود) دست و پا چلفتی است و نیاز به تغییر دارد، اما مشکل جهانی تر است. همین تصویر در کتابخانه صفحه نمایش مشاهده می شود و حتی گوش دادن به UART از طریق نظرسنجی انجام شده است. به طور کلی، من شروع به فکر کردم که بازنویسی همه اجزاء در HAL چندان ایده احمقانه ای نیست.

البته من با چیز ساده‌تری شروع کردم - یک درایور UART که به جریان داده‌ها از GPS گوش می‌دهد. رابط آردوینو به شما این امکان را نمی دهد که به وقفه UART متصل شوید و کاراکترهای دریافتی را در حال ربودن کنید. در نتیجه، تنها راه برای به دست آوردن داده ها از طریق نظرسنجی مداوم است. البته، من vTaskDelay(10) را به هندلر GPS اضافه کردم تا بار حداقل کمی کاهش یابد، اما در واقع این یک عصا است.

البته اولین فکر، پیوست کردن DMA بود. حتی اگر پروتکل NMEA نبود، کار می کرد. مشکل این است که در این پروتکل اطلاعات به سادگی جریان می یابد و بسته های فردی(خطوط) با یک کاراکتر شکست خط از هم جدا می شوند. علاوه بر این، هر خط می تواند طول های متفاوتی داشته باشد. به همین دلیل، از قبل مشخص نیست که چه مقدار داده باید دریافت شود. DMA اینطور کار نمی کند - تعداد بایت ها باید از قبل هنگام شروع انتقال تنظیم شود. به طور خلاصه، DMA دیگر مورد نیاز نیست، بنابراین ما به دنبال راه حل دیگری هستیم.

اگر به طراحی کتابخانه NeoGPS دقت کنید، می بینید که کتابخانه داده های ورودی را بایت به بایت می پذیرد، اما مقادیر فقط زمانی به روز می شوند که کل خط وارد شود (به طور دقیق تر، دسته ای از چندین خط. ). که فرقی نمی‌کند که بایت‌های کتابخانه را یکی یکی در زمان دریافت، تغذیه کنیم یا یک‌باره. بنابراین، می توانید با ذخیره خط دریافتی در یک بافر، در زمان پردازنده صرفه جویی کنید و می توانید این کار را مستقیماً در وقفه انجام دهید. هنگامی که کل خط دریافت شد، پردازش می تواند آغاز شود.

طرح زیر ظاهر می شود

کلاس راننده UART

// اندازه بافر ورودی UART const uint8_t gpsBufferSize = 128; // این کلاس رابط UART را مدیریت می کند که کاراکترها را از GPS دریافت می کند و آنها را در یک کلاس بافر GPS_UART ذخیره می کند ( // UART handle hardware UART_HandleTypeDef uartHandle; // Receive ring buffer uint8_t rxBuffer; volatile uint8_t lastReadIndex = 0 //VollatIndex = 0/Volat. / دسته موضوع GPS TaskHandle_t xGPSThread = NULL;


اگرچه مقدار اولیه از STM32GENERIC کپی شده است، اما کاملاً با آنچه CubeMX ارائه می دهد مطابقت دارد.

مقداردهی اولیه UART

void init() (// بازنشانی نشانگرها (فقط در صورتی که فردی init() را چندین بار فراخوانی کند) lastReadIndex = 0؛ lastReceivedIndex = 0؛ // راه‌اندازی دسته موضوع GPS xGPSThread = xTaskGetCurrentTaskHandle(); // فعال کردن clocking peri_CL_KHP )؛ __HAL_RCC_USART1_CLK_ENABLE()؛ // پین‌های راه‌اندازی در حالت عملکرد متناوب GPIO_InitTypeDef GPIO_InitStruct؛ GPIO_InitStruct.Pin = GPIO_PIN_9؛ //TX پین GPIO_InitStruct.Mode =GPIO_InitStruct.Mode =GPIOSPOPit. O_SPEED_FRE Q_HIGH؛ HAL_GPIO_Init (GPIOA، &GPIO_InitStruct)؛ GPIO_InitStruct .Pin = GPIO_PIN_10؛ // پین RX GPIO_InitStruct.Mode = GPIO_MODE_INPUT؛ GPIO_InitStruct.Pull = GPIO_NOPULL؛ HAL_GPIO_Init(GPIOA, &GPIO_InitStruct)؛ //HBay_InitStruct. نرخ = 9600؛ uart Handle.Init. WordLength = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // ما از وقفه UART برای دریافت داده HAL_NVIC_SetPriority استفاده خواهیم کرد (USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn)؛ // ما منتظر دریافت یک نویسه راست حق بافر هستیم HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


در واقع، پین TX نمی تواند مقداردهی اولیه شود، اما uartHandle.Init.Mode می تواند روی UART_MODE_RX تنظیم شود - ما فقط آن را دریافت می کنیم. با این حال، اجازه دهید - اگر من نیاز به پیکربندی ماژول GPS و نوشتن دستورات روی آن داشته باشم، چه می شود.

اگر محدودیت های معماری HAL نبود، طراحی این کلاس می توانست بهتر به نظر برسد. بنابراین، ما نمی توانیم به سادگی حالت را تنظیم کنیم، آنها می گویند، همه چیز را بپذیریم، مستقیماً به وقفه متصل شده و بایت های دریافتی را مستقیماً از رجیستر دریافت کننده ربوده باشیم. ما باید از قبل به HAL بگوییم که چند بایت و از کجا دریافت خواهیم کرد - خود کنترل کننده های مربوطه بایت های دریافتی را در بافر ارائه شده می نویسند. برای این منظور، در آخرین خط تابع مقداردهی اولیه، به HAL_UART_Receive_IT() فراخوانی می شود. از آنجایی که طول رشته از قبل ناشناخته است، باید هر بار یک بایت برداریم.

شما همچنین باید حداکثر 2 تماس را اعلام کنید. یکی یک کنترل کننده وقفه است، اما وظیفه آن فقط فراخوانی کنترل کننده از HAL است. تابع دوم «بازخوانی» HAL است که بایت قبلاً دریافت شده و از قبل در بافر است.

تماس های UART

// پردازش وقفه UART را به HAL خارجی "C" void USART1_IRQHandler(void) (HAL_UART_IRQHandler(gpsUart.getUartHandle())؛ ) // HAL زمانی که یک کاراکتر از UART دریافت می کند، این تماس را پاسخ می دهد. آن را به کلاس خارجی "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) ارسال کنید (gpsUart.charReceivedCB();


متد charReceivedCB() HAL را برای دریافت بایت بعدی آماده می کند. همچنین این کسی است که تعیین می کند که خط قبلاً به پایان رسیده است و این می تواند به برنامه اصلی سیگنال داده شود. سمافور در حالت سیگنال می تواند به عنوان وسیله ای برای همگام سازی استفاده شود، اما برای چنین اهداف ساده ای توصیه می شود از اعلان های مستقیم استفاده کنید.

پردازش یک بایت دریافتی

// کاراکتر دریافت شد، برای charReceivedCB() باطل درون خطی بعدی آماده شوید (نویسه lastReceivedChar = rxBuffer; lastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize دریافت شده است، اگر خط GPS دریافت نشد، اگر خط دریافت نشده a11)، . برای خواندن در دسترس است اگر(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread، NULL)؛ )


تابع پاسخ (انتظار) waitForString() است. وظیفه آن این است که به سادگی روی شی همگام سازی آویزان شود و منتظر بماند (یا با مهلت زمانی خارج شود)

منتظر پایان صف باشید

// صبر کنید تا کل خط دریافت شود bool waitForString() ( return ulTaskNotifyTake(pdTRUE, 10); )


اینجوری کار میکنه رشته ای که وظیفه GPS را بر عهده دارد، معمولاً در تابع ()waterForString می خوابد. بایت هایی که از GPS می آیند توسط کنترل کننده وقفه به بافر اضافه می شوند. اگر کاراکتر \n (پایان خط) وارد شود، وقفه رشته اصلی را بیدار می‌کند، که شروع به ریختن بایت‌ها از بافر به تجزیه‌گر می‌کند. خوب، وقتی تجزیه کننده پردازش بسته پیام را به پایان رساند، داده ها را در مدل GPS به روز می کند.

جریان GPS

void vGPSTask(void *pvParameters) ( // مقداردهی اولیه GPS باید در رشته GPS انجام شود زیرا دسته رشته ذخیره می شود // و بعداً برای اهداف همگام سازی استفاده می شود gpsUart.init(); برای (;;) ( // صبر کنید تا کل رشته دریافت شد if(!gpsUart.waitForString()) ادامه دارد؛ // خواندن رشته دریافتی و تجزیه کاراکتر جریان GPS بر اساس char while(gpsUart.available()) (int c = gpsUart.readChar(); //SerialUSB.write(c) ؛ gpsParser.handle(c)؛ ) if(gpsParser.available()) (GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.Parser.Parser.satellites. ) ) vTaskDelay(10); ) )


من با یک لحظه بسیار غیر ضروری روبرو شدم که چندین روز در آن گیر کردم. به نظر می رسد که کد همگام سازی از نمونه ها گرفته شده است، اما در ابتدا کار نکرد - کل سیستم را خراب کرد. فکر می کردم مشکل در اعلان های مستقیم است (توابع xTaskNotifyXXX)، آن را به سمافورهای معمولی تغییر دادم، اما همچنان برنامه از کار افتاد.

معلوم شد که باید در اولویت وقفه بسیار مراقب باشید. به طور پیش فرض، من تمام وقفه ها را روی صفر (بالاترین) اولویت قرار می دهم. اما FreeRTOS این الزام را دارد که اولویت ها در محدوده مشخصی باشند. وقفه های با اولویت خیلی بالا نمی توانند توابع FreeRTOS را فراخوانی کنند. فقط وقفه هایی با تنظیمات اولویتیLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY و پایین تر می توانند تماس بگیرند توابع سیستم(توضیح خوب و ). این تنظیمات پیش فرض روی 5 تنظیم شده است. من اولویت وقفه UART را به 6 تغییر دادم و همه چیز درست شد.

دوباره وجود دارد: I2C از طریق DMA

اکنون می توانید کارهای پیچیده تری مانند درایور نمایشگر انجام دهید. اما در اینجا باید به تئوری اتوبوس I2C بپردازیم. این گذرگاه خود پروتکل انتقال داده را در گذرگاه تنظیم نمی کند - می توانید بایت ها را بنویسید یا آنها را بخوانید. حتی می توانید در یک تراکنش بنویسید و سپس بخوانید (مثلاً یک آدرس بنویسید و سپس داده ها را در این آدرس بخوانید).

با این حال، اکثر دستگاه‌ها پروتکل سطح بالاتر را تقریباً به همان روش تعریف می‌کنند. این دستگاه مجموعه ای از رجیسترها را در اختیار کاربر قرار می دهد که هر کدام آدرس خاص خود را دارند. علاوه بر این، در پروتکل ارتباطی، اولین بایت (یا چندین) در هر تراکنش، آدرس سلول (رجیستر) را مشخص می کند که در آن بیشتر می خوانیم یا می نویسیم. در این حالت، تبادل چند بایتی به سبک "اکنون بایت های زیادی را از این آدرس می نویسیم/خوانیم" نیز امکان پذیر است. آخرین گزینه برای DMA خوب است.

متأسفانه، صفحه نمایش مبتنی بر کنترلر SSD1306 یک پروتکل - فرمان کاملاً متفاوت را ارائه می دهد. اولین بایت هر تراکنش، ویژگی “command or data” است. در مورد دستور، بایت دوم کد فرمان است. اگر دستوری به آرگومان نیاز داشته باشد، آنها به عنوان دستورات جداگانه به دنبال دستور اول ارسال می شوند. برای مقداردهی اولیه نمایشگر، باید حدود 30 دستور ارسال کنید، اما نمی توان آنها را در یک آرایه قرار داد و در یک بلوک ارسال کرد. باید آنها را یکی یکی بفرستید.

اما هنگام ارسال آرایه ای از پیکسل ها (فریم بافر)، استفاده از سرویس های DMA کاملاً امکان پذیر است. این چیزی است که ما سعی خواهیم کرد.

اما کتابخانه Adafruit_SSD1306 بسیار ناشیانه نوشته شده است و فشرده کردن آن غیرممکن است خون کمکار نمی کند. ظاهراً کتابخانه ابتدا برای ارتباط با نمایشگر از طریق SPI نوشته شده است. سپس شخصی پشتیبانی I2C را اضافه کرد، اما پشتیبانی SPI فعال باقی ماند. سپس شخصی شروع به اضافه کردن انواع بهینه‌سازی‌های سطح پایین و پنهان کردن آنها در پشت ifdef‌ها کرد. در نتیجه، معلوم شد که کد برای پشتیبانی از رابط‌های مختلف به هم ریخته است. بنابراین، قبل از رفتن بیشتر، لازم بود آن را مرتب کنیم.

در ابتدا سعی کردم با قاب بندی کدهای رابط های مختلف با ifdefs، این را مرتب کنم. اما اگر بخواهم کد ارتباطی را با نمایشگر بنویسم، از DMA و همگام سازی از طریق FreeRTOS استفاده کنم، نمی توانم کار زیادی انجام دهم. دقیق تر خواهد بود، اما این کد باید مستقیماً در کد کتابخانه نوشته شود. بنابراین، تصمیم گرفتم یک بار دیگر کتابخانه را دوباره کار کنم، یک رابط ایجاد کنم و هر درایور را در یک کلاس جداگانه قرار دهم. کد پاک‌تر شد و اضافه کردن بدون دردسر برای درایورهای جدید بدون تغییر خود کتابخانه امکان‌پذیر بود.

نمایش رابط درایور

// رابط برای درایور سخت‌افزار // Adafruit_SSD1306 مستقیماً با سخت‌افزار کار نمی‌کند // همه درخواست‌های ارتباطی به کلاس درایور ISSD1306Driver ارسال می‌شوند ( public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0 ارسال اطلاعات خالی مجازی (uint8_t * داده، اندازه_t اندازه) = 0; );


پس بزن بریم. من قبلاً مقداردهی اولیه I2C را نشان داده ام. هیچ چیز آنجا تغییر نکرده است. اما ارسال دستور کمی ساده تر شد. به یاد دارید زمانی که در مورد تفاوت بین پروتکل های ثبت و فرمان برای دستگاه های I2C صحبت کردم؟ و اگرچه نمایشگر یک پروتکل فرمان را پیاده سازی می کند، می توان آن را به خوبی با استفاده از یک پروتکل ثبت شبیه سازی کرد. فقط باید تصور کنید که نمایشگر فقط 2 رجیستر دارد - 0x00 برای دستورات و 0x40 برای داده. و HAL حتی یک تابع برای این نوع انتقال فراهم می کند

ارسال فرمان به صفحه نمایش

void DisplayDriver::sendCommand(uint8_t cmd) (HAL_I2C_Mem_Write(&handle, i2c_addr, 0x00, 1, &cmd, 1, 10); )


در ابتدا در مورد ارسال داده ها خیلی واضح نبود. کد اصلی داده ها را در بسته های کوچک 16 بایتی ارسال می کرد

کد ارسال اطلاعات عجیب

برای (uint16_t i=0; i


سعی کردم با اندازه بسته بازی کنم و بسته های بزرگتر بفرستم، اما در بهترین حالت صفحه نمایش مچاله شده ای داشتم. خوب، یا همه چیز معلق بود.

نمایشگر برش خورده



دلیل آن بی اهمیت بود - سرریز بافر. کلاس Wire از آردوینو (حداقل STM32GENERIC) بافر خود را تنها 32 بایت ارائه می دهد. اما اگر کلاس Adafruit_SSD1306 قبلاً یک بافر دارد، اصلاً چرا به یک بافر اضافی نیاز داریم؟ ضمناً با HAL ارسال در یک خط انجام می شود

انتقال صحیح اطلاعات

void DisplayDriver::sendData(uint8_t * data، size_t اندازه) (HAL_I2C_Mem_Write(&handle, i2c_addr, 0x40, 1, data, size, 10); )


بنابراین، نیمی از نبرد انجام شد - ما یک درایور برای نمایشگر در HAL خالص نوشتیم. اما در این نسخه هنوز منابع مورد نیاز است - 12٪ پردازنده برای نمایشگر 128x32 و 23٪ برای نمایشگر 128x64. استفاده از DMA در اینجا واقعاً مورد استقبال قرار می گیرد.

ابتدا اجازه دهید DMA را مقداردهی اولیه کنیم. ما می خواهیم انتقال داده را در I2C شماره 1 پیاده سازی کنیم و این تابع در کانال ششم DMA زندگی می کند. کپی بایت به بایت از حافظه به وسایل جانبی را راه اندازی کنید

راه اندازی DMA برای I2C

// ساعت کنترلر DMA فعال می کند __HAL_RCC_DMA1_CLK_ENABLE(); // مقداردهی اولیه DMA hdma_tx.Instance = DMA1_Channel6; hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; hdma_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_tx); // دسته DMA اولیه را به دسته I2C __HAL_LINKDMA(&handle, hdmatx, hdma_tx) مرتبط کنید. /* شروع وقفه DMA */ /* پیکربندی وقفه DMA1_Channel6_IRQn */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn)؛


وقفه ها بخشی ضروری از طراحی هستند. در غیر این صورت، تابع HAL_I2C_Mem_Write_DMA() یک تراکنش I2C را شروع می کند، اما هیچکس آن را کامل نمی کند. باز هم ما با طراحی دست و پا گیر HAL و نیاز به دو تماس برگشتی سروکار داریم. همه چیز دقیقاً مانند UART است. یک تابع یک کنترل کننده وقفه است - ما به سادگی تماس را به HAL هدایت می کنیم. تابع دوم سیگنالی است که داده ها قبلا ارسال شده اند.

کنترل کننده های وقفه DMA

خارجی "C" void DMA1_Channel6_IRQHandler(void) (HAL_DMA_IRQHandler(displayDriver.getDMAHandle());


البته، ما دائماً از I2C نظرسنجی نمی کنیم تا ببینیم آیا انتقال قبلاً به پایان رسیده است؟ در عوض، باید روی شی همگام سازی بخوابید و منتظر بمانید تا انتقال کامل شود

انتقال داده از طریق DMA با همگام سازی

void DisplayDriver::sendData(uint8_t * data, size_t size) ( // شروع انتقال داده HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // صبر کنید تا انتقال کامل شود ulTaskNotifyTake, 1, void0) DisplayDriver::transferCompletedCB() ( // Resume the display thread vTaskNotifyGiveFromISR(xDisplayThread, NULL)؛ )


انتقال داده هنوز 24 میلی ثانیه طول می کشد - این زمان انتقال تقریباً خالص 1 کیلوبایت (اندازه بافر نمایشگر) در 400 کیلوهرتز است. فقط در این مورد، بیشتر اوقات پردازنده به سادگی می خوابد (یا کارهای دیگری انجام می دهد). بار کلی CPU از 23٪ به 1.5-2٪ کاهش یافت. به نظر من این رقم ارزش جنگیدن را داشت!

دوباره وجود دارد: SPI از طریق DMA

اتصال یک کارت SD از طریق SPI به نوعی ساده تر بود - در این زمان من شروع به نصب کتابخانه sdfat کردم و در آنجا افراد خوب قبلاً ارتباط با کارت را در یک رابط راننده جداگانه جدا کرده بودند. درست است، با کمک تعریف ها می توانید تنها یکی از 4 نسخه درایور آماده را انتخاب کنید، اما این می تواند به راحتی تلف شود و با پیاده سازی خود جایگزین شود.

رابط درایور SPI برای کار با کارت SD

// این پیاده سازی سفارشی کلاس SPI Driver است. کتابخانه SdFat // از این کلاس برای دسترسی به کارت SD از طریق SPI استفاده می کند // // هدف اصلی این پیاده سازی هدایت انتقال داده // روی DMA و همگام سازی با قابلیت های FreeRTOS است. کلاس SdFatSPIDriver: عمومی SdSpiBaseDriver (// ماژول SPI SPI_HandleTypeDef spiHandle; // دسته رشته GPS TaskHandle_t xSDThread = NULL؛ عمومی: SdFatSPIDriver(); virtual void activate(); virtual void activate(); uint8_t دریافت()؛ uint8_t دریافت مجازی (uint8_t* buf, size_t n)؛ ارسال باطل مجازی (داده uint8_t)؛ ارسال باطل مجازی (const uint8_t* buf, size_t n)؛ باطل مجازی انتخاب(); باطل مجازی setSpiSettings(SPISettings spiSettings مجازی void unselect(); );


مانند قبل، ما با چیز ساده شروع می کنیم - با اجرای بلوط بدون هیچ گونه DMA. مقداردهی اولیه توسط CubeMX تولید شده و تا حدی با اجرای SPI STM32GENERIC ادغام شده است.

مقداردهی اولیه SPI

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) (// نادیده گرفتن پین CS عبور داده شده - این درایور با یک (void)chipSelectPin از پیش تعریف شده کار می کند؛ // راه اندازی دسته GPS Thread xSDThread = xTaskGetCurrentTaskHandle (/////). CLK_ENABLE() ؛ __HAL_RCC_SPI1_CLK_ENABLE(); // پین‌های راه‌اندازی GPIO_InitTypeDef GPIO_InitStruct؛ GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7؛ //MOSI و SCK GPIO_InitStructructroct. peed = GPIO_ SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_6؛ //MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT؛ GPIO_InitStruct.Pull = GPIO_NOPULL؛ HAL_GPIO_Init(GPIOA، &GPIO_InitStruct)؛ GPIO_InitStruct.GPIO_InitStruct.PIO_InitStruct. = GPIO_MODE_OUTPUT_PP؛ GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init( GPIOA، &GPIO_InitStruct)؛ // تنظیم پین CS بالا به طور پیش‌فرض HAL_GPIO_WritePin(GPIOA، GPIO_PIN_4، GPIO_PIN_SET)؛ // راه‌اندازی SPI spiHandle.Instance = SPI1؛ spiHandle.Init.Mode = SPI_MODE_MASTER; spiHandle.Init.Direction = SPI_DIRECTION_2LINES; spiHandle.Init.DataSize = SPI_DATASIZE_8BIT; spiHandle.Init.CLK قطبی = SPI_POLARITY_LOW; spiHandle.Init.CLKPhase = SPI_PHASE_1EDGE; spiHandle.Init.NSS = SPI_NSS_SOFT; spiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; spiHandle.Init.FirstBit = SPI_FIRSTBIT_MSB; spiHandle.Init.TIMode = SPI_TIMODE_DISABLE; spiHandle.Init.CRCCcalculation = SPI_CRCCALCULATION_DISABLE; spiHandle.Init.CRCP چند جمله ای = 10; HAL_SPI_Init(&spiHandle); __HAL_SPI_ENABLE(&spiHandle); )


طراحی رابط برای آردوینو با پین های شماره گذاری شده با یک شماره طراحی شده است. در مورد من، تنظیم پین CS از طریق پارامترها فایده ای نداشت - من این سیگنال را به شدت به پین ​​A4 گره زده ام، اما لازم بود از رابط پیروی کنم.

با طراحی کتابخانه SdFat، سرعت پورت SPI قبل از هر تراکنش تنظیم می شود. آن ها از نظر تئوری، می توانید با سرعت کم با کارت ارتباط برقرار کنید و سپس آن را افزایش دهید. اما من از این کار منصرف شدم و سرعت را یک بار در متد () begin تنظیم کردم. بنابراین مشخص شد که روش‌های فعال/غیرفعال خالی هستند. مانند setSpiSettings()

کنترل کننده تراکنش های بی اهمیت

void SdFatSPIDriver::activate() ( // بدون نیاز به فعال سازی خاصی ) void SdFatSPIDriver::deactivate() ( // بدون غیرفعال کردن خاصی مورد نیاز است) void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) ( - ما از تنظیمات استفاده می کنیم تنظیمات یکسان برای همه انتقال)


روش های کنترل سیگنال CS کاملاً بی اهمیت هستند

کنترل سیگنال CS

void SdFatSPIDriver::select() ( HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);


بیایید به بخش سرگرم کننده - خواندن و نوشتن برسیم. اولین اجرای بلوط بدون DMA

انتقال داده بدون DMA

uint8_t SdFatSPIDriver::receive() ( uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive(&spiHandle, &dummy, &buf, 1, 10; return buf; ) uint8_t (SdFatSPID/Size:,8_t) TODO : دریافت از طریق DMA در اینجا memset(buf, 0xff, n); HAL_SPI_Receive(&spiHandle, buf, n, 10)؛ بازگشت 0; ) void SdFatSPIDriver::send(uint8_t data) (HAL_SPI_Transmit(&data0,1,1); ) void SdFatSPIDriver::send(const uint8_t* buf, size_t n) ( // TODO: انتقال از طریق DMA در اینجا HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); )


در رابط SPI، دریافت و انتقال داده ها به طور همزمان انجام می شود. برای دریافت چیزی باید چیزی بفرستید. معمولا HAL این کار را برای ما انجام می دهد - ما به سادگی تابع HAL_SPI_Receive() را فراخوانی می کنیم و ارسال و دریافت را سازماندهی می کند. اما در واقع این تابع زباله هایی را که در بافر دریافت بوده را ارسال می کند.
برای فروش چیزی غیر ضروری، ابتدا باید چیزی غیر ضروری (C) Prostokvashino بخرید

اما یک تفاوت ظریف وجود دارد. کارت های SD بسیار دمدمی مزاج هستند. آنها دوست ندارند در حالی که کارت در حال ارسال داده است چیزی به آنها داده شود. بنابراین، من مجبور شدم از تابع HAL_SPI_TransmitReceive() استفاده کنم و در حین دریافت داده ها به اجبار 0xffs ارسال کنم.

بیایید اندازه گیری کنیم. بگذارید یک رشته 1 کیلوبایت داده را در یک حلقه روی کارت بنویسد.

کد تست ارسال جریان داده به کارت SD

uint8_t sd_buf; uint16_t i=0; uint32_t prev = HAL_GetTick(); while(true) (bulkFile.write(sd_buf, 512); bulkFile.write(sd_buf, 512); i++; uint32_t cur = HAL_GetTick(); if(cur-prev >= 1000) (prev = cur; usbDebugWrite( "%d kb\n ذخیره شد"، i)؛ i = 0؛ ) )


با این رویکرد می توان در هر ثانیه حدود 15-16 کیلوبایت ضبط کرد. زیاد نیست. اما معلوم شد که من پیش مقیاس کننده را روی 256 تنظیم کردم. کلاکینگ SPI روی بسیار کمتر از توان ممکن تنظیم شده است. به طور تجربی متوجه شدم که تنظیم فرکانس بالاتر از 9 مگاهرتز بی معنی است (پیش مقیاس کننده روی 8 تنظیم شده است) - نمی توان به سرعت ضبط بالاتر از 100-110 کیلوبایت در ثانیه دست یافت (به هر حال، در درایو فلش دیگر ، به دلایلی فقط 50-60 کیلوبایت بر ثانیه ضبط می شد و در مورد سوم معمولاً فقط 40 کیلوبایت بر ثانیه است). ظاهراً همه چیز به زمان‌های زمانی خود فلش درایو بستگی دارد.

در اصل، این در حال حاضر بیش از اندازه کافی است، اما ما می خواهیم داده ها را از طریق DMA پمپاژ کنیم. ما طبق طرح آشنا پیش می رویم. اول از همه، مقداردهی اولیه. ما به ترتیب در کانال دوم و سوم DMA از طریق SPI دریافت و ارسال می کنیم.

مقداردهی اولیه DMA

// ساعت کنترلر DMA فعال می کند __HAL_RCC_DMA1_CLK_ENABLE(); // کانال Rx DMA dmaHandleRx.Instance = DMA1_Channel2; dmaHandleRx.Init.Direction = DMA_PERIPH_TO_MEMORY; dmaHandleRx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleRx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleRx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleRx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleRx.Init.Mode = DMA_NORMAL; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle، hdmarx، dmaHandleRx)؛ // کانال Tx DMA dmaHandleTx.Instance = DMA1_Channel3; dmaHandleTx.Init.Direction = DMA_MEMORY_TO_PERIPH; dmaHandleTx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleTx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleTx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleTx.Init.Mode = DMA_NORMAL; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle، hdmatx، dmaHandleTx)؛


فراموش نکنید که وقفه ها را فعال کنید. برای من آنها با اولویت 8 پیش می روند - کمی پایین تر از UART و I2C

پیکربندی وقفه های DMA

// راه اندازی DMA HAL_NVIC_SetPriority را قطع می کند(DMA1_Channel2_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn)؛ HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn)؛


من تصمیم گرفتم که هزینه های سربار اجرای DMA و همگام سازی برای انتقال های کوتاه می تواند بیشتر از مزیت باشد، بنابراین برای بسته های کوچک (تا 16 بایت) گزینه قدیمی را ترک کردم. بسته های بیش از 16 بایت از طریق DMA ارسال می شوند. روش همگام سازی دقیقاً مانند قسمت قبل است.

ارسال اطلاعات از طریق DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( memset(buf, 0xff, n); // عدم استفاده از DMA برای انتقال کوتاه if(n<= DMA_TRESHOLD) { return HAL_SPI_TransmitReceive(&spiHandle, buf, buf, n, 10); } // Start data transfer HAL_SPI_TrsnsmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { HAL_SPI_Transmit(&spiHandle, buf, n, 10); return; } // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread vTaskNotifyGiveFromISR(xSDThread, NULL); }


البته هیچ راهی بدون وقفه وجود ندارد. همه چیز در اینجا مانند مورد I2C است

DMA قطع می کند

خارجی SdFatSPIDriver spiDriver; extern "C" void DMA1_Channel2_IRQHandler(void) (HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); SPI_ TxCpltCallback (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) خارجی "C" void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransfer)


بیایید راه اندازی و بررسی کنیم. برای اینکه درایو فلش را عذاب ندهم، تصمیم گرفتم با خواندن یک فایل بزرگ، و نه با نوشتن، اشکال زدایی کنم. در اینجا به یک نکته بسیار جالب پی بردم: سرعت خواندن در نسخه غیر DMA حدود 250-260 کیلوبایت بر ثانیه بود در حالی که با DMA فقط 5 بود!!! علاوه بر این، مصرف CPU بدون استفاده از DMA 3٪ و با DMA - 75-80٪ بود!!! آن ها نتیجه دقیقا برعکس چیزی است که انتظار می رفت.

خارج از تاپیک حدود 3%

در اینجا من یک مشکل خنده‌دار با اندازه‌گیری بار پردازنده داشتم - گاهی اوقات عملکرد می‌گفت که پردازنده فقط 3٪ بارگذاری شده است، اگرچه درصد باید بدون توقف در حال کوبیدن باشد. در واقع، بار 100٪ بود و عملکرد اندازه گیری من اصلاً فراخوانی نشد - کمترین اولویت را دارد و به سادگی زمان کافی برای آن وجود نداشت. بنابراین، من آخرین مقدار به خاطر سپرده شده را قبل از شروع اجرا دریافت کردم. در شرایط عادی عملکرد صحیح تر کار می کند.


با ثبت کد درایور تقریباً در هر خط، یک مشکل را کشف کردم: از عملکرد برگشت تماس اشتباه استفاده کردم. در ابتدا، کد من از HAL_SPI_Receive_DMA() استفاده می کرد و همراه با آن از پاسخ به تماس HAL_SPI_RxCplt استفاده شد. این طراحی به دلیل تفاوت ظریف با ارسال همزمان 0xff کار نمی کند. وقتی HAL_SPI_Receive_DMA() را به HAL_SPI_TransmitReceive_DMA() تغییر دادم، همچنین مجبور شدم پاسخ تماس را به HAL_SPI_TxRxCpltCallback() تغییر دهم. آن ها در واقع، خواندن انجام شد، اما به دلیل عدم تماس، سرعت با تایم اوت 100 میلی ثانیه تنظیم شد.

پس از رفع تماس، همه چیز سر جای خود قرار گرفت. بار پردازنده به 2.5٪ کاهش یافت (اکنون صادقانه)، و سرعت حتی به 500 کیلوبایت بر ثانیه رسید. درست است، پیش مقیاس کننده باید روی 4 تنظیم می شد - با پیش مقیاس کننده روی 2، ادعاها در کتابخانه SdFat سرازیر می شدند. به نظر می رسد این محدودیت سرعت کارت من است.

متاسفانه این ربطی به سرعت ضبط ندارد. سرعت نوشتن هنوز حدود 50-60 کیلوبایت بر ثانیه بود و بار پردازنده در محدوده 60-70 درصد نوسان داشت. اما بعد از گشت و گذار در تمام طول شب و اندازه گیری در مکان های مختلف، متوجه شدم که تابع send() خود درایور من (که یک بخش 512 بایتی را می نویسد) فقط 1-2 میلی ثانیه طول می کشد، از جمله انتظار و همگام سازی. با این حال، گاهی اوقات نوعی وقفه رخ می دهد و ضبط 5-7 میلی ثانیه طول می کشد. اما مشکل در واقع در درایور نیست، بلکه در منطق کار با سیستم فایل FAT است.

با رفتن به سطح فایل ها، پارتیشن ها و کلاسترها، وظیفه نوشتن 512 روی یک فایل چندان پیش پا افتاده نیست. شما باید جدول FAT را بخوانید، جایی در آن پیدا کنید تا سکتور نوشته شود، خود سکتور را بنویسید، ورودی های جدول FAT را به روز کنید، این بخش ها را روی دیسک بنویسید، ورودی ها را در جدول فایل ها و دایرکتوری ها به روز کنید. و یه سری چیزای دیگه به طور کلی، یک فراخوانی به FatFile::write () می‌تواند 15 تا 20 میلی‌ثانیه طول بکشد، و بخش زیادی از این زمان توسط کار واقعی پردازنده برای پردازش رکوردها در سیستم فایل گرفته می‌شود.

همانطور که قبلاً اشاره کردم ، بار پردازنده هنگام ضبط 60-70٪ است. اما این عدد به نوع فایل سیستم (Fat16 یا Fat32)، اندازه و بر این اساس، تعداد این خوشه ها روی پارتیشن، سرعت خود فلش، میزان شلوغ و پراکنده بودن رسانه، استفاده نیز بستگی دارد. نام فایل های طولانی و موارد دیگر. بنابراین من از شما می خواهم که این اندازه گیری ها را به عنوان نوعی ارقام نسبی در نظر بگیرید.

دوباره وجود دارد: USB با بافر دوگانه

با این جزء جالب شد. اجرای اصلی USB Serial از STM32GENERIC یکسری کاستی داشت و تصمیم گرفتم آن را برای خودم بازنویسی کنم. اما در حالی که من در حال مطالعه نحوه عملکرد USB CDC، خواندن کد منبع و مطالعه مستندات بودم، بچه های STM32GENERIC به طور قابل توجهی پیاده سازی خود را بهبود دادند. اما اول از همه.

بنابراین، اجرای اصلی به دلایل زیر مناسب من نبود:

  • پیام ها به صورت همزمان ارسال می شوند. آن ها انتقال پیش پا افتاده بایت به بایت داده از GPS UART به USB منتظر می ماند تا هر بایت جداگانه ارسال شود. به همین دلیل، بار پردازنده می تواند به 30-50٪ برسد که البته بسیار زیاد است (سرعت UART فقط 9600 است)
  • هیچ هماهنگی وجود ندارد. هنگام چاپ پیام ها از چندین رشته، خروجی رشته ای از پیام ها است که تا حدی یکدیگر را بازنویسی می کنند.
  • بیش از حد بافرهای دریافت و ارسال. چند بافر در USB Middleware اعلام شده است، اما در واقع استفاده نمی شود. چند بافر دیگر در کلاس SerialUSB اعلام شده است، اما از آنجایی که من فقط از خروجی استفاده می کنم، بافر دریافت فقط حافظه را هدر می دهد.
  • در نهایت، من فقط از رابط کلاس Print اذیت شدم. برای مثال، اگر بخواهم رشته "سرعت فعلی XXX کیلومتر در ساعت" را نمایش دهم، باید حداکثر 3 تماس برقرار کنم - برای قسمت اول رشته، برای شماره و برای بقیه رشته. من شخصاً از نظر روحی به چاپ کلاسیک نزدیکتر هستم. پخش‌های پلاس نیز مشکلی ندارند، اما باید به نوع کدی که توسط کامپایلر تولید می‌شود نگاه کنید.
در حال حاضر، اجازه دهید با چیزی ساده شروع کنیم - ارسال همزمان پیام ها، بدون همگام سازی و قالب بندی. در واقع، من صادقانه کد را از STM32GENERIC کپی کردم.

پیاده سازی "سر به سر"

USBD_HandleTypeDef خارجی hUsbDeviceFS. void usbDebugWrite(uint8_t c) ( usbDebugWrite(&c, 1); ) void usbDebugWrite(const char * str) ( usbDebugWrite((const uint8_t *)str, strlen(str)); ) ( // اگر USB وصل نیست از ارسال پیام چشم پوشی کنید if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) بازگشت؛ // پیام را ارسال کنید اما بیش از زمان اتمام uint32_t timeout = HAL_GetTick() + 5؛ while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


به طور رسمی، این کد همزمان نیست، زیرا منتظر ارسال اطلاعات نمی ماند. اما این تابع تا ارسال اطلاعات قبلی صبر می کند. آن ها تماس اول داده ها را به پورت ارسال می کند و از آن خارج می شود، اما تماس دوم منتظر می ماند تا داده های ارسال شده در تماس اول واقعاً ارسال شود. در صورت وقفه، داده ها از بین می روند. همچنین، اگر اصلاً اتصال USB وجود نداشته باشد، هیچ اتفاقی نمی افتد.

البته این فقط یک آماده سازی است، زیرا ... این پیاده سازی مشکلات شناسایی شده را حل نمی کند. چه چیزی لازم است تا این کد ناهمزمان و غیر مسدود شود؟ خوب، حداقل یک بافر. اما چه زمانی این بافر را انتقال دهیم؟

من فکر می کنم ارزش دارد که یک سفر کوتاه به اصول عملکرد USB داشته باشیم. واقعیت این است که فقط میزبان می تواند انتقال را در پروتکل USB آغاز کند. اگر دستگاهی نیاز به انتقال داده به هاست داشته باشد، داده ها در بافر مخصوص PMA (Packet Memory Area) آماده می شوند و دستگاه منتظر می ماند تا میزبان این داده ها را دریافت کند. تابع CDC_Transmit_FS() بافر PMA را آماده می کند. این بافر در داخل USB جانبی زندگی می کند و نه در کد کاربر.

من صادقانه می خواستم یک تصویر زیبا در اینجا بکشم، اما نمی توانستم بفهمم که چگونه آن را به بهترین نحو نمایش دهم.

اما اجرای طرح زیر جالب خواهد بود. کد مشتری در صورت نیاز، داده ها را در بافر ذخیره سازی (کاربر) می نویسد. هر از گاهی میزبان می آید و هر چیزی را که در آن لحظه در بافر جمع شده است با خود می برد. این بسیار شبیه به آنچه در پاراگراف قبلی توضیح دادم است، اما یک نکته مهم وجود دارد: داده ها در بافر کاربر هستند، نه در PMA. آن ها من می‌خواهم بدون فراخوانی CDC_Transmit_FS()، که داده‌ها را از بافر کاربر به PMA منتقل می‌کند، انجام دهم و در عوض پاسخ تماس «در اینجا میزبان رسیده است و داده‌ها را درخواست می‌کند» را دریافت کنم.

متأسفانه، این رویکرد در طراحی فعلی USB CDC Middleware امکان پذیر نیست. به طور دقیق تر، ممکن است امکان پذیر باشد، اما شما باید خود را در اجرای درایور CDC غرق کنید. من هنوز تجربه کافی در پروتکل های USB برای انجام این کار ندارم. علاوه بر این، من مطمئن نیستم که محدودیت های زمانی USB برای چنین عملیاتی کافی باشد.

خوشبختانه در آن لحظه متوجه شدم که STM32GENERIC قبلاً چنین چیزی را دور زده است. در اینجا کدی است که من خلاقانه از آنها دوباره کار کردم.

USB Serial Double Buffered

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; فرار uint16_t usbTxHead = 0; فرار uint16_t usbTxTail = 0; volatile uint16_t usbTransmitting = 0; uint16_t transmitContiguousBuffer() (uint16_t count = 0; // داده های پیوسته را تا انتهای بافر انتقال دهید اگر (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = usbxTxTail; FS (&usbTxBuffer, count؛ return count; ) void usbDebugWriteInternal(const char *buffer, size_t size, bool reverse = false) ( // اگر USB متصل نیست ارسال پیام را نادیده بگیرید if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) بازگشت // / پیام را ارسال کنید اما نه بیشتر از timeout uint32_t timeout = HAL_GetTick() + 5؛ // محافظت از این تابع از قفل MutexLocker چند ورودی (usbMutex)؛ // کپی داده ها در بافر برای (size_t i=0; i)< size; i++) { if(reverse) --buffer; usbTxBuffer = *buffer; usbTxHead = (usbTxHead + 1) % sizeof(usbTxBuffer); if(!reverse) buffer++; // Wait until there is a room in the buffer, or drop on timeout while(usbTxHead == usbTxTail && HAL_GetTick() < timeout); if (usbTxHead == usbTxTail) break; } // If there is no transmittion happening if (usbTransmitting == 0) { usbTransmitting = transmitContiguousBuffer(); } } extern "C" void USBSerialTransferCompletedCB() { usbTxTail = (usbTxTail + usbTransmitting) % sizeof(usbTxBuffer); if (usbTxHead != usbTxTail) { usbTransmitting = transmitContiguousBuffer(); } else { usbTransmitting = 0; } }


ایده پشت این کد به شرح زیر است. اگرچه دریافت اعلان "میزبان وارد شده است و داده می خواهد" ممکن نبود، اما مشخص شد که امکان سازماندهی یک تماس وجود دارد "من داده ها را برای میزبان ارسال کردم، می توانید مورد بعدی را بریزید." معلوم می شود که نوعی بافر دوگانه است - در حالی که دستگاه منتظر ارسال داده از بافر داخلی PMA است، کد کاربر می تواند بایت هایی را به بافر ذخیره سازی اضافه کند. هنگامی که ارسال داده ها تکمیل شد، بافر ذخیره سازی به PMA منتقل می شود. تنها چیزی که باقی می‌ماند این است که این تماس برگشتی را سازماندهی کنیم. برای انجام این کار، باید عملکرد USBD_CDC_DataIn() را کمی تغییر دهید

فایل میان افزار USB

static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev، uint8_t epnum) (USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(pdev-ct)=p 0؛ USBSerialTransferComp letedCB(); USBD_OK را برگردانید ;) else (برگردان USBD_FAIL؛ ))


به هر حال، تابع usbDebugWrite توسط یک mutex محافظت می شود و باید از چندین رشته به درستی کار کند. من از تابع USBSerialTransferCompletedCB() محافظت نکردم - از یک وقفه فراخوانی می شود و روی متغیرهای فرار عمل می کند. صادقانه بگویم، جایی در اینجا یک اشکال وجود دارد، نمادها گاهی اوقات بلعیده می شوند. اما برای من این برای رفع اشکال حیاتی نیست. این در کد "تولید" نامیده نمی شود.

دوباره وجود دارد: printf

تا کنون این چیز فقط با رشته های ثابت کار می کند. وقت آن است که آنالوگ printf() را سفت کنید. من نمی‌خواهم از تابع real printf() استفاده کنم - شامل 12 کیلوبایت کد اضافی و یک "هیپ" است که من ندارم. بالاخره دیباگ لاگرم را پیدا کردم که زمانی برای AVR نوشتم. پیاده سازی من می تواند رشته ها و همچنین اعداد را در قالب اعشاری و هگزادسیمال چاپ کند. پس از اتمام و آزمایش، چیزی شبیه به این شد:

اجرای printf ساده شده

// پیاده سازی sprintf بیش از 10 کیلوبایت طول می کشد و هیپ به پروژه اضافه می شود. من فکر می کنم این // برای عملکردی که من نیاز دارم خیلی زیاد است // // در زیر یک تابع dumping printf مانند homebrew است که می پذیرد: // - %d برای ارقام // - %x برای اعداد به صورت HEX // - %s برای رشته ها // - %% برای نماد درصد // // پیاده سازی همچنین از مقدار عرض و همچنین لایه صفر پشتیبانی می کند // چاپ شماره در بافر (به ترتیب معکوس) // تعداد نمادهای چاپ شده را برمی گرداند size_t PrintNum(مقدار int بدون علامت , uint8_t radix, char * buf, uint8_t عرض, char padSymbol) (//TODO در اینجا منفی را علامت بزنید size_t len ​​= 0؛ // عدد do را چاپ کنید ( char digit = مقدار % radix; *(buf++) = رقم< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0)؛ //افزودن صفر padding while(len< width) { *(buf++) = padSymbol; len++; } return len; } void usbDebugWrite(const char * fmt, ...) { va_list v; va_start(v, fmt); const char * chunkStart = fmt; size_t chunkSize = 0; char ch; do { // Get the next byte ch = *(fmt++); // Just copy the regular characters if(ch != "%") { chunkSize++; continue; } // We hit a special symbol. Dump string that we processed so far if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize); // Process special symbols // Check if zero padding requested char padSymbol = " "; ch = *(fmt++); if(ch == "0") { padSymbol = "0"; ch = *(fmt++); } // Check if width specified uint8_t width = 0; if(ch >"0" && ch<= "9") { width = ch - "0"; ch = *(fmt++); } // check the format switch(ch) { case "d": case "u": { char buf; size_t len = PrintNum(va_arg(v, int), 10, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "x": case "X": { char buf; size_t len = PrintNum(va_arg(v, int), 16, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "s": { char * str = va_arg(v, char*); usbDebugWriteInternal(str, strlen(str)); break; } case "%": { usbDebugWriteInternal(fmt-1, 1); break; } default: // Otherwise store it like a regular symbol as a part of next chunk fmt--; break; } chunkStart = fmt; chunkSize=0; } while(ch != 0); if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize - 1); // Not including terminating NULL va_end(v); }


پیاده سازی من بسیار ساده تر از نسخه کتابخانه است، اما می تواند هر کاری را که نیاز دارم انجام دهد - چاپ رشته ها، اعداد اعشاری و هگزادسیمال با قالب بندی (عرض فیلد، تکمیل عدد با صفر در سمت چپ). هنوز نمی داند چگونه اعداد منفی یا اعداد ممیز شناور را چاپ کند، اما اضافه کردن آن کار سختی نیست. بعداً ممکن است این امکان را فراهم کنم که نتیجه را در یک بافر رشته (مانند sprintf) بنویسم و ​​نه فقط در USB.

عملکرد این کد حدود 150-200 کیلوبایت بر ثانیه با احتساب انتقال از طریق USB است و به تعداد (طول) پیام ها، پیچیدگی رشته فرمت و اندازه بافر بستگی دارد. این سرعت برای ارسال چند هزار پیام کوچک در ثانیه کافی است. مهمترین چیز این است که تماس ها مسدود نمی شوند.

حتی بدتر: HAL سطح پایین

در اصل، ما می توانستیم در آنجا به پایان برسیم، اما متوجه شدم که بچه های STM32GENERIC اخیراً یک HAL جدید اضافه کرده اند. نکته جالب در مورد آن این است که بسیاری از فایل ها با نام stm32f1xx_ll_XXXX.h ظاهر می شوند. آنها اجرای جایگزین و سطح پایین تر HAL را نشان دادند. آن ها یک HAL معمولی یک رابط نسبتاً سطح بالا به سبک "این آرایه را بگیرید و با استفاده از این رابط به من ارسال کنید." گزارش تکمیل با وقفه.» در مقابل، فایل‌هایی با حروف LL در نام، یک رابط سطح پایین‌تری مانند «این پرچم‌ها را برای فلان رجیستر تنظیم کنید» ارائه می‌کنند.

عرفان شهر ما

با دیدن فایل های جدید در مخزن STM32GENERIC، می خواستم کیت کامل را از وب سایت ST دانلود کنم. اما گوگل فقط من را به HAL (STM32 Cube F1) نسخه 1.4 رساند که حاوی این فایل های جدید نیست. پیکربندی گرافیکی STM32CubeMX نیز این نسخه را ارائه کرد. من از توسعه دهندگان STM32GENERIC پرسیدم که نسخه جدید را از کجا دریافت کرده اند. در کمال تعجب، لینک همان صفحه را دریافت کردم، فقط اکنون نسخه 1.6 را پیشنهاد داده است. گوگل همچنین به طور ناگهانی شروع به "پیدا کردن" نسخه جدید و همچنین به روز شده CubeMX کرد. عرفان و دیگر هیچ!


چرا این لازم است؟ در بیشتر موارد، یک رابط سطح بالا در واقع مشکل را به خوبی حل می کند. HAL (لایه انتزاعی سخت افزار) کاملاً مطابق با نام خود است - کد را از رجیسترهای پردازنده و سخت افزار انتزاع می کند. اما در برخی موارد، HAL تخیل برنامه‌نویس را محدود می‌کند، در حالی که با استفاده از انتزاعات سطح پایین‌تر می‌توان کار را کارآمدتر اجرا کرد. در مورد من اینها GPIO و UART هستند.

بیایید رابط های جدید را امتحان کنیم. بیایید با لامپ شروع کنیم. متاسفانه هنوز نمونه های کافی در اینترنت وجود ندارد. ما سعی خواهیم کرد نظرات کد را در مورد توابع درک کنیم، خوشبختانه همه چیز مرتب است.

ظاهراً این موارد سطح پایین را نیز می توان به 2 قسمت تقسیم کرد:

  • توابع سطح کمی بالاتر به سبک یک HAL معمولی - در اینجا ساختار مقداردهی اولیه است، لطفاً حاشیه را برای من مقداردهی اولیه کنید.
  • تنظیم‌کننده‌ها و دریافت‌کنندگان پرچم‌ها یا ثبت‌کننده‌های فردی سطح کمی پایین‌تر. در بیشتر موارد توابع این گروه درون خطی و فقط هدر هستند
به طور پیش فرض، اولین ها توسط USE_FULL_LL_DRIVER غیرفعال می شوند. خوب، آنها معلول هستند و به جهنم آنها. ما از دومی استفاده خواهیم کرد. پس از کمی شمنیسم این درایور LED را گرفتم

Morgulka در LL HAL

// کلاس برای کپسوله کردن کار با LED(های) داخلی // // توجه: این کلاس پین های مربوطه را در سازنده مقداردهی اولیه می کند. // اگر اشیاء این کلاس به‌عنوان متغیرهای سراسری ایجاد شوند، ممکن است درست کار نکند. LL_GPIO_SetPinMode(GPIOC، پین، LL_GPIO_MODE_OUTPUT)؛ LL_GPIO_SetPinOutputType(GPIOC، پین، LL_GPIO_OUTPUT_PUSHPULL)؛ LL_GPIO_SetPinSpeed(GPIOC_GPIO_MODE_OUTPUT)(GPIOC_GPIO_MODE_OUTPUT) ( LL_GPIO_ResetO utputPin(GPIOC، پین)؛ ) void turnOff() (LL_GPIO_SetOutputPin(GPIOC , پین)؛ ) void toggle() (LL_GPIO_TogglePin(GPIOC، پین); ) ); void vLEDThread(void *pvParameters) ( LEDDriver led; // فقط هر 2 ثانیه یک بار برای (;;) پلک بزنید (vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); ))


همه چیز خیلی ساده است! نکته خوب این است که در اینجا شما واقعاً مستقیماً با رجیسترها و پرچم ها کار می کنید. هیچ سرباری برای ماژول HAL GPIO که خود تا 450 بایت کامپایل می کند و کنترل پین از STM32GENERIC که 670 بایت دیگر می گیرد وجود ندارد. در اینجا، به طور کلی، کل کلاس با تمام فراخوانی ها در تابع vLEDThread که تنها 48 بایت حجم دارد، قرار می گیرد!

من کنترل ساعت را از طریق LL HAL بهبود نداده ام. اما این مهم نیست، زیرا ... فراخوانی __HAL_RCC_GPIOC_IS_CLK_ENABLED() از HAL معمولی در واقع یک ماکرو است که فقط چند پرچم را در ثبات های خاص تنظیم می کند.

با دکمه ها به همین راحتی است

دکمه ها از طریق LL HAL

// انتساب پین ها const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // راه‌اندازی دکمه‌های مربوط به موارد void initButtons() (//فعال کردن ساعت به دستگاه جانبی GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // تنظیم پین‌های دکمه LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PINO_PINO_DE; (GPIOC، SEL_BUTTON_PIN، LL_GPIO_PULL_DOWN)؛ LL_GPIO_SetPinMode(GPIOC , OK_BUTTON_PIN, LL_GPIO_MODE_INPUT؛ LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); ) // وضعیت دکمه خواندن (اول انجام debounce) inline bool getButintPI2_InLtate (GPIOC، پین)) (// dobouncing vTaskDelay(DEBOUNCE_DURATION )؛ if(LL_GPIO_IsInputPinSet(GPIOC، پین)) true را برگردانید؛ ) false را برگردانید؛ )


با UART همه چیز جالب تر خواهد بود. بگذارید مشکل را به شما یادآوری کنم. هنگام استفاده از HAL، دریافت باید پس از هر بایت دریافت شده "شارژ" شود. حالت "برداشتن همه چیز" در HAL ارائه نشده است. و با LL HAL باید موفق شویم.

تنظیم پین ها نه تنها باعث شد دوبار فکر کنم، بلکه باعث شد به کتابچه راهنمای مرجع نگاه کنم

راه اندازی پین های UART

// پین های راه اندازی در حالت عملکرد جایگزین LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); //پین TX LL_GPIO_SetPinSpeed(GPIOA، LL_GPIO_PIN_9، LL_GPIO_SPEED_FREQ_HIGH); LL_GPIO_SetPinOutputType(GPIOA، LL_GPIO_PIN_9، LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinMode(GPIOA، LL_GPIO_PIN_10، LL_GPIO_MODE_INPUT)؛ // پین RX


بازسازی UART برای رابط های جدید

مقداردهی اولیه UART

// آماده سازی اولیه LL_USART_Disable(USART1); // Init LL_USART_SetBaudRate(USART1, HAL_RCC_GetPCLK2Freq(), 9600); LL_USART_SetDataWidth(USART1، LL_USART_DATAWIDTH_8B)؛ LL_USART_SetStopBitsLength(USART1، LL_USART_STOPBITS_1); LL_USART_SetParity(USART1، LL_USART_PARITY_NONE)؛ LL_USART_SetTransferDirection(USART1، LL_USART_DIRECTION_TX_RX); LL_USART_SetHWFlowCtrl(USART1، LL_USART_HWCONTROL_NONE)؛ // ما از وقفه UART برای دریافت داده HAL_NVIC_SetPriority استفاده خواهیم کرد (USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn)؛ // فعال کردن وقفه UART در دریافت بایت LL_USART_EnableIT_RXNE(USART1); // در نهایت LL_USART_Enable (USART1) محیطی را فعال کنید.


حالا وقفه در نسخه قبلی، ما تا 2 عملکرد داشتیم - یکی وقفه را پردازش می کرد و دومی یک تماس (از همان وقفه) در مورد بایت دریافتی بود. در نسخه جدید، وقفه را طوری پیکربندی کردیم که فقط یک بایت دریافت کند، بنابراین بلافاصله بایت دریافتی را دریافت خواهیم کرد.

وقفه UART

// ذخیره بایت دریافتی inline void charReceivedCB(uint8_t c) (rxBuffer = c; lastReceivedIndex++; // اگر نماد EOL دریافت شد، به رشته GPS اطلاع دهید که این خط برای خواندن در دسترس است if(c == "\n") vTaskNotifyGiveFromISre(xGPSTh NULL؛ ) خارجی "C" void USART1_IRQHandler(void) (uint8_t بایت = LL_USART_ReceiveData8(USART1)؛ gpsUart.charReceivedCB(byte)؛ )


حجم کد درایور از 1242 به 436 بایت و مصرف رم از 200 به 136 (که 128 بافر هستند) کاهش یافته است. بد نیست به نظر من تنها حیف این است که این پرخور ترین قسمت نیست. می توان چیز دیگری را کمی کوتاه کرد، اما در حال حاضر من به طور خاص دنبال مصرف منابع نیستم - هنوز آنها را دارم. و رابط سطح بالا HAL در مورد سایر تجهیزات جانبی کاملاً خوب عمل می کند.

نگاهی به گذشته

اگرچه در شروع این مرحله از پروژه، من در مورد HAL شک داشتم، اما هنوز موفق شدم همه کارها را با تجهیزات جانبی بازنویسی کنم: GPIO، UART، I2C، SPI و USB. من پیشرفت زیادی در درک نحوه کار این ماژول ها داشته ام و سعی کردم دانش را در این مقاله منتقل کنم. اما این اصلاً ترجمه راهنمای مرجع نیست. برعکس، من در زمینه این پروژه کار کردم و نشان دادم که چگونه می توان درایورهای جانبی را در HAL خالص نوشت.

این مقاله کمابیش یک داستان خطی بود. اما در واقع، من تعدادی برانچ داشتم که در آنها به طور همزمان دقیقاً در جهت مخالف اره می کردم. صبح ممکن است با عملکرد برخی از کتابخانه‌های آردوینو با مشکل مواجه شوم و قاطعانه تصمیم بگیرم همه چیز را در HAL بازنویسی کنم، و عصر متوجه شدم که شخصی قبلاً پشتیبانی DMA را به STM32GENERIC اضافه کرده است و من می‌خواهم به عقب برگردم. . یا به عنوان مثال، چند روز را صرف مبارزه با رابط های آردوینو کنید و سعی کنید بفهمید که چگونه انتقال داده ها از طریق I2C راحت تر است، در حالی که در HAL این کار در 2 خط انجام می شود.

در کل به چیزی که می خواستم رسیدم. کار اصلی با لوازم جانبی تحت کنترل من است و با HAL نوشته شده است. آردوینو تنها به عنوان یک آداپتور برای برخی از کتابخانه ها عمل می کند. درست است، هنوز چند دم باقی مانده بود. شما هنوز باید جسارت خود را جمع کنید و STM32GENERIC را از مخزن خود حذف کنید و تنها چند کلاس واقعا ضروری باقی بمانید. اما چنین تمیزکاری دیگر برای این مقاله اعمال نخواهد شد.

در مورد آرودینو و کلون هایش. من هنوز این چارچوب را دوست دارم. با استفاده از آن، می‌توانید به سرعت از چیزی نمونه‌سازی کنید، بدون اینکه واقعاً خودتان را با خواندن کتابچه‌های راهنما و دیتاشیت‌ها آزار دهید. در اصل، اگر شرایط خاصی برای سرعت، مصرف یا حافظه وجود نداشته باشد، حتی می توانید دستگاه های پایانی را با آردوینو بسازید. در مورد من، این پارامترها بسیار مهم هستند، بنابراین مجبور شدم به HAL بروم.

من شروع به کار روی stm32duino کردم. اگر می‌خواهید یک آردوینو در STM32 داشته باشید و همه چیز خارج از جعبه باشد، این کلون واقعاً شایسته توجه است. علاوه بر این، مصرف رم و فلش را به دقت زیر نظر دارند. برعکس، STM32GENERIC خود ضخیم تر است و بر اساس HAL هیولایی ساخته شده است. اما این چارچوب به طور فعال در حال توسعه است و در شرف تکمیل است. به طور کلی، من می توانم هر دو چارچوب را با ترجیح کمی برای STM32GENERIC توصیه کنم زیرا HAL و توسعه پویاتر در حال حاضر است. علاوه بر این، اینترنت پر از نمونه هایی برای HAL است و همیشه می توانید چیزی را مطابق با خودتان سفارشی کنید.

من هنوز خود HAL را با درجاتی از انزجار می دانم. کتابخانه خیلی حجیم و زشت است. من برای این واقعیت که کتابخانه مبتنی بر C است، در نظر می‌گیرم که استفاده از نام‌های طولانی توابع و ثابت‌ها را ضروری می‌کند. اما با این حال، این کتابخانه ای نیست که کار با آن سرگرم کننده باشد. بلکه اقدامی ضروری است.

بسیار خوب، رابط - داخل نیز شما را به فکر وادار می کند. عملکردهای عظیم با عملکرد برای همه موارد مستلزم اتلاف منابع است. علاوه بر این، اگر بتوانید با استفاده از بهینه‌سازی زمان لینک با کدهای اضافی در فلش مقابله کنید، مصرف هنگفت RAM تنها با بازنویسی آن در LL HAL برطرف می‌شود.

اما این حتی ناراحت کننده نیست، اما در برخی جاها فقط بی توجهی به منابع است. بنابراین من متوجه استفاده بیش از حد از حافظه در کد USB Middleware شدم (به طور رسمی HAL نیست، اما به عنوان بخشی از STM32Cube ارائه می شود). ساختارهای USB 2.5 کیلوبایت حافظه را اشغال می کنند. علاوه بر این، ساختار USBD_HandleTypeDef (544 بایت) تا حد زیادی PCD_HandleTypeDef را از لایه پایین (1056 بایت) تکرار می کند - نقاط پایانی نیز در آن تعریف شده اند. بافرهای فرستنده گیرنده نیز حداقل در دو مکان اعلان می شوند - USBD_CDC_HandleTypeDef و UserRxBufferFS/UserTxBufferFS.

توصیفگرها معمولاً در RAM تعریف می شوند. برای چی؟ ثابت هستند! تقریبا 400 بایت در رم. خوشبختانه، برخی از توصیفگرها ثابت هستند (کمی کمتر از 300 بایت). توصیفگرها اطلاعات تغییرناپذیر هستند. و در اینجا یک کد ویژه وجود دارد که آنها را وصله می کند، و دوباره با یک ثابت. و حتی یکی که قبلاً در آنجا گنجانده شده است. به دلایلی، توابعی مانند SetBuffer یک بافر ثابت را نمی پذیرند، که همچنین از قرار دادن توصیفگرها و برخی موارد دیگر در فلش جلوگیری می کند. دلیل ش چیه؟ 10 دقیقه دیگه درست میشه!!!

یا ساختار اولیه بخشی از دسته شی است (به عنوان مثال i2c). چرا این را پس از مقداردهی اولیه دستگاه جانبی ذخیره کنید؟ چرا به نشانگرهایی برای ساختارهای استفاده نشده نیاز دارم - برای مثال، اگر از آن استفاده نکنم چرا به داده های مرتبط با DMA نیاز دارم؟

و همچنین کدهای تکراری.

مورد USB_DESC_TYPE_CONFIGURATION: if(pdev->dev_speed == USBD_SPEED_HIGH) (pbuf = (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; ) break;


یک تبدیل ویژه به "نوع یونیکد"، که می تواند در زمان کامپایل انجام شود. علاوه بر این، یک بافر ویژه برای این کار اختصاص داده شده است

تمسخر داده های آماری

ALIGN_BEGIN uint8_t USBD_StrDesc __ALIGN_END؛ void USBD_GetString (const char *desc, uint8_t *unicode, uint16_t *len) (uint8_t idx = 0; if (desc != NULL) ( *len = USBD_GetLen(desc) * 2 + 2؛ unicode = *len; unicode = *len; یونیکد = USBSTRING_DE ؛ while (*desc != "\0") (یونیکد = *desc++؛ یونیکد = 0x00; ) )


کشنده نیست، اما شما را متعجب می کند که آیا HAL به همان خوبی است که معذرت خواهی ها در مورد آن می نویسند؟ خوب، این چیزی نیست که شما از یک کتابخانه از سازنده و طراحی شده برای حرفه ای ها انتظار دارید. اینها میکروکنترلر هستند! در اینجا مردم هر بایت را ذخیره می کنند و هر میکروثانیه ارزشمند است. و در اینجا، می دانید، یک بافر نیم کیلوگرمی و تبدیل در حین پرواز رشته های ثابت وجود دارد. شایان ذکر است که بیشتر نظرات در مورد USB Middleware اعمال می شود.

UPD: در HAL 1.6 پاسخ تماس I2C DMA Transfer Completed نیز شکسته شد. آن ها در آنجا، کدی که هنگام ارسال داده ها از طریق DMA تأیید ایجاد می کند، به طور کامل ناپدید شده است، اگرچه در مستندات توضیح داده شده است. یکی برای دریافت وجود دارد، اما نه برای انتقال. من مجبور شدم برای ماژول I2C به HAL 1.4 برگردم، خوشبختانه یک ماژول - یک فایل وجود دارد.

در آخر مصرف فلش و رم قطعات مختلف رو میدم. در بخش درایورها، هم برای درایورهای مبتنی بر HAL و هم برای درایورهای مبتنی بر LL HAL مقادیر ارائه کرده ام. در حالت دوم، بخش های مربوطه از بخش HAL استفاده نمی شود.

مصرف حافظه

دسته بندی زیرمجموعه .متن rodata .داده ها .bss
سیستم بردار وقفه 272
کنترل کننده های ISR ساختگی 178
libc 760
ریاضی شناور 4872
گناه / cos 6672 536
اصلی و غیره 86
کد من کد من 7404 833 4 578
printf 442
فونت ها 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit GFX 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
میان افزار USB هسته 1740 333 2179
CDC 772
رانندگان UART 268 200
یو اس بی 264 846
I2C 316 164
SPI 760 208
دکمه های LL 208
LED LL 48
UART LL 436 136
آردوینو gpio 370 296 16
متفرقه 28 24
چاپ 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
پشته (واقعا استفاده نشده) 1068
FreeRTOS Heap 10240

همین. خوشحال خواهم شد که نظرات سازنده و همچنین توصیه هایی را دریافت کنم اگر چیزی در اینجا بهبود یابد.

برچسب ها:

  • HAL
  • STM32
  • مکعب STM32
  • آردوینو
افزودن برچسب

لیستی از مقالاتی که حتی به افراد مبتدی در یادگیری میکروکنترلر STM32 کمک می کند. جزئیات در مورد همه چیز با مثال هایی از چشمک زدن LED تا کنترل موتور بدون جاروبک. نمونه ها از SPL استاندارد (کتابخانه محیطی استاندارد) استفاده می کنند.

برد تست STM32F103، برنامه نویس ST-Link و نرم افزار برای سیستم عامل ویندوز و اوبونتو.

VIC (کنترل کننده وقفه بردار تودرتو) - ماژول کنترل وقفه. راه اندازی و استفاده از وقفه ها. اولویت ها را قطع کنید وقفه های تو در تو.

ADC (مبدل آنالوگ به دیجیتال). نمودار منبع تغذیه و نمونه هایی از استفاده از ADC در حالت های مختلف. کانال های منظم و تزریقی استفاده از ADC با DMA دماسنج داخلی. نگهبان آنالوگ

تایمرهای عمومی ایجاد وقفه در فواصل منظم. اندازه گیری زمان بین دو رویداد

ضبط سیگنال توسط تایمر با استفاده از مثال کار با سنسور اولتراسونیک HC-SR04

استفاده از تایمر برای کار با رمزگذار

تولید PWM کنترل روشنایی LED کنترل سروو درایو (servo). تولید صدا.

من نشان دادم که کتابخانه استاندارد به سیستم متصل است. در واقع، CMSIS متصل است - سیستم نمایش ساختاری تعمیم یافته MK، و همچنین SPL - کتابخانه محیطی استاندارد. بیایید به هر یک از آنها نگاه کنیم:

CMSIS
این مجموعه ای از فایل های هدر و مجموعه ای کوچک از کد برای یکپارچه سازی و ساختاردهی کار با هسته و حاشیه MK است. در واقع بدون این فایل ها نمی توان به طور معمول با MK کار کرد. می توانید کتابخانه را در صفحه MK دریافت کنید.
این کتابخانه، طبق توضیحات، برای یکسان سازی رابط ها هنگام کار با هر MK از خانواده Cortex ایجاد شده است. با این حال، در واقعیت معلوم می شود که این فقط برای یک تولید کننده صادق است، یعنی. با سوئیچ کردن به میکروکنترلر از یک شرکت دیگر، مجبور می شوید تقریباً از ابتدا تجهیزات جانبی آن را مطالعه کنید.
اگرچه آن دسته از فایل‌هایی که به هسته پردازنده MK مربوط می‌شوند از همه تولیدکنندگان یکسان هستند (اگر فقط به این دلیل که مدل هسته پردازنده یکسانی دارند - که در قالب بلوک‌های IP توسط ARM ارائه شده است).
بنابراین، کار با بخش‌هایی از هسته مانند ثبات‌ها، دستورالعمل‌ها، وقفه‌ها و واحدهای پردازشگر برای همه استاندارد است.
در مورد محیط، STM32 و STM8 (به طور ناگهانی) تقریباً مشابه هستند و این نیز تا حدی برای MKهای دیگر منتشر شده توسط ST صادق است. در بخش عملی، من نشان خواهم داد که استفاده از CMSIS چقدر آسان است. با این حال، مشکلات در استفاده از آن با بی میلی افراد به خواندن اسناد و درک طرح MK همراه است.

SPL
کتابخانه محیطی استاندارد - کتابخانه محیطی استاندارد. همانطور که از نام آن پیداست، هدف این کتابخانه ایجاد یک انتزاع برای حاشیه MK است. این کتابخانه شامل فایل‌های سرصفحه‌ای است که در آن ثابت‌های قابل خواندن توسط انسان برای پیکربندی و کار با وسایل جانبی MK، و همچنین فایل‌های کد منبع جمع‌آوری شده در خود کتابخانه برای عملیات با وسایل جانبی، اعلان شده‌اند.
SPL یک انتزاع از CMSIS است که به کاربر یک رابط مشترک برای همه MK ها نه تنها از یک سازنده، بلکه به طور کلی برای همه MK ها با هسته پردازنده Cortex-Mxx.
اعتقاد بر این است که برای مبتدیان راحت تر است، زیرا ... به شما این امکان را می دهد که به نحوه عملکرد دستگاه های جانبی فکر نکنید، اما کیفیت کد، جهانی بودن رویکرد و محدودیت رابط ها محدودیت های خاصی را بر توسعه دهنده تحمیل می کند.
همچنین، عملکرد کتابخانه همیشه به شما اجازه نمی دهد تا پیکربندی برخی از مؤلفه ها مانند USART (درگاه سریال همگام-ناهمزمان جهانی) را تحت شرایط خاص به طور دقیق پیاده سازی کنید. در قسمت عملی نیز کار با این قسمت از کتابخانه را شرح خواهم داد.

بهترین مقالات در این زمینه