Kako podesiti pametne telefone i računare. Informativni portal
  • Dom
  • vijesti
  • STM32F407(STM32F4-DISCOVERY) - Nestandardni pristup - Standardna biblioteka dio 1.

STM32F407(STM32F4-DISCOVERY) - Nestandardni pristup - Standardna biblioteka dio 1.

Softver potreban za razvoj. U ovom članku ću vam reći kako ga pravilno konfigurirati i povezati. Sva komercijalna okruženja kao što su IAR EWARM ili Keil uVision obično sami rade ovu integraciju, ali u našem slučaju sve će morati da se konfiguriše ručno, trošeći dosta vremena na to. Prednost je što imate priliku da shvatite kako sve to funkcionira iznutra, te dodatno fleksibilno sve prilagodite sebi. Prije početka postavljanja, razmotrite strukturu okruženja u kojem ćemo raditi:

Eclipse će se koristiti za jednostavno uređivanje datoteka implementacije funkcija ( .c), fajlovi zaglavlja ( .h), kao i datoteke za montažu ( .S). Pod "zgodnim" mislim na upotrebu dovršavanja koda, isticanje sintakse, refaktoring, navigaciju kroz funkcije i njihove prototipove. Datoteke se automatski šalju odgovarajućim kompajlerima koji generiraju objektni kod (u datotekama .o). Za sada, ovaj kod ne sadrži apsolutne adrese varijabli i funkcija, pa stoga nije pogodan za izvršenje. Rezultirajuće objektne datoteke spaja povezivač. Da bi znao koje dijelove adresnog prostora koristiti, sakupljač koristi posebnu datoteku ( .ld), koji se naziva linker skripta. Obično sadrži definicije adresa sekcija i njihove veličine (odjeljak koda mapiran na flash, varijabilni odjeljak mapiran na RAM, itd.).

Na kraju, linker generiše .elf datoteku (izvršni i povezivi format), koja sadrži, pored instrukcija i podataka, informacije za otklanjanje grešaka koje koristi debuger. Za normalno flešovanje sa vsprogom, ovaj format nije prikladan, jer to zahteva primitivniju memorijsku datoteku slike (na primer, Intel HEX - .hex). Za generiranje, postoji i alat iz Sourcery CodeBench skupa (arm-none-eabi-objcopy), koji se savršeno integrira u eclipse koristeći instalirani ARM dodatak.

Za implementaciju samog otklanjanja grešaka koriste se tri programa:

  1. sama eclipse, koja omogućava programeru da "vizuelno" koristi otklanjanje grešaka, hoda duž linija, pomera kursor miša preko varijabli da vidi njihove vrednosti i druge pogodnosti
  2. arm-none-eabi-gdb - GDB klijent - debager, koji je skriven pod kontrolom eclips-a (preko stdin-a) kao reakcija na akcije navedene u paragrafu 1. Zauzvrat, GDB se povezuje na OpenOCD Debug server, a sve ulazne komande GDB debuger prevodi u komande razumljive za OpenOCD. GDB kanal<->OpenOCD je implementiran preko TCP protokola.
  3. OpenOCD je debug server koji može komunicirati direktno sa programerom. Pokreće se ispred klijenta i čeka TCP vezu.

Ova šema vam može izgledati veoma beskorisna: zašto koristiti klijenta i servera odvojeno i još jednom izvoditi prevod komandi, ako se sve ovo može uraditi sa jednim debagerom? Stvar je u tome što takva arhitektura teoretski omogućava da se na jednostavan način obavi razmjena klijenta i servera. Na primjer, ako trebate koristiti drugi programator umjesto versaloon-a, koji neće podržavati OpenOCD, ali će podržavati drugi poseban Debug server (na primjer, texane / stlink za stlink programator - koji se nalazi na STM32VLDiscovery ploči za otklanjanje grešaka), onda umjesto toga pokretanja OpenOCD-a, jednostavno ćete pokrenuti željeni server i sve bi trebalo da radi, bez ikakvih dodatnih pokreta. Istovremeno, moguća je i obrnuta situacija: recimo da želite da koristite okruženje IAR EWARM umesto paketa Eclipse + CodeBench, zajedno sa versaloon-om. IAR ima svoj ugrađeni Debug klijent koji će uspješno komunicirati sa OpenOCD-om i upravljati njime, kao i primati potrebne podatke kao odgovor. Međutim, sve ovo ponekad ostaje samo u teoriji, budući da standardi za komunikaciju između klijenta i servera nisu striktno regulirani, a na nekim mjestima se mogu razlikovati, međutim, konfiguracije koje sam naveo sa st-link + eclipse i IAR + versaloon bile uspješne za mene.

Obično klijent i server rade na istoj mašini, a veza sa serverom se javlja na lokalni host:3333(Za openocd), ili lokalni host:4242(za teksan/stlink st-util). Ali niko se ne trudi otvoriti port 3333 ili 4242 (i proslijediti ovaj port na ruteru na eksternu mrežu) i vaše kolege iz drugog grada će moći da se povežu i otklone vaš komad željeza. Ovaj trik često koriste embedderi koji rade na udaljenim lokacijama s ograničenim pristupom.

Počinjemo

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, onda bi bilo logičnije da ga nazovete "stm32_md_vl") :

Kliknite na Završi, minimizirajte ili zatvorite prozor dobrodošlice. Dakle, projekat je kreiran i folder stm32_ld_vl bi se trebao pojaviti u vašem radnom prostoru. Sada ga treba popuniti potrebnim bibliotekama.

Kao što ste shvatili iz naziva projekta, kreiraću projekat za prikaz lenjira linija vrijednosti niske gustine(LD_VL). Da biste kreirali projekat za druge mikrokontrolere, morate zamijeniti sve datoteke i definicije u čijem nazivu se nalazi _LD_VL (ili_ld_vl) na one koje su Vam potrebne, prema tabeli:

Pogled vladara Oznaka Mikrokontroleri (x se može promijeniti)
Linija vrijednosti niske gustine _LD_VL STM32F100x4 STM32F100x6
niske gustine _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Linija vrijednosti srednje gustine _MD_VL STM32F100x8 STM32F100xB
srednje gustine
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Linija vrijednosti visoke gustine _HD_VL STM32F100xC STM32F100xD STM32F100xE
velika gustoća _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-gustina _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Linija povezivanja _CL STM32F105xx i STM32F107xx

Da biste razumjeli logiku tabele, morate biti upoznati sa STM32 oznakama. Odnosno, ako imate VLDiscovery, onda ćete dalje morati zamijeniti sve što se odnosi na _LD_VL sa _MD_VL, budući da je čip STM32F100RB koji se odnosi na liniju vrijednosti srednje gustine zalemljen u otkrivanju.

Dodavanje CMSIS i STM32F10x standardne biblioteke perifernih uređaja projektu

CMSIS(Cortex Microcontroller Software Interface Standard) - standardizovana biblioteka za rad sa Cortex mikrokontrolerima koja implementira nivo HAL (Hardware Abstraction Layer), odnosno omogućava vam da apstrahujete od detalja rada sa registrima, tražeći adrese registra pomoću tablica podataka, itd. Biblioteka je skup izvornih kodova u C i Asm. Osnovni (Core) dio biblioteke je isti za sve Cortexe (bilo da se radi o ST, NXP, ATMEL, TI ili bilo kome drugom), a razvio ga je ARM. Drugi dio biblioteke je odgovoran za periferne uređaje, koji se prirodno razlikuju od proizvođača do proizvođača. Stoga, na kraju, punu biblioteku i dalje distribuira proizvođač, iako se dio kernela i dalje može preuzeti zasebno sa ARM web stranice. Biblioteka sadrži definicije adresa, kod za inicijalizaciju generatora takta (pogodno konfigurabilan sa definicijama), i sve ostalo što štede programera od ručnog uvođenja u svoje projekte definicije adresa svih perifernih registara i definicije bitova vrednosti ove registre.

Ali momci iz ST su otišli dalje. Osim CMSIS podrške, oni pružaju još jednu biblioteku za STM32F10x pod nazivom Standardna periferna biblioteka(SPL) koji se može koristiti kao dodatak CMSIS-u. Biblioteka omogućava brži i praktičniji pristup periferiji, a takođe kontroliše (u nekim slučajevima) ispravan rad sa periferijama. Stoga se ova biblioteka često naziva skupom drajvera za periferne module. Prati ga paket primjera podijeljenih u kategorije za različite periferne uređaje. Biblioteka je takođe dostupna ne samo za STM32F10x, već i za druge serije.

Možete preuzeti cijelu SPL+CMSIS verziju 3.5 ovdje: STM32F10x_StdPeriph_Lib_V3.5.0 ili na web stranici ST. Raspakujte arhivu. Kreirajte fascikle CMSIS i SPL u fascikli projekta i počnite kopirati datoteke u svoj projekat:

Šta kopirati

Gdje kopirati (uzimajući u obzir
da je fascikla projekta stm32_ld_vl)

Opis datoteke
Biblioteke/CMSIS/CM3/
CoreSupport/ core_cm3.c
stm32_ld_vl/CMSIS/ core_cm3.c Opis Cortex M3 jezgre
Biblioteke/CMSIS/CM3/
CoreSupport/ core_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h Zaglavlja opisa kernela

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c inicijalizacijske funkcije i
kontrola sata
Biblioteke/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Zaglavlja za ove funkcije
Biblioteke/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Osnovni opis perifernih uređaja
Biblioteke/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Upozorenje ekstenzija datoteke CAPITAL S)
Datoteka vektorske tabele
prekida i init-ami na asm
Project/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h Šablon za prilagođavanje
perifernih modula

inc/ *
stm32_ld_vl/SPL/inc/ * SPL fajlovi zaglavlja
Libraries/STM32F10x_StdPeriph_Driver/
src/ *
stm32_ld_vl/SPL/src/ * Implementacija SPL

Nakon kopiranja, idite na Eclipse i izvršite Refresh u kontekstualnom meniju projekta. Kao rezultat, u Project Exploreru bi trebali dobiti istu strukturu kao na slici desno.

Možda ste primetili da postoje fascikle za različite IDE u fascikli Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ (različiti kompajleri se koriste u različitim IDE-ovima). Odabrao sam Ride7 IDE jer koristi kompajler GNU Tools za ARM Embedded, koji je kompatibilan sa našim Sourcery CodeBench.

Cijela biblioteka je konfigurisana pomoću pretprocesora (koristeći defines), što će vam omogućiti da riješite sve potrebne grane u fazi kompilacije (ili bolje rečeno, čak i prije nje) i izbjegnete opterećenje samog kontrolera (što bi se primijetilo ako bi konfiguracija je izvršena u RunTime). Na primjer, sva oprema je različita za različite lenjire i stoga, kako bi biblioteka "znala" koji ravnalo želite koristiti, od vas se traži da dekomentirate u datoteci stm32f10x.h jedna od definicija (koja odgovara vašoj liniji):

/* #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čujem da to radite. Za sada nećemo dirati datoteke biblioteke, ali ćemo definirati kasnije koristeći postavke kompajlera u Eclipseu. A onda će Eslipse pozvati kompajler sa ključem -D STM32F10X_LD_VL, što je za pretprocesor apsolutno ekvivalentno situaciji ako ste dekomentirali "#define STM32F10X_LD_VL". Dakle, nećemo mijenjati kod, kao rezultat toga, ako želite, jednog dana ćete moći premjestiti biblioteku u poseban direktorij, a ne kopirati je u mapu svakog novog projekta.

Linker skripta

U kontekstualnom meniju projekta izaberite Novo->Datoteka->Ostalo->Općenito->Datoteka, Sljedeće. Odaberite korijenski folder projekta (stm32_ld_vl). Unesite naziv datoteke "stm32f100c4.ld" (ili "stm32f100rb.ld" za otkrivanje). Sada kopirajte i zalijepite u eclipse:

ENTRY(Reset_Handler) MEMORIJA (FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K ) _estack( + ORIGINING) 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 drugi podaci idu u FLASH * / .text: ( . = ALIGN(4); /* Kod */ *(.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 = .); ) >init_array_end = .); = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); ) >FLASH .fini_array: (PROVIDE_HIDDEN (__fini_array_KEEP = .); ); (.fini_array*)) KEEP (*(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 = .; /* definira globalni simbol na kraju podataka */ ) >RAM /* Neinicijalizirani podaci */ . = ALIGN(4); .bss: ( /* Ovo se koristi od strane pokretanja da bi inicijalizirao .bss sekciju */ _sbss = .; /* definiraj globalni simbol na početku bss */ __bss_start__ = _sbss; *(.bss) *(.bss *) *(COMMON) .= ALIGN(4); _ebss = .; /* definiše globalni simbol na kraju bss */ __bss_end__ = _ebss; ) >RAM PROVIDE(end = _ebss); PROVIDE(_end = _ebss); PROVIDE(__HEAP_START = _ebss); /* User_heap_stack odjeljak, koji se koristi 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(*) ) )

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

Sačuvamo fajl.

Postavljanje gradnje (C/C++ gradnja)

Idite na Project->Properties->C/C++ Build-> Settings->Tool Settings i počnite postavljati alate za izradu:

1) Target Precessor

Mi biramo pod kojim Cortex jezgrom će kompajler raditi.

  • Procesor: cortex-m3

2) ARM Sourcery Linux GCC C kompajler -> Preprocesor

Dodajemo dva define-a tako što ih propuštamo kroz prekidač -D do kompajlera.

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

3) ARM Sourcery Linux GCC C kompajler -> Direktoriji

Dodajte staze za uključivanje biblioteka.

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

Sada, na primjer, ako napišemo:

#include "stm32f10x.h

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

4) ARM Sourcery Linux GCC C kompajler -> Optimizacija

Omogućite optimizaciju funkcija i podataka

  • -ffunction-sections
  • -fdata-sekcije

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

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

Dodajte putanju našoj linker skripti: "$(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, podešavanje je završeno. UREDU.

Od stvaranja projekta uradili smo mnogo stvari, a nešto Eclipse možda neće primijetiti, pa mu moramo reći da revidira strukturu projektnih datoteka. Da biste to učinili, morate učiniti iz kontekstnog menija projekta Indeks -> rebuild.

Zdravo LED diode na STM32

Vrijeme je da kreirate glavnu datoteku projekta: File -> New -> C/C++ -> Source File. sljedeći. Naziv datoteke Izvorni fajl: 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 sat RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Omogući TIM2 Periph sat // Onemogući JTAG za oslobađanje LED PIN RCC->APENR RCC->APB2 |= AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Brisanje bitova kontrolnog registra PB4 i PB5 GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MoDE5 | GPIO_CRL_Pill izlaz na maks. 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // 1 T Interrupt/1IM_c | // Omogući tim2 prekid TIM2->CR1 |= TIM_CR1_CEN; // Početni broj NVIC_EnableIRQ(TIM2_IRQn); // Omogući IRQ while(1); // Beskonačna petlja ) void TIM2_IRQHandler(void) ( TIM2->SR &= IF ~TIM_SR_U ; //Očisti UIF oznaku if (1 == (i++ & 0x1)) ( GPIOB->BSRR = GPIO_BSRR_BS4; // Postavi PB4 bit GPIOB->BSRR = GPIO_BSRR_BR5; // Resetuj PB5 bit ) else ( GPIOB->BSRR = GPIO_BSRR_B S5; // Postavi PB5 bit GPIOB->BSRR = GPIO_BSRR_BR4; // Resetuj PB4 bit ) )

Iako smo uključili SPL biblioteke, ona se ovdje nije koristila. Svi pristupi na terenu kao što je RCC->APB2ENR u potpunosti su dokumentirani 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 generisan od elf-a pomoću ugrađenih alata. Treperimo datoteku i vidimo kako LED diode trepću 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 svoju putanju do radnog prostora.

Za STM32VLDiscovery

Kod se malo razlikuje od onog koji sam dao gore za moj šal za otklanjanje grešaka. Razlika je u pinovima na kojima "vise" LED diode. Ako su na mojoj ploči to bili PB4 i PB5, onda su u Discoveryju 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 // Obriši PC8 i PC9 kontrolnih bitova registra & GPIOC->CRH ~ (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Konfigurišite PC8 i PC9 kao Push Pull izlaz na maksimalnih 10Mhz GPIOC->CRH |= GPIO_CRH_MODE8_0 | TGPIO_CRH0C = GPIO_CRH0; >ARR = 1000; // 1 prekid/sek (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Omogući tim2 prekid TIM2->CR1 |= TIM_CR1_CEN; // Broj starta NVIC_EnableIRQ(TIM2_IRQn); // Omogući IRQn while(1); // Beskonačna petlja ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF; //Očisti UIF oznaku if (1 == (i++ & 0x1)) ( GPIOC->BSRR = GPIO_BSRR_BS8 ; / / Postavi PC8 bit GPIOC->BSRR = GPIO_BSRR_BR9; // Reset PC9 bit ) else ( GPIOC->BSRR = GPIO_BSRR_BS9; // Postavi PC9 bit GPIOC->BSRR = GPIO_BSRR_BR8; // Reset PC8 bit ) )

Pod Windows-om, možete flešovati rezultujući heksadecimalni (/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) uslužnim programom iz ST.

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

Ekstenzija datoteke se neće promijeniti (hex će ostati kao što je bio), ali će se promijeniti format datoteke. I tek nakon toga možete učiniti:

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

Inače, na račun ekstenzije i formata: obično se binarne datoteke označavaju ekstenzijom .bin, dok se datoteke Intel HEX formata nazivaju ekstenzijom .hex. Razlika u ova dva formata je više tehnička nego funkcionalna: binarni format jednostavno sadrži bajtove instrukcija i podataka koje će programer jednostavno upisati u kontroler "kao što jesu". IntelHEX, s druge strane, nema binarni format, već tekstualni: potpuno isti bajtovi su podijeljeni na 4 bita i predstavljeni su znak po znak u ASCII formatu, a koriste se samo znakovi 0-9, AF ( bin i heksadecimalni su sistemi brojeva sa više baza, odnosno, 4 bita u bin mogu biti predstavljena kao jedna cifra u heksadecimala). Dakle, ihex format je više od 2 puta veći od obične binarne datoteke (svaka 4 bita se zamjenjuju bajtom + prijelom reda radi lakšeg čitanja), ali se može čitati u običnom uređivaču teksta. Stoga, ako ćete nekome poslati ovu datoteku, ili je koristiti u drugim programskim programima, onda je preporučljivo preimenovati je u stm32_md_vl.bin, kako ne biste zavarali one koji će pogledati njen naziv.

Tako smo postavili sklop firmvera za stm32. Sledeći put ću ti reći kako

Prilikom kreiranja prve aplikacije na STM32 mikrokontroleru, postoji nekoliko načina. Prvi, klasični, uzimamo tačan opis kontrolera na web stranici www.st.com, koji se pojavljuje pod nazivom "Referentni priručnik" i čitamo opis perifernih registara. Zatim pokušavamo da ih zapišemo i vidimo kako periferne jedinice rade. Čitanje ovog dokumenta je vrlo korisno, ali u prvoj fazi savladavanja mikrokontrolera, ovo se može napustiti, čudno. STMicroelectronics inženjeri su napisali standardnu ​​biblioteku drajvera za periferne uređaje. Štaviše, napisali su mnoge primjere korištenja ovih drajvera, koji mogu smanjiti programiranje vaše aplikacije na pritiskanje tipki Ctrl + C i Ctrl + V, nakon čega slijedi mala promjena u primjeru drajvera kako bi odgovarala vašim potrebama. Stoga je povezivanje biblioteke drajvera periferije sa vašim projektom drugi način izgradnje aplikacije. Osim brzine pisanja, postoje i druge prednosti ove metode: univerzalnost koda i korištenje drugih vlasničkih biblioteka, kao što su USB, Ethernet, kontrola pogona itd., koje su navedene u izvorima i koriste standardni drajver periferije. Postoje i nedostaci ove metode: Tamo gdje možete proći s jednom linijom koda, standardni STM32 drajver za periferiju će napisati 10. Sama biblioteka periferije je također dostupna kao izvorni fajlovi, tako da možete pratiti koji dio registruje ovo ili ta funkcija se menja. Po želji, biće moguće preći sa drugog načina pisanja programa na prvi komentarisanjem dela koda koji samostalno koristi standardnu ​​biblioteku, koja direktno kontroliše periferni registar. Kao rezultat takve akcije, dobit ćete u brzini kontrole, količini RAM-a i ROM-a, a izgubiti u univerzalnosti koda. U svakom slučaju, inženjeri Promelectronice preporučuju korištenje biblioteke standardnih perifernih uređaja barem u prvoj fazi.

Najveće poteškoće očekuju programera prilikom povezivanja biblioteke sa svojim projektom. Ako ne znate kako to učiniti, možete potrošiti dosta vremena na ovaj događaj, što je u suprotnosti sa samom idejom korištenja gotovog vozača. Materijal je posvećen povezivanju standardne biblioteke sa bilo kojom porodicom STM32.

Svaka STM32 porodica ima svoju biblioteku standardnih perifernih uređaja. To je zbog činjenice da je sama periferija drugačija. Na primjer, periferni uređaji STM32L kontrolera imaju funkciju uštede energije kao jedan od zadataka, što podrazumijeva dodavanje kontrolnih funkcija. Klasičan primjer je ADC, koji u STM32L ima mogućnost isključivanja hardvera, u nedostatku naredbe za konverziju duže vrijeme - jedna od posljedica zadatka uštede energije. ADC kontroleri iz familije STM32F nemaju takvu funkciju. U stvari, zbog prisustva hardverske razlike na periferiji, imamo različite biblioteke drajvera. Pored očigledne razlike u funkcijama kontrolera, došlo je i do poboljšanja perifernih uređaja. Dakle, periferni uređaji kontrolera porodica koji su kasnije pušteni mogu biti promišljeniji i praktičniji. Na primjer, periferni uređaji STM32F1 i STM32F2 kontrolera imaju razlike u kontroli. Po mišljenju autora, upravljanje periferijama STM32F2 je praktičnije. I ovo je razumljivo zašto: porodica STM32F2 je objavljena kasnije i to je omogućilo programerima da uzmu u obzir neke nijanse. Shodno tome, za ove porodice - individualne periferne kontrolne biblioteke. Ideja gore navedenog je jednostavna: na stranici mikrokontrolera koji ćete koristiti nalazi se periferna biblioteka pogodna za to.

Uprkos razlikama u periferiji u porodicama, vozači kriju 90% razlika u sebi. Na primjer, funkcija podešavanja ADC-a koja je gore spomenuta izgleda isto za sve porodice:

void ADC_Init(ADC_Nom, ADC_Param),

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

ADC_Param - pokazivač strukture podataka, kako konfigurirati ADC (od čega početi, koliko kanala digitalizirati, da li to raditi ciklično, itd.)

10% porodičnih razlika, u ovom primjeru, koje će se morati ispraviti prilikom prelaska iz jedne STM32 porodice u drugu, skriveno je u ADC_Param strukturi. U zavisnosti od porodice, broj polja u ovoj strukturi može biti različit. Opšti dio ima istu sintaksu. Dakle, prenos aplikacije za jednu STM32 familiju, napisanu na osnovu standardnih perifernih biblioteka, u drugu je vrlo jednostavan. U smislu univerzalizacije rješenja baziranih na mikrokontrolerima, STMicroelectronics je neodoljiv!

Dakle, preuzeli smo biblioteku za korišteni STM32. Šta je sledeće? Zatim moramo kreirati projekat i povezati potrebne datoteke s njim. Razmislite o kreiranju projekta koristeći IAR Embedded Workbench razvojno okruženje kao primjer. Pokrećemo razvojno okruženje i idemo na karticu "Projekt", odaberite stavku "Kreiraj projekat" za kreiranje projekta:

U novom projektu koji se pojavi unesite postavke tako što ćete preći mišem preko naziva projekta, pritisnuti desnu tipku miša i odabrati "Opcije" iz padajućeg izbornika:

Memorijska područja RAM-a i ROM-a:

Kada kliknete na dugme “Sačuvaj”, okruženje će ponuditi da upiše novu datoteku opisa kontrolera u fasciklu projekta. Autor preporučuje kreiranje individualne *.icp datoteke za svaki projekat i pohranjivanje u direktorij projekta.

Ako ćete otklanjati greške u svom projektu u krugu, što se preporučuje, unesite tip debuggera koji ćete koristiti:

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



Od sada je naš projekat bez biblioteka spreman za kompajliranje i učitavanje u kontroler. Ostala okruženja kao što su Keil uVision4, Resonance Ride7, itd. morat će slijediti iste korake.

Ako upišete red 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"

označavajući lokaciju ove datoteke, ili kopiranjem ove datoteke u fasciklu projekta, tada će neke memorijske oblasti biti povezane sa perifernim registrima odgovarajuće porodice. Sama datoteka se nalazi u folderu standardne periferne biblioteke u odjeljku: \CMSIS\CM3\DeviceSupport\ST\STM32F10x (ili sličnog imena za druge porodice). Od sada, adresu perifernog registra kao broj zamjenjujete njenim imenom. Čak i ako nećete koristiti funkcije standardne biblioteke, preporučuje se da napravite takvu vezu.

Ako ćete koristiti prekide u svom projektu, preporučljivo je uključiti početnu datoteku sa ekstenzijom *.s, koja se nalazi duž putanje \CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\iar, ili slično, za druge porodice. Važno je napomenuti da svako okruženje ima svoj fajl. Shodno tome, ako koristimo IAR EWB, moramo preuzeti datoteku iz IAR foldera. To je zbog male razlike u sintaksi okruženja. Stoga, kako bi projekat odmah započeo, STMicroelectronics inženjeri su napisali nekoliko varijanti startnih datoteka za nekoliko najpopularnijih razvojnih okruženja. Većina STM32 porodica ima jednu datoteku. Porodica STM32F1 ima nekoliko datoteka za pokretanje:

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

Dakle, u zavisnosti od porodice, podporodice i razvojnog okruženja, dodajte datoteku za pokretanje projektu:

Ovo je mjesto gdje mikrokontroler završava kada se program pokrene. Prekid sekvencijalno poziva funkciju SystemInit(), a zatim __iar_program_start. Druga funkcija resetuje ili upisuje unaprijed definirane vrijednosti globalnih varijabli, nakon čega se prebacuje na glavni() korisnički program. Funkcija SystemInit() postavlja sat mikrokontrolera. Ona je ta koja odgovara na pitanja:

  • Trebam li preći na eksterni kristal (HSE)?
  • Kako pomnožiti frekvenciju iz HSI/HSE?
  • Da li je potrebno povezati red za preuzimanje naredbi?
  • Koje je kašnjenje potrebno prilikom učitavanja naredbe (zbog male brzine Flash memorije)
  • Kako podijeliti taktiranje perifernih sabirnica?
  • Da li kod treba biti smješten u vanjski RAM?

Funkcija SystemInit() može se ručno napisati u vašem projektu. Ako ovu funkciju izdate kao praznu, tada će kontroler raditi na internom RC generatoru sa frekvencijom od oko 8 MHz (ovisno o vrsti porodice). Opcija 2 - povežite fajl system_stm32f10x.c sa projektom (ili sličan po imenu u zavisnosti od tipa porodice koji se koristi), koji se nalazi u biblioteci duž putanje: Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x. Ova datoteka sadrži funkciju SystemInit(). Obratite pažnju na frekvenciju eksternog HSE_VALUE kristala. Ovaj parametar je postavljen u datoteci zaglavlja stm32f10x.h. Standardna vrijednost je 8 i 25MHz, ovisno o STM32 porodici. Glavni zadatak funkcije SystemInit () je prebaciti sat na vanjski kvarc i pomnožiti ovu frekvenciju na određeni način. Šta se dešava ako je vrijednost HSE_VALUE 8MHz, jezgra treba da radi na 72MHz, a u stvari ploča ima kvarc od 16MHz? Kao rezultat takvih pogrešnih radnji, jezgro će dobiti takt od 144 MHz, što može biti iznad garantovanog rada sistema na STM32. One. kada uključujete datoteku system_stm32f10x.c, morat ćete navesti vrijednost HSE_VALUE. Sve ovo znači da fajlovi system_stm32f10x.c, system_stm32f10x.h i stm32f10x.h (ili slični po imenu za druge porodice) moraju biti individualni za svaki projekat. I

STMicroelectronics inženjeri su kreirali alat za konfiguraciju sata, koji vam omogućava da ispravno konfigurišete sistemski sat. Ovo je Excel datoteka koja generiše datoteku system_stm32xxx.c (sličnu po imenu datoj porodici porodica) nakon postavljanja sistemskih ulaznih i izlaznih parametara. Razmotrite njegov rad na primjeru porodice STM32F4.

Opcije: interni RC oscilator, interni RC oscilator sa množenjem frekvencije ili eksterni kristal sa množenjem frekvencije. Nakon odabira izvora takta, unosimo parametre željene konfiguracije sistema, kao što su ulazna frekvencija (kada se koristi eksterni kvarc), frekvencija takta jezgra, djelitelj frekvencije takta perifernih magistrala, rad bafera za dohvaćanje instrukcija, i drugi. Klikom na dugme “Generiraj” dobijamo prozor


Povezivanje datoteke system_stm32f4xx.c i njenih analoga zahtijevat će povezivanje druge datoteke periferne standardne biblioteke. Za kontrolu sata postoji čitav skup funkcija koje se pozivaju iz datoteke system_stm32xxxxxx.c. Ove funkcije se nalaze u datoteci stm32f10x_rcc.c i njenom zaglavlju. Shodno tome, prilikom povezivanja fajla system_stm32xxxxxx.c sa projektom, potrebno je povezati stm32f10x_rcc.c, inače će linker okruženja prijaviti odsustvo opisa funkcije sa imenom RCC_xxxxxxx. Navedena datoteka se nalazi u perifernoj biblioteci duž putanje: Libraries\STM32F10x_StdPeriph_Driver\src, i njeno zaglavlje \Libraries\STM32F10x_StdPeriph_Driver\inc.

Datoteke zaglavlja drajvera periferije su povezane u datoteci stm32f10x_conf.h, koju referencira stm32f10x.h. Datoteka stm32f10x_conf.h je jednostavno skup datoteka zaglavlja drajvera za određene periferije kontrolera koje treba uključiti u projekat. U početku, sva "#include" zaglavlja su označena kao komentari. Povezivanje zaglavlja fajla periferije se sastoji u uklanjanju komentara iz odgovarajućeg naziva fajla. U našem slučaju, ovo je red #include "stm32f10x_rcc.h". Očigledno, datoteka stm32f10x_conf.h je individualna za svaki projekat, jer različiti projekti koriste različite periferne uređaje.

I posljednji. Morate navesti nekoliko direktiva za pretprocesor kompajlera i putanje do datoteka zaglavlja.



Putanja do datoteka zaglavlja mogu biti različite, u zavisnosti od lokacije periferne biblioteke u odnosu na fasciklu projekta, ali prisustvo “USE_STDPERIPH_DRIVER” je obavezno kada povezujete drajvere periferije standardne biblioteke.

Dakle, povezali smo standardnu ​​biblioteku sa projektom. Štaviše, povezali smo jedan od standardnih perifernih drajvera na projekat koji kontroliše sistemski sat.

Naučili smo kako bibliotečki uređaj izgleda iznutra, a sada nekoliko riječi o tome kako izgleda izvana.



Stoga, uključivanje datoteke zaglavlja stm32f10x.h u aplikaciju uključuje i druge datoteke zaglavlja i datoteke koda. Neki od prikazanih na slici su gore opisani. Nekoliko riječi o ostalom. Datoteke STM32F10x_PPP.x su datoteke drajvera periferije. Primjer povezivanja takve datoteke prikazan je gore, ovo je RCC - periferija kontrole sistemskog sata. Ako želimo da povežemo drajvere drugih perifernih uređaja, tada se naziv povezanih datoteka dobija zamenom „PPP“ imenom periferije, na primer, ADC - STM32F10x_ADC.s, ili I/O portovi STM32F10x_GPIO.s, ili DAC - STM32F10x_DAC.s. Općenito, intuitivno je jasno koju datoteku treba povezati prilikom povezivanja date periferije. Fajlovi "misc.c", "misc.h" su uglavnom isti STM32F10x_PPP.x, oni kontrolišu samo kernel. Na primjer, postavljanje vektora prekida, koji je ugrađen u kernel, ili upravljanje SysTick tajmerom, koji je dio kernela. Datoteke xxxxxxx_it.c opisuju NMI vektore kontrolera. Mogu se dopuniti perifernim vektorima prekida. Datoteka core_m3.h opisuje CortexM3 jezgro. 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 CortexM jezgrene biblioteke, nakon čega ju je ARM standardizovao i distribuirao drugim proizvođačima mikrokontrolera. Tako će prelazak na STM32 sa kontrolera drugih proizvođača sa CortexM jezgrom biti malo lakši.

Dakle, možemo povezati standardnu ​​perifernu biblioteku na bilo koju STM32 familiju. Onoga ko je to naučio čeka nagrada: vrlo jednostavno programiranje mikrokontrolera. Biblioteka, pored drajvera u obliku izvornih datoteka, sadrži mnogo primjera upotrebe perifernih uređaja. Kao primjer, razmislite o kreiranju projekta koji uključuje izlaze za poređenje tajmera. Uz tradicionalni pristup, pažljivo ćemo proučiti opis registara ove periferije. Ali sada možemo proučiti tekst programa koji radi. Idemo u mapu primjera standardnih perifernih uređaja, koja se nalazi duž putanje ProjectSTM32F10x_StdPeriph_Examples. Ovdje su mape primjera s nazivima korištenih perifernih uređaja. Idemo u folder "TIM". Tajmeri u STM32 imaju mnogo funkcija i postavki, tako da je nemoguće demonstrirati mogućnosti kontrolera na jednom primjeru. Stoga, unutar navedenog direktorija, postoji mnogo primjera upotrebe tajmera. Zanima nas generiranje PWM signala pomoću tajmera. Idemo u folder "7PWM_Output". Unutra se nalazi opis programa na engleskom i set fajlova:

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

Ako projekat nema prekide, tada se sadržaj u potpunosti nalazi u datoteci main.c. Kopirajte ove datoteke u direktorij projekta. Nakon kompajliranja projekta, dobićemo program za STM32, koji će konfigurisati tajmer i I/O portove za generisanje 7 PWM signala iz tajmera 1. Zatim možemo prilagoditi već napisani kod našem zadatku. 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 svrhom za one koji znaju malo engleski. Radi jasnoće, evo dijela koda uzetog primjera:

/* Konfiguracija vremenske baze */ TIM_TimeBaseStructure.TIM_Prescaler = 0; // nema predskaler brojača impulsa (16-bitni registar) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // smjer brojanja prema gore TIM_TimeBaseStructure TIM_Period = TimerPeriod; // brojanje do vrijednosti TimerPeriod (konstanta u programu) TIM_TimeBaseStructure.TIM_ClockDivision = 0; // nema pred-podjele brojača 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 tajmera 1 (unos podataka u ovu // varijablu je iznad) /* Konfiguracija kanala 1, 2, 3 i 4 u PWM modu */ // konfiguracija PWM izlaza TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2 ; // PWM2 način rada TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // omogući izlaz PWM signala tajmera TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // omogući komplementarni izlaz PWM tajmera TIM_OCInitStructure.TIM_Pulse = Channel1Pulse; // širina impulsa Channel1Pulse je 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; // postavljanje sigurnog PWM izlaznog stanja 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); // timer1 kanal 2 PWM registri TIM_OCInitStructure.TIM_Pulse = Channel3Pulse; // mijenjamo širinu impulsa u varijabli OCInitStructure i unosimo je u TIM_OC3Init(TIM1, &TIM_OCInitStructure); // timer1 kanal 3 PWM registri TIM_OCInitStructure.TIM_Pulse = Channel4Pulse; // mijenjamo širinu impulsa u varijabli OCInitStructure i unosimo je u TIM_OC4Init(TIM1, &TIM_OCInitStructure); // timer1 kanal 4 PWM registri /* Omogućavanje brojača TIM1 */ TIM_Cmd(TIM1, ENABLE); // start timer1 /* Omogući glavni izlaz TIM1 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); // omogućavanje tajmera 1 usporedi izlaze

Na desnoj strani, autor je ostavio komentar na ruskom jeziku za svaki red programa. Ako otvorimo 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 vezu do vlastitog opisa i izvornog koda. Ovo je veoma korisno jer znajući šta funkcija radi, možemo pratiti kako to radi, koji bitovi perifernih registara i kako to utječe. Ovo je, prvo, još jedan izvor informacija za savladavanje kontrolera, zasnovan na praktičnoj upotrebi kontrolera. One. prvo rješavate tehnički problem, a zatim proučavate samo rješenje. Drugo, ovo je polje za optimizaciju programa za one koji nisu zadovoljni bibliotekom u smislu brzine i veličine koda.



Pa, za sada sve ide kako treba, ali spremne su samo sijalice i dugmad. Sada je vrijeme da preuzmete teže periferne uređaje - USB, UART, I2C i SPI. Odlučio sam da počnem sa USB-om - ST-Link debugger (čak i onaj pravi iz Discoveryja) je tvrdoglavo odbijao da otklanja greške na mojoj ploči, tako da je otklanjanje grešaka na otiscima preko USB-a jedina dostupna metoda za otklanjanje grešaka. Možete, naravno, putem UART-a, ali ovo je gomila dodatnih žica.

Opet sam otišao dug put - generisao sam odgovarajuće praznine u STM32CubeMX, dodao USB Middleware iz STM32F1Cube paketa u svoj projekat. Potrebno je samo da omogućite USB taktiranje, definišete odgovarajuće rukovaoce USB prekidima i dotjerate male stvari. Uglavnom sam sve bitne postavke USB modula kopirao sa STM32GENERIC, osim što sam malo uneo alokaciju memorije (oni su koristili malloc, a ja sam koristio statičku alokaciju).

Evo par zanimljivih komada koje sam dovukao sebi. Na primjer, da bi domaćin (računar) shvatio da je nešto na njega povezano, uređaj “žonglira” USB D+ linijom (koja je povezana na pin A12). Vidjevši to, domaćin počinje ispitivati ​​uređaj ko je, koja sučelja može, kojom brzinom želi komunicirati itd. Ne razumijem zašto ovo treba učiniti prije inicijalizacije USB-a, ali u stm32duino to se radi na isti način.

USB podrhtavanje

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() ( // Inicijaliziraj PA12 pin GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed ​​= GPIO_InitTypeDef na USB uređajima; sabirnica HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); for(unsigned int i=0; i<512; i++) {}; // Restore pin mode pinInit.Mode = GPIO_MODE_INPUT; pinInit.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pinInit); for(unsigned int i=0; i<512; i++) {}; } void initUSB() { Reenumerate(); USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); }


Još jedna zanimljiva stvar je podrška za stm32duino bootloader. Da biste učitali firmver, prvo morate ponovo pokrenuti kontroler u bootloader. Najlakši način je da pritisnete dugme za resetovanje. Ali da biste ga učinili praktičnijim, možete učiti iz Arduino iskustva. Kada su stabla bila mlada, AVR kontroleri još nisu imali USB podršku na ploči, postojao je USB-UART adapter na ploči. DTR signal UART-a je povezan sa resetovanjem mikrokontrolera. Kada host pošalje DTR signal, mikrokontroler se ponovo učitava u bootloader. Radi sa betonom!

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

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t dužina) ( ... case CDC_SET_CONTROL_LINE_STATE: dtr_pin++; //DTR pin je omogućen prekid; ... static int8_t CDC_Receive_FS (uint8_int* F) (uint8_int*) bajt je magični paket "1EAF" koji stavlja MCU u bootloader.*/ if(*Len >= 4) ( /** * Provjerite da li dolazni sadrži niz "1EAF".* Ako jeste, provjerite da li DTR ima */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&& (Buf == "F")) ( HAL_NVIC_SystemReset( ); ) dtr_pin = 0; ) ) ... )

Pozadi: MiniArduino

Općenito, USB je zaradio. Ali ovaj sloj radi samo sa bajtovima, ne i nizovima. Zato ispisi za otklanjanje grešaka izgledaju tako ružno.

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

  • Zavrnite klasični printf. Opcija izgleda nije loša, ali vuče na +12kb firmvera (nekako sam slučajno u sebi nazvao sprintf)
  • Iskopajte sopstvenu implementaciju printf-a u svom skrovištu. Jednom sam pisao pod AVR, kao da je ova implementacija manja.
  • Uvrnite klasu Print iz arduina u implementaciju STM32GENERIC
Odabrao sam potonju opciju jer se kod biblioteke Adafruit GFX također oslanja na Print, tako da još uvijek moram da ga uvrnem. Osim toga, već sam imao STM32GENERIC kod pri ruci.

Napravio sam MiniArduino direktorij u svom projektu kako bih tamo stavio minimalnu potrebnu količinu koda kako bih implementirao dijelove arduino interfejsa koji su mi potrebni. Počeo sam da kopiram jednu po jednu datoteku i vidim koje su druge zavisnosti potrebne. Ovo mi je dalo kopiju klase Print i neke datoteke za omotavanje.

Ali ovo nije dovoljno. Kao i prije, bilo je potrebno nekako povezati klasu Print sa USB funkcijama (na primjer, CDC_Transmit_FS()). Da bih to uradio, morao sam da uvučem klasu SerialUSB. Povukao je klasu Stream i dio GPIO inicijalizacije sa sobom. Sljedeći korak je bio povezivanje UART-a (imam GPS povezan na njega). Tako sam također uvukao klasu SerialUART, koja je povukla još jedan sloj periferne inicijalizacije iz STM32GENERIC.

Generalno, našao sam se u sledećoj situaciji. Kopirao sam skoro sve fajlove sa STM32GENERIC na moj MiniArduino. Imao sam i svoju kopiju USB i FreeRTOS biblioteka (trebao sam imati kopije HAL-a i CMSIS-a, ali sam bio previše lijen). U isto vrijeme mjesec i po obilježavam vrijeme - spajam i rastavljam različite komade, ali pritom nisam napisao niti jedan red novog koda.

Postalo je jasno da moja prvobitna ideja da preuzmem kontrolu nad cijelim dijelom sistema nije bila uspješna. U svakom slučaju, dio inicijalizacionog koda živi u STM32GENERIC i čini se da mu je tamo ugodnije. Naravno, bilo je moguće izrezati sve zavisnosti i napisati vlastite klase omotača za svoje zadatke, ali bi me to usporilo na još mjesec dana - ovaj kod još uvijek treba otkloniti greške. Naravno, za vaš CSV to bi bilo super, ali morate ići naprijed!

Generalno, izbacio sam sve duplikate biblioteke i skoro ceo sistemski sloj i vratio se na STM32GENERIC. Ovaj projekat se razvija prilično dinamično - nekoliko urezivanja dnevno je stabilno. Osim toga, u proteklih mjesec i po dana, dosta sam proučavao, pročitao većinu STM32 Referentnog priručnika, pogledao kako se prave HAL biblioteke i omoti STM32GENERIC, napredovao u razumijevanju USB deskriptora i perifernih uređaja mikrokontrolera. Generalno, sada sam bio mnogo sigurniji u STM32GENERIC nego ranije.

Nazad: I2C

Međutim, mojim avanturama tu nije bio kraj. Još su postojali UART i I2C (imam ekran tamo). S UART-om je sve bilo prilično jednostavno. Upravo sam uklonio dinamičku dodjelu memorije, i da neiskorišteni UART-ovi ne pojedu baš ovu memoriju, jednostavno sam ih komentirao.

Ali implementacija I2C u STM32GENERIC zasadila je kaku. Ono što je jako interesantno, ali za koje su mi trebale najmanje 2 večeri. Pa, ili dali 2 večeri teškog otklanjanja grešaka na otiscima - ovo je s koje strane gledati.

Općenito, implementacija prikaza nije počela. U već tradicionalnom stilu - to jednostavno ne ide i to je to. Šta ne radi nije jasno. Čini se da je biblioteka samog ekrana (Adafruit SSD1306) testirana na prethodnoj implementaciji, ali ne treba isključiti smetnje grešaka. Sumnja pada na HAL i implementaciju I2C iz STM32GENERIC.

Za početak, prokomentirao sam sav ekran i I2C kod i napisao I2C inicijalizaciju bez ikakvih biblioteka, 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.Instance = I2C1; hi2c1.Init.ClockSpeed ​​= 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c1);


Izbacio sam stanje registara odmah nakon inicijalizacije. Napravio sam isti dump u radnoj verziji na stm32duino. Evo šta sam dobio (sa komentarima za sebe)

Dobro (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Omogućen prekid greške, 36Mhz
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 adresa
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: 400kHz način rada
40005420: 0 0 0 B - I2C_TRISE

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

Druga razlika je skup bitova zauzetosti. U početku mu nisam pridavao nikakvu važnost, ali gledajući unaprijed, reći ću da je upravo on signalizirao problem!.. Ali prvo o svemu.

Zabrljao sam inicijalizacijski kod na svom kolenu bez ikakvih biblioteka.

Inicijalizacija prikaza

void sendCommand(I2C_HandleTypeDef * handle, uint8_t cmd) ( SerialUSB.print("Naredba za slanje "); SerialUSB.println(cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Master_Trans_AD, ICE<<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 malo truda, ovaj kod mi je uspio (u ovom slučaju sam nacrtao pruge). Dakle, problem je u I2C sloju STM32GENERIC. Počeo sam postepeno uklanjati svoj kod, zamjenjujući ga odgovarajućim dijelovima iz biblioteke. Ali čim sam prebacio kod za inicijalizaciju pina sa svoje implementacije na bibliotečki, ceo I2C transfer je počeo da pada na vremensko ograničenje.

Onda sam se sjetio zauzetosti i pokušao shvatiti kada se to dogodi. Ispostavilo se da se zastavica zauzetosti javlja čim inicijalizacijski kod uključi I2c sat. One. Modul se uključuje i odmah ne radi. Zanimljivo.

Pada pri inicijalizaciji

uint8_t * pv = (uint8_t*)0x40005418; //I2C_SR2 registar. Traži se BUSY flag SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Ispisuje 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // ispisuje 2


Iznad ovog koda je samo inicijalizacija pinova. Pa, šta da radimo - pokrivamo debug sa otiscima kroz liniju i tamo

STM32GENERIC pin inicijalizacija

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instanca, GPIO_TypeDef *port, uint32_t pin, uint32_t mod, uint32_t pull) ( … GPIO_InitTypenitDef GPIO_InitTypenitDef GPIO_InitTypenitDef GPIO_InitTypenitDef; ; ;GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_VERY_HIGH;HAL_GPIO_Init(port, &GPIO_InitStruct); ... )


Ali ovdje je problem - GPIO_InitStruct je ispravno popunjen. Samo moj radi, ovaj ne. Zaista mistično!!! Sve je po udžbeniku, ali ništa ne radi. Proučavao sam šifru biblioteke red po red u potrazi za bilo č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 bubu u njemu? I ona je! Čak sam i uklonio nepotrebne parametre da bi problem bio vidljiviji. Generalno, razlika je u tome što moj kod inicijalizuje oba pina odjednom u jednoj strukturi, a STM32GENERIC kod zauzvrat. Očigledno kod inicijalizacije pina na neki način utiče na nivo na ovom pinu. Prije inicijalizacije, ništa se ne izlazi na ovaj pin i nivo se povlači na jedinicu pomoću otpornika. U trenutku inicijalizacije, iz nekog razloga, kontroler postavlja nulu na odgovarajuću nogu.

Ova činjenica sama po sebi je bezopasna. Ali problem je u tome što je snižavanje SDA linije dok je SCL linija podignuto početni uvjet za i2c sabirnicu. Zbog toga prijemnik kontrolera poludi, postavlja zastavicu ZAUZET i počinje čekati podatke. Odlučio sam da ne uništavam biblioteku kako bih dodao mogućnost inicijalizacije nekoliko pinova odjednom. Umjesto toga, samo sam zamijenio ova 2 reda - inicijalizacija ekrana je bila uspješna. Ispravka je usvojena u STM32GENERIC.

Inače, inicijalizacija magistrale u libmapleu je zanimljiva. Prije početka inicijalizacije i2c perifernih uređaja na magistrali, prvo se vrši resetiranje. Da bi to učinila, biblioteka stavlja igle u uobičajeni GPIO mod i trza ove noge nekoliko puta, simulirajući sekvence pokretanja i zaustavljanja. Ovo pomaže da se uređaji zaglavljeni u autobusu ožive. Nažalost, u HAL-u nema sličnog. Ponekad mi se ekran zaglavi i tada samo isključim uštedu energije.

i2c inicijalizacija iz stm32duino

/** * @brief Resetujte I2C sabirnicu. * * Resetovanje se postiže tako što se impulsi isključuju sve dok svi obješeni slave * ne oslobode SDA i SCL, a zatim generišu START stanje, zatim STOP * stanje. * * @param dev I2C uređaj */ void i2c_bus_reset(const i2c_dev *dev) ( /* Otpusti oba reda */ i2c_master_release_bus(dev); /* * Uvjerite se da je magistrala slobodna tako što ćete je taktirati sve dok bilo koji slave ne otpusti * sabirnicu. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* Sačekajte da se bilo koje 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); /* Ponovo povuci visoko */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* Generiraj uslov starta 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

Bio sam uzbuđen što sam se konačno vratio programiranju i nastavio pisati funkcije. Sljedeći veliki dio bilo je povezivanje SD kartice preko SPI. Ovo je samo po sebi uzbudljivo, zanimljivo i puno bola. Definitivno ću o tome posebno govoriti u sljedećem članku. Jedan od problema je bilo veliko opterećenje (> 50%) procesora. To je dovelo u pitanje energetsku efikasnost uređaja. Da, i korištenje uređaja je bilo neugodno, jer. UI užasno glup.

Razumijevajući problem, pronašao sam razlog za takvu potrošnju resursa. Sav rad sa SD karticom obavljen je bajt po bajt, uz pomoć 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! Tu je i DMA! Da, SD biblioteka (ona koja dolazi uz Arduino) je nespretna i treba je promijeniti, ali problem je globalniji. Ista slika je uočena u biblioteci ekrana, a čak je i slušanje UART-a obavljeno putem ankete. Generalno, počeo sam da mislim da prepisivanje svih komponenti u HAL-u nije tako glupa ideja.

Počeo sam, naravno, sa nečim jednostavnijim - UART drajverom, koji sluša tok podataka sa GPS-a. Arduino sučelje vam ne dozvoljava da se priključite na UART prekid i u hodu otimate dolazne znakove. Kao rezultat, jedini način da dođete do podataka je stalna anketa. Naravno, dodao sam vTaskDelay(10) u GPS rukovaoce da malo smanjim opterećenje, ali ovo je zapravo štaka.

Prva pomisao, naravno, bila je zeznuti DMA. To bi čak i funkcioniralo da nije bilo NMEA protokola. Problem je u tome što se u ovom protokolu informacije jednostavno strimuju, a pojedinačni paketi (linije) su odvojeni znakom za prelom reda. Osim toga, svaka linija može biti različite dužine. 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 nestaje, tražimo drugo rješenje.

Ako pažljivo pogledate dizajn NeoGPS biblioteke, možete vidjeti da biblioteka prihvata ulazne podatke bajt po bajt, ali se vrijednosti ažuriraju tek kada stigne cijela linija (tačnije, paket nekoliko redova). To. nije bitno da li hranite biblioteku bajtove jedan po jedan kako se primaju ili onda sve odjednom. Dakle, možete uštedjeti vrijeme procesora - sačuvati primljenu liniju u međuspremnik, i to možete učiniti odmah u prekidu. Kada se string primi u cijelosti, obrada može početi.

Pojavljuje se sljedeći dizajn

UART klasa drajvera

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


Iako je inicijalizacija preuzeta sa STM32GENERIC, ona je u potpunosti u skladu sa onom koju nudi CubeMX

UART inicijalizacija

void init() ( // Resetuj pokazivače (samo u slučaju da neko pozove init() više puta) lastReadIndex = 0; lastReceivedIndex = 0; // Inicijaliziraj GPS Thread handle xGPSThread = xTaskGetCurrentTaskHandle(); // Omogući taktiranje odgovarajuće periperhalne __GPIOBALE_C() ); __HAL_RCC_USART1_CLK_ENABLE (); // Init igle u alternativnu modu funkciji GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; // TX pin 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); // RX pin GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init (GPIOA, & GPIO_InitStruct); // // Init uart. WordLength = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.H; wFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // Koristit ćemo UART prekid da dobijemo podatke HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Čekaćemo da dobijemo pravo jednog znaka pravo na bafer HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


U stvari, TX pin nije mogao biti inicijaliziran, ali uartHandle.Init.Mode bi mogao biti postavljen na UART_MODE_RX - samo ćemo to prihvatiti. Međutim, neka bude - odjednom moram nekako konfigurirati GPS modul i napisati komande na njega.

Dizajn ove klase mogao bi izgledati bolje da nije bilo ograničenja HAL arhitekture. Dakle, ne možemo samo postaviti režim, kažu, prihvatiti sve, direktno priključiti na prekid i otimati primljene bajtove direktno iz prijemnog registra. Moramo unaprijed reći HAL-u koliko i gdje ćemo primiti bajtove - odgovarajući rukovatelji će sami zapisati primljene bajtove u predviđeni bafer. Za ovo, u posljednjem redu funkcije inicijalizacije nalazi se poziv HAL_UART_Receive_IT (). Pošto dužina niza nije poznata unapred, mora se primiti jedan bajt.

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

UART povratni pozivi

// Proslijedi obradu UART prekida na HAL ekstern "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 bi se mogao koristiti kao sredstvo za sinhronizaciju, ali za tako jednostavne svrhe preporučuje se korištenje direktnih obavijesti.

Obrada primljenih bajtova

// 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 koji nije primio GPS nit); je dostupan za čitanje if(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); )


Funkcija odgovora (čekanja) je waitForString(). Njegov zadatak je jednostavno da visi na objektu za sinhronizaciju i čeka (ili izađe s timeoutom)

Čeka se kraj reda

// Čekamo dok se ne primi cijeli red bool waitForString() (vrati ulTaskNotifyTake(pdTRUE, 10); )


Radi ovako. Nit koja je odgovorna za GPS normalno spava u funkciji waitForString(). Bajtovi koji dolaze iz GPS-a dodaju se u bafer pomoću rukovaoca prekida. Ako stigne znak \n (kraj reda), tada prekid budi glavnu nit, koja počinje sipati bajtove iz bafera u parser. Pa, kada parser završi obradu serije poruka, ažurirat će podatke u GPS modelu.

GPS stream

void vGPSTask(void *pvParameters) ( // GPS inicijalizacija mora biti obavljena unutar GPS niti jer je ručnik niti pohranjen // i kasnije se koristi u svrhu sinhronizacije gpsUart.init(); for (;;) ( // Sačekajte dok se cijeli niz ne prikaže primljeno if(!gpsUart.waitForString()) continue; // Čitanje primljenog niza i raščlanjivanje GPS toka char po char while(gpsUart.available()) ( int c = gpsUart.readChar(); //SerialUSB.write(c) ; gpsParser.handle(c); ) if(gpsParser.available()) ( GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.satellites, gpscountParser.s ); ) vTaskDelay(10); ) )


Naletio sam na jedan vrlo netrivijalan trenutak, koji je trajao nekoliko dana. Čini se da je kod za sinhronizaciju preuzet iz primjera, ali u početku nije radio - poklopio je cijeli sistem. Mislio sam da je problem u direktnim obavještenjima (xTaskNotifyXXX funkcije), promijenio sam to u obične semafore, ali je aplikacija i dalje prekinula vezu.

Ispostavilo se da morate biti veoma oprezni sa prioritetom prekida. Podrazumevano, sve prekide postavljam na nulu (najviši) prioritet. Ali FreeRTOS ima zahtjev da prioriteti budu unutar datog raspona. Prekidi sa previsokim prioritetom ne mogu pozvati FreeRTOS funkcije. Samo prekidi s prioritetom configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY i ispod mogu pozvati sistemske funkcije (dobro objašnjenje i ). Ova postavka je po defaultu postavljena na 5. Promijenio sam prioritet prekida UART-a na 6 i sve se pokrenulo.

Opet tu: I2C preko DMA

Sada možete da uradite nešto komplikovanije, kao što je drajver za ekran. Ali ovdje morate napraviti izlet u teoriju I2C sabirnice. Sama po sebi, ova sabirnica ne reguliše protokol za prenos podataka na magistralu - možete ili pisati bajtove ili čitati. Moguće je čak i prvo pisati, pa čitati u jednoj transakciji (na primjer, zapisati adresu, a zatim pročitati podatke na ovoj adresi).

Međutim, većina uređaja definira protokol višeg sloja na isti način. uređaj daje korisniku skup registara, svaki sa svojom adresom. Istovremeno, u komunikacijskom protokolu prvi bajt (ili nekoliko) u svakoj transakciji određuje adresu ćelije (registra) u koju ćemo dalje čitati ili pisati. U ovom slučaju je moguća i višebajtna razmjena u stilu „ajmo napisati/čitati puno bajtova počevši od ove adrese“. Poslednja opcija je veoma pogodna za DMA.

Nažalost, ekran baziran na SSD1306 kontroleru pruža potpuno drugačiji protokol – komandni. Prvi bajt svake transakcije je znak "komanda ili podaci". U slučaju komande, drugi bajt je kod komande. Ako su naredbi potrebni argumenti, oni se prosljeđuju kao zasebne naredbe nakon prve. Za inicijalizaciju prikaza potrebno je poslati oko 30 komandi, 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 usluge DMA. To je ono što ćemo pokušati.

Ali biblioteka Adafruit_SSD1306 je napisana vrlo nespretno i nemoguće je ugurati se u nju sa malo krvi. Očigledno, biblioteka je prvo napisana da komunicira sa ekranom preko SPI-a. Onda je neko dodao I2C podršku, a SPI podrška je ostala uključena. Onda je neko počeo da dodaje sve vrste optimizacija niskog nivoa i sakriva ih iza ifdef "s. Kao rezultat toga, dobili smo rezance od koda za podršku različitih interfejsa. Tako da pre nego što smo krenuli dalje, morali smo to da popravimo.

U početku sam pokušao da ga očistim tako što sam omotao kod za različita interfejsa sa ifdefs-om. Ali ako želim da napišem kod za komunikaciju sa ekranom, koristim DMA i sinhronizujem preko FreeRTOS-a, onda neću uspeti. Preciznije, ispostaviće se, ali ovaj kod će morati biti napisan direktno u kodu biblioteke. Stoga sam odlučio da još jednom razmislim o biblioteci, napravim interfejs i stavim svaki drajver u posebnu klasu. Kod je postao čišći i bilo bi moguće bezbolno dodati podršku za nove drajvere bez promjene same biblioteke.

Prikaz interfejsa drajvera

// Sučelje za hardverski drajver // Adafruit_SSD1306 ne radi direktno s hardverom // Svi zahtjevi za komunikaciju se prosljeđuju klasi drajvera ISSD1306Driver ( public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0 virtual void sendData(uint8_t * data, size_t size) = 0; );


Pa idemo. Već sam pokazao inicijalizaciju I2C. Tu se ništa nije promijenilo. A ovdje je sa slanjem naredbe bilo malo pojednostavljeno. Sjećate li se da sam govorio o razlici između protokola registra i komandnog protokola za I2C uređaje? I iako ekran implementira komandni protokol, može se dobro imitirati korištenjem protokola registra. Samo treba da zamislite da ekran ima samo 2 registra - 0x00 za komande i 0x40 za podatke. A HAL čak pruža funkciju za ovu vrstu prijenosa

Slanje komande na displej

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

za (uint16_t i=0; i


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

zgužvan displej



Ispostavilo se da je razlog trivijalan - prekoračenje bafera. Arduino Wire klasa (barem STM32GENERIC) pruža vlastiti bafer od samo 32 bajta. Ali zašto nam je uopće potreban dodatni bafer ako ga klasa Adafruit_SSD1306 već ima? Štaviše, sa HAL-om, slanje se dobija u jednom redu

Ispravan prijenos podataka

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


Dakle, pola posla je obavljeno - napisali smo drajver za displej na čistom HAL-u. Ali u ovoj verziji i dalje je zahtjevan za resurse - 12% za ekran od 128x32 i 23% za ekran od 128x64. Upotreba DMA ovdje već traži.

Prvo, inicijalizirajmo DMA. Želimo implementirati prijenos podataka u I2C #1, a ova funkcija živi na šestom DMA kanalu. Inicijaliziramo bajt po bajt kopiranje iz memorije na periferne uređaje

Postavljanje DMA 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 inicijalizirani DMA ručku sa 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 obavezni dio dizajna. U suprotnom, funkcija HAL_I2C_Mem_Write_DMA() će započeti I2C transakciju, ali je niko neće dovršiti. Opet, imamo posla sa glomaznim HAL dizajnom i potrebom za čak dva povratna poziva. Sve je potpuno isto kao i sa UART-om. Jedna funkcija je rukovalac prekida - samo preusmjerite poziv na HAL. Druga funkcija je signal da su podaci već poslani.

DMA rukovaoci prekida

ekstern "C" void DMA1_Channel6_IRQHandler(void) ( HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); ) ekstern "C" void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef display) (DriverTypeDef display)hi2c.);


Naravno, nećemo stalno ispitivati ​​I2C, ali je li transfer već završen? Umjesto toga, trebate spavati na objektu za sinhronizaciju i čekati dok se prijenos ne završi

Prijenos podataka preko DMA sa sinhronizacijom

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


Prijenos podataka i dalje traje 24 ms - to je praktično čisto vrijeme prijenosa od 1 kb (veličina bafera prikaza) na 400 kHz. Samo u ovom slučaju, većinu vremena, procesor samo spava (ili radi druge stvari). Ukupna potrošnja CPU-a pala je sa 23% na samo 1,5-2%. Mislim da se vrijedilo boriti za ovaj pokazatelj!

Opet tu: SPI preko DMA

Povezivanje SD kartice preko SPI-a bilo je u određenom smislu lakše - do tada sam počeo da šrafujem sdfat biblioteku, a tamo su ljubazni ljudi već odvojili komunikaciju sa karticom u poseban interfejs drajvera. 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 drajver interfejs za rad sa SD karticom

// Ovo je prilagođena implementacija klase SPI Driver. Biblioteka SdFat // koristi ovu klasu za pristup SD kartici preko SPI // // Glavna namjera ove implementacije je pokrenuti prijenos podataka // preko DMA-a i sinkronizirati se sa mogućnostima FreeRTOS-a. class SdFatSPIDriver: public SdSpiBaseDriver ( // SPI modul SPI_HandleTypeDef spiHandle; // GPS nit ručica TaskHandle_t xSDThread = NULL; public: SdFatSPIDriver(); virtual void activate(); virtual void begin(uint8_t virtual deactivate); vSelect uint8_t receive(); virtual uint8_t receive(uint8_t* buf, size_t n); virtual void send(uint8_t data); virtual void send(const uint8_t* buf, size_t n); virtual void select(); virtual void setSpiSettings(SPISettings spiSettings ); virtualna void unselect(); );


Kao i prije, počinjemo s jednostavnim - sa hrastovom implementacijom bez DMA. Inicijalizacija je dijelom generisana od strane CubeMX-a, a dijelom iz SPI implementacije STM32GENERIC

SPI inicijalizacija

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) ( // Ignoriraj proslijeđeni CS pin - Ovaj drajver radi sa unaprijed definiranim (void)chipSelectPin; // Inicijaliziraj GPS Thread handle xSDThread = xTaskGetCurrentTaskHandle(); // odgovarajući_HGPIOChandle(); // odgovarajući_HGPIOChandle(); ; __HAL_RCC_SPI1_CLK_ENABLE (); // Init igle 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 interfejsa je skrojen za arduino sa numerisanjem pinova sa jednim brojem. U mom slučaju, nije imalo smisla postavljati CS pin kroz parametre - ovaj signal imam čvrsto vezan za A4 pin, ali morao sam pratiti interfejs.

Dizajnom biblioteke SdFat, brzina SPI porta se konfiguriše prije svake transakcije. One. teoretski, možete započeti komunikaciju s karticom malom brzinom, a zatim je povećati. Ali odustao sam od toga i jednom sam podesio brzinu u metodi begin(). Tako su se metode aktiviranja/deaktiviranja pokazale praznima za mene. Sviđa mi se 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 ista podešavanja za sve transfere)


Metode kontrole CS signala su prilično 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_SET);


Dolazimo do najzanimljivijeg – čitanja i pisanja. Prva najbolja implementacija bez DMA

Prijenos podataka bez DMA

uint8_t SdFatSPIDriver::receive() ( uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive(&spiHandle, &dummy, &buf, 1, 10); return buf; ) uint8_t SdFatSPIDriver (DODO_T) : Prijem preko DMA ovdje memset(buf, 0xff, n); HAL_SPI_Receive(&spiHandle, buf, n, 10); return 0; ) void SdFatSPIDriver::send(uint8_t data) ( HAL_SPI_Transmit(&taspiHandle, &daspiHandle1, &daspiHandle1); ) void SdFatSPIDriver::send(const uint8_t* buf, size_t n) ( // TODO: Prijenos preko DMA ovdje HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); )


U SPI interfejsu, podaci se primaju i prenose istovremeno. Da biste nešto primili, morate nešto poslati. Obično HAL to radi umjesto nas - samo pozivamo funkciju HAL_SPI_Receive () i ona organizira i slanje i primanje. Ali u stvari, 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 vrlo kapriciozne. Ne vole da budu nasumično prevareni dok kartica šalje podatke. Stoga sam morao koristiti funkciju HAL_SPI_TransmitReceive () i nasilno slati 0xffs dok primam podatke.

Hajde da izmerimo. Neka jedna nit zapiše 1kb 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( "Sačuvano %d kb\n", i); i = 0; ) )


Ovakvim pristupom uspijeva napisati oko 15-16 kb u sekundi. Ne mnogo. Ali ispostavilo se da sam već postavio predskaler na 256. SPI takt je postavljen na mnogo manje od moguće propusnosti. Eksperimentalno sam otkrio da nema smisla postavljati frekvenciju veću od 9 MHz (preskaler je postavljen na 8) - nemoguće je postići brzinu snimanja veću od 100-110 kb/s (na drugom fleš disku, pomoću način na koji se iz nekog razloga moglo snimiti samo 50-60 kb/s, a na trećem uglavnom samo 40 kb/s). Očigledno sve zavisi od tajm-auta samog fleš diska.

U principu, ovo je već više nego dovoljno, ali mi ćemo pumpati podatke preko DMA. Poslujemo na uobičajen način. Prije svega, inicijalizacija. Imamo SPI prijem i prenos na drugom i trećem DMA kanalu, respektivno.

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. Za mene će ići sa prioritetom 8 - nešto nižim od 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 i sinhronizacije za kratke prijenose mogli premašiti dobit, pa sam za male pakete (do 16 bajtova) napustio staru opciju. Paketi duži od 16 bajtova se šalju preko DMA. Metoda sinhronizacije je potpuno ista kao u prethodnom odeljku.

Prijenos podataka preko DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( memset(buf, 0xff, n); // Ne koristi se DMA za kratke prijenose if(n<= DMA_TRESHOLD) { return HAL_SPI_TransmitReceive(&spiHandle, buf, buf, n, 10); } // Start data transfer HAL_SPI_TrsnsmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { HAL_SPI_Transmit(&spiHandle, buf, n, 10); return; } // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread vTaskNotifyGiveFromISR(xSDThread, NULL); }


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

DMA prekida

extern SdFatSPIDriver spiDriver; ekstern "C" void DMA1_Channel2_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); ) ekstern "C" void DMA1_Channel3_IRQHandler(void) ( HAL_DMA_IRQHandler(void) ( HAL_DMA_IRQHandler) explCtmtDriver. (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) extern "C" void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); )


Počinjemo, proveravamo. Da ne bih mučio fleš disk, odlučio sam da otklonim greške čitanjem velike datoteke, a ne pisanjem. Ovdje sam otkrio vrlo zanimljivu stvar: brzina čitanja u ne-DMA verziji bila je oko 250-260 kb / s, dok je sa DMA bila samo 5 !!! Štaviše, potrošnja CPU-a bez korišćenja DMA je 3%, a sa DMA - 75-80%!!! One. Rezultat je upravo suprotan od očekivanog.

Offtop pro 3%

Ovdje sam imao smiješnu grešku s mjerenjem opterećenja CPU-a - ponekad je funkcija rekla da je procesor opterećen samo 3%, iako je CPU trebao mlatiti bez zaustavljanja. U stvari, opterećenje je bilo 100% i moja funkcija mjerenja uopće nije pozvana - ona ima najniži prioritet i jednostavno nije bilo dovoljno vremena za to. Stoga sam primio posljednju memorisanu vrijednost prije početka izvršenja. U normalnim uslovima, funkcija radi ispravnije.


Nakon što sam preklopio kod drajvera sa evidentiranjem skoro svake linije, otkrio sam problem: koristio sam pogrešnu funkciju povratnog poziva. U početku sam koristio HAL_SPI_Receive_DMA () u svom kodu i, zajedno s njim, povratni poziv HAL_SPI_RxCpltCallback korišten je u parovima. Ovaj dizajn nije funkcionirao zbog nijanse sa slanjem 0xff u isto vrijeme. Kada sam promijenio HAL_SPI_Receive_DMA() u HAL_SPI_TransmitReceive_DMA(), morao sam promijeniti povratni poziv u HAL_SPI_TxRxCpltCallback() u isto vrijeme. One. u stvari, očitavanje se dogodilo, ali zbog nedostatka povratnih poziva, brzina je regulirana timeoutom od 100ms.

Nakon popravljanja povratnog poziva, sve je došlo na svoje mjesto. Opterećenje procesora je palo na 2,5% (sada iskreno), a brzina je čak skočila do 500kb/s. Istina, preskaler je morao biti postavljen na 4 - sa predskalerom na 2, tvrdnje su se slijevale u biblioteku SdFat. Izgleda da je ovo ograničenje brzine moje kartice.

Nažalost, ovo nema nikakve veze sa brzinom pisanja. Brzina pisanja je i dalje bila oko 50-60 kb/s, a korištenje CPU-a je variralo u rasponu od 60-70%. Ali nakon kopanja po cijeloj večeri i mjerenja na različitim mjestima, otkrio sam da stvarna funkcija slanja () mog drajvera (koji piše jedan sektor od 512 bajtova) radi za samo 1-2 ms, uključujući čekanje i sinhronizaciju . Ponekad, međutim, pali neka vrsta timeout-a i snimanje traje 5-7ms. Ali problem zapravo nije u drajveru, već u logici rada sa FAT sistemom datoteka.

Idući do nivoa datoteka, particija i klastera, zadatak upisivanja 512 u datoteku nije tako trivijalan. Morate pročitati FAT tabelu, pronaći mjesto u njoj za sektor za upisivanje, napisati sam sektor, ažurirati unose u FAT tablici, zapisati ove sektore na disk, ažurirati unose u tablici datoteka i direktorija i hrpu drugih stvari. Uopšteno govoreći, jedan poziv FatFile::write() može potrajati do 15-20 ms, a značajan dio tog vremena zauzima stvarni rad procesora za obradu zapisa u sistemu datoteka.

Kao što sam već napomenuo, opterećenje procesora tokom snimanja je 60-70%. Ali ovaj broj zavisi i od tipa sistema datoteka (Fat16 ili Fat32), veličine i, shodno tome, broja ovih klastera na particiji, brzine samog fleš diska, začepljenja i fragmentacije medija, upotrebe dugih naziva datoteka i još mnogo toga. Stoga vas molimo da ove mjere tretirate kao neke relativne brojke.

Tamo pozadi: USB sa dvostrukim baferom

Sa ovom komponentom je ispalo zanimljivo. Bilo je nekoliko nedostataka u originalnoj USB Serial implementaciji od STM32GENERIC, i ja sam se obavezao da je prepišem za sebe. Ali dok sam proučavao kako USB CDC radi, čitao izvorni kod i proučavao dokumentaciju, momci iz STM32GENERIC su značajno poboljšali njihovu implementaciju. Ali prvo stvari.

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

  • Poruke se šalju sinhrono. One. banalni bajt-po-bajt prijenos podataka sa GPS UART-a na USB čeka da se pošalje svaki pojedinačni bajt. Zbog toga opterećenje procesora može doseći i do 30-50%, što je naravno mnogo (UART brzina je samo 9600)
  • Nema sinhronizacije. Prilikom ispisa poruka iz nekoliko niti, izlaz je zbrka poruka koje djelomično prepisuju jedna drugu
  • Previše bafera za primanje i slanje. Nekoliko bafera je deklarirano u USB Middleware-u, ali se zapravo ne koriste. Još nekoliko bafera je deklarirano u klasi SerialUSB, ali pošto ja koristim samo izlaz, bafer za primanje samo troši memoriju.
  • Konačno, interfejs klase Print me jednostavno nervira. Ako, na primjer, želim da prikažem string „trenutna brzina XXX km/h“, onda moram obaviti čak 3 poziva - za prvi dio niza, za broj i za ostatak niza. Lično sam duhom bliži klasičnom printf. Plus streamovi su također u redu, ali morate pogledati kakav kod generira kompajler.
Za sada, krenimo s jednostavnim - sinhronim slanjem poruka, bez sinhronizacije i formatiranja. U stvari, iskreno sam zalupio kod sa STM32GENERIC.

Implementacija `na čelo`

eksterni 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 size uint8_t * ) ( // Zanemari slanje poruke ako USB nije povezan if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Prenesi poruku ali ne duže od timeouta uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


Formalno, ovo nije sinhroni kod, jer ne čeka slanje podataka. Ali ova funkcija čeka da se pošalju prethodni podaci. One. prvi poziv će poslati podatke na port i izaći, ali će drugi poziv čekati dok se podaci poslani u prvom pozivu stvarno ne pošalju. U slučaju isteka vremena, podaci se gube. Takođe se ništa ne dešava ako uopšte nema USB veze.

Naravno, ovo je samo praznina, jer. ova implementacija ne rješava identificirane probleme. Šta je potrebno da ovaj kod bude asinhroni i neblokirajući? Pa, barem tampon. Samo ovdje kada se ovaj bafer prenosi?

Mislim da je vrijedno napraviti malu digresiju u principe USB-a. Činjenica je da prijenos u USB protokolu može inicirati samo domaćin. Ako uređaj treba da prenese podatke prema hostu, podaci se pripremaju u posebnom baferu PMA (Packet Memory Area) i uređaj čeka da host preuzme te podatke. Funkcija CDC_Transmit_FS() je odgovorna za pripremu PMA bafera. Ovaj bafer živi unutar USB perifernih uređaja, a ne u korisničkom kodu.

Iskreno, htio sam ovdje nacrtati lijepu sliku, ali nisam smislio kako da je bolje prikažem

Ali bilo bi cool implementirati sljedeću shemu. Klijentski kod, po potrebi, upisuje podatke u neki memorijski (korisnički) bafer. S vremena na vrijeme dolazi domaćin i uzima sve što se nakupilo u baferu do ove tačke. Ovo je vrlo slično onome što sam opisao u prethodnom paragrafu, ali postoji jedno ključno upozorenje: podaci su u korisničkom baferu, a ne u PMA. One. Želio bih uopće ne pozivati ​​CDC_Transmit_FS (), koji prebacuje podatke iz korisničkog bafera u PMA, a umjesto toga uhvati povratni poziv „ovdje je došao host, traže se podaci“.

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

Srećom, u tom trenutku sam primijetio da je STM32GENERIC već putovao oko takve stvari. Evo koda koji sam kreativno redizajnirao od njih.

USB serijski dvostruki bafer

#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; // Prenesite susjedne podatke do kraja međuspremnika if (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = sizeof(usbTxTxTail) (&usbTxBuffer, count); return count; ) void usbDebugWriteInternal(const char *buffer, size_t veličina, bool reverse = false) ( // Zanemari slanje poruke ako USB nije povezan if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; / Prenesite poruku, ali ne duže od timeouta uint32_t timeout = HAL_GetTick() + 5; // Zaštitite ovu funkciju od više ulaza MutexLocker locker(usbMutex); // Kopiraj 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 stigao, želi podatke“, ispostavilo se da je moguće organizirati povratni poziv „Poslao sam podatke hostu, možete uliti sljedeći“. Ispada takav dvostruki bafer - dok uređaj čeka da se podaci pošalju iz internog PMA bafera, korisnički kod može dodati bajtove u akumulacijski bafer. Kada je slanje podataka završeno, akumulacijski bafer se sipa u PMA. Ostaje samo organizirati ovaj povratni poziv. Da biste to učinili, morate malo upisati funkciju USBD_CDC_DataIn ().

Uneseni USB međuvera

statički uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(lasscNUCB;tcdxt;fcdxt;dctxt; ) else (vrati USBD_FAIL; ) )


Usput, funkcija usbDebugWrite je zaštićena mutexom i trebala bi ispravno raditi iz više niti. Nisam zaštitio funkciju USBSerialTransferCompletedCB() - ona se poziva iz prekida i radi na promjenjivim varijablama. Iskreno govoreći, negdje buba i dalje hoda ovdje, simboli se vrlo povremeno gutaju. Ali za mene to nije kritično za otklanjanje grešaka. Ovo se neće pozivati ​​u proizvodnom kodu.

Opet tu: printf

Dok ovaj komad može raditi samo sa stalnim linijama. Vrijeme je da zeznete analog printf (). Ne želim da koristim 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 ispisati nizove kao i brojeve u decimalnom i heksadecimalnom formatu. Nakon malo dorade i testiranja, ispalo je otprilike ovako:

Pojednostavljena implementacija printf

// implementacija sprintf-a 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 cifre // - %x za brojeve kao HEX // - %s za nizove // ​​- %% za simbol procenta // // Implementacija podržava i širinu vrijednosti kao i nulti padding // Ispis broja u bafer (obrnutim redoslijedom) // Vraća broj ispisanih simbola size_t PrintNum(unsigned int vrijednost , uint8_t radix, char * buf, uint8_t width, char padSymbol) ( //TODO provjeri negativan ovdje size_t len ​​= 0; // Ispiši broj do ( char digit = vrijednost % radix; *(buf++) = cifra< 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" && gl<= "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 mnogo jednostavnija od one u biblioteci, ali može da uradi sve što mi treba - štampa nizove, decimalne i heksadecimalne brojeve sa formatiranjem (širina polja, završavanje broja nulama na levoj strani). Još ne ispisuje negativne brojeve i brojeve s pokretnim zarezom, ali to je lako dodati. Kasnije ću možda omogućiti zapisivanje rezultata u string buffer (kao sprintf), a ne samo na USB.

Performanse ovog koda su oko 150-200 kb/s sa prijenosom preko USB-a i zavise od broja (dužine) poruka, složenosti niza formata, kao i od veličine bafera. Ova brzina je dovoljna za slanje nekoliko hiljada malih poruka u sekundi. Ono što je najvažnije, pozivi nisu blokirani.

Još čvršće: HAL niskog nivoa

U principu je ovo moglo da se završi, ali primetio sam da su momci iz STM32GENERIC bukvalno pre neki dan sipali novi HAL. Zanimljivo je da su se mnogi fajlovi pojavili pod imenom stm32f1xx_ll_XXXX.h. Pronašli su alternativu i implementaciju HAL-a na nižem nivou. One. uobičajeni HAL pruža interfejs prilično visokog nivoa u stilu „uzmi ovaj niz i prosledi mi ga kroz ovaj interfejs. Prijavite završetak s prekidom.” Nasuprot tome, fajlovi sa slovima LL u imenu pružaju interfejs nižeg nivoa kao što je „postavi ove zastavice za taj i takav registar“.

Misterija našeg grada

Videvši nove datoteke u STM32GENERIC repozitorijumu, želeo sam da preuzmem kompletan komplet sa ST veb stranice. Ali guglanje me je dovelo samo do HAL-a (STM32 Cube F1) verzije 1.4 koji ne sadrži ove nove datoteke. STM32CubeMX grafički konfigurator je također ponudio ovu verziju. Pitao sam programere STM32GENERIC gdje su dobili novu verziju. Na moje iznenađenje, dobio sam link na istu stranicu, samo što je sada ponudio preuzimanje verzije 1.6. Google je takođe iznenada počeo da "pronalazi" novu verziju, kao i ažurirani CubeMX. Mystic i još mnogo toga!


Zašto je to potrebno? U većini slučajeva, interfejs visokog nivoa radi zaista dobar posao. HAL (Hardware Abstraction Layer) u potpunosti opravdava svoj naziv - apstrahuje kod iz procesorskih i hardverskih registara. Ali u nekim slučajevima, HAL ograničava maštu programera, dok se korišćenjem apstrakcija nižeg nivoa zadatak može efikasnije implementirati. U mom slučaju, to su GPIO i UART.

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

Očigledno, te stvari niskog nivoa također se mogu podijeliti na 2 dijela:

  • malo više funkcija visokog nivoa u stilu običnog HAL-a - evo inicijalizacijske strukture za vas, molim vas inicijalizirajte periferiju za mene.
  • Nešto niži nivoi postavljači i getteri za pojedinačne zastavice ili registre. Uglavnom, funkcije ove grupe su inline i samo zaglavlje
Prema zadanim postavkama, prvi su onemogućeni USE_FULL_LL_DRIVER definiranjem. Pa, dođavola s njima. Koristićemo drugi. Nakon malo šamanizma, dobio sam ovaj LED drajver

Morgulka na LL HAL

// Klasa za enkapsulaciju rada sa ugrađenim LED(ovima) // // Napomena: ova klasa inicijalizira odgovarajuće pinove u konstruktoru. // Možda neće raditi ispravno ako se objekti ove klase kreiraju 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(); // Pokreni PC 13 kao izlaz LL_GPIO_SetPinMode (GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType (GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinSpeed ​​(GPIOC, pin, LL_GPIO_SPEED_FREQ_LOW);) void Turnon () (LL_GPIO_ResetOutputPin (GPIOC, pin);) void skretanja () (LL_GPIO_SetOutputPin (GPIOC, pin); ) void toggle() ( 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! Lijepa stvar je što zaista postoji rad sa registrima i zastavicama direktno. Nema dodatnih troškova za HAL GPIO modul, koji sam kompajlira čak 450 bajtova, i kontrolu pinova sa STM32GENERIC, koji povlači još 670 bajtova. Ovdje je, općenito, cijela klasa sa svim pozivima umetnuta u funkciju vLEDThread, veličine samo 48 bajtova!

Nisam upravljao taktom preko LL HAL-a. Ali to nije kritično, jer. pozivanje __HAL_RCC_GPIOC_IS_CLK_ENABLED() iz normalnog HAL-a je zapravo makro koji samo postavlja nekoliko zastavica u određenim registrima.

Dugmad je jednako laka.

Dugmad 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; // Inicijalizacija tipke stvari koje se tiču void initButtons () (// omogućiti sat na GPIOC periferne __HAL_RCC_GPIOC_IS_CLK_ENABLED (); // SET UP igle LL_GPIO_SetPinMode (GPIOC, SEL_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull (GPIOC, SEL_BUTTONGP_PIN, LL_GPIO_GPIOW_PULLIOnMode);, OK_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); ) // Čitanje stanja dugmeta (prvo izvrši debounce) inline bool getButtonState(uint32_t pin) ( if(LL_putbouPIONC) LL_GPIO_IsInputPinSet(GPIOC, pin)) vrati true; ) vrati false; )


Sa UART-om će sve biti zanimljivije. Dozvolite mi da vas podsjetim na problem. Kada se koristi HAL, prijem je morao biti "ponovno učitan" nakon svakog primljenog bajta. Način "prihvati sve" nije predviđen u HAL-u. A sa LL HAL-om, trebali bismo uspjeti.

Postavljanje pinova natjeralo me ne samo da razmišljam o tome, već i da pogledam Referentni priručnik

Postavljanje UART pinova

// Pokretanje pinova u alternativnom funkcijskom modu 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


Prerada UART inicijalizacije na 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 da dobijemo podatke HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Omogući UART prekid na prijemu bajtova LL_USART_EnableIT_RXNE(USART1); // Konačno omogućite periferiju 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, konfigurisali smo prekid da prima samo bajt, tako da ćemo primljeni bajt primiti odmah.

UART prekid

// 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 if(c == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); ) ekstern "C" void USART1_IRQHandler(void) ( uint8_t bajt = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); )


Veličina koda drajvera je smanjena sa 1242 na 436 bajtova, a potrošnja RAM-a je smanjena sa 200 na 136 (od čega je 128 bafer). 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 interfejs visokog nivoa radi prilično dobro u slučaju ostalih perifernih uređaja.

Gledajući unazad

Iako sam na početku ove faze projekta bio skeptičan prema HAL-u, ipak sam uspio da prepišem sav rad sa periferijama: GPIO, UART, I2C, SPI i USB. Napravio sam dubok napredak u razumijevanju načina na koji ovi 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.

Ispostavilo se da je članak manje-više linearna priča. Ali u stvari, stvorio sam brojne marende u kojima sam istovremeno pilao u potpuno suprotnim smjerovima. Ujutro bih mogao da naletim na probleme sa performansama neke vrste arduino biblioteke i čvrsto odlučim da sve prepišem u HAL, a uveče bih otkrio da je neko već podneo DMA podršku u STM32GENERIC i imao sam želju da se vratim . Ili, na primjer, da se zaglavite sa arduino interfejsima nekoliko dana pokušavajući da shvatite kako je zgodnije prenositi podatke preko I2C, dok se na HAL-u to radi u 2 reda.

Generalno, postigao sam ono što sam želeo. Većina perifernih poslova je pod mojom kontrolom i napisana je u HAL-u. Arduino, s druge strane, djeluje samo kao adapter za neke biblioteke. Istina, još je bilo repova. I dalje morate skupiti hrabrost i ukloniti STM32GENERIC iz svog spremišta, ostavljajući samo nekoliko zaista potrebnih klasa. Ali takvo čišćenje se više neće odnositi na ovaj članak.

Što se tiče Arudina i njegovih klonova. I dalje mi se sviđa ovaj okvir. Pomoću njega možete brzo napraviti prototip nečega bez da se stvarno mučite s čitanjem priručnika i tablica podataka. Sa arduinom, u principu, možete napraviti i krajnje uređaje ako nema posebnih zahtjeva za brzinu, potrošnju ili memoriju. U mom slučaju, ovi parametri su veoma važni, pa sam morao da pređem na HAL.

Počeo sam raditi na stm32duino. Ovaj klon zaista zaslužuje pažnju ako želite da imate "arduino" na STM32 i da sve radi iz kutije. Osim toga, pažljivo prate potrošnju RAM-a i flash memorije. Nasuprot tome, sam STM32GENERIC je deblji i baziran na monstruoznom HAL-u. Ali ovaj okvir se aktivno razvija i izgleda dovršenije. Generalno, mogu preporučiti oba okvira sa malom preferencijom za STM32GENERIC za HAL i dinamičnijim razvojem u ovom trenutku. Osim toga, internet je pun primjera za HAL i uvijek možete nešto podesiti za sebe.

I dalje se prema HAL-u odnosim sa određenim stepenom gađenja. Biblioteka je preglomazna i ružna. Uzimam u obzir činjenicu da je biblioteka sish, što dovodi do upotrebe dugih imena funkcija i konstanti. Ali ipak, ovo nije biblioteka s kojom je zabavno raditi. To je prije iznuđena mjera.

U redu, interfejs - unutrašnjost vas takođe navodi na razmišljanje. Ogromne funkcije s funkcionalnošću za sve prilike podrazumijevaju gubitak resursa. Štaviše, ako se možete boriti sa dodatnim kodom u flash-u koristeći optimizaciju vremena veze, onda se ogromna potrošnja RAM-a tretira samo prepisivanjem u LL HAL.

Ali nije čak ni to da je to frustrirajuće, već je na nekim mjestima samo zanemarivanje resursa. Tako sam primijetio ogromnu preopterećenost memorije u USB Middleware kodu (formalno ovo nije HAL, ali dolazi sa STM32Cube). Usb strukture zauzimaju 2,5 kb memorije. Štaviše, struktura USBD_HandleTypeDef (544 bajta) u velikoj meri ponavlja PCD_HandleTypeDef iz nižeg sloja (1056 bajtova) - krajnje tačke su takođe definisane u njoj. Baferi primopredajnika su takođe deklarisani na najmanje dva mesta - USBD_CDC_HandleTypeDef i UserRxBufferFS/UserTxBufferFS.

Deskriptori su generalno deklarisani u RAM-u. Zašto? Oni su konstantni! Skoro 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, sa konstantom. Da, i onaj koji je tu već upisan. Funkcije kao što je SetBuffer iz nekog razloga prihvataju nekonstantni bafer, koji takođe sprečava da se deskriptori i neke druge stvari stave u flash. Šta je razlog? Popravlja se za 10 minuta!

Ili, struktura inicijalizacije je dio ručke objekta (na primjer, i2c). Zašto ga pohraniti nakon što se periferija inicijalizira? Zašto su mi potrebni pokazivači na nekorištene strukture - na primjer, zašto su mi potrebni podaci povezani sa DMA ako ih ne koristim?

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_TYPE_CONFIGURATION; ) else (*plassp pbuf->) else (*plass pbuf) GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; ) break;


Posebna konverzija u "unicode tip", koja se može obaviti u vremenu kompajliranja. Štaviše, za to je dodijeljen poseban bafer.

Izvrgavanje const podataka

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


Nije fatalno, ali vas tjera da se zapitate da li je HAL toliko dobar koliko apologeta pišu o tome? Pa, ovo nije ono što očekujete od biblioteke proizvođača i dizajnirane za profesionalce. Ovo su mikrokontroleri! Ovdje ljudi štede svaki bajt i svaka mikrosekunda je dragocjena. A ovdje, razumiješ, bafer za pola kilograma i konverzija konstantnih nizova u hodu. Treba napomenuti da se većina primjedbi odnosi na USB Middleware.

UPD: u HAL-u 1.6, povratni poziv I2C DMA Dovršen je također prekinut. One. tu je kod potpuno nestao, koji u slučaju slanja podataka preko DMA generiše potvrdu, iako je to opisano u dokumentaciji. Prijem postoji, ali nema prijenosa. Morao sam da se vratim na HAL 1.4 za I2C modul, pošto postoji jedan modul - jedna datoteka.

Na kraju ću dati potrošnju flash i RAM-a raznih komponenti. U odeljku Drivers, naveo sam vrednosti i za drajvere zasnovane na HAL-u i za LL HAL-bazirane drajvere. U drugom slučaju, odgovarajuće sekcije iz HAL sekcije se ne koriste.

Potrošnja memorije

Kategorija potkategorija .text .rodata .data .bss
Sistem vektor prekida 272
lažni ISR ​​rukovaoci 178
libc 760
float math 4872
sin/cos 6672 536
glavni&itd 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 Middleware Core 1740 333 2179
CDC 772
Vozači UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Dugmad LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
misc 28 24
print 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šteno) 1068
FreeRTOS hrpa 10240

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

Tagovi:

  • HAL
  • STM32
  • STM32cube
  • arduino
Dodaj oznake

Lista članaka koji će čak i početniku pomoći da prouče STM32 mikrokontroler. Detalji svega s primjerima od treperenja LED-a do vožnje motora bez četkica. Primjeri koriste Standardnu ​​perifernu biblioteku (SPL).

STM32F103 testna ploča, ST-Link programator i firmver 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č). Shema napajanja i primjeri korištenja ADC-a u različitim načinima rada. Redovni i ubrizgani kanali. Korištenje ADC-a sa DMA. Unutrašnji termometar. Analogni čuvar.

Tajmeri opšte namene. Generisanje prekida u redovnim intervalima. Mjerenje vremena između dva događaja.

Hvatanje signala sa tajmerom na primjeru rada sa ultrazvučnim senzorom HC-SR04

Korišćenje tajmera za rad sa koderom.

PWM generacija. LED kontrola svjetline. Servo upravljanje (servos). Generacija zvuka.

Naglasio sam da je standardna biblioteka povezana sa sistemom. U stvari, CMSIS je povezan - generalizovani strukturalni sistem predstavljanja za MK, kao i SPL - standardna periferna biblioteka. Razmotrimo svaki od njih:

CMSIS
To je skup datoteka zaglavlja i mali set koda za objedinjavanje i strukturiranje rada sa jezgrom i periferijama MK-a. U stvari, bez ovih datoteka nemoguće je normalno raditi sa MK. Biblioteku možete nabaviti na stranici za MK.
Ova biblioteka, prema opisu, kreirana je da objedini interfejse pri radu sa bilo kojim MK-om iz porodice Cortex. Međutim, u stvarnosti se ispostavilo da to vrijedi samo za jednog proizvođača, tj. prelaskom na mikrokontroler druge kompanije, primorani ste da proučavate njegovu periferiju gotovo od nule.
Iako su oni fajlovi koji se odnose na jezgru MK procesora identični za sve proizvođače (makar samo zato što imaju jedan model jezgre procesora - obezbeđen u obliku ip-blokova od strane ARM-a).
Stoga je rad s takvim dijelovima kernela kao što su registri, instrukcije, prekidi i koprocesorski blokovi standardan za sve.
Što se tiče periferije, STM32 i STM8 (odjednom) su skoro slični, a to je djelimično tačno i za ostale MK-ove koje je objavio ST. U praktičnom dijelu pokazaću koliko je jednostavno koristiti CMSIS. Međutim, poteškoće u korištenju povezane su s nevoljnošću ljudi da pročitaju dokumentaciju i razumiju MK uređaj.

SPL
Standardna periferna biblioteka - standardna periferna biblioteka. Kao što naziv implicira, svrha ove biblioteke je stvaranje apstrakcije za MK periferiju. Biblioteka se sastoji od datoteka zaglavlja u kojima su deklarisane konstante razumljive ljudima za konfigurisanje i rad sa MK periferijama, kao i fajlovi izvornog koda sastavljeni u samu biblioteku za operacije sa periferijama.
SPL je apstrakcija preko CMSIS-a, predstavljajući korisniku zajednički interfejs za sve MK-ove, ne samo jednog proizvođača, već uopšteno za sve MK-ove sa Cortex-Mxx procesorskom jezgrom.
Vjeruje se da je pogodnije za početnike, jer. omogućava vam da ne razmišljate o tome kako rade periferne jedinice, međutim, kvalitet koda, univerzalnost pristupa i krutost sučelja nameću određena ograničenja programeru.
Takođe, funkcionalnost biblioteke ne dozvoljava vam uvek da precizno implementirate konfiguraciju nekih komponenti kao što je USART (Univerzalni sinhroni-asinhroni serijski port) pod određenim uslovima. U praktičnom dijelu opisaću i način rada sa ovim dijelom biblioteke.

Top Related Articles