STM32F407(STM32F4-DISCOVERY) - Abordare non-standard - Bibliotecă standard partea 1. Clasa de șofer UART

Până în acest moment, am folosit biblioteca standard de kernel - CMSIS. Pentru a configura orice port pentru modul de operare dorit, a trebuit să ne contactăm pentru a găsi persoana responsabilă functie specificaînregistrați, precum și căutați în documentul mare alte informații legate de acest proces. Lucrurile vor deveni și mai dureroase și de rutină atunci când începem să lucrăm cu un cronometru sau ADC. Numărul de registre de acolo este mult mai mare decât cel al porturilor I/O. Configurarea manuală necesită mult timp și crește șansa de a face greșeli. Prin urmare, mulți oameni preferă să lucreze cu biblioteca periferică standard - StdPeriph. Ce dă? Este simplu - nivelul de abstractizare crește, nu trebuie să intri în documentație și să te gândești la registre în cea mai mare parte. În această bibliotecă, toate modurile și parametrii de funcționare ai periferiei MK sunt descriși sub formă de structuri. Acum, pentru a configura un dispozitiv periferic, trebuie doar să apelați funcția de inițializare a dispozitivului cu o structură umplută.

Mai jos este o imagine cu o reprezentare schematică a nivelurilor de abstractizare.

Am lucrat cu CMSIS (care este „cel mai aproape” de nucleu) pentru a arăta cum funcționează microcontrolerul. Următorul pas este biblioteca standard, pe care vom învăța să o folosim acum. Urmează driverele de dispozitiv. Ele sunt înțelese ca fișiere *.c \ *.h care oferă o interfață software convenabilă pentru controlul oricărui dispozitiv. De exemplu, în acest curs vă vom oferi drivere pentru cipul max7219 și modulul WiFi esp8266.

Un proiect standard va include următoarele fișiere:


În primul rând, desigur, acestea sunt fișierele CMSIS care permit bibliotecii standard să lucreze cu nucleul, am vorbit deja despre ele. În al doilea rând, fișierele bibliotecii standard. Și în al treilea rând, fișierele utilizator.

Fișierele bibliotecii se găsesc pe pagina dedicată MK-ului țintă (pentru noi este stm32f10x4), în secțiunea Resurse de proiectare(în IDE-ul CooCox, aceste fișiere sunt descărcate din depozitul mediului de dezvoltare). Fiecare periferic îi corespunde două fișiere - antet (*.h) și cod sursa(*.c). O descriere detaliată poate fi găsită în fișierul de suport, care se află în arhiva cu biblioteca de pe site.

  • stm32f10x_conf.h - fișier de configurare a bibliotecii. Utilizatorul poate conecta sau deconecta module.
  • stm32f10x_ppp.h - fișier antet periferie. În loc de ppp poate fi gpio sau adc.
  • stm32f10x_ppp.c - driver de dispozitiv periferic scris în limbaj C.
  • stm32f10x_it.h - fișier antet care include toți manipulatorii de întreruperi posibili (prototipurile acestora).
  • stm32f10x_it.c este un fișier cod sursă șablon care conține rutina de întrerupere a serviciului (ISR) pentru situații de excepție în Cortex M3. Utilizatorul își poate adăuga propriile ISR-uri pentru perifericele utilizate.

Biblioteca standard și perifericele au o convenție în denumirea funcțiilor și a notației.

  • PPP este un acronim pentru periferice, cum ar fi ADC.
  • Fișiere de sistem, antet și cod sursă - începeți cu stm32f10x_.
  • Constantele utilizate într-un fișier sunt definite în acel fișier. Constantele utilizate în mai multe fișiere sunt definite în fișierele antet. Toate constantele din biblioteca periferică sunt cel mai adesea scrise cu majuscule.
  • Registrele sunt tratate ca constante și sunt numite și litere MAJUSCULE.
  • Numele de funcții specifice perifericelor includ un acronim, cum ar fi USART_SendData() .
  • Pentru a configura fiecare dispozitiv periferic, se folosește structura PPP_InitTypeDef, care este transmisă funcției PPP_Init().
  • Pentru a deinițializa (setați valoarea implicită), puteți utiliza funcția PPP_DeInit().
  • Funcția care vă permite să activați sau să dezactivați periferice se numește PPP_Cmd().
  • Funcția de activare/dezactivare a întreruperii se numește PPP_ITConfig.

Puteți vedea din nou lista completă în fișierul de suport al bibliotecii. Acum să rescriem LED-ul care clipește folosind biblioteca standard de periferice!

Înainte de a începe lucrul, să ne uităm la fișierul stm32f10x.h și să găsim linia:

#define USE_STDPERIPH_DRIVER

Dacă configurați proiectul de la zero folosind fișiere de bibliotecă din arhiva descărcată, atunci va trebui să decomentați această linie. Vă va permite să utilizați biblioteca standard. Această definiție(macro) va comanda preprocesorului să includă fișierul stm32f10x_conf.h:

#ifdef USE_STDPERIPH_DRIVER #include „stm32f10x_conf.h” #endif

Acest fișier conține module. Dacă aveți nevoie doar de unele specifice, dezactivați restul, acest lucru va economisi timp în timpul compilării. După cum probabil ați ghicit, avem nevoie module RTCși GPIO (cu toate acestea, în viitor veți avea nevoie și de _bkp.h , _flash , _pwr.h , _rtc.h , _spi.h , _tim.h , _usart.h ):

#include „stm32f10x_flash.h” // pentru init_pll() #include „stm32f10x_gpio.h” #include „stm32f10x_rcc.h”

Ca și data trecută, mai întâi trebuie să activați tactarea portului B. Acest lucru se face de funcția declarată în stm32f10x_rcc.h:

Void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

Enumerația FunctionalState este definită în stm32f10x.h:

Typedef enum (DISABLE = 0, ENABLE = !DISABLE) FunctionalState;

Să declarăm o structură pentru configurarea piciorului nostru (o puteți găsi în fișierul stm32f10x_gpio.h):

LED GPIO_InitTypeDef;

Acum trebuie să o completăm. Să ne uităm la conținutul acestei structuri:

Typedef struct ( uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; ) GPIO_InitTypeDef;

Toate enumerările și constantele necesare pot fi găsite în același fișier. Apoi, funcția init_leds() rescrisă va lua următoarea formă:

Void led_init() ( // Activați sincronizarea RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Declarați structura și umpleți-o GPIO_InitTypeDef LED; LED.GPIO_Pin = GPIO_Pin_0; LED.GPIO_Mode.GPIO_Speed_GPIO_Speed._GPIO _Out_PP // Inițializați portul GPIO_Init( GPIOB,&LED);

Să rescriem funcția main():

Int main(void) (led_init(); în timp ce (1) ( GPIO_SetBits(GPIOB, GPIO_Pin_0); delay(10000000); GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay(10000000); ) )

Principalul lucru este să înțelegeți ordinea de inițializare: porniți ceasul periferic, declarați structura, completați structura, apelați metoda de inițializare. Alte dispozitive periferice sunt de obicei configurate într-un mod similar.

Ei bine, până acum totul merge bine, dar doar becurile și butoanele sunt gata. Acum este timpul să preluăm periferice mai grele - USB, UART, I2C și SPI. Am decis să încep cu USB - depanatorul ST-Link (chiar și cel real de la Discovery) a refuzat cu încăpățânare să-mi depaneze placa, așa că depanarea prin USB este singura metodă de depanare disponibilă pentru mine. Puteți, desigur, prin UART, dar aceasta este o grămadă de fire suplimentare.

Am luat din nou calea lungă - am generat spațiile corespunzătoare în STM32CubeMX și am adăugat USB Middleware din pachetul STM32F1Cube la proiectul meu. Trebuie doar să activați sincronizarea USB, să definiți gestionatorii de întreruperi USB corespunzători și să lustruiți lucrurile mici. În cea mai mare parte, am copiat toate setările importante ale modulului USB de la STM32GENERIC, cu excepția faptului că am modificat ușor alocarea memoriei (au folosit malloc, iar eu am folosit alocarea statică).

Iată câteva piese interesante pe care le-am smuls. De exemplu, pentru ca gazda (calculatorul) să înțeleagă că ceva este conectat la el, dispozitivul „distorsionează” linia USB D+ (care este conectată la pinul A12). După ce a văzut acest lucru, gazda începe să interogheze dispozitivul despre cine este, ce interfețe poate gestiona, cu ce viteză dorește să comunice etc. Nu înțeleg cu adevărat de ce trebuie făcut acest lucru înainte de inițializarea USB, dar în stm32duino se face aproape în același mod.

smucitură USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() ( // Inițializați pinul PA12 GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed ​​​​= GPIO_SPEED_FREQ_LOW; Permiteți să știe dispozitivul HAL_GPIOA pe magistrala 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); }


Un alt punct interesant este suportul pentru bootloader-ul stm32duino. Pentru a încărca firmware-ul, trebuie mai întâi să reporniți controlerul în bootloader. Cel mai simplu mod este să apăsați butonul de resetare. Dar pentru a face acest lucru mai convenabil, puteți adopta experiența Arduino. Când copacii erau tineri, controlerele AVR nu aveau încă suport USB la bord, pe placă era un adaptor USB-UART. Semnalul DTR UART este conectat la resetarea microcontrolerului. Când gazda trimite semnalul DTR, microcontrolerul este repornit în bootloader. Funcționează ca betonul armat!

În cazul utilizării USB, emulăm doar un port COM. În consecință, trebuie să reporniți singur în bootloader. Bootloader-ul stm32duino, pe lângă semnalul DTR, pentru orice eventualitate, se așteaptă și la o constantă magică specială (1EAF - o referință la Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length) (... caz CDC_SET_CONTROL_LINE_STATE: dtr_pin++; //Pinul DTR este activat break; ... static int8_t CDC_Receive_FS (uint8_int3*) (uint8_t3*) Four *_f byte este pachetul magic „1EAF” care pune MCU în bootloader */ if(*Len >= 4) ( /** * Verificați dacă mesajul primit conține șirul „1EAF". * Dacă da, verificați dacă DTR-ul are. fost setat, pentru a pune MCU în modul bootloader */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&&. (Buf == "F")) ( HAL_NVIC_SystemReset(); ) dtr_pin = 0 ) ) ... )

Întoarcere: MiniArduino

În general, USB a funcționat. Dar acest strat funcționează doar cu octeți, nu șiruri. De aceea, amprentele de depanare arată atât de urât.

CDC_Transmit_FS((uint8_t*)"Ping\n", 5); // 5 este un strlen ("Ping") + zero octet
Acestea. Nu există deloc suport pentru ieșirea formatată - nu puteți tipări un număr sau asambla un șir din bucăți. Următoarele opțiuni apar:

  • Înșurubați printf clasic. Opțiunea pare să fie bună, dar necesită +12 kb de firmware (am numit cumva accidental sprintf)
  • Descoperiți propria implementare a printf din stocul dvs. Am scris odată pentru AVR, se pare că această implementare a fost mai mică.
  • Atașați clasa Print de la Arduino la implementarea STM32GENERIC
Am ales a doua opțiune, deoarece codul bibliotecii Adafruit GFX se bazează și pe Print, așa că mai trebuie să-l înșurubesc. În plus, aveam deja codul STM32GENERIC la îndemână.

Am creat un director MiniArduino în proiectul meu cu scopul de a pune acolo cantitatea minimă necesară de cod pentru a implementa piesele interfeței arduino de care aveam nevoie. Am început să copiez câte un fișier și să mă uit la ce alte dependențe erau necesare. Așa că am ajuns cu o copie a clasei Print și mai multe fișiere de legare.

Dar acest lucru nu este suficient. Era încă necesar să se conecteze cumva clasa Print cu funcții USB (de exemplu, CDC_Transmit_FS()). Pentru a face acest lucru, a trebuit să tragem în clasa SerialUSB. A tras clasa Stream și o parte de inițializare GPIO. Următorul pas a fost conectarea UART-ului (am un GPS conectat la el). Așa că am adus și clasa SerialUART, care a mai tras cu ea un alt strat de inițializare periferică de la STM32GENERIC.

În general, m-am trezit în următoarea situație. Am copiat aproape toate fișierele de pe STM32GENERIC pe MiniArduino. Aveam și propria mea copie a bibliotecilor USB și FreeRTOS (ar fi trebuit să am și copii ale HAL și CMSIS, dar eram prea leneș). În același timp, am marcat timpul de o lună și jumătate - conectând și deconectând diferite piese, dar în același timp nu am scris o singură linie de cod nou.

A devenit clar că ideea mea inițială era să preiau controlul asupra tuturor parte a sistemului Nu merge prea bine. Oricum, o parte din codul de inițializare se află în STM32GENERIC și pare să fie mai confortabil acolo. Desigur, aș putea tăia toate dependențele și a-mi scrie propriile clase de wrapper pentru sarcinile mele, dar acest lucru m-ar încetini încă o lună - acest cod încă trebuie depanat. Desigur, acest lucru ar fi grozav pentru propria ta situație de urgență, dar trebuie să mergi mai departe!

Deci, am aruncat toate bibliotecile duplicate și aproape întregul meu strat de sistem și am revenit la STM32GENERIC. Acest proiect se dezvoltă destul de dinamic - mai multe comite pe zi în mod constant. În plus, în această lună și jumătate am studiat mult, am citit majoritatea manualului de referință STM32, m-am uitat la modul în care au fost făcute biblioteci HALși ambalaje STM32GENERIC, înțelegere avansată a descriptorilor USB și perifericelor microcontrolerului. În general, acum eram mult mai încrezător în STM32GENERIC decât înainte.

Revers: I2C

Totuși, aventurile mele nu s-au încheiat aici. Mai existau UART și I2C (afișajul meu locuiește acolo). Cu UART totul a fost destul de simplu. Tocmai am eliminat alocarea memoriei dinamice și, pentru ca UART-urile neutilizate să nu consume chiar această memorie, le-am comentat pur și simplu.

Dar implementarea I2C în STM32GENERIC a fost o mică problemă. Una foarte interesantă, dar care mi-a luat cel puțin 2 seri. Ei bine, sau a dat 2 seri de depanare grea pe printuri - așa privești lucrurile.

În general, implementarea afișajului nu a început. În stilul deja tradițional, pur și simplu nu funcționează și atât. Ce nu merge nu este clar. Biblioteca afișajului în sine (Adafruit SSD1306) pare să fi fost testată în implementarea anterioară, dar erorile de interferență încă nu ar trebui excluse. Suspiciunea cade asupra HAL și a implementării I2C de la STM32GENERIC.

Pentru început, am comentat tot afișajul și codul I2C și am scris o inițializare I2C fără biblioteci, în pur HAL

Inițializare I2C

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __I2C1_CLK_ENABLE(); hi2c1.Instanță = 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);


Am eliminat starea registrelor imediat după inițializare. Am făcut același dump într-o versiune de lucru pe stm32duino. Asta am primit (cu comentarii pentru mine)

Bun (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: întrerupere de eroare activată, 36Mhz
40005408: 0 0 0 0 - I2C_OAR1: adresa proprie zero

40005410: 0 0 0 AF - I2C_DR: registru de date

40005418: 0 0 0 0 - I2C_SR2: registru de stare

Rău (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Activare periferică
40005404: 0 0 0 24 - I2C_CR2: 36Mhz
40005408: 0 0 40 0 ​​​​- I2C_OAR1: !!! Bit nedescris în setul de registru de adrese
4000540C: 0 0 0 0 - I2C_OAR2: Registrul de adrese propriu
40005410: 0 0 0 0 - I2C_DR: registru de date
40005414: 0 0 0 0 - I2C_SR1: registru de stare
40005418: 0 0 0 2 - I2C_SR2: bit ocupat setat
4000541C: 0 0 80 1E - I2C_CCR: modul 400kHz
40005420: 0 0 0 B - I2C_TRISE

Prima mare diferență este al 14-lea bit setat în registrul I2C_OAR1. Acest bit nu este descris deloc în fișa de date și se încadrează în secțiunea rezervată. Adevărat, cu avertismentul că mai trebuie să scrieți unul acolo. Acestea. Acesta este o eroare în libmaple. Dar din moment ce totul funcționează acolo, atunci nu aceasta este problema. Să săpăm mai departe.

O altă diferență este că bitul ocupat este setat. La început nu i-am acordat nicio importanță, dar privind înainte voi spune că el a fost cel care a semnalat problema!.. Dar mai întâi lucrurile.

Am creat codul de inițializare fără biblioteci.

Inițializarea afișajului

void sendCommand(I2C_HandleTypeDef * handle, uint8_t cmd) ( SerialUSB.print ("Trimiterea comenzii "); SerialUSB.println(cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Transmit_Master,_ICE_C1DRESS<<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); }


După ceva efort, acest cod a funcționat pentru mine (în acest caz, a desenat dungi). Aceasta înseamnă că problema se află în stratul I2C al STM32GENERIC. Am început să-mi elimin treptat codul, înlocuindu-l cu părțile corespunzătoare din bibliotecă. Dar de îndată ce am schimbat codul de inițializare pin de la implementarea mea la cea de bibliotecă, întreaga transmisie I2C a început să expire.

Apoi mi-am amintit de partea ocupată și am încercat să înțeleg când se întâmplă. S-a dovedit că indicatorul de ocupat apare imediat ce codul de inițializare activează ceasul I2c. Acestea. Modulul pornește și imediat nu funcționează. Interesant.

Cădem pe inițializare

uint8_t * pv = (uint8_t*)0x40005418; //Registrul I2C_SR2. Se caută flag BUSY SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Imprimă 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); //Tipărește 2


Deasupra acestui cod este doar inițializarea pinilor. Ei bine, ce să faceți - acoperiți depanarea cu amprente peste linie și acolo

Se inițializează pinii STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instanță, GPIO_TypeDef *port, uint32_t pin, uint32_t mode, uint32_t pull) ( ... GPIO_InitTypeDef GPIO_InitTypeDef GPIO_InitStruct. Mode = mod; GPIO_InitStruct.Pull = pull ; GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_VERY_HIGH (port, &GPIO_InitStruct ... )


Dar ghinion - GPIO_InitStruct este completat corect. Doar al meu funcționează, dar acesta nu. Într-adevăr, mistic!!! Totul este conform manualului, dar nimic nu funcționează. Am studiat codul bibliotecii linie cu linie, căutând ceva suspect. În cele din urmă, am dat peste acest cod (apelează funcția de mai sus)

Încă o piesă de inițializare

void stm32AfI2CInit(const I2C_TypeDef *instanță, ...) ( stm32AfInit(chip_af_i2c_sda, ...); stm32AfInit(chip_af_i2c_scl, ...); )


Vedeți un bug în el? Și ea este! Am eliminat chiar și parametrii care nu sunt necesari pentru a clarifica problema. În general, diferența este că codul meu inițializează ambii pini simultan într-o structură, iar codul STM32GENERIC unul câte unul. Aparent, codul de inițializare a pinului afectează cumva nivelul acestui pin. Înainte de inițializare, nu iese nimic pe acest pin și rezistorul ridică nivelul la unu. În momentul inițializării, din anumite motive, controlerul setează zero pe piciorul corespunzător.

Acest fapt în sine este inofensiv. Dar problema este că coborârea liniei SDA în timp ce se ridică linia SCL este o condiție de pornire pentru magistrala i2c. Din această cauză, receptorul controlerului înnebunește, setează steag-ul BUSY și începe să aștepte date. Am decis să nu elimin biblioteca pentru a adăuga capacitatea de a inițializa mai mulți pini deodată. În schimb, pur și simplu am schimbat aceste 2 linii - inițializarea afișajului a avut succes. Remedierea a fost adoptată în STM32GENERIC.

Apropo, în libmaple inițializarea magistralei se face într-un mod interesant. Înainte de a începe să inițializați perifericele i2c de pe magistrală, faceți mai întâi o resetare. Pentru a face acest lucru, biblioteca comută pinii în modul GPIO normal și scutură aceste picioare de mai multe ori, simulând secvențele de pornire și oprire. Acest lucru ajută la revigorarea dispozitivelor blocate în autobuz. Din păcate, nu există un lucru similar în HAL. Uneori, afișajul meu se blochează și atunci singura soluție este să opresc alimentarea.

Inițializarea i2c de la stm32duino

/** * @brief Resetați o magistrală I2C. * * Resetarea se realizează prin sincronizarea impulsurilor până când orice sclav * eliberează SDA și SCL, apoi generează o condiție START, apoi o condiție STOP *. * * @param dev Dispozitiv I2C */ void i2c_bus_reset(const i2c_dev *dev) ( /* Eliberați ambele linii */ i2c_master_release_bus(dev); /* * Asigurați-vă că magistrala este liberă, tacându-l până când orice slave eliberează *busul. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* Așteptați ca orice extindere a ceasului să se termine */ while (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ; delay_us(10) ); /* Pull low */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0 /* Release high */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); ) /* Generați condiția de start și oprire */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0 gpio_write_bit(scl_port(dev), dev->scl_pin, 1); gpio_write_bit(sda_port(dev), dev->sda_pin, 1);

Din nou: UART

M-am bucurat să revin în sfârșit la programare și să continui să scriu funcții. Următoarea piesă mare a fost conectarea cardului SD prin SPI. Aceasta în sine este o activitate incitantă, interesantă și dureroasă. Cu siguranță voi vorbi despre asta separat în articolul următor. Una dintre probleme a fost încărcarea mare a procesorului (>50%). Acest lucru a pus sub semnul întrebării eficiența energetică a dispozitivului. Și a fost incomod să folosești dispozitivul, pentru că... Interfața de utilizare a fost teribil de proastă.

Înțelegând problema, am găsit motivul acestui consum de resurse. Toate lucrările cu cardul SD s-au întâmplat octet cu octet, folosind procesorul. Dacă a fost necesar să scrieți un bloc de date pe card, atunci pentru fiecare octet este apelată funcția de trimitere octet

Pentru (uint16_t i = 0; i< 512; i++) { spiSend(src[i]);
Nu, nu e grav! Există DMA! Da, biblioteca SD (cea care vine cu Arduino) este neîndemânatică și trebuie schimbată, dar problema este mai globală. Aceeași imagine este observată în biblioteca de ecran și chiar și ascultarea UART a fost făcută printr-un sondaj. În general, am început să cred că rescrierea tuturor componentelor în HAL nu este o idee atât de stupidă.

Am început, desigur, cu ceva mai simplu - un driver UART care ascultă fluxul de date de la GPS. Interfața Arduino nu vă permite să vă atașați la întreruperea UART și să smulgeți din mers caracterele primite. În cele din urmă singura cale Primirea datelor este un sondaj constant. Desigur, am adăugat vTaskDelay(10) la handlerul GPS pentru a reduce încărcarea măcar puțin, dar în realitate aceasta este o cârjă.

Primul gând, desigur, a fost să atașez DMA. Ar funcționa chiar dacă nu ar fi protocolul NMEA. Problema este că în acest protocol, informațiile circulă pur și simplu, iar pachetele (liniile) individuale sunt separate printr-un caracter de întrerupere de linie. Mai mult, fiecare linie poate avea lungimi diferite. Din acest motiv, nu se știe dinainte câte date trebuie primite. DMA nu funcționează așa - numărul de octeți trebuie setat în prealabil la inițializarea transferului. Pe scurt, DMA nu mai este necesar, așa că căutăm o altă soluție.

Dacă te uiți îndeaproape la designul bibliotecii NeoGPS, poți vedea că biblioteca acceptă datele de intrare octet cu octet, dar valorile sunt actualizate doar când a ajuns întreaga linie (mai precis, un lot de mai multe linii ). Acea. nu are nicio diferență dacă să alimentezi octeții de bibliotecă unul câte unul pe măsură ce sunt primiți sau apoi toți odată. Deci, puteți economisi timp procesor prin salvarea liniei primite într-un buffer și puteți face acest lucru direct în întrerupere. Când întreaga linie este primită, procesarea poate începe.

Următorul design apare

Clasa de șofer UART

// Dimensiunea tamponului de intrare UART const uint8_t gpsBufferSize = 128; // Această clasă se ocupă de interfața UART care primește caractere de la GPS și le stochează într-o clasă tampon GPS_UART ( // mâner hardware UART UART_HandleTypeDef uartHandle; // Primește buffer inel uint8_t rxBuffer; volatil uint8_t lastReadIndex = 0; volatil uint8_t lastReadIndex; / Mânerul firului GPS TaskHandle_t xGPSThread = NULL;


Deși inițializarea este copiată de pe STM32GENERIC, corespunde complet cu ceea ce oferă CubeMX

Inițializare UART

void init() ( // Resetați pointerii (doar în cazul în care cineva apelează init() de mai multe ori) lastReadIndex = 0; lastReceivedIndex = 0; // Inițializați mânerul Thread-ului GPS xGPSThread = xTaskGetCurrentTaskHandle(); // Activați sincronizarea periperhalului corespunzătoare_CLC___HALAABLE_RC__GPIO __HAL_RCC_USART1_CLK_ENABLE(); // Init pins în mod alternativ GPIO_InitStruct.Pin = GPIO_PIN_9 //TX pin GPIO_InitStruct. InitStruct); GPIO_PIN_10; //RX pin GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_GPIO_Init(GPIO_InitStruct); 1; uartHandle.Init.Parity = UART_PARITY_NONE; Init.Mode = UART_MODE_TX_RX; uartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // Vom folosi întrerupere UART pentru a obține date HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Vom aștepta un singur drept primit drept către buffer HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


De fapt, pinul TX nu a putut fi inițializat, dar uartHandle.Init.Mode ar putea fi setat la UART_MODE_RX - doar îl vom primi. Totuși, lasă-l să fie - ce se întâmplă dacă trebuie să configurez cumva modul GPSși scrieți comenzi în el.

Designul acestei clase ar fi putut arăta mai bine dacă nu ar fi fost pentru limitările arhitecturii HAL. Deci, nu putem pur și simplu să setăm modul, spun ei, să acceptăm totul, să ne atașăm direct la întrerupere și să smulgem octeții primiți direct din registrul de recepție. Trebuie să spunem în avans lui HAL câți și unde vom primi octeți - handlerii corespunzători vor scrie octeții primiți în buffer-ul furnizat. În acest scop, în ultima linie a funcției de inițializare există un apel către HAL_UART_Receive_IT(). Deoarece lungimea șirului este necunoscută în avans, trebuie să luăm câte un octet.

De asemenea, trebuie să declarați până la 2 apeluri inverse. Unul este un handler de întrerupere, dar sarcina lui este doar să apeleze handler-ul din HAL. A doua funcție este „callback” de la HAL, conform căreia octetul a fost deja primit și este deja în buffer.

Reapeluri UART

// Redirecționează procesarea întreruperii UART către HAL extern "C" void USART1_IRQHandler(void) ( HAL_UART_IRQHandler(gpsUart.getUartHandle()); ) // HAL apelează acest apel invers când primește un caracter de la UART. Redirecționați-l către clasa extern „C” void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) ( gpsUart.charReceivedCB(); )


Metoda charReceivedCB() pregătește HAL pentru a primi următorul octet. De asemenea, este cea care determină că linia sa încheiat deja și că acest lucru poate fi semnalat programului principal. Un semafor în modul semnal ar putea fi folosit ca mijloc de sincronizare, dar în scopuri atât de simple se recomandă utilizarea notificărilor directe.

Procesarea unui octet primit

// Char primit, pregătiți-vă pentru următorul inline void charReceivedCB() ( char lastReceivedChar = rxBuffer; lastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize), 1); // Acea linie nu este primită simbolul EOL EOL este disponibil pentru a citi if(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL )


Funcția de răspuns (în așteptare) este waitForString(). Sarcina sa este pur și simplu să se atârne pe obiectul de sincronizare și să aștepte (sau să iasă cu un timeout)

Așteptând sfârșitul firului

// Așteptați până când întreaga linie este primită bool waitForString() ( returnează ulTaskNotifyTake(pdTRUE, 10); )


Funcționează așa. Firul care este responsabil pentru GPS doarme în mod normal în funcția waitForString(). Octeții care provin de la GPS sunt adăugați într-un buffer de către handler-ul de întrerupere. Dacă sosește caracterul \n (sfârșitul rândului), atunci întreruperea trezește firul principal, care începe să toarne octeți din buffer în parser. Ei bine, atunci când analizatorul termină de procesat pachetul de mesaje, va actualiza datele din modelul GPS.

Flux GPS

void vGPSTask(void *pvParameters) ( // Inițializarea GPS trebuie făcută în firul GPS, deoarece mânerul firului este stocat // și folosit mai târziu în scopuri de sincronizare gpsUart.init(); pentru (;;) ( // Așteptați până când întregul șir este primit if(!gpsUart.waitForString()) continua // Citește șirul primit și analizează fluxul GPS char by 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, gpsParser. ) vTaskDelay(10);


Am dat peste un moment foarte nebanal în care am rămas blocat câteva zile. Se pare că codul de sincronizare a fost luat din exemple, dar la început nu a funcționat - a prăbușit întregul sistem. Am crezut că problema era în notificările directe (funcțiile xTaskNotifyXXX), am schimbat-o la semafoare obișnuite, dar aplicația încă s-a blocat.

S-a dovedit că trebuie să fii foarte atent cu prioritatea întreruperii. Implicit, am setat toate întreruperile la zero (cea mai mare) prioritate. Dar FreeRTOS are o cerință ca prioritățile să fie într-un interval dat. Întreruperile cu o prioritate prea mare nu pot apela funcțiile FreeRTOS. Numai întreruperile cu prioritate configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY și mai jos pot apela funcțiile sistemului(explicație bună și ). Această setare implicită este setată la 5. Am schimbat prioritatea de întrerupere UART la 6 și totul a funcționat.

Din nou: I2C prin DMA

Acum puteți face ceva mai complex, cum ar fi driverul de afișare. Dar aici trebuie să facem o excursie în teoria autobuzului I2C. Această magistrală în sine nu reglementează protocolul de transfer de date pe magistrală - puteți fie să scrieți octeți, fie să îi citiți. Puteți chiar să scrieți și apoi să citiți într-o singură tranzacție (de exemplu, să scrieți o adresă și apoi să citiți datele la această adresă).

Cu toate acestea, majoritatea dispozitivelor definesc un protocol mai mult nivel inalt cam la fel. dispozitivul oferă utilizatorului un set de registre, fiecare cu propria sa adresă. Mai mult, în protocolul de comunicare, primul octet (sau mai mulți) din fiecare tranzacție determină adresa celulei (registrului) în care vom citi sau scrie în continuare. În acest caz, este posibil și schimbul multi-byte în stilul „acum vom scrie/citi mulți octeți începând de la această adresă”. Ultima opțiune este bună pentru DMA.

Din păcate, afișajul bazat pe controlerul SSD1306 oferă un protocol complet diferit - comandă. Primul octet al fiecărei tranzacții este atributul „comandă sau date”. În cazul unei comenzi, al doilea octet este codul comenzii. Dacă o comandă are nevoie de argumente, acestea sunt transmise ca comenzi separate după prima. Pentru a inițializa afișajul, trebuie să trimiteți aproximativ 30 de comenzi, dar acestea nu pot fi puse într-o singură matrice și trimise într-un singur bloc. Trebuie să le trimiteți pe rând.

Dar atunci când trimiteți o matrice de pixeli (frame buffer), este foarte posibil să utilizați serviciile DMA. Asta vom încerca.

Dar biblioteca Adafruit_SSD1306 este scrisă foarte neîndemânatic și este imposibil să o strângi cu puțin efort. Se pare că biblioteca a fost scrisă pentru prima dată pentru a comunica cu afișajul prin SPI. Apoi cineva a adăugat suport I2C, dar suportul SPI a rămas activat. Apoi cineva a început să adauge tot felul de optimizări de nivel scăzut și să le ascundă în spatele ifdef-urilor. Prin urmare, s-a dovedit a fi o mizerie de cod pentru suportarea diferitelor interfețe.

La început am încercat să pun acest lucru în ordine prin încadrarea codului pentru diferite interfețe cu ifdefs. Dar dacă vreau să scriu cod de comunicare cu afișajul, să folosesc DMA și sincronizarea prin FreeRTOS, atunci nu voi putea face mare lucru. Va fi mai precis, dar acest cod va trebui scris direct în codul bibliotecii. Prin urmare, am decis să reprocesez biblioteca încă o dată, să fac o interfață și să pun fiecare driver într-o clasă separată. Codul a devenit mai curat și ar fi posibil să adăugați fără durere suport pentru noi drivere, fără a schimba biblioteca în sine.

Afișează interfața driverului

// Interfață pentru driverul hardware // Adafruit_SSD1306 nu funcționează direct cu hardware-ul // Toate solicitările de comunicare sunt redirecționate către clasa de driver ISSD1306Driver ( public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0 ; virtual void sendData(uint8_t * data, size_t size) = 0);


Deci să mergem. Am arătat deja inițializarea I2C. Nu s-a schimbat nimic acolo. Dar trimiterea comenzii a devenit puțin mai ușoară. Vă amintiți când am vorbit despre diferența dintre protocoalele de registru și de comandă pentru dispozitivele I2C? Și deși afișajul implementează un protocol de comandă, acesta poate fi simulat destul de bine folosind un protocol de registru. Trebuie doar să vă imaginați că afișajul are doar 2 registre - 0x00 pentru comenzi și 0x40 pentru date. Și HAL oferă chiar și o funcție pentru acest tip de transfer

Trimiterea unei comenzi pe afișaj

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


La început nu era foarte clar despre trimiterea datelor. Codul original a trimis date în pachete mici de 16 octeți

Cod ciudat de trimitere a datelor

pentru (uint16_t i=0; i


Am încercat să mă joc cu dimensiunea pachetului și să trimit pachete mai mari, dar în cel mai bun caz am primit un afișaj mototolit. Ei bine, sau totul era atârnat.

Afișaj decupat



Motivul s-a dovedit a fi banal - depășirea tamponului. Clasa Wire de la Arduino (cel puțin STM32GENERIC) oferă propriul buffer de doar 32 de octeți. Dar de ce avem nevoie de un buffer suplimentar dacă clasa Adafruit_SSD1306 are deja unul? Mai mult, cu HAL, trimiterea se face pe o singură linie

Transfer corect de date

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


Deci, jumătate din bătălie este gata - am scris un driver pentru afișaj în pur HAL. Dar în această versiune este încă solicitant resurse - 12% din procesor pentru un afișaj 128x32 și 23% pentru un afișaj 128x64. Utilizarea DMA este foarte recomandată aici.

Mai întâi, să inițializam DMA. Vrem să implementăm transmiterea datelor în I2C nr. 1, iar această funcție se află pe al șaselea canal DMA. Inițializați copierea octet cu octet din memorie pe periferice

Configurarea DMA pentru I2C

// Activarea ceasului controlerului DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Inițializați 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); // Asociați mânerul DMA inițializat cu mânerul I2C __HAL_LINKDMA(&handle, hdmatx, hdma_tx); /* Inițiere întrerupere DMA */ /* Configurare întrerupere DMA1_Channel6_IRQn */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);


Întreruperile sunt o parte obligatorie a proiectării. În caz contrar, funcția HAL_I2C_Mem_Write_DMA() va începe o tranzacție I2C, dar nimeni nu o va finaliza. Din nou avem de-a face cu designul HAL greoi și cu nevoia de până la două apeluri. Totul este exact la fel ca cu UART. O funcție este un handler de întrerupere - pur și simplu redirecționăm apelul către HAL. A doua funcție este un semnal că datele au fost deja trimise.

Manageri de întreruperi DMA

extern „C” void DMA1_Channel6_IRQHandler(void) ( HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); ) extern „C” void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c_transfer.());Drited;


Desigur, nu vom sonda în mod constant I2C pentru a vedea dacă transferul s-a încheiat deja? În schimb, trebuie să dormiți pe obiectul de sincronizare și să așteptați până când transferul este finalizat

Transfer de date prin DMA cu sincronizare

void DisplayDriver::sendData(uint8_t * data, size_t size) ( // Începe transferul de date HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // Așteptați până când transferul este finalizat ulTaskNotifyTake(pd00TRUE);) void100TRUE; DisplayDriver::transferCompletedCB() ( // Reluați firul de afișare vTaskNotifyGiveFromISR(xDisplayThread, NULL); )


Transferul de date durează încă 24 ms - acesta este un timp de transfer aproape pur de 1 kB (dimensiunea tamponului de afișare) la 400 kHz. Doar în acest caz, de cele mai multe ori procesorul pur și simplu dorm (sau face alte lucruri). Sarcina totală a procesorului a scăzut de la 23% la doar 1,5-2%. Cred că a meritat să lupți pentru această cifră!

Din nou: SPI prin DMA

Conectarea unui card SD prin SPI a fost într-un fel mai ușoară - până atunci am început să instalez biblioteca sdfat și acolo oamenii buni au separat deja comunicarea cu cardul într-o interfață separată de driver. Adevărat, cu ajutorul definițiilor puteți alege doar una dintre cele 4 versiuni de driver gata făcute, dar acest lucru ar putea fi ușor irosit și înlocuit cu propria implementare.

Interfață driver SPI pentru lucrul cu un card SD

// Aceasta este implementarea personalizată a clasei SPI Driver. Biblioteca SdFat // folosește această clasă pentru a accesa cardul SD prin SPI // // Intenția principală a acestei implementări este de a conduce transferul de date // prin DMA și sincronizarea cu capabilitățile FreeRTOS. clasa SdFatSPIDriver: public SdSpiBaseDriver ( // modulul SPI SPI_HandleTypeDef spiHandle; // mâner de fir GPS TaskHandle_t xSDThread = NULL; public: SdFatSPIDriver(); virtual void activate(); virtual void begin(uint8_t); virtual voidPinte(); virtual voidPinte(); uint8_t receive( virtual uint8_t receive(uint8_t* buf, size_t n virtual void send(uint8_t data) );


Ca și înainte, începem cu ceva simplu - cu o implementare de stejar fără niciun DMA. Inițializarea este parțial generată de CubeMX și parțial fuzionată cu implementarea SPI a STM32GENERIC

Inițializare SPI

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) ( // Ignorați pinul CS trecut - Acest driver funcționează cu un (void)chipSelectPin predefinit; // Inițializați mânerul Thread GPS xSDThread = xTaskGetCurrentTaskHandle(); // Enable_CurrentTaskHandle(); // Enable_CurrentTaskHandle(); ABLE() __HAL_RCC_SPI1_CLK_ENABLE(); // Init pins GPIO_InitTypeDef GPIO_PIN_5|GPIO_PIN_7 //MOSI & SCK GPIO_InitStruct _Init(GPIOA, &GPIO_InitStruct.Pin = GPIO_PIN_6; //MISO GPIO_InitStruct.Mode); = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_HIGH(GPIOA, &GPIO_InitStruct, set HIP_GP_IP_IP_IP_IP ; , GPIO_PIN_SET); // Init SPI spiHandle.Instance = 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); )


Designul interfeței este adaptat pentru Arduino cu pini numerotați cu un număr. În cazul meu, nu avea niciun rost să setăm pinul CS prin parametri - am acest semnal strict legat de pinul A4, dar a fost necesar să se respecte interfața.

Prin proiectarea bibliotecii SdFat, viteza portului SPI este ajustată înainte de fiecare tranzacție. Acestea. teoretic, puteți începe să comunicați cu cardul la viteză mică și apoi să o creșteți. Dar am renunțat la asta și am ajustat viteza o dată în metoda begin(). Deci metodele de activare/dezactivare s-au dovedit goale. La fel ca setSpiSettings()

Manipulatori banali de tranzacții

void SdFatSPIDriver::activate() ( // Nu este nevoie de activare specială ) void SdFatSPIDriver::deactivate() ( // Nu este necesară o dezactivare specială ) void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) ( // Ignoră setările - folosim aceleași setări pentru toate transferurile)


Metodele de control al semnalului CS sunt destul de banale

Controlul semnalului CS

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


Să trecem la partea distractivă - citit și scris. Prima implementare cea mai de stejar fără DMA

Transfer de date fără 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 (size:::8_receive) : Primește prin DMA aici memset(buf, 0xff, n ) void SdFatSPIDriver::send(const uint8_t* buf, size_t n) ( // TODO: Transmite prin DMA aici HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n,); 10);


În interfața SPI, recepția și transmisia datelor au loc simultan. Pentru a primi ceva trebuie să trimiteți ceva. De obicei, HAL face acest lucru pentru noi - pur și simplu numim funcția HAL_SPI_Receive() și organizează atât trimiterea, cât și primirea. Dar, de fapt, această funcție trimite gunoiul care se afla în bufferul de primire.
Pentru a vinde ceva inutil, trebuie mai întâi să cumpărați ceva inutil (C) Prostokvashino

Dar există o nuanță. Cardurile SD sunt foarte capricioase. Nu le place să li se înmâneze nimic în timp ce cardul trimite date. Prin urmare, a trebuit să folosesc funcția HAL_SPI_TransmitReceive() și să trimit forțat 0xffs în timp ce primim date.

Să luăm măsurători. Lăsați un fir să scrie 1 kb de date pe card într-o buclă.

Cod de testare pentru trimiterea unui flux de date pe un card SD

uint8_t sd_buf; uint16_t i=0; uint32_t prev = HAL_GetTick(); while(true) (​bulkFile.write(sd_buf, 512); bulkFile.write(sd_buf, 512); i++; uint32_t cur = HAL_GetTick(); if(cur-prev >= 1000) (prev = cur; usbDebugWrite(); „Salvat %d kb\n”, i = 0 ) )


Cu această abordare, aproximativ 15-16 kb pot fi înregistrate pe secundă. Nu prea mult. Dar s-a dovedit că am setat prescaler-ul la 256. Adică. Clockingul SPI este setat la mult mai puțin decât debitul posibil. Experimental, am aflat că nu are sens să setați frecvența mai mare de 9 MHz (prescaler-ul este setat la 8) - o viteză de înregistrare mai mare de 100-110 kb/s nu poate fi atinsă (pe altă unitate flash, de altfel , din anumite motive a fost posibil să se înregistreze doar 50-60 kb/s, iar pe al treilea este în general doar 40 kb/s). Aparent, totul depinde de timeout-urile unității flash în sine.

În principiu, acest lucru este deja mai mult decât suficient, dar vom pompa date prin DMA. Procedăm conform schemei deja cunoscute. În primul rând, inițializarea. Primim și transmitem prin SPI pe al doilea și respectiv al treilea canal DMA.

Inițializare DMA

// Activarea ceasului controlerului DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Canalul Rx DMA dmaHandleRx.Instance = DMA1_Channel2; dmaHandleRx.Init.Direction = DMA_PERIPH_TO_MEMORY; dmaHandleRx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleRx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleRx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleRx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleRx.Init.Mode = DMA_NORMAL; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle, hdmarx, dmaHandleRx); // Tx canal DMA dmaHandleTx.Instance = DMA1_Channel3; dmaHandleTx.Init.Direction = DMA_MEMORY_TO_PERIPH; dmaHandleTx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleTx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleTx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleTx.Init.Mode = DMA_NORMAL; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle, hdmatx, dmaHandleTx);


Nu uitați să activați întreruperile. Pentru mine vor merge cu prioritate 8 - puțin mai mică decât UART și I2C

Configurarea întreruperilor DMA

// Configurarea DMA întrerupe 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);


Am decis că suprasarcina de rulare a DMA și sincronizare pentru transferuri scurte ar putea depăși beneficiul, așa că pentru pachetele mici (până la 16 octeți) am părăsit vechea opțiune. Pachetele mai lungi de 16 octeți sunt trimise prin DMA. Metoda de sincronizare este exact aceeași ca în secțiunea anterioară.

Redirecționarea datelor prin DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( memset(buf, 0xff, n); // Nu se utilizează DMA pentru transferuri scurte dacă (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); }


Desigur, nu există nicio cale fără întreruperi. Totul aici este la fel ca în cazul I2C

DMA se întrerupe

extern SdFatSPIDriver spiDriver; extern "C" void DMA1_Channel2_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); ) extern "C" void DMA1_Channel3_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle()._SpiDriver."); _T xCpltCallback (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) extern „C” void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); )


Hai să lansăm și să verificăm. Pentru a nu chinui unitatea flash, am decis să depanez citind un fișier mare, și nu scriind. Aici am descoperit un punct foarte interesant: viteza de citire in varianta non-DMA era de aproximativ 250-260 kb/s, in timp ce la DMA era de doar 5!!! Mai mult decat atat, consumul CPU fara folosirea DMA a fost de 3%, iar cu DMA - 75-80%!!! Acestea. rezultatul este exact opusul celui așteptat.

Offtopic aproximativ 3%

Aici am avut o eroare amuzantă cu măsurarea încărcării procesorului - uneori funcția spunea că procesorul a fost încărcat doar 3%, deși procentul ar fi trebuit să fie treierat fără oprire. De fapt, sarcina a fost de 100% și funcția mea de măsurare nu a fost apelată deloc - are cea mai mică prioritate și pur și simplu nu a fost suficient timp pentru aceasta. Prin urmare, am primit ultima valoare amintită înainte de a începe execuția. În condiții normale, funcția funcționează mai corect.


După ce am înregistrat codul de driver aproape pe fiecare linie, am descoperit o problemă: am folosit funcția de apel invers greșit. Inițial, codul meu a folosit HAL_SPI_Receive_DMA() și împreună cu acesta a fost folosit callback-ul HAL_SPI_RxCpltCallback. Acest design nu a funcționat din cauza nuanței cu trimiterea simultană a 0xff. Când am schimbat HAL_SPI_Receive_DMA() în HAL_SPI_TransmitReceive_DMA(), a trebuit să schimb și apelul înapoi în HAL_SPI_TxRxCpltCallback(). Acestea. de fapt, citirea a avut loc, dar din cauza lipsei de apeluri inverse, viteza a fost reglată printr-un timeout de 100ms.

După ce am remediat apelul înapoi, totul a căzut la loc. Sarcina procesorului a scăzut la 2,5% (acum sincer), iar viteza a sărit chiar la 500 kb/s. Adevărat, prescaler-ul a trebuit să fie setat la 4 - cu prescaler-ul la 2, afirmațiile se revărsau în biblioteca SdFat. Se pare că aceasta este limita de viteză a cardului meu.

Din păcate, acest lucru nu are nimic de-a face cu viteza de înregistrare. Viteza de scriere era încă de aproximativ 50-60 kb/s, iar sarcina procesorului a fluctuat în intervalul 60-70%. Dar după ce m-am uitat toată seara și am făcut măsurători în diferite locuri, am aflat că funcția send() a driverului meu în sine (care scrie un sector de 512 octeți) durează doar 1-2 ms, inclusiv așteptarea și sincronizarea. Uneori, totuși, apare un fel de timeout și înregistrarea durează 5-7ms. Dar problema nu este de fapt în driver, ci în logica lucrului cu sistemul de fișiere FAT.

Trecând la nivelul fișierelor, partițiilor și clusterelor, sarcina de a scrie 512 într-un fișier nu este atât de banală. Trebuie să citiți tabelul FAT, să găsiți un loc în el pentru sectorul de scris, să scrieți sectorul în sine, să actualizați intrările din tabelul FAT, să scrieți aceste sectoare pe disc, să actualizați intrările din tabelul de fișiere și directoare, și o grămadă de alte lucruri. În general, un apel la FatFile::write() poate dura până la 15-20 ms, iar o mare parte din acest timp este ocupată de munca efectivă a procesorului pentru a procesa înregistrările în sistemul de fișiere.

După cum am observat deja, încărcarea procesorului la înregistrare este de 60-70%. Dar acest număr depinde și de tipul de sistem de fișiere (Fat16 sau Fat32), de dimensiunea și, în consecință, de numărul acestor clustere de pe partiție, de viteza unității flash în sine, de cât de aglomerat și fragmentat este media, de utilizarea de nume lungi de fișiere și multe altele. Așa că vă rog să tratați aceste măsurători ca pe un fel de cifre relative.

Din nou: USB cu tampon dublu

S-a dovedit interesant cu această componentă. Implementarea originală a USB Serial de la STM32GENERIC a avut o serie de deficiențe și am decis să o rescriu pentru mine. Dar în timp ce studiam cum funcționează USB CDC, citeam codul sursă și studiam documentația, băieții de la STM32GENERIC și-au îmbunătățit semnificativ implementarea. Dar mai întâi lucrurile.

Deci, implementarea inițială nu mi s-a potrivit din următoarele motive:

  • Mesajele sunt trimise sincron. Acestea. un banal transfer octet cu octet de date de la GPS UART la USB așteaptă ca fiecare octet individual să fie trimis. Din acest motiv, sarcina procesorului poate ajunge până la 30-50%, ceea ce este, desigur, mult (viteza UART este de doar 9600)
  • Nu există sincronizare. Când tipăriți mesaje din mai multe fire, rezultatul este un taite de mesaje care se suprascriu parțial unul pe celălalt
  • Excesul de buffer-uri de primire și trimitere. Câteva buffer-uri sunt declarate în USB Middleware, dar nu sunt de fapt utilizate. În clasa SerialUSB sunt declarate încă câteva buffere, dar, din moment ce folosesc doar ieșirea, tamponul de primire doar irosește memorie.
  • În cele din urmă, sunt doar enervat de interfața clasei Print. Dacă, de exemplu, vreau să afișez șirul „viteza curentă XXX km/h”, atunci trebuie să fac până la 3 apeluri - pentru prima parte a șirului, pentru număr și pentru restul șirului. Personal, sunt mai aproape în spirit de printf clasic. În plus, fluxurile sunt de asemenea în regulă, dar trebuie să vă uitați la ce fel de cod este generat de compilator.
Deocamdată, să începem cu ceva simplu - trimiterea sincronă a mesajelor, fără sincronizare și formatare. De fapt, am copiat sincer codul din STM32GENERIC.

Implementare „direct”

extern 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 size,8_const ) ( // Ignorați trimiterea mesajului dacă USB nu este conectat if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Transmite mesajul, dar nu mai mult de timeout uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


În mod formal, acesta nu este cod sincron, deoarece nu asteapta sa fie trimise date. Dar această funcție așteaptă până când datele anterioare sunt trimise. Acestea. primul apel va trimite date către port și va ieși, dar al doilea apel va aștepta până când datele trimise în primul apel sunt efectiv trimise. În cazul unui timeout, datele se pierd. De asemenea, nu se întâmplă nimic dacă nu există deloc conexiune USB.

Desigur, aceasta este doar o pregătire, pentru că... această implementare nu rezolvă problemele identificate. De ce este nevoie pentru a face acest cod asincron și neblocant? Ei bine, cel puțin un tampon. Dar când să transferăm acest buffer?

Cred că merită să facem o scurtă excursie în principiile funcționării USB. Faptul este că transferul în protocolul USB poate fi inițiat doar de gazdă. Dacă un dispozitiv trebuie să transfere date către gazdă, datele sunt pregătite într-un buffer special PMA (Packet Memory Area) și dispozitivul așteaptă ca gazda să preia aceste date. Funcția CDC_Transmit_FS() pregătește tamponul PMA. Acest buffer locuiește în interiorul perifericului USB și nu în codul utilizatorului.

Sincer, am vrut să desenez o imagine frumoasă aici, dar nu mi-am putut da seama cum să o arăt cel mai bine.

Dar ar fi cool să implementăm următoarea schemă. Codul client scrie datele într-un buffer de stocare (utilizator) după cum este necesar. Din când în când gazda vine și ia tot ce s-a acumulat în tampon în acel moment. Acest lucru este foarte asemănător cu ceea ce am descris în paragraful anterior, dar există o avertizare esențială: datele sunt în buffer-ul utilizatorului, nu în PMA. Acestea. Aș dori să fac fără a apela CDC_Transmit_FS(), care transferă date din buffer-ul utilizatorului în PMA și, în schimb, să prind apelul înapoi „aici gazda a sosit, cerând date”.

Din păcate, această abordare nu este posibilă în designul actual al USB CDC Middleware. Mai precis, s-ar putea, dar trebuie să vă implicați în implementarea driverului CDC. Încă nu am suficient de experimentat în protocoalele USB pentru a face asta. În plus, nu sunt sigur că limitele de timp USB sunt suficiente pentru o astfel de operațiune.

Din fericire, în acel moment am observat că STM32GENERIC deja plimbase în jurul așa ceva. Iată codul pe care l-am reelaborat creativ de la ei.

USB Serial Double Buffered

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; volatil uint16_t usbTxHead = 0; volatil uint16_t usbTxTail = 0; volatile uint16_t usbTransmitting = 0; uint16_t transmitContiguousBuffer() ( uint16_t count = 0; // Transmite datele adiacente până la sfârșitul buffer-ului dacă (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = sizeof(usbTx)Tail;)_FSCTransmit (&usbTxBuffer, count); return count; void usbDebugWriteInternal(const char *buffer, size_t size, bool reverse = false) ( // Ignoră trimiterea mesajului dacă USB nu este conectat if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED); / Transmite mesajul, dar nu mai mult de timeout uint32_t timeout = HAL_GetTick() + 5 // Protejează această funcție de intrare multiplă MutexLocker locker(usbMutex // Copiază datele în buffer pentru (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; } }


Ideea din spatele acestui cod este următoarea. Deși nu a fost posibil să se prindă notificarea „gazda a sosit și vrea date”, s-a dovedit că a fost posibil să se organizeze un apel invers „Am trimis datele gazdei, o poți turna pe următoarea”. Se dovedește a fi un fel de buffer dublu - în timp ce dispozitivul așteaptă ca datele să fie trimise din bufferul intern PMA, codul utilizatorului poate adăuga octeți în memoria tampon de stocare. Când trimiterea datelor este finalizată, memoria tampon de stocare este transferată la PMA. Rămâne doar să organizezi acest apel invers. Pentru a face acest lucru, trebuie să modificați ușor funcția USBD_CDC_DataIn().

Fileed USB Middleware

static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; =>pClassData =>dpd)->D 0; USBSerialTransferComplet edCB(); ) else ( return USBD_FAIL; ) )


Apropo, funcția usbDebugWrite este protejată de un mutex și ar trebui să funcționeze corect din mai multe fire. Nu am protejat funcția USBSerialTransferCompletedCB() - este apelată dintr-o întrerupere și operează pe variabile volatile. Sincer vorbind, există un bug undeva aici, simbolurile sunt înghițite foarte ocazional. Dar pentru mine acest lucru nu este critic pentru depanare. Acest lucru nu va fi numit în codul „producție”.

Din nou: printf

Până acum acest lucru poate funcționa numai cu șiruri constante. Este timpul să înăspriți analogul printf(). Nu vreau să folosesc funcția reală printf() - implică 12 kiloocteți de cod suplimentar și un „heap” pe care nu îl am. Mi-am găsit în sfârșit loggerul de depanare, pe care l-am scris odată pentru AVR. Implementarea mea poate imprima șiruri, precum și numere în format zecimal și hexazecimal. După câteva terminari și teste, sa dovedit ceva de genul:

Implementare printf simplificată

// Implementarea sprintf durează mai mult de 10 kb și adaugă heap la proiect. Cred că acest lucru este // prea mult pentru funcționalitatea de care am nevoie // // Mai jos este o funcție de descărcare homebrew tip printf care acceptă: // - %d pentru cifre // - %x pentru numere ca HEX // - %s pentru șiruri // - %% pentru simbolul procent // // Implementarea acceptă și lățimea valorii, precum și padding zero // Imprimă numărul în buffer (în ordine inversă) // Returnează numărul de simboluri tipărite size_t PrintNum(unsigned int value) , uint8_t radix, char * buf, uint8_t width, char padSymbol) ( //TODO verifica negativ aici size_t len ​​​​= 0; // Imprimă numărul do ( char digit = valoare % radix; *(buf++) = digit< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0); //Adăugați zero 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" && cap<= "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); }


Implementarea mea este mult mai simplă decât cea din bibliotecă, dar poate face tot ce am nevoie - imprima șiruri, numere zecimale și hexazecimale cu formatare (lățimea câmpului, finisarea numărului cu zerouri în stânga). Încă nu știe cum să imprime numere negative sau numere în virgulă mobilă, dar nu este greu de adăugat. Mai târziu, pot face posibilă scrierea rezultatului într-un buffer de șir (cum ar fi sprintf) și nu doar pe USB.

Performanța acestui cod este de aproximativ 150-200 kb/s inclusiv transmisia prin USB și depinde de numărul (lungimea) mesajelor, de complexitatea șirului de format și de dimensiunea bufferului. Această viteză este suficientă pentru a trimite câteva mii de mesaje mici pe secundă. Cel mai important lucru este că apelurile nu sunt blocate.

Și mai rău: HAL de nivel scăzut

În principiu, am fi putut încheia aici, dar am observat că băieții de la STM32GENERIC au adăugat recent un nou HAL. Lucrul interesant este că multe fișiere au apărut sub numele stm32f1xx_ll_XXXX.h. Ei au dezvăluit o implementare alternativă și la nivel inferior a HAL. Acestea. un HAL obișnuit oferă o interfață de nivel destul de înalt în stilul „luați această matrice și transmiteți-mi-o folosind această interfață. Raportați finalizarea cu o întrerupere.” Dimpotrivă, fișierele cu literele LL în nume oferă o interfață de nivel inferior, cum ar fi „setați aceste steaguri pentru un astfel de registru”.

Misticismul orașului nostru

După ce am văzut noile fișiere în depozitul STM32GENERIC, am vrut să descarc kitul complet de pe site-ul ST. Dar căutarea pe google m-a condus doar la HAL (STM32 Cube F1) versiunea 1.4, care nu conține aceste fișiere noi. Configuratorul grafic STM32CubeMX a oferit și această versiune. I-am întrebat pe dezvoltatorii STM32GENERIC de unde au luat noua versiune. Spre surprinderea mea, am primit un link către aceeași pagină, doar că acum se oferea descărcarea versiunii 1.6. De asemenea, Google a început brusc să „găsească” o nouă versiune, precum și un CubeMX actualizat. Misticism și nimic mai mult!


De ce este necesar acest lucru? În cele mai multe cazuri, o interfață de nivel înalt rezolvă de fapt problema destul de bine. HAL (Hardware Abstraction Layer) este pe deplin la înălțimea numelui său - extrage codul din registrele procesorului și hardware. Dar, în unele cazuri, HAL limitează imaginația programatorului, în timp ce folosind abstracții de nivel inferior ar fi posibilă implementarea sarcinii mai eficient. În cazul meu, acestea sunt GPIO și UART.

Să încercăm noile interfețe. Să începem cu becurile. Din păcate, încă nu există suficiente exemple pe internet. Vom încerca să înțelegem comentariile de cod la funcții, din fericire totul este în ordine.

Aparent, aceste lucruri de nivel scăzut pot fi, de asemenea, împărțite în 2 părți:

  • funcții de nivel puțin mai înalt în stilul unui HAL obișnuit - aici este structura de inițializare, vă rog să inițializați periferia pentru mine.
  • Setter-uri de nivel ușor mai scăzute și getters de steaguri sau registre individuale. În cea mai mare parte, funcțiile acestui grup sunt inline și numai pentru antet
În mod implicit, primele sunt dezactivate de USE_FULL_LL_DRIVER. Ei bine, sunt cu handicap și la naiba cu ei. O vom folosi pe a doua. După puțin șamanism am primit acest driver LED

Morgulka pe LL HAL

// Clasă de încapsulat lucrul cu LED-uri integrate // // Notă: această clasă inițializează pinii corespunzători în constructor. // Este posibil să nu funcționeze corect dacă obiectele din această clasă sunt create ca variabile globale LEDDriver ( const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver() ( //activează ceasul la perifericul GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // în outputit PC(); LL_GPIO_SetPinMode(GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType(GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); ); OutputPin(GPIOC, pin) void turnOff() ( LL_GPIO_SetOutputPin(GPIOC, pin) ) void toggle() ( LL_GPIO_TogglePin(GPIOC, pin); ) ); void vLEDThread(void *pvParameters) ( LEDDriver LED; // Doar clipește o dată la 2 secunde pentru (;;) ( vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); ) )


Totul este foarte simplu! Lucrul frumos este că aici chiar lucrezi direct cu registre și steaguri. Nu există nicio suprasarcină pentru modulul HAL GPIO, care el însuși compile până la 450 de octeți, și controlul pin de la STM32GENERIC, care necesită încă 670 de octeți. Aici, în general, întreaga clasă cu toate apelurile este integrată în funcția vLEDThread, care are o dimensiune de numai 48 de octeți!

Nu am îmbunătățit controlul ceasului prin LL HAL. Dar acest lucru nu este critic, pentru că... Apelarea __HAL_RCC_GPIOC_IS_CLK_ENABLED() din HAL normal este de fapt o macrocomandă care setează doar câteva steaguri în anumite registre.

Este la fel de ușor cu butoanele

Butoane prin LL HAL

// Atribuire pini const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // Inițializați chestii legate de butoane void initButtons() ( //activează ceasul pe perifericul GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Configurați pinii butoanelor LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PIN, LLID_GPIOTPull , SEL_BUTTON_PIN, LL _GPIO_PULL_DOWN LL_GPIO_SetPinMode(); GPIOC) , OK_BUTTON_PIN, LL_GPIO_MODE_INPUT; LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); ) ( // dobouncing vTaskDelay( DEBOUNCE_DURATION ; if(LL_GPIO_IsInputPinSet(GPIOC, pin)) returnează false;


Cu UART totul va fi mai interesant. Lasă-mă să-ți amintesc de problemă. Când se folosește HAL, recepția trebuia „reîncărcată” după fiecare octet primit. Modul „luați totul” nu este furnizat în HAL. Și cu LL HAL ar trebui să reușim.

Configurarea acelui nu numai că m-a făcut să mă gândesc de două ori, dar m-a și făcut să mă uit la Manualul de referință

Configurarea pinii UART

// Inițiază pini în modul de funcționare alternativ 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); //Pinul RX


Reprelucrare inițializarea UART pentru interfețe noi

Inițializare UART

// Pregătiți pentru inițializare LL_USART_Disable(USART1); // Inițiază 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); // Vom folosi întrerupere UART pentru a obține date HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Activează întreruperea UART la recepția octetului LL_USART_EnableIT_RXNE(USART1); // În cele din urmă, activează perifericul LL_USART_Enable(USART1);


Acum întrerupere. În versiunea anterioară, aveam până la 2 funcții - una procesa întreruperea, iar a doua era un callback (din aceeași întrerupere) despre octetul primit. În noua versiune, am configurat întreruperea să primească doar un octet, așa că vom primi imediat octetul primit.

întrerupere UART

// Stocați octetul primit inline void charReceivedCB(uint8_t c) ( rxBuffer = c; lastReceivedIndex++; // Dacă a primit un simbol EOL, notificați firul GPS că linia este disponibilă pentru a fi citit dacă (c == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); ) extern "C" void USART1_IRQHandler(void) (uint8_t byte = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); )


Dimensiunea codului de driver a scăzut de la 1242 la 436 de octeți, iar consumul de RAM de la 200 la 136 (dintre care 128 sunt buffere). Nu e rau dupa parerea mea. Singura păcat este că aceasta nu este partea cea mai lacomă. Ar fi posibil să tăiem puțin altceva, dar în acest moment nu urmăresc în mod deosebit consumul de resurse - încă le am. Iar interfața HAL de nivel înalt funcționează destul de bine în cazul altor periferice.

Privind in urma

Deși la începutul acestei faze a proiectului eram sceptic cu privire la HAL, totuși am reușit să rescriu toate lucrările cu perifericele: GPIO, UART, I2C, SPI și USB. Am făcut progrese mari în înțelegerea modului în care funcționează aceste module și am încercat să transmit cunoștințele în acest articol. Dar aceasta nu este deloc o traducere a Manualului de referință. Dimpotrivă, am lucrat în contextul acestui proiect și am arătat cum puteți scrie drivere periferice în HAL pur.

Articolul s-a dovedit a fi o poveste mai mult sau mai puțin liniară. Dar, de fapt, am avut o serie de brunchuri în care am tăiat simultan în direcții exact opuse. Dimineața aș putea să întâmpin probleme cu performanța unei biblioteci Arduino și să decid ferm să rescriu totul în HAL, iar seara aș descoperi că cineva a adăugat deja suport DMA la STM32GENERIC și aș avea dorința să dau înapoi. . Sau, de exemplu, petreceți câteva zile luptând cu interfețele Arduino, încercând să înțelegeți cum este mai convenabil să transferați date prin I2C, în timp ce pe HAL acest lucru se face în 2 linii.

Per total, am realizat ceea ce mi-am dorit. Lucrarea principală cu perifericele este sub controlul meu și este scrisă în HAL. Arduino acționează doar ca adaptor pentru unele biblioteci. Adevărat, au mai rămas niște cozi. Încă trebuie să-ți aduni curajul și să eliminați STM32GENERIC din depozit, lăsând doar câteva clase cu adevărat necesare. Dar o astfel de curățare nu se va mai aplica acestui articol.

Cât despre Arudino și clonele sale. Încă îmi place acest cadru. Cu el, puteți prototipa rapid ceva fără să vă deranjați cu adevărat să citiți manuale și fișe de date. În principiu, puteți realiza chiar și dispozitive finale cu Arduino, dacă nu există cerințe speciale de viteză, consum sau memorie. În cazul meu, acești parametri sunt destul de importanți, așa că a trebuit să trec la HAL.

Am început să lucrez la stm32duino. Această clonă chiar merită atenție dacă doriți să aveți un Arduino pe STM32 și să aveți totul să funcționeze din cutie. În plus, monitorizează îndeaproape consumul de memorie RAM și flash. Dimpotrivă, STM32GENERIC în sine este mai gros și se bazează pe monstruosul HAL. Dar acest cadru se dezvoltă activ și este pe cale să fie finalizat. În general, pot recomanda ambele cadre cu o ușoară preferință pentru STM32GENERIC deoarece HAL și dezvoltare mai dinamică în acest moment. În plus, internetul este plin de exemple pentru HAL și poți oricând personaliza ceva potrivit pentru tine.

Încă îl privesc pe HAL cu un anumit grad de dezgust. Biblioteca este prea voluminoasă și urâtă. Țin cont de faptul că biblioteca este bazată pe C, ceea ce necesită utilizarea unor nume lungi de funcții și constante. Dar totuși, aceasta nu este o bibliotecă cu care să lucrezi distractiv. Mai degrabă, este o măsură necesară.

Bine, interfața - și interiorul te pune pe gânduri. Funcțiile uriașe cu funcționalitate pentru toate ocaziile implică o risipă de resurse. Mai mult, dacă puteți face față cu excesul de cod în flash utilizând optimizarea timpului de legătură, atunci consumul uriaș de RAM poate fi vindecat doar prin rescrierea lui în LL HAL.

Dar asta nici măcar nu este ceea ce deranjează, dar în unele locuri este doar nesocotirea față de resurse. Așa că am observat uriașa suprasolicitare a memoriei în codul USB Middleware (în mod oficial nu este HAL, dar este furnizat ca parte a STM32Cube). Structurile USB ocupă 2,5 kb de memorie. Mai mult, structura USBD_HandleTypeDef (544 de octeți) repetă în mare măsură PCD_HandleTypeDef din stratul inferior (1056 de octeți) - punctele finale sunt de asemenea definite în ea. Bufferele transceiver sunt, de asemenea, declarate în cel puțin două locuri - USBD_CDC_HandleTypeDef și UserRxBufferFS/UserTxBufferFS.

Descriptorii sunt în general declarați în RAM. Pentru ce? Sunt constante! Aproape 400 de octeți în RAM. Din fericire, unii dintre descriptori sunt constanți (puțin mai puțin de 300 de octeți). Descriptorii sunt informații imuabile. Și aici există un cod special care le corectează și, din nou, cu o constantă. Și chiar și unul care este deja inclus acolo. Din anumite motive, funcții precum SetBuffer nu acceptă un buffer constant, ceea ce împiedică, de asemenea, punerea în flash a descriptorilor și a altor lucruri. Care este motivul? Se va repara in 10 minute!!!

Sau, structura de inițializare face parte din mânerul obiectului (de exemplu i2c). De ce să păstrați acest lucru după ce perifericul este inițializat? De ce am nevoie de pointeri către structuri neutilizate - de exemplu, de ce am nevoie de date asociate cu DMA dacă nu le folosesc?

Și, de asemenea, cod duplicat.

caz USB_DESC_TYPE_CONFIGURATION: if(pdev->dev_speed == USBD_SPEED_HIGH) ( pbuf = (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; =) (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; =) else GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION ) break;


O conversie specială în „tip Unicode”, care ar putea fi făcută în timp de compilare. Mai mult, pentru aceasta este alocat un buffer special

Batjocorirea datelor statistice

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


Nu este fatal, dar te face să te întrebi dacă HAL este la fel de bun pe cât scriu apologeții despre asta? Ei bine, acest lucru nu este ceea ce vă așteptați de la o bibliotecă de la producător și concepută pentru profesioniști. Acestea sunt microcontrolere! Aici oamenii salvează fiecare octet și fiecare microsecundă este prețioasă. Și aici, știți, există un tampon de jumătate de kilogram și o conversie din mers a șirurilor constante. Este de remarcat faptul că majoritatea comentariilor se aplică pentru USB Middleware.

UPD: în HAL 1.6, apelul I2C DMA Transfer Completed a fost de asemenea întrerupt. Acestea. Acolo, codul care generează o confirmare când datele sunt trimise prin DMA a dispărut complet, deși este descris în documentație. Există unul pentru recepție, dar nu pentru transmisie. A trebuit să mă întorc la HAL 1.4 pentru modulul I2C, din fericire există un modul - un fișier.

În cele din urmă, voi da flash-ului și consumului de RAM al diferitelor componente. În secțiunea Drivere, am furnizat valori atât pentru driverele bazate pe HAL, cât și pentru driverele bazate pe LL HAL. În al doilea caz, secțiunile corespunzătoare din secțiunea HAL nu sunt utilizate.

Consumul de memorie

Categorie Subcategorie .text .rodata .date .bss
Sistem vector de întrerupere 272
manipulatori ISR ​​falși 178
libc 760
float matematică 4872
sin/cos 6672 536
principal & etc 86
Codul meu Codul meu 7404 833 4 578
printf 442
Fonturi 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit GFX 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
USB Middleware Miez 1740 333 2179
CDC 772
Șoferii UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Butoane LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
misc 28 24
Imprimare 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
grămadă (nu prea folosit) 1068
FreeRTOS Heap 10240

Asta e tot. Voi fi bucuros să primesc comentarii constructive, precum și recomandări dacă ceva aici poate fi îmbunătățit.

Etichete:

  • HAL
  • STM32
  • STM32cube
  • arduino
Adaugă etichete

Încă o dată vreau să scriu despre un început simplu cu STM32, doar de data aceasta fără a folosi șabloanele sau exemplele nimănui - cu o explicație a fiecărui pas. Articolele vor avea numerotarea continuă a pașilor.

1. Instalați IAR

Construirea unui proiect în IAR

1. Preprocesor

  1. sterge comentariile

2. Compilator

3. Linker

3. Creați un nou proiect în IAR

După lansarea IAR, apare o fereastră de centru de informare, de care nu avem nevoie. Faceți clic pe meniul Proiect –> Creare proiect nou. Selectați lanțul de instrumente: ARM (este puțin probabil să aveți altceva pe acea listă), Șabloane de proiect: C –> principal.

În fereastra din stânga ("Workspace"), faceți clic dreapta pentru a apela meniul și creați grup nou(Adăugați –>

Faceți clic dreapta pe CMSIS

Pentru grup Lansare

Am terminat cu CMSIS.

Pentru grup StdPeriphLib

Pentru grup Utilizator

5. Stabilirea proiectului

  1. Opțiuni generale –> Țintă –>
Selectați ST –> STM32F100 –> ST STM32F100xB. Acesta este controlerul nostru. 2. Opțiuni generale –> Configurare bibliotecă –> CMSIS: bifați caseta Utilizare CMSIS. Deci vom folosi compilatorul încorporat biblioteca CMSIS. Începând cu versiunea 6.30, IAR a început să fie livrat cu un CMSIS încorporat, iar acest lucru pare să fie mai bun - dar a introdus o oarecare confuzie cu proiecte mai vechi. 3. Compilatorul C/C++ –>
$PROJ_DIR$\

* Depanator –> Setup –> Driver: selectați ST–Link, deoarece acesta este programatorul încorporat în placa Discovery. Acum configuram programatorul in sine: * Debugger –> ST–LINK –> Interfata: selectati SWD (programatorul de pe placa este conectat la controler prin SWD, nu prin JTAG). * Depanator –>
#include „stm32f10x_conf.h” 

void main()
{
în timp ce (1)
{
}
}

<1000000; i++);


pentru(i=0; i<1000000; i++);

#include „stm32f10x_conf.h”

void main()
{





int i;
în timp ce (1)
{

pentru(i=0; i<1000000; i++);

<1000000; i++); } }

arhiva cu proiectul GPIO. Din fericire, puteți salva acest proiect și îl puteți folosi ca șablon, astfel încât să nu mai trebuiască să treceți din nou prin toate setările. Întregul ciclu: 1. Porturi I/O (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - de la zero la RTOS. 2: Timer și întreruperi") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ " STM32 - de la zero la RTOS 3: Timer outputs") [Încă o dată vreau să scriu despre un început simplu cu STM32, doar că de data aceasta fără a folosi șabloanele sau exemplele nimănui - cu o explicație a fiecărui pas. Articolele vor avea numerotarea continuă a pașilor.

0. Extragem placa STM32VLDiscovery

Îl cumpărăm în magazin, costă 600 de ruble. Va trebui să instalați drivere pe placă - cred că acest lucru nu va cauza dificultăți.

1. Instalați IAR

Vom lucra în IAR - un IDE bun cu un compilator excelent. Îi lipsește confortul de a scrie cod - dar pentru scopurile noastre este destul de suficient. Folosesc versiunea IAR 6.50.3, știți de unde să o obțineți.

2. Descărcați biblioteca de periferice

Nu sunt un fan să lucrez cu registre în timpul fazei de învățare. Prin urmare, vă sugerez să descărcați biblioteca de periferice din ST pentru a obține funcții convenabile pentru accesarea tuturor setărilor necesare.

Creați un folder „STM32_Projects”, puneți acolo folderul Biblioteci din arhiva descărcată (stsw-stm32078.zip/an3268/stm32vldiscovery_package), conține CMSIS (o bibliotecă de la ARM pentru toate microcontrolerele Cortex, descrierea și adresele tuturor resurselor) și STM_32FPeriph_StRiver10 - o bibliotecă periferică de la ST cu toate caracteristicile.

De asemenea, creăm acolo un folder „1. GPIO”, care va fi primul nostru proiect.

Arborele folderului este afișat în imagine. Fă-o astfel, pentru că mai târziu căile relative din acest arbore vor fi foarte importante.

Ei bine, pentru a înțelege despre ce vorbim, descărcați documentul de 1100 de pagini de pe aceste controlere.

Construirea unui proiect în IAR

Este necesar să înțelegem clar esența procesului de asamblare a proiectului. Pentru comoditate, îl vom împărți în etape.

1. Preprocesor

Preprocesorul parcurge toate fișierele .c ale proiectului (atât main.c, cât și toate fișierele din spațiul de lucru). Face următoarele:

  1. sterge comentariile
  2. extinde directivele #include, înlocuindu-le cu conținutul fișierului specificat. Acest proces are loc recursiv, pornind de la fișierul .c și introducând fiecare #include .h întâlnit, iar dacă directivele #include sunt întâlnite și în fișierul .h, preprocesorul le va introduce și el. Acest lucru are ca rezultat un arbore de incluziuni. Vă rugăm să rețineți: nu se ocupă de situația dublelor incluziuni, adică. același fișier .h poate fi inclus de mai multe ori dacă este #inclus în mai multe locuri în proiect. Această situație trebuie tratată cu definiții.
  3. efectuează substituții de macro-uri - extinde macro-urile
  4. colectează directivele compilatorului.

Preprocesorul generează fișiere .i, care sunt destul de convenabile atunci când se caută erori de compilare - chiar dacă numai pentru că toate macrocomenzile sunt complet dezvăluite în ele. Salvarea acestor fișiere poate fi activată în setările proiectului.

În acest moment, constructorul are toate fișierele .c din proiect pregătite pentru a fi compilate - ca fișiere .i. Nu există încă conexiuni între fișiere.

2. Compilator

După trecerea prin preprocesor, compilatorul optimizează și compilează fiecare fișier .i, creând cod binar. Aici trebuie să specificați tipul procesorului, memoria disponibilă, limbajul de programare, nivelul de optimizare și lucruri similare.

Ce face compilatorul când întâlnește un apel de funcție într-un fișier .c care nu este descris în acest fișier? O caută în titluri. Dacă anteturile spun că funcția se află într-un alt fișier .c, pur și simplu lasă un pointer către acest alt fișier în acest loc.

În acest moment, constructorul are toate fișierele .c ale proiectului asamblate în fișiere .o. Acestea se numesc module compilate. Acum există conexiuni între fișiere sub formă de pointeri în locurile în care sunt numite funcții „străine” - dar acestea sunt încă mai multe fișiere diferite.

3. Linker

Aproape totul este gata, trebuie doar să verificați toate conexiunile dintre fișiere - treceți prin main.o și înlocuiți pointerii către funcțiile altor persoane - module compilate. Dacă nu se utilizează o funcție din biblioteci, fie nu va fi compilată deloc în etapa anterioară, fie nu va fi înlocuită nicăieri de linker (în funcție de metoda de funcționare a asamblatorului). În orice caz, nu va fi inclus în codul binar terminat.

Linker-ul poate efectua, de asemenea, unele acțiuni finale asupra binarului, cum ar fi calcularea sumei de control.

Primul proiect lucrează cu porturi I/O

3. Creați un nou proiect în IAR

După lansarea IAR, apare o fereastră de centru de informare, de care nu avem nevoie. Faceți clic pe meniul Proiect –> Creare proiect nou. Selectați lanțul de instrumente: ARM (este puțin probabil să aveți altceva pe acea listă), Șabloane de proiect: C –> principal.

Acum aveți un nou proiect C gol și un fișier main.c.

4. Conectați bibliotecile la proiect

În fereastra din stânga („Spatiu de lucru”), faceți clic dreapta pe meniu și creați un nou grup (Adăugați –> Adăugați grup), să-l numim CMSIS. În același mod, vom crea grupurile StdPeriphLib, Startup și User. Acum adăugăm fișiere la grupuri (voi sublinia toate fișierele pentru a fi mai ușor de urmărit).

Faceți clic dreapta pe CMSIS, Add, Add files - mergeți la Libraries/CMSIS/CM3, din folderul DeviceSupport/ST/STM32F10x (chip support) luați system_stm32f10x.c (aceasta este o descriere a periferiei unui anumit cristal și a setărilor de ceas). În folderul CoreSupport (suport kernel) există și core_cm3.c (aceasta este o descriere a nucleului Cortex M3), dar nu o vom lua - pentru că este deja în compilator. Voi scrie mai departe despre asta.

Pentru grup Lansare adăugați fișierul startup_stm32f10x_md_vl.s din folderul Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/iar. Acestea sunt acțiunile care trebuie efectuate la pornire. Aproape în întregime, este vorba despre configurarea diverșilor handler-uri de întrerupere (handlerii înșiși vor fi puțin mai departe). Există și fișiere pentru alte cristale, dar ne interesează md_vl - asta înseamnă densitate medie (volum mediu de memorie, există și cristale cu volum mic și mare), linie de valoare (linia de evaluare - cristalul STM32F100 este destinat doar aprecierii capabilități și trecerea la următoarele familii).

Am terminat cu CMSIS.

Pentru grup StdPeriphLib adăugați fișierele stm32f10x_rcc.c și stm32f10x_gpio.c din folderul Libraries/STM32F10x_StdPeriph_Driver/src. Primul este funcțiile de lucru cu sistemul de ceas, iar al doilea este lucrul cu pinii I/O.

Pentru grup Utilizator trageți principalul nostru.c . Acest lucru nu este necesar, dar este mai frumos.

Arborele de proiect GPIO arată acum astfel:

Spațiul de lucru este gata, nu vom mai adăuga nimic la el.

Tot ce rămâne este să puneți un alt fișier în folderul de proiect care conectează anteturile la toate fișierele bibliotecii periferice. Îl poți scrie singur, dar este mai ușor să iei unul gata făcut. Mergem la stsw-stm32078.zip/an3268/stm32vldiscovery_package/Project/Examples/GPIOToggle - acolo luăm fișierul stm32f10x_conf.h (configurarea proiectului) și îl punem în folderul „1. GPIO". Acesta este singurul fișier gata făcut pe care îl luăm.

stm32f10x_conf.h este doar un dump de include ale modulelor necesare și funcții assert. Această funcție va fi apelată atunci când există erori când se lucrează cu funcțiile de bibliotecă periferice: de exemplu, introducerea unor gunoi în funcția GPIO_WriteBit în loc de GPIOC - pe scurt, ST a jucat în siguranță. În această funcție puteți rula pur și simplu o buclă infinită - while(1); Mai trebuie să intrăm în stm32f10x_conf.h - pentru a comenta rândurile pentru includerea fișierelor de periferice inutile, lăsând doar stm32f10x_rcc.h, stm32f10x_gpio.h și misc.h - ca să le putem scrie noi înșine.

5. Stabilirea proiectului

Faceți clic dreapta pe numele proiectului în fereastra Spațiului de lucru:

  1. Opțiuni generale –> Țintă –> Varianta procesor: selectați „Dispozitiv”, apăsați butonul din dreapta
Selectați ST –> STM32F100 –> ST STM32F100xB. Acesta este controlerul nostru. 2. Opțiuni generale –> Configurare bibliotecă –> CMSIS: bifați caseta Utilizare CMSIS. Deci vom folosi biblioteca CMSIS încorporată în compilator. Începând cu versiunea 6.30, IAR a început să fie livrat cu un CMSIS încorporat, iar acest lucru pare să fie mai bun - dar a introdus o oarecare confuzie cu proiecte mai vechi. 3. Compilator C/C++ –> Preprocesor. Aici scriem căile către folderele bibliotecii:
$PROJ_DIR$\
$PROJ_DIR$\..\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
$PROJ_DIR$\..\Libraries\STM32F10x_StdPeriph_Driver\inc
Macro-ul $PROJ_DIR$ înseamnă folderul curent (dosarul de proiect), iar .. - merge cu un nivel mai sus. Am specificat căile către folderul cu o descriere a cristalului, precum și către fișierele de antet ale bibliotecii periferice, deoarece toate fișierele .c din proiect includ anteturile lor, iar compilatorul trebuie să știe unde să le caute. Aici trebuie să scrieți, de asemenea, USE\_STDPERIPH\_DRIVER în simboluri definite. Aceasta va include fișierele de configurare necesare (de exemplu stm32f10x_conf.h menționate) pentru proiect. Deci fila Preprocesor va arăta astfel: * Depanator –> Setup –> Driver: selectați ST–Link, deoarece acesta este programatorul încorporat în placa Discovery. Acum configuram programatorul in sine: * Debugger –> ST–LINK –> Interfata: selectati SWD (programatorul de pe placa este conectat la controler prin SWD, nu prin JTAG). * Depanator –> Descărcare: bifați caseta Folosiți încărcător(e) flash, „Încărcați firmware în memoria flash”. Este logic, fără el nimic nu va inunda.## 6. Scrierea codului În primul rând, voi scrie ce va face acest cod. Va demonstra un lucru simplu, clipind un LED (PC8 pe placa Discovery) cu o pauză într-o buclă nesfârșită. Includem fișierul antet de configurare a proiectului, stm32f10x\_conf.h. În el găsim linia #include „stm32f10x\_exti.h” - aceasta este linia 35 și o comentăm cu două bare oblice. Cert este că proiectul nostru nu va avea nevoie de modulul EXTI. Fișierul main.c are deja o funcție int main și singura acțiune din el este return 0. Ștergem această linie (nu vom returna nicio valoare), schimbăm tipul funcției la void (din același motiv), și scrieți o buclă infinită:
#include „stm32f10x_conf.h” 

void main()
{
în timp ce (1)
{
}
}

### Lansarea modulului GPIO Porturile de intrare/ieșire din STM32 se numesc GPIO - Intrare/Ieșire de uz general. De aceea am inclus biblioteca stm32f10x_gpio.c. Totuși, asta nu este tot ce ne trebuie, puțină teorie: Toate perifericele de pe cip sunt dezactivate implicit, atât de la sursa de alimentare, cât și de la frecvența ceasului. Pentru a-l porni, trebuie să trimiteți un semnal de ceas. Acesta este gestionat de modulul RCC și există un fișier stm32f10x_rcc.c pentru a lucra cu el. Modulul GPIO se blochează pe magistrala APB2. Există, de asemenea, AHB (un analog al magistralei procesor-northbridge) și APB1 (precum și APB2 - un analog al magistralei northbridge-southbridge). Prin urmare, primul lucru pe care trebuie să-l facem este să activăm sincronizarea modulului GPIOC. Acesta este modulul responsabil pentru PORTC; există și GPIOA, GPIOB și așa mai departe. Aceasta se face astfel: RCC\_APB2PeriphClockCmd(RCC\_APB2Periph_GPIOC, ENABLE); Este simplu - numim funcția de trimitere a unui semnal de ceas de la magistrala APB2 la modulul GPIOC și, prin urmare, pornim acest modul. Desigur, facem acest lucru chiar la începutul funcției principale void. Iată doar elementele de bază pe care trebuie să le înțelegeți. Mai am și un [articol detaliat despre modulul GPIO](/index.php/stm32-%e2%86%92-%d0%bf%d0%be%d1%80%d1%82%d1%8b- gpio / „STM32 → porturi GPIO”). ### Configurarea modulului GPIOC Mai este foarte puțin, trebuie să configurați modulul GPIOC. Instalăm pinul de ieșire (există și o intrare și funcții alternative), ajustăm claritatea fronturilor (în scopul compatibilității EM) și driverul de ieșire (push-pull sau open source). Facem acest lucru imediat după inițializarea portului. GPIO\_InitTypeDef GPIO\_InitStructure; GPIO\_InitStructure.GPIO\_Speed ​​​​= GPIO\_Speed\_2MHz; GPIO\_InitStructure.GPIO\_Mode = GPIO\_Mode\_Out_PP; GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_8; GPIO\_Init(GPIOC, &GPIO\_InitStructure); Ei bine, asta este, după aceasta piciorul PC8 va funcționa ca o ieșire push-pull cu margini relativ netede (frecvența de comutare maximă 2 MHz. Marginile ascuțite sunt 50 MHz). Nu vom observa netezimea fronturilor cu ochiul, dar poate fi văzută pe un osciloscop. ### Porniți LED-ul Apelați funcția GPIO\_WriteBit(GPIOC, GPIO\_Pin\_8, Bit\_SET); LED-ul se va aprinde. ### Porniți-l și opriți-l într-o buclă În bucla while(1), scriem codul pentru pornirea, întreruperea, oprirea și întreruperea din nou:

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);  pentru(i=0; i<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
pentru(i=0; i<1000000; i++);

Astfel, întregul fișier main.c arată astfel:

#include „stm32f10x_conf.h”

void main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed ​​​​= GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOC, &GPIO_InitStructure);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);

int i;
în timp ce (1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);
pentru(i=0; i<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); pentru(i=0; i<1000000; i++); } }

## 7. Hai să lansăm! Conectam placa STM32VLDiscovery la computer prin microUSB, facem clic pe butonul Download and Debug din IAR. Programul este încărcat pe microcontroler (veți observa o fereastră cu o bară de progres care se închide rapid - dimensiunea programului este atât de mică) și începe depanarea. IAR se oprește la prima instrucțiune a codului (acest lucru este destul de convenabil la depanare), trebuie să îl porniți cu butonul Go. Totul ar trebui să funcționeze - LED-ul albastru PC8 de pe placa STM32VLDiscovery ar trebui. Ca întotdeauna, puteți descărca arhiva cu proiectul GPIO. Din fericire, puteți salva acest proiect și îl puteți utiliza ca șablon, astfel încât să nu mai trebuiască să treceți din nou prin toate setările. Întregul ciclu: 1. Porturi I/O (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - de la zero la RTOS. 2: Timer și întreruperi") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ " STM32 - de la zero la RTOS 3: ieșiri timer")

](/index.php/stm32-from_zero_to_rtos-4_exti_nvic/ „STM32 - de la zero la RTOS. 4: întreruperi externe și NVIC”) 5. Instalați FreeRTOS

Interacțiunea codului utilizatorului cu registrele nucleului și periferiei microcontrolerelor STM32 poate fi realizată în două moduri: folosind biblioteci standard sau folosind seturi de fragmente (sugestii software). Alegerea dintre ele depinde de cantitatea de memorie proprie a controlerului, de viteza necesară și de timpul de dezvoltare. Articolul analizează caracteristicile structurale, avantajele și dezavantajele seturi de fragmente pentru microcontrolere din familiile STM32F1 și STM32L0 produse de STMicroelectronics.

Unul dintre avantajele utilizării microcontrolerelor STMicroelectronics este o gamă largă de instrumente de dezvoltare: documentație, plăci de dezvoltare, software.

Software-ul pentru STM32 include software proprietar produs de STMicroelectronics, surse Open Source și software comercial.

Software-ul STMicroelectronics are avantaje importante. În primul rând, este disponibil pentru descărcare gratuită. În al doilea rând, bibliotecile software sunt prezentate sub formă de coduri sursă - utilizatorul poate modifica el însuși codul, ținând cont de restricțiile minore descrise în acordul de licență.

Bibliotecile STMicroelectronics respectă ANSI-C și pot fi împărțite după nivelul de abstractizare (Figura 1):

  • CMSIS (Core Peripheral Access Layer) – nivel de registru nucleu și periferic, bibliotecă ARM;
  • Hardware Abstraction Layer – biblioteci de nivel scăzut: biblioteci periferice standard, seturi de fragmente;
  • Middleware – biblioteci de nivel mediu: sisteme de operare în timp real (RTOS), sisteme de fișiere, USB, TCP/IP, Bluetooth, Display, ZigBee, Touch Sensing și altele;
  • Domeniu de aplicație – biblioteci la nivel de aplicație: audio, control motor, soluții auto și industriale.

Figura 1 arată că pentru a interacționa cu nivelul CMSIS, STMicroelectronics oferă utilizarea a două instrumente principale - biblioteci standard și fragmente.

Biblioteca standard este un set de drivere. Fiecare driver oferă utilizatorului funcții și definiții pentru lucrul cu un anumit bloc periferic (SPI, USART, ADC și așa mai departe). Utilizatorul nu interacționează direct cu registrele de nivel CMSIS.

Seturile de fragmente sunt exemple de programare extrem de eficiente care folosesc acces direct la registrele CMSIS. Dezvoltatorii de software pot folosi implementări ale funcțiilor din aceste exemple în propriul cod.

Fiecare metodă are avantaje și dezavantaje. Alegerea dintre ele se face ținând cont de cantitatea disponibilă de FLASH și RAM, viteza necesară, timpul de dezvoltare, experiența programatorilor și alte circumstanțe.

nivelul CMSIS

Un microcontroler este un cip digital-analogic complex format dintr-un nucleu de procesor, memorie, unități periferice, magistrale digitale și așa mai departe. Interacțiunea cu fiecare bloc are loc folosind registre.

Din punctul de vedere al programatorilor, un microcontroler reprezintă un spațiu de memorie. Conține nu numai RAM, FLASH și EEPROM, ci și registre de program. Fiecare registru hardware corespunde unei celule de memorie. Astfel, pentru a scrie date într-un registru sau a scădea valoarea acestuia, programatorul trebuie să acceseze locația corespunzătoare din spațiul de adrese.

O persoană are anumite particularități ale percepției. De exemplu, numele simbolice sunt percepute de el mult mai bine decât adresele celulelor de memorie. Acest lucru este vizibil mai ales atunci când se utilizează un număr mare de celule. În microcontrolerele ARM, numărul de registre și, prin urmare, celulele utilizate, depășește o mie. Pentru a ușura lucrurile, este necesar să definiți indicatori simbolici. Această determinare se face la nivelul CMSIS.

De exemplu, pentru a seta starea pinilor portului A, trebuie să scrieți date în registrul GPIOA_ODR. Acest lucru se poate face în două moduri - utilizați un pointer cu adresa de celulă 0xEBFF FCFF cu offset 0x14 sau utilizați un pointer cu numele simbolic GPIOA și o structură gata făcută care definește offset-ul. Evident, a doua variantă este mult mai ușor de înțeles.

CMSIS îndeplinește și alte funcții. Este implementat ca următorul grup de fișiere:

  • startup_stm32l0xx.s conține codul de pornire a asamblatorului pentru Cortex-M0+ și un tabel de vectori de întrerupere. După finalizarea inițializării de pornire, controlul este transferat mai întâi către funcția SystemInit() (explicațiile vor fi date mai jos), iar apoi către funcția principală int main(void);
  • stm32l0xx.h conține definiții necesare pentru a efectua operațiuni de bază pe biți și o definiție a tipului de microprocesor utilizat;
  • system_stm32l0xx.c/.h. După inițializarea inițială, funcția SystemInit() este executată. Efectuează configurarea inițială a perifericelor de sistem, temporizările de bloc RCC;
  • stm32l0yyxx.h – fișiere de implementare pentru microcontrolere specifice (de exemplu, stm32l051xx.h). În ele sunt definite indicatori de caractere, structuri de date, constante de biți și decalaje.

Interacțiunea cu CMSIS. Biblioteci și fragmente standard

Numărul de registre pentru microcontrolerele STM32 în majoritatea modelelor depășește o mie. Dacă utilizați acces direct la registre, codul utilizatorului va deveni ilizibil și complet inutilizabil pentru asistență și modernizare. Această problemă poate fi rezolvată prin utilizarea bibliotecii periferice standard.

Biblioteca standard de periferice este un set de drivere de nivel scăzut. Fiecare driver oferă utilizatorului un set de funcții pentru lucrul cu o unitate periferică. În acest fel, utilizatorul folosește funcțiile în loc să acceseze direct registrele. În acest caz, nivelul CMSIS este ascuns de programator (Figura 2a).

Orez. 2. Interacțiunea cu CMSIS folosind biblioteca standard (a) și fragmentele (b)

De exemplu, interacțiunea cu porturile I/O din STM32L0 este implementată folosind un driver realizat sub forma a două fișiere: stm32l0xx_hal_gpio.h și stm32l0xx_hal_gpio.c. stm32l0xx_hal_gpio.h oferă definiții de bază ale tipurilor și funcțiilor, iar stm32l0xx_hal_gpio.c oferă implementarea acestora.

Această abordare are avantaje destul de evidente (Tabelul 1):

  • Creare rapidă de cod. Programatorul nu trebuie să studieze lista de registre. Începe imediat să lucreze la un nivel superior. De exemplu, pentru a interfața direct cu portul I/O de pe STM32L0, trebuie să cunoașteți și să puteți opera unsprezece registre de control/stare, dintre care majoritatea au până la 32 de biți configurabili. Când utilizați driverul de bibliotecă, este suficient să stăpâniți opt funcții.
  • Simplitatea și claritatea codului. Codul utilizatorului nu este înfundat cu nume de registru, poate fi transparent și ușor de citit, ceea ce este important atunci când lucrați cu o echipă de dezvoltare.
  • Nivel ridicat de abstractizare. Când utilizați biblioteca standard, codul se dovedește a fi destul de independent de platformă. De exemplu, dacă schimbați microcontrolerul STM32L0 cu microcontrolerul STM32F0, o parte din codul care funcționează cu porturile I/O nu va trebui schimbat deloc.

Tabelul 1. Comparația metodelor de implementare a codului personalizat

Parametru de comparație Când utilizați standard
biblioteci periferice
Când utilizați seturi de fragmente
Dimensiunea codului in medie minim
RAM costă in medie minim
Performanţă in medie maxim
Lizibilitatea codului excelent scăzut
Nivelul de independență a platformei in medie mic de statura
Viteza de creare a programului înalt scăzut

Prezența unui shell suplimentar sub formă de drivere are, de asemenea, dezavantaje evidente (Tabelul 1):

  • Creșterea volumului codului programului. Funcțiile implementate în codul bibliotecii necesită spațiu de memorie suplimentar.
  • Costuri RAM crescute datorită creșterii numărului de variabile locale și utilizării structurilor voluminoase de date.
  • Performanță redusă datorită supraîncărcării crescute la apelarea funcțiilor bibliotecii.

Prezența acestor deficiențe a condus la faptul că utilizatorul a fost adesea forțat să optimizeze codul - să implementeze independent funcții pentru interacțiunea cu CMSIS, să optimizeze funcțiile bibliotecii prin eliminarea tuturor lucrurilor inutile, să copieze implementările funcțiilor bibliotecii direct în codul lor, utilizați directivele __INLINE pentru a crește viteza de execuție. Ca urmare, a fost alocat timp suplimentar pentru a rafina codul.

STMicroelectronics, întâlnindu-se cu dezvoltatorii la jumătatea drumului, a lansat colecții de fragmente STM32SnippetsF0 și STM32SnippetsL0.

Fragmentele sunt incluse în codul utilizatorului (Figura 2b).

Utilizarea fragmentelor oferă avantaje evidente:

  • creșterea eficienței și vitezei codului;
  • reducerea sferei de aplicare a programului;
  • reducerea cantității de RAM utilizată și a încărcării stivei.

Cu toate acestea, merită remarcat dezavantajele:

  • reducerea simplității și clarității codului datorită „contaminarii” acestuia cu nume de registre și implementării independente a funcțiilor de nivel scăzut;
  • dispariția independenței platformei.

Deci alegerea dintre biblioteca standard și fragmente nu este evidentă. În cele mai multe cazuri, merită să vorbim nu despre concurență, ci despre utilizarea lor reciprocă. În fazele inițiale, pentru a construi rapid cod „frumos”, este logic să folosiți drivere standard. Dacă optimizarea este necesară, puteți apela la fragmente gata făcute pentru a nu pierde timpul dezvoltând propriile funcții optime.

Bibliotecile standard de drivere și fragmente STM32F0 și STM32L0 (Tabelul 2) sunt disponibile pentru descărcare gratuită de pe site-ul www.st.com.

Tabelul 2. Biblioteci de nivel scăzut pentru STM32F10 și STM32L0

O cunoaștere mai apropiată a fragmentelor, ca și în cazul oricărui software, ar trebui să înceapă prin a lua în considerare caracteristicile acordului de licență.

Acord de licențiere

Orice programator responsabil studiază cu atenție acordul de licență înainte de a utiliza produse software de la terți. În ciuda faptului că colecțiile de fragmente produse de ST Microelectronics nu necesită licență și sunt disponibile pentru descărcare gratuită, aceasta nu înseamnă că nu există restricții privind utilizarea lor.

Acordul de licență este inclus cu toate produsele descărcabile gratuit fabricate de STMicroelectronics. După descărcarea STM32SnippetsF0 și STM32SnippetsL0 în directorul rădăcină este ușor de găsit documentul MCD-ST Liberty SW License Agreement V2.pdf, care introduce utilizatorul în regulile de utilizare a acestui software.

Dosarul Proiect conține subdirectoare cu exemple pentru anumite unități periferice, proiecte gata făcute pentru ARM Keil și EWARM, precum și fișiere main.c.

Lansare și caracteristici de utilizare a setului de fragmente STM32SnippetsF0 și STM32SnippetsL0

O caracteristică specială a acestor seturi de fragmente este dependența lor de platformă. Sunt proiectate să funcționeze cu plăci specifice. STM32SnippetsL0 utilizează placa Discovery STM32L053, iar STM32SnippetsF0 utilizează placa Discovery STM32F072.

Când folosiți plăci proprietare, codul și design-urile trebuie modificate, acest lucru va fi discutat mai detaliat în ultima secțiune.

Pentru a rula exemplul, trebuie să parcurgeți o serie de pași:

  • rulați proiectul terminat din directorul cu exemplul necesar. Pentru simplitate, puteți utiliza proiecte gata făcute pentru mediile ARM Keil sau EWARM, aflate în folderul MDK-ARM\ și, respectiv, EWARM\;
  • porniți placa de dezvoltare STM32L053 Discovery/STM32F072 Discovery;
  • Conectați sursa de alimentare a plăcii de depanare la computer folosind un cablu USB. Datorită depanatorului ST-Link/V2 încorporat, nu este necesar niciun programator suplimentar;
  • deschideți, configurați și rulați proiectul;
    • Pentru ARM Keil:
      • proiect deschis;
      • compilați proiectul – Proiect → Reconstruiți toate fișierele țintă;
      • încărcați-l în controler – Debug → Start/Stop Debug Session;
      • rulați programul în fereastra Debug → Run (F5).
    • Pentru EWARM:
      • proiect deschis;
      • compilați proiectul – Proiect → Rebuild all;
      • încărcați-l în controler – Proiect → Depanare;
      • rulați programul în fereastra Debug → Go (F5).
  • efectuați testarea în conformitate cu algoritmul descris în principal.c.

Pentru a analiza codul programului, luați în considerare un exemplu specific din STM32SnippetsL0: Projects\LPUART\01_WakeUpFromLPM\.

Rularea unui exemplu pentru LPUART

O caracteristică distinctivă a noilor microcontrolere din familia STM32L0 bazate pe nucleul Cortex-M0+ este capacitatea de a schimba dinamic consumul datorită unui număr mare de inovații. Una dintre aceste inovații a fost apariția perifericelor Low Power: temporizatorul LPTIM pe 16 biți și transceiver-ul LPUART. Aceste blocuri au capacități de sincronizare care sunt independente de sincronizarea magistralei periferice principale APB. Dacă este necesar să se reducă consumul de energie, frecvența de operare a magistralei APB (PCLK) poate fi redusă, iar controlerul în sine poate fi comutat în modul de consum redus. În același timp, perifericele Low Power continuă să funcționeze la performanță maximă.

Să ne uităm la un exemplu din directorul Projects\LPUART\01_WakeUpFromLPM\, care ia în considerare posibilitatea de funcționare independentă a LPUART în modul de consum redus.

La deschiderea unui proiect în mediul ARM Keil, sunt afișate doar trei fișiere: startup_stm32l053xx.s, system_stm32l0xx.c și main.c (Figura 4). Dacă ar fi folosită biblioteca standard, ar fi necesar să adăugați fișiere driver la proiect.

Funcționarea și analiza structurii fișierelor Main.c

Exemplul de program selectat este executat în mai multe etape.

După pornire, funcția SystemInit(), implementată în system_stm32l0xx.c, este lansată. Acesta configurează parametrii blocului de ceas RCC (timinguri și frecvențe de operare). Apoi, controlul este transferat la funcția principală int main(void). Inițializează perifericele utilizatorului - porturi de intrare/ieșire, LPUART - după care controlerul este comutat în modul STOP consum redus. În ea, periferia și nucleul obișnuit sunt oprite, doar LPUART funcționează. Așteaptă începerea transferului de date de pe dispozitivul extern. Când sosește bitul de pornire, LPUART trezește sistemul și primește mesajul. Recepția este însoțită de pâlpâirea LED-ului plăcii de depanare. După aceasta, controlerul este comutat înapoi în starea STOP și așteaptă următorul transfer de date dacă nu au fost detectate erori.

Transferul de date are loc folosind un port COM virtual și software suplimentar.

Să ne uităm la main.c din proiectul nostru. Acest fișier este un fișier C standard. Caracteristica sa principală este auto-documentarea - prezența unor comentarii detaliate, explicații și recomandări. Partea explicativă conține mai multe secțiuni:

  • un titlu care indică numele fișierului, versiunea, data, autorul și o scurtă explicație a scopului;
  • descrierea secvenței de configurare a perifericelor de sistem (caracteristici specifice RCC): FLASH, RAM, sisteme de alimentare și de tac, magistrale periferice și așa mai departe;
  • lista resurselor microcontrolerului utilizate (MCU Resources);
  • o scurtă explicație a modului de utilizare a acestui exemplu;
  • o scurtă explicație a testării exemplului și a algoritmului pentru implementarea acestuia (Cum se testează acest exemplu).

Funcția int main(void) are o formă compactă și este echipată cu comentarii, care în Lista 1, pentru o mai mare claritate, sunt traduse în rusă.

Listarea 1. Exemplu de implementare a funcției principale

int main(void)
{
/* Până la începutul execuției acestei părți, când unitățile de sistem au fost deja configurate în funcția SystemInit(), implementată în system_stm32l0xx.c. */
/* configurația unităților periferice */
Configure_GPIO_LED();
Configure_GPIO_LPUART();
Configurare_LPUART();
Configurare_LPM_Stop();
/* verifica erorile in timpul receptiei */
while (!eroare) /* buclă fără sfârșit */
{
/* așteptați ca LPUART să fie gata și treceți în modul STOP */
if((LPUART1->ISR & USART_ISR_REACK) == USART_ISR_REACK)
{
__WFI();
}
}
/* când apare o eroare */
SysTick_Config(2000); /* setarea perioadei de întrerupere a temporizatorului sistemului la 1 ms */
în timp ce(1);
}

Fișierul main.c declară și definește funcțiile de configurare periferică și două funcții de gestionare a întreruperilor. Să luăm în considerare caracteristicile lor.

Exemplul de mai jos folosește patru funcții de configurare (Listing 2). Toate nu au argumente și nu returnează valori. Scopul lor principal este de a inițializa rapid și cu cea mai mică cantitate de cod necesară pentru a inițializa perifericele. Acest lucru se realizează prin două caracteristici: utilizarea accesului direct la registre și utilizarea directivei __INLINE (Listing 3).

Lista 2. Declararea funcțiilor de configurare periferice

void Configure_GPIO_LED(void);
void Configure_GPIO_LPUART(void);
void Configure_LPUART(void);
void Configure_LPM_Stop(void);

Lista 3. Exemplu de implementare a funcției __INLINE cu acces direct la registrele LPUART

INLINE void Configure_LPUART(void)
{
/* (1) Activați ceasul interfeței de alimentare */
/* (2) Dezactivează registrul de protecție de rezervă pentru a permite accesul la domeniul de ceas RTC */
/* (3) LSE pe */
/* (4) Așteptați LSE gata */
/* (5) Activați registrul de protecție de rezervă pentru a permite accesul la domeniul de ceas RTC */
/* (6) LSE mapat pe LPUART */
/* (7) Activați ceasul periferic LPUART */
/* Configurați LPUART */
/* (8) supraeșantionare cu 16, 9600 baud */
/* (9) 8 biți de date, 1 bit de pornire, 1 bit de oprire, fără paritate, modul de recepție, modul de oprire */
/* (10) Setați prioritatea pentru LPUART1_IRQn */
/* (11) Activați LPUART1_IRQn */
RCC->APB1ENR |= (RCC_APB1ENR_PWREN); /* (1) */
PWR->CR |= PWR_CR_DBP; /* (2) */
RCC->CSR |= RCC_CSR_LSEON; /* (3) */
în timp ce ((RCC->CSR & (RCC_CSR_LSERDY)) != (RCC_CSR_LSERDY)) /*(4)*/
{
/* adăugați timp aici pentru o aplicație robustă */
}
PWR->CR &=~ PWR_CR_DBP; /* (5) */
RCC->CCIPR |= RCC_CCIPR_LPUART1SEL; /* (6) */
RCC->APB1ENR |= RCC_APB1ENR_LPUART1EN; /*(7) */
LPUART1->BRR = 0x369; /* (8) */
LPUART1->CR1 = USART_CR1_UESM | USART_CR1_RXNEIE | USART_CR1_RE | USART_CR1_UE; /* (9) */
NVIC_SetPriority(LPUART1_IRQn, 0); /* (10) */
NVIC_EnableIRQ(LPUART1_IRQn); /* (unsprezece) */
}

Operatorii de întrerupere de la temporizatorul de sistem și de la LPUART folosesc, de asemenea, acces direct la registre.

Astfel, comunicarea cu CMSIS se realizează fără o bibliotecă standard. Codul se dovedește a fi compact și foarte eficient. Cu toate acestea, lizibilitatea sa se va deteriora semnificativ din cauza abundenței acceselor la registre.

Folosind fragmente în propriile dezvoltări

Seturile de fragmente propuse au limitări: este necesară utilizarea plăcii Discovery STM32L053 pentru STM32SnippetsL0 și placa Discovery STM32F072 pentru STM32SnippetsF0.

Pentru a utiliza fragmente în dezvoltările dvs., va trebui să faceți o serie de modificări. Mai întâi, trebuie să reconfigurați proiectul pentru procesorul dorit. Pentru a face acest lucru, trebuie să modificați fișierul de pornire startup_stm32l053xx.s în fișierul altui controler și să definiți constanta necesară: STM32L051xx, STM32L052xx, STM32L053xx, STM32L062xx, STM32L063xx, STM32L063xx, STM32L061F03, STM32F03, STM32F03 1 și altele. După aceasta, la compilarea stm32l0xx.h, fișierul necesar cu definiția perifericelor controlerului stm32l0yyxx.h (stm32l051xx.h/stm32l052xx.h/stm32l053xx.h/stm32l061xx.h/stm32l061xx.h/stm32l061xx.h/stm32l0610/stm32xxl0/stm32xx.h) va fi inclus automat. În al doilea rând, trebuie să selectați programatorul corespunzător în setările proprietăților proiectului. În al treilea rând, modificați codul funcțiilor din exemple dacă acestea nu îndeplinesc cerințele aplicației utilizator.

Concluzie

Seturile de fragmente și bibliotecile periferice standard produse de ST Microelectronics nu se exclud reciproc. Se completează reciproc, adăugând flexibilitate la crearea aplicațiilor.

Biblioteca standard face posibil acest lucru creație rapidă cod clar cu un nivel ridicat de abstractizare.

Fragmentele vă permit să îmbunătățiți eficiența codului - creșteți performanța și reduceți cantitatea de memorie FLASH și RAM ocupată.

Literatură

  1. Rezumat de date. STM32SnippetsF0. Pachetul de firmware STM32F0xx Snippets. Rev. 1. – ST Microelectronics, 2014.
  2. Rezumat de date. STM32SnippetsL0. Pachetul de firmware STM32F0xx Snippets. Rev. 1. – ST Microelectronics, 2014.
  3. Acord de licență MCD-ST Liberty SW V2.pdfRelee electromecanice. Informații tehnice. – ST Microelectronics, 2011.
  4. Rezumat de date. 32L0538DISCOVERY Kit de descoperire pentru microcontrolere STM32L053. Rev. 1. – ST Microelectronics, 2014.
  5. http://www.st.com/.
Despre ST Microelectronics

Așadar, deja ne-am pus pe picioare, în sensul că avem tot ce ne trebuie conectat la pinii microcontrolerului de pe placa Discovery STM32VL, am învățat să vorbim în limbajul de programare C, este timpul să creăm un proiect pentru clasa I.

Scrierea unui program

După ce ați terminat de creat și configurat proiectul, puteți începe să scrieți programul propriu-zis. După cum este obișnuit pentru toți programatorii, primul program scris pentru a funcționa pe un computer este un program care afișează inscripția „HelloWorld” pe ecran, iar pentru toate microcontrolerele, primul program pentru un microcontroler produce LED-uri intermitente. Nu vom fi o excepție de la această tradiție și vom scrie un program care va controla LED-ul LD3 de pe placa STM32VL Discovery.

După crearea unui proiect gol în IAR, acesta produce cod de program minim:

Acum programul nostru se va „învârti” întotdeauna într-o buclă in timp ce.

Pentru a putea controla LED-ul, trebuie să activăm tactarea portului la care este conectat și să configuram ieșirea corespunzătoare a portului microcontrolerului. După cum am discutat mai devreme în prima parte, pentru a permite sincronizarea porturilor CU biți răspunsuri IOPCEN Inregistreaza-te RCC_APB2ENR. Potrivit documentului " RM0041Referinţămanual.pdf» pentru a activa sincronizarea magistralei portului CU cerute în registru RCC_APB2ENR set bit IOPCEN pe unitate. Pentru a ne asigura că atunci când acest bit este setat, nu resetam alții setate în acest registru, trebuie să aplicăm o operație logică de adăugare („SAU”) logică la starea curentă a registrului și apoi să scriem valoarea rezultată în conținutul Registrul. În conformitate cu structura bibliotecii ST, accesul la o valoare de registru pentru citire și scriere se face printr-un pointer către structură RCC-> APB2 ENR. Astfel, amintind materialul din partea a doua, putem scrie următorul cod care setează bitul IOPCENîn registru RCC_APB2ENR:

După cum puteți vedea din fișierul „stm32f10x.h”, valoarea biților IOPCEN definit ca 0x00000010, care corespunde celui de-al patrulea bit ( IOPCEN) Inregistreaza-te APB2ENRși se potrivește cu valoarea indicată în fișa de date.

Acum să configuram ieșirea în același mod 9 port CU. Pentru a face acest lucru, trebuie să configuram acest pin de port pentru a ieși în modul push-pull. Registrul este responsabil pentru setarea modului de intrare/ieșire a portului GPIOC_CRH, ne-am uitat deja la el, descrierea sa se află și în secțiunea „7.2.2 Port configuration register high” din fișa de date. Pentru a seta ieșirea în modul de ieșire performanță maximă 2MHz, necesar în registru GPIOC_CRH instalare MODE9 la unul și resetați bitul MODE9 la zero. Biții sunt responsabili pentru setarea modului de funcționare de ieșire ca funcție principală cu ieșire push-pull CNF9 Și CNF9 , pentru a configura modul de operare de care avem nevoie, ambii acești biți trebuie resetati la zero.

Acum pinul portului la care este conectat LED-ul este setat la ieșire, pentru a controla LED-ul trebuie să schimbăm starea pinului portului setând ieșirea la una logică. Există două moduri de a schimba starea pinului portului, prima este să scrieți direct în registrul de stare a portului conținutul modificat al registrului de porturi, la fel cum am configurat portul. Această metodă nu este recomandată din cauza posibilității unei situații în care o valoare incorectă poate fi scrisă în registrul portului. Această situație poate apărea dacă, în timpul unei modificări a stării registrului, din momentul în care starea registrului a fost deja citită și până în momentul în care starea modificată este scrisă în registru, un dispozitiv periferic sau întreruperea schimbă starea acestui port. . La finalizarea operațiunii de modificare a stării registrului, valoarea va fi scrisă în registru fără a ține cont de modificările intervenite. Deși probabilitatea apariției acestei situații este foarte scăzută, merită totuși să folosiți o altă metodă în care situația descrisă este exclusă. În acest scop, în microcontroler există două registre GPIOx_BSRRȘi GPIOx_BRR. Când scrieți unul logic la bitul de registru necesar GPIOx_BRR pinul portului corespunzător va fi resetat la zero logic. Inregistreaza-te GPIOx_BSRR poate efectua atât setarea, cât și resetarea stării pinii portului pentru a seta pinul portului la o unitate logică, este necesar să setați biții; BSN, corespunzător numărului de bit necesar, acești biți sunt localizați în registrele inferioare ale octetului. Pentru a reseta starea de ieșire a portului la zero logic, trebuie să scrieți biții BRn pinii corespunzători, acești biți sunt localizați în cei mai semnificativi biți ai registrului portului.

LED-ul LD3 este conectat la pin 9 port CU. Pentru a porni acest LED, trebuie să aplicăm unul logic pinului portului corespunzător pentru a „aprinde” LED-ul.

Să adăugăm codul pentru configurarea ieșirii portului LED în programul nostru și, de asemenea, să adăugăm o funcție de întârziere software pentru a reduce frecvența de comutare a LED-ului:

//Nu uitați să includeți fișierul antet care descrie registrele microcontrolerului

#include „stm32f10x.h”

golÎntârziere ( gol);

golÎntârziere ( gol)
{
nesemnat lung eu;
pentru(i=0; i<2000000; i++);
}

//Funcția noastră principală

gol principal( gol)
{


RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

//Ștergeți biții MODE9 (resetați biții MODE9_1 și MODE9_0 la zero)
GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Setați bitul MODE9_1 pentru a configura ieșirea la ieșire cu o viteză de 2MHz
GPIOC->CRH |= GPIO_CRH_MODE9_1;

//Ștergeți biții CNF (setați ca o ieșire de uz general, simetric (push-pull))
GPIOC->CRH &= ~GPIO_CRH_CNF9;

in timp ce(1)
{

//Setarea pinului 9 al portului C la unul logic („LED-ul aprins”)
GPIOC->BSRR = GPIO_BSRR_BS9;


Întârziere();


GPIOC->BSRR = GPIO_BSRR_BR9;


Întârziere();

}
}

Puteți descărca arhiva cu codul sursă al programului scris folosind controlul direct al registrelor microcontrolerului din link.

Primul nostru program funcțional a fost scris când îl scriem, pentru a opera și configura perifericele, am folosit date din fișa de date oficială”; RM0041Referinţămanual.pdf», această sursă informațiile despre registrele microcontrolerului sunt cele mai precise, dar pentru a le utiliza trebuie să recitiți o mulțime de informații, ceea ce complică scrierea programelor. Pentru a facilita procesul de configurare a perifericelor microcontrolerului, există diverse generatoare de cod, utilitate oficială de la firma ST este prezentat programul Microxplorer, dar este încă de puțină funcționalitate și din acest motiv dezvoltatori terți a fost creat un program alternativ „STM32 Program Code Generator”. » . Acest program vă permite să obțineți cu ușurință codul de configurare periferic folosind o interfață grafică convenabilă, intuitivă (vezi Fig. 2).


Orez. 2 Captură de ecran a programului generator de cod STM32

După cum se poate observa din Figura 2, codul de configurare a ieșirii LED generat de program coincide cu codul pe care l-am scris mai devreme.

Pentru a rula programul scris, după compilarea codului sursă, trebuie să încărcăm programul în microcontroler și să vedem cum rulează.

Video cu LED-ul care clipește modul de depanare a programului

Video cu programul LED care clipește pe placa Discovery STM32VL

Funcții de bibliotecă pentru lucrul cu periferice

Pentru a simplifica munca de configurare a registrelor periferice ale microcontrolerului, compania ST a dezvoltat biblioteci, datorită cărora nu este nevoie să citiți atât de amănunțit fișa de date, deoarece atunci când utilizați aceste biblioteci, munca de scriere a unui program va deveni mai aproape de scrierea programelor de nivel înalt, având în vedere faptul că toate funcțiile de nivel inferior sunt implementate la nivel de funcție de bibliotecă. Cu toate acestea, nu ar trebui să renunțăm complet la utilizarea lucrului direct cu registrele microcontrolerului, deoarece funcțiile de bibliotecă necesită mai mult timp de procesor pentru execuția lor și, ca urmare, utilizarea lor în secțiunile de timp critice ale programului nu este justificată. Dar totuși, în cele mai multe cazuri, lucruri precum inițializarea perifericelor nu sunt critice pentru timpul de execuție, iar comoditatea utilizării funcțiilor de bibliotecă este mai de preferat.

Acum să scriem programul nostru folosind biblioteca ST. Programul necesită configurarea porturilor de intrare/ieșire pentru a utiliza funcțiile de bibliotecă pentru configurarea porturilor, trebuie să conectați fișierul antet „; stm32f10x_gpio.h„(vezi tabelul. 1). Acest fișier poate fi conectat prin decomentarea liniei corespunzătoare din antetul conectat Fișier de configurare « stm32f10x_conf.h" La sfârșitul fișierului " stm32f10x_gpio.h» există o listă de declarații de funcții pentru lucrul cu porturi. O descriere detaliată a tuturor funcțiilor disponibile poate fi găsită în fișierul „ stm32f10x_stdperiph_lib_um.chm", o scurtă descriere a celor mai frecvent utilizate este dată în Tabelul 2.

Tabelul 2. Descrierea funcțiilor principale de configurare a porturilor

Funcţie

Descrierea funcției, parametrii trecuți și returnați

GPIO_DeInit(
GPIO_TypeDef* GPIOx)

Setează registrele de configurare a portului GPIOx la valorile implicite.

GPIO_Init(
GPIO_TypeDef* GPIOx,

Setează registrele de configurare a portului GPIOx în conformitate cu parametrii specificați în structura GPIO_InitStruct

GPIO_StructInit(
GPIO_InitTypeDef* GPIO_InitStruct)

Completează toate câmpurile structurii GPIO_InitStruct cu valori implicite

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Citirea valorii de intrare a pinului GPIO_Pin al portului GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Citirea valorilor de intrare ale tuturor pinilor portului GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Setarea valorii de ieșire a pinului GPIO_Pin al portului GPIOx la unul logic

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Resetarea valorii de ieșire a pinului GPIO_Pin al portului GPIOx la zero logic

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Scrieți valoarea BitVal în pinul GPIO_Pin al portului GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Scrieți valoarea PortVal pe portul GPIOx

După cum se poate vedea din descrierea funcțiilor, ca parametri pentru setările portului etc., nu sunt trecuți funcției diferiți parametri individuali, ci o singură structură. Structurile sunt date combinate care au o anumită relație logică. Spre deosebire de matrice, structurile pot conține date tipuri diferite. Cu alte cuvinte, o structură reprezintă un set de variabile diferite cu tipuri variate, combinate într-o variabilă unică. Variabilele situate în această structură se numesc câmpuri ale structurii și sunt accesate în felul următor: mai întâi scrieți numele structurii, apoi scrieți un punct și numele câmpului structurii (numele variabilei din această structură).

Lista variabilelor incluse în structurile pentru funcțiile care lucrează cu porturi este descrisă în același fișier puțin deasupra descrierii funcțiilor. Deci, de exemplu, structura " GPIO_InitTypeDef„are următoarea structură:

typedef struct
{

uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
Acest parametru poate fi orice valoare a lui @ref GPIO_pins_define */

GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
Acest parametru poate fi o valoare a @ref GPIOSpeed_TypeDef */

GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
Acest parametru poate fi o valoare a @ref GPIOMode_TypeDef */

)GPIO_InitTypeDef;

Primul câmp al acestei structuri conține variabila " GPIO_ Pin" tip nesemnat mic de statura, este necesar să scrieți în această variabilă steagurile numerelor pinilor corespunzători pentru care se intenționează să se efectueze setările necesare. Puteți configura mai mulți pini deodată specificând mai multe constante ca parametru folosind operatorul biți SAU(cm. ). Un SAU pe biți le va „colecta” pe toate din constantele enumerate, iar constantele în sine sunt o mască doar destinată unei astfel de utilizări. Definițiile macro ale constantelor sunt listate în același fișier de mai jos.

Al doilea câmp al structurii " GPIO_InitTypeDef» setează viteza maximă posibilă a ieșirii portului. Lista de valori posibile a acestui domeniu listat mai sus:

Descrierea valorilor posibile:

  • GPIO_Mode_AIN- intrare analogică (în engleză: Analog INput);
  • GPIO_Mode_IN_FLOATING- intrare fără strângere, atârnând (intrare engleză float) în aer
  • GPIO_Mode_IPD- pull-down de intrare
  • GPIO_Mode_IPU- tragere de intrare
  • GPIO_Mode_Out_OD- Ieșire Open Drain
  • GPIO_Mode_Out_PP- ieșire în două stări (în engleză: Output Push-Pull - înainte și înapoi)
  • GPIO_Mode_AF_OD- deschidere evacuare pt funcții alternative(Engleză: Funcție alternativă). Folosit în cazurile în care pinul trebuie controlat de un periferic atașat la un anumit pin de port (de exemplu, pinul Tx al USART1 etc.)
  • GPIO_Mode_AF_PP- același lucru, dar cu două stări

Într-un mod similar, puteți vizualiza structura variabilelor altor structuri necesare pentru a lucra cu funcțiile de bibliotecă.

Pentru a lucra cu structuri, acestea, ca și variabilele, trebuie să fie declarate și atribuite acestora nume unic, după care puteți accesa câmpurile structurii declarate după numele atribuit acesteia.

//Declară structura

/*
Înainte de a începe să completați câmpurile structurii, este recomandat să inițializați conținutul structurii cu date implicite, pentru a preveni scrierea datelor incorecte dacă, din anumite motive, nu au fost completate toate câmpurile structurii; .

Pentru a transmite valorile unei structuri unei funcții, trebuie să precedați numele structurii cu simbolul &. Acest simbol îi spune compilatorului că este necesar să se transmită funcției nu valorile însele conținute în structură, ci adresa din memorie în care se află aceste valori. Acest lucru se face pentru a reduce numărul acțiunile necesare procesor pentru copierea conținutului structurii și, de asemenea, vă permite să salvați RAM. Astfel, în loc să treci mai mulți octeți conținuti în structură către funcție, se va trece doar unul care să conțină adresa structurii.
*/

/* Scrieți în câmpul GPIO_Pin al structurii GPIO_Init_struct numărul de pin al portului pe care îl vom configura în continuare */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Completați câmpul GPIO_Speed ​​​​în același mod */

/*
După ce am completat câmpurile necesare ale structurii, această structură trebuie trecută la o funcție care va face intrarea necesară în registrele corespunzătoare. Pe lângă structura cu setările pentru această funcție, este necesar să se treacă și numele portului pentru care sunt destinate setările.
*/

Aproape toate perifericele sunt configurate aproximativ în același mod, singurele diferențe sunt în parametrii și comenzile specifice fiecărui dispozitiv.

Acum să scriem programul nostru de LED intermitent folosind doar funcții de bibliotecă.

//Nu uitați să includeți fișierul antet cu o descriere a registrelor microcontrolerului

#include „stm32f10x.h”
#include „stm32f10x_conf.h”

//declară funcția de întârziere software

golÎntârziere ( gol);

//funcția de întârziere software în sine

golÎntârziere ( gol)
{
nesemnat lung eu;
pentru(i=0; i<2000000; i++);
}

//Funcția noastră principală

gol principal( gol)
{

//Permite sincronizarea magistralei portului C
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Declară o structură pentru configurarea portului
GPIO_InitTypeDef GPIO_Init_struct;

//Umpleți structura cu valori inițiale
GPIO_StructInit(&GPIO_Init_struct);

/* Scrieți în câmpul GPIO_Pin al structurii GPIO_Init_struct numărul de pin al portului pe care îl vom configura în continuare */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Completați câmpurile GPIO_Speed ​​​​și GPIO_Mode în același mod
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Treceți structura umplută pentru a efectua acțiuni de configurare a registrelor
GPIO_Init(GPIOC, &GPIO_Init_struct);

//Bucla noastră principală fără sfârșit
in timp ce(1)
{
//Setarea pinului 9 al portului C la cel logic (LED aprins)
GPIO_SetBits(GPIOC, GPIO_Pin_9);

//Adăugați o întârziere software, astfel încât LED-ul să lumineze pentru un timp
Întârziere();

//Resetați starea pinului 9 al portului C la zero logic
GPIO_ResetBits(GPIOC, GPIO_Pin_9);

//Adăugați din nou o întârziere software
Întârziere();
}
}

legătură.

Din exemplul de mai sus, este clar că utilizarea funcțiilor de bibliotecă pentru lucrul cu periferice vă permite să apropiați programele de scriere pentru un microcontroler de programarea orientată pe obiecte și, de asemenea, reduce nevoia de acces frecvent la fișa de date pentru a citi descrierile microcontrolerului. registre, dar utilizarea funcțiilor de bibliotecă necesită cunoștințe mai mari ale limbajului de programare. Având în vedere acest lucru, pentru persoanele care nu sunt deosebit de familiarizate cu programarea, o opțiune mai simplă de scriere a programelor va fi o modalitate de a scrie programe fără a utiliza funcții de bibliotecă, cu acces direct la registrele microcontrolerului. Pentru cei care cunosc bine limbajul de programare, dar sunt slab versați în microcontrolere, în special STM32, utilizarea funcțiilor de bibliotecă simplifică semnificativ procesul de scriere a programelor.

Această împrejurare, precum și faptul că ST s-a îngrijit de un grad ridicat de compatibilitate, atât în ​​hardware cât și în software, a diferitelor sale microcontrolere, facilitează studierea acestora, deoarece nu este nevoie să se aprofundeze caracteristicile structurale ale diverse controlere din seria STM32 și vă permite să selectați oricare dintre microcontrolerele disponibile în linia STM32 ca microcontroler pentru studiu.

Manager de întreruperi

Microcontrolerele au o capacitate remarcabilă - de a opri execuția programului principal pentru un anumit eveniment și de a trece la execuția unei subrutine speciale - handler de întrerupere. Sursele de întrerupere pot fi fie evenimente externe - întreruperi pentru primirea/transmiterea datelor prin orice interfață de transfer de date, fie o modificare a stării de ieșire, fie cele interne - overflow timer, etc. O listă cu posibile surse de întrerupere pentru microcontrolerele din seria STM32 este dată în fișa de date. " RM0041 Manual de referință„În capitolul” 8 Întreruperi și evenimente».

Deoarece handler-ul de întrerupere este și o funcție, va fi scrisă ca o funcție obișnuită, dar pentru ca compilatorul să știe că această funcție este un handler de întrerupere specific, ca nume de funcție ar trebui selectate nume predefinite, către care redirecționări vector de întrerupere. sunt specificate. O listă cu numele acestor funcții cu o scurtă descriere se află în fișierul de asamblare " startup_stm32f10x_md_vl.s" Un handler de întreruperi poate avea mai multe surse care cauzează întreruperi, de exemplu funcția de gestionare a întreruperilor " USART1_IRQHandler„poate fi apelat atunci când sfârșitul recepției și sfârșitul transmiterii unui octet etc.

Pentru a începe să lucrați cu întreruperi, trebuie să configurați și să inițializați controlerul de întrerupere NVIC. În arhitectura Cortex M3, fiecărei întreruperi i se poate atribui propriul grup de prioritate pentru cazurile în care apar mai multe întreruperi simultan. Apoi trebuie să configurați sursa de întrerupere.

Câmpul NVIC_IRQChannel indică ce întrerupere dorim să configuram. Constanta USART1_IRQn denotă canalul responsabil pentru întreruperile asociate cu USART1. Este definit în fișierul " stm32f10x.h„, acolo sunt definite și alte constante similare.

Următoarele două câmpuri indică prioritatea întreruperii (valorile maxime pentru acești doi parametri sunt determinate de grupul de prioritate selectat). Ultimul câmp permite de fapt utilizarea unei întreruperi.

A functiona NVIC_Init, la fel ca la configurarea porturilor, se trece un pointer către o structură pentru a aplica setările efectuate și a le scrie în registrele corespunzătoare ale microcontrolerului.

Acum, în setările modulului, trebuie să setați parametrii prin care acest modul va genera o întrerupere. Mai întâi trebuie să activați întreruperea, aceasta se face apelând funcția Nume_ITConfig(), care se află în fișierul antet al dispozitivului periferic.

//Activați întreruperile la finalizarea transferului de octeți prin USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Activați întreruperile când se primește un octet prin USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

O descriere a parametrilor trecuți funcției poate fi găsită în fișierul de cod sursă al dispozitivului periferic, chiar deasupra locației funcției în sine. Această funcție activează sau dezactivează întreruperile pentru diverse evenimente de la modulul periferic specificat. Când această funcție este executată, microcontrolerul va putea genera întreruperi pentru evenimentele de care avem nevoie.

După ce intrăm în funcția de gestionare a întreruperilor, trebuie să verificăm de la ce eveniment a avut loc întreruperea și apoi să resetam steag-ul, altfel, la ieșirea din întrerupere, microcontrolerul va decide că nu am procesat întrerupere, deoarece steag-ul de întrerupere este încă setat.

Pentru a efectua diverse, mici, acțiuni repetate cu o perioadă precisă, microcontrolerele cu nucleu Cortex-M3 au un cronometru de sistem special conceput pentru aceasta. Funcțiile acestui cronometru includ doar apelarea unei întreruperi la intervale de timp strict specificate. De obicei, întreruperea apelată de acest temporizator conține cod pentru a măsura durata diferitelor procese. Declarația funcției de setare a temporizatorului se află în fișierul " miez_ cm3. h" Argumentul transmis funcției specifică numărul de cicluri de ceas al magistralei de sistem între intervalele de apelare a gestionarului de întrerupere a temporizatorului de sistem.

SysTick_Config(clk);

Acum că ne-am ocupat de întreruperi, să ne rescriem programul folosind temporizatorul de sistem ca element de sincronizare. Din momentul cronometrului " SysTick” este unul de sistem și poate fi folosit de diferite blocuri funcționale ale programului nostru, atunci ar fi rezonabil să mutați funcția de gestionare a întreruperilor temporizatorului de sistem într-un fișier separat, iar din această funcție apelăm funcții pentru fiecare bloc funcțional separat.

Un exemplu de fișier „main.c” pentru un program care clipește LED folosind o întrerupere:

//Conectează fișierul antet cu o descriere a registrelor microcontrolerului

#include „stm32f10x.h”
#include „stm32f10x_conf.h”
#include „principal.h”

nesemnat int LED_timer;

//Funcție apelată din funcția de gestionare a întreruperii temporizatorului de sistem

gol SysTick_Timer_main( gol)
{
//Dacă variabila LED_timer nu a ajuns încă la 0,
dacă(LED_timer)
{
//Verificați valoarea acestuia, dacă este mai mare de 1500, aprindeți LED-ul
dacă(LED_timer>1500) GPIOC->BSRR= GPIO_BSRR_BS9;

// în caz contrar, dacă este mai mic sau egal cu 1500, atunci dezactivați-l
altfel GPIOC->BSRR= GPIO_BSRR_BR9;

//Reduceți variabila LED_timer
LED_timer--;
}

//Dacă valoarea variabilei ajunge la zero, setați o nouă valoare de 2000
altfel LED_timer=2000;
}

//Funcția noastră principală

gol principal( gol)
{

//Permite sincronizarea magistralei portului C
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Declară o structură pentru configurarea portului
GPIO_InitTypeDef GPIO_Init_struct;

//Umpleți structura cu valori inițiale
GPIO_StructInit(&GPIO_Init_struct);

/* Scrieți în câmpul GPIO_Pin al structurii GPIO_Init_struct numărul de pin al portului pe care îl vom configura în continuare */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Completați câmpurile GPIO_Speed ​​​​și GPIO_Mode în același mod
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Treceți structura umplută pentru a efectua acțiuni de configurare a registrelor
GPIO_Init(GPIOC, &GPIO_Init_struct);

//selectați un grup prioritar pentru întreruperi
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

//Configurați temporizatorul de sistem cu un interval de 1 ms
SysTick_Config(24000000/1000);

//Bucla noastră principală fără sfârșit
in timp ce(1)
{
//De data aceasta este gol, toate comenzile LED au loc în întreruperi
}
}

O parte a codului sursă din fișierul „stm32f10x_it.c”:


#include „principal.h”

/**
* @brief Această funcție se ocupă de SysTick Handler.
* @param Nici unul
* @retval Nici unul
*/

gol SysTick_Handler( gol)
{
SysTick_Timer_main();
}

Un exemplu de schiță de lucru a unui program pentru clipirea unui LED folosind o întrerupere poate fi descărcat de pe link.

Aceasta încheie povestea mea despre elementele de bază ale dezvoltării programelor pentru microcontrolerul STM32. Am furnizat toate informațiile necesare pentru a putea studia în continuare în mod independent microcontrolerele STM32. Materialul furnizat este doar un început, deoarece o descriere completă a lucrului cu microcontrolere nu poate fi descrisă în cadrul niciunui articol. În plus, studiul microcontrolerelor fără a dobândi experiență practică este imposibil, iar experiența reală vine treptat cu ani de muncă, experimente, cu acumularea de diverse dezvoltări software și hardware, precum și citirea diverselor articole și documentații despre microcontrolere. Dar nu lăsați acest lucru să vă sperie, deoarece informațiile furnizate în articol sunt destul de suficiente pentru a vă crea primul dispozitiv pe un microcontroler și puteți dobândi mai multe cunoștințe și experiență pe cont propriu, dezvoltând de fiecare dată dispozitive din ce în ce mai complexe și mai bune. și îmbunătățirea abilităților dvs.

Sper că v-am putut interesa să studiați microcontrolere și să dezvolt dispozitive bazate pe acestea, iar lucrările mele vă vor fi utile și interesante.