Kako postaviti pametne telefone i računala. Informativni portal
  • Dom
  • vijesti
  • STM32F407 (STM32F4-DISCOVERY) - Nestandardni pristup - Standardna knjižnica dio 1.

STM32F407 (STM32F4-DISCOVERY) - Nestandardni pristup - Standardna knjižnica dio 1.

Softver potreban za razvoj. U ovom članku pokazat ću vam kako ga pravilno konfigurirati i povezati. Sva komercijalna okruženja kao što su IAR EWARM ili Keil uVision obično sami izvode ovu integraciju, ali u našem slučaju sve će se morati konfigurirati ručno, trošeći puno vremena na to. Prednost je što imate priliku razumjeti kako sve to funkcionira iznutra, a u budućnosti sve možete fleksibilno prilagoditi sebi. Prije početka postavljanja razmotrite strukturu okruženja u kojem ćemo raditi:

Eclipse će se koristiti za praktično uređivanje datoteka implementacije funkcija ( .c), datoteke zaglavlja ( .h) kao i montažne datoteke ( .S). Pod "prikladnim" mislim na korištenje dovršavanja koda, isticanje sintakse, refaktoriranje, navigaciju kroz funkcije i njihove prototipove. Datoteke se automatski šalju ispravnim prevodiocima koji generiraju objektni kod (u datotekama .o). Za sada ovaj kod ne sadrži apsolutne adrese varijabli i funkcija, pa stoga nije prikladan za izvršenje. Rezultirajuće objektne datoteke skuplja se pomoću povezivača. Da bi znao koje dijelove adresnog prostora koristiti, graditelj koristi posebnu datoteku ( .ld), koja se naziva skripta povezivača. Obično sadrži definiciju adresa odjeljaka i njihove veličine (odjeljak koda prikazan na flashu, varijabilni odjeljak prikazan na RAM-u, itd.).

Na kraju, povezivač generira .elf datoteku (Izvršni i Linkable Format), koja sadrži, osim uputa i podataka, informacije za otklanjanje pogrešaka (Informacije o debuggingu) koje koristi program za ispravljanje pogrešaka. Za obični firmware vsprog, ovaj format nije prikladan, jer to zahtijeva primitivniju memorijsku slikovnu datoteku (na primjer, Intel HEX - .hex). Tu je i alat iz Sourcery CodeBench skupa (arm-none-eabi-objcopy) za njegovo generiranje, a oni se savršeno integriraju u eclipse pomoću instaliranog ARM dodatka.

Za izvođenje samog otklanjanja pogrešaka koriste se tri programa:

  1. sama eclipse, koja omogućuje programeru da "vizualno" koristi otklanjanje pogrešaka, prolazi kroz redove, drži pokazivač miša iznad varijabli da vidi njihove vrijednosti i druge pogodnosti
  2. arm-none-eabi-gdb - GDB klijent - debugger, koji je potajno kontroliran eclips-om (putem stdin) kao reakcija na radnje navedene u stavku 1. Zauzvrat, GDB se povezuje na OpenOCD Debug poslužitelj, a sve dolazne naredbe GDB debugger prevodi u naredbe koje OpenOCD razumije. GDB kanal<->OpenOCD se implementira preko TCP protokola.
  3. OpenOCD je poslužitelj za ispravljanje pogrešaka koji može izravno komunicirati s programatorom. Pokreće se ispred klijenta i čeka TCP vezu.

Ova shema vam se može činiti vrlo beskorisnom: zašto koristiti klijenta i poslužitelja odvojeno i još jednom izvoditi prijevod naredbi, ako se sve to može učiniti jednim debugerom? Činjenica je da takva arhitektura teoretski omogućuje prikladnu izmjenu klijenta i poslužitelja. Na primjer, ako trebate koristiti drugi programator umjesto versaloona, koji neće podržavati OpenOCD, ali će podržavati drugi poseban poslužitelj za ispravljanje pogrešaka (na primjer, texane / stlink za stlink programator - koji se nalazi na ploči za otklanjanje pogrešaka STM32VLDiscovery), umjesto toga pokretanja OpenOCD-a jednostavno ćete pokrenuti traženi poslužitelj i sve bi trebalo raditi, bez ikakvih dodatnih pokreta. Istodobno, moguća je i suprotna situacija: recimo da ste htjeli koristiti okruženje IAR EWARM zajedno s versaloonom umjesto paketa Eclipse + CodeBench. IAR ima vlastiti ugrađeni Debug klijent, koji će se uspješno povezati na OpenOCD i kontrolirati ga, kao i primati potrebne podatke kao odgovor. Međutim, sve to ponekad ostaje samo u teoriji, budući da standardi za komunikaciju između klijenta i poslužitelja nisu strogo regulirani i mogu se mjestimično razlikovati, međutim, konfiguracije koje sam naznačio sa st-link + eclipse i IAR + versaloon bile su uspješne za mene.

Obično klijent i poslužitelj rade na istom stroju i povezuju se s poslužiteljem na adresi lokalni domaćin: 3333(Za openocd), ili lokalni domaćin: 4242(za teksan / stlink st-util). Ali nitko se ne trudi otvoriti port 3333 ili 4242 (i proslijediti ovaj port na usmjerivaču na vanjsku mrežu) i vaši kolege iz drugog grada moći će se povezati i otkloniti pogreške na vašem komadu hardvera. Ovaj trik često koriste embedderi koji rade na udaljenim objektima, pristup kojima je ograničen.

Započnimo

Pokrenite eclipse i odaberite File-> New-> C Project, odaberite tip projekta ARM Linux GCC (Sorcery G++ Lite) i naziv "stm32_ld_vl" (ako imate STV32VLDiscovery, bilo bi logičnije da ga nazovete "stm32_md_vl" ):

Kliknite Završi, minimizirajte ili zatvorite prozor dobrodošlice. Dakle, projekt je stvoren, a mapa stm32_ld_vl trebala bi se pojaviti u vašem radnom prostoru. Sada ga treba popuniti potrebnim knjižnicama.

Kao što ste shvatili iz naziva projekta, izradit ću projekt za prikaz ravnala linija vrijednosti niske gustoće(LD_VL). Za izradu projekta za druge mikrokontrolere morate zamijeniti sve datoteke i definirati u čijem se nazivu nalazi _LD_VL (ili_ld_vl) na one koje trebate, u skladu s tablicom:

Pogled ravnala Oznaka Mikrokontroleri (x može varirati)
Linija vrijednosti niske gustoće _LD_VL STM32F100x4 STM32F100x6
Niska gustoća _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Linija vrijednosti srednje gustoće _MD_VL STM32F100x8 STM32F100xB
Srednje gustoće
_DOKTOR MEDICINE
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Linija vrijednosti visoke gustoće _HD_VL STM32F100xC STM32F100xD STM32F100xE
Visoka gustoća _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-gustoća _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Linija za povezivanje _CL STM32F105xx i STM32F107xx

Da biste razumjeli logiku tablice, morate biti upoznati sa STM32 označavanjem. Odnosno, ako imate VLDiscovery, tada ćete morati zamijeniti sve što je povezano s _LD_VL s _MD_VL, budući da je čip STM32F100RB, koji pripada liniji vrijednosti srednje gustoće, zalemljen u otkriću.

Dodavanje biblioteke standardnih perifernih uređaja CMSIS i STM32F10x projektu

CMSIS(Cortex Microcontroller Software Interface Standard) je standardizirana knjižnica za rad s Cortex mikrokontrolerima koja implementira razinu HAL (Hardware Abstraction Layer), odnosno omogućuje apstrahiranje od detalja rada s registrima, pronalaženja adresa registara prema tablicama podataka itd. . Knjižnica je zbirka C i Asm izvora. Temeljni dio biblioteke isti je za sve Cortexe (bilo da se radi o ST, NXP, ATMEL, TI ili bilo čemu drugom), a razvio ju je ARM. Drugi dio knjižnice odgovoran je za periferne uređaje, koji su prirodno različiti za različite proizvođače. Stoga, na kraju, potpunu biblioteku i dalje distribuira proizvođač, iako se temeljni dio još uvijek može zasebno preuzeti s ARM web stranice. Biblioteka sadrži definicije adresa, inicijalizacijski kod generatora takta (pogodno konfiguriran po definicijama) i sve ostalo što programera spašava od ručnog unosa u svoje projekte definicije adresa svih perifernih registara i definicije bitova vrijednosti ovih registara.

Ali dečki iz ST-a otišli su dalje. Osim podrške za CMSIS, oni pružaju još jednu biblioteku za STM32F10x pod nazivom Knjižnica standardnih perifernih uređaja(SPL), koji se može koristiti uz CMSIS. Knjižnica omogućuje brži i praktičniji pristup perifernim uređajima, a također kontrolira (u nekim slučajevima) ispravnost rada s perifernim uređajima. Stoga se ova biblioteka često naziva skupom upravljačkih programa za periferne module. Dolazi s paketom primjera, kategoriziranih za različite periferne uređaje. Također postoji biblioteka ne samo za STM32F10x, već i za druge serije.

Cijelu verziju SPL + CMSIS 3.5 možete preuzeti ovdje: STM32F10x_StdPeriph_Lib_V3.5.0 ili na web stranici ST. Raspakirajte arhivu. Napravite mape CMSIS i SPL u mapi projekta i počnite kopirati datoteke u svoj projekt:

Što kopirati

Gdje kopirati (uzimajući u obzir,
to je mapa projekta stm32_ld_vl)

Opis datoteke
Knjižnice / CMSIS / CM3 /
CoreSupport / jezgra_cm3.c
stm32_ld_vl / CMSIS / jezgra_cm3.c Opis jezgre Cortex M3
Knjižnice / CMSIS / CM3 /
CoreSupport / jezgra_cm3.h
stm32_ld_vl / CMSIS / core_cm3.h Zaglavlja opisa jezgre

ST / STM32F10x / system_stm32f10x.c
stm32_ld_vl / CMSIS /system_stm32f10x.c Funkcije inicijalizacije i
kontrola sata
Knjižnice / CMSIS / CM3 / DeviceSupport /
ST / STM32F10x / system_stm32f10x.h
stm32_ld_vl / CMSIS /system_stm32f10x.h Naslovi prema ovim funkcijama
Knjižnice / CMSIS / CM3 / DeviceSupport /
ST / STM32F10x / stm32f10x.h
stm32_ld_vl / CMSIS /stm32f10x.h Osnovni opis periferije
Knjižnice / CMSIS / CM3 / DeviceSupport /
ST / STM32F10x / pokretanje / gcc_ride7 /
startup_stm32f10x_ld_vl.s
stm32_ld_vl / CMSIS /startup_stm32f10x_ld_vl.S
(!!! Upozorenje ekstenzija datoteke CAPITAL S)
Datoteka vektorske tablice
prekida i init-s na asm
Projekt / STM32F10x_StdPeriph_Template /
stm32f10x_conf.h
stm32_ld_vl / CMSIS / stm32f10x_conf.h Predložak za prilagodbu
perifernih modula

inc / *
stm32_ld_vl / SPL / inc / * SPL zaglavlja
Knjižnice / STM32F10x_StdPeriph_Driver /
src / *
stm32_ld_vl / SPL / src / * SPL implementacija

Nakon kopiranja idite na Eclipse i izvršite Osvježi u kontekstnom izborniku projekta. Kao rezultat toga, u Project Exploreru biste trebali dobiti istu strukturu kao na slici desno.

Možda ste primijetili da u mapi Libraries / CMSIS / CM3 / DeviceSupport / ST / STM32F10x / startup / postoje mape za različite IDE-ove (različiti IDE-ovi koriste različite kompajlere). Odabrao sam IDE Ride7 jer koristi kompajler GNU Tools za ARM Embedded, koji je kompatibilan s našim Sourcery CodeBench.

Cijela knjižnica se konfigurira pomoću predprocesora (koristeći define-s), što će omogućiti rješavanje svih potrebnih grana u fazi kompilacije (ili bolje rečeno, čak i prije nje) i izbjegavanje opterećenja u samom kontroleru (što bi se primijetilo ako bi konfiguracija je izvedena u RunTime). Na primjer, sva oprema je različita za različite ravnale, pa kako bi knjižnica "doznala" koje ravnalo želite koristiti, od vas se traži da dekomentirate u datoteci stm32f10x.h jedna od definicija (koja odgovara vašem vladaru):

/ * #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 */

itd...

Ali ne preporučam to učiniti. Za sada nećemo dirati datoteke biblioteke, ali ćemo kasnije definirati pomoću postavki kompajlera u Eclipseu. A onda će Eslipse pozvati prevodilac s ključem -D STM32F10X_LD_VL, što je za pretprocesor apsolutno ekvivalentno situaciji ako ne komentirate "#define STM32F10X_LD_VL"... Dakle, nećemo mijenjati kod, pa ćete, ako želite, jednog dana moći premjestiti biblioteku u poseban direktorij, a ne kopirati je u mapu svakog novog projekta.

Linker skripta

U kontekstnom izborniku projekta odaberite Novo-> Datoteka-> Ostalo-> Općenito-> Datoteka, Sljedeće. Odaberite korijensku mapu projekta (stm32_ld_vl). Unesite naziv datoteke "stm32f100c4.ld" (ili "stm32f100rb.ld" za otkrivanje). Sada kopirajte i zalijepite u eclipse:

ULAZ (Reset_Handler) MEMORIJA (FLASH (rx): PORIJEKLO = 0x08000000, DULJINA = 16K RAM-a (xrw): PORIJEKLO = 0x20000000, DULJINA = 4K) _estack = PODRIJETLO (RAM) + LENGTH = ORIGIN (RAM) + LENGTH; MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECTIONS (/ * Tablica vektora prekida * / .isr_vector: (. = ALIGN (4); KEEP (* (. Isr_vector))). = ALIGN (4);)> FLASH / * Programski kod i ostali podaci idu u FLASH * / .text: (. = ALIGN (4); / * Code * / * (. text) * (. text *) / * Konstante * / * (. rodata) * (. rodata *) / * ARM-> Thumb i Thumb-> ARM kod ljepila * / * (. 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. atributi: (* (. ARM.attributes))> FLASH .preinit_array: (PROVIDE_HIDDEN (__preinit_array_start =.); KEEP (* (. preinit_array *)) PROVIDE_HIDDEN (__preinit_array_end =.);)> FLASH_array: .i end.); )> FLASH: .init_array_end =.);)> FLASH: .init_array_end =.); KEEP (* (SORT (.init_array. *))) KEEP (* (. Init_array *)) PROVIDE_HIDDEN (__init_array_end =.);) > FLASH .fini_array: (PROVIDE_HIDDEN (__fini_array_start = *.); KEEP (.fini_array *)) ZADRŽATI (* (SORT (.fini_array. *))) PROVIDE_HIDDEN (__fini_array_end =.); )> FLASH _sidata =.; / * Inicijalizirani podaci * / .data: AT (_sidata) (. = ALIGN (4); _sdata =.; / * Kreirajte globalni simbol na početku podataka * / * (. Data) * (. Data *). = ALIGN (4); _edata =.; / * Definirajte globalni simbol na kraju podataka * /)> RAM / * Neinicijalizirani podaci * /. = POravnati (4); .bss: (/ * Ovo se koristi za pokretanje kako bi inicijalizirao .bss sekciju * ​​/ _sbss =.; / * definirajte globalni simbol na početku bss * / __bss_start__ = _sbss; * (. bss) * (. bss *) * (COMMON). = ALIGN (4); _ebss =.; / * Definirajte globalni simbol na kraju bss * / __bss_end__ = _ebss;)> RAM PROVIDE (end = _ebss); OBAVIJESTI (_end = _ebss); PROVIDE (__ HEAP_START = _ebss); / * Odjeljak User_heap_stack, koristi se za provjeru da li je ostalo dovoljno RAM-a * / ._user_heap_stack: (. = ALIGN (4);. =. + MIN_HEAP_SIZE;. =. + MIN_STACK_SIZE;. = ALIGN (4);)> RAM / ODBACI /: (libc.a (*) libm.a (*) libgcc.a (*)))

S obzirom na l skripta za inker bit će dizajnirana posebno za kontroler STM32F100C4 (koji ima 16 KB flash i 4 KB RAM-a), ako imate drugu, morat ćete promijeniti parametre DULJINE za FLASH i RAM područja na početku datoteku (za STM32F100RB, koji je u Discoveryju: Flash 128K i RAM 8K).

Spremamo datoteku.

Postavljanje gradnje (C / C ++ Build)

Idite na Projekt-> Svojstva-> C / C ++ Build-> Postavke-> Postavke alata i počnite konfigurirati alate za izradu:

1) Ciljni prethodnik

Odabiremo za koju će Cortex jezgru kompajler raditi.

  • Procesor: cortex-m3

2) ARM Sourcery Linux GCC C kompajler -> Preprocesor

Dodajte dva define-a tako što ćete ih proći kroz prekidač -D kompajleru.

  • STM32F10X_LD_VL - definira ravnalo (pisao sam o ovoj definiciji gore)
  • USE_STDPERIPH_DRIVER - daje upute CMSIS biblioteci da treba koristiti SPL drajver

3) ARM Sourcery Linux GCC C prevodilac -> Direktoriji

Dodajte staze u uključene biblioteke.

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

Sada, na primjer, ako napišemo:

#include "stm32f10x.h

Zatim prevodilac mora prvo potražiti datoteku stm32f10x.h u direktoriju projekta (on to uvijek radi), on ga tamo neće pronaći i počet će tražiti u mapi CMSIS, put do kojeg smo naveli, pa, on će ga pronaći.

4) ARM Sourcery Linux GCC C kompajler -> Optimizacija

Uključite optimizaciju značajki i podataka

  • -ffunction-sekcije
  • -fdata-odjeljci

Kao rezultat toga, sve funkcije i elementi podataka bit će smješteni u zasebne odjeljke, a sakupljač će moći razumjeti koji se odjeljci ne koriste i jednostavno će ih izbaciti.

5) ARM Sourcery Linux GCC C prevodilac -> Općenito

Dodajte put našoj skripti povezivača: "$ (workspace_loc: / $ (ProjName) /stm32f100c4.ld)" (ili kako god to nazvali).

I postavite opcije:

  • Nemojte koristiti standardne početne datoteke - nemojte koristiti standardne početne datoteke.
  • Ukloni neiskorištene dijelove - uklonite neiskorištene dijelove

To je to, postavljanje je završeno. U REDU.

Od nastanka projekta napravili smo mnogo, a ima nekoliko stvari koje je Eclipse možda propustio, pa mu moramo reći da revidira strukturu projektne datoteke. Da biste to učinili, morate učiniti iz kontekstnog izbornika projekta Indeks -> obnoviti.

Pozdrav LED diode na STM32

Vrijeme je za stvaranje glavne datoteke projekta: File -> New -> C / C ++ -> Source File. Sljedeći. Naziv izvorne datoteke: main.c.

Kopirajte i zalijepite sljedeće u datoteku:

#include "stm32f10x.h" uint8_t i = 0; int main (void) (RCC-> APB2ENR | = RCC_APB2ENR_IOPBEN; // Omogući PORTB Periph clock RCC-> APB1ENR | = RCC_APB1ENR_TIM2EN; // Omogući TIM2 Periph sat // Onemogući JTAG za oslobađanje LEDB PIN RCCENB2 | -> MAPR | = AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Vedro PB4 i kontrola PB5 registrirati bita GPIOB-> CRL & = ~ (GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_Cull_output max kao PBF5) 10MHz GPIOB-> CRL | = GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2-> PSC = SystemCoreClock / 1000 - 1; // 1000 otkucaja / sek TIM2-> ARR = 1000; // 1 prekid / 1 sek TIM2-> DIER | = TIM_DIER_UIE; // Omogući tim2 prekid TIM2-> CR1 | = TIM_CR1_CEN; Početni broj NVIC_EnableIRQ (TIM2_IRQn); // Omogući IRQ dok (1); // Beskonačna petlja) void TIM2_IRQHandler (void) (TIM2-> SR & = ~ TIM_SR_UIF ; // Očisti UIF zastavu ako (1 == (i ++) & 0x1)) (GPIOB-> BSRR = GPIO_BSRR_BS4; // Postavi PB4 bit GPIOB-> BSRR = GPIO_BSRR_BR5; // Poništi PB5 bit) drugo (GPIOB-> BSRR = GPIO_BSRR_B S5; // Postavi PB5 bit GPIOB-> BSRR = GPIO_BSRR_BR4; // Poništi PB4 bit))

Iako smo uključili SPL knjižnice, ovdje se nije koristio. Svi pozivi u polja poput RCC-> APB2ENR u potpunosti su opisani u CMSIS-u.

Možete pokrenuti Project -> Build All. Ako je sve uspjelo, tada bi se datoteka stm32_ld_vl.hex trebala pojaviti u mapi Debug projekta. Automatski je generiran iz elf ugrađenim alatima. Treperimo datoteku i vidimo kako LED diode trepere frekvencijom od jednom u sekundi:

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

Naravno, umjesto / home / user / workspace / morate unijeti svoj put do radnog prostora.

Za STM32VLDiscovery

Kod se malo razlikuje od onog koji sam dao gore za moju ploču za otklanjanje pogrešaka. Razlika leži u iglama na kojima "vise" LED diode. Ako su u mojoj ploči to bili PB4 i PB5, onda su u Discoveryju bili PC8 i PC9.

#include "stm32f10x.h" uint8_t i = 0; int main (void) (RCC-> APB2ENR | = RCC_APB2ENR_IOPCEN; // Omogući PORTC Periph sat RCC-> APB1ENR | = RCC_APB1ENR_TIM2EN; // Omogući TIM2 Periph sat // Očisti PC8 i PC9 bitove kontrolnog registra GPIOC-> (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Konfigurirajte PC8 i PC9 kao Push Pull izlaz na maksimalno 10 MHz GPIOC-> CRH | = GPIO_CRH_MODE8_0 | GPIO_CRH_MODE8_0 | GPIO_CRH_CNF9); // Konfigurirajte PC8 i PC9 kao Push Pull izlaz na maks. 10 MHz GPIOC-> CRH | = GPIO_CRH_MODE8_0 | GPIO_CRH_MODE8_0 | GPIO_CRH_CRH> - GPIO_1 sec. Prekid / sec (1000/100) TIM2-> DIER | = TIM_DIER_UIE; // Omogući tim2 prekid TIM2-> CR1 | = TIM_CR1_CEN; // Broj pokretanja NVIC_EnableIRQ (TIM2_IRQn); // Omogući IRQ dok (1); // Beskonačnost petlja) void TIM2_IRQHandler (void) (TIM2-> SR & = ~ TIM_SR_UIF; // Očisti UIF zastavu if (1 == (i ++ & 0x1)) (GPIOC-> BSRR = GPIO_BSRR_BS8 ; // Postavi PC8 bit GPIOC- > BSRR = GPIO_BSRR_BR9; // Poništi PC9 bit) drugo (GPIOC-> BSRR = GPIO_BSRR_BS9; // Postavi PC9 bit GPIOC-> BSRR = GPIO_BSRR_BR8; // Reset PC8 bit))

U sustavu Windows možete fleširati rezultirajući heksadecimalni (/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) pomoću uslužnog programa iz ST.

Pa, pod linuxom, st-flash uslužni program. ALI!!! Uslužni program ne hakuje heksadecimalni format Intel HEX formata (koji se generira prema zadanim postavkama), stoga je iznimno važno odabrati binarni format u postavkama za izradu Flash slike:

Datotečna ekstenzija se neće promijeniti (hex će ostati onakav kakav je bio), ali će se promijeniti format datoteke. I tek nakon toga možete izvršiti:

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

Usput, o proširenju i formatu: obično su binarne datoteke označene nastavkom .bin, dok se Intel HEX datoteke nazivaju nastavkom .hex. Razlika u ova dva formata je prije tehnička nego funkcionalna: binarni format sadrži samo bajtove instrukcija i podataka koje će programer jednostavno zapisati u kontroler "kao što jesu". IntelHEX, s druge strane, nema binarni format, već tekstualni: potpuno isti bajtovi su podijeljeni u 4 bita i predstavljeni su znak po znak u ASCII formatu, a koriste se samo znakovi 0-9, AF (bin i heksadecimalni su brojevni sustavi s više baza, odnosno 4 bita u binu mogu se predstaviti kao jedna znamenka u hex). Dakle, ihex format je više od 2 puta veći od obične binarne datoteke (svaka 4 bita su zamijenjena bajtom + prijelom reda radi lakšeg čitanja), ali se može čitati u običnom uređivaču teksta. Stoga, ako ćete ovu datoteku nekome slati, ili je koristiti u drugim programima, preporučljivo je preimenovati je u stm32_md_vl.bin kako ne biste zavarali one koji će pogledati njezino ime.

Dakle, konfigurirali smo firmware build za stm32. Sljedeći put ću vam reći kako

Prilikom izrade svoje prve aplikacije na STM32 mikrokontroleru, postoji nekoliko načina. Prvi, klasični, uzimamo točan opis kontrolera na web stranici www.st.com, koji se pojavljuje pod nazivom "Referentni priručnik" i čitamo opis perifernih registara. Zatim ih pokušavamo zapisati i vidjeti kako periferne jedinice rade. Čitanje ovog dokumenta vrlo je korisno, ali u prvoj fazi svladavanja mikrokontrolera, možete to odbiti, čudno. STMicroelectronics inženjeri su napisali biblioteku upravljačkih programa za standardne periferije. Štoviše, napisali su mnoge primjere korištenja ovih drajvera, koji mogu svesti programiranje vaše aplikacije na pritiskanje tipki Ctrl + C i Ctrl + V, nakon čega slijedi mala izmjena primjera korištenja drajvera prema vašim potrebama. Stoga je povezivanje biblioteke perifernih upravljačkih programa s vašim projektom druga metoda izgradnje aplikacije. Osim brzine pisanja, postoje i druge prednosti ove metode: svestranost koda i korištenje drugih vlasničkih knjižnica kao što su USB, Ethernet, kontrola pogona, itd., koje su navedene u izvornom kodu i pomoću standardni periferni drajver. Postoje i nedostaci ove metode: Gdje se možete snaći s jednom linijom koda, standardni upravljački program periferije STM32 će napisati 10. Sama biblioteka periferije također je dostupna u obliku izvornih datoteka, tako da možete pratiti koji dio registrirati promjene određene funkcije. Ako želite, možete prijeći s druge metode pisanja programa na prvu komentirajući dio koda koji samostalno koristi standardnu ​​biblioteku, koja izravno kontrolira periferni registar. Kao rezultat ove akcije, dobit ćete u brzini kontrole, količini RAM-a i ROM-a, a izgubiti u univerzalnosti koda. U svakom slučaju, inženjeri tvrtke Promelectronica preporučuju korištenje knjižnice standardnih perifernih uređaja barem u prvoj fazi.

Najveće poteškoće očekuju razvojnog programera pri povezivanju knjižnice sa svojim projektom. Ako ne znate kako to učiniti, možete potrošiti puno vremena na ovaj događaj, što je u suprotnosti sa samom idejom korištenja gotovog drajvera. Materijal je posvećen povezivanju standardne biblioteke s bilo kojom obitelji STM32.

Svaka obitelj STM32 ima svoju standardnu ​​perifernu biblioteku. To je zbog činjenice da je sama periferija drugačija. Na primjer, periferija STM32L kontrolera ima funkciju štednje energije kao jedan od zadataka, što podrazumijeva dodavanje upravljačkih funkcija. Klasični primjer može se smatrati ADC-om, koji u STM32L ima mogućnost isključivanja hardvera, u nedostatku naredbe za pretvorbu dulje vrijeme - jedna od posljedica zadatka za uštedu energije. ADC kontrolera obitelji STM32F nemaju ovu funkciju. Zapravo, zbog hardverske razlike na periferiji, imamo različite biblioteke drajvera. Osim očite razlike u funkcijama kontrolera, dolazi do poboljšanja na periferiji. Dakle, periferni uređaji kontrolora obitelji koji su kasnije objavljeni mogu biti promišljeniji i praktičniji. Na primjer, periferni uređaji STM32F1 i STM32F2 kontrolera imaju razlike u upravljanju. Po mišljenju autora, upravljanje periferijama STM32F2 je praktičnije. I to je razumljivo zašto: obitelj STM32F2 objavljena je kasnije i to je omogućilo programerima da uzmu u obzir neke nijanse. Sukladno tome, za ove obitelji - individualne periferne kontrolne knjižnice. Ideja iza gore navedenog je jednostavna: na stranici mikrokontrolera koji ćete koristiti nalazi se odgovarajuća periferna biblioteka za njega.

Unatoč razlici u periferiji u obiteljima, vozači kriju 90% razlika u sebi. Na primjer, gore spomenuta funkcija ugađanja ADC-a izgleda isto za sve obitelji:

void ADC_Init (ADC_Nom, ADC_Param),

gdje je ADC_Nom broj ADC-a u obliku ADC1, ADC2, ADC3, itd.

ADC_Param - pokazivač na strukturu podataka, kako ADC treba biti konfiguriran (od čega početi, koliko kanala digitalizirati, treba li to izvoditi ciklički, itd.)

10% razlika između obitelji, u ovom primjeru, koje će se morati ispraviti pri prelasku iz jedne STM32 obitelji u drugu, skriveno je u ADC_Param strukturi. Broj polja u ovoj strukturi može varirati ovisno o obitelji. Opći dio ima istu sintaksu. Dakle, prijevod aplikacije za jednu STM32 obitelj, napisanu na temelju standardnih perifernih biblioteka u drugu, prilično je jednostavan. Što se tiče univerzalizacije rješenja na mikrokontrolerima, STMicroelectronics je neodoljiva!

Dakle, preuzeli smo biblioteku za korišteni STM32. Što je sljedeće? Zatim moramo stvoriti projekt i povezati potrebne datoteke s njim. Razmotrimo stvaranje projekta koristeći razvojno okruženje IAR Embedded Workbench kao primjer. Pokrećemo razvojno okruženje i idemo na karticu "Projekt", odabiremo stavku "Kreiraj projekt" za izradu projekta:

U novom projektu koji se pojavi unesite postavke tako da zadržite pokazivač iznad naziva projekta, pritisnete desnu tipku miša i odaberete "Opcije" u padajućem izborniku:

RAM i ROM memorijska područja:

Kada kliknete gumb "Spremi", okolina će ponuditi pisanje nove datoteke opisa kontrolera u mapu projekta. Autor preporučuje stvaranje pojedinačne * .icp datoteke za svaki projekt i pohranjivanje u mapu projekta.

Ako ćete otklanjati pogreške u svom projektu u krugu, što je preporučljivo, tada unosimo vrstu debuggera koji ćemo koristiti:

Na kartici odabranog debuggera označavamo sučelje za povezivanje debuggera (u našem slučaju je odabran ST-Link) na kontroler:



Od sada je naš projekt bez knjižnica spreman za kompajliranje i učitavanje u kontroler. U drugim okruženjima kao što su Keil uVision4, Resonance Ride7 itd., morat ćete učiniti isto.

Ako upišete redak u datoteku main.c:

#include "stm32f10x.h" ili

#include "stm32f2xx.h" ili

#include "stm32f4xx.h" ili

#include "stm32l15x.h" ili

#include "stm32l10x.h" ili

#include "stm32f05x.h"

navodeći lokaciju ove datoteke ili kopiranje ove datoteke u mapu projekta, tada će neka memorijska područja biti povezana s perifernim registrima odgovarajuće obitelji. Sama datoteka nalazi se u mapi standardne periferne biblioteke u odjeljku: \ CMSIS \ CM3 \ DeviceSupport \ ST \ STM32F10x (ili sličan naziv za druge obitelji). Od sada, adresu perifernog registra kao broj zamjenjujete njenim imenom. Čak i ako ne namjeravate koristiti funkcije standardne biblioteke, preporuča se napraviti takvu vezu.

Ako ćete koristiti prekide u svom projektu, preporuča se povezati početnu datoteku s nastavkom * .s, koja se nalazi duž putanje \ CMSIS \ CM3 \ DeviceSupport \ ST \ STM32F10x \ startup \ iar ili slično za druge obitelji. Važno je napomenuti da je datoteka različita za svako okruženje. U skladu s tim, ako koristimo IAR EWB, tada moramo uzeti datoteku iz mape IAR. To je zbog malo drugačije sintakse okruženja. Stoga, kako bi projekt odmah krenuo, STMicroelectronics inženjeri napisali su nekoliko opcija za pokretanje datoteka za nekoliko najpopularnijih razvojnih okruženja. Većina obitelji STM32 ima jednu datoteku. Obitelj STM32F1 ima nekoliko datoteka za pokretanje:

  • startup_stm32f10x_cl.s - za STM32F105 / 107 mikrokontrolere
  • startup_stm32f10x_xl.s - za STM32F101 / STM32F103 mikrokontrolere 768 kb i više
  • startup_stm32f10x_hd.s - za STM32F101 / STM32F103 mikrokontrolere s 256-512 kb Flash memorije
  • startup_stm32f10x_md.s - za STM32F101 / STM32F102 / STM32F103 mikrokontrolere sa 64-128 kb Flash memorije
  • startup_stm32f10x_ld.s - za STM32F101 / STM32F102 / STM32F103 mikrokontrolere s Flash memorijom manjom od 64 kb
  • startup_stm32f10x_hd_vl.s za mikrokontrolere STM32F100 s 256-512 kb flash memorije
  • startup_stm32f10x_md_vl.s za mikrokontrolere STM32F100 sa 64-128 kb Flash memorije
  • startup_stm32f10x_ld_vl.s za mikrokontrolere STM32F100 s 32 KB Flash memorije ili manje

Dakle, ovisno o obitelji, podobitelji i razvojnom okruženju, dodajte datoteku za pokretanje projektu:

Ovdje se ispostavlja da je mikrokontroler kada se program pokrene. Prekid sekvencijalno poziva funkciju SystemInit (), a zatim __iar_program_start. Druga funkcija nula ili upisuje unaprijed definirane vrijednosti globalnih varijabli, nakon čega se prebacuje na glavni () korisnički program. Funkcija SystemInit () podešava sat mikrokontrolera. Ona je ta koja daje odgovore na pitanja:

  • Trebam li prijeći na vanjski kvarc (HSE)?
  • Kako pomnožiti frekvenciju iz HSI / HSE?
  • Trebate li povezati red za preuzimanje naredbi?
  • Koliko je kašnjenja potrebno pri učitavanju naredbe (zbog male brzine Flash memorije)
  • Kako podijeliti taktiranje perifernih sabirnica?
  • Trebate li kod staviti u vanjski RAM?

Funkcija SystemInit () može se ručno napisati u vašem projektu. Ako ovu funkciju uredite kao praznu, tada će kontroler raditi na internom RC oscilatoru s frekvencijom od oko 8 MHz (ovisno o vrsti obitelji). Opcija 2 - povežite datoteku system_stm32f10x.c s projektom (ili sličnog imena, ovisno o vrsti korištene obitelji), koja se nalazi u biblioteci duž puta: Libraries \ CMSIS \ CM3 \ DeviceSupport \ ST \ STM32F10x. Ova datoteka sadrži funkciju SystemInit (). Obratite pažnju na frekvenciju vanjskog kristala HSE_VALUE. Ovaj je parametar postavljen u datoteci zaglavlja stm32f10x.h. Standardna vrijednost je 8 i 25MHz, ovisno o obitelji STM32. Glavni zadatak funkcije SystemInit () je prebaciti taktiranje na vanjski kristal i pomnožiti ovu frekvenciju na određeni način. Što se događa ako je vrijednost HSE_VALUE navedena kao 8MHz, jezgra mora imati takt od 72MHz, ali u stvari postoji kristal od 16MHz na ploči? Kao rezultat takvih netočnih radnji, jezgra će dobiti takt od 144 MHz, što može biti iznad zajamčenog rada sustava na STM32. Oni. kada povezujete datoteku system_stm32f10x.c, morat ćete navesti vrijednost HSE_VALUE. Sve to znači da datoteke system_stm32f10x.c, system_stm32f10x.h i stm32f10x.h (ili slične po imenu za druge obitelji) moraju biti individualne za svaki projekt. I

STMicroelectronics inženjeri stvorili su alat za konfiguraciju sata koji vam omogućuje ispravnu konfiguraciju takta sustava. Ovo je Excel datoteka koja generira datoteku system_stm32xxx.c (sličnu po imenu za danu obitelj obitelji) nakon navođenja ulaznih i izlaznih parametara sustava. Pogledajmo kako to radi koristeći obitelj STM32F4 kao primjer.

Opcije su interni RC oscilator, unutarnji RC oscilator s množenjem frekvencije ili vanjski kristal s množenjem frekvencije. Nakon odabira izvora takta, unesite parametre željene konfiguracije sustava, kao što su ulazna frekvencija (kada koristite vanjski kristal), frekvencija takta jezgre, djelitelj frekvencije takta periferne sabirnice, rad međuspremnika za dohvaćanje naredbi i ostalo . Klikom na gumb "Generiraj" dobivamo prozor


Povezivanje datoteke system_stm32f4xx.c i njezinih analoga zahtijevat će povezivanje još jedne datoteke standardne periferne biblioteke. Za upravljanje satom postoji cijeli skup funkcija koje se pozivaju iz datoteke system_stm32xxxxxx.c. Te se funkcije nalaze u datoteci stm32f10x_rcc.c i njenom zaglavlju. Sukladno tome, kada povezujete datoteku system_stm32xxxxxx.c s projektom, trebate spojiti stm32f10x_rcc.c, inače će povezivač okruženja prijaviti odsutnost opisa funkcija s imenom RCC_xxxxxxx. Navedena datoteka nalazi se u perifernoj biblioteci pod stazom: Libraries \ STM32F10x_StdPeriph_Driver \ src, i njeno zaglavlje \ Libraries \ STM32F10x_StdPeriph_Driver \ inc.

Datoteke zaglavlja perifernog drajvera povezane su u datoteci stm32f10x_conf.h, na koju upućuje stm32f10x.h. Datoteka stm32f10x_conf.h jednostavno je skup datoteka zaglavlja za specifične upravljačke programe periferije kontrolera koji će biti uključeni u projekt. U početku su sva zaglavlja "#include" označena kao komentari. Povezivanje datoteke zaglavlja periferije sastoji se od uklanjanja komentara iz odgovarajućeg naziva datoteke. U našem slučaju, ovo je redak #include "stm32f10x_rcc.h". Očito je da je datoteka stm32f10x_conf.h individualna za svaki projekt, budući da različiti projekti koriste različite periferne uređaje.

I posljednja stvar. Potrebno je navesti nekoliko direktiva za predprocesor kompajlera i putove do datoteka zaglavlja.



Putovi do datoteka zaglavlja mogu biti različiti, ovisno o lokaciji periferne biblioteke u odnosu na mapu projekta, ali prisutnost "USE_STDPERIPH_DRIVER" je obavezna pri povezivanju perifernih upravljačkih programa standardne biblioteke.

Dakle, standardnu ​​biblioteku smo povezali s projektom. Štoviše, na projekt smo spojili jedan od standardnih perifernih drajvera koji kontrolira sat sustava.

Saznali smo kako struktura knjižnice izgleda iznutra, a sada par riječi o tome kako izgleda izvana.



Dakle, povezivanje datoteke zaglavlja stm32f10x.h u aplikaciji podrazumijeva povezivanje drugih datoteka zaglavlja i kodnih datoteka. Neki od prikazanih na slici su gore opisani. Nekoliko riječi o ostalom. Datoteke STM32F10x_PPP.x su datoteke upravljačkog programa periferije. Primjer povezivanja takve datoteke prikazan je gore, ovo je RCC - periferni uređaji za upravljanje satom sustava. Ako želimo spojiti drajvere drugih perifernih uređaja, tada se naziv povezanih datoteka dobiva zamjenom "PPP" s nazivom periferije, na primjer, ADC - STM32F10x_ADC.s, ili I/O portovi STM32F10x_GPIO.s, ili DAC - STM32F10x_DAC.s. Općenito, intuitivno je jasno koju datoteku treba spojiti prilikom povezivanja određene periferije. Datoteke "misc.c", "misc.h" su u osnovi iste STM32F10x_PPP.x, samo što kontroliraju kernel. Na primjer, postavljanje vektora prekida, koji je ugrađen u kernel, ili upravljanje SysTick timerom, koji je dio jezgre. Datoteke xxxxxxx_it.c opisuju vektore nemaskiranih prekida kontrolera. Mogu se nadopuniti perifernim vektorima prekida. Datoteka core_m3.h opisuje jezgru CortexM3. Ova jezgra je standardizirana i može se naći u mikrokontrolerima drugih proizvođača. Za univerzalizaciju na više platformi, STMicroelectronics je radio na stvaranju zasebne knjižnice CortexM jezgre, nakon čega ju je ARM standardizirao i proširio na druge proizvođače mikrokontrolera. Tako će prijelaz na STM32 s kontrolera drugih proizvođača s CortexM jezgrom biti malo lakši.

Dakle, standardnu ​​perifernu biblioteku možemo povezati s bilo kojom obitelji STM32. Oni koji su to naučili dobit će nagradu: vrlo jednostavno programiranje mikrokontrolera. Knjižnica, osim upravljačkih programa u obliku izvornih datoteka, sadrži mnoge primjere korištenja perifernih uređaja. Na primjer, razmislite o stvaranju projekta koji uključuje izlaze za usporedbu vremena. Tradicionalnim pristupom pomno ćemo proučiti opis registara ove periferije. Ali sada možemo proučiti tekst programa za trčanje. Idemo u mapu s primjerima standardnih perifernih uređaja, koja se nalazi duž putanje ProjectSTM32F10x_StdPeriph_Examples. Ovdje ćete pronaći primjere mapa s nazivima korištenih perifernih uređaja. Idemo u mapu "TIM". Tajmeri u STM32 imaju mnogo funkcija i postavki, pa je nemoguće demonstrirati s jednim primjerom mogućnosti kontrolera. Stoga, unutar navedenog imenika postoji mnogo primjera korištenja mjerača vremena. Zanima nas generiranje PWM signala pomoću timera. Idemo u mapu "7PWM_Output". Unutar se nalazi opis programa na engleskom i skup datoteka:

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

Ako projekt nema prekida, tada se sadržaj u potpunosti nalazi u datoteci main.c. Te datoteke kopiramo u direktorij projekta. Nakon što smo sastavili projekt, dobit ćemo program za STM32 koji će konfigurirati timer i I/O portove za generiranje 7 PWM signala iz timera 1. Zatim možemo prilagoditi već napisani kod za naš zadatak. Na primjer, smanjite broj PWM signala, promijenite radni ciklus, smjer brojanja itd. Funkcije i njihovi parametri dobro su opisani u datoteci stm32f10x_stdperiph_lib_um.chm. Nazivi funkcija i njihovi parametri lako se povezuju s njihovom namjenom za one koji malo znaju engleski. Radi jasnoće predstavljamo dio koda uzetog primjera:

/ * Konfiguracija vremenske baze * / TIM_TimeBaseStructure.TIM_Prescaler = 0; // nema prethodnog dodjeljivanja impulsa za brojanje (16-bitni registar) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // smjer odbrojavanja prema gore TIM_TimeBaseStructure.TIM_Period = TimerPeriod; // broji do vrijednosti TimerPeriod (konstanta u programu) TIM_TimeBaseStructure.TIM_ClockDivision = 0; // nema brojanja unaprijed dodjele TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // brojač prekoračenja za generiranje događaja (ne koristi se u programu) TIM_TimeBaseInit (TIM1, & TIM_TimeBaseStructure); // unos vrijednosti TimeBaseStructure u registre timera 1 (unos podataka u ovu // varijablu je iznad) / * Konfiguracija kanala 1, 2, 3 i 4 u PWM modu * / // postavljanje PWM izlaza TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; // PWM2 način rada TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // omogućimo izlaz signala PWM timera TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // omogućiti besplatni izlaz PWM timera TIM_OCInitStructure.TIM_Pulse = Channel1Pulse; // širina impulsa Channel1Pulse - konstanta u programu TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // postavljanje izlaznog polariteta TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; // postavljanje polariteta komplementarnog izlaza TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; // postavlja sigurno stanje PWM izlaza TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset; // postavlja sigurno stanje komplementarnog PWM izlaza TIM_OC1Init (TIM1, & TIM_OCInitStructure); // unesite vrijednosti varijable TIM_OCInitStructure u PWM registre kanala 1 // timer1 TIM_OCInitStructure.TIM_Pulse = Channel2Pulse; // mijenjamo širinu impulsa u varijabli OCInitStructure i unosimo je u TIM_OC2Init (TIM1, & TIM_OCInitStructure); // PWM registri kanala 2 timer1 TIM_OCInitStructure.TIM_Pulse = Channel3Pulse; // mijenjamo širinu impulsa u varijabli OCInitStructure i unosimo je u TIM_OC3Init (TIM1, & TIM_OCInitStructure); // PWM registri kanala 3 timer1 TIM_OCInitStructure.TIM_Pulse = Channel4Pulse; // mijenjamo širinu impulsa u varijabli OCInitStructure i unosimo je u TIM_OC4Init (TIM1, & TIM_OCInitStructure); // registrira PWM kanal 4 timer1 / * Omogućavanje brojača TIM1 * / TIM_Cmd (TIM1, ENABLE); // start timer1 / * Omogući glavni izlaz TIM1 * / TIM_CtrlPWMO izlazi (TIM1, ENABLE); // omogući usporedbu izlaza timera 1

Na desnoj strani, autor je ostavio komentar na ruskom jeziku za svaki redak programa. Ako otvorite isti primjer u opisu funkcija biblioteka stm32f10x_stdperiph_lib_um.chm, vidjet ćemo da svi korišteni parametri funkcije imaju vezu na vlastiti opis, gdje će biti naznačene njihove moguće vrijednosti. Same funkcije također imaju poveznicu na vlastiti opis i izvorni kod. Ovo je vrlo korisno jer znajući što funkcija radi, možemo pratiti kako to radi, koji bitovi perifernih registara i kako utječe. Ovo je, prvo, još jedan izvor informacija za ovladavanje kontrolerom, koji se temelji na praktičnoj upotrebi kontrolera. Oni. prvo riješite tehnički problem, a zatim proučite samo rješenje. Drugo, ovo je polje za optimizaciju programa za one koji nisu zadovoljni s knjižnicom u smislu brzine rada i količine koda.



Dobro, zasad sve ide dobro, ali samo su žarulje i gumbi spremni. Sada je vrijeme da se uhvatite u koštac s težim perifernim uređajima - USB, UART, I2C i SPI. Odlučio sam početi s USB-om - ST-Link debugger (čak i onaj pravi iz Discoveryja) tvrdoglavo nije želio otklanjati pogreške na mojoj ploči, pa mi je ispravljanje pogrešaka na ispisima putem USB-a jedina dostupna metoda za otklanjanje pogrešaka. Možete, naravno, kroz UART, ali ovo je hrpa dodatnih žica.

Opet sam otišao daleko - generirao sam odgovarajuće predloške u STM32CubeMX, dodao USB Middleware iz STM32F1Cube paketa u svoj projekt. Vi samo trebate omogućiti USB taktiranje, definirati odgovarajuće rukovaoce USB prekidima i dotjerati male stvari. Uglavnom sam sve bitne postavke USB modula kopirao sa STM32GENERIC, osim što sam malo srezao dodjelu memorije (oni su koristili malloc, a ja statičku).

Evo par zanimljivih komada koje sam ponio sa sobom. Na primjer, kako bi domaćin (računalo) shvatio da je nešto na njega spojeno, uređaj "žonglira" USB D + linijom (koja je spojena na pin A12). Ugledavši takvog domaćina, počinje ispitivati ​​uređaj tko je, koja sučelja može, kojom brzinom želi komunicirati itd. Ne razumijem zašto bi to trebalo učiniti prije inicijalizacije USB-a, ali u stm32duino to se radi na isti način.

Žonglirajte USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate () (// Pokretanje PA12 klin GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed = GPIO_SPEED_FREQ_LOW, HALInGPIO na pinInit (enrate uređaji) HAL_GPIO_WritePin (GPIOA, GPIO_PIN_12, GPIO_PIN_RESET) za (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); }


Još jedna zanimljiva točka je podrška za stm32duino bootloader. Da biste učitali firmware, prvo morate ponovno pokrenuti kontroler u bootloader. Najlakši način je pritisnuti gumb za resetiranje. Ali da bi bilo praktičnije, možete učiti iz iskustva arduina. Kad su stabla bila mlada, AVR kontroleri još nisu imali USB podršku na ploči, na ploči je bio USB-UART adapter. DTR UART signal je povezan s resetiranjem mikrokontrolera. Kada host pošalje DTR signal, mikrokontroler se ponovno učitava u bootloader. Radi sa armiranim betonom!

U slučaju korištenja USB-a, emuliramo samo COM port. U skladu s tim, morate se sami ponovno pokrenuti u bootloader. stm32duino loader, osim DTR signala, za svaki slučaj još uvijek očekuje posebnu magičnu konstantu (1EAF - referenca na Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t * pbuf, uint16_t duljina) (... case CDC_SET_CONTROL_LINE_STATE: dtr_pin ++; // DTR pin je omogućen prekid; ... static int8_t CDC_Receive_FS (uint8_t * je Bu magic bypack * 1EAF" koji MCU stavlja u bootloader. * / ako (* Len> = 4) (/ ** * Provjerite sadrži li dolazni niz niz "1EAF". * Ako da, provjerite je li DTR postavljen, da biste stavili MCU u bootloader način rada. * / if (dtr_pin> 3) (if ((Buf == "1") && (Buf == "E") && (Buf == "A") && (Buf == "F ")) (HAL_NVIC_SystemReset ();) dtr_pin = 0;)) ...)

Stražnja strana: MiniArduino

Općenito, USB radi. Ali ovaj sloj radi samo s bajtovima, ne i nizovima. Stoga ispisi za otklanjanje pogrešaka izgledaju ovako ružno.

CDC_Transmit_FS ((uint8_t *) "Ping \ n", 5); // 5 je strlen (“Ping”) + nula bajta
Oni. uopće nema podrške za formatirani izlaz - niti ispisujete broj, niti sastavljate niz od komada. Pojavljuju se sljedeće opcije:

  • Uvrnite klasični printf. Čini se da opcija nije loša, ali vuče na + 12kb firmware-a (već sam nekako slučajno od sebe nazvao sprintf)
  • Kopajte vlastitu implementaciju printf-a u svoju zalihu. Jednom sam pisao pod AVR-om, kao da je ova implementacija bila manja.
  • Uvrnite klasu Print iz arduina u implementaciju STM32GENERIC
Odabrao sam potonju opciju jer se kod knjižnice Adafruit GFX također oslanja na Print, pa ga još uvijek trebam zašrafiti. Osim toga, kod STM32GENERIC mi je već bio na dohvat ruke.

Napravio sam MiniArduino direktorij u svom projektu kako bih tamo stavio minimalnu potrebnu količinu koda kako bih implementirao dijelove arduino sučelja koji su mi potrebni. Počeo sam kopirati jednu po jednu datoteku i vidjeti koje su druge ovisnosti potrebne. Tako sam dobio kopiju klase Print i nekoliko uvezujućih datoteka.

Ali ovo nije dovoljno. I dalje ste morali nekako povezati klasu Print s USB funkcijama (na primjer, CDC_Transmit_FS ()). Da bismo to učinili, morali smo povući klasu SerialUSB. Povukao je klasu Stream i dio GPIO inicijalizacije. Sljedeći korak je bio povezivanje UART-a (na njega imam spojen GPS). Tako sam također povukao klasu SerialUART, koja je povukla još jedan sloj periferne inicijalizacije iz STM32GENERIC.

Općenito sam se našao u sljedećoj situaciji. Kopirao sam gotovo sve datoteke sa STM32GENERIC na svoj MiniArduino. Imao sam i svoju kopiju USB i FreeRTOS knjižnica (trebalo je biti više kopija HAL-a i CMSIS-a, ali sam bio previše lijen). U isto vrijeme mjesec i pol obilježavam vrijeme – spajam i odspajam razne komade, ali pritom nisam napisao niti jedan redak nove šifre.

Postalo je jasno da moja prvotna ideja o preuzimanju kontrole nad cijelim dijelom sustava nije bila baš uspješna. Svejedno, dio inicijalizacijskog koda živi u STM32GENERIC i, čini se, tamo mu je ugodnije. Naravno, mogli biste hakirati sve ovisnosti i napisati svoje vlastite klase omotača za svoje zadatke, ali to bi me usporilo još mjesec dana - ovaj kod još uvijek treba otkloniti greške. Naravno, bilo bi super za vaš CSR, ali morate ići naprijed!

Općenito, izbacio sam sve duplikate knjižnica i gotovo cijeli sloj sustava i vratio se na STM32GENERIC. Ovaj se projekt razvija prilično dinamično - nekoliko obrada dnevno je stabilno. Osim toga, tijekom ovih mjesec i pol dana, dosta sam učio, čitao većinu STM32 Referentnog priručnika, gledao kako su napravljene HAL knjižnice i STM32GENERIC omoti, napredovao u razumijevanju USB deskriptora i perifernih uređaja mikrokontrolera. Općenito, sada sam bio puno sigurniji u STM32GENERIC nego prije.

Revers: I2C

Međutim, tu mojim avanturama nije bio kraj. Još su postojali UART i I2C (moj zaslon živi tamo). S UART-om je sve bilo dovoljno jednostavno. Upravo sam uklonio dinamičku dodjelu memorije, a kako neiskorišteni UART-ovi ne bi pojeli ovu memoriju, jednostavno sam ih komentirao.

Ali implementacija I2C u STM32GENERIC zasadio kaku. Štoviše, bilo je vrlo zanimljivo, ali mi je trebalo barem 2 večeri. Pa, ili dao 2 večeri teškog otklanjanja pogrešaka na otiscima - ovo je s koje strane gledati.

Općenito, implementacija prikaza nije započela. U već tradicionalnom stilu - to jednostavno ne ide i to je to. Što ne radi nije jasno. Čini se da je knjižnica samog zaslona (Adafruit SSD1306) testirana na prethodnoj implementaciji, ali i dalje ne treba isključiti smetnje bugova. Sumnja pada na HAL i implementaciju I2C iz STM32GENERIC.

Za početak, komentirao sam sav prikaz i I2C kod i napisao I2C inicijalizaciju bez ikakvih knjižnica, u čistom HAL-u

I2C inicijalizacija

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.Instanca = 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);


Izbacio sam stanje registara odmah nakon inicijalizacije. Isti sam dump napravio u radnoj verziji na stm32duino. Ovo sam dobio (s komentarima za sebe)

Dobro (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Omogućen prekid pogreške, 36 Mhz
40005408: 0 0 0 0 - I2C_OAR1: nula vlastite adrese

40005410: 0 0 0 AF - I2C_DR: registar podataka

40005418: 0 0 0 0 - I2C_SR2: statusni registar

Loše (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Omogućavanje periferije
40005404: 0 0 0 24 - I2C_CR2: 36 MHz
40005408: 0 0 40 0 ​​- I2C_OAR1: !!! Neopisani bit u skupu registra adrese
4000540C: 0 0 0 0 - I2C_OAR2: Registar vlastitih adresa
40005410: 0 0 0 0 - I2C_DR: registar podataka
40005414: 0 0 0 0 - I2C_SR1: statusni registar
40005418: 0 0 0 2 - I2C_SR2: postavljen bit zauzet
4000541C: 0 0 80 1E - I2C_CCR: način rada od 400 kHz
40005420: 0 0 0 B - I2C_TRISE

Prva velika razlika je u tome što je bit 14 postavljen u registru I2C_OAR1. Ovaj bit uopće nije opisan u podatkovnoj tablici i spada u rezervirani odjeljak. Istina, s tim da tamo još trebate napisati jedinicu. Oni. ovo je greška u libmapleu. Ali pošto tamo sve funkcionira, onda to nije problem. Kopanje dalje.

Druga razlika je skup bitova zauzetosti. Isprva tome nisam pridavao nikakvu važnost, ali prije svega ću reći - on je bio taj koji je signalizirao problem! .. Ali prije svega.

Bio sam na koljenima i kvario inicijalizacijski kod bez ikakvih knjižnica.

Inicijalizacija prikaza

void sendCommand (I2C_HandleTypeDef * ručka, uint8_t cmd) (SerialUSB.print ("Naredba za slanje"); SerialUSB.println (cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Master<<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); }


Nakon određenog truda, ovaj kod mi je uspio (u ovom slučaju crtao je pruge). Dakle, problem je u STM32GENERIC I2C sloju. Počeo sam malo po malo brisati svoj kod, zamjenjujući ga odgovarajućim dijelovima iz biblioteke. Ali čim sam prebacio kod za inicijalizaciju pina iz svoje implementacije u onaj za knjižnicu, sav I2C prijenos počeo je padati zbog isteka.

Tada sam se sjetio zauzetosti i pokušao shvatiti kada se to dogodi. Pokazalo se da se zastavica zauzetosti pojavljuje čim inicijalizacijski kod uključi I2c taktiranje. Oni. Modul se uključuje i ne radi odmah. Zanimljiv.

Pad u inicijalizaciju

uint8_t * pv = (uint8_t *) 0x40005418; // Registar I2C_SR2. Traži se oznaka ZAUZETA SerialUSB.print ("40005418 ="); SerialUSB.println (* pv, 16); // Ispisuje 0 __HAL_RCC_I2C1_CLK_ENABLE (); SerialUSB.print ("40005418 ="); SerialUSB.println (* pv, 16); // Ispisi 2


Iznad ovog koda je samo inicijalizacija pinova. Pa, što učiniti - pokrivamo debug s otiscima kroz liniju i tamo

Inicijalizacija pinova STM32GENERIC

void stm32AfInit (const lista stm32_af_pin_list_type, int veličina, const void * primjer, GPIO_TypeDef * luka uint32_t igla, uint32_t način, uint32_t pull) (... GPIO_InitTypeDef GPIO_InitStruct; GPIO_Inull = pinStruct;. = GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH, HAL_GPIO_Init (port, & GPIO_InitStruct); ...)


Ali ovdje je loša sreća - GPIO_InitStruct je ispravno popunjen. Samo moj radi, ali ovaj ne. Stvarno, mistika!!! Sve je kao u udžbeniku, ali ništa ne radi. Proučavao sam kod knjižnice red po red u potrazi za nečim sumnjivim. Na kraju sam naišao na ovaj kod (poziva funkciju iznad)

Još jedan dio inicijalizacije

void stm32AfI2CInit (const I2C_TypeDef * instanca,…) (stm32AfInit (chip_af_i2c_sda,…); stm32AfInit (chip_af_i2c_scl,…);)


Vidite li bug u njemu? I ona je! Čak sam i uklonio nepotrebne parametre kako bi problem bio jasniji. Općenito, razlika je u tome što moj kod inicijalizira oba pina odjednom u istoj strukturi, a STM32GENERIC kod zauzvrat. Očito kod inicijalizacije pina nekako utječe na razinu na ovom pinu. Prije inicijalizacije, ništa se ne izlazi na ovaj pin i otpornik se povlači na razinu na jedan. U trenutku inicijalizacije, iz nekog razloga, regulator postavlja nulu na odgovarajuću nogu.

Ova činjenica je sama po sebi bezopasna. Ali problem je u tome što je spuštanje SDA linije kada je SCL linija podignuta početni uvjet za i2c sabirnicu. Zbog toga prijemnik kontrolera poludi, postavlja zastavicu ZAUZET i počinje čekati podatke. Odlučio sam ne izbaciti biblioteku kako bih dodao mogućnost inicijalizacije više pinova odjednom. Umjesto toga, samo sam preuredio ova 2 retka na mjesta - inicijalizacija prikaza bila je uspješna. Popravak je usvojen u STM32GENERIC.

Inače, zanimljiva je inicijalizacija sabirnice u libmapleu. Prije početka inicijalizacije i2c perifernih uređaja na sabirnici, oni najprije vrše resetiranje. Da bi to učinila, knjižnica prebacuje igle u normalni GPIO način rada i nekoliko puta trza ove noge, oponašajući sekvence pokretanja i zaustavljanja. To pomaže oživljavanju uređaja zaglavljenih na gumi. Nažalost, u HAL-u nema sličnog. Ponekad mi se zaslon zaglavi, a onda samo nestanak struje spašava.

Inicijalizacija i2c iz stm32duino

/ ** * @brief Resetirajte I2C sabirnicu. * * Reset se postiže tako što se impulsi isključuju sve dok obješeni * slave ne otpuste SDA i SCL, zatim generiraju START uvjet, zatim STOP * uvjet. * * @param dev I2C uređaj * / void i2c_bus_reset (const i2c_dev * dev) (/ * Otpustite oba retka * / i2c_master_release_bus (dev); / * * Provjerite je li sabirnica slobodna tako što ćete je taktirati sve dok bilo koji podređeni ne otpusti * sabirnicu. * / while (! gpio_read_bit (sda_port (dev), dev-> sda_pin)) (/ * Pričekajte da se istezanje sata završi * / while (! gpio_read_bit (scl_port (dev), dev-> scl_pin)); delay_us (10 ); / * Povuci nisko * / gpio_write_bit (scl_port (dev), dev-> scl_pin, 0); delay_us (10); / * Ponovno otpusti visoko * / gpio_write_bit (scl_port (dev), dev-> scl_pin, 1); delay_us (10);) / * Generiraj uvjet pokretanja i zaustavljanja * / 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);)

Opet tu: UART

Bilo mi je drago što sam se konačno vratio programiranju i nastavio pisati značajke. Sljedeći veliki dio bilo je povezivanje SD kartice putem SPI-a. Ovo je samo po sebi uzbudljivo, zanimljivo i bolno. Svakako ću o tome posebno govoriti u sljedećem članku. Jedan od problema bilo je veliko opterećenje (> 50%) procesora. To je dovelo u pitanje energetsku učinkovitost uređaja. I bilo je neugodno koristiti uređaj, tk. UI je bio užasno glup.

Razumijevajući pitanje, pronašao sam razlog za ovu potrošnju resursa. Sav rad sa SD karticom obavljen je bajt po bajt, uz korištenje procesora. Ako je bilo potrebno upisati blok podataka na karticu, tada se za svaki bajt poziva funkcija slanja bajta

Za (uint16_t i = 0; i< 512; i++) { spiSend(src[i]);
Ne, pa ovo nije ozbiljno! Postoji DMA! Da, SD knjižnica (ona koja dolazi s Arduinom) je nespretna i treba je mijenjati, ali problem je globalniji. Ista se slika uočava u biblioteci za rad s ekranom, a čak se i slušanje UART-a vrši putem anketiranja. Općenito, počeo sam misliti da prepisivanje svih komponenti u HAL-u nije tako glupa ideja.

Počeo sam, naravno, s nečim jednostavnijim – UART drajverom koji sluša tok podataka s GPS-a. Arduino sučelje ne dopušta vam da se uhvatite za UART prekid i u hodu otimate dolazne znakove. Kao rezultat toga, jedini način za dobivanje podataka je konstantno ispitivanje. Naravno, u GPS handler sam dodao vTaskDelay (10) kako bih barem malo smanjio opterećenje, ali to je zapravo štaka.

Prva pomisao, naravno, bila je zeznuti DMA. Čak bi i radio da nije NMEA protokola. Problem je u tome što u ovom protokolu informacije samo teku u toku, a pojedinačni paketi (linije) su odvojeni znakom za unos retka. Štoviše, svaka linija može biti različite duljine. Zbog toga se unaprijed ne zna koliko podataka treba primiti. DMA ne radi tako - tamo se broj bajtova mora unaprijed postaviti prilikom inicijalizacije prijenosa. Ukratko, DMA više nije potreban, tražimo drugo rješenje.

Ako pažljivo pogledate dizajn biblioteke NeoGPS, možete vidjeti da knjižnica prihvaća ulazne podatke bajt po bajt, ali se vrijednosti ažuriraju tek kada stigne cijela linija (točnije, paket od nekoliko linije). Da. nema razlike hoće li se u biblioteku hraniti bajtovi jedan po jedan kako se prima ili onda sve odjednom. Dakle, možete uštedjeti vrijeme procesora - spremiti primljenu liniju u međuspremnik, a to možete učiniti odmah u prekidu. Kada je cijela linija prihvaćena, možete početi s obradom.

Pojavljuje se sljedeći dizajn

UART klasa drajvera

// Veličina UART ulaznog međuspremnika const uint8_t gpsBufferSize = 128; // Ova klasa rukuje UART sučeljem koje prima znakove od GPS-a i pohranjuje ih u klasu međuspremnika GPS_UART (// UART hardverska ručica UART_HandleTypeDef uartHandle; // Primanje prstenastog međuspremnika uint8_t rxBuffer; volatile uint8_t lastReadIndex = 0; volatile lastRe uint8_t de lastRe uint8_ved / Ručica GPS niti TaskHandle_t xGPSThread = NULL;


Iako je inicijalizacija procurila iz STM32GENERIC, ona u potpunosti odgovara onoj koju nudi CubeMX

UART inicijalizacija

void init () (// Poništi pokazivače (samo u slučaju da netko pozove init () više puta) lastReadIndex = 0; lastReceivedIndex = 0; // Inicijaliziraj GPS ručku niti xGPSThread = xTaskGetCurrentTaskHandle (); // Omogući taktiranje odgovarajuće periperhalne __GPALARK) -__HAL_RCC_USART1_CLK_ENABLE () // Init igle u alternativni modova GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; // TX igla GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH, HAL_GPIO_Init (GPIOA, i GPIO_InitStruct) GPIO_InitStruct. Pin = GPIO_PIN_10; // RX pin GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init (GPIOA, & GPIO_InitStruct); // Init uartHandle.andB.Init uartHandle. Paritet = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.H wFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init (& uartHandle); // Koristit ćemo UART prekid za dobivanje podataka HAL_NVIC_SetPriority (USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ (USART1_IRQn); // Čekat ćemo da dobijemo pravo jednog znaka pravo na međuspremnik HAL_UART_Receive_IT (& uartHandle, rxBuffer, 1); )


Zapravo, TX pin nije mogao biti inicijaliziran, ali je uartHandle.Init.Mode mogao biti postavljen na UART_MODE_RX - tek ćemo primiti. Međutim, neka bude - odjednom moram nekako konfigurirati GPS modul i napisati mu naredbe.

Dizajn ove klase mogao bi izgledati bolje da nije bilo ograničenja HAL arhitekture. Dakle, ne možemo samo postaviti način, kažu, prihvatiti sve, izravno zakačiti prekid i otimati primljene bajtove izravno iz registra primatelja. Potrebno je unaprijed reći HAL-u koliko i gdje ćemo primiti bajtove - odgovarajući rukovatelji će primljene bajtove zapisati u predviđeni međuspremnik. Za to se u zadnjem retku funkcije inicijalizacije nalazi poziv HAL_UART_Receive_IT (). Budući da duljina niza nije unaprijed poznata, moramo prihvatiti jedan po jedan bajt.

Također morate deklarirati čak 2 povratna poziva. Jedan je rukovalac prekida, ali njegov je posao samo pozvati rukovatelja iz HAL-a. Druga funkcija je HAL "povratni poziv" da je bajt već primljen i da je već u međuspremniku.

UART povratni pozivi

// Proslijedi obradu UART prekida na HAL vanjski "C" void USART1_IRQHandler (void) (HAL_UART_IRQHandler (gpsUart.getUartHandle ());) // HAL poziva ovaj povratni poziv kada primi znak od UART-a. Proslijedite ga eksternu klase "C" void HAL_UART_RxCpltCallback (UART_HandleTypeDef * uartHandle) (gpsUart.charReceivedCB ();)


Metoda charReceivedCB () priprema HAL za primanje sljedećeg bajta. I on je taj koji utvrđuje da je linija već završila i to možete signalizirati glavnom programu. Semafor u signalnom modu mogao bi se koristiti kao sredstvo sinkronizacije, ali za tako jednostavne svrhe preporuča se koristiti izravne obavijesti.

Obrada primljenog bajta

// Char primljen, pripremite se za sljedeći jedan inline void charReceivedCB () (char lastReceivedChar = rxBuffer; lastReceivedIndex ++; HAL_UART_Receive_IT (& uartHandle, rxBuffer + (lastReceivedIndex% gpsBufferSize), EOL simbol da je nit); za čitanje if (lastReceivedChar == "\ n") vTaskNotifyGiveFromISR (xGPSThread, NULL);)


Funkcija odgovora (na čekanju) je waitForString (). Njegov zadatak je jednostavno visi na objektu sinkronizacije i čekati (ili izaći s timeoutom)

Čeka se kraj reda

// Pričekajte dok se ne primi cijeli red bool waitForString () (return ulTaskNotifyTake (pdTRUE, 10);)


Djeluje ovako. Nit koja je odgovorna za GPS normalno spava u funkciji waitForString (). Bajtovi koji dolaze iz GPS-a od strane rukovatelja prekida dodaju se u međuspremnik. Ako se primi znak \ n (kraj retka), tada prekid budi glavnu nit, koja počinje sipati bajtove iz međuspremnika u parser. Pa, kada parser završi obradu paketa poruke, ažurirat će podatke u GPS modelu.

GPS tok

void vGPSTask (void * pvParameters) (// inicijalizacija GPS-a mora se obaviti unutar GPS niti jer je ručka niti pohranjena // i kasnije se koristi za potrebe sinkronizacije gpsUart.init (); for (;;) (// Pričekajte dok se cijeli niz ne prikaže primljeno if (! gpsUart.waitForString ()) continue; // Čitanje primljenog niza i raščlanjivanje GPS toka char po char dok (gpsUart.available ()) (int c = gpsUart.readChar (); //SerialUSB.write(c) ; gpsParser.handle (c);) if (gpsParser.available ()) (GPSDataModel :: instanca (). processNewGPSFix (gpsParser.read ()); GPSDataModel :: instance (). processNewSatellitesData (gpsParser.satellites, gpsParser.sat_count );) vTaskDelay (10);))


Naletio sam na jedan vrlo netrivijalan trenutak u kojem sam zapeo nekoliko dana. Čini se da je kod za sinkronizaciju uzet iz primjera, ali isprva nije radio - poklopio je cijeli sustav. Mislio sam da je problem u izravnim obavijestima (funkcije xTaskNotifyXXX), promijenio sam ga u obične semafore, ali aplikacija je i dalje poklopila.

Pokazalo se da morate biti vrlo oprezni s prioritetom prekida. Prema zadanim postavkama, sve prekide postavljam na nulti (najviši) prioritet. Ali FreeRTOS ima zahtjev da prioriteti budu u zadanom rasponu. Prekidi s previsokim prioritetom ne mogu pozvati FreeRTOS funkcije. Samo prekidi s prioritetom configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY i ispod mogu pozvati funkcije sustava (dobro objašnjenje je). Ova zadana postavka je 5. Promijenio sam prioritet prekida UART-a na 6 i sve je počelo.

Opet tu: I2C preko DMA

Sada možete učiniti nešto kompliciranije, poput upravljačkog programa zaslona. Ali ovdje morate napraviti izlet u teoriju I2C autobusa. Sama po sebi, ova sabirnica ne regulira protokol za prijenos podataka preko sabirnice - možete pisati bajtove ili čitati. Čak je moguće prvo napisati, a zatim pročitati u jednoj transakciji (na primjer, napisati adresu, a zatim pročitati podatke na ovoj adresi).

Međutim, većina uređaja definira protokol višeg sloja na približno isti način. uređaj korisniku daje skup registara, svaki sa svojom adresom. Istodobno, u komunikacijskom protokolu prvi bajt (ili nekoliko) u svakoj transakciji određuje adresu ćelije (registra) u koju ćemo nastaviti čitati ili pisati. Istovremeno je moguća i višebajtna razmjena u stilu "zapisat ćemo / pročitati mnogo bajtova počevši od ove adrese". Posljednja opcija dobro funkcionira za DMA.

Nažalost, zaslon baziran na SSD1306 kontroleru pruža potpuno drugačiji protokol – naredbeni protokol. Prvi bajt svake transakcije je znak "naredba ili podaci". U slučaju naredbe, kod naredbe je drugi bajt. Ako su za naredbu potrebni argumenti, oni se prosljeđuju kao zasebne naredbe nakon prve. Za inicijalizaciju prikaza potrebno je poslati oko 30 naredbi, ali one se ne mogu dodati u jedan niz i poslati u jednom bloku. Morate ih poslati jednu po jednu.

Ali sa slanjem niza piksela (frame buffer), sasvim je moguće koristiti DMA usluge. To je ono što ćemo pokušati.

Ali biblioteka Adafruit_SSD1306 napisana je vrlo nespretno i nemoguće je ugurati se tamo s malo krvi. Očigledno, knjižnica je prvo napisana da komunicira s zaslonom putem SPI-a. Tada je netko dodao I2C podršku, dok je SPI podrška ostala uključena. Onda je netko počeo dodavati sve vrste optimizacija niske razine i skrivati ​​ih iza ifdef-ova. Kao rezultat toga, dobili smo noodle iz koda za podršku različitim sučeljima, pa je prije nego što ide dalje bilo potrebno pročešljati.

Isprva sam to pokušao srediti uokvirujući kod za različita sučelja s ifdefovima. Ali ako želim napisati kod za komunikaciju sa zaslonom, koristiti DMA i sinkronizaciju preko FreeRTOS-a, onda neću moći puno učiniti. Radit će točnije, ali ovaj će kod morati biti napisan izravno u kodu knjižnice. Stoga sam odlučio još jednom izbezumiti knjižnicu, napraviti sučelje i staviti svaki drajver u zasebnu klasu. Kôd je postao čišći i bilo bi bezbolno dodati podršku za nove upravljačke programe bez promjene same biblioteke.

Sučelje upravljačkog programa zaslona

// Sučelje za hardverski upravljački program // Adafruit_SSD1306 ne radi izravno s hardverom // Svi komunikacijski zahtjevi prosljeđuju se u klasu drajvera ISSD1306Driver (javno: virtualni void begin () = 0; virtual void sendCommand (uint8_t cmd) = 0 ; virtualni void sendData (uint8_t * podaci, veličina_t veličine) = 0;);


Pa, idemo. Već sam pokazao inicijalizaciju I2C. Tu se ništa nije promijenilo. Ali slanje naredbe postalo je malo lakše. Sjećate li se da sam vam govorio o razlici između protokola registra i naredbe za I2C uređaje? I iako zaslon implementira naredbeni protokol, može se dobro simulirati pomoću registra. Samo trebate zamisliti da zaslon ima samo 2 registra - 0x00 za naredbe i 0x40 za podatke. A HAL čak nudi i funkciju za ovu vrstu prijenosa

Slanje naredbe na zaslon

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


U početku nije bilo baš jasno sa slanjem podataka. Izvorni kod je poslao podatke u malim paketima od 16 bajtova

Čudan kod za slanje podataka

za (uint16_t i = 0; i


Pokušao sam petljati s veličinom paketa i slati veće pakete, ali u najboljem slučaju dobio sam zgužvan zaslon. Pa, ili je sve visilo.

Iskrivljeni zaslon



Razlog se pokazao trivijalnim - prelijevanje međuspremnika. Klasa Wire iz arduine (barem STM32GENERIC) pruža vlastiti međuspremnik od samo 32 bajta. Ali zašto nam je uopće potreban dodatni međuspremnik, ako ga klasa Adafruit_SSD1306 već ima? Štoviše, s HAL-om se slanje dobiva u jednom redu.

Ispravan prijenos podataka

void DisplayDriver :: sendData (uint8_t * data, size_t veličina) (HAL_I2C_Mem_Write (& handle, i2c_addr, 0x40, 1, data, size, 10);)


Dakle, pola bitke je gotovo - napisali smo driver za zaslon u čistom HAL-u. Ali u ovoj verziji i dalje je zahtjevan za resurse - 12% za zaslon od 128x32 i 23% za zaslon od 128x64. Korištenje DMA to već traži.

Prvo, inicijalizirajmo DMA. Želimo implementirati prijenos podataka u I2C #1, a ova funkcija živi na šestom DMA kanalu. Inicijaliziranje kopiranja bajtova bajta iz memorije na periferne uređaje

DMA postavka za I2C

// Omogućavanje sata DMA kontrolera __HAL_RCC_DMA1_CLK_ENABLE (); // Inicijaliziraj 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); // Poveži inicijaliziranu DMA ručku s I2C ručkom __HAL_LINKDMA (& handle, hdmatx, hdma_tx); / * Pokretanje DMA prekida * / / * Konfiguracija prekida DMA1_Channel6_IRQn * / HAL_NVIC_SetPriority (DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ (DMA1_Channel6_IRQn);


Prekidi su obvezni dio dizajna. Inače će funkcija HAL_I2C_Mem_Write_DMA () pokrenuti I2C transakciju, ali je nitko neće dovršiti. Opet imamo posla s glomaznim HAL dizajnom i potrebom za čak dva povratna poziva. Sve je potpuno isto kao i kod UART-a. Jedna funkcija je rukovalac prekida - jednostavno prosljeđujemo poziv na HAL. Druga funkcija je signal da su podaci već poslani.

DMA rukovatelji prekida

extern "C" void DMA1_Channel6_IRQHandler (void) (HAL_DMA_IRQHandler (displayDriver.getDMAHandle ());) extern "C" void HAL_I2C_MemTxCpltCallback (I2C_HandleTypeDeffer * hi2c


Naravno, nećemo stalno ispitivati ​​I2C i je li transfer već gotov? Umjesto toga, trebate zaspati na objektu sinkronizacije i pričekati dok prijenos ne završi.

Prijenos podataka putem DMA sa sinkronizacijom

void DisplayDriver :: sendData (uint8_t * data, size_t veličina) (// Pokreni prijenos podataka HAL_I2C_Mem_Write_DMA (& handle, i2c_addr, 0x40, 1, podaci, veličina); // Pričekajte dok se prijenos ne završi ulTaskNotifyTake (pdTRUE, 100); void DisplayDriver :: transferCompletedCB () (// Nastavi prikaz niti vTaskNotifyGiveFromISR (xDisplayThread, NULL);)


Prijenos podataka i dalje traje 24 ms, što je gotovo neto vrijeme prijenosa od 1 KB (veličina međuspremnika prikaza) na 400 kHz. Samo u ovom slučaju procesor većinu vremena samo spava (ili radi druge stvari). Ukupna iskorištenost CPU-a pala je s 23% na samo 1,5-2%. Mislim da se vrijedilo boriti za ovaj pokazatelj!

Opet tu: SPI preko DMA

Povezivanje SD kartice putem SPI-a bilo je, u neku ruku, lakše - do tada sam počeo zajebavati sdfat biblioteku, a tamo su dobri ljudi već dodijelili komunikaciju s karticom u zasebno sučelje upravljačkog programa. Istina, uz pomoć definea možete odabrati samo jednu od 4 gotove verzije drajvera, ali to se lako može protraćiti i zamijeniti vlastitom implementacijom.

SPI upravljačko sučelje za rad sa SD karticom

// Ovo je prilagođena implementacija klase SPI Driver. SdFat biblioteka // koristi ovu klasu za pristup SD kartici preko SPI // // Glavna namjera ove implementacije je potaknuti prijenos podataka // preko DMA-a i sinkronizirati se s mogućnostima FreeRTOS-a. class SdFatSPIDriver: public SdSpiBaseDriver (// SPI modul SPI_HandleTypeDef spiHandle; // GPS nit hvataljka taskHandle_t xSDThread = NULL; public: SdFatSPIDriver (); virtualna void aktivacija (); virtualna void begin (uint8_t) dePin_int_int receivet (uint8_t) dePin_int chip); primiti (uint8_t * buf, size_t n); virtualno void poslati (uint8_t podaci); virtualno void poslati (const uint8_t * buf, size_t n); virtual void select (); virtual void setSpiSettings (SPISettings spiSettings); virtualno void poništiti odabir () ;);


Kao i prije, počinjemo s jednostavnim - s hrastovom implementacijom bez DMA. Inicijalizaciju djelomično generira CubeMX, a djelomično je usklađena sa SPI implementacijom STM32GENERIC

SPI inicijalizacija

SdFatSPIDriver :: SdFatSPIDriver () () // void SdFatSPIDriver :: aktivirati (); void SdFatSPIDriver :: begin (uint8_t chipSelectPin) (// Zanemari proslijeđeni CS pin - Ovaj upravljački program radi s unaprijed definiranim (void) chipSelectPin; // Inicijaliziraj GPS ručku niti xSDThread = xTaskGetCurrentTaskHandle (); // Odgovarajući CL_LAC peri_Chandle_Chandle (); // Odgovarajući CL_LAC_PERSKLEC () // init PIN-a GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; // Mosi & SCK GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH, HAL_GPIO_Init (GPIOA, i GPIO_InitStruct) GPIO_InitStruct.Pin = GPIO_PIN_6 -// MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL, HAL_GPIO_Init (GPIOA, i GPIO_InitStruct) GPIO_InitStruct.Pin = GPIO_PIN_4; // CS GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH, HAL_GPIO_Init (GPIOA , & GPIO_InitStruct); // Postavi CS pin High prema zadanim postavkama HAL_GPIO_WritePin (GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Init SPI spiHandl e.Instanca = SPI1; spiHandle.Init.Mode = SPI_MODE_MASTER; spiHandle.Init.Direction = SPI_DIRECTION_2LINES; spiHandle.Init.DataSize = SPI_DATASIZE_8BIT; spiHandle.Init.CLKPolarity = 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.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiHandle.Init.CRCPolynomial = 10; HAL_SPI_Init (& spiHandle); __HAL_SPI_ENABLE (& spiHandle); )


Dizajn sučelja je skrojen za arduino s jednim brojem pinova. U mom slučaju nije bilo smisla postavljati CS pin kroz parametre - ovaj signal imam čvrsto vezan za pin A4, ali sučelje se moralo promatrati.

Dizajnom biblioteke SdFat, brzina SPI porta se konfigurira prije svake transakcije. Oni. teoretski, možete početi komunicirati s karticom malom brzinom, a zatim je povećati. Ali postigao sam na ovome i prilagodio brzinu jednom u start () metodi. Dakle, metode aktiviranja/deaktiviranja su za mene prazne. Kao setSpiSettings ()

Trivijalni rukovaoci transakcijama

void SdFatSPIDriver :: activate () (// Nije potrebna posebna aktivacija) void SdFatSPIDriver :: deactivate () (// Nije potrebna posebna deaktivacija) void SdFatSPIDriver :: setSpiSettings (const SPISettings & spiSettings) (// Zanemari postavke - koristimo iste postavke za sav prijenos)


Metode kontrole CS signala prilično su trivijalne

Kontrola CS signala

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


Dolazimo do zabavnog dijela – čitanja i pisanja. Prva većina hrastova implementacija bez DMA

Prijenos podataka bez DMA

uint8_t SdFatSPIDriver :: primiti () (uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive (& spiHandle, & dummy, & buf, 1, 10); return buf;) uint8_t SdFatSPIDriver_t, primiti me / postavi me veličinu DMA: ovdje (buf, 0xff, n); HAL_SPI_Receive (& spiHandle, buf, n, 10); povratak 0;) void SdFatSPIDriver :: pošalji (uint8_t podatke) (HAL_SPI_Transmit (& spiHandle, & data, 1, 10); ) void SP void SdF :: send (const uint8_t * buf, size_t n) (// TODO: Prijenos preko DMA ovdje HAL_SPI_Transmit (& spiHandle, (uint8_t *) buf, n, 10);)


U SPI sučelju primanje i prijenos podataka odvija se istovremeno. Da biste nešto primili morate nešto poslati u isto vrijeme. Obično HAL to radi umjesto nas - samo pozivamo funkciju HAL_SPI_Receive () i ona organizira i slanje i primanje. Ali u stvarnosti, ova funkcija šalje smeće koje je bilo u međuspremniku za primanje.
Da biste prodali nešto nepotrebno, prvo morate kupiti nešto nepotrebno (C) Prostokvashino

Ali postoji nijansa. SD kartice su prilično hirovite. Ne vole da im se ništa gura dok kartica šalje podatke. Stoga sam morao koristiti funkciju HAL_SPI_TransmitReceive () i prisilno slati 0xffs dok sam primao podatke.

Izmjerimo mjere. Neka jedan tok zapiše 1 kb podataka na karticu u petlji.

Testni kod za slanje toka podataka na SD karticu

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 ( "Spremljeno % d kb \ n ", i); i = 0;))


Ovakvim pristupom uspijeva napisati oko 15-16 kb u sekundi. Ne mnogo. No pokazalo se da sam predskaler postavio čak 256. SPI sat je postavljen na mnogo manje od moguće propusnosti. Eksperimentalno sam otkrio da je besmisleno postavljati frekvenciju veću od 9 MHz (preskaler je postavljen na 8) - ne može se postići brzina snimanja iznad 100-110 kb/s (na drugom flash disku, inače, za iz nekog razloga se moglo snimiti samo 50-60 kb/s, a na trećem samo 40 kb/s). Navodno sve počiva na timeoutima samog flash pogona.

U principu, ovo je više nego dovoljno, ali mi ćemo pumpati podatke preko DMA. Djelujemo prema već poznatoj shemi. Prvi korak je inicijalizacija. Prijem i prijenos putem SPI uživo na drugom i trećem DMA kanalu.

DMA inicijalizacija

// Omogućavanje sata DMA kontrolera __HAL_RCC_DMA1_CLK_ENABLE (); // Rx DMA kanal 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 kanal 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);


Ne zaboravite omogućiti prekide. Oni će ići s prioritetom 8 za mene - nešto niži od onoga kod UART-a i I2C

Konfiguriranje DMA prekida

// Postavljanje DMA prekida 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);


Odlučio sam da bi troškovi pokretanja DMA-a i sinkronizacije za kratke prijenose mogli nadmašiti isplativost, pa sam za male pakete (do 16 bajtova) napustio staru opciju. Paketi duži od 16 bajtova prosljeđuju se putem DMA. Metoda sinkronizacije potpuno je ista kao u prethodnom odjeljku.

DMA prijenos

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver :: primanje (uint8_t * buf, size_t n) (memset (buf, 0xff, n); // Ne koristi se DMA za kratke prijenose ako (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); }


Naravno, bez prekida nema. Sve je isto kao u slučaju I2C.

DMA prekida

vanjski SdFatSPIDriver spiDriver; vanjski "C" void DMA1_Channel2_IRQHandler (void) (HAL_DMA_IRQHandler (spiDriver.getHandle () hdmarx)). vanjski "C" void DMA1_Channel3_IRQHandler (void) (HAL_DMA_IRHiDxHandler (HD) (* SPI_HandleTypeDef hspi) (spiDriver.dmaTransferCompletedCB ()) vanjski "C" poništava HAL_SPI_RxCpltCallback (SPI_HandleTypeDef * hspi) (spiDriver.dmaTransferCompletedCB ();)


Pokrećemo, provjeravamo. Kako ne bih mučio flash pogon, odlučio sam otkloniti pogreške čitanjem velike datoteke, a ne pisanjem. Ovdje sam otkrio vrlo zanimljivu točku: brzina čitanja u ne-DMA verziji bila je oko 250-260 kb / s, dok je s DMA bila samo 5 !!! Štoviše, potrošnja CPU-a bez korištenja DMA-a bila je 3%, a s DMA-om - 75-80% !!! Oni. rezultat je upravo suprotan od očekivanog.

Offtopic oko 3%

Ovdje sam imao smiješnu grešku s mjerenjem opterećenja procesora - ponekad je funkcija govorila da je procesor opterećen samo 3%, iako je postotak morao mlatiti bez zaustavljanja. Zapravo, opterećenje je bilo 100% i moja mjerna funkcija uopće nije pozvana - ona ima najniži prioritet i jednostavno nije imala dovoljno vremena za to. Stoga sam primio posljednju memoriranu vrijednost prije početka izvršenja. Funkcija radi ispravnije u normalnim uvjetima.


Nakon što sam kod vozača preklopio prijavljivanje gotovo kroz red, otkrio sam problem: koristio sam pogrešnu funkciju povratnog poziva. U početku sam koristio HAL_SPI_Receive_DMA () u svom kodu i HAL_SPI_RxCpltCallback povratni poziv je korišten zajedno s njim. Ova konstrukcija nije funkcionirala zbog nijanse slanja 0xff u isto vrijeme. Kada sam promijenio HAL_SPI_Receive_DMA () u HAL_SPI_TransmitReceive_DMA (), također sam trebao promijeniti povratni poziv u HAL_SPI_TxRxCpltCallback (). Oni. zapravo, očitanje se dogodilo, ali zbog nedostatka povratnih poziva brzina je regulirana timeoutom od 100ms.

Nakon što je popravljen povratni poziv, sve je došlo na svoje mjesto. Opterećenje procesora palo je na 2,5% (sada iskreno), a brzina je čak skočila do 500 kb/s. Istina, predskaler je morao biti postavljen na 4 - s predskalerom na 2, tvrdnje su izlivene u biblioteku SdFat. Čini se da je ovo ograničenje brzine moje kartice.

Nažalost, to nema nikakve veze sa brzinom pisanja. Brzina pisanja je i dalje bila oko 50-60 kb/s, a opterećenje procesora variralo je u rasponu od 60-70%. No, nakon kopanja po cijeloj večeri i mjerenja na različitim mjestima, otkrio sam da je funkcija send () mog drajvera (koji piše jedan sektor od 512 bajtova) završena za samo 1-2 ms, uključujući čekanje i sinkronizaciju. Ponekad, međutim, pali neka vrsta timeouta i snimanje traje 5-7ms. No, problem zapravo nije u upravljačkom programu, već u logici rada s FAT datotečnim sustavom.

Prelazeći na razinu datoteka, particija i klastera, zadatak upisivanja 512 u datoteku nije tako trivijalan. Morate pročitati FAT tablicu, pronaći mjesto u njoj za sektor koji se upisuje, napisati sam sektor, ažurirati zapise u tablici FAT, zapisati ove sektore na disk, ažurirati zapise u tablici datoteka i direktorija i hrpa drugih stvari. Općenito, jedan poziv FatFile :: write () mogao bi potrajati i do 15-20 ms, a veliki dio tog vremena zauzima stvarni rad procesora za obradu zapisa u datotečnom sustavu.

Kao što sam već napomenuo, opterećenje procesora tijekom snimanja je 60-70%. Ali ovaj broj također ovisi o vrsti datotečnog sustava (Fat16 ili Fat32), veličini i, sukladno tome, broju tih klastera na particiji, brzini samog flash pogona, začepljenom i fragmentiranom mediju, korištenju dugih nazivi datoteka i još mnogo toga. Stoga vas molimo da ove mjere tretirate kao neku vrstu relativnih brojeva.

Opet tu: USB s dvostrukim međuspremnikom

Ispalo je zanimljivo s ovom komponentom. Originalna implementacija USB Serial-a iz STM32GENERIC imala je niz nedostataka i ja sam se obvezao da je prepišem za sebe. No dok sam proučavao kako funkcionira USB CDC, čitao izvore i proučavao dokumentaciju, dečki iz STM32GENERIC su značajno poboljšali njihovu implementaciju. Ali prije svega.

Dakle, originalna implementacija mi nije odgovarala iz sljedećih razloga:

  • Poruke se šalju sinkrono. Oni. banalni bajt prijenos podataka s GPS UART na USB čeka da se svaki pojedini bajt pošalje. Zbog toga opterećenje procesora može doseći i do 30-50%, što je, naravno, puno (brzina UART-a je samo 9600)
  • Nema sinkronizacije. Prilikom ispisa poruka iz više tokova, izlaz su rezanci iz poruka koje djelomično prepisuju jedna drugu
  • Prekomjerni međuspremnici za slanje i primanje. Nekoliko međuspremnika je deklarirano u USB Middlewareu, ali se zapravo ne koriste. Još nekoliko međuspremnika je deklarirano u klasi SerialUSB, ali budući da koristim samo izlaz, međuspremnik za primanje samo troši memoriju.
  • Konačno, samo me nervira sučelje klase Print. Ako, na primjer, želim prikazati liniju "trenutna brzina XXX km / h", onda moram obaviti čak 3 poziva - za prvi dio linije, za broj i za ostatak linije. Osobno mi je po duhu bliži klasični printf. Plus streamovi su također u redu, ali morate paziti kakav kod generira kompajler.
Za sada krenimo s jednostavnim – slanjem poruka sinkrono, bez sinkronizacije i formatiranja. Zapravo, iskreno sam zalupio kod iz STM32GENERIC.

Provedba u čelo

vanjski USBD_HandleTypeDef hUsbDeviceFS; void usbDebugWrite (uint8_t c) (usbDebugWrite (& c, 1);) void usbDebugWrite (const char * str) (usbDebugWrite ((const uint8_t *) str, strlen (str));) void usbDebugWrite (const uint8 size)_t * (// Zanemari slanje poruke ako USB nije spojen ako se vrati (hUsbDeviceFS.dev_state! = USBD_STATE_CONFIGURED); // Pošalji poruku, ali ne duže od timeouta uint32_t timeout = HAL_GetTick () + 5; dok (HAL_GetTick ()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


Formalno, ovo nije sinkroni kod, budući da ne čeka slanje podataka. Ali ova funkcija čeka da se pošalju prethodni podaci. Oni. prvi poziv će poslati podatke na port i izaći, ali će drugi poziv pričekati dok se podaci poslani u prvom pozivu zapravo ne pošalju. U slučaju isteka, podaci se gube. Također, ništa se ne događa ako uopće nema USB veze.

Naravno, ovo je samo praznina, budući da ova implementacija ne rješava naznačene probleme. Što je potrebno da ovaj kod bude asinkroni i neblokirajući? Pa, barem tampon. Samo kada treba prenijeti ovaj međuspremnik?

Mislim da je vrijedno napraviti malu digresiju u principe rada USB-a. Činjenica je da samo domaćin može pokrenuti prijenos u USB protokolu. Ako uređaj treba prenijeti podatke prema hostu, podaci se pripremaju u posebnom međuspremniku PMA (Packet Memory Area) i uređaj čeka da host preuzme te podatke. PMA međuspremnik priprema funkcija CDC_Transmit_FS (). Ovaj međuspremnik živi unutar USB perifernih uređaja, a ne u korisničkom kodu.

Iskreno, htio sam ovdje nacrtati lijepu sliku, ali još uvijek nisam mogao shvatiti kako da je bolje prikažem

Ali bilo bi dobro provesti sljedeću shemu. Klijentski kod po potrebi zapisuje podatke u određeni akumulativni (korisnički) međuspremnik. S vremena na vrijeme dolazi domaćin i uzima sve što se do tog trenutka nakupilo u tamponu. Ovo je vrlo slično onome što sam opisao u prethodnom odlomku, ali postoji jedno ključno upozorenje: podaci su u korisničkom međuspremniku, a ne u PMA-u. Oni. Želio bih uopće bez poziva CDC_Transmit_FS (), koji prenosi podatke iz korisničkog međuspremnika u PMA, i umjesto toga uhvatiti povratni poziv „ovdje je domaćin došao, podaci traže“.

Nažalost, ovaj pristup nije moguć u trenutnom dizajnu USB CDC Middleware-a. Točnije, možda je moguće, ali morate se uglaviti u implementaciju CDC drajvera. Još nisam dovoljno iskusan s USB protokolima za to. Osim toga, nisam siguran da će USB vremenska ograničenja biti dovoljna za takvu operaciju.

Srećom, u ovom trenutku sam primijetio da je STM32GENERIC već zaobišao takvo što. Evo koda koji sam kreativno preradio za njih.

USB serijski dvostruki međuspremnik

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; volatile uint16_t usbTxHead = 0; volatile uint16_t usbTxTail = 0; volatile uint16_t usbTransmitting = 0; uint16_t transmitContiguousBuffer () (uint16_t count = 0; // Prijenos susjednih podataka do kraja međuspremnika if (usbTxHead> usbTxTail) (count = usbTxHead - usbTxTail;) else (count = sizeof (usbTxBuffer);) CDFSC (&) CDFSC; usbTxBuffer, count); return count;) void usbDebugWriteInternal (const char * buffer, size_t size, bool reverse = false) (// Zanemari slanje poruke ako USB nije spojen ako se (hUsbDeviceFS.dev_state! = USBD_STATE_CONFIGURED) vrati; / / Prenesite poruku, ali ne duže od timeouta uint32_t timeout = HAL_GetTick () + 5; // Zaštitite ovu funkciju od višestrukog ulaza MutexLocker ormarića (usbMutex); // Kopirajte podatke u međuspremnik za (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; } }


Ideja iza ovog koda je sljedeća. Iako nije bilo moguće uhvatiti obavijest “domaćin je došao i želi podatke”, pokazalo se da je moguće organizirati povratni poziv “Poslao sam podatke hostu, možete preliti sljedeće”. Ispada takav dvostruki međuspremnik - dok uređaj čeka da se podaci pošalju iz internog PMA međuspremnika, korisnički kod može dodati bajtove u međuspremnik za pohranu. Kada je slanje podataka završeno, kumulativni međuspremnik se ulijeva u PMA. Ostaje samo organizirati upravo ovaj povratni poziv. Da biste to učinili, morate lagano upisati funkciju USBD_CDC_DataIn ().

Spremljeni USB Middleware

statički uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef * pdev, uint8_t epnum) (USBD_CDC_HandleTypeDef * hcdc = (USBD_CDC_HandleTypeDef *) pdev-> pClassData; if (lassturre USB-FA>) if (lassturre USB)


Usput, funkcija usbDebugWrite zaštićena je mutexom i trebala bi ispravno raditi s više niti. Funkcija USBSerialTransferCompletedCB () nije zaštićena - poziva se iz prekida i radi na promjenjivim varijablama. Iskreno govoreći, ovdje negdje postoji greška, vrlo rijetko se simboli gutaju. Ali nije kritično za mene da otklanjam greške. To se neće zvati u "proizvodnom" kodu.

Opet tu: printf

Za sada, ova stvar može podnijeti samo stalne nizove. Vrijeme je za podešavanje analognog printf (). Ne želim koristiti pravu funkciju printf () - ona povlači 12 kilobajta dodatnog koda i hrpu koju nemam. Pronašao sam svoj debug logger, koji sam jednom napisao za AVR. Moja implementacija može ispisivati ​​nizove kao i brojeve u decimalnom i heksadecimalnom formatu. Nakon dorade i testiranja, ispalo je otprilike ovako:

Pojednostavljena implementacija printf-a

// implementacija sprintf zauzima više od 10 kb i dodaje hrpu projektu. Mislim da je ovo // previše za funkcionalnost koja mi je potrebna // // Ispod je homebrew printf funkcija dampinga koja prihvaća: // -% d za znamenke // -% x za brojeve kao HEX // -% s za nizove // ​​- %% za simbol postotka // // Implementacija također podržava širinu vrijednosti kao i nulti padding // Ispis broja u međuspremnik (obrnutim redoslijedom) // Vraća broj ispisanih simbola size_t PrintNum (vrijednost bez predznaka int , uint8_t radix, char * buf, uint8_t width, char padSymbol) (// TODO provjeri negativan ovdje size_t len ​​= 0; // Ispiši broj do (char digit = value% radix; * (buf ++) = znamenka< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0); // Dodaj nulti 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" && pog<= "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); }


Moja implementacija je puno jednostavnija od one biblioteke, ali može sve što trebam - ispisati nizove, decimalne i heksadecimalne brojeve s formatiranjem (širina polja, završetak broja nulama s lijeve strane). Još ne zna ispisivati ​​negativne brojeve i brojeve s pomičnim zarezom, ali ga je lako zbrajati. Kasnije bih mogao omogućiti zapisivanje rezultata u string buffer (kao sprintf), a ne samo na USB.

Izvedba ovog koda je oko 150-200 kb/s zajedno s prijenosom preko USB-a i ovisi o broju (duljini) poruka, složenosti niza formata i veličini međuspremnika. Ova brzina je dovoljna za slanje nekoliko tisuća malih poruka u sekundi. Najvažnije je da se pozivi ne blokiraju.

Još čvršće: HAL niske razine

U principu, ovo je moglo završiti, ali sam primijetio da su dečki iz STM32GENERIC tek nedavno ulili novi HAL. Ono što je zanimljivo u vezi s tim je da postoji mnogo datoteka s imenom stm32f1xx_ll_XXXX.h. Otkrili su alternativnu i nižu implementaciju HAL-a. Oni. uobičajeni HAL pruža sučelje prilično visoke razine u stilu „uzmi ovaj niz i proslijedi mi ga ovdje pomoću ovog sučelja. Izvijestite o završetku s prekidom." Naprotiv, datoteke sa slovima LL u nazivu pružaju sučelje niže razine poput "postavi ove zastavice tog i takvog registra".

Mistika našeg grada

Vidjevši nove datoteke u spremištu STM32GENERIC, htio sam preuzeti kompletan komplet sa ST web stranice. Ali googlanje me dovelo samo do HAL-a (STM32 Cube F1) verzije 1.4, koji ne sadrži ove nove datoteke. STM32CubeMX grafički konfigurator također je ponudio ovu verziju. Pitao sam programere STM32GENERIC gdje su dobili novu verziju. Na moje iznenađenje, dobio sam poveznicu na istu stranicu, samo što je sada ponudio preuzimanje verzije 1.6. I Google je iznenada počeo "pronaći" novu verziju, kao i ažurirani CubeMX. Misticizam i još mnogo toga!


Zašto je to potrebno? U većini slučajeva sučelje visoke razine radi stvarno dobar posao. HAL (Hardware Abstraction Layer) u potpunosti opravdava svoj naziv – apstrahira kod iz procesorskih i hardverskih registara. Ali u nekim slučajevima, HAL ograničava let mašte programera, dok bi korištenjem apstrakcija niže razine bilo moguće učinkovitije implementirati zadatak. U mom slučaju to su GPIO i UART.

Pokušajmo osjetiti nova sučelja. Počnimo sa žaruljama. Nažalost, na internetu još nema dovoljno primjera. Pokušat ćemo razumjeti kod komentara funkcije, jer je s tim sve u redu.

Očigledno, te se stvari niske razine također mogu podijeliti u 2 dijela:

  • funkcije malo više razine u stilu običnog HAL-a - evo strukture inicijalizacije, inicijalizirajte periferiju za mene, molim.
  • Nešto niže razine postavljači i dobivači pojedinačnih zastava ili registara. Uglavnom, funkcije ove grupe su umetnute i samo u zaglavlju.
Prema zadanim postavkama, prvi su onemogućeni zadanim USE_FULL_LL_DRIVER. Dobro i k vragu s njima. Koristit ćemo potonje. Nakon malo šamanizma, dobio sam ovaj LED drajver

Morgul na LL HAL-u

// Klasa za enkapsulaciju rada s ugrađenim LED (s) // // Napomena: ova klasa inicijalizira odgovarajuće pinove u konstruktoru. // Možda neće raditi ispravno ako su objekti ove klase kreirani kao globalne varijable klase LEDDriver (const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver () (// omogući sat na GPIOC periferiji __HAL_RCC_GPIOC_IS_CLK_ENABLED (); // kao Init PC 13 izlaz LL_GPIO_SetPinMode (GPIOC, igla, LL_GPIO_MODE_OUTPUT) LL_GPIO_SetPinOutputType (GPIOC, igla, LL_GPIO_OUTPUT_PUSHPULL) LL_GPIO_SetPinSpeed ​​(GPIOC, igla, LL_GPIO_SPEED_FREQ_LOW)) void turnOn () (LL_GPIO_ResetOutputPin (GPIOC, pin)) void skretanja () (LL_GPIO_SetOutputPin (GPIOC , pin);) void preklop () (LL_GPIO_TogglePin (GPIOC, pin);)); void vLEDThread (void * pvParameters) (LEDDriver LED; // Samo trepnite jednom u 2 sekunde za (;;) (vTaskDelay (2000); led.turnOn (); vTaskDelay (100); led.turnOff (;))


Sve je vrlo jednostavno! Lijepo je što ovdje stvarno posao s registrima i zastavicama ide direktno. Nema dodatnih troškova za HAL GPIO modul, koji sam kompajlira na čak 450 bajtova, i kontrolu pinova iz STM32GENERIC koji izvlači još 670 bajtova. Ovdje je, općenito, cijela klasa sa svim pozivima umetnuta u funkciju vLEDThread, koja je veličine samo 48 bajtova!

Niasilil sam kontrolu sata kroz LL HAL. Ali to nije kritično, jer pozivanje __HAL_RCC_GPIOC_IS_CLK_ENABLED () iz običnog HAL-a je zapravo makronaredba koja samo postavlja nekoliko zastavica u određenim registrima.

Gumbi su jednako jednostavni

Gumbi preko LL HAL-a

// Dodjela pinova const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // inicijalizirati gumbi stvari povezanih void initButtons () (// omogućiti sat na GPIOC periferne __HAL_RCC_GPIOC_IS_CLK_ENABLED (); // gumb Postavi igle LL_GPIO_SetPinMode (GPIOC, SEL_BUTTON_PIN, LL_GPIO_MODE_INPUT) LL_GPIO_SetPinPull (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);) // Čitanje stanja gumba (prvo izvedi debounce) inline bool getButtonState (uintGPUTspin) (uintGPUTspin) (uint32etspin) (uint32etspin) vrati false ;)


S UART-om će sve biti zanimljivije. Dopustite da vas podsjetim na problem. Kada se koristi HAL, prijem se morao "napuniti" nakon svakog primljenog bajta. Način "uzmi sve" nije predviđen u HAL-u. A s LL HAL-om sve bi nam trebalo uspjeti.

Postavljanje pinova natjeralo me ne samo na razmišljanje, nego i na Referentni priručnik

Postavljanje UART pinova

// Pokretanje pinova u načinu alternativne funkcije LL_GPIO_SetPinMode (GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); // TX pin 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 pin


Ponavljamo inicijalizaciju UART'a za nova sučelja

UART inicijalizacija

// Priprema za inicijalizaciju 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); // Koristit ćemo UART prekid za dobivanje podataka HAL_NVIC_SetPriority (USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ (USART1_IRQn); // Omogući UART prekid na prijemu bajta LL_USART_EnableIT_RXNE (USART1); // Konačno omogućite periferni uređaj LL_USART_Enable (USART1);


Prekini sada. U prethodnoj verziji imali smo čak 2 funkcije – jedna je obrađivala prekid, a druga je bila povratni poziv (iz istog prekida) o primljenom bajtu. U novoj verziji smo konfigurirali prekid samo za primanje bajta, tako da ćemo primljeni bajt primiti odmah.

Prekinite UART

// Pohrani primljeni bajt inline void charReceivedCB (uint8_t c) (rxBuffer = c; lastReceivedIndex ++; // Ako je primljen EOL simbol, obavijestite GPS nit da je linija dostupna za čitanje ako (c == "\ n") vTaskNotifyGiveFromISR ( xGPSThread, NULL);) extern "C" void USART1_IRQHandler (void) (uint8_t byte = LL_USART_ReceiveData8 (USART1); gpsUart.charReceivedCB (byte);)


Veličina koda upravljačkog programa smanjena je sa 1242 na 436 bajtova, a potrošnja RAM-a sa 200 na 136 (od čega je 128 međuspremnik). Nije loše po mom mišljenju. Šteta je samo što ovo nije najproždrljiviji dio. Moglo bi se još nešto malo upisati, ali trenutno ne jurim posebno za potrošnjom resursa - još ih imam. A HAL sučelje visoke razine radi prilično dobro u slučaju ostalih perifernih uređaja.

Gledajući unatrag

Iako sam na početku ove faze projekta bio skeptičan prema HAL-u, ali sam ipak uspio prepisati sav rad s periferijama: GPIO, UART, I2C, SPI i USB. Ušao sam duboko u razumijevanje kako ti moduli rade i pokušao sam prenijeti znanje u ovom članku. Ali ovo uopće nije prijevod Referentnog priručnika. Naprotiv, radio sam u kontekstu ovog projekta i pokazao kako možete napisati periferne drajvere u čistom HAL-u.

Članak se pokazao kao manje-više linearna priča. Ali zapravo sam uzgojio niz grana u kojima sam istovremeno pilio u suprotnim smjerovima. Ujutro sam mogao naići na probleme s izvedbom neke arduino biblioteke i čvrsto odlučiti sve prepisati u HAL, a do večeri sam otkrio da je netko već zapisao DMA podršku u STM32GENERIC i imao sam želju da se vratim. Ili, na primjer, par dana bockanje s arduino sučeljima pokušavajući shvatiti kako je praktičnije prenositi podatke preko I2C, dok se na HAL-u to radi u 2 reda.

Općenito, postigao sam ono što sam želio. Glavni posao s periferijom je pod mojom kontrolom i napisan je u HAL-u. Arduino djeluje samo kao adapter za neke biblioteke. Istina, ostalo je još repova. Još uvijek se trebate pripremiti i ukloniti STM32GENERIC iz svog spremišta, ostavljajući samo nekoliko stvarno potrebnih klasa. Ali ovo čišćenje se više neće odnositi na ovaj članak.

Što se tiče Arudina i njenih klonova. I dalje mi se sviđa ovaj okvir. Pomoću njega možete napraviti prototip nečega bez da se mučite čitanjem priručnika i podatkovnih tablica. U principu, čak i krajnji uređaji mogu se napraviti s arduinom, ako nema posebnih zahtjeva za brzinu, potrošnju ili memoriju. U mom slučaju su ti parametri jako bitni pa sam morao prijeći na HAL.

Počeo sam raditi na stm32duino. Ovaj klon zaista zaslužuje pažnju ako želite imati "arduino" na STM32 i da sve radi iz kutije. Osim toga, pomno prate potrošnju RAM-a i bljeskalice. Nasuprot tome, sam STM32GENERIC je deblji i temelji se na monstruoznom HAL-u. Ali ovaj okvir se aktivno razvija i tek će biti gotov. Općenito, mogu preporučiti oba okvira s blagom preferencijom za STM32GENERIC za HAL i dinamičniji razvoj u ovom trenutku. Osim toga, internet za HAL je pun primjera i uvijek možete nešto dotjerati za sebe.

Još uvijek imam malo gađenja prema samom HAL-u. Knjižnica je preglomazna i ružna. Uzimam u obzir činjenicu da je knjižnica C, što dovodi do upotrebe dugih naziva funkcija i konstanti. Ali ipak, ovo nije knjižnica s kojom je zabavno raditi. Naprotiv, to je obvezna mjera.

U redu sučelje - i vas unutrašnjost tjera na razmišljanje. Ogromne funkcije s funkcionalnošću za sve prilike zahtijevaju gubitak resursa. A ako se možete boriti s dodatnim kodom u flashu pomoću optimizacije vremena povezivanja, tada se velika potrošnja RAM-a može izliječiti samo prepisivanjem u LL HAL.

Ali ni to nije uznemirujuće, već na nekim mjestima samo zanemarivanje resursa. Tako sam primijetio veliku prekomjernu upotrebu memorije u USB Middleware kodu (formalno, ovo nije HAL, ali se isporučuje kao dio STM32Cube). USB strukture zauzimaju 2,5 kb memorije. Štoviše, struktura USBD_HandleTypeDef (544 bajta) u velikoj mjeri ponavlja PCD_HandleTypeDef iz nižeg sloja (1056 bajtova) - također definira krajnje točke. Međuspremnici primopredajnika također su deklarirani na najmanje dva mjesta - USBD_CDC_HandleTypeDef i UserRxBufferFS / UserTxBufferFS.

Deskriptori se općenito deklariraju u RAM-u. Za što? Stalni su! Gotovo 400 bajtova u RAM-u. Srećom, neki od deskriptora su još uvijek konstantni (nešto manje od 300 bajtova). Deskriptori su nepromjenjive informacije. I ovdje postoji poseban kod koji ih krpi, i opet s konstantom. Pa čak i onaj koji je tamo već upisan. Iz nekog razloga, funkcije tipa SetBuffer prihvaćaju nekonstantni međuspremnik, što također sprječava stavljanje deskriptora i nekih drugih stvari u flash. Koji je razlog? Bit će popravljeno za 10 minuta!!!

Ili, struktura inicijalizacije je dio ručke objekta (na primjer, i2c). Zašto zadržati ovo nakon inicijalizacije perifernih uređaja? Zašto su mi potrebni pokazivači na neiskorištene strukture - na primjer, zašto su mi potrebni podaci povezani s DMA ako ga ne koristim?

I također dupli kod.

case USB_DESC_TYPE_CONFIGURATION: if (pdev-> dev_speed == USBD_SPEED_HIGH) (pbuf = (uint8_t *) pdev-> pClass-> GetHSConfigDescriptor (& len); pbuf = USB_DESC_TYPURE = *CONFIG ili GetFSConfig_; ;


Posebna konverzija u "unicode tip", koja se može izvesti u vremenu prevođenja. Štoviše, za to je dodijeljen poseban međuspremnik.

Zlostavljanje identiteta

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_DESC_TY = PE USB_DE ; dok (* desc! = "\ 0") (unicode = * desc ++; unicode = 0x00;)))


Nije fatalno, ali vas tjera na razmišljanje, je li HAL toliko dobar koliko o tome pišu apologeti? Pa, to nije ono što očekujete od knjižnice proizvođača i dizajnirane za profesionalce. Ovo su mikrokontroleri! Ovdje ljudi štede svaki bajt i svaka mikrosekunda je skupa. A ovdje, znate, postoji međuspremnik za funtu i pretvaranje konstantnih nizova u hodu. Treba napomenuti da se većina komentara odnosi na USB Middleware.

UPD: u HAL 1.6 I2C DMA prijenos Dovršeni povratni poziv također je prekinut. Oni. tu je kod potpuno nestao, što u slučaju slanja podataka preko DMA generira potvrdu, iako je to opisano u dokumentaciji. Prijem postoji, ali nema prijenosa. Morao sam se vratiti na HAL 1.4 za I2C modul, budući da postoji jedan modul - jedna datoteka.

Na kraju ću navesti potrošnju flasha i RAM-a raznih komponenti. U odjeljku Upravljački programi dao sam vrijednosti i za upravljačke programe temeljene na HAL-u i za upravljačke programe temeljene na LL HAL-u. U drugom slučaju, odgovarajući odjeljci iz HAL odjeljka se ne koriste.

Potrošnja memorije

Kategorija Podkategorija .tekst .rodata .podaci .bss
Sustav vektor prekida 272
lažni ISR ​​rukovatelji 178
libc 760
float matematika 4872
grijeh / cos 6672 536
glavni i sl 86
Moj kod Moj kod 7404 833 4 578
printf 442
Fontovi 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit gfx 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
USB međuverzija Jezgra 1740 333 2179
CDC 772
Vozači UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Gumbi LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
Razno 28 24
Ispis 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
hrpa (nije baš korištena) 1068
FreeRTOS hrpa 10240

To je sve. Bilo bi mi drago dobiti konstruktivne komentare, kao i preporuke ako se ovdje može nešto poboljšati.

Oznake:

  • HAL
  • STM32
  • STM32 kocka
  • arduino
Dodaj oznake

Popis članaka koji će pomoći naučiti STM32 mikrokontroler čak i za početnike. Naučite sve detaljno s primjerima, od treperenja LED-a do upravljanja motorom bez četkica. Primjeri koriste Standardnu ​​perifernu knjižnicu (SPL).

STM32F103 testna ploča, ST-Link programator i firmware za Windows i Ubuntu.

VIC (Nested vectored interrupt controller) - upravljački modul prekida. Postavljanje i korištenje prekida. Prioriteti prekida. Ugniježđeni prekidi.

ADC (analogno-digitalni pretvarač). Strujni krug i primjeri korištenja ADC-a u različitim načinima rada. Redovni i ubrizgani kanali. Korištenje ADC-a s DMA. Unutarnji termometar. Analogni čuvar.

Tajmeri opće namjene. Generiranje prekida u redovitim intervalima. Mjerenje vremena između dva događaja.

Hvatanje signala timerom na primjeru rada s ultrazvučnim senzorom HC-SR04

Korištenje mjerača vremena za rad s koderom.

PWM generacija. Kontrola svjetline LED dioda. Servo upravljanje (servo). Generacija zvuka.

Naznačio sam da je standardna knjižnica spojena na sustav. Zapravo, povezan je CMSIS - sustav generaliziranog strukturnog prikaza MK, kao i SPL - standardna periferna biblioteka. Razmotrimo svaki od njih:

CMSIS
To je skup datoteka zaglavlja i mali skup koda za objedinjavanje i strukturiranje rada s kernelom i periferijama MK-a. Zapravo, bez ovih datoteka nemoguće je normalno raditi s MK-om. Knjižnicu možete nabaviti na MK stranici.
Ova knjižnica, ako vjerujete u opis, stvorena je za objedinjavanje sučelja pri radu s bilo kojim MK-om iz obitelji Cortex. Međutim, u stvarnosti se ispostavilo da to vrijedi samo za jednog proizvođača, t.j. kada prijeđete na MK druge tvrtke, prisiljeni ste proučavati njezinu periferiju gotovo ispočetka.
Iako su datoteke koje se odnose na jezgru procesora MK identične za sve proizvođače (makar samo zato što imaju isti model jezgre procesora - koje pruža ARM u obliku ip-blokova).
Stoga je rad s takvim dijelovima jezgre kao što su registri, upute, prekidi i koprocesorski blokovi standardan za sve.
Što se periferije tiče, STM32 i STM8 (odjednom) su gotovo slični, to djelomično vrijedi i za ostale MC-e koje je objavio ST. U praktičnom dijelu pokazat ću vam kako je jednostavno koristiti CMSIS. Međutim, poteškoće u korištenju povezane su s nevoljkošću ljudi da pročitaju dokumentaciju i razumiju MK uređaj.

SPL
Standardna periferna knjižnica je standardna periferna knjižnica. Kao što ime govori, svrha ove biblioteke je stvoriti apstrakciju za MK periferiju. Knjižnica se sastoji od datoteka zaglavlja u kojima su deklarirane čovjeku čitljive konstante za konfiguriranje i rad s MK periferijama, kao i datoteke izvornog koda koje se sklapaju u samu knjižnicu za operacije s periferijama.
SPL je apstrakcija preko CMSIS-a, koja korisniku predstavlja zajedničko sučelje za sve MCU-ove, ne samo jednog proizvođača, već općenito za sve MCU-ove s Cortex-Mxx procesorskom jezgrom.
Vjeruje se da je prikladnije za početnike, jer omogućuje vam da ne razmišljate o tome kako periferni uređaji rade, ali kvaliteta koda, svestranost pristupa i ograničenja sučelja nameću određena ograničenja programeru.
Također, funkcionalnost knjižnice ne dopušta vam uvijek točnu implementaciju konfiguracije nekih komponenti kao što je USART (univerzalni sinkroni-asinkroni serijski port) u određenim uvjetima. U praktičnom dijelu opisat ću i način rada s ovim dijelom knjižnice.

Vrhunski povezani članci